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

87 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.