TASK 47 take four: alert ticker visible in the preview + 3 display methods
The creator confirmed the clip fix ("the video plays now"), then asked where the
scrolling text was — it was invisible, for a structural reason. AlertTickerFrame
existed only as a frame-pump callback blitted into the OUTPUT; PreviewPane.xaml had
no element for it, because the strip is master-width and global, not a Source, so it
cannot ride a per-element Image. Nothing was wrong in the renderer: there was no
consumer in the preview. Same class of defect as the missing IsAlertBox trigger, one
layer up (MyMistakes RULE 5/6).
- tickerPreviewSink on AlertOverlayLayer, published from RefreshAlertPreviews() so it
is always the UI thread; MainViewModel.AlertTicker writes it into one reused
WriteableBitmap bound to a new global AlertTickerElement, mirroring SocialBarElement.
- Source.AlertDisplayMethod + panel "Display" selector: TickerScroll / Flash / Solid.
Flash pulses 0.5s on / 0.5s off for the whole alert; Solid is centred and still.
- The marquee was also unreadable: a fixed 140px/s took ~17s per pass, so a 10s alert
showed the text once, entering from the right and never crossing. Paced in reads per
alert instead (TickerReadsPerAlert = 3 inside the alert's own length, speed derived
from it) — never px/s. Research (websearch: how do OBS/Streamlabs/StreamElements
alert boxes present announcement timing?) settled the unit: Streamlabs exposes "Alert
Duration: choose how long your alert stays on your stream" and "Text Delay", never a
scroll-speed slider (https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts).
Run is phase-started half a frame in so the first frame isn't blank.
- Persistence: AlertDisplayMethod INTEGER NOT NULL DEFAULT 0 via the idempotent
table_info migration, appended LAST in the SELECT because the Source reader is
positional (GetInt32(32..34)) — a mid-list insert would silently shift a neighbour.
Tests: 15 new facts (suite 339/339). RealApp STA host: the pane draws the strip and
collapses at alert end; the layer publishes a real 1920x48 frame for all three methods
and nothing when the ticker is off; three passes counted in 10s by the pill's leading
edge resetting (a seamless marquee never blanks, so an empty frame cannot count a pass);
Solid byte-identical at every moment; Flash on for half of each second; the panel shows
and writes back the choice; the DB round-trips all three alert fields together.
Incidental finding: a bound ItemsSource ComboBox in LeftPanel.xaml broke
LayerReorderPersistenceTests.RealMouseDrag (that test injects PHYSICAL mouse input, so a
load-time re-measure moves the rows out from under the cursor). Rewritten as inline
ComboBoxItems, the shape the chat Font selector already uses in that panel. Recorded as
MyMistakes RULE (8).
This commit is contained in:
@@ -63,11 +63,21 @@
|
||||
same `FadeDurationSeconds = 0.30` envelope → drain (StopClip + dispose, idle null
|
||||
again). Ticker: `AlertTickerFrame` composes "Author — Kind · amount" from the live
|
||||
message and scrolls it.
|
||||
- **`Services/Compositor/AlertTickerRenderer.cs`** — marquee strip: 1920×48 transparent
|
||||
- **`Services/Compositor/AlertTickerRenderer.cs`** — announcement strip: 1920×48 transparent
|
||||
BGRA, text-pill rasterized once per text on the UI thread (cached, unpremultiplied),
|
||||
then pure byte-math per call (scroll `pos = elapsed × 140px/s % cycle`, pill drawn
|
||||
straight-alpha over transparent with a repeat-gap copy), so the pump can call it from
|
||||
its own thread every tick with no locking.
|
||||
then pure byte-math per call, so the pump can call it from its own thread every tick
|
||||
with no locking. Three display methods (`Source.AlertDisplayMethod`, creator-set in the
|
||||
panel):
|
||||
- `TickerScroll` — marquee. Paced in **reads per alert**, not px/s: one pass takes
|
||||
`alertSeconds / TickerReadsPerAlert(3)`, and the speed is derived from that, so the
|
||||
10s default clip carries the announcement across three times. The old fixed
|
||||
`140px/s` needed ~17s per pass, i.e. barely one sighting in a 10s alert.
|
||||
- `Flash` — the static pill blinks `0.5s` on / `0.5s` off (1Hz) for the whole alert;
|
||||
the off half returns null so the preview collapses too.
|
||||
- `Solid` — the static pill centred and still for the whole alert.
|
||||
The run is phase-started half a frame in so the first published frame isn't blank,
|
||||
and the wrap copies in from the right the instant the pill clears the left (a
|
||||
seamless loop — the frame never goes empty).
|
||||
- **Ticker threading** — dynamic overlay like the social bar, but **never baked**
|
||||
(the social bar IS baked into `BakeStaticBase`; the ticker is a per-frame marquee):
|
||||
`SceneCompositor.Render` + `CompositeLayers` take a trailing `tickerFrame` param
|
||||
@@ -81,7 +91,11 @@
|
||||
it. The layer applies `volume × fade` per sample before forwarding.
|
||||
- **UI — Stream Alerts section** (LeftPanel, visible only on an `AlertBox`):
|
||||
"Built-in video" pill; custom path box + Browse…; "Reset to built-in"; "Message
|
||||
ticker" pill; "Volume" slider with % readout. Wiring: `BrowseAlertVideoCommand` +
|
||||
ticker" pill; **"Display" selector (Scroll / Flash / Solid)**; "Volume" slider with %
|
||||
readout. The selector is inline `<ComboBoxItem>`s bound by `SelectedValuePath="Tag"`,
|
||||
the same shape as the chat Font selector in this panel — NOT a bound `ItemsSource`
|
||||
(see RULE (8) in `MyMistakes.md`: a data-bound selection re-measures the panel and
|
||||
breaks the real-mouse layer-drag test). Wiring: `BrowseAlertVideoCommand` +
|
||||
`ResetAlertVideoCommand` (CanExecute gates on `Source { Type: AlertBox }`); browse =
|
||||
OpenFileDialog (mp4/mov/webm), result writes `AlertVideoPath` + switches the pill off.
|
||||
|
||||
@@ -255,3 +269,43 @@ Two Good Dog tests, because the fakes had been hiding this:
|
||||
the fix, green after.
|
||||
|
||||
Full suite 324/324.
|
||||
|
||||
## Take four (2026-09-26) — the ticker was output-only, and unreadably paced
|
||||
|
||||
The creator confirmed the clip fix ("the video plays now"), then asked two things:
|
||||
**"where is the scrolling text? I do not see it"** and whether ~3 reads inside the
|
||||
10-second default alert would be readable. Answer: yes, three is right — and the text
|
||||
was invisible for a structural reason, not a rendering fault.
|
||||
|
||||
**Bug 1 — the strip had no consumer in the preview.** `AlertTickerFrame` existed only as
|
||||
a frame-pump callback, blitted into the OUTPUT at (0,0). `PreviewPane.xaml` had no
|
||||
element for it because the strip is master-width and global, not a `Source`, so it
|
||||
cannot ride a per-element `Image` (the trap RULE (5) records, one layer up — RULE (6)).
|
||||
Fix: `tickerPreviewSink` on the ctor + `AlertOverlayLayer` (UI-thread `DispatcherTimer`
|
||||
only, never the pump thread), the layer publishes from `RefreshAlertPreviews()`, and
|
||||
`MainViewModel` writes it into one reused `WriteableBitmap`
|
||||
(`AlertTickerImageSource` / `AlertTickerVisible`) bound to a new global
|
||||
`AlertTickerElement` in `PreviewPane.xaml`, mirroring `SocialBarElement`.
|
||||
|
||||
**Bug 2 — a fixed px/s cannot express "three reads".** See the renderer note above.
|
||||
Incumbent convention agrees: Streamlabs exposes *Alert Duration* ("choose how long your
|
||||
alert stays on your stream") and *Text Delay*, never a scroll-speed slider
|
||||
(https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts),
|
||||
so the unit is repeats-per-alert, not pixels-per-second.
|
||||
|
||||
**Persistence** — `Source.AlertDisplayMethod INTEGER NOT NULL DEFAULT 0` via the
|
||||
idempotent `PRAGMA table_info` migration (no `user_version` bump: the guard is the
|
||||
column's presence). The column is appended **last** in the `SELECT` on purpose — the
|
||||
reader is POSITIONAL (`GetInt32(32..34)`), so inserting anywhere but the end would
|
||||
silently shift a neighbour rather than fail to load.
|
||||
|
||||
**Tests** (`ytLive.Tests/AlertTickerPreviewDisplayTests.cs`, RealApp STA host):
|
||||
the pane draws the strip and collapses when the alert ends; the layer publishes a real
|
||||
1920x48 frame for all three methods (and nothing at all when the ticker is off); three
|
||||
passes are counted in 10s by the pill's leading edge resetting (a seamless marquee never
|
||||
blanks, so "blank frame" cannot count a pass); Solid is byte-identical at every moment;
|
||||
Flash is on for half of each second; the panel shows and writes back the chosen method.
|
||||
`LayoutStorePersistenceTests.AlertDisplayMethod_RoundTrips` guards the positional-reader
|
||||
risk by asserting all three alert fields together.
|
||||
|
||||
Full suite 339/339.
|
||||
|
||||
Reference in New Issue
Block a user