feat(stream): TASK 41 Test Stream mode — private test broadcast + TEST drawer
Test button next to Start runs the real private-only go-live pipeline as a session variant (IsTestStream): BeginTestStream -> extracted StartStreamingSession(alsoRecord:false) shared with the dialog path — no Go Live dialog, no 'last live' stamp, no recording. Gold top bar + TEST badge + 'End Test' button face; TEST drawer (third right-rail pull-out, three-way exclusivity) auto-opens once the liveChatId resolves. Chat tooling: Mock Chat Input sends a REAL liveChat/messages.insert (YouTubeStreamService.InsertChatMessageAsync) that round-trips the real ~2s poll and renders through the live overlay; simulated Subscriber/New Member/Super Chat events inject through the poller's MessageReceived seam (YouTubeChatService.InjectSimulatedMessage, ChatMessage.IsSimulated) — the insert API only creates textMessageEvent, so non-text events are local-only by design (ref: developers.google.com/youtube/v3/live/docs/liveChatMessages/insert). Scopes youtube + youtube.force-ssl already cover insert; no re-consent. Good Dog: ytLive.Tests/TestStreamTests.cs — coordinator protocol with fake HTTP + real-window drawer exclusivity/gate. 0 warnings; 314/314 pass (clean run; intermittent host abort is the pre-existing WinRT-webcam flake). Docs ride in the same change: TASKS.md row 41, TASKS/task-41-test-stream-mode.md, ai.md (YouTube Live API constraints), HANDOFF rewrite.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# TASK 41 — Test Stream mode: private test broadcast + TEST drawer
|
||||
|
||||
**Status:** ✅ Done (2026-09-22) — one scope-checked work unit, one Good Dog integration test,
|
||||
314/314 suite green, 0 warnings.
|
||||
|
||||
## Creator ask (2026-09-22)
|
||||
|
||||
"YouTube Test Stream & Chat Integration": a **Test/Preview** button beside Start that runs a
|
||||
private broadcast via the API, monitors stream/ingest, activates the chat endpoint (liveChatId),
|
||||
and provides a **chat simulator**: mock chat input (`liveChatMessages.insert`) + a toggle to
|
||||
inject mock chat, with alert/event triggers for subscriber, membership, and Super Chat.
|
||||
|
||||
## Locked decisions (user answers, 2026-09-22)
|
||||
|
||||
- **Chat-overlay events only** — simulated events validate the existing chat overlay styling
|
||||
(text / member `memberColor` / SuperChat `superChatColor` rows). Dedicated stinger/TTS alert
|
||||
widgets are NOT part of this task — they stay queued under **TASK 3 items 16/17/20**.
|
||||
- **TEST drawer in the right rail** — third white tab below YPP, three-way one-open-at-a-time
|
||||
exclusivity, auto-opens when a test broadcast boots.
|
||||
- **Hybrid simulation** — text = REAL `liveChat/messages.insert` (round-trips the real 2s poll
|
||||
and renders through the live overlay path); member/SuperChat/subscriber = **local injection** at
|
||||
the poller's `MessageReceived` seam (marked `IsSimulated`). YouTube's insert API only creates
|
||||
`textMessageEvent`s — non-text events are local-only by design.
|
||||
|
||||
## Facts the build sits on (verified in code, 2026-09-22 research pass)
|
||||
|
||||
- **The app is already private-only go-live** — `YouTubeStreamService.CreateBroadcast` hardcodes
|
||||
`privacyStatus="private"` + `enableAutoStart/Stop` + `low latency`, no monitor stream
|
||||
(`enableMonitorStream=false`), and the Go Live dialog is locked to Private. "Test" is therefore a
|
||||
**session variant over the real pipeline**, not a new broadcast mode.
|
||||
- The **liveChatId flow already exists**: `GetBroadcastLiveChatIdAsync` → `_youtubeChat.Start(liveChatId)`
|
||||
in `PrepareAndStartLiveAsync` — the "chat endpoint activation" the ask describes.
|
||||
- OAuth scope already includes `youtube` + `youtube.force-ssl` (`YouTubeAuthService.cs:18`) —
|
||||
enough for `liveChat/messages.insert`; no re-consent.
|
||||
- Chat = **polling** (`YouTubeChatService`, 2s): no webhook exists or is planned.
|
||||
- No alert system exists anywhere in the codebase (grep verified) — hence the scope decision above.
|
||||
- Existing mock-chat *preview* (`ChatOverlayLayer.StartMockChatPreview`) is static placeholders,
|
||||
not live-injectable — separate from this task.
|
||||
|
||||
## What shipped (TASK 41, 2026-09-22)
|
||||
|
||||
- **`Controls/TopBar.xaml`** — **Test** button (secondary style) left of Start; visible only when
|
||||
`ShowTestButton` (offline ∧ not recording ∧ signed-in); **TEST** amber badge while
|
||||
`IsTestStream` (next to the elapsed clock).
|
||||
- **`Controls/PreviewPane.xaml`** — **TEST** tab (gold text, below YPP on the rail) + TEST drawer:
|
||||
session caption / liveChatId line, **Mock Chat Input** (TextBox + Send), **SIMULATED EVENTS**
|
||||
(Subscriber / New Member / Super Chat buttons), live feedback line, footer "End Test" + the
|
||||
"private, same pipeline as a real live" note; drawer exclusivity wired three-way.
|
||||
- **`Services/TestSessionViewModel.cs`** (NEW) — the TEST drawer VM: `BeginTest(broadcastId,
|
||||
liveChatId)` (auto-opens the dock once the liveChatId resolves), `SendTestMessageAsync` → real
|
||||
insert, `Simulate{Subscriber,Member,SuperChat}Command` → `InjectSimulatedMessage`, `EndTestCommand`
|
||||
→ hands to `MainViewModel.StopStream`, `EndTestSilently` (idempotent, called on any stop/
|
||||
rollback). Fully testable — takes `YouTubeStreamService` + `YouTubeChatService` via ctor.
|
||||
- **`Services/YouTubeStreamService.InsertChatMessageAsync(liveChatId, text)`** —
|
||||
`POST liveChat/messages?part=snippet`; returns string error or null; never throws.
|
||||
- **`Services/YouTubeChatService.InjectSimulatedMessage(ChatMessage)`** — raises `MessageReceived`,
|
||||
the exact seam the real poll uses, so simulations flow through the whole overlay pipeline;
|
||||
marked `ChatMessage.IsSimulated`.
|
||||
- **`ViewModels/MainViewModel.*`** — `IsTestStream` (internal setter, test seam like `IsRecording`);
|
||||
`BeginTestStream()` → the shared `StartStreamingSession(alsoRecord:false)` extracted from
|
||||
`BeginGoLive` (no dialog — a Test needs no confirmation, every broadcast is already private; no
|
||||
`BroadcastForm.CaptureGoLive` "last live" stamp; no recording). `StopStream` clears the face and
|
||||
forgets the drawer session. `EndButtonText` = "End Test" while in a test. Gold top bar + glow
|
||||
(`#9c6f1c`) identify a rehearsal vs the live red.
|
||||
- **Drawer exclusivity** (this task): opening TEST collapses Stream Settings + YPP and vice versa
|
||||
(wired in `MainViewModel.Ypp.cs`).
|
||||
|
||||
## Good Dog — ONE integration test
|
||||
|
||||
`ytLive.Tests/TestStreamTests.cs` — one `[Fact]`, two sections:
|
||||
1. **Coordinator (no network, `RecordingHandler`-faked HTTP):** idle → `BeginTest` opens the dock
|
||||
+ enables chat; Mock Chat Input emits the insert (POST body asserted for the liveChatId +
|
||||
messageText); Subscriber/New Member/Super Chat simulations arrive through `MessageReceived`
|
||||
marked `IsSimulated` with the right shapes (member level, `$5.00` Super Chat, plain subscriber
|
||||
row); End hands back to the app; `EndTestSilently` clears.
|
||||
2. **Real window (temp DB):** TEST tab/drawer exist, three-way drawer exclusivity, Test button
|
||||
gate follows sign-in deterministically.
|
||||
|
||||
## Follow-ups (NOT in this task)
|
||||
|
||||
- Real stinger/TTS alert widgets → TASK 3 items 16/17/20.
|
||||
- Test-mode health-panel depth (per-broadcast status inside the drawer) if ever wanted.
|
||||
Reference in New Issue
Block a user