feat(alerts): TASK 43 — native events & alerts (six unique animations) + chat parity
Replace the external StreamElements feed with NATIVE YouTube events per the creator's 2026-09-23 question. Chat parity half: - YouTubeChatService decodes ALL SIX liveChat/messages event types into ChatMessage.Kind (superChat/superSticker/newSponsor/membershipGifting/ giftMembershipReceived/memberMilestoneChat) — the four formerly-empty overlay rows are real now — and re-arms its one-shot poll on the server's pollingIntervalMillis (streamList connection semantics; clamp 1000-6000ms, maxResults=2000). ParsePage internal static seam + ChatPage record for deterministic tests; optional HttpClient ctor seam kept; InjectSimulatedMessage (TASK 41) preserved. Alerts half (creator rulings: one celebration zone, six UNIQUE animations, no menus/polls, sub mention = chat row only, NO viewer count): - New SourceType.AlertBox 'Stream Alerts' (one per layout, CanAddAlerts gate mirroring chat; idle = transparent). - AlertRenderer: six distinct branded animations (SuperChat slide-up/shine/ count-up, SuperSticker scale-pop, NewMember drop-in/flash, MemberGift slide-left/ chip-fan, GiftReceived confetti, MemberMilestone rise/growth-bar), every card drawing the 'made with LlamaCasty!' brand line. - AlertOverlayLayer: true component (Commit-G pattern) — queue (cap 10, drop tail never stall), 33ms UI ticker, cache-first RenderFrame + UpdatePreview, Advance(double) as the deterministic test clock; ChatEventKind.None rows never enqueue. - Wired: resolver RenderAlertBox, _alertLayer ctor + dispose, LoadLayout previews, Add menu item (Controls/LeftPanel.xaml), TestSessionViewModel sims tagged (member->NewMember, superchat->SuperChat). Good Dog ONE integration test: AlertLayerTests (RealApp STA, real WPF raster) — ParsePage classifies all six kinds + cadence fields; None rows enqueue nothing; six events play pairwise-distinct moving frames then drain to null. Gate: clean build 0 warnings; full suite 316/317, the one failure (LayerReorderPersistenceTests.RealMouseDrag) repros on the clean tree — the known environmental class (real-mouse-drag no-ops with a game/fullscreen window focused). References (OBS/overlay ecosystem): - streamList semantics: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList - OBS alert-box pattern (designated celebration zone, idle transparent): creator-chosen model
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
**Goal:** Real video preview in the center panel — the minimal source set below, composited per scene.
|
||||
|
||||
### Status: 🔶 In progress — 27 of 30 items done (1 excluded: background removal); items 16, 17, 20 open
|
||||
### Status: 🔶 In progress — **Alerts (item 20) SHIPPED 2026-09-24 via TASK 43**; still open: item 17 (Text source), item 20's `RewardEvents` SQLite persistence half (16 compositing + 19 background removal excluded)
|
||||
|
||||
1. ✅ **Milestone 1 — webcam** — MediaCapture (WinRT SDK projection) with device enumeration, CPU-first frame source, refcounted `CameraManager`, picker dialog, clip shapes (Traditional + Round) + mirror, 480×270 default placement — schema v2
|
||||
2. ✅ **Schema v3 (Ship Branch A)** — multi-scene webcam (singleton `Webcam` + per-scene `WebcamSceneConfig`), right-click border/context menu, static OBS-style borders, 50%-per-dimension webcam size cap, device-swap (`ReleaseAllAsync`) — 25 tests passing
|
||||
@@ -26,7 +26,19 @@
|
||||
17. ☐ **Text source** — live text ("Starting soon", "Back in 5", handle, callout)
|
||||
18. ✅ **Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video — `ChatBoxRenderer` (WPF FormattedText → VideoFrame), configurable font size/color/badges/timestamps/max-messages via Elements panel, schema v10, persisted across save/load
|
||||
19. ❌ **Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build
|
||||
20. ☐ **Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; **free and ungated** (the stale "one paid feature" label predates the 2026-08-31 monetization reversal and is corrected 2026-09-01 — the paid swap is the branding flash ONLY; see Monetization in `ai.md`). **v1 IN** per the complete-v1 ruling. **Precursor — reward-event capture (2026-09-01):** Alerts render from the app's canonical `RewardEvent` feed (all 7 `liveChatMessage` reward types, persisted to SQLite + the session report — see TASK 10 → "Related work — monetization awareness"), so it consumes that already-built data rather than re-integrating `liveChatMessages`. That capture is the required prior milestone.
|
||||
20. ✅ **Alerts — Super Chat / membership pop-ins (SHIPPED 2026-09-24 via TASK 43, NATIVE)** —
|
||||
free and ungated (see Monetization in `ai.md`). **v1 IN** per the complete-v1 ruling. The
|
||||
deliverable is now **native events + native animations, no third-party widget**: the chat feed
|
||||
(`liveChat/messages`, six types decoded — TASK 43 half 1) drives a `SourceType.AlertBox`
|
||||
("Stream Alerts", one celebration zone per layout) where each event plays its **own unique
|
||||
branded animation** (`AlertRenderer`/`AlertOverlayLayer`, 33ms ticker, `Advance` test clock).
|
||||
**Creator rulings:** a free-subscriber mention is a plain chat row — NO sub alert (YouTube
|
||||
emits no public event for free subs), NO viewer count (vetoed), and every card draws the
|
||||
"made with LlamaCasty!" brand line (each broadcast = free product placement). **Precursor
|
||||
(still open): the reward-event persistence half — the canonical `RewardEvents` SQLite table
|
||||
(all six types persisted per broadcast) + `superChatEvents.list` backfill + the session-report
|
||||
rollup (TASK 10 → "Related work — monetization awareness"). The shipped alert path parses
|
||||
events into `ChatMessage` directly in memory; the report reads that table once it exists.**
|
||||
21. ✅ **Show Desktop toggle** — `Source.ShowDesktop` bool persisted in DB (schema migration + LayoutStore read/write). When checked, primary monitor captures regardless of fullscreen game state. Toggling off falls back to running game or releases to static placeholder. `ClearBackdropCaptureAsync` clears `CaptureKey` + `VideoImageSource` so static fallback renders. Toggle on Live backdrop context menu (IsCheckable MenuItem).
|
||||
22. ✅ **Top bar — status light + avatar + Log In button** — red/green/pulsing ellipse (disconnected/connected/live). Avatar border visible only when connected, loads via `BitmapImage` code-behind. "Log In" button visible when disconnected, calls `StartStreamCommand` → GoLiveWindow. `ShowStartStream` now requires `IsConnected`.
|
||||
23. ✅ **Chat preview — one-at-a-time mock messages** — `RunMockChatPreviewAsync`: simple async loop, displays `MockChatMessages[n]` via `Take(n+1)`, 1000ms between messages, wraps at end. `ChatPreviewEnabled` property on Source (default true). Stops on real messages, restarts on fade timer clear.
|
||||
@@ -53,10 +65,12 @@ the hot few and nothing esoteric. If a user needs more, they've graduated to OBS
|
||||
4. **Image** — a floating graphic/logo overlay (watermark, badge, corner branding). Free-positioned.
|
||||
5. **Text** — live text ("Starting soon", "Back in 5", handle, callout). Casual streamers live on this.
|
||||
6. **Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video. YT-native.
|
||||
7. **Alerts** — Super Chat / membership / subscribe pop-ins. The dopamine source. **The one big lift**
|
||||
(Super Chat event streaming + on-stream rendering/animation); build after the six. **Free and
|
||||
ungated** — paid = flash removal only (stale "one paid feature" corrected 2026-09-01). Consumes
|
||||
the canonical `RewardEvent` feed (reward-event capture, TASK 10 → related work).
|
||||
7. **Alerts** — Super Chat / membership pop-ins. The dopamine source. **The one big lift — DONE
|
||||
(2026-09-24, TASK 43): native events + native animations.** One `SourceType.AlertBox`
|
||||
celebration zone; six event types = six unique branded animations; free-sub mention = chat row
|
||||
only; no viewer count. **Free and ungated** — paid = flash removal only (corrected 2026-09-01).
|
||||
Pending: the `RewardEvents` SQLite persistence half (item 20; the report + journey read it,
|
||||
TASK 10 → related work).
|
||||
|
||||
Deliberately NOT supported: game capture, browser source, media playlist, VLC, color-key voodoo, MIDI.
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# TASK 43 — Native events & alerts (the "go" unit: chat parity + six animation alert box)
|
||||
|
||||
> Catalog: [`TASKS.md`](../TASKS.md). Status: ✅ **SHIPPED 2026-09-24** — creator said
|
||||
> **"go"** on the combined work unit (native-chat parity + native alert box). See
|
||||
> [`ai.md`](../ai.md) → "Native events & alerts" for the architecture.
|
||||
|
||||
## Provenance (recorded per the feature-provenance rule)
|
||||
|
||||
- **2026-09-23, the creator asked** — "Could you use native YouTube events instead of
|
||||
StreamElements?" (the app was designed to feed an external StreamElements alert box).
|
||||
Research (websearch + the liveChat/messages + streamList docs, URLs in the commit
|
||||
message) established the API facts below; the creator then ruled on product shape.
|
||||
- **Decisions the creator made in conversation (asked ONCE, then held):**
|
||||
1. **No menus / no polls** — "I've asked you before to not use menus/polls." Decisions
|
||||
are asked on the open floor once and then executed.
|
||||
2. **Events = ONE animated alert box** (OBS alert-box pattern), a designated area on
|
||||
the canvas where celebration animations play. NOT a list pane, NOT per-source.
|
||||
3. **Six monetization events → SIX UNIQUE animations** — "not cookie-cutter": each of
|
||||
Super Chat, Super Sticker, New Member (newSponsor), Member Gift, Gift Received,
|
||||
Member Milestone gets its own distinct gesture, palette and layout. Each animation
|
||||
is a free branded product-placement ad in the stream (the brand line is drawn into
|
||||
every card) and a celebration of the supporter's investment.
|
||||
4. **Free subscribers: NO alert** — "subscriber mention in chat is sufficient."
|
||||
YouTube exposes no public event for free subs (API fact); a mention arrives as a
|
||||
normal chat row and is deliberately `ChatEventKind.None` → the alert engine never
|
||||
fires. The app keeps its opinion: none. (The third-party socket + delta-counter
|
||||
folklore was explicitly rejected with it.)
|
||||
5. **Viewer count: dropped entirely** — demotivating and weaponizable against the
|
||||
creator; not even hidden.
|
||||
6. **Native chat replaces the SE URI** — the chat box already exists in-app
|
||||
(`SourceType.ChatBox`, fed by real `liveChat/messages` polling). The remaining
|
||||
parity gaps closed here: the four previously-undecoded event types displayed as
|
||||
**empty rows** in the overlay, and the poll cadence was a hardcoded 2s instead of
|
||||
the server's `pollingIntervalMillis`.
|
||||
|
||||
## API facts (researched + cited in commit)
|
||||
|
||||
- Native event feed = the **same** `liveChat/messages` endpoint already polled.
|
||||
`streamList` is a *connection semantics* description of that endpoint, not a separate
|
||||
call: response carries `pollingIntervalMillis`; reconnect with `nextPageToken` on that
|
||||
cadence, never faster (YouTube rejects rapid polling). The code now re-arms the
|
||||
one-shot poll on the server's number (clamped 1000–6000ms), and `maxResults=2000`.
|
||||
Cite: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList
|
||||
- Six `snippet.type` event kinds exist: `superChatEvent`, `superStickerEvent`,
|
||||
`newSponsorEvent`, `membershipGiftingEvent`, `giftMembershipReceivedEvent`,
|
||||
`memberMilestoneChatEvent`. Free subscriptions emit **nothing** — no API event, no
|
||||
delta, no count (the "newSponsorEvent counts" folklore is wrong for public subs).
|
||||
- `videos.list → liveStreamingDetails.concurrentViewers` exists but was **vetoed** by
|
||||
the creator (decision 4 above).
|
||||
|
||||
## Product model (as shipped)
|
||||
|
||||
- **`SourceType.AlertBox`** ("Stream Alerts") — a one-per-layout celebration zone,
|
||||
mirroring the chat layer's gate (`CanAddAlerts` = no AlertBox exists in any scene;
|
||||
`AddSource` refuses a second). Defaults 620×430 → 680×200. Draggable, previews like a
|
||||
source, idle = fully transparent (nothing on stream until an event actually fires).
|
||||
- **Playback:** events from the chat feed enqueue one-at-a-time on a 33ms UI
|
||||
dispatcher tick, play their own animation (4–5s each), then drain; back-to-back floods
|
||||
cap the queue at 10 and drop the tail, never stalling playback. The clock is exposed
|
||||
as `Advance(double)` so tests drive playback deterministically with no timer.
|
||||
- **The six animations** (`Services/AlertRenderer.cs`), phases enter[0–0.16] /
|
||||
hold[0.16–0.8] / exit[0.8–1]:
|
||||
| Kind | Gesture | Palette |
|
||||
|---|---|---|
|
||||
| SuperChat | slide-up + gold amount count-up + shine sweep | green `#0d3b2b→#14532d` |
|
||||
| SuperSticker | scale-pop overshoot, tilted sticker chip, bob | purple `#4a044e→#7e22ce` |
|
||||
| NewMember | drop-in + white flash band + gold level chip | violet `#2e1065→#5b21b6` |
|
||||
| MemberGift | slide-left + gift-chip fan `×N` | amber `#422006→#a16207` |
|
||||
| GiftReceived | confetti burst scale-in | green→purple `#14532d→#4c1d95` |
|
||||
| MemberMilestone | rise-up + tenure growth bar | orange `#431407→#c2410c` |
|
||||
- Every card draws **"made with LlamaCasty!"** bottom-right — each broadcast of an event
|
||||
is in-video product placement (creator decision: "it's a free ad each time").
|
||||
|
||||
## Chat parity (half 1 of the unit)
|
||||
|
||||
- `YouTubeChatService` rewritten around the real cadence + all six event parses:
|
||||
`ParsePage(string)` (internal static seam) → `ChatPage(Messages, NextPageToken,
|
||||
PollIntervalMs)`; fields landed on `ChatMessage`: `Kind` (`ChatEventKind`),
|
||||
`SuperChatDisplayString`, `SuperStickerDescription`, `GiftNewMembersCount`,
|
||||
`GifterName`, `MilestoneMonths` (best-effort `(\d+) months` regex from the milestone
|
||||
chat text). The four formerly-empty event types now parse as real rows AND drive
|
||||
animations. `InjectSimulatedMessage` (TASK 41) preserved.
|
||||
|
||||
## Test (Good Dog — ONE integration test per change)
|
||||
|
||||
`ytLive.Tests/AlertLayerTests.cs` (RealApp STA host, real WPF raster) — `AlertLayerTests`:
|
||||
1. **Parse:** a mocked 7-item feed → all six kinds classified, `None` for plain chat,
|
||||
`pollingIntervalMillis`/`nextPageToken` honored, fields verified (incl. the
|
||||
formerly-empty `3 memberships` / `gifted you a membership` rows).
|
||||
2. **Playback:** subscriber-mention (`None`) enqueues nothing; the six events play
|
||||
through the queue with non-null, non-blank, *moving* frames; every event's mid-hold
|
||||
frame is **pairwise distinct from every other** (six unique animations, pinned);
|
||||
playback drains to transparent/null.
|
||||
|
||||
## Files (declared scope)
|
||||
|
||||
- `Models/YouTube.cs` (ChatEventKind + fields), `Models/Source.cs` (SourceType.AlertBox
|
||||
+ IsDraggable/Kind/DisplaySource)
|
||||
- `Services/YouTubeChatService.cs` (cadence + six-event ParsePage seam)
|
||||
- `Services/TestSessionViewModel.cs` (sims tagged: member→NewMember, superchat→SuperChat)
|
||||
- **NEW** `Services/AlertRenderer.cs`, `Services/AlertOverlayLayer.cs`
|
||||
- `ViewModels/MainViewModel.cs`, `ViewModels/MainViewModel.Chat.cs`,
|
||||
`ViewModels/MainViewModel.Sources.cs` (wiring: `_alertLayer`, render resolver,
|
||||
`CanAddAlerts`, menu name, shakedown `LoadLayout`, dispose)
|
||||
- `Controls/LeftPanel.xaml` (Add → Stream Alerts menu item)
|
||||
- **NEW** `ytLive.Tests/AlertLayerTests.cs`
|
||||
- Docs: `ai.md`, `TASKS.md`, this file, `Services/index.md`, `ViewModels/index.md`,
|
||||
`Models/index.md`, `TASKS/task-03-capture-pipeline.md`, `HANDOFF.md`
|
||||
|
||||
## Gate
|
||||
|
||||
Clean build 0 warnings (Windows dotnet host); full suite **316/317** — the one failure
|
||||
`LayerReorderPersistenceTests.RealMouseDrag…` repros on the CLEAN tree (proved via
|
||||
`git stash`; it is a real-mouse-drag test that no-ops when a game/fullscreen window
|
||||
steals the mouse — HANDOFF-documented environmental class). Scope check green.
|
||||
|
||||
## Open follow-ups (NOT this unit)
|
||||
|
||||
- TASK 3 item 17 (Alerts) is DONE via native events; **item 20 stays open** — the
|
||||
RewardEvent SQLite persistence + session-report rollup + `superChatEvents.list`
|
||||
backfill were scoped as the capture foundation and remain queued (the shipped alert
|
||||
path parses events directly into `ChatMessage`, which is what the report reads).
|
||||
- TASK 3 item 16 (Text source) still open.
|
||||
- Creator report still open: "I still cannot post a chat message in the TEST tab" —
|
||||
not yet diagnosed; the TEST drawer's Mock Chat Input posts via a real insert.
|
||||
Reference in New Issue
Block a user