feat(alerts): TASK 47 — alert box plays a video (built-in/custom clip) + read-time fade + message ticker
TASK 43's alert box grows a real video celebration. Per-alert IAlertClipDecoder
(ffmpeg bgra + f32le pipes, real-time paced, disposed at drain) plays the shipped
Assets/alert-default.mp4 (stamped into the Asset table at startup) unless the
creator picks their own file — path reference only, never stored in the DB; the
six AlertRenderer animations stay the fallback. ~0.3s fade rides the alpha
envelope on straight-source copies (EOF freeze-frames then fades out); audio
forwards to a new AudioMixer alert ring (8s, 48k stereo) drained at unity — no
duck, creator ruling — scaled by volume × fade. An auto-composed marquee ticker
('Funder — Super Chat · $10.00', 140px/s) scrolls top-of-frame via a
FramePump._alertTicker seam through Render/CompositeLayers, mixed into the cache
signature (dynamic overlay, never baked). New Stream Alerts section in LeftPanel.
Derivative-work references (how OBS/Streamlabs alert boxes do per-alert video):
- https://support.streamlabs.com/hc/en-us/articles/217741147-Setting-Up-Your-Streamlabs-Alerts (custom image/video per alert type + variations)
- https://obsproject.com/kb/stream-tutorial-2-alerts (alert overlay as an on-screen zone)
- https://streamlabs.com/content-hub/widgets/alert-box (per-event alert playback)
Good Dog: AlertLayerVideoTests drives a fake IAlertClipDecoder through the whole
lifecycle in one pass (custom path wins, decoder spawns/disposes, fade envelope
0→127→255, audio volume×fade, ticker scrolls, EOF fade-drain to idle). It caught
the clip branch of Advance not clearing _current before AdvanceToNext — the layer
stayed IsPlaying after drain (MyMistakes post-mortem).
Full vstest 319/319; clean build 0 warnings; scope check green.
This commit is contained in:
+56
-39
@@ -1,66 +1,83 @@
|
||||
# HANDOFF — 2026-09-25 (TASK 46 shipped + PUSHED: drawers close on any outside click, TEST included)
|
||||
# HANDOFF — 2026-09-26 (TASK 47 shipped, NOT yet pushed: alert box video)
|
||||
|
||||
## Branch / Commit State
|
||||
|
||||
`main` HEAD = new commit for TASK 46 (drawer click-outside dismiss for all three tabs);
|
||||
previous: TASK 45 commit (chat-insert 400 fix), `0981a72`. **PUSHED 2026-09-25
|
||||
(creator directive after successful testing): `origin/main` = `197ee81..ecb329e`,
|
||||
up to date, 0 ahead.**
|
||||
|
||||
**Working tree CLEAN after this unit's commit.**
|
||||
`main` — TASK 47 changes are staged-ready, **uncommitted** in the working tree (repo was clean at
|
||||
`2c9af94`, the TASK 46 handoff commit; `origin/main` = `ecb329e`). Commit + push pending the
|
||||
creator's go / milestone signal. The TASK 46 commit was pushed 2026-09-25
|
||||
(`origin/main` = `197ee81..ecb329e`).
|
||||
|
||||
## ⚠️ The unit that just shipped (creator directive)
|
||||
|
||||
"when the test slide-out is active, then any click off the div should close the div —
|
||||
this behaviour applies to all tabs, not just TEST."
|
||||
TASK 43 closed with "make the alerts animate like a video" / "I want to play my own video for
|
||||
alerts" — that follow-up is **TASK 47, done 2026-09-26**:
|
||||
|
||||
- **Root cause:** right-rail slides (Stream Settings / YPP / TEST, all inside
|
||||
`TextPullOutHost`) close on outside clicks via `MainWindow.Window_PreviewMouseLeftButtonDown`
|
||||
— but that handler only closed `BroadcastForm` + `Ypp`; **`TestSession` was never in the
|
||||
close list**, so the TEST drawer ignored outside clicks. The XAML comment even said "both".
|
||||
- **Fix (TASK 46, `TASKS/task-46-drawer-click-outside-close.md`):** add
|
||||
`TestSession.CloseDrawerCommand` to the same `!IsClickInsideDrawer` branch; corrected
|
||||
the stale "both" comment. No containment change needed — all three share the rail host.
|
||||
- **Video alerts:** the alert box plays a clip on every alert — the shipped built-in
|
||||
(`Assets/alert-default.mp4`, stamped into the `Asset` table at startup via
|
||||
`StampDefaultAlertVideo`, id tracked in the `AlertDefaultVideoAssetId` setting) unless the
|
||||
creator picks their own file (path-only reference, **never stored in the DB**; the six
|
||||
`AlertRenderer` animations remain the fallback when nothing plays). Per-alert
|
||||
`IAlertClipDecoder` (ffmpeg bgra + f32le pipes, real-time paced, disposed at drain).
|
||||
- **Read-time fade** (`FadeDurationSeconds = 0.30` in `AlertOverlayLayer`): fade-in alpha on a
|
||||
straight-alpha frame copy; EOF → freeze-frame → fade-out → drain back to idle/transparent.
|
||||
- **Message ticker top-of-frame** ("Funder — Super Chat · $10.00"): `AlertTickerRenderer`
|
||||
(marquee 140px/s), **never baked** — threaded via a `FramePump._alertTicker` seam into
|
||||
`SceneCompositor.Render/CompositeLayers` and mixed into `BuildFullRenderSignature`.
|
||||
- **Alert audio at unity, NO duck** (creator ruling): `AudioMixer.EnqueueAlertAudio` + an 8s
|
||||
stereo-48k ring drained pre-limiter; per-alert `Volume` slider instead.
|
||||
- New Stream Alerts config section in LeftPanel (built-in pill, path + Browse…, Reset, ticker
|
||||
pill, volume).
|
||||
|
||||
**Good Dog:** ONE integration test — the existing
|
||||
`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` section-2 window test was extended:
|
||||
opens TEST and Stream Settings, raises a window-root `PreviewMouseLeftButtonDown` (source =
|
||||
window ⇒ outside the rail; deterministic, no OS mouse, immune to the fullscreen mouse-steal
|
||||
flake), asserts each closes.
|
||||
**Gate: clean build 0 warnings; full vstest 318/318 passed** (the LayerReorder
|
||||
real-mouse-drag test passed this run too).
|
||||
**Good Dog test:** `ytLive.Tests/AlertLayerVideoTests.cs` (RealApp STA) — one `[Fact]` driving a
|
||||
**fake** `IAlertClipDecoder` through the whole lifecycle: custom path wins per-source, decoder
|
||||
spawns/disposes (Start=1, Dispose=1 at drain), alpha envelope 0→127→255, pre-fade audio dropped
|
||||
while ramp audio forwards at volume×fade (0.8×0.5→[0.2,−0.1]) and full-fade at volume only,
|
||||
ticker renders non-null, has pixels and **scrolls** between ticks (and null when idle), EOF
|
||||
freeze-fades to a null frame. The test caught a real bug on the first run: the clip branch of
|
||||
`Advance` never cleared `_current` before `AdvanceToNext` → the layer stayed `IsPlaying` after
|
||||
drain (see `MyMistakes.md`).
|
||||
**Gate: clean build 0 warnings; alert tests 2/2; full vstest 318/319** — the one failure
|
||||
`LayerReorderPersistenceTests.RealMouseDrag…` is the HANDOFF-documented env class (physical
|
||||
mouse drag no-ops when the desktop/session isn't interactive; this unit it failed 2× then passed
|
||||
in isolation on re-run; SourceList row geometry provably unchanged — `AlertOverlayLayer` —
|
||||
and its two sibling reorder tests, the same persistence path, pass).
|
||||
|
||||
## Follow-ups queued (NOT done in this unit)
|
||||
|
||||
- **TASK 3 item 20 persistence half** — the canonical `RewardEvents` SQLite table +
|
||||
`superChatEvents.list` (30-day) backfill + session-report rollup (events still parse to
|
||||
`ChatMessage` in memory only).
|
||||
- TASK 3 item 16 (Text source) still open; TASK 40 units A/C/D queued; TASK 32–36 queued.
|
||||
- **TASK 3 item 20 persistence half** — canonical `RewardEvents` SQLite table +
|
||||
`superChatEvents.list` (30-day) backfill + session-report rollup (still in-memory only).
|
||||
- TASK 3 item 16 (Text source); TASK 40 units A/C/D; TASK 32–36; TASK 12 master limiter — all
|
||||
queued unchanged.
|
||||
|
||||
## Around the task (carried facts)
|
||||
|
||||
- RealMouseDrag tests no-op while a game/fullscreen window steals the mouse (POE 2 seen
|
||||
2026-09-22) — **close fullscreen windows before full-suite runs**; the single test
|
||||
passes in isolation when re-run alone. AudioPipeline timing flake similar.
|
||||
2026-09-22) — **close fullscreen windows before full-suite runs**; the single test passes in
|
||||
isolation when re-run alone. AudioPipeline timing flake similar.
|
||||
- Test-env trap: saved OAuth session loads synchronously → force `vm.IsConnected` in signed-in tests.
|
||||
- `subscriberCount` YPP slice 2 needs re-consent — do not merge with other units. `$99/yr` Polar
|
||||
must become one-time before launch. `MARCOM.md`/`MONETIZATION.md` gitignored — never commit.
|
||||
- Latent bug the YPP 403 masked: statistics are JSON **strings** — always the ValueKind-first
|
||||
`ReadInt64` (MyMistakes recipe), never `GetInt64`.
|
||||
- **API facts (TASK 44 + 45):** a liveChatId fetch at broadcast-insert time (lifecycleStatus
|
||||
`ready`) returns nothing by design — missing liveChatId ≈ "broadcast not live yet", not an
|
||||
auth failure, until ~20s post push. And `400 MISSING_REQUIRED_FIELD` from
|
||||
- **API facts (TASK 44 + 45):** a liveChatId fetch before the broadcast is live returns nothing by
|
||||
design (read `snippet.liveChatId`, poll until active); `400 MISSING_REQUIRED_FIELD` from
|
||||
`liveChat/messages.insert` = wrong body shape (missing `snippet.type`), NOT auth/scope.
|
||||
- **WPF test technique (TASK 46):** a `MouseButtonEventArgs { RoutedEvent = Mouse.PreviewMouseLeftButtonDownEvent }` raised on the window root deterministically simulates an "outside the drawer rail" click — InputBindings/MouseBindings are NOT triggered by `RaiseEvent` (only by the real InputManager), so no tab toggles fire.
|
||||
- Every committed change needs a **close + relaunch** of the running app to be seen — running
|
||||
builds are live; verify can't run while the exe is locked.
|
||||
- **WPF test technique (TASK 46):** a `MouseButtonEventArgs { RoutedEvent =
|
||||
Mouse.PreviewMouseLeftButtonDownEvent }` raised on the window root deterministically simulates an
|
||||
"outside the drawer rail" click — InputBindings are NOT triggered by `RaiseEvent`.
|
||||
- **TASK 47 sticky facts:** bgra from ffmpeg is opaque (alpha=255); audio pipe = carry-buffer loop
|
||||
(PCM bytes → float chunks straddle pipe reads); default clip lives at `%TEMP%\ytLive-alert-{id}.mp4`
|
||||
when materialized; decoder per-play, never the refcounted `MediaVideoSourceManager`. Recipe in
|
||||
`MyMistakes.md`. DB stays `user_version 10` — TASK 47's Source columns are additive guarded ALTERs.
|
||||
- Every committed change needs a **close + relaunch** of the running app to be seen.
|
||||
|
||||
## Next step
|
||||
|
||||
1. Nothing is in-flight to verify on the drawer path (creator confirmed the behavior
|
||||
on the last run).
|
||||
2. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next.
|
||||
3. `origin/main` is current (`ecb329e`); future commitments push at the next creator-approved milestone.
|
||||
1. Run `scripts/scope-check.sh` with the declared file list (TASK 47), final clean build (verify,
|
||||
0 warnings, `"/mnt/c/Program Files/dotnet/dotnet.exe" build ytLive.csproj`) + full vstest.
|
||||
2. Single commit (code + this unit's docs: `ai.md`, `TASKS.md`, `TASKS/task-47-alert-videos.md`,
|
||||
`Services/index.md`, `HANDOFF.md`, `MyMistakes.md`). Push at the creator's go/milestone.
|
||||
3. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next.
|
||||
|
||||
## Critical working rules (unchanged, still binding)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user