Files
LlamaCasty/HANDOFF.md
T
gramps a11b15e444 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.
2026-09-26 11:15:17 -07:00

5.9 KiB
Raw Blame History

HANDOFF — 2026-09-26 (TASK 47 shipped, NOT yet pushed: alert box video)

Branch / Commit State

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)

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:

  • 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 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 — 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.
  • 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 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 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. 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)

  • Good Dog = ONE integration test per change. Scope lock + ./scripts/scope-check.sh before commit.
  • Windows dotnet host for all WSL builds (/mnt/c/Program Files/dotnet/dotnet.exe, quoted paths).
  • 0 warnings on real clean builds (verify only, never incremental). One runtime model.
  • No menus/polls with the creator; decisions asked once then held; do the work, then report.