feat(alerts): add scene-local recent ticker
Model the AlertBox and standalone ticker with shared settings; scene tickers render the in-memory active and queued events without saving history. Keep AlertBox content active-event-only. Ref: https://obsproject.com/kb/obs-studio-overview
This commit is contained in:
+21
-11
@@ -98,16 +98,24 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`):
|
||||
EOF → freeze-frame → fade-out → drain (the clip branch of `Advance` **clears
|
||||
`_current` before `AdvanceToNext`** — the miss that the TASK 47 test caught). Audio
|
||||
forwards to the mixer sink scaled by `volume × fade`.
|
||||
- **Alert ticker (announcement strip):** `AlertTickerFrame` (in the layer) composes
|
||||
- **Ticker settings and alert feed:** `Models/Ticker.cs` is shared by the
|
||||
AlertBox and standalone `SourceType.Ticker` layers. Each source owns a distinct
|
||||
settings instance (`IsEnabled` + `DisplayMethod`); the existing `AlertShowTicker`
|
||||
and `AlertDisplayMethod` columns persist those settings on their own Source row,
|
||||
so this is additive without a schema migration. The standalone ticker can be
|
||||
added once in each scene and shows the in-memory active event plus queued events.
|
||||
Completed events are not retained as history. The AlertBox ticker uses the same
|
||||
renderer/settings shape but shows only the active event.
|
||||
- **AlertBox ticker (announcement strip):** `AlertBoxTickerFrame` composes
|
||||
"Author — Kind · amount" and hands it to `Services/Compositor/AlertTickerRenderer.cs` —
|
||||
a strip rasterized **on the UI thread once per text** (cached, unpremultiplied
|
||||
straight-alpha pill) then positioned by pure byte-math per call, so the pump calls it
|
||||
from its own thread with no locking. **It is a dynamic overlay, NEVER baked** (the
|
||||
social bar IS baked into `BakeStaticBase`): `SceneCompositor.Render` +
|
||||
`CompositeLayers` take a trailing `tickerFrame` threaded from
|
||||
`FramePump._alertTicker` (`Func<VideoFrame?>` ctor seam) through
|
||||
`FramePump._alertTicker` (`Func<Scene, VideoFrame?>` ctor seam) through
|
||||
`RenderScene`/`RenderFull` and **mixed into `BuildFullRenderSignature`** so a scroll
|
||||
changes the cache key. Toggled by `AlertShowTicker`.
|
||||
changes the cache key. It reads the active scene's AlertBox settings.
|
||||
- **It draws INSIDE the Stream Alerts box (creator ruling 2026-09-26).** Was a global
|
||||
1920px bar pinned to the top edge: *"the ticker should appear over the stream alerts
|
||||
video, not over the entire preview window"*. The strip is now rendered at the
|
||||
@@ -135,14 +143,16 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`):
|
||||
old fixed `140px/s` took ~17s per pass, so a 10s alert showed the message once.
|
||||
Same convention as the incumbent (Streamlabs: "Alert Duration" + "Text Delay", not a
|
||||
scroll-speed slider). Flash's off half returns null.
|
||||
- **A ticker still needs its own preview element, not a `Source`.** It is a frame-pump
|
||||
producer, so it never rides a per-element `Image`; `AlertOverlayLayer` takes a
|
||||
`tickerPreviewSink` and publishes from `RefreshAlertPreviews()` (UI thread only —
|
||||
the pump thread must never touch a `WriteableBitmap`); the VM writes it into one
|
||||
reused `WriteableBitmap` (`AlertTickerImageSource`/`AlertTickerVisible`) bound to
|
||||
`AlertTickerElement` in `PreviewPane.xaml`, mirroring `SocialBarElement`. The rect
|
||||
is re-raised on **every** published frame, not just on a bitmap resize, so a box
|
||||
moved or resized mid-alert tracks live.
|
||||
- **AlertBox preview:** `tickerPreviewSink` publishes from
|
||||
`RefreshAlertPreviews()` (UI thread only — the pump thread must never touch a
|
||||
`WriteableBitmap`); the VM writes the box-sized frame into the reused bitmap bound
|
||||
to `AlertTickerElement` in `PreviewPane.xaml`. The rect is re-raised on every
|
||||
published frame so a box moved or resized mid-alert tracks live.
|
||||
- **Standalone ticker preview/output:** it is an ordinary dynamic Source with a
|
||||
per-element frame. `AlertOverlayLayer` updates each scene ticker's reusable
|
||||
`VideoImageSource` on its UI timer for preview; the output resolver renders the
|
||||
same source with that source's Ticker settings. It is not the AlertBox global
|
||||
ticker callback, so each scene's layer and settings remain independent.
|
||||
- **Two measurement traps here:** a seamless marquee never goes blank (a wrapped copy
|
||||
enters as the pill clears), so count a pass by the pill's leading edge resetting, not
|
||||
by an empty frame; and the run is phase-started half a frame in, or the first
|
||||
|
||||
Reference in New Issue
Block a user