docs: reconcile Day 2 release policy and output tasks

This commit is contained in:
2026-11-04 11:18:16 -08:00
parent 6310f50c27
commit f921c0174b
17 changed files with 120 additions and 153 deletions
+3 -2
View File
@@ -141,8 +141,9 @@ line points at the file that explains it.
→ `docs/ai/ui-surface.md` → `docs/ai/ui-surface.md`
- **Licensing: no non-CC0 icon art, no dropped `THIRD-PARTY-NOTICES.txt`, no - **Licensing: no non-CC0 icon art, no dropped `THIRD-PARTY-NOTICES.txt`, no
licence-incompatible dependency, no unvetted ML model.** → `docs/ai/product.md` licence-incompatible dependency, no unvetted ML model.** → `docs/ai/product.md`
- **The private-only broadcast lock is deliberate** — do not unlock it, do not raise it as a - **Force Private only for development/test runs.** The released creator flow offers Private,
question; it ends at v1 by the creator's ruling. → `docs/ai/product.md` Unlisted, or Public, defaults to Private, and allows live visibility changes through
`UpdateBroadcast`. → `docs/ai/youtube-api.md`
- **Read render numbers in Release only** — Debug pixel paths are several× slower. - **Read render numbers in Release only** — Debug pixel paths are several× slower.
→ `docs/ai/environment.md` → `docs/ai/environment.md`
+14 -13
View File
@@ -1,24 +1,25 @@
# HANDOFF — 2026-11-02 (Logs severity and acknowledgment) # HANDOFF — 2026-11-04 (Day 2 reconciliation and YouTube status color)
## State ## State
- `main`; previous Logs tab and 13 test-failure fixes are committed locally, not pushed. - `main`; recent commits: Logs `a6aea5b`, close-out dialog ownership `76e8c22`, vertical scaling via
- Logs wraps and colors by explicit message type. Class 1 (fatal/unhandled) and Class 2 (recoverable, FFmpeg `6310f50`.
actionable) are independent metadata; Class 1 pins above Class 2. Acknowledging unpins but retains - Day 2 documentation reconciliation is verified and pending its own commit. It retires unused
the entry in the current-launch timeline. Disk format and rotation are unchanged. `StreamConfig` defaults, records the vertical resolution-only check, and updates visibility/tier policy.
- Logs integration test passes; clean app build is 0 warnings and scope-check passes. The last full - Top-bar status color is implemented and its focused test passes: normal ON-AIR stays red until
run was 521/522: only the real-mouse layer-reorder test failed while the creator was using the YouTube reports `live`, then turns green; TEST remains gold; stopping restores the idle color.
desktop. It was not rerun; the focused drag test passes in isolation. - Full verification: clean app build, 522/523 tests (the one failure was the known
`AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback`), scope check passed.
- Four untracked `Assets/llamacasty-*` files remain untouched.
## Next action ## Next action
Commit this Logs enhancement with the interactive-mouse test exception recorded. Do not rerun that - Commit the pending Day 2 documentation unit and the top-bar color unit separately.
test while the creator is playing; do not push absent a checkpoint request. - Then continue TASK 53: enumerate output tiers and pin their geometry. Do not push absent a checkpoint request.
## Landmines ## Landmines
- `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` injects physical - `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` injects physical
mouse input. It passed in isolation; do not run it while the desktop is in use. mouse input. Do not run it while the desktop is in use.
- Any integration test constructing `MainWindow` must set a temporary `LayoutPathOverride` and - Integration tests constructing `MainWindow` must set a temporary `LayoutPathOverride` and clear it in `finally`.
clear it in `finally`.
- Bundled FFmpeg stays outside the single-file exe (`ExcludeFromSingleFile="true"`). - Bundled FFmpeg stays outside the single-file exe (`ExcludeFromSingleFile="true"`).
+2 -2
View File
@@ -16,8 +16,8 @@ Plain data types. No logic beyond what a property can carry. See
| `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` | | `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` |
| `BorderColorPalette.cs` | **TASK 60:** the colours the webcam's border-colour drop-down offers — `NamedColor(Hex, Name)` + `BorderColorPalette.All` (the single source of truth; the drop-down is bound to it) and `NameOf(hex)` (case-insensitive; **empty** for a colour outside the palette — "no name" is the honest answer, TASK 62). The creator asked for a colour NAME next to each hex: `#e94560 Crimson`. **TASK 63:** `EntryFor(hex)` (the palette entry for a hex, case-insensitively; a nameless entry for a colour outside the palette) and `OptionsFor(hex)` (the palette + that entry) — the two the drop-down binds to; `NamedColor.ToString()` is `"Crimson (#e94560)"` | | `BorderColorPalette.cs` | **TASK 60:** the colours the webcam's border-colour drop-down offers — `NamedColor(Hex, Name)` + `BorderColorPalette.All` (the single source of truth; the drop-down is bound to it) and `NameOf(hex)` (case-insensitive; **empty** for a colour outside the palette — "no name" is the honest answer, TASK 62). The creator asked for a colour NAME next to each hex: `#e94560 Crimson`. **TASK 63:** `EntryFor(hex)` (the palette entry for a hex, case-insensitively; a nameless entry for a colour outside the palette) and `OptionsFor(hex)` (the palette + that entry) — the two the drop-down binds to; `NamedColor.ToString()` is `"Crimson (#e94560)"` |
| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider); **TASK 57 (the "vv" feature):** the vertical (9:16) tier's OWN placement per scene — `VerticalX/Y/Width/Height/VerticalClipShape/VerticalRectWidth/VerticalRectHeight` (persisted, NULL = no vertical placement yet) + the transient `VerticalBand`/`VerticalLayoutLive`/`LandscapeX…LandscapeRectHeight` (the parked 16:9 layout) + `BeginVerticalLayout`/`EndVerticalLayout`/`CommitLiveLayoutToVertical` (the tier swap) and the `X`/`Y`/`Width`/`Height` overrides that clamp every write into the band; `VerticalShapeLocked` (the Chat scene's vertical layout is rect-only) **overrides** `ClipShape` + `ToggleClipShape()` so no writer can slip a circle in | | `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider); **TASK 57 (the "vv" feature):** the vertical (9:16) tier's OWN placement per scene — `VerticalX/Y/Width/Height/VerticalClipShape/VerticalRectWidth/VerticalRectHeight` (persisted, NULL = no vertical placement yet) + the transient `VerticalBand`/`VerticalLayoutLive`/`LandscapeX…LandscapeRectHeight` (the parked 16:9 layout) + `BeginVerticalLayout`/`EndVerticalLayout`/`CommitLiveLayoutToVertical` (the tier swap) and the `X`/`Y`/`Width`/`Height` overrides that clamp every write into the band; `VerticalShapeLocked` (the Chat scene's vertical layout is rect-only) **overrides** `ClipShape` + `ToggleClipShape()` so no writer can slip a circle in |
| `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)`, `RequiresPro` derives from the pixel/FPS budget — drives the quality dropdown and output-rect math | | `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)`, `RequiresPro` derives from `OutputProfile` (current code uses pixel/FPS; the FPS-only policy correction is queued in `commercial.md`) — drives the quality dropdown and output-rect math |
| `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum | | `StreamConfig.cs` | Legacy `StreamConfig` (no production call sites; defaults do not drive output), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum; encoder output comes from the selected quality tier |
| `MicStatus.cs` | Footer mic indicator: `NotConnected` / `Connected` / `Problem` — the state behind the MIC button's status dot (red = no device/capture off, green = capturing, yellow = a requested mic failed: in use/unplugged) | | `MicStatus.cs` | Footer mic indicator: `NotConnected` / `Connected` / `Problem` — the state behind the MIC button's status dot (red = no device/capture off, green = capturing, yellow = a requested mic failed: in use/unplugged) |
| `YouTube.cs` | `YouTubeChannel` and `ChatMessage` (chat feed) — since TASK 43 every message carries `Kind` (`ChatEventKind` None/SuperChat/SuperSticker/NewMember/MemberGift/GiftReceived/MemberMilestone) + event payload fields (`SuperChatDisplayString`, `SuperStickerDescription`, `GiftNewMembersCount`, `GifterName`, `MilestoneMonths`) | | `YouTube.cs` | `YouTubeChannel` and `ChatMessage` (chat feed) — since TASK 43 every message carries `Kind` (`ChatEventKind` None/SuperChat/SuperSticker/NewMember/MemberGift/GiftReceived/MemberMilestone) + event payload fields (`SuperChatDisplayString`, `SuperStickerDescription`, `GiftNewMembersCount`, `GifterName`, `MilestoneMonths`) |
| `Socials.cs` | **Social bar** (global layer, never a Source): `SocialService` enum (YouTube/Twitch/X/Instagram/TikTok/Facebook/Discord/Kick/Threads/Bluesky/GitHub/LinkedIn/Pinterest/Snapchat/Reddit/WhatsApp/Telegram/Link/Website/**Fediverse**), `SocialEntry` (Service/Handle/ProfileUrl/`FediverseSoftware`), `SocialsConfig` (Entries + `BarPosition` Top/Bottom + `BarEnabled` on/off — `BarJustify` dropped, column back-compat), `SocialServiceIcons` (canonical URL builder + `DetectService` (URL domain / fediverse `@user@domain` → **Fediverse** / bare→Website) + bundled SVG logo path data per service (`LogoDataFor`) + `LogoDataForFediverse(software)` mapping nodeinfo software names (mastodon/peertube/pixelfed/misskey/lemmy/pleroma/firefish) to logos with a generic fediverse glyph fallback (`FediverseIconData`) + `LockedIconData`/`DoNotIconData` — initials/colors gone) | | `Socials.cs` | **Social bar** (global layer, never a Source): `SocialService` enum (YouTube/Twitch/X/Instagram/TikTok/Facebook/Discord/Kick/Threads/Bluesky/GitHub/LinkedIn/Pinterest/Snapchat/Reddit/WhatsApp/Telegram/Link/Website/**Fediverse**), `SocialEntry` (Service/Handle/ProfileUrl/`FediverseSoftware`), `SocialsConfig` (Entries + `BarPosition` Top/Bottom + `BarEnabled` on/off — `BarJustify` dropped, column back-compat), `SocialServiceIcons` (canonical URL builder + `DetectService` (URL domain / fediverse `@user@domain` → **Fediverse** / bare→Website) + bundled SVG logo path data per service (`LogoDataFor`) + `LogoDataForFediverse(software)` mapping nodeinfo software names (mastodon/peertube/pixelfed/misskey/lemmy/pleroma/firefish) to logos with a generic fediverse glyph fallback (`FediverseIconData`) + `LockedIconData`/`DoNotIconData` — initials/colors gone) |
+5
View File
@@ -1707,6 +1707,11 @@ afterward** — and I got it wrong in the very commit written to apply that idea
## Asked twice about a deliberate, already-documented decision (2026-10-04) ## Asked twice about a deliberate, already-documented decision (2026-10-04)
**Superseded 2026-11-04:** the creator clarified that forcing Private is a development/test safeguard,
not a permanent product lock. Release go-live must let the creator choose Private/Unlisted/Public,
default to Private, and allow live changes. Do not repeat the former “always Private until v1” rule
as current policy; keep development and test runs Private.
**What happened:** the creator asked whether the test streams still upload private, and added "I **What happened:** the creator asked whether the test streams still upload private, and added "I
don't think you're able to delete them". I answered, researched `videos.delete`, and then — in the don't think you're able to delete them". I answered, researched `videos.delete`, and then — in the
very next exchange — *offered to build a cleanup tool*, which re-litigated a decision that has been very next exchange — *offered to build a cleanup tool*, which re-litigated a decision that has been
+16 -6
View File
@@ -18,7 +18,7 @@ corrected. No new scope mid-week; anything discovered goes on a list for day 6.
| Day | Units | Done when | | Day | Units | Done when |
|---|---|---| |---|---|---|
| **1 · Mon** — licensing | Paid-app ownership check (`BIsSubscribedApp`) + Debug fake + app-wide `IsAppOwned` · greyed tier entries · About conversion copy + llamacasty.com link · current-launch Logs tab (replaces the separate system-message strip) · Debug-only `InternalsVisibleTo` | A locked tier opens About without changing the selected free tier or leaving the app; the site opens only from its link | | **1 · Mon** — licensing | Paid-app ownership check (`BIsSubscribedApp`) + Debug fake + app-wide `IsAppOwned` · greyed tier entries · About conversion copy + llamacasty.com link · current-launch Logs tab (replaces the separate system-message strip) · Debug-only `InternalsVisibleTo` | A locked tier opens About without changing the selected free tier or leaving the app; the site opens only from its link |
| **2 · Tue** — release blockers | Visibility picker + `UpdateBroadcast` · `StreamConfig` defaults · TASK 53 output contract (geometry test per tier, 9:16 e2e) · 9·5 webcam identity key · Move transitions (per-layer carry-across-scenes) | A public broadcast starts and changes while live; every tier pinned by a test | | **2 · Tue** — release blockers | Visibility flow (creator-selected at release; development/test stays Private) · `OutputProfile` entitlement: 30 fps free / 60 fps paid regardless of resolution · ~~`StreamConfig` defaults~~ retired (no production call sites) · TASK 53 (vertical resolution check ✅; remaining output contract open) · 9·5 webcam identity key · Move transitions (per-layer carry-across-scenes) | Visibility is selectable at creation and live-editable; the 30/60-fps gate is tested; remaining Day 2 items stay queued |
| **3 · Wed** — resilience | TASK 32: blip retry → measured grace → amber RECONNECTING → one-click Back on air → fragmented MP4 → ingest pre-flight · 33 bandwidth step-down | Killing the network mid-broadcast recovers without ending it | | **3 · Wed** — resilience | TASK 32: blip retry → measured grace → amber RECONNECTING → one-click Back on air → fragmented MP4 → ingest pre-flight · 33 bandwidth step-down | Killing the network mid-broadcast recovers without ending it |
| **4 · Thu** — content & media | 3·17 Text layer · 21 media acquisition wiring + file picker + loop/volume UI · 49 chat profanity filter · 34 scheduled streams | Text layer renders/persists; media file plays, loops, has volume; scheduled stream created | | **4 · Thu** — content & media | 3·17 Text layer · 21 media acquisition wiring + file picker + loop/volume UI · 49 chat profanity filter · 34 scheduled streams | Text layer renders/persists; media file plays, loops, has volume; scheduled stream created |
| **5 · Fri** — data & settings | 3·20 RewardEvent persistence + `superChatEvents.list` backfill · 39·2 YPP slice 2 · 37 defaults-vs-current split · 35 scene-linked audio · 40·A/C/D settings round · 55·2–3 disk hygiene | Session report reads persisted events; defaults can be reverted; temp clips cleaned | | **5 · Fri** — data & settings | 3·20 RewardEvent persistence + `superChatEvents.list` backfill · 39·2 YPP slice 2 · 37 defaults-vs-current split · 35 scene-linked audio · 40·A/C/D settings round · 55·2–3 disk hygiene | Session report reads persisted events; defaults can be reverted; temp clips cleaned |
@@ -41,7 +41,9 @@ persisted; renderer mirrors `Services/ChatBoxRenderer.cs`.
> 2026-11-07 are a failure of the plan, not a scheduling note. > 2026-11-07 are a failure of the plan, not a scheduling note.
| Task | Status | Scope | Detail | | Task | Status | Scope | Detail |
|---|---|---|---| |---|---|---|---|
| 3·17 | ☐ **NEXT** | **Text layer** — content, font family/size/weight/style, colour, alignment, drag/resize, persisted | [`task-03`](TASKS/task-03-capture-pipeline.md) | | 53 | 🔶 **NEXT** | Output-format contract — next slice: inventory actual tiers and pin their geometry; the supplied vertical recording passed resolution-only review | [`task-53`](TASKS/task-53-output-formats.md) |
| — | ☐ | Output entitlement: change `OutputProfile` and its tests to free at 30 fps / paid at 60 fps, independent of resolution | [`commercial`](docs/ai/commercial.md) |
| 3·17 | ☐ | **Text layer** — content, font family/size/weight/style, colour, alignment, drag/resize, persisted | [`task-03`](TASKS/task-03-capture-pipeline.md) |
| 21 | 🔶 | Media source: acquisition wiring + file picker + loop/volume UI (decoder, manager, pacing, loop all shipped — **no `AcquireAsync` caller exists**) | [`task-21`](TASKS/task-21-media-source.md) | | 21 | 🔶 | Media source: acquisition wiring + file picker + loop/volume UI (decoder, manager, pacing, loop all shipped — **no `AcquireAsync` caller exists**) | [`task-21`](TASKS/task-21-media-source.md) |
| — | ☐ | Transitions **Move**: per-layer carry-across-scenes flag, per-scene placement, fixed z band | [`ui-surface`](docs/ai/ui-surface.md) | | — | ☐ | Transitions **Move**: per-layer carry-across-scenes flag, per-scene placement, fixed z band | [`ui-surface`](docs/ai/ui-surface.md) |
| 55·2–3 | ☐ | Disk hygiene: delete `%TEMP%` alert clips on close + sweep at startup; low-disk guard before REC (slice 1 shipped) | [`task-55`](TASKS/task-55-disk-hygiene.md) | | 55·2–3 | ☐ | Disk hygiene: delete `%TEMP%` alert clips on close + sweep at startup; low-disk guard before REC (slice 1 shipped) | [`task-55`](TASKS/task-55-disk-hygiene.md) |
@@ -56,7 +58,6 @@ persisted; renderer mirrors `Services/ChatBoxRenderer.cs`.
| 39·2 | ☐ | YPP slice 2: Analytics scopes, watch-hours/Shorts velocity + ETA | [`task-39`](TASKS/task-39-ypp-journey-tracker.md) | | 39·2 | ☐ | YPP slice 2: Analytics scopes, watch-hours/Shorts velocity + ETA | [`task-39`](TASKS/task-39-ypp-journey-tracker.md) |
| 49 | ☐ | Chat profanity filter: local, opt-in, **non-persistent**, user-supplied word list | [`task-49`](TASKS/task-49-chat-profanity-filter.md) | | 49 | ☐ | Chat profanity filter: local, opt-in, **non-persistent**, user-supplied word list | [`task-49`](TASKS/task-49-chat-profanity-filter.md) |
| 50 | ☐ | One-button publish of the local recording (resumable upload) | [`task-50`](TASKS/task-50-publish-recording.md) | | 50 | ☐ | One-button publish of the local recording (resumable upload) | [`task-50`](TASKS/task-50-publish-recording.md) |
| 53 | ☐ | Output-format contract: pin every tier with a geometry test, prove 9:16 end-to-end, recorded file matches output | [`task-53`](TASKS/task-53-output-formats.md) |
| 54 | 🔶 | Freemium: EULA + privacy shipped; free Steam Demo and paid app use one codebase; paid app ID and Steam Demo setup still pending | [`task-54`](TASKS/task-54-marcom-free-merch.md) | | 54 | 🔶 | Freemium: EULA + privacy shipped; free Steam Demo and paid app use one codebase; paid app ID and Steam Demo setup still pending | [`task-54`](TASKS/task-54-marcom-free-merch.md) |
| — | 🔶 | **Steam ownership** — `BIsSubscribedApp(paidAppId)` + Debug fake + app-wide `IsAppOwned`; paid app ID awaits Steamworks | [`commercial`](docs/ai/commercial.md) | | — | 🔶 | **Steam ownership** — `BIsSubscribedApp(paidAppId)` + Debug fake + app-wide `IsAppOwned`; paid app ID awaits Steamworks | [`commercial`](docs/ai/commercial.md) |
| — | ❌ | **Polar subscription path** — retired 2026-11-01 with the one-price model. Design kept in `decisions.md`, not built | [`decisions`](docs/ai/decisions.md) | | — | ❌ | **Polar subscription path** — retired 2026-11-01 with the one-price model. Design kept in `decisions.md`, not built | [`decisions`](docs/ai/decisions.md) |
@@ -67,7 +68,7 @@ persisted; renderer mirrors `Services/ChatBoxRenderer.cs`.
| — | ☐ | **Training-video series** — capture the first 8–10 recurring questions as videos on `@llamacasty`; each is documentation, channel content and monetization at once | MARCOM → support→content loop | | — | ☐ | **Training-video series** — capture the first 8–10 recurring questions as videos on `@llamacasty`; each is documentation, channel content and monetization at once | MARCOM → support→content loop |
| 9·5 | ☐ | Webcam identity key reconciliation | [`task-09`](TASKS/task-09-live-stream-management.md) | | 9·5 | ☐ | Webcam identity key reconciliation | [`task-09`](TASKS/task-09-live-stream-management.md) |
| 14 | 🔶 | Creator feedback batch — branch 3+ | [`task-14`](TASKS/task-14-creator-feedback-batch.md) | | 14 | 🔶 | Creator feedback batch — branch 3+ | [`task-14`](TASKS/task-14-creator-feedback-batch.md) |
| — | ☐ | Small: `StreamConfig` defaults (`TargetBitrate=6000`, `1920x1080`) are stale — the dropdown drives `StreamHealth` | this line | | — | ❌ | Retired: `StreamConfig` defaults are unused; encoder output comes from the selected quality tier | [`Models/index.md`](Models/index.md) |
| 36 | ☐ | **Gold pass** — the v1 gate, always the last unit (see 1.0 gates) | [`task-36`](TASKS/task-36-gold-pass.md) | | 36 | ☐ | **Gold pass** — the v1 gate, always the last unit (see 1.0 gates) | [`task-36`](TASKS/task-36-gold-pass.md) |
| 48 | ⏸ | Distribution & packaging — route settled 2026-10-31 (winget/portable first; Steam + Store parked). Build steps: [`Distribution.md`](Distribution.md); starts after the entitlement seam | [`task-48`](TASKS/task-48-distribution-msix.md) | | 48 | ⏸ | Distribution & packaging — route settled 2026-10-31 (winget/portable first; Steam + Store parked). Build steps: [`Distribution.md`](Distribution.md); starts after the entitlement seam | [`task-48`](TASKS/task-48-distribution-msix.md) |
| 10 | ❌ | Retired 2026-10-08; superseded by the TASK 54 rows above — the Polar design it described is rebuilt as a lease (`commercial.md`), not revived as written | [`task-10`](TASKS/task-10-monetization.md) | | 10 | ❌ | Retired 2026-10-08; superseded by the TASK 54 rows above — the Polar design it described is rebuilt as a lease (`commercial.md`), not revived as written | [`task-10`](TASKS/task-10-monetization.md) |
@@ -81,10 +82,19 @@ persisted; renderer mirrors `Services/ChatBoxRenderer.cs`.
- ⏳ **Webcam cadence**: a Release take in the failing geometry (vertical tier first) blocks Units 1–2. - ⏳ **Webcam cadence**: a Release take in the failing geometry (vertical tier first) blocks Units 1–2.
No fix until a reading names the stage. No fix until a reading names the stage.
## Creator-requested follow-ups (park for Day 6)
- Group controls under **This PC** and **YouTube**.
- ON-AIR opens a preflight using the saved visibility default and an explicit **Start Broadcast — [scope]** action.
- Persistent Help icon immediately left of Edit opens the creator-triggered, app-only 1-2-3 first-broadcast guide. Keep copy top-aligned and high-contrast; never composite the guide into broadcast output.
- Add YouTube category selection; send tags through the video API; support thumbnail upload.
- Add tooltips to settings tabs and make Logs wrapping effective.
## 1.0 gates (before the first shipped build) ## 1.0 gates (before the first shipped build)
- **Visibility picker** — replace the always-Private lock, hide the TEST button, and wire - **Release visibility flow** — default to Private; let the creator choose Private/Unlisted/Public
`UpdateBroadcast` in the same unit (it is the only code that can change a broadcast's privacy). in the ON-AIR preflight and change it live through `UpdateBroadcast`; keep development/test runs
forced Private. Hide the separate TEST button as already decided for v1.
- **Kill the dev multi-instance affordance** — `Helpers/InstanceProfile.cs` is already `#if DEBUG`; - **Kill the dev multi-instance affordance** — `Helpers/InstanceProfile.cs` is already `#if DEBUG`;
re-prove it on a Release build. re-prove it on a Release build.
- **Release engineering** — `scripts/publish.sh` **done** (self-contained win-x64, `ffmpeg\` - **Release engineering** — `scripts/publish.sh` **done** (self-contained win-x64, `ffmpeg\`
+3 -4
View File
@@ -13,7 +13,7 @@
4. ✅ **WASAPI audio capture SHIPPED** (2026-08-12) — NAudio loopback (desktop/game) + the picked mic feeding `AudioLevel`, so the realtime meter comes alive (see the ship step 4 plan below) 4. ✅ **WASAPI audio capture SHIPPED** (2026-08-12) — NAudio loopback (desktop/game) + the picked mic feeding `AudioLevel`, so the realtime meter comes alive (see the ship step 4 plan below)
5. ✅ **Frame-pipeline wiring SHIPPED** (2026-08-12) — `CameraManager`/`ScreenCaptureManager` → compositor resolver → encoder, driven by a paced `FramePump` (see the ship step 5 plan below) 5. ✅ **Frame-pipeline wiring SHIPPED** (2026-08-12) — `CameraManager`/`ScreenCaptureManager` → compositor resolver → encoder, driven by a paced `FramePump` (see the ship step 5 plan below)
6. ✅ **Health stats SHIPPED** (2026-08-13) — `FramePump.HealthUpdated` (encoder's parsed bitrate/FPS/dropped/duration, already forwarded from `FfmpegEncoder.OnStderrLine`) now lands in the bottom bar: `MainViewModel.OnFramePumpHealthUpdated` marshals to the UI thread (the stderr loop raises on a background thread) and copies into `CurrentHealth` (the bottom bar's existing binding); `ResetHealth` zeroes dropped/duration on go-live and on End so stats never linger from a previous session (bitrate/FPS stay on the tier's targets). The bar lights up with real values once TASK 9 supplies the RTMP URL (until then the pump skips the encoder and the bar shows the tier's targets) 6. ✅ **Health stats SHIPPED** (2026-08-13) — `FramePump.HealthUpdated` (encoder's parsed bitrate/FPS/dropped/duration, already forwarded from `FfmpegEncoder.OnStderrLine`) now lands in the bottom bar: `MainViewModel.OnFramePumpHealthUpdated` marshals to the UI thread (the stderr loop raises on a background thread) and copies into `CurrentHealth` (the bottom bar's existing binding); `ResetHealth` zeroes dropped/duration on go-live and on End so stats never linger from a previous session (bitrate/FPS stay on the tier's targets). The bar lights up with real values once TASK 9 supplies the RTMP URL (until then the pump skips the encoder and the bar shows the tier's targets)
7. ✅ **One-click go live + private-only enforcement SHIPPED** (2026-08-14) — the Go Live dialog is **locked to Private** (no dropdown, `GoLiveViewModel.Visibility` is a get-only "Private"); `YouTubeStreamService.CreateBroadcast` **always sends `privacyStatus = "private"`** (dialog + service enforcement, requirement 8 — nothing can go out non-private) and gained an injectable `HttpClient? http = null` seam for tests; `BeginGoLive` now calls `CreateBroadcastAsync` and remembers `_currentBroadcastId` for TASK 9's bind/transition (failure → `StreamStatus.Error`, never a crash; `StopStream` clears the ID); the REC sign shows a **PRIVATE badge** (dark-red border, next to REC, `IsLivePrivate`) when the live stream is private; settings' dead "Default Visibility" dropdown + `MainViewModel.Visibilities`/`DefaultStreamVisibility` removed. The broadcast-insert integration test asserts the request body carries `"privacyStatus":"private"` (2 new tests → 173 passing, 0 warnings) 7. ✅ **One-click go live + initial Private development safeguard SHIPPED** (2026-08-14) — at the time, the Go Live dialog and `YouTubeStreamService.CreateBroadcast` forced `privacyStatus = "private"`; the broadcast-insert test pinned that behavior. The creator clarified 2026-11-04 that this is a development/test safeguard, not permanent product policy. See TASK 9 and `docs/ai/youtube-api.md` for the release visibility flow.
The pipeline chain the encoder needs doesn't exist yet: **scene compositing** (the master 1920×1080 frame The pipeline chain the encoder needs doesn't exist yet: **scene compositing** (the master 1920×1080 frame
without the preview's editing chrome) → **audio capture** (WASAPI, feeds the meter) → **H.264+AAC encode** without the preview's editing chrome) → **audio capture** (WASAPI, feeds the meter) → **H.264+AAC encode**
@@ -46,8 +46,8 @@ until a frame source exists, so the compositor is ship step 1. The pipeline is
5. **Health stats** — bitrate, FPS, dropped frames reported live in the bottom bar (encoder-side) 5. **Health stats** — bitrate, FPS, dropped frames reported live in the bottom bar (encoder-side)
6. **One-click go live** — defaults that work out of the box 6. **One-click go live** — defaults that work out of the box
7. **Audio capture (feeds the meter — this task ships the wiring)** — WASAPI loopback (desktop/game at unity, zero UI — "it just is") + the picked mic (`MicSourceName` from the `MicPickerDialog`). The mic capture feeds `AudioLevel` so the realtime meter comes alive (today it reads 0 — the mixer feed is pending, see `ai.md` audio notes). AAC mono/stereo @ 48 kHz per the compliance rules. 7. **Audio capture (feeds the meter — this task ships the wiring)** — WASAPI loopback (desktop/game at unity, zero UI — "it just is") + the picked mic (`MicSourceName` from the `MicPickerDialog`). The mic capture feeds `AudioLevel` so the realtime meter comes alive (today it reads 0 — the mixer feed is pending, see `ai.md` audio notes). AAC mono/stereo @ 48 kHz per the compliance rules.
8. **Private-only go live until v1 (reputation guard, decided 2026-08-10)** — until the v1 release, go-live is **locked to private streams only** so a software error can never publish something public/unlisted that damages the creator's reputation. RTMP push itself has no privacy — privacy lives on the YouTube **live broadcast object**, which this app already controls via its OAuth API calls. So the lock is purely API-side: the Go Live flow always creates/updates the broadcast with `privacyStatus = "private"` and a guard **refuses** to set anything else (same spirit as the Live-only backdrop policy). The UI shows a clear "PRIVATE" badge next to the stream state so the creator always knows who can see them. Enforcement must be verifiable in the auth-service tests (fake the broadcast-insert/update call, assert `privacyStatus` is forced to private). 8. **Development/test privacy safeguard (creator clarification 2026-11-04)** — force `Private` during development and test runs so unfinished captures cannot publish accidentally. This is not a permanent product lock: the released creator flow offers Private/Unlisted/Public, defaults to Private, and supports live visibility changes. Verify the environment-specific guard and release flow in tests.
9. **v1 release gate: bundle the full license texts (decided 2026-08-10)** — `THIRD-PARTY-NOTICES.txt` currently links the canonical license texts rather than embedding them. At the **v1 (GA) release**, the full texts of every license it names (LGPL v2.1+, BSD-2-Clause, MIT, Apache-2.0) MUST be bundled alongside it (shipped in the app output, e.g. a `licenses/` folder next to the notices file, still reachable from the About screen). This is a **release blocker for v1, not a task to queue early** — do it in the release pass. The repo should treat this like the private-only go-live gate: a checkbox that cannot silently lapse. 9. **v1 release gate: bundle the full license texts (decided 2026-08-10)** — `THIRD-PARTY-NOTICES.txt` currently links the canonical license texts rather than embedding them. At the **v1 (GA) release**, the full texts of every license it names (LGPL v2.1+, BSD-2-Clause, MIT, Apache-2.0) MUST be bundled alongside it (shipped in the app output, e.g. a `licenses/` folder next to the notices file, still reachable from the About screen). This is a **release blocker for v1, not a task to queue early** — do it in the release pass; the checkbox cannot silently lapse.
#### Ship step 1 — Scene compositor (the frame source) #### Ship step 1 — Scene compositor (the frame source)
@@ -336,4 +336,3 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash),
(`SocialBarSnap`) were removed with the click-toggle supersession. (`SocialBarSnap`) were removed with the click-toggle supersession.
**Not included (say the word):** rewriting the healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps`. **Not included (say the word):** rewriting the healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps`.
+5 -6
View File
@@ -5,23 +5,23 @@
**Goal:** Create/bind broadcasts, monitor YouTube-side stream health — the v3 way. **Goal:** Create/bind broadcasts, monitor YouTube-side stream health — the v3 way.
### Status: ⏳ In progress — items 1–3 SHIPPED (reusable stream 2026-08-16; report-by-exception health 2026-08-16); item 4 SHIPPED (status reconciled 2026-09-01); item 5 open; item 6 = deliberate lock (TASK 36); item 7 scoped to core fields — advanced tab is out-of-product ### Status: ⏳ In progress — items 1–3 SHIPPED (reusable stream 2026-08-16; report-by-exception health 2026-08-16); item 4 SHIPPED (status reconciled 2026-09-01); item 5 open; item 6 is the Day 2 release-visibility work, with a development/test Private safeguard; item 7 scoped to core fields — advanced tab is out-of-product
1. ☑ **Broadcast creation** — title/description/privacy/scheduledStartTime via API, with the v3 flags above (SHIPPED: `CreateBroadcast` sends `enableAutoStart/Stop`, `enableMonitorStream=false`, `latencyPreference=low`, `selfDeclaredMadeForKids=false`) 1. ☑ **Broadcast creation** — title/description/privacy/scheduledStartTime via API, with the v3 flags above (SHIPPED: `CreateBroadcast` sends `enableAutoStart/Stop`, `enableMonitorStream=false`, `latencyPreference=low`, `selfDeclaredMadeForKids=false`)
2. ☑ **Reusable stream** — create once, cache + reuse; bind to broadcast (SHIPPED: `GetOrCreateReusableStreamAsync` lists-then-inserts the `variable`/`isReusable` stream, cached via `LayoutStore` Settings, bound at broadcast insert via `boundStreamId`; `_rtmpUrlProvider` yields the ingest URL so go-live actually encodes + pushes) 2. ☑ **Reusable stream** — create once, cache + reuse; bind to broadcast (SHIPPED: `GetOrCreateReusableStreamAsync` lists-then-inserts the `variable`/`isReusable` stream, cached via `LayoutStore` Settings, bound at broadcast insert via `boundStreamId`; `_rtmpUrlProvider` yields the ingest URL so go-live actually encodes + pushes)
3. ☑ **Health monitoring** — poll `liveStreams.list` `healthStatus` + `configurationIssues[]`, surface banner only on warning/error (SHIPPED: `GetStreamHealthAsync(streamId)` 30s while live; pure `StreamHealthReporter.BannerFor` = report-by-exception; banner strip under the top bar, amber warning / dark-red error, via `HealthIssueBanner`/`HealthIssueBackground`; poll failures log-only; ONE integration test `GetStreamHealthAsync_Report_By_Exception_Banner_Only_On_Warning_Or_Error`) 3. ☑ **Health monitoring** — poll `liveStreams.list` `healthStatus` + `configurationIssues[]`, surface banner only on warning/error (SHIPPED: `GetStreamHealthAsync(streamId)` 30s while live; pure `StreamHealthReporter.BannerFor` = report-by-exception; banner strip under the top bar, amber warning / dark-red error, via `HealthIssueBanner`/`HealthIssueBackground`; poll failures log-only; ONE integration test `GetStreamHealthAsync_Report_By_Exception_Banner_Only_On_Warning_Or_Error`)
4. ☑ Live chat — poll `liveChat/messages`, render in right panel, support Super Chat + membership badges — **SHIPPED** (status reconciled 2026-09-01: `YouTubeChatService` polls + parses `superChatEvent`/`newSponsorEvent` with badge/level on `ChatMessage`; the panel MOVED to the left-panel live state by Control Surface UX, "right panel" superseded; the remaining reward types land with the monetization chain, TASK 10 → related work) 4. ☑ Live chat — poll `liveChat/messages`, render in right panel, support Super Chat + membership badges — **SHIPPED** (status reconciled 2026-09-01: `YouTubeChatService` polls + parses `superChatEvent`/`newSponsorEvent` with badge/level on `ChatMessage`; the panel MOVED to the left-panel live state by Control Surface UX, "right panel" superseded; the remaining reward types land with the monetization chain, TASK 10 → related work)
5. ☐ Error handling — the YouTube error codes: `errorStreamInactive`, `invalidTransition`, `redundantTransition`, `liveStreamDeletionNotAllowed`, `liveStreamModificationNotAllowed`, `liveBroadcastBindingNotAllowed` 5. ☐ Error handling — the YouTube error codes: `errorStreamInactive`, `invalidTransition`, `redundantTransition`, `liveStreamDeletionNotAllowed`, `liveStreamModificationNotAllowed`, `liveBroadcastBindingNotAllowed`
6. ❌ **Visibility picker** — DELIBERATE TEST-PHASE LOCK (creator decision 2026-09-01), not open work: "always Private" stays during multi-month real-world testing so breakage VODs never clutter the channel. `recordFromStart`/DVR stay on — Private VODs are invisible review tapes; bulk-delete pre-GA. Unlock = TASK 36 gold-pass item (remove the override in `YouTubeStreamService.CreateBroadcast`, wire the dialog selection). See `ai.md` → YouTube Live API constraints. 6. ☐ **Release visibility flow** — persist a default of Private; offer Private/Unlisted/Public in the ON-AIR preflight with an explicit “Start Broadcast — [scope]” action; apply the selected value at creation and via `UpdateBroadcast` while live. Development/test runs stay forced Private.
7. 🔶 **Full broadcast form** — RESCOPED 2026-09-01, not the old two-tab plan. **Core fields (SHIPPED as the Text drawer, 2026-08-24):** title, description, tags, visibility, made-for-kids — live-editable. **Scheduling:** ships as **TASK 34** (Scheduled checkbox + datetime in the same drawer). **Advanced tab is PERMANENTLY OUT** (the 10% margin): latency (locked `low`), DVR/record-from-start (locked on), embed, projection, CC, region restrictions stay fixed at sane defaults, invisible — every exposed field is a support ticket. `categoryId` removal: done (not a `liveBroadcast` field). Monetization enablement (if ever needed) rides the reward-events chain via `liveBroadcasts.update`, not a form field 7. 🔶 **Full broadcast form** — RESCOPED 2026-09-01, not the old two-tab plan. **Core fields (SHIPPED as the Text drawer, 2026-08-24):** title, description, tags, and made-for-kids; release visibility is item 6. **Scheduling:** ships as **TASK 34** (Scheduled checkbox + datetime in the same drawer). Advanced settings remain out of product: latency, DVR/record-from-start, embed, projection, and captions stay at fixed defaults. Category selection and tag updates through the video API, plus thumbnail upload, remain follow-ups; `categoryId` is video metadata, not a `liveBroadcast` field. Monetization enablement (if ever needed) rides the reward-events chain via `liveBroadcasts.update`, not a form field.
### Design decisions (v3) ### Design decisions (v3)
1. **One-click go-live** — `liveBroadcasts.insert` with `enableAutoStart=true`, `enableAutoStop=true`, `enableMonitorStream=false`, `selfDeclaredMadeForKids=false`, `latencyPreference=low`. No `transition(live)` call, no testing stage, no liveStarting polling. Encoder starts → YouTube brings it live by itself. 1. **One-click go-live** — `liveBroadcasts.insert` with `enableAutoStart=true`, `enableAutoStop=true`, `enableMonitorStream=false`, `selfDeclaredMadeForKids=false`, `latencyPreference=low`. No `transition(live)` call, no testing stage, no liveStarting polling. Encoder starts → YouTube brings it live by itself.
2. **Variable reusable stream** — `cdn.resolution=variable`, `cdn.frameRate=variable`, `isReusable=true`. Create once per channel, cache ingestion URL + stream name, reuse for every broadcast. Any quality tier works without recreation; auto step-down needs no API calls. 2. **Variable reusable stream** — `cdn.resolution=variable`, `cdn.frameRate=variable`, `isReusable=true`. Create once per channel, cache ingestion URL + stream name, reuse for every broadcast. Any quality tier works without recreation; auto step-down needs no API calls.
3. **Report-by-exception** — poll `liveStreams.list`; banner only on `healthStatus` warning/error issues (`configurationIssues[]`). Bottom strip = YouTube logo + green/red connection dot (clickable → opens the dialog). 3. **Report-by-exception** — poll `liveStreams.list`; banner only on `healthStatus` warning/error issues (`configurationIssues[]`). Bottom strip = YouTube logo + green/red connection dot (clickable → opens the dialog).
4. **One dialog, three states** — `not connected` (sign-in) / `connected-offline` (all editable) / `live` (title + description + visibility editable; quality + account greyed out). Both entry points (Start Stream button + bottom strip) open it; prefilled from saved session profile. 4. **Go-live preflight** — ON-AIR opens a scope preflight using the saved visibility default; the creator explicitly confirms with “Start Broadcast — [scope]”.
5. **Live edits** — `liveBroadcasts.update` with part=`snippet,status` for title/description/privacy. 5. **Live edits in Release** — `liveBroadcasts.update` with part=`snippet,status` applies title/description/privacy changes. Development/test runs remain Private.
6. **End stream** — stop encoder → `transition(complete)`, with `enableAutoStop` as the safety net. **SHIPPED 2026-09-01** (`EndBroadcastAsync`, wired into `StopStream` after RTMP EOF; the call had been specced since TASK 9 but never existed — found during recording verification). **Hardened 2026-09-22:** a blind complete 403s `invalidTransition` once autoStop/YouTube marks the broadcast complete (seen on every 2026-09-22 end) — the call now pre-flights `liveBroadcasts.list→lifeCycleStatus` and only transitions from `live`/`testing`, skipping already-complete broadcasts silently (`enableAutoStop` finishes every skip). 6. **End stream** — stop encoder → `transition(complete)`, with `enableAutoStop` as the safety net. **SHIPPED 2026-09-01** (`EndBroadcastAsync`, wired into `StopStream` after RTMP EOF; the call had been specced since TASK 9 but never existed — found during recording verification). **Hardened 2026-09-22:** a blind complete 403s `invalidTransition` once autoStop/YouTube marks the broadcast complete (seen on every 2026-09-22 end) — the call now pre-flights `liveBroadcasts.list→lifeCycleStatus` and only transitions from `live`/`testing`, skipping already-complete broadcasts silently (`enableAutoStop` finishes every skip).
7. **Broadcast ID == Video ID** — one ID to track status, health, and the auto-created VOD (`recordFromStart` + `enableDvr`). 7. **Broadcast ID == Video ID** — one ID to track status, health, and the auto-created VOD (`recordFromStart` + `enableDvr`).
@@ -34,4 +34,3 @@
5. **Error handling** — the YouTube error codes: `errorStreamInactive`, `invalidTransition`, `redundantTransition`, `liveStreamDeletionNotAllowed`, `liveStreamModificationNotAllowed`, `liveBroadcastBindingNotAllowed` 5. **Error handling** — the YouTube error codes: `errorStreamInactive`, `invalidTransition`, `redundantTransition`, `liveStreamDeletionNotAllowed`, `liveStreamModificationNotAllowed`, `liveBroadcastBindingNotAllowed`
6. **Visibility picker** — Private/Unlisted/Public from the go-live dialog 6. **Visibility picker** — Private/Unlisted/Public from the go-live dialog
7. **Full broadcast form** — Core + Advanced tabs with all API-supported fields 7. **Full broadcast form** — Core + Advanced tabs with all API-supported fields
+1 -2
View File
@@ -54,5 +54,4 @@ The tasks below are ordered by dependency and risk. Each task builds on the prev
14. **TASK 35** — Scene-linked audio ("scenes remember the room"). 14. **TASK 35** — Scene-linked audio ("scenes remember the room").
15. **Text source (TASK 3 item 17)** + **the monetization-awareness chain + Alerts (TASK 10 related work → TASK 3 item 20)** — capture → report → journey → alerts, one slice per integration test. 15. **Text source (TASK 3 item 17)** + **the monetization-awareness chain + Alerts (TASK 10 related work → TASK 3 item 20)** — capture → report → journey → alerts, one slice per integration test.
16. **TASK 13** — Social media launch kit (MARCOM) — after features, before gold. 16. **TASK 13** — Social media launch kit (MARCOM) — after features, before gold.
17. **TASK 36** — Gold pass, always last: visibility unlock + flash-live enable + screens/layers audit + native verification suite + expiry reminders + signing/installer/EULA/Velopack. 17. **TASK 36** — Gold pass, always last: verify the release visibility flow while development/test runs remain Private + flash-live enable + screens/layers audit + native verification suite + expiry reminders + signing/installer/EULA/Velopack.
+2 -4
View File
@@ -28,7 +28,5 @@ channel countdown; come Friday, one click goes live into the scheduled broadcast
confirmation. (b) delete semantics for a ready broadcast bound to a reusable stream. confirmation. (b) delete semantics for a ready broadcast bound to a reusable stream.
6. ☐ Test (the ONE): schedule → persists + inserts (fake service captures a future scheduledStartTime 6. ☐ Test (the ONE): schedule → persists + inserts (fake service captures a future scheduledStartTime
+ boundStreamId); start → adopts instead of inserting; uncheck → delete called. + boundStreamId); start → adopts instead of inserting; uncheck → delete called.
- **Test-phase note:** dark by design while the visibility lock holds (Private notifies nobody); the - **Visibility note (updated 2026-11-04):** development/test scheduled broadcasts stay Private; the
creator can still exercise insert/adopt/cancel. Full shine arrives with TASK 36's unlock — pleasing released creator flow uses the saved default or scope selected in the ON-AIR preflight.
coupling, deliberate.
+2 -3
View File
@@ -6,8 +6,8 @@
**Goal:** the single deliberate pass that turns the dev product into the shipped product. Nothing here **Goal:** the single deliberate pass that turns the dev product into the shipped product. Nothing here
may be done opportunistically mid-development — each item has a lock/comment pointing at this task. may be done opportunistically mid-development — each item has a lock/comment pointing at this task.
1. ☐ **Visibility unlock** — remove the `CreateBroadcast` Private override (TASK 9 item 6); dialog 1. ☐ **Release visibility verification** — verify the Day 2 Private/Unlisted/Public creator flow,
selection rules. The channel-protection stance ends at GA, by hand, here. including live `UpdateBroadcast`; development/test runs must remain forced Private.
2. ☐ **Branding flash goes live** — end the preview-only pre-GA posture: composite the escalating 2. ☐ **Branding flash goes live** — end the preview-only pre-GA posture: composite the escalating
obnoxious-promo flash onto output + recordings for free users. Paid removes it; that stays the obnoxious-promo flash onto output + recordings for free users. Paid removes it; that stays the
ONLY paid delta. (Escalation curve finalized here, first.) ONLY paid delta. (Escalation curve finalized here, first.)
@@ -65,4 +65,3 @@ flash is preview-only (see ai.md → Monetization; TASK 36 flips it live).
*If a future session is tempted by anything on this list, the answer is already written. New ideas *If a future session is tempted by anything on this list, the answer is already written. New ideas
must survive this page before they get a task number.* must survive this page before they get a task number.*
+4 -5
View File
@@ -24,10 +24,9 @@ inject mock chat, with alert/event triggers for subscriber, membership, and Supe
## Facts the build sits on (verified in code, 2026-09-22 research pass) ## Facts the build sits on (verified in code, 2026-09-22 research pass)
- **The app is already private-only go-live** — `YouTubeStreamService.CreateBroadcast` hardcodes - **Test mode is always Private** — it remains a **session variant over the real pipeline**, not a
`privacyStatus="private"` + `enableAutoStart/Stop` + `low latency`, no monitor stream new broadcast mode. The creator clarified 2026-11-04 that normal Release Go Live offers a
(`enableMonitorStream=false`), and the Go Live dialog is locked to Private. "Test" is therefore a visibility choice; development/test runs stay forced Private.
**session variant over the real pipeline**, not a new broadcast mode.
- The **liveChatId flow already exists**: `GetBroadcastLiveChatIdAsync` → `_youtubeChat.Start(liveChatId)` - The **liveChatId flow already exists**: `GetBroadcastLiveChatIdAsync` → `_youtubeChat.Start(liveChatId)`
in `PrepareAndStartLiveAsync` — the "chat endpoint activation" the ask describes. in `PrepareAndStartLiveAsync` — the "chat endpoint activation" the ask describes.
- OAuth scope already includes `youtube` + `youtube.force-ssl` (`YouTubeAuthService.cs:18`) — - OAuth scope already includes `youtube` + `youtube.force-ssl` (`YouTubeAuthService.cs:18`) —
@@ -79,4 +78,4 @@ inject mock chat, with alert/event triggers for subscriber, membership, and Supe
## Follow-ups (NOT in this task) ## Follow-ups (NOT in this task)
- Real stinger/TTS alert widgets → TASK 3 items 16/17/20. - 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. - Test-mode health-panel depth (per-broadcast status inside the drawer) if ever wanted.
+10 -8
View File
@@ -13,17 +13,19 @@ prove it. Make the output formats a tested, locked contract instead of an assump
**Slices (one integration test each, in order):** **Slices (one integration test each, in order):**
1. ☐ **What we actually emit** — enumerate the real encoding configurations (widescreen 1080p60/ 1. ☐ **What we actually emit** — enumerate the real encoding configurations (widescreen 1080p60/
1080p30/720p60/720p30 + vertical 9:16 1080p60) and pin them in one place (`FfmpegArgs`-adjacent), 1080p30/720p60/720p30 + vertical 9:16 1080p60/1080p30) and pin them in one place (`FfmpegArgs`-adjacent),
each with its own geometry contract test. This turns "formats exist" into "formats are defined". each with its own geometry contract test. This turns "formats exist" into "formats are defined".
2. ☐ **9:16 geometry** — vertical render must be true 1080×1920 (or the GUP-negotiated size) with 2. ✅ **9:16 output resolution (resolution-only acceptance, 2026-11-04)** — creator-supplied
the composite laid out for a phone: viewer-safe top strip (TikTok/Shorts overlays live top-left `ty-20261104-0927-982-0000-2.mp4` reports H.264 1080×1920 at 60 fps. The creator accepted the
and bottom), caption/text clearance, and the webcam not cropped off-centre. Test: composite a resolution check only; captions and visual safe-zone placement were not reviewed.
known layout, decode the output frame, assert dimensions + element placement vs the contract. 3. ☐ **9:16 safe-zone geometry** — confirm the composite is laid out for a phone: viewer-safe top
3. ☐ **Live-shorts check on a real stream** — private Test Stream at 9:16, then pull the thumbnail/ strip (TikTok/Shorts overlays live top-left and bottom), caption/text clearance, and webcam
placement. Test a known layout and assert element placement against the contract.
4. ☐ **Live-shorts check on a real stream** — private Test Stream at 9:16, then pull the thumbnail/
player quirk list from YouTube (Title visible, overlays); confirm nothing the streamer put on player quirk list from YouTube (Title visible, overlays); confirm nothing the streamer put on
screen sits under YouTube's own chrome. This is the "never tested" item the creator flagged — screen sits under YouTube's own chrome. This is the "never tested" item the creator flagged —
test on a real vertical test stream and cite the observation. test on a real vertical test stream and cite the observation.
4. ☐ **Recorded .mp4 matches output** — the saved vertical .mp4 must carry the same 9:16 frame the 5. ☐ **Recorded .mp4 matches output** — the saved vertical .mp4 must carry the same 9:16 frame the
stream showed, not a squeezed/wider master. Test: record vertical, decode first frames, assert. stream showed, not a squeezed/wider master. Test: record vertical, decode first frames, assert.
**Design decisions:** **Design decisions:**
@@ -31,4 +33,4 @@ prove it. Make the output formats a tested, locked contract instead of an assump
- Geometry is a contract with tests, not a hope. Derived ratios (stream vs composite vs canvas) - Geometry is a contract with tests, not a hope. Derived ratios (stream vs composite vs canvas)
get a derived-value test so one change can't silently wreck Shorts/TikTok output. get a derived-value test so one change can't silently wreck Shorts/TikTok output.
- Keep widescreen untouched — only add what vertical needs (the existing test suite stays green - Keep widescreen untouched — only add what vertical needs (the existing test suite stays green
for 16:9). for 16:9).
+2 -2
View File
@@ -8,8 +8,8 @@
A Windows-only WPF (.NET 8) app that live-streams to YouTube. Five fixed scenes A Windows-only WPF (.NET 8) app that live-streams to YouTube. Five fixed scenes
(Starting / Live / BRB / Chat / Ending), a pure C# software compositor over an always-1920×1080 (Starting / Live / BRB / Chat / Ending), a pure C# software compositor over an always-1920×1080
master, a bundled-ffmpeg encoder pushing RTMP, and a WASAPI audio chain. Freemium — free is the master, a bundled-ffmpeg encoder pushing RTMP, and a WASAPI audio chain. Freemium — the whole app
whole app at a 720p30 output profile, Pro buys higher output quality — under a proprietary EULA, is free at 30 fps across supported resolutions; Pro unlocks 60 fps — under a proprietary EULA,
with no account creation: it links an existing YouTube channel. One-click go-live is the product; with no account creation: it links an existing YouTube channel. One-click go-live is the product;
OBS is the escape hatch for anything bigger. OBS is the escape hatch for anything bigger.
+20 -14
View File
@@ -13,7 +13,8 @@ storefront, support or promo work. Cause of death for every retired path:
- **Pro is the paid Steam app; the free Demo is associated with it.** The paid app is **$79 during - **Pro is the paid Steam app; the free Demo is associated with it.** The paid app is **$79 during
the announced launch period, then $99** on the announced date. No subscription or third-party the announced launch period, then $99** on the announced date. No subscription or third-party
checkout. checkout.
- **The only delta is output quality.** Features are never gated; a payment buys pixels. - **The only delta is frame rate.** Features are never gated; every supported resolution is free at
30 fps, and 60 fps is paid. Resolution does not decide entitlement.
- **No watermark, ever**, in either edition. The branding flash is retired as a paid delta and - **No watermark, ever**, in either edition. The branding flash is retired as a paid delta and
stays off (`ShowBrandFlash`, default `false`). stays off (`ShowBrandFlash`, default `false`).
- **Clean numbers, no `.99`**, no discount codes, no countdowns. **The one dated exception** is the - **Clean numbers, no `.99`**, no discount codes, no countdowns. **The one dated exception** is the
@@ -25,33 +26,38 @@ storefront, support or promo work. Cause of death for every retired path:
## Output profile — the one entitlement rule ## Output profile — the one entitlement rule
The cap is a pixel budget plus a frame rate, not the word "720p": The product rule is frame rate, not pixel count or a resolution label:
| | Free | Pro | | | Free | Pro |
|---|---|---| |---|---|---|
| Pixels | ≤ **921,600** | any shipped tier | | Pixels | any supported tier | any supported tier |
| Frame rate | ≤ **30 fps** | any shipped tier | | Frame rate | ≤ **30 fps** | up to **60 fps** |
| Landscape | 1280×720 @30 | 1280×720 @60 · 1920×1080 @30 · @60 | | Landscape | 1280×720 @30 · 1920×1080 @30 | all Free tiers + 1280×720 @60 · 1920×1080 @60 |
| Vertical 9:16 | 720×1280 @30 | 1080×1920 @60 | | Vertical 9:16 | 1080×1920 @30 | all Free tiers + 1080×1920 @60 |
- **Records too.** The cap governs the file the creator keeps as well as the stream; otherwise - **Implementation gap (2026-11-04):** `Models/OutputProfile.cs` still gates by pixel budget plus
free 1080p recording is a re-upload hole. **There is one ffmpeg process with two output blocks** frame rate. Update that rule and its tests to use frame rate alone; this docs unit records the
product decision but does not change code.
- **Records too.** The frame-rate rule governs the file the creator keeps as well as the stream;
otherwise free 60-fps recording is a re-upload hole. **There is one ffmpeg process with two output blocks**
(FLV to RTMP, MP4 to disk) fed by one `EncoderOptions`, and the profile is resolved once when (FLV to RTMP, MP4 to disk) fed by one `EncoderOptions`, and the profile is resolved once when
those options are built (`MainViewModel.Streaming.Operations.cs` → `Models/OutputProfile.cs`). those options are built (`MainViewModel.Streaming.Operations.cs` → `Models/OutputProfile.cs`).
There is no second encoder to keep in step and therefore no second check to forget. There is no second encoder to keep in step and therefore no second check to forget.
- **Fail closed.** Unresolved, unreadable or unvalidated entitlement = free tier. Never Pro. - **Fail closed.** Unresolved, unreadable or unvalidated entitlement = free tier. Never Pro.
- **The development override exists and never ships.** `MainViewModel.DeveloperMode` bypasses the - **The development override exists and never ships.** `MainViewModel.DeveloperMode` bypasses the
budget in a Debug build only; the Settings row that toggles it is **excluded from the Release entitlement gate in a Debug build only; the Settings row that toggles it is **excluded from the Release
compile** (`ytLive.csproj` removes its Page and code-behind), and the setter refuses the value compile** (`ytLive.csproj` removes its Page and code-behind), and the setter refuses the value
when `Helpers/DevBuild.IsDevelopment` is false. Not a hidden row — an absent one. when `Helpers/DevBuild.IsDevelopment` is false. Not a hidden row — an absent one.
- **Never downgrade a running stream.** The gate applies to the next start; pulling quality out - **Never downgrade a running stream.** The gate applies to the next start; pulling 60 fps out of a
of a live broadcast is a worse bug than one free 1080p session. live broadcast is a worse bug than one session starting at an unentitled frame rate.
- **The greyed menu entry is the sales surface.** It stays clickable and is labelled ("720p30 · - **The greyed menu entry is the sales surface.** It stays clickable; locked 60-fps tiers are marked
free tier"). Selecting a locked tier opens the in-app About panel; the browser opens only if the as paid. Selecting a locked tier opens the in-app About panel; the browser opens only if the
creator then clicks its llamacasty.com link. creator then clicks its llamacasty.com link.
- **Ownership is checked at app startup** and cached in `IsAppOwned` for that run. Purchases and - **Ownership is checked at app startup** and cached in `IsAppOwned` for that run. Purchases and
refunds are reflected on the next app start; there is no subscription lapse or sign-out path. refunds are reflected on the next app start; there is no subscription lapse or sign-out path.
- **Bitrate follows the profile** — a capped user is not offered a 1080p bitrate. - **Bitrate follows the selected profile** — free users may choose any supported 30-fps tier; paid
users may choose supported 60-fps tiers.
## Entitlement — one source ## Entitlement — one source
+4 -3
View File
@@ -28,9 +28,10 @@ paid app's Steam App ID. The website links the store page; it never takes paymen
nothing" proposal was rejected: Steamworks requires in-app purchases to use the Steam Wallet, so nothing" proposal was rejected: Steamworks requires in-app purchases to use the Steam Wallet, so
it is a listing-level risk to save ~$24 a sale. it is a listing-level risk to save ~$24 a sale.
**Kept:** the free tier is the whole app at a 720p30 output profile, the paid delta is output **Kept at the time; superseded 2026-11-04:** the free tier was described as a 720p30 profile. The
quality only, and the app never sells anything itself — the greyed tier opens the Steam store current policy is the whole app free at any supported resolution up to 30 fps; Pro unlocks 60 fps,
overlay. regardless of resolution. The app never sells anything itself — the greyed tier opens the Steam
store overlay.
## Commercial mechanics — died 2026-10-31 (same week they were written) ## Commercial mechanics — died 2026-10-31 (same week they were written)
+1 -1
View File
@@ -15,7 +15,7 @@ Read the file whose row matches the task. There is no required reading order.
| [`environment.md`](environment.md) | LAN hosts/IPs/SSH and the Gitea push recipe, the Windows `dotnet.exe` build + run commands, the test suite map, and the hermetic real-`MainWindow` test rule | Before building, running, adding a test, or reaching any box on the LAN | | [`environment.md`](environment.md) | LAN hosts/IPs/SSH and the Gitea push recipe, the Windows `dotnet.exe` build + run commands, the test suite map, and the hermetic real-`MainWindow` test rule | Before building, running, adding a test, or reaching any box on the LAN |
| [`product.md`](product.md) | Brand/naming split, AI transparency, the licensing prohibitions, the freemium posture, auth/account assumptions, and packaging behaviour | Branding, naming, packaging, or a legal/third-party obligation | | [`product.md`](product.md) | Brand/naming split, AI transparency, the licensing prohibitions, the freemium posture, auth/account assumptions, and packaging behaviour | Branding, naming, packaging, or a legal/third-party obligation |
| [`commercial.md`](commercial.md) | The live commercial policy: model and prices, the output-profile cap (stream **and** recording), paid-app ownership (`BIsSubscribedApp`), **Steam as the only channel**, signing (moot), costs, **support policy**, anti-piracy posture | Any question about money or support: pricing, what free gets, how it ships, what a sale costs | | [`commercial.md`](commercial.md) | The live commercial policy: model and prices, the output-profile cap (stream **and** recording), paid-app ownership (`BIsSubscribedApp`), **Steam as the only channel**, signing (moot), costs, **support policy**, anti-piracy posture | Any question about money or support: pricing, what free gets, how it ships, what a sale costs |
| [`youtube-api.md`](youtube-api.md) | Go-live ordering + the broadcast-binding fix, the private-only lock, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service | | [`youtube-api.md`](youtube-api.md) | Go-live ordering + broadcast binding, the development/test Private safeguard and release visibility flow, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service |
| [`overlays.md`](overlays.md) | The social bar, the native alert box (six animations, alert clip, ticker) and the chat box conventions — everything composited over the frame | Adding or changing anything drawn on top of the output | | [`overlays.md`](overlays.md) | The social bar, the native alert box (six animations, alert clip, ticker) and the chat box conventions — everything composited over the frame | Adding or changing anything drawn on top of the output |
| [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid-tier history and pricing, the free-forever era, Store IAP, the Store ordering, rejected monetization and anti-piracy ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled | | [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid-tier history and pricing, the free-forever era, Store IAP, the Store ordering, rejected monetization and anti-piracy ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled |
| [`ui-surface.md`](ui-surface.md) | The control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, brand logo, launch geometry, and the theme/properties-panel conventions | Changing anything the creator sees: layout, controls, theming, properties panel | | [`ui-surface.md`](ui-surface.md) | The control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, brand logo, launch geometry, and the theme/properties-panel conventions | Changing anything the creator sees: layout, controls, theming, properties panel |
+26 -78
View File
@@ -1,7 +1,7 @@
# YouTube Live API — design constraints (do not violate) # YouTube Live API — design constraints (do not violate)
Go-live ordering and binding, the deliberate private-only lock, health parsing, end-stream Go-live ordering and binding, development/test privacy safeguards and release visibility, health
pre-flight, Test mode and chat-insert rules. Read before touching go-live, broadcasts, health, parsing, end-stream pre-flight, Test mode and chat-insert rules. Read before touching go-live, broadcasts, health,
or the chat service. The native alert box built on these events lives in or the chat service. The native alert box built on these events lives in
[`overlays.md`](overlays.md). [`overlays.md`](overlays.md).
@@ -107,77 +107,23 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
register as active server-side, which is a genuine step forward — combined with the 99MB muxed register as active server-side, which is a genuine step forward — combined with the 99MB muxed
in the earlier run, the stream half of the system is now verified healthy. The broadcast half is in the earlier run, the stream half of the system is now verified healthy. The broadcast half is
still unobserved pending a re-run. still unobserved pending a re-run.
- **Visibility picker (TASK 9 item 6) — DELIBERATE TEST-PHASE LOCK, not drift (2026-09-01).** - **Visibility policy (creator clarification 2026-11-04):** force Private during development and
The "always Private" enforcement in `YouTubeStreamService.CreateBroadcast` stays while the creator test runs so unfinished captures cannot accidentally publish. This is a safeguard for those
runs multi-month real-world testing: non-private test streams would clutter the channel with VODs environments, not a permanent product lock. The released creator flow offers Private, Unlisted,
highlighting where the product breaks. `recordFromStart`/DVR stay ON — Private VODs are invisible or Public, defaults to Private, and supports changing visibility while live. A PRIVATE badge is
to subscribers and serve as post-mortem review tapes; bulk-delete pre-GA. The unlock is a shown whenever the current broadcast is private.
deliberate final-pass item in **TASK 36 (gold pass)**, wired with the dialog selection — never - **Release visibility flow (TASK 9, Day 2):** persist a default visibility (Private initially);
opportunistic. The PRIVATE badge keeps showing when the stream is actually private. ON-AIR opens a preflight with that default selected and an explicit “Start Broadcast — [scope]”
- **⛔⛔ STOP. The always-Private lock is a DECISION, not a bug, not drift, not an open question. action. Apply the selected scope when creating a broadcast and through
(Creator ruling, restated 2026-10-04 after being asked about it AGAIN.)** `YouTubeStreamService.UpdateBroadcast` for live changes. Development/test runs stay forced Private.
`privacyStatus = "private"` hardcoded in `YouTubeStreamService.CreateBroadcast` is - **Implementation status:** current code still forces Private for all broadcasts. Do not mistake
**exactly right as written. Do not change it, do not "unlock" it, and — this is the part I keep that description of today's code for the target Release behavior; the selectable flow is queued,
getting wrong — do not raise it as a question, an observation, a suggestion, or a not implemented. `UpdateBroadcast` currently has no production caller; Day 2 wires it for live
"heads-up".** The creator has now answered this more than once. visibility changes, while development/test runs remain Private.
- **What it becomes:** the hardcoded lock is replaced by a real, user-selectable - **Test mode remains Private.** `BeginTestStream` forces `StreamVisibility = "Private"`, so retain
visibility option in the Go-Live dialog — **at v1, not before.** Not at 1.0-candidate, not that safeguard after normal Release go-live offers a choice. The existing v1 decision to hide the
at GA prep, not opportunistically during any other unit. separate TEST button remains tracked with the release flow.
- **Why it exists:** pre-v1 test streams would otherwise publish VODs of a product still - **Full broadcast form (TASK 9 item 7) — RESCOPED 2026-09-01** — the core editable fields ship (since 2026-08-24) as the always-visible **Text drawer**: title, description, tags, and made-for-kids; release visibility is the Day 2 flow above. Scheduling ships as **TASK 34** (drawer ☑ Scheduled + adoption at Start). The old **Advanced tab is permanently out of product**: latency locked `low`, DVR/record-from-start locked on, embed/projection/captions fixed at sane defaults, invisible. `categoryId` is video metadata, not a `liveBroadcast` field; category selection, tag updates through the video API, and thumbnail upload remain follow-ups. Monetization enablement (if ever needed) rides the reward-events chain via `liveBroadcasts.update`, not a form field. Go-live order (TASK 9, shipped 2026-08-16):
breaking, in public, to subscribers. Private keeps those tapes usable for post-mortem review
while staying invisible to the channel's audience.
- **Do not let these look like symptoms of the one they resemble.** All are working as designed:
`privacy=private` in the broadcast diag line; the PRIVATE badge; `recordFromStart`/DVR ON;
and the absence of any delete call in the codebase (there is no delete feature, by design at
this stage — "can we delete the test VODs" is a *retired* question, answered 2026-10-04:
`videos.delete` accepts `youtube.force-ssl` which we already hold, private is a visibility
setting not a deletion lock, and anyway a broadcast that never reaches `live` never becomes a
VOD at all).
- **`UpdateBroadcast` is NOT the unlock — and it is also currently DEAD CODE. (Fact corrected
2026-10-04: an earlier version of this note said `CreateBroadcast:112` /
`UpdateBroadcastMetadataAsync`, both wrong.)** The method that can change a live broadcast's
privacy is `YouTubeStreamService.UpdateBroadcast(broadcastId, meta)`, which sends
`privacyStatus = meta.Visibility ?? "private"`. **It has no production caller** — the only
caller in the repo is `BroadcastPullOutTests.cs:120`. So `CreateBroadcast` hardcoding private
is not merely backed up by convention, it is the *only* thing enforcing it.
- **⇒ Is a Go Live video private? YES — verified 2026-10-04, and this answers a standing
creator doubt.** Three independent hardcodes (`CreateBroadcast` → `"private"`;
`GoLiveViewModel.Visibility => "Private"`; `BeginTestStream` → `StreamVisibility = "Private"`)
plus one unreachable method. There is no path by which a broadcast this app creates can become
non-private today. The creator's actual concern — "do not populate my channel with videos
showing the app at its worst" — is fully met.
- **⛔ THE LANDMINE, and the reason the above matters:** `LiveBroadcastFormViewModel` still
exposes `VisibilityOptions = {private, unlisted, public}` and `UpdateBroadcast` would happily
honour it. **The v1 visibility option is the first thing that will ever wire `UpdateBroadcast`
up — so v1 is exactly when this guarantee ends, by the creator's own choice.** When that
happens, private stops being guaranteed and becomes a default the user can change. That is
intended at v1; it is a disaster before v1. Do not wire `UpdateBroadcast` into any
production path before v1, and do not "fix" the dead-code smell by calling it.
- **⛔ The TEST button is HIDDEN at v1 — it exists only because of the lock above (creator
ruling 2026-10-04).** This was **absent from the map entirely** until that date, which is how the
dry-run framing below came to be the only recorded one. Both are true, and the second is the
load-bearing one:
- **Its real job is being the guaranteed-private path.** `BeginTestStream` hard-sets
`StreamVisibility = "Private"` (`MainViewModel.Streaming.Operations.cs:105`), so Test can never
publish. That is *why* it exists for the whole pre-v1 phase — a dry run you cannot leak.
- **At v1 it is redundant and gets hidden**, because once visibility is a real dialog option the
user picks Private themselves and a separate "safe" button no longer earns its place. Hiding it
is part of the v1 visibility work, not a separate cleanup.
- **The pre-existing "dry run" framing is now known to be incomplete** (`ShowTestButton`'s own
comment calls it "a dry-run of the livestream"). Do not read that comment as the reason the
button must persist, and do not re-derive the button's purpose from it. If a dry run is still
wanted at v1 it must be re-justified on its own merits — that question is **open, and is for
v1, not now.**
- **It is a computed predicate, not a setting** — `ShowTestButton => ShowStreamActions &&
IsConnected` (`MainViewModel.Streaming.cs:182`), feeding both `CanStartTest` and the XAML
visibility. So the v1 hide is a one-line predicate change, not a settings migration.
- **Not a v1 blocker:** the Test route is the *same* pipeline as Go Live —
`BeginTestStream` → `StartStreamingSession(alsoRecord: false)` → `PrepareAndStartLiveAsync` —
so Test and Go Live create identical broadcasts (`boundStreamId`, `enableAutoStart`). Test
differs only in never recording (`alsoRecord: false`) and forcing Private. **This is why Test
is the correct way to run any diagnostic stream**, and why a Test result is evidence about Go
Live.
- **Full broadcast form (TASK 9 item 7) — RESCOPED 2026-09-01** — the core editable fields ship (since 2026-08-24) as the always-visible **Text drawer**: title, description, tags, visibility, made-for-kids, live-editable; scheduling ships as **TASK 34** (drawer ☑ Scheduled + adoption at Start). The old **Advanced tab is permanently out of product** (the 10% margin — see TASKS.md → "Out of product"): latency locked `low`, DVR/record-from-start locked on, embed/projection/CC/region fixed at sane defaults, invisible — every exposed field is a support ticket. `categoryId` is removed from `CreateBroadcast` (not a `liveBroadcast` field, silently ignored). Monetization enablement (if ever needed) rides the reward-events chain via `liveBroadcasts.update`, not a form field. Go-live order (TASK 9, shipped 2026-08-16):
`BeginGoLive` → `PrepareAndStartLiveAsync` — ensure the reusable stream (`GetOrCreateReusableStreamAsync`, `BeginGoLive` → `PrepareAndStartLiveAsync` — ensure the reusable stream (`GetOrCreateReusableStreamAsync`,
cache it), create the broadcast bound to it (`CreateBroadcast(..., stream.Id)` → `boundStreamId`), cache it), create the broadcast bound to it (`CreateBroadcast(..., stream.Id)` → `boundStreamId`),
THEN start the pump (the URL must exist before `FramePump.StartAsync`, which reads it once). Failure → THEN start the pump (the URL must exist before `FramePump.StartAsync`, which reads it once). Failure →
@@ -196,8 +142,10 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
polls `liveStreams.list?part=status`; render nothing on `good`/`ok`/`noData`, surface a banner only polls `liveStreams.list?part=status`; render nothing on `good`/`ok`/`noData`, surface a banner only
on `configurationIssues[]` with `warning`/`error` severity. The decision is the pure on `configurationIssues[]` with `warning`/`error` severity. The decision is the pure
`Services/StreamHealthReporter.BannerFor` (null text = no banner; error beats warning). The VM polls `Services/StreamHealthReporter.BannerFor` (null text = no banner; error beats warning). The VM polls
every **30s while live** (`DispatcherTimer` `_healthPollTimer`, first poll right after go-live) and every **2s until YouTube confirms `lifeCycleStatus=live`**, then every **30s** (`DispatcherTimer`
clears on End via `ResetHealth`; poll failures log only. **Parsing (fix 2026-09-22):** the real API `_healthPollTimer`, first poll immediately after the frame pump starts); the top bar turns green only
on that confirmed `live` state. It clears on End via `ResetHealth`; poll failures log only.
**Parsing (fix 2026-09-22):** the real API
nests `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` — reading it as nests `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` — reading it as
a string crashed every poll (`requires an element of type 'String'`, seen 2026-09-22 on both test a string crashed every poll (`requires an element of type 'String'`, seen 2026-09-22 on both test
sessions); the parse now reads the nested `.status`/`configurationIssues` and tolerates the old sessions); the parse now reads the nested `.status`/`configurationIssues` and tolerates the old
@@ -213,9 +161,9 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
mono/stereo only. YouTube flags violations via health status. mono/stereo only. YouTube flags violations via health status.
- **Broadcast ID == Video ID** — one ID tracks status, health, and the auto-created VOD - **Broadcast ID == Video ID** — one ID tracks status, health, and the auto-created VOD
(`recordFromStart` + `enableDvr` default true). (`recordFromStart` + `enableDvr` default true).
- **Test Stream mode (TASK 41, SHIPPED 2026-09-22)** — every broadcast is private-only anyway - **Test Stream mode (TASK 41, SHIPPED 2026-09-22)** — Test remains forced Private even when
(see the visibility lock above), so "Test" is a **session variant over the real go-live normal Release go-live offers a visibility choice, so "Test" is a **session variant over the real
pipeline**, not a new mode: `BeginTestStream` → `StartStreamingSession(alsoRecord:false)` go-live pipeline**, not a new mode: `BeginTestStream` → `StartStreamingSession(alsoRecord:false)`
(the dialog path was extracted into this shared boot — no Go Live dialog for a Test, no (the dialog path was extracted into this shared boot — no Go Live dialog for a Test, no
`BroadcastForm.CaptureGoLive` "last live" stamp, no recording). `IsTestStream` flips the face: `BroadcastForm.CaptureGoLive` "last live" stamp, no recording). `IsTestStream` flips the face:
gold top bar + glow (`#9c6f1c`), a **TEST** badge, and the End button reads **"End Test"**; gold top bar + glow (`#9c6f1c`), a **TEST** badge, and the End button reads **"End Test"**;