docs: v1 = feature-complete ruling — queue TASKs 32-36, close the out-of-product list, fix the map's lies
Audit of institutional knowledge lost across the refactor (creator PM session 2026-09-01): - New queue: TASK 32 resilience (blip retry/measured-grace sign/one-click back-on-air/ crash-safe fragmented-MP4 recording/pre-flight), TASK 33 auto step-down (v1 - the map claimed it existed; it didn't), TASK 34 drawer scheduling, TASK 35 scene-linked audio, TASK 36 gold pass (visibility unlock, flash-live enable, screens/layers audit, native verification suite, expiry reminders, signing/installer/EULA/Velopack, compile-flag ceiling HARDENED+MOCK_REWARDS). - 'v1 = the finished product' + closed 'Out of product' list (Stream Deck, profiles, chroma, virtual cam, replay buffer, restream, clipping, advanced-tab, bug-reporter, D3DImage preview) - the 10% margin, bounded and written. - Corrections: ffmpeg 'does reconnect' claims (input-side flags only; retry is app-side); TASK 2's orphaned 'resume deferred to TASK 3' now owned by TASK 32; TASK 2 End-signs-out superseded by TASK 18 explicit sign-out; Alerts freed from stale 'the one paid feature' language (paid = flash removal only); TASK 3 item 16 superseded; TASK 9 items 4/6/7 reconciled; TASK 10 monetization chain scoped v1; README roadmap/Structure rewritten.
This commit is contained in:
@@ -45,25 +45,28 @@ dotnet run
|
||||
|
||||
## Version Roadmap
|
||||
|
||||
| Version | Scope |
|
||||
|---------|-------|
|
||||
| v0.1 | Scene/source management, YouTube RTMP ingest, YouTube OAuth2, live chat, stream health |
|
||||
| v0.2 | Recording to local file |
|
||||
| v0.3 | Stream scheduling |
|
||||
| v0.4 | Multi-destination restreaming |
|
||||
| 1.0 | General availability |
|
||||
**v1 is the finished product** (creator ruling, 2026-09-01 — there are no v1.x releases).
|
||||
Everything still queueing lands in v1; anything not on the feature list is on the closed
|
||||
**"Out of product — permanently"** list in `TASKS.md` — read that page before asking for a feature.
|
||||
|
||||
What v1 ships: the five-scene director's control surface, webcam + screen capture, media + web +
|
||||
text sources, chat on-stream, the social bar, mic/desktop audio with auto-duck and TRAX, local
|
||||
recording, variable quality tiers with auto step-down, one-click go-live + scheduling, stream
|
||||
resilience (auto-reconnect inside YouTube's grace, one-click back on air), built-in monetization
|
||||
awareness (reward capture, session reports, YPP journey tracker), Alerts — all free and ungated;
|
||||
the subscription only removes the branding flash.
|
||||
|
||||
## Structure
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `Models/` | Scene, Source, QualityOption, StreamConfig, StreamHealth, YouTube channel/chat, Socials (social bar) |
|
||||
| `ViewModels/` | MainViewModel — scenes, stream controls, chat; GoLiveViewModel, ReuseImageViewModel, SocialsDialogViewModel |
|
||||
| `Services/` | YouTubeAuthService (OAuth2), YouTubeStreamService (broadcast/health), YouTubeChatService (chat polling), LayoutStore (SQLite), SocialValidator (social link/fediverse validation + nodeinfo icon resolution) |
|
||||
| `Helpers/` | ViewModelBase, RelayCommand, ImageCache, AppLog, FocusPreservingListBox, OAuthCredentials |
|
||||
| `Models/` | Scene, Source, WebcamSceneConfig, SceneElement, SceneCatalog, QualityOption, StreamConfig/Health, chat, Socials, BroadcastMetadata, Music, MicStatus |
|
||||
| `ViewModels/` | MainViewModel (partial classes per area) + dialogs: GoLive, CameraPicker, MicPicker, HotkeyConfig, Socials, LiveBroadcastForm |
|
||||
| `Services/` | YouTube (OAuth, streams/broadcasts, chat polling), LayoutStore (SQLite), capture (camera/screen-capture/media-file via FFmpeg), SceneCompositor + FramePump, FFmpeg encoder + RTMP, audio pipeline (WASAPI loopback + mic, mixer, filters, ducker, limiter, TRAX), SocialValidator, notifications, hotkeys, SceneGraph, ChatOverlayLayer |
|
||||
| `Helpers/` | ViewModelBase, RelayCommand, ImageCache, AppLog, TokenStore (DPAPI), OAuthCredentials, converters |
|
||||
| `Themes/` | `Controls.xaml` — the dark-theme control styles, merged once in `App.xaml` |
|
||||
| `MainWindow.xaml` | Dark-theme main UI: scene/source panel, preview, chat, status bar |
|
||||
| `SocialsDialog.xaml` | Social Media Site Promotion dialog — 6-slot social bar editor with sign-in gate and validation |
|
||||
| `MainWindow.xaml` | Dark-theme control surface: thumbnails + central preview, two-state left panel (layers/properties ↔ live chat), top bar, footer |
|
||||
| `Distribution.md` | GA-time plan: signing, installer, EULA, hardening — executed as TASK 36 |
|
||||
|
||||
## Docs (memory map)
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
> 3. ☐ — not completed / pending (empty box)
|
||||
> 4. ❌ — exception (blocked, known-issue, or deliberately excluded from this build)
|
||||
|
||||
> **2026-09-01 — v1 = feature-complete ruling:** there is no v1.x. Everything queues to v1 or to
|
||||
> **"Out of product — permanently"** (file end). Read those two sections before adding or reviving
|
||||
> anything here.
|
||||
|
||||
## YouTube Live API — research facts (authoritative, v3 build)
|
||||
|
||||
Lifecycle: `created → ready → [testing] → live → complete` (transitional `liveStarting` / `testStarting`).
|
||||
@@ -22,7 +26,7 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
||||
### Two features that reshape the design
|
||||
|
||||
1. **enableAutoStart / enableAutoStop** — instant one-click go-live, no transition call. With `enableAutoStart=true` we never call `transition(live)`: the broadcast auto-goes-live the moment the encoder starts. Combined with `enableMonitorStream=false` (our preview pane replaces YouTube's monitor stream — the thing that forces a testing stage), the flow is **create → bind → Start Stream → encoder starts → YouTube brings it live**. No testing, no transition polling, no liveStarting stuck-state handling.
|
||||
2. **`cdn.resolution=variable` / `cdn.frameRate=variable`** — free auto step-down. YouTube auto-detects what we send; since we ARE the encoder we can drop bitrate/resolution on the fly with zero API calls. Declaring an explicit resolution instead (e.g. 1080p) requires a new stream, which can't happen mid-broadcast. Variable is the enabler for the whole auto step-down feature.
|
||||
2. **`cdn.resolution=variable` / `cdn.frameRate=variable`** — free auto step-down. YouTube auto-detects what we send; since we ARE the encoder we can drop bitrate/resolution on the fly with zero API calls. Declaring an explicit resolution instead (e.g. 1080p) requires a new stream, which can't happen mid-broadcast. Variable is the enabler for the whole auto step-down feature. **(Claim-check 2026-09-01: the enabler is real, the governor is not built — the drop-side policy ships as TASK 33, v1 per the complete-v1 ruling.)**
|
||||
|
||||
### Compliance gotchas (maps perfectly to report-by-exception)
|
||||
|
||||
@@ -66,7 +70,7 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
||||
2. ✅ Real OAuth2 wiring — baked-in Google credentials (desktop client; loopback callback) + `YouTubeAuthService` complete: browser launch, `HttpListener` callback, token exchange, refresh, channel fetch
|
||||
3. ✅ Token persistence via Windows DPAPI (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`), best-effort reload + proactive refresh at startup, saved after every exchange/refresh
|
||||
4. ✅ Account sign-in/change surfaced in the GoLive dialog (saved account shown with "Change Account"; "Sign in to YouTube" when none; Start disabled until signed in)
|
||||
5. ✅ **End Livestream signs out** — a graceful end completes the session: `StopStream()` calls `YouTubeAuthService.ClearSession()` + `TokenStore.Clear()` + `IsConnected = false`, so the next Start Stream dialog requires a fresh sign-in. A crash never runs End, so the DPAPI token survives and the creator stays signed in. Resume/reconnect after a midstream crash is deliberately deferred to TASK 3: the socket can't be resumed (it dies with the process), so "resume" = fast reconnect with a saved broadcast ID/stream key within YouTube's disconnect-grace window; too slow and `enableAutoStop` ends the broadcast
|
||||
5. ✅ ~~**End Livestream signs out**~~ **SUPERSEDED by TASK 18 (2026-08-29): explicit sign-out only** — `StopStream()` no longer clears the session (sign out via Logout / Change Account, `SignOutYouTubeAsync`); stopping a stream or recording leaves the creator signed in. Originally shipped as: a graceful end completes the session: `StopStream()` calls `YouTubeAuthService.ClearSession()` + `TokenStore.Clear()` + `IsConnected = false`, so the next Start Stream dialog requires a fresh sign-in. A crash never runs End, so the DPAPI token survives and the creator stays signed in. Resume/reconnect after a midstream crash is deliberately deferred to TASK 3: the socket can't be resumed (it dies with the process), so "resume" = fast reconnect with a saved broadcast ID/stream key within YouTube's disconnect-grace window; too slow and `enableAutoStop` ends the broadcast. **Orphan closed 2026-09-01:** this mechanism is owned by **TASK 32 (Stream resilience)** — in-session blip retry inside the grace window; cross-process "resume" of a dead broadcast is officially out-of-product (the API makes it impossible — creator ruling).
|
||||
6. ✅ Tests in `ytLive.Tests` (xUnit, net8.0-windows): TokenStore DPAPI roundtrip/corrupt/missing/clear + mocked exchange channel-parse + refresh expiry bump + `ClearSession` — 7 passing
|
||||
|
||||
**Design constraint:** Sign-in must NEVER block core exploration. Users can build scenes, add sources, and audition the software without authenticating. But **going live requires authentication** — the "Start Stream" dialog is where the account sign-in lives, alongside all stream metadata.
|
||||
@@ -115,11 +119,11 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
||||
13. ✅ The connected YouTube account's avatar/name shows in the top bar next to Start Stream (`SyncConnectedAccount`); the scenes list is content-height now (no dead space before SOURCES)
|
||||
14. ✅ **Social bar v2 (six-slot dialog, sign-in gate, real logos)** — global bar layer (never a Source, no sources-list row), content-sized, centered, GREEN glow when ON, top/bottom snap-drag (default BOTTOM, persisted `SocialBarPosition`; drag clamps to 0/1040, tie→bottom). Footer Social button gets a state dot (green=ON). Dialog "Social Media Site Promotion" (`SocialsDialog` + `ViewModels/SocialsDialogViewModel`, WPF-free + injected `ISocialValidator`/sign-in/sign-out fakes): ON/OFF bar switch (`SocialsConfig.BarEnabled`, schema v8), 6 fixed slots — row 1 always YouTube (signed-in → channel handle; signed-out → sign-in gate → OAuth; delete → confirm sign-out, mirrors `StopStream`), row 2 free, rows 3-6 lock icons on freemium (Premium seam: all six). Validation: `DetectService` (URL domain / fediverse `@user@domain` / bare→Website) → async `ISocialValidator` on confirm/Save; valid snaps to text + real service logo (bundled SVG path data via `LogoDataFor`, Simple Icons CC0 — initials badges gone); invalid → red do-not, stays editable, Save blocked. LCR justify dropped (`BarJustify` unread), per-scene toggle dropped (`Scene.HasSocialBar` back-compat). **Post-test fixes (2026-08-12):** footer label "Socials" (not "Social"); fediverse `@user@domain` validates — the full handle is the identity end-to-end (`DetectService`/`CanonicalUrlFor`/`HttpSocialValidator` build `https://domain/@user`, no domain loss); **Cancel is a hard stop** — `ISocialValidator.LookupAsync` takes a `CancellationToken`, dialog VM owns a CTS, Cancel/X/Save abort in-flight lookups (HTTP request killed, canceled continuations never touch slot state), and `ConfirmEdit` skips re-submitting identical text (LostFocus on dismiss never re-fires a lookup). `HttpSocialValidator` now has real tests (fake `HttpMessageHandler`). 105 tests passing. **Post-test fixes (2026-08-12, round 2):** fediverse `@user@domain` no longer shows a generic chain — it resolves to the instance's actual software via nodeinfo (`/.well-known/nodeinfo` → `software.name`; `SocialService.Fediverse` enum member + `SocialEntry.FediverseSoftware` persisted in a new `SocialEntry.Software` column, schema migration by column-presence) and renders that software's bundled logo (`LogoDataForFediverse`: mastodon/peertube/pixelfed/misskey/lemmy/pleroma/firefish, generic fediverse honeycomb fallback). Dialog row-2 edit/trash icons were too dark — `IconButton` style gains `Foreground=#d0d0d0`; trash overrides `#e94560` (app red). 112 tests passing. **Post-test fixes (2026-08-12, round 3):** a fediverse handle whose identity domain is itself a redirect (e.g. YunoHost default-app subdomains — `@user@llamachile.tube` where the mastodon instance lives at `mastodon.llamachile.tube`) now still resolves its software: nodeinfo on the identity domain is SSO-blocked, so `HttpSocialValidator` follows the bare root `https://domain/` 302 to the real instance host and re-runs the nodeinfo lookup there.
|
||||
15. ✅ **Window capture** — absorbed into the Screen picker (no separate source type); dedicated window-as-source work is pending
|
||||
16. ☐ **Scene compositing** — the D3DImage/MediaElement preview compositor (this task's requirement 5; the output compositor ships as TASK 4 ship step 1)
|
||||
16. ❌ **Scene compositing (D3DImage/MediaElement)** — SUPERSEDED, permanently out (2026-09-01): the software output compositor (TASK 4 step 1) + the XAML preview ARE the design; a D3D11 swap stays a possibility behind the `VideoFrame` seam, not a feature
|
||||
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; **the one paid feature** (see Monetization in `ai.md`). **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 / 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.
|
||||
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.
|
||||
@@ -147,9 +151,9 @@ the hot few and nothing esoteric. If a user needs more, they've graduated to OBS
|
||||
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. **Also the one
|
||||
paid feature** — see Monetization in `ai.md`. Consumes the canonical `RewardEvent` feed (reward-event
|
||||
capture, TASK 10 → related work).
|
||||
(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).
|
||||
|
||||
Deliberately NOT supported: game capture, browser source, media playlist, VLC, color-key voodoo, MIDI.
|
||||
|
||||
@@ -234,7 +238,7 @@ until a frame source exists, so the compositor is ship step 1. The pipeline is
|
||||
### Requirements:
|
||||
|
||||
1. **Encoding** — H.264 (hardware via NVENC/AMD, fallback x264) + AAC audio; **must comply**: keyframes ≤ 4s (gopSizeLong), closed GOP, AAC/MP3 @ 44.1/48kHz, mono/stereo only. **License posture (decided): GPL-free build** — NVENC (NVIDIA) / QSV (Intel) / AMF (AMD) + OpenH264 software fallback + built-in AAC; no libx264 (GPL contaminates a paid product). Output containers are identical either way (H.264+AAC in `.flv` for RTMP, `.mp4`/`.ts` for VOD) — the format is NOT the differentiator, the license and per-GPU quality are. **License guardrails (never violate — see `ai.md` → "Licensing — do not violate"):** only BtbN `lgpl`/`lgpl-shared` builds; never GPL (gyan.dev) or `nonfree` (fdk-aac); never static for distribution (LGPL §6 relink material); never link FFmpeg into the app; never drop `THIRD-PARTY-NOTICES.txt` from the app/About screen.
|
||||
2. **RTMP push** — **FFmpeg subprocess (decided)**: app feeds raw frames via stdin, parses stderr for health; one battle-tested binary does encode + FLV mux + push + reconnect. **Binary distribution (decided): check-then-pull** — probe `where ffmpeg`/PATH at first go-live; if absent, download a **pinned** build (**BtbN LGPL-shared win64** zip, ~75 MB — gyan.dev's builds are GPLv3 and ship libx264, which violates the license posture; BtbN's LGPL variant drops x264/x265 while keeping NVENC/QSV/AMF + libopenh264 + native AAC) to `%APPDATA%\ytLlive\tools\ffmpeg.exe` (extract `ffmpeg.exe` **plus the `libav*.dll` family**) and cache it, offline-friendly. Behind an `IFfmpegLocator` seam so tests fake it (ship step 2, below). Push goes to the cached reusable stream's ingestion URL
|
||||
2. **RTMP push** — **FFmpeg subprocess (decided)**: app feeds raw frames via stdin, parses stderr for health; one battle-tested binary does encode + FLV mux + push. **No self-heal (claim-check 2026-09-01): ffmpeg's reconnect flags are input-side; an RTMP *output* push does not reconnect itself — retry is app-side, owned by TASK 32.** **Binary distribution (decided): check-then-pull** — probe `where ffmpeg`/PATH at first go-live; if absent, download a **pinned** build (**BtbN LGPL-shared win64** zip, ~75 MB — gyan.dev's builds are GPLv3 and ship libx264, which violates the license posture; BtbN's LGPL variant drops x264/x265 while keeping NVENC/QSV/AMF + libopenh264 + native AAC) to `%APPDATA%\ytLlive\tools\ffmpeg.exe` (extract `ffmpeg.exe` **plus the `libav*.dll` family**) and cache it, offline-friendly. Behind an `IFfmpegLocator` seam so tests fake it (ship step 2, below). Push goes to the cached reusable stream's ingestion URL
|
||||
3. **Quality ladder** — the offered tiers, with **1080p60 @ 8 Mbps as the standard/default**:
|
||||
1. 720p30 @ 6 Mbps
|
||||
2. 720p60 @ 6 Mbps
|
||||
@@ -384,7 +388,7 @@ cache refresh, empty payload, missing zip entry, downloader failure) — **78 pa
|
||||
#### Ship step 3 — Encoder + RTMP push (the FFmpeg subprocess)
|
||||
|
||||
**Goal:** encode raw BGRA master frames into H.264+AAC FLV and push them to the reusable stream's RTMP
|
||||
ingestion URL — one battle-tested subprocess doing encode + mux + push + reconnect, the app feeding
|
||||
ingestion URL — one battle-tested subprocess doing encode + mux + push (retry is app-side, TASK 32 — ffmpeg does not self-reconnect an RTMP output), the app feeding
|
||||
frames via stdin and parsing stderr for health (req 2).
|
||||
|
||||
**Decisions (locked):** the encoder is a thin orchestrator over `ffmpeg.exe` — no H.264/AAC code in the
|
||||
@@ -672,15 +676,15 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash),
|
||||
|
||||
**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); items 4–7 still open (each its own branch/PR)
|
||||
### 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
|
||||
|
||||
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)
|
||||
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
|
||||
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`
|
||||
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.
|
||||
7. ☐ **Full broadcast form** — expose all YouTube API-supported fields in the go-live dialog. Core tab: title, description, visibility, made-for-kids, schedule (start + optional end). Advanced tab (expandable, sane defaults): latency (Normal/Low/Ultra-Low), DVR, embed, record-from-start, projection (rectangular/360°), closed captions, auto-start, auto-stop, monitor stream, region restrictions. Monetization via `liveBroadcasts.update` (insert-only on that resource) — separate step after broadcast creation. Remove unsupported `categoryId` (not a `liveBroadcast` field, silently ignored)
|
||||
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
|
||||
|
||||
### Design decisions (v3)
|
||||
|
||||
@@ -719,6 +723,8 @@ in-app replacement for the emailed "stream activity report"), and a journey trac
|
||||
ETA vs. versioned, date-aware YPP thresholds; the Tier-2 bar doubles for new applicants 2027-02-01).
|
||||
Details and design notes: `ai.md` → Monetization. This is tracked as related work under this task's
|
||||
narrative, not a separate task number — it ships in its own code slices with one integration test each.
|
||||
**Scope: v1** (2026-09-01 complete-v1 ruling — there is no v1.x): slice order = reward-event capture →
|
||||
session report → journey tracker → Alerts (TASK 3 item 20).
|
||||
|
||||
**Business details (pricing, Polar product/checkout/discounts) in `MONETIZATION.md` (gitignored).**
|
||||
|
||||
@@ -995,10 +1001,16 @@ The tasks below are ordered by dependency and risk. Each task builds on the prev
|
||||
5b. ✅ **Broadcast metadata pull-out + launch geometry** — shipped 2026-08-24 (row 29 above). Live-screen "Text" tab → broadcast form, persistence + remote update; window default/minimums bumped (1920×1040 / 1366×768) with size+position restore.
|
||||
6. ✅ **TASK 17** — Web source (WebView2) — shipped 2026-08-28; enables alert ecosystem.
|
||||
7. ✅ **TASK 18** — Local recording — shipped 2026-08-29 (running-app verification pending in handoff).
|
||||
8. **TASK 21** — Media source — video file playback for non-Live scenes.
|
||||
9. **TASK 22** — Audio sync offset — small, quality-of-life.
|
||||
8. 🔶 **TASK 21** — Media source — video file playback for non-Live scenes. **Remaining: the UI picker slice** (handoff spec in HANDOFF.md).
|
||||
9. ✅ **TASK 22** — Audio sync offset — shipped 2026-08-31 (status reconciled 2026-09-01).
|
||||
10. ✅ **TASK 15** — Stock bg images — all 5 scenes seeded (Starting, BRB, Live, Chat, Ending). `EnsureDefaultBackdrop()` runs on every scene switch.
|
||||
11. **TASK 13** — Social media launch kit — after all v1 features ship.
|
||||
11. **TASK 32** — Stream resilience (blip retry → grace countdown on the ON-AIR sign → one-click Back on air → crash-safe recording → pre-flight).
|
||||
12. **TASK 33** — Bandwidth auto step-down (depends on TASK 32's restart machinery).
|
||||
13. **TASK 34** — Scheduled streams (Text-drawer schedule + adoption at Start).
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -1395,16 +1407,172 @@ dynamic-only pixel change does not.
|
||||
|
||||
---
|
||||
|
||||
## Backlog (future versions)
|
||||
## TASK 32 — Stream resilience (queued 2026-09-01 — merges the orphaned TASK 2 resume clause into the pills/signs architecture)
|
||||
|
||||
1. v1.1 — Stream Deck / Loupedeck integration (requires hotkey foundation from TASK 20)
|
||||
2. v1.1 — Per-source audio sync offset (global offset ships in TASK 22)
|
||||
3. v1.1 — Multiple profiles/presets (save different configs for different stream types)
|
||||
4. v1.1 — Chroma key filter (green screen removal, or ONNX background removal)
|
||||
5. v1.1 — Virtual camera output (Zoom/Discord/Teams)
|
||||
6. v1.1 — Replay buffer (instant replay with hotkey)
|
||||
7. v2 — Multi-destination restreaming (if needed; casual streamers may outgrow LlamaCasty first)
|
||||
8. v2 — Stream clipping
|
||||
9. v2 — Export/import settings
|
||||
**Goal:** a dropped stream heals itself when physically possible, tells the truth when it isn't, and
|
||||
gets the creator back on air in one click. The ON-AIR sign + primary button in the top bar are the
|
||||
surface — they already mean "reality" (pills = intent, lights = reality, TASK 18/30); reality just
|
||||
isn't two-state. Resumption of a completed broadcast is explicitly **out of scope** (impossible via
|
||||
the API — creator ruling 2026-09-01): no lying in the copy.
|
||||
|
||||
**Slices (one integration test each, in order):**
|
||||
|
||||
1. ☐ **Blip retry** — app-side encoder restart: push dies while live (ProcessFailed / stdin
|
||||
backpressure death) → bounded-backoff relaunch (2s/5s/10s, N attempts) against the cached
|
||||
constant reusable-stream ingest URL. Success = push running again while the broadcast is still
|
||||
live; viewers never know. Fixes the map's stale "ffmpeg does reconnect" claim (corrected
|
||||
2026-09-01: reconnect flags are input-side only). Test: fake process fails once then starts →
|
||||
pump recovers without surfacing to the VM.
|
||||
2. ☐ **Measured grace + the sign** — while the push is dead, escalate the existing health poll
|
||||
(30s → ~5s). ON-AIR dot goes **amber "RECONNECTING"** with a mm:ss countdown to the observed
|
||||
YouTube cutoff; the grace length is a **measured, pinned constant** — probe it on a real private
|
||||
test stream, cite the observation (spin-guard rule; do NOT invent a number). Poll reports the
|
||||
broadcast `complete` → dot **red "OFF AIR"**, primary button relabels **"● Back on air"** and
|
||||
pulses. Test: fake status provider drives the dot through live → reconnecting → cut-off.
|
||||
3. ☐ **One-click Back on air** — the relabeled button fires the existing go-live path with saved
|
||||
`Broadcast.*` + cached stream, dialog skipped: new broadcast, new VOD (accepted cost, stated
|
||||
in-product). Test: seeded saved form + fake stream service → click creates + pushes, no dialog.
|
||||
4. ☐ **Crash-safe recording** — fragmented MP4 on the record block (`-movflags
|
||||
frag_keyframe+empty_moov+default_base_moof`): plain MP4's trailing moov atom means a power loss
|
||||
kills the WHOLE file. Rename-on-stop unchanged. Closes the TASK 18 "MKV as an option" design-note
|
||||
remnant (fragmented MP4 chosen over MKV: single container, YouTube-upload-native — verify player/
|
||||
editor tolerance, cite the research). Test: `FfmpegArgs` emits the movflags only on the record block.
|
||||
5. ☐ **Pre-flight** — before broadcast insert, one TCP probe to the ingest host:1935 (~2s timeout).
|
||||
Unreachable → Error toast, no insert, no ghost broadcast ("check VPN/network"). The "forgot the
|
||||
VPN" story, solved as prevention instead of recovery. Test: fake probe failure aborts go-live before any API call.
|
||||
|
||||
### Design decisions
|
||||
|
||||
- **The sign is the status, the button is the action** — no new chrome, no log lines, no spinners.
|
||||
- **Honest windows:** countdown = observed grace; at zero we say OFF AIR, we don't pretend to reconnect.
|
||||
- **Why us > incumbents:** constant reusable stream + persisted form make retry/restart cheap; OBS
|
||||
shows a reconnecting log line and Streamlabs silently dies.
|
||||
|
||||
---
|
||||
|
||||
## TASK 33 — Bandwidth auto step-down (queued 2026-09-01; v1 per the complete-v1 ruling)
|
||||
|
||||
**Goal:** when the upload can't hold the tier, drop it automatically instead of drowning viewers in
|
||||
freeze frames. The `variable` reusable stream (shipped) makes this zero-API; the governor that decides
|
||||
WHEN is what TASKS.md:25 always claimed existed and was never built.
|
||||
|
||||
1. ☐ Pure `StepDownPolicy` — sliding window over the encoder's reported dropped-frame rate
|
||||
(`StreamHealth`, already parsed): sustained overrun → downgrade exactly one 16:9 tier
|
||||
(1080p60 → 1080p30 → 720p60 → 720p30). Hysteresis cooldown; **one-way within a broadcast**
|
||||
(no oscillation — a recovered connection doesn't climb back mid-show; the creator may manually
|
||||
step up). Vertical tier stays a manual choice.
|
||||
2. ☐ Restart machinery = TASK 32 slice 1 (tier change is an encoder relaunch at new W×H/FPS over the
|
||||
same pipe/URL; the master canvas is 1920×1080 regardless, so geometry never rewrites).
|
||||
3. ☐ Surface: the resolution badge + dropdown update to the landed tier; ONE Info toast
|
||||
("Quality lowered to hold your stream") — no nagging after that.
|
||||
4. ☐ Tests: pure-policy units (drop-rate windows, one-way, cooldown) + the ONE integration: fake
|
||||
health stream with sustained drops → exactly one lower-tier encoder restart.
|
||||
|
||||
---
|
||||
|
||||
## TASK 34 — Scheduled streams, Text-drawer version (queued 2026-09-01; option (b) with the creator's placement)
|
||||
|
||||
**Goal:** announce "Friday 8pm" from inside the app — YouTube notifies subscribers and runs the
|
||||
channel countdown; come Friday, one click goes live into the scheduled broadcast. Scheduling is a
|
||||
"my channel" concern, so it lives in the always-visible **Text drawer**, not the go-live modal.
|
||||
|
||||
1. ☐ Drawer gains ☑ *Scheduled livestream* + date/time (replacing the read-only "Scheduled Start"
|
||||
row). Checking + Save **creates** the broadcast: insert `liveBroadcasts` with a future
|
||||
`scheduledStartTime`, bound to the cached reusable stream — the drawer's Save path grows its first
|
||||
create operation (today it can only update an existing broadcast).
|
||||
2. ☐ Invariant: **at most one open scheduled broadcast**; unchecking before airtime →
|
||||
`liveBroadcasts.delete` (legal in created/ready).
|
||||
3. ☐ **Adoption:** go-live checks for the open scheduled broadcast and pushes into it instead of
|
||||
inserting a new one. Mid-stream title/desc edits ride the existing Update path (unchanged).
|
||||
4. ☐ `LiveBroadcastFormViewModel` owns `IsScheduled` + `ScheduledStart` (nullable), persisted with the
|
||||
other `Broadcast.*` keys; local time in, RFC3339 offset on the wire — the timezone edge is where the
|
||||
bugs will live: test it (DST boundary, non-UTC creator).
|
||||
5. ☐ **Research flags (cite before building — spin guard):** (a) pushing *before* the scheduled start
|
||||
with `enableAutoStart` — live immediately or held? Decides a "starting early — it goes live NOW"
|
||||
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
|
||||
+ boundStreamId); start → adopts instead of inserting; uncheck → delete called.
|
||||
- **Test-phase note:** dark by design while the visibility lock holds (Private notifies nobody); the
|
||||
creator can still exercise insert/adopt/cancel. Full shine arrives with TASK 36's unlock — pleasing
|
||||
coupling, deliberate.
|
||||
|
||||
---
|
||||
|
||||
## TASK 35 — Scene-linked audio: "scenes remember the room" (queued 2026-09-01; spec approved by the creator same day)
|
||||
|
||||
**Goal:** the BRB scene mutes the mic; the Chat scene brings it back. The creator stops fiddling with
|
||||
audio at transitions — the scene carries the room state. OBS needs scripts/plugins; native = the
|
||||
creator-proof pitch.
|
||||
|
||||
1. ☐ `Scene` gains nullable `SceneMutesMic` / `SceneMutesDesktop` — `null` = don't touch (the default;
|
||||
scenes are passive until told). Schema column bump + roundtrip.
|
||||
2. ☐ Applied on **live transitions only** (thumbnail click / hotkey), ONCE at entry — never on
|
||||
offline staging (no surprise stream-state edits while auditioning).
|
||||
3. ☐ The creator is boss: a manual mute/unmute mid-scene is never overridden until the next entry.
|
||||
4. ☐ BRB ships `SceneMutesMic = true`; other scenes default `null` — creator's choice.
|
||||
5. ☐ Edit UI: two checkboxes in the scene's Properties section (left panel, edit mode) — no new chrome.
|
||||
6. ☐ Test (the ONE): transition applies scene state; `null` leaves state untouched; manual override
|
||||
survives until the next entry.
|
||||
|
||||
---
|
||||
|
||||
## TASK 36 — Gold pass (queued 2026-09-01; the going-gate, always the last work unit)
|
||||
|
||||
**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.
|
||||
|
||||
1. ☐ **Visibility unlock** — remove the `CreateBroadcast` Private override (TASK 9 item 6); dialog
|
||||
selection rules. The channel-protection stance ends at GA, by hand, here.
|
||||
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
|
||||
ONLY paid delta. (Escalation curve finalized here, first.)
|
||||
3. ☐ **Screens/layers settings audit** — the retired 2026-08-22 landmine (ai.md): fine-tooth-comb
|
||||
every screen's Background layer properties, pill persistence, context-menu visibility, (+) items.
|
||||
4. ☐ **Native-Windows verification suite** — the checklist TASKS/HANDOFF never had: real webcam,
|
||||
real mp4 decode/loop/pacing (TASK 21 picker must have landed), real recording file opens +
|
||||
plays (fragmented MP4), a real private go-live end-to-end, GUI/RealApp test classes green on
|
||||
the desktop (they've only ever run against fakes).
|
||||
5. ☐ **License-expiry reminders** — T-14 / T-7 / T-1 toasts via the existing notification stack
|
||||
("renews <date> — manage here" → `PremiumUrl`); lapsed: flash returns (the product itself is the
|
||||
notice) + one Info toast; active paid users see ZERO license UI. Research: does Polar's
|
||||
validate response carry `expires_at` reliably — cite before building.
|
||||
6. ☐ **Release engineering** (the `Distribution.md` plan, 644 lines, finally tasked): code signing
|
||||
(HARD blocker — an unsigned installer is a SmartScreen scarewall), installer + Velopack update URL
|
||||
(TASK 10 item 8), EULA draft/review, THIRD-PARTY-NOTICES license-texts gate (TASK 4 req 9).
|
||||
Build posture (agreed ceiling): compile flags max out at `HARDENED` (release-only anti-debug/
|
||||
tamper checks) + `MOCK_REWARDS` (fake reward payloads for Alerts/dev) — **additive-only**, never
|
||||
branching normal code paths, both compiled every CI build so they can't rot. Obfuscation /
|
||||
assembly-splitting stay GA-time decisions, not compile-time ones.
|
||||
|
||||
---
|
||||
|
||||
## v1 = the finished product (creator ruling, 2026-09-01)
|
||||
|
||||
**There is no v1.x.** v1 ships feature-complete: everything in the queue above lands, or is
|
||||
explicitly buried below. The parking lot is closed — nothing is "deferred to a later version"
|
||||
anymore; the only two states are **v1 task** and **out of product, permanently**. The 10% margin of
|
||||
error is this list, bounded and written down — not vibes.
|
||||
|
||||
**Monetization posture restated (what "final" means):** free gives everything; the only difference
|
||||
is the branding flash. License activation flips exactly one bit (`IsPremium` → flash off). Nagware =
|
||||
the escalating obnoxious self-promo flash itself — never modals, never feature-locks. Pre-GA the
|
||||
flash is preview-only (see ai.md → Monetization; TASK 36 flips it live).
|
||||
|
||||
## Out of product — permanently (the 10% margin, closed list — do not re-litigate)
|
||||
|
||||
| Idea | Verdict & reason |
|
||||
|------|------------------|
|
||||
| Stream Deck / Loupedeck integration | The device sends keystrokes — **global hotkeys already ARE the integration** (TASK 20). Redundant, not deferred. |
|
||||
| Per-source audio sync offset | Global offset shipped (TASK 22); per-source is the OBS mixer rabbit hole. Condemned by the Audio Assumption (one knob). |
|
||||
| Profiles / presets (multi-config) | Same hydra as multi-layout, already ruled at TASK 30: **that's what OBS is for.** |
|
||||
| Chroma key / background removal | Already ❌ (TASK 3 item 19). NVIDIA Broadcast does it free; model+GPU+lighting caveats = support hell for a solo dev. Stays buried. |
|
||||
| Virtual camera output (Zoom/Discord) | Different product, device-driver swamp. Our output is one YouTube stream. |
|
||||
| Replay buffer (instant replay) | Game-clipping culture, not our persona. **The most valuable corpse here** — if the list is ever reopened at all, this is candidate #1, and it reopens by creator ruling, not by AI suggestion. |
|
||||
| Multi-destination restreaming | "Casual streamers outgrow LlamaCasty first" — the map already said it; the per-destination audio/monitoring expectations are a different product. |
|
||||
| Stream clipping | YouTube killed clips; trimming local MP4 is an editor. |
|
||||
| Advanced broadcast-form tab (latency/DVR/embed/projection/CC/region) | TASK 9 item 7 rescoped: fixed sane defaults, invisible. Every exposed field is a support ticket. |
|
||||
| In-app bug-report mechanism | Never built, never will be: support = email + GitHub issues. (ai.md's support line corrected to match, 2026-09-01. Second-place corpse if the list ever reopens: with replay buffer.) |
|
||||
| D3DImage/GPU preview compositor | TASK 3 item 16 superseded: XAML preview + software output compositor are the design; a D3D11 swap remains a seam-respecting possibility, not a feature. |
|
||||
|
||||
*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.*
|
||||
|
||||
@@ -186,7 +186,7 @@ The AI hallucinated through multiple commits that night on background/scene prop
|
||||
|
||||
### Current limitations / TODOs
|
||||
|
||||
- `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). A **graceful End Livestream signs out**: `StopStream()` clears the session + token, so the next go-live needs a fresh sign-in; a crash never runs End, so the token survives and the creator stays signed in. `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` callback (test seam + save hook; services are still constructed in `MainViewModel`)
|
||||
- `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). Sign-out is **explicit only** (Logout / Change Account → `SignOutYouTubeAsync`): `StopStream()` does NOT clear the session — TASK 18's 2026-08-29 reversal ("stopping a recording leaves the creator signed in") — the older "graceful End signs out" line here was stale and is corrected against the code (2026-09-01). `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` callback (test seam + save hook; services are still constructed in `MainViewModel`)
|
||||
- Scene/source/asset layout + the social bar persist (SQLite, schema v8); the OAuth session persists (DPAPI); the paid-unlock state persists (LayoutStore Settings table — `LicenseKey`/`LicenseValidatedAt`/`IsPremium`, 14-day offline grace)
|
||||
- `YouTubeStreamService` manages the **variable reusable stream** (shipped 2026-08-16): `GetOrCreateReusableStreamAsync` lists `liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, creating it only on first use (`resolution=variable`, `frameRate=variable`); the stream is cached via `LayoutStore` (`SaveReusableStream`/`LoadReusableStream`, Settings table) and bound at broadcast insert (`boundStreamId`). Health (shipped 2026-08-16): `GetStreamHealthAsync(streamId)` polls `liveStreams?part=status` for `healthStatus` + `configurationIssues[]` → `StreamHealth`; banner decision in pure `StreamHealthReporter`
|
||||
- Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is SHIPPED**, **the FFmpeg locator (TASK 4 ship step 2) is SHIPPED**, **the encoder + RTMP push (TASK 4 ship step 3) is SHIPPED**, **WASAPI audio capture (TASK 4 ship step 4) is SHIPPED**, **frame-pipeline wiring (TASK 4 ship step 5) is SHIPPED**, **health stats (TASK 4 ship step 6) is SHIPPED**, **one-click go-live + private-only enforcement (TASK 4 ship step 7) is SHIPPED** — TASK 4 (RTMP Ingest) is fully done; full plan in `TASKS.md`
|
||||
@@ -870,11 +870,14 @@ crooked.
|
||||
Product: `d105dfa1-497e-423b-8cd4-e0ee2e3abbc0`. Checkout: `llamacasty.com` → Polar hosted page.
|
||||
Org ID: `c05fb364-b967-4f6c-adf2-8a144e46d085` (org-scoped OAT — `organization_id` omitted from API calls).
|
||||
Key prefix: `LCYT-`. Discounts: `LLAMAFOUNDER` (100% off, 50 uses), `LLAMA50` (50% off, 12 months). Details in `MONETIZATION.md`.
|
||||
- **Support (creator's model):** in-app bug-reporting mechanism → issues into git; most queries are
|
||||
- **Support (creator's model, corrected 2026-09-01):** support = email + GitHub issues — the
|
||||
once-planned in-app bug-reporter is **out of product** (TASKS.md → closed list). Most queries are
|
||||
how-tos / feature requests / manual-skimmers. Maintenance cadence = "when I get around to it" with
|
||||
emergency patches; not a 24/7 service promise.
|
||||
emergency patches; not a 24/7 service promise. The only license chatter is the paid-user expiry
|
||||
reminder (TASK 36 item 5); active subscribers see zero license UI.
|
||||
|
||||
**Monetization awareness (built-in, ungated, free — 2026-09-01):** the app is monetization-aware by
|
||||
**Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1
|
||||
ruling — build order: capture → report → journey → Alerts):** the app is monetization-aware by
|
||||
design, for **every** creator, free and ungated (the only subscriber swap remains the branding-flash
|
||||
removal above). It has three layers, all free — this is the product's differentiator, not a paid tier:
|
||||
|
||||
@@ -904,8 +907,9 @@ removal above). It has three layers, all free — this is the product's differen
|
||||
**What was rejected:** always-on watermark (obscurable — replaced by the flash), hard stream-time
|
||||
cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
|
||||
routinely run 2-4 hours), soft-limit nagging, freemium feature tiers, and donation-only (relies on
|
||||
the kindness of strangers). Resolution/quality ceilings are **deferred** — that decision belongs to
|
||||
the resolution & streaming-constraints conversation, not monetization.
|
||||
the kindness of strangers). Resolution/quality ceilings stay rejected (fixed 2026-09-01, no longer
|
||||
"deferred"): the free tier never loses quality — auto step-down (**TASK 33**) is a *protect the
|
||||
stream* feature, never a monetization penalty.
|
||||
|
||||
## Auth gates Go Live, but not exploration
|
||||
|
||||
@@ -940,16 +944,7 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
to subscribers and serve as post-mortem review tapes; bulk-delete pre-GA. The unlock is a
|
||||
deliberate final-pass item in **TASK 36 (gold pass)**, wired with the dialog selection — never
|
||||
opportunistic. The PRIVATE badge keeps showing when the stream is actually private.
|
||||
- **Full broadcast form (TASK 9 item 7)** — the go-live dialog exposes all YouTube API-supported fields.
|
||||
**Core tab** (always visible): title, description, visibility (Private/Unlisted/Public), made-for-kids,
|
||||
schedule (start + optional end datetime). **Advanced tab** (expandable, sane defaults): latency
|
||||
(Normal/Low/Ultra-Low, default Low), DVR (default on), embed (default on), record-from-start
|
||||
(default on), projection (rectangular/360°, default rectangular), closed captions
|
||||
(disabled/embedded/HTTP, default disabled), auto-start (default on), auto-stop (default on),
|
||||
monitor stream (default off, for testing), region restrictions (country codes, optional).
|
||||
`categoryId` is removed from `CreateBroadcast` (not a `liveBroadcast` field, silently ignored).
|
||||
Monetization via `liveBroadcasts.update` (not settable on insert) — separate step after broadcast
|
||||
creation. Go-live order (TASK 9, shipped 2026-08-16):
|
||||
- **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`,
|
||||
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 →
|
||||
@@ -959,7 +954,9 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
(`cdn.resolution=variable`, `cdn.frameRate=variable`, `isReusable=true`) only on first use; the
|
||||
ingestion URL is cached via `LayoutStore` Settings (`SaveReusableStream`/`LoadReusableStream`) and
|
||||
bound to each broadcast at insert (`boundStreamId`). Any quality tier works without recreating the
|
||||
stream, and auto step-down is done by us dropping bitrate on the fly (zero API calls).
|
||||
stream, and auto step-down rides the same property — we drop bitrate/resolution on the fly, zero
|
||||
API calls — **but the deciding governor is not built**: the old present tense there was a map-lie,
|
||||
corrected 2026-09-01; step-down is **TASK 33** (v1 scope).
|
||||
- **Quality is greyed out while live** — resolution/frameRate/ingestionType are immutable after
|
||||
stream creation; editing title/description/privacy is fine at any time.
|
||||
- **Report-by-exception health (SHIPPED 2026-08-16, TASK 9 item 3)** — `YouTubeStreamService.GetStreamHealthAsync(streamId)`
|
||||
|
||||
Reference in New Issue
Block a user