# ytLlive β€” Task List > **Catalog:** task files live in [`TASKS/`](TASKS/) (one file per task). This file is the > index. Memory-map conventions: [`schema.md`](schema.md); architecture/decisions: [`ai.md`](ai.md). > **Checklist markers** β€” every task's Status line uses the same states: > 1. βœ… β€” completed / shipped > 2. πŸ”Ά β€” in progress > 3. ⏳ β€” blocked / waiting > 4. ☐ β€” not started / queued > 5. ❌ β€” exception (known-issue, deliberately excluded, or merged) > **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. --- ## Task catalog | # | Task | Status | File | |---|------|--------|------| | 01 | Initial Scaffold | βœ… Done | [`TASKS/task-01-initial-scaffold.md`](TASKS/task-01-initial-scaffold.md) | | 02 | YouTube OAuth2 Authentication | βœ… Done | [`TASKS/task-02-oauth2-auth.md`](TASKS/task-02-oauth2-auth.md) | | 03 | Capture Pipeline (Scenes/Sources) | πŸ”Ά In progress β€” 27/30 items done; items 16, 17, 20 open | [`TASKS/task-03-capture-pipeline.md`](TASKS/task-03-capture-pipeline.md) | | 04 | RTMP Ingest to YouTube | βœ… Done β€” all 7 items shipped | [`TASKS/task-04-rtmp-ingest.md`](TASKS/task-04-rtmp-ingest.md) | | 05 | Layout Persistence (SQLite) | βœ… Done | [`TASKS/task-05-layout-persistence.md`](TASKS/task-05-layout-persistence.md) | | 06 | UI polish batch | βœ… Done | [`TASKS/task-06-ui-polish-batch.md`](TASKS/task-06-ui-polish-batch.md) | | 07 | Meter scaling amplification | βœ… Done | [`TASKS/task-07-meter-scaling.md`](TASKS/task-07-meter-scaling.md) | | 08 | Audio milestone | βœ… SHIPPED 2026-08-14 | [`TASKS/task-08-audio-milestone.md`](TASKS/task-08-audio-milestone.md) | | 09 | YouTube Live Stream Management | ⏳ In progress β€” items 1–3, 4 shipped; item 5 open; item 6 locked (TASK 36) | [`TASKS/task-09-live-stream-management.md`](TASKS/task-09-live-stream-management.md) | | 10 | Monetization: watermark-only paid delta (branding flash) | πŸ”Ά **TEARDOWN, not a build** β€” Polar deleted in favour of Store IAP (2026-09-27); `$10/mo` + `$400` lifetime, no annual. **SUPERSEDED 2026-10-08** β€” free-forever ruling (watermark-free, gratuity/merch only); see TASK 54 | [`TASKS/task-10-monetization.md`](TASKS/task-10-monetization.md) | | 11 | Post-pause polish batch (creator's 8 review issues) | βœ… SHIPPED 2026-08-15 | [`TASKS/task-11-polish-batch.md`](TASKS/task-11-polish-batch.md) | | 12 | Master limiter on the live mix | βœ… Shipped 2026-08-15 β€” `Services/Audio/MasterLimiter.cs` (βˆ’1 dBFS ceiling, instant attack, smoothed release) applied at the end of `AudioMixer.FillAndMix` | [`TASKS/task-12-master-limiter.md`](TASKS/task-12-master-limiter.md) | | 13 | Social media launch kit | πŸ”Ά Scoped β€” queued after v1 | [`TASKS/task-13-social-launch-kit.md`](TASKS/task-13-social-launch-kit.md) | | 14 | Creator feedback batch | πŸ”Ά In progress β€” Branch 2 shipped | [`TASKS/task-14-creator-feedback-batch.md`](TASKS/task-14-creator-feedback-batch.md) | | 15 | Stock scene background images | βœ… Shipped | [`TASKS/task-15-scene-backgrounds.md`](TASKS/task-15-scene-backgrounds.md) | | 16 | Kill infinity display | βœ… Shipped | [`TASKS/task-16-kill-infinity-display.md`](TASKS/task-16-kill-infinity-display.md) | | 17 | Web source rendering (WebView2) | βœ… Done | [`TASKS/task-17-web-source-rendering.md`](TASKS/task-17-web-source-rendering.md) | | 18 | Local recording | βœ… Shipped `a9eb360` (2026-08-29) | [`TASKS/task-18-local-recording.md`](TASKS/task-18-local-recording.md) | | 19 | Scene transitions | MERGED β†’ TASK 23 (Control Surface UX) | [`TASKS/task-19-scene-transitions.md`](TASKS/task-19-scene-transitions.md) | | 20 | Hotkeys (keyboard shortcuts) | βœ… Done β€” shipped 2026-08-26 | [`TASKS/task-20-hotkeys.md`](TASKS/task-20-hotkeys.md) | | 21 | Media source (video file playback) | πŸ”Ά In progress β€” UI picker remaining | [`TASKS/task-21-media-source.md`](TASKS/task-21-media-source.md) | | 22 | Audio sync offset | βœ… Done (2026-08-31) | [`TASKS/task-22-audio-sync-offset.md`](TASKS/task-22-audio-sync-offset.md) | | 23 | Studio mode | MERGED β†’ TASK 23 (Control Surface UX) | [`TASKS/task-23-studio-mode.md`](TASKS/task-23-studio-mode.md) | | 24 | Toast notifications | βœ… Done (2026-08-23) | [`TASKS/task-24-toast-notifications.md`](TASKS/task-24-toast-notifications.md) | | 25 | Background consolidation | βœ… Done (2026-08-23) | [`TASKS/task-25-background-consolidation.md`](TASKS/task-25-background-consolidation.md) | | 30 | Gear menu cleanup + Default Location for Recordings + preview layout | βœ… Done (2026-08-29) | [`TASKS/task-30-gear-cleanup-preview-layout.md`](TASKS/task-30-gear-cleanup-preview-layout.md) | | 31 | SceneGraph component + static/dynamic compositor optimization | βœ… Done (2026-08-31) | [`TASKS/task-31-scenegraph-optimization.md`](TASKS/task-31-scenegraph-optimization.md) | | 32 | Stream resilience | ☐ Queued (2026-09-01) | [`TASKS/task-32-stream-resilience.md`](TASKS/task-32-stream-resilience.md) | | 33 | Bandwidth auto step-down | ☐ Queued (2026-09-01) | [`TASKS/task-33-bandwidth-step-down.md`](TASKS/task-33-bandwidth-step-down.md) | | 34 | Scheduled streams (Text-drawer version) | ☐ Queued (2026-09-01) | [`TASKS/task-34-scheduled-streams.md`](TASKS/task-34-scheduled-streams.md) | | 35 | Scene-linked audio | ☐ Queued (2026-09-01) | [`TASKS/task-35-scene-linked-audio.md`](TASKS/task-35-scene-linked-audio.md) | | 36 | Gold pass | ☐ Queued (2026-09-01) | [`TASKS/task-36-gold-pass.md`](TASKS/task-36-gold-pass.md) | | 37 | Defaults vs current layout split | ☐ Queued (2026-09-20) | [`TASKS/task-37-defaults-current-split.md`](TASKS/task-37-defaults-current-split.md) | | 38 | Capture Window… backdrop (in-app window picker) | βœ… Done β€” shipped 2026-09-21 | [`TASKS/task-38-window-backdrop.md`](TASKS/task-38-window-backdrop.md) | | 39 | YPP journey tracker (slice 1: current-scope data) | πŸ”Ά Slice 1 βœ… β€” 2026-09-22; **refresh 403 fixed 2026-09-23** (auditDetails part dropped); slice 2 (Analytics + ETA) queued | [`TASKS/task-39-ypp-journey-tracker.md`](TASKS/task-39-ypp-journey-tracker.md) | | 40 | App Settings round: active-camera picker + config dialog, gear move, defaults, accent | ☐ Queued (2026-09-22) β€” plan saved | [`TASKS/task-40-app-settings-round.md`](TASKS/task-40-app-settings-round.md) | | 41 | Test Stream mode: private test broadcast + TEST drawer (mock chat input + simulated events) | βœ… Done (2026-09-22) | [`TASKS/task-41-test-stream-mode.md`](TASKS/task-41-test-stream-mode.md) | | 42 | Top bar redesign: one REC-or-Stream surface, gear up top, sign-in/account zone world-gated | βœ… Done (2026-09-22) | [`TASKS/task-42-top-bar-redesign.md`](TASKS/task-42-top-bar-redesign.md) | | 43 | Native events & alerts: chat parity (all six event types + server poll cadence) + six-animation native alert box | βœ… Done (2026-09-24) | [`TASKS/task-43-native-alerts.md`](TASKS/task-43-native-alerts.md) | | 44 | TEST-tab chat fix: resolve liveChatId from snippet (not contentDetails) + poll until the broadcast is live | βœ… Done (2026-09-25); full suite 318/318 | [`TASKS/task-44-test-chat-fix.md`](TASKS/task-44-test-chat-fix.md) | | 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | βœ… Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) | | 46 | Drawers: click outside the rail closes whichever is open β€” TEST added to the existing Stream Settings + YPP dismiss behavior | βœ… Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) | | 47 | Alert box video: built-in/custom alert clip (+ six-animation fallback) with read-time fade in/out + ticker (now in the preview too, 3 display methods) + alert volume in the live mix | βœ… Done (2026-09-26) | [`TASKS/task-47-alert-videos.md`](TASKS/task-47-alert-videos.md) | | 48 | Distribution & packaging: **MSIX + Store IAP** | ⏸ **DEFERRED 2026-10-26 β€” revisit near v1, not now** (creator: the remaining programming work is far from release, and a complete UX redesign comes first). Frozen until then: items 6-8, the signing decision, the updater choice, and the route question itself. History: **βœ… route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28**; **item 2 (`Package.appxmanifest` + `scripts/package-msix.sh`) SHIPPED 2026-10-22 β€” a real 159 MB `LlamaCasty.msix` packs**; **item 3 (remove the Velopack updater) SHIPPED 2026-10-22** β€” the Store updates the app. Open: items 6-8 (recording invariant, disqualifiers, pre-submission gates); items 4/5 are the creator's (Certification Kit, Partner Center listing) | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) | | 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) | | 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) | | 51 | **Webcam background effects: Remove + Blur** | βœ… **Built 2026-10-09** (v1 requirement) β€” bundled MediaPipe Selfie Segmenter β†’ ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Creator check 2026-10-09: **Remove jitter FIXED** (temporal mask smoothing 0.75/0.25, OBS/Jitsi-derived) and **Blur "heavy" FIXED** (quadratic radius 1β†’6 small px, default 0.5 = 2, StreamFX-consensus); recheck on the real camera pending | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) | | 52 | **Zoom box (anchored 2Γ— magnifier)** | βœ… Done β€” shipped 2026-10-08 (3 creator corrections during the build: loupe β†’ anchored capture over the full composite, drawn last β†’ never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) | | 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) β€” output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) | | 54 | **Marcom round: free-forever positioning + merch storefront** | πŸ”Ά In progress β€” llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) | | 55 | **Disk hygiene: log rotation + low-disk guard** | πŸ”Ά In progress β€” **slice 1 shipped 2026-10-22** (`startup.log` rolls at 5 MB to `startup.1.log`, one copy kept β€” it was uncapped at 23 MB and growing every few seconds); slices 2 (temp alert-clip cleanup) + 3 (low-disk guard) open | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) | | 56 | **Pre-publication secret scan (full git history)** | βœ… Done β€” 2026-10-22, **CLEAN** (432 reachable commits + 63 dangling + worktree; 4 hits, all false positives: a `your-client-secret` doc placeholder and a test dummy β€” nothing to rotate). ⚠️ the repo was **already public** when the scan ran (anonymous GET β†’ 200), so the gate came late β€” but nothing was exposed | [`TASKS/task-56-secret-scan.md`](TASKS/task-56-secret-scan.md) | | 57 | **Vertical (9:16) tier camera placement β€” the "vv" feature** | βœ… **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier β€” small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only β€” the circle is not offered β€” the tile or one whole 607Γ—540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) | | 58 | **Properties-panel style consistency (the theme owns the look)** | βœ… Done β€” 2026-10-21 (the creator's "why is the drop-down not in the same style as the others?" β€” the drop-downs/fields/check boxes had been hand-painted instead of themed, so WPF drew the light system template; now every one carries a theme style, guarded by a test) | [`TASKS/task-58-properties-style-consistency.md`](TASKS/task-58-properties-style-consistency.md) | | 59 | **The properties panel is a hierarchy (titles own their content)** | βœ… Done β€” 2026-10-21 (the creator's ruling: a "Shape" title above the Mirror/Rect row, the drag hint indented under it, the same for Geometry / Border Color / Border Thickness, Remove + Blur under Background, Strength under Blur β€” and the shape became a CHOICE: Rect | Circle with Rect selected by default) | [`TASKS/task-59-properties-hierarchy.md`](TASKS/task-59-properties-hierarchy.md) | | 60 | **The border-colour drop-down names every colour** | βœ… Done β€” 2026-10-21 (the creator's "the #hex values are good, but add a corresponding color name/label next to the value" β€” the 14 colours moved out of the click handler into `Models/BorderColorPalette.cs`, and each row now reads "#e94560 Crimson") | [`TASKS/task-60-border-color-names.md`](TASKS/task-60-border-color-names.md) | | 61 | **The geometry fields share one fixed width** | βœ… Done β€” 2026-10-21 (the creator's "the Y and H boxes should be the same width as the X and W boxes and those widths should be fixed to accomodate the max number of possible digits" β€” the Y/H fields used to stretch to fill the row; all four are now 44px, wide enough for 1920 plus a sign) | [`TASKS/task-61-geometry-field-widths.md`](TASKS/task-61-geometry-field-widths.md) | | 62 | **The border-colour field shows the colour's name next to the hex** | βœ… Done β€” 2026-10-21 (the creator: "the border color drop down box content should also include the name of the color next to the hex value as they in the drop-down list box" β€” the field keeps the hex editable and the palette's name sits right beside it, empty for a custom hex) | [`TASKS/task-62-border-colour-name-in-the-field.md`](TASKS/task-62-border-colour-name-in-the-field.md) | | 63 | **The border-colour picker is the app's own drop-down** | βœ… Done β€” 2026-10-21 (popup row click deselection fixed 2026-10-22: `LeftPanel.IsDescendantOf` now crosses `PopupRoot` β†’ `PART_Popup` via `LogicalTreeHelper.GetParent` when `VisualTreeHelper.GetParent` returns `null`, so clicking a colour in the open `YtComboBox` updates `BorderColor` instead of exiting properties) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.md) | --- ## Research index | Research | Covers | |---|---| | [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) | YouTube Live Streaming API v3 β€” authoritative facts for the v3 build | | [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) | **Windows Store policies 7.20 + MSIX packaging + code-signing economics** (2026-09-27). Feeds TASK 48. Records which policies bind, which don't, and why β€” read it before re-litigating distribution | --- ## Open items (at a glance) ### Creator-reported batch (2026-09-26) β€” proof-of-concept round, recordings used as the reference The creator is **not** getting YouTube private test recordings saved, so local recordings are the proof of concept for compositing; the live path is assumed to share it. **Verified true, not assumed:** one `_framePump` is built in the `MainViewModel` ctor with a single `brandFlash:` callback, and BOTH `Streaming.Operations.cs:68` (go-live) and `:199` (record) call that same `StartAsync` β€” there is no second encoder path needing a "redirect". Record+simulcast is one ffmpeg with two outputs. 1. βœ… **Recording save dialog: Cancel discards** β€” was silently saving under the default name. Enter accepts the default; Cancel/Escape/X **deletes** the footage and names the file if the delete fails. `MainViewModel.CompleteRecordingSave` (internal) + `ytLive.Tests/RecordingSaveDialogTests.cs`. 2. βœ… **Brand flash in ALL scenes + in recordings** β€” was gated on `IsLive` via `UpdateLiveVisuals()`, so a recording made without ever going live carried no credit. The presenter is now `Start()`ed once in the `MainViewModel` ctor and never stopped on live-state churn; the licence gate rides on `IsPremium` β†’ `Enabled` as before. Also fixed the premium-edge/restart trap that app-lifetime exposed. Tests: `BrandFlashOutputTests` (12 facts) incl. `Credit_IsComposited_WithNoLiveSession_AndSoARecordingCarriesIt` and `ADowngradeMidSession_RestartsTheCadenceTimer`. 3. βœ… **Ticker confined to the alert box** β€” was a global 1920Γ—48 bar at the top edge; the creator wants it over the *Stream Alerts video*. The strip now renders at the alert box's own size and carries the box's origin on a new `VideoFrame.Placement`; every blit site reads `OriginX/OriginY` instead of a literal `0, 0`, and the preview element is bound to the same rect so preview and output cannot drift. No alert box β‡’ no ticker. Height clamped to the 48px strip; `CopyStrip` clips rows so a short box can't overrun the buffer. Tests: 3 new facts incl. an output-side `SceneCompositor.Render` check that the pixels land in the box and NOT at the top-left corner. 4. ☐ **Two instances still refuse to run.** `InstanceProfile` isolates layout/auth/log/WebView, but `FfmpegLocator._toolsDir` is still the hardcoded shared `%APPDATA%\ytLlive\tools` and both instances can `Directory.CreateDirectory` + `ExtractBinaries` into it. Also the launch method is unconfirmed: `dotnet run` while the first app holds `bin/…/ytLive.exe` dies with `MSB3027` before any instance starts β€” that is a build-output lock, not a log conflict. 5. ☐ **Post-session efficacy report** β€” on end of stream *or* recording, report what worked and what failed, including the **dropped frames** the creator saw. `CurrentHealth` already tracks `DroppedFrames`/`StreamDuration`; `SessionTeardownTests` is the natural home for the roll-up. - **TASK 3** β€” items 16 (Text source) + 20 (RewardEvent capture β†’ SQLite, Alerts' persistence half) open; **17 (Alerts) is DONE via TASK 43 (2026-09-24) β€” native six-event alert box** - **TASK 9** β€” item 5 open (webcam identity key reconciliation) - **TASK 10** β€” the Velopack update URL is **RETIRED (2026-10-22)**: the updater was deleted (TASK 48 slice 3) and the Store updates the app - **Shipping / release build (creator-queued 2026-09-26)** β€” no publish config exists yet: the csproj has only `OutputType=WinExe` + `TargetFramework`, so a plain `dotnet publish` is **framework-dependent** (customer needs the .NET 8 Desktop Runtime preinstalled). `Distribution.md` targets a self-contained ~80–120MB `LlamaCasty.exe`. β›” **The Polar File-Download benefit is dead** β€” the Store hosts and updates the app, so there is no upload step (TASK 48). Two deliverables: 1. **`scripts/publish.sh`** wrapping `dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained true -o ./publish/win-x64`, and **asserting the output contains no test/xunit artifacts**. (The compile side needs nothing β€” the tests are a separate project with a one-way reference, plus explicit `` in `ytLive.csproj`; the risk is *packaging*: `ytLive.Tests/bin/.../ytLive.exe` exists and would ship a Debug build + test DLLs if anyone ever zips a bin folder by hand. Never copy folders; always publish.) 2. **Condition `InternalsVisibleTo("ytLive.Tests")` to Debug only** (`ytLive.csproj:78-82` is unconditional, so it ships in Release) β€” it hands the test assembly name to anyone decompiling and advertises that `internal` members are externally reachable, which conflicts with the obfuscation posture in `Distribution.md` Β§1.2. 3. **Decide the shipped exe name.** `Distribution.md` promises the customer `LlamaCasty.exe`; `ytLive` actually publishes `ytLive.exe`. Renaming the AssemblyName is 4 lines (1 csproj + 3 pack URIs that spell the assembly name β€” `MainWindow.xaml:14` is the **window icon**, so missing it ships a broken taskbar/Alt-Tab icon). Namespaces stay `ytLive.*`; the 230-file re-brand is NOT wanted. Do **NOT** touch the `%APPDATA%\ytLlive` folder β€” breaking (orphans users' layout DB). See the naming table in `ai.md`. Do **NOT** add `-p:PublishTrimmed=true`: WPF is not trim-compatible and it fails at runtime (BAML/XAML resource resolution), not at build time. `PublishReadyToRun` is safe. - **TASK 12** β€” queued future - **TASK 13** β€” queued post-v1 - **TASK 14** β€” Branch 2 shipped; branch 3+ in progress - **TASK 21** β€” UI picker slice remaining (Windows-only verification pending) - **TASK 32–36** β€” all queued future work (2026-09-01) - **βœ… TASK 57 β€” the "vv" vertical camera layout** (both units shipped 2026-10-21). The vertical tier is a centred band crop of the same master, so the 16:9 camera placement falls outside the cropped window and the creator is not in the vertical stream at all. **Unit 1:** each scene keeps its own vertical placement (position, size, shape); the Live scene gets a small tile in one of six spots (top/bottom Γ— left/centre/right), circle or rect, placed by drag-snap or typed into the new Geometry fields; switching back to 16:9 restores the desktop layout exactly. **Unit 2:** the Chat scene in the vertical tier β€” rect only (the circle option is not offered and the model refuses it), the small tile or enlarged to fill one whole 607Γ—540 area, top or bottom by where the camera is (the creator's table, 2026-10-17). **Left:** the creator's hands-on check of the whole vv feature. See [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) and `ai.md` β†’ "Vertical (9:16) tier camera placement". - **Webcam resource lifecycle (2026-09-17)** β€” startup slice shipped (OS poll at start, single-camera auto-lock, persistent red alert in the Layers panel on lock failure, re-polls until resolved); gate slice shipped same day β€” webcam = app-level default, one per stream per scene, Add places the default directly, offered only when a camera is ATTAINABLE (identity + live session, not just a saved identity), app base lock keeps the session after the last placement is removed, startup adopts a solo camera as default. TASK 26's app-wide gate superseded by creator directive. The App Settings webcam selector is now **TASK 40 Unit A** (plan saved 2026-09-22). Remaining next slice (user-queued): static (+) catalog rows (Background, YouTubeEvent, quoted labels) with reason-greying. - **πŸ”Ά Webcam cadence β†’ resolution β€” IN PROGRESS, creator-queued 2026-10-05** (DHH/lipreading: the webcam judders, ~1 hitch in 5, invisible in the output file). **UNIT 0 shipped** (counters + `Helpers/FrameCadence.cs`; see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) β†’ "Frame cadence"). **Needs a creator recording before the next unit.** Remaining units, all agreed 2026-10-05, one Good Dog test each: - **β›” UNIT 1 IS NOT NEXT β€” re-scoped 2026-10-05.** The first real reading exonerated the camera: it delivers 30.0/s with zero hitches; the compositor manages **6.8 composites/s at ~141ms each**, so the file is **88.7% duplicate frames** and that is the judder. 720p triples the bytes stretched per render on an already-over-budget path. Keep `ExclusiveControl` + 720p as a **sharpness** win and schedule it AFTER the compositor work. - **βœ… SHIPPED 2026-10-16 (measurement only): per-element composite timing + a scene dump.** `CompositeTimings` + the pump's `FramePump parts (N renders/5s): …` line and `FramePump scene dump:` block (see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) β†’ "Per-element composite timing + scene dump"). One integration test: `ytLive.Tests/CompositeTimingTests.cs`. **NOW WAITING ON A CREATOR TAKE (Release):** the parts line must name which of the 6 elements eats the ~140ms and the dump must show the shape of the slow take vs the fast one (10:03 β†’ 12:01, 29–34ms β†’ 143–155ms, same shape). **No fix until a real reading names the element** β€” the next unit is the fix that reading dictates. - **πŸ“– FIRST INSTRUMENTED TAKE READ (2026-10-16, Release, recording, 1920Γ—1080 1:1 master tier): THE JUDDER DID NOT REPRODUCE β€” and no element is expensive.** 60 composites/s, avg render 1.0–1.1ms, biggest element in the scene = webcam **0.4ms** (raster 0.2ms), backdrop 0.2ms, the other four under the 0.1ms floor; webcam 30.0/s at the resolver (longest gap 50ms); the FILE holds 29.6 distinct webcam images/s with 82% of images held exactly 2 frames. **Nothing to fix yet.** The historic slow numbers are different configurations, not a slow scene β€” Oct 9 19:48 was the **vertical** tier (`comp=608x1080`, 45ms) and Oct 5 12:01's geometry was never logged (the `scene=/comp=` suffix didn't exist); every slow window shows `cache xR/0H` (the whole-frame cache never reusing) while the healthy one shows `151R/150H`, and one Oct 5 take latches fastβ†’slow mid-take. **NEXT: a Release take in the failing geometry** β€” vertical tier first, then the 21:9 display config β€” and only that take's `parts` line names the stage. See `ai.md` β†’ "βœ… What the first instrumented take said". - **Unit 2 (later, unchanged):** blinking amber **"SD" badge**, preview only, never in the recording/stream, when a better format was available and not obtained. Top-right corner, or the 1:30 rim point when the clip is round. - **Deferred to after Unit 1:** `MediaCaptureSharingMode.ExclusiveControl` + MJPG β‰₯30fps up to 1280Γ—720 via the existing `SetMediaStreamPropertiesAsync` path (refused only because `SharedReadOnly`); same-camera `SharedReadOnly` retry. **The creator reversed the ban on taking an exclusive camera lock.** API probe already done β€” see HANDOFF.md, incl. the `SetFormatAsync` dead end. - β›” **No fallback-camera feature, ever** β€” *"that's bypassing the user's explicit choice."* - **Background removal + blur = v1 requirement (2026-10-08)** β€” reverses the 2026-10-05 exclusion. Bundled ONNX + onnxruntime, no downloads, no GPU requirement. Per-element Remove/Blur + strength. See [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md). - **TASK 40 β€” App Settings round** β€” plan saved 2026-09-22 (`TASKS/task-40-app-settings-round.md`), four units, one Good Dog test each: **A** active web camera section under Recordings (Change… sub-menu = live enumeration incl. vcams; Tune… config dialog via `ICameraControlProbe`, live-only tweaks), **B** gear moved top-left right of the wordmark (left-click App Settings, right-click Bug/Feature/About), **C** persist output resolution default + surface default scene transition/duration + confirm-before-End, **D** accent/theme color (consolidate 69 hardcoded accent sites into a DynamicResource brush + picker). **Unit B (gear) is DONE early** β€” shipped 2026-09-22 as part of TASK 42 (top bar redesign): gear top-right of the brand, single click = the Settings/Bug/ Feature/About menu, removed from BottomBar. A + C + D remain queued. - **TASK 37** β€” queued (2026-09-20): split the layout data into a `default` set (the established DB) and a `current` set carrying the build-id; a saved `current` is honored only by the same build-id, otherwise defaults load β€” enables one-click revert to defaults too. Capture needed work OUT of current scope here; do not bolt it onto an in-flight change. - **TASK 41 β€” Test Stream mode** β€” DONE 2026-09-22 (`TASKS/task-41-test-stream-mode.md`): a Test button next to Start runs the real private-only go-live pipeline with `IsTestStream` set, and the TEST drawer (right rail, third tab) hosts Mock Chat Input (real `liveChat/messages.insert` β†’ ~2s poll round-trip renders on the overlay) + simulated Subscriber/New Member/Super Chat events that inject through the poller's `MessageReceived` seam (`IsSimulated` β€” YouTube's insert API only creates text, so these are local-only by design; styled member/SuperChat rows validate the chat overlay). **Real Stinger/TTS alert widgets resolved by TASK 43's native alert box.** - **TASK 43 β€” Native events & alerts** β€” **DONE 2026-09-24** (`TASKS/task-43-native-alerts.md`): StreamElements replaced with NATIVE YouTube events. Chat parity half: `YouTubeChatService` now decodes all SIX `liveChat/messages` event types (`ChatEventKind` on `ChatMessage`) β€” the four formerly-empty event rows fixed β€” and re-arms its poll on the server's `pollingIntervalMillis` (streamList semantics, clamp 1000–6000ms, `maxResults=2000`). Alerts half: a new **`SourceType.AlertBox`** ("Stream Alerts", one per layout) is an OBS-style celebration zone where the six events play **six unique branded animations** (`AlertRenderer` + the `AlertOverlayLayer` component; 33ms ticker, deterministic `Advance` test clock; idle = transparent). Creator rulings baked in: free-sub mention = chat row only (no sub alert β€” YouTube emits none), NO viewer count, every alert card carries the "made with LlamaCasty!" brand line. - **TASK 44 β€” TEST-tab chat fix** β€” **DONE 2026-09-25** (`TASKS/task-44-test-chat-fix.md`): the open creator report ("I still cannot post a chat message in the TEST tab") was a single cause: `GetBroadcastLiveChatIdAsync` read `contentDetails.liveChatId` (lifeChatId lives in **`snippet`**) AND ran before the broadcast was live (RTMP push flips readyβ†’live via enableAutoStart; YouTube only populates the id on live broadcasts β€” official GetLiveChatId.java sample lists broadcastStatus=active). Fixed by reading `snippet.liveChatId` and polling with a bounded retry after the frame pump starts; chat stays non-fatal. - **TASK 47 take four (2026-09-26)** β€” the announcement strip was **output-only** (a frame-pump callback with no preview consumer, since a master-width global overlay cannot ride a per-element `Image`) and the marquee was paced at a fixed `140px/s` (~17s per pass, so a 10s alert showed the text once). Now: a `tickerPreviewSink` publishes it to a global `AlertTickerElement` in `PreviewPane.xaml` (mirroring `SocialBarElement`), and the creator picks **Scroll / Flash / Solid** in the panel, with Scroll paced in *reads per alert* (3 inside a 10s alert) rather than px/s. 15 new facts; suite 339/339. - **TASK 47 β€” Alert box video** β€” **DONE 2026-09-26** (`TASKS/task-47-alert-videos.md`): the TASK 43 alert box now plays a **video** on every alert β€” a shipped built-in mp4 (`Assets/alert-default.mp4`, stamped into the `Asset` table at startup) that the creator can swap for their own file via the new Stream Alerts section (path only, never stored in the DB; six `AlertRenderer` animations remain the fallback). Per-alert decode via a new `IAlertClipDecoder` fx (ffmpeg rawvideo bgra + f32le pipes, real-time paced, disposed at drain); ~0.3s fade-in/out rides the alpha envelope + scales the audio (mixed into the live stream at unity β€” **no duck**, a per-alert Volume slider instead); an auto-composed message **ticker** ("Funder β€” Super Chat Β· $10.00") scrolls along the very top of the frame (never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings: custom + fallback (not either/or), ticker on top, no ducking. - **TASK 36 item 6 (release engineering) is now TASK 48** (2026-09-27) β€” code signing, the installer, and the Velopack update URL have one owner instead of three scattered mentions. **EULA draft/review and the THIRD-PARTY-NOTICES license-texts gate stay in TASK 36 item 6**, as does the agreed build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only). - **TASK 48 β€” distribution & packaging** β€” **βœ… ROUTE DECIDED 2026-09-27: Microsoft Store, MSIX package, Store IAP.** Creator's criteria, verbatim: *"zero headaches, minimal maintenance (for me) while still providing accountability and a reasonably easy upgrade flow."* Route A is the only option where all four are solved by handing the work to Microsoft rather than to a certificate vendor β€” **$0/yr, no certificate, no HSM, no annual renewal, no SmartScreen ramp**, plus Store auto-update, Store-side payments/entitlements/ refunds/support, and Microsoft review as the accountability layer. The rejected options (Store+Polar, Polar-hosted + own cert, Store-EXE) are recorded with their reasons in `TASKS/research-store-certification.md` Β§3 so the decision is not reopened. - **The consequence that makes it cheap: the entire licensing subsystem is deleted.** `Services/PolarLicenseService.cs` (HTTP validation, swallowed network failures, the ignored `expires_at`), `Helpers/PolarLicense.cs`, `ViewModels/MainViewModel.License.cs` (`PremiumUrl`, customer portal, the `OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy), and the incorrect "Polar unlocks alerts" string in `Controls/OverlayHost.xaml` all become dead code. `IsPremium` is derived from the **Store entitlement** instead of a remote HTTP call β€” one locally cached bit refreshed by the OS, and the whole "network flaky β†’ app thinks I'm expired" bug class disappears with the offline-grace machine. - **Unchanged:** the watermarks posture. Free gets everything; the branding flash stays the ONLY paid delta (TASK 36 item 2). Store IAP changes how the bit is *obtained*, never what it *gates*. - **Both formerly-open questions are now CLOSED (2026-09-27).** Store revenue share is **verified at 15%, subscriptions included** (ADA v8.10 Β§6(b)(i) β€” read from the PDF, not a search excerpt), and the IAP tier shape is **decided: `$10/mo` subscription + `$400` lifetime, no annual**, declared in Partner Center as two products. Full detail in `TASKS/task-48-distribution-msix.md` item 0. ⚠️ Do **not** re-open the fee question without re-reading the ADA, and do not *assume* 15% if the terms ever move. - **βœ… item 1 SHIPPED 2026-09-28 β€” bundle ffmpeg instead of downloading it.** The app no longer fetches an executable at runtime (Store policy 10.2.2, and the pin 404'd on the creator's first real recording on 2026-09-01). `scripts/fetch-ffmpeg.sh` populates a gitignored `ffmpeg/` from a **SHA-256-verified** pinned month-end BtbN artifact; the csproj copies it to output/publish and **fails the build** if it is missing; `FfmpegLocator` lost its downloader and now resolves **bundle β†’ PATH**. Immutable tag recorded per the licensing rule: `autobuild-2026-08-31-13-27`. `ffplay.exe` is not shipped. Verified: ffmpeg was the **only** download-and-execute third-party binary β€” `e_sqlite3.dll`/`WebView2Loader.dll` already ship via NuGet and OpenH264 is compiled into `avcodec-63.dll`. **Remaining gaps are not bundling problems:** the WebView2 Evergreen *runtime* is a machine prerequisite (item 2), and **Velopack's updater downloads and executes β€” its teardown is a compliance action.** Detail: `TASKS/task-48-distribution-msix.md` Β§1. - **⏸ TASK 48 / ALL distribution & packaging work β€” DEFERRED 2026-10-26 (creator ruling). Do not start, plan, or re-litigate any of it until we are close to v1.** Verbatim reason: *"we're not anywhere close to release just considering the remaining programming tasks"* and *"I am getting ready to do a complete UX redesign"* β€” packaging, signing, installer and updater decisions would be chasing UI and scope that are about to change, so the urgency is withdrawn, not the task. What is frozen: - **TASK 48 items 6-8** (recording invariant comment, disqualifier re-check, pre-submission gates) and the creator's items 4/5 (Certification Kit, Partner Center listing). - **The route question itself.** The map currently contradicts itself: `ai.md` β†’ "Windows packaging & distribution" says the **Microsoft Store route is dead (2026-10-22)** β€” direct download from `llamacasty.com` + Steam, ship unsigned β€” and `MONETIZATION.md`/`MARCOM.md` agree, while *this file*, `TASKS/task-48-distribution-msix.md` and `HANDOFF.md` still read "Microsoft Store / Store IAP decided". **Resolve it in ONE distribution sweep at the revisit β€” do not patch it piecemeal now.** - **Code signing** (ship unsigned vs buy OV/EV) β€” current recorded posture: ship unsigned and accept the "unrecognized publisher" warning until download reputation accrues. - **The updater.** Deleting Velopack (TASK 48 slice 3) left the app with **no update path at all**, and `ytLive.Tests/StorePackagingTests.cs` currently *asserts that no updater exists*. That guard must be **rewritten with the chosen updater** at the revisit, never left to veto one. - The MSIX package + `scripts/package-msix.sh` still build (159 MB artifact) β€” a working artefact, submitted nowhere. Leave it alone. - **TASK 10 is now a teardown, not a build** (2026-09-27) β€” Store IAP deletes the Polar licensing path. The Polar fee tables, the perpetual-key model, and the customer-portal plumbing in `MONETIZATION.md` (gitignored, local) are obsolete; the **watermark-only entitlement and the branding-flash delta carry over unchanged**. The stale Polar product (`$99/yr`, id `d105dfa1-…`) is not reused β€” Store IAP products are declared in Partner Center instead. - **Pricing β€” βœ… DECIDED 2026-09-27: `$10/mo` subscription, or `$400` lifetime. No annual.** `MONETIZATION.md` carries a **one-time perpetual** decision from 2026-09-21 (`$29` founder β†’ `$49` list) that explicitly superseded the old `$99/yr` subscription. The creator ruled against it and back to a subscription, anchored on **Meld** (`$20/mo = $240/yr` entry β€” the only competitor sharing our shape: streaming tool, subscription, YouTube-native). The creator's own read: **"XSplit only has legs because of their vcam product"** β€” its price is carried by a system-wide driver, and **virtual camera output is permanently out of product**, so XSplit is not a pricing comparator and its "lifetime β‰ˆ 2x one year of sub" anchor is retired. - **The finding that drove no-annual (2026-09-27): Microsoft Store does not pro-rate.** Pro-rated refunds on cancellation exist only in a narrow set of countries, and **"monthly subscriptions and initial (pre-renewal) purchases aren't eligible for a prorated refund"** β€” so a first-year annual subscriber who cancels loses the remaining months in most countries, **and the developer cannot refund it**. That is verbatim the grievance the creator raised, so it is exactly the trap to avoid. Sources in `MONETIZATION.md` β†’ "No annual tier". - β›” **Clean numbers, no `.99`** (creator ruling: *"Let's stop with the x.99 stuff - I find that irritating. Just say: ten bucks a month"*). The `$x.99` convention only makes a price *look* cheaper and is incoherent for a brand sold on honesty and no-dark-patterns. `MONETIZATION.md` β†’ "Clean numbers". - **No feature gating** β€” pre-existing repo posture (TASK 36 monetization stance: free gets everything, the branding flash is the only paid delta), noted here because it gains a **revenue** argument: the free tier's reach is the funnel and the flash is an ad running inside other people's content. Not a new ruling. - βœ… **Store revenue share β€” VERIFIED 15%, subscriptions included.** From the ADA v8.10 PDF itself, Β§6(b)(i): 15% applies to "any Apps (and any In-App Products in such Apps) that are not listed in Section 6(b)(iii)". LlamaCasty is a Windows PC App β€” not a Game, not Xbox console, not Windows 8 β€” so **15% governs both tiers; there is no subscription surcharge.** The agreement's own changelog (v8.0, Oct 26 2017) says it outright: *"implement the 85/15 revenue share for non-Game subscriptions."* β‡’ **`$10/mo` nets `$8.50`; `$400` nets `$340`.** - β›” **Two obligations the subscription creates (ADA Β§6(h))**, which drive the lifetime tier's value as a hedge: (1) we must **fulfil the subscription for the whole period as marketed**, and on breach Microsoft may refund *"the full amount, plus taxes... in Microsoft's sole discretion"*; (2) **raising the price disables auto-renew**, so `$10` is effectively locked for the product's life. - **TASK 49 β€” chat profanity filter** β€” queued, not blocked, size S. Local, on-device, **non-persistent**, opt-in, **user-supplied word list (never a hardcoded slur list)**, and it must **never match the SuperChat amount or reward fields** (financial data, Store 10.5.5). The non-persistence half is the same property that makes the 11.12 UGC certification answer strong, so **no future chat-history/moderation-log/analytics feature may quietly break it.** --- ## 1.0 gates (do these before the first shipped build) - **β›” Replace the always-Private broadcast lock with a real visibility option (creator ruling, restated 2026-10-04).** `privacyStatus = "private"` hardcoded in `YouTubeStreamService.CreateBroadcast` is deliberate and correct for the whole pre-v1 test phase β€” it is the reason test VODs of a still-breaking product stay off the channel. At v1 it becomes a user-selectable visibility option in the Go-Live dialog. **Before v1, do not touch it and do not raise it as a question, observation or suggestion β€” it has been asked and answered repeatedly.** Full rationale and the retired "can we delete the test VODs?" question: `ai.md` β†’ YouTube Live API design constraints. - **Verified 2026-10-04: a Go Live video IS private.** Three independent hardcodes (`CreateBroadcast` β†’ `"private"`, `GoLiveViewModel.Visibility => "Private"`, `BeginTestStream` β†’ `StreamVisibility = "Private"`) and the one method that could change it, `YouTubeStreamService.UpdateBroadcast`, has **no production caller** β€” only `BroadcastPullOutTests.cs:120`. No path exists today by which an app-created broadcast becomes non-private. - **Two coupled v1 items β€” both are the deliberate end of that guarantee:** 1. **Hide the TEST button.** It exists as the guaranteed-private path; once visibility is selectable, a separate safe button is redundant. One-line change β€” `ShowTestButton => ShowStreamActions && IsConnected` is a computed predicate, not a setting. 2. **This is the unit that first wires `UpdateBroadcast` into production.** It is the only code that can change a broadcast's privacy, and it is currently dead. Wiring it is exactly when private stops being guaranteed and becomes a user-selectable default. Correct at v1, ruinous before β€” and "it's dead code, let me call it" is not a reason to do it early. - A **dry run** may still be worth having at v1 on its own merits; that is a separate question from the privacy mechanism and is **not** decided by the above. - **Kill the dev multi-instance affordance.** `Helpers/InstanceProfile.cs` is already entirely `#if DEBUG`, so a Release build ships as `DataRoot => DefaultRoot` / `WebViewDataFolder => null`. Nothing to do but *keep it that way* β€” and re-prove it: `InstanceIsolationTests.TheAlternateProfile_IsConfinedToTheDebugBuild` (source-level, always runs) plus a Release build in which the `InstanceVariable` **field** is absent from metadata. ⚠️ Do NOT grep the binary for `YTLIVE_INSTANCE` β€” consts are inlined and appear in neither build. - **TASK 36 is now shipped** β€” the branding flash composites into recordings and the stream. Test VODs from an unlicensed instance carry the credit; use a license key for clean captures. - Still queued from earlier: `scripts/publish.sh` (self-contained win-x64, no test artifacts, Debug-only `InternalsVisibleTo`, no WPF trimming), and the `LlamaCasty.exe` assembly rename (three pack URIs incl. `MainWindow.xaml:14` β€” do not rename `%APPDATA%\ytLlive`). - βœ… **RESOLVED β€” do not re-open.** The `OverlayHost.xaml` copy claimed Polar unlocked *alerts*; alerts were never gated by `IsPremium`. The file is **deleted outright** by the TASK 10 teardown (TASK 48 item 0), so the copy is gone with it. The "or gate the alerts" option is **permanently closed** β€” there is no feature gating, ever. --- ## YouTube Live API research facts See [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) β€” authoritative facts for v3 build. --- ## Out of product β€” permanently These were explicitly ruled out: - **D3D11 swap compositor** (superseded by software compositor; TASK 3 item 16) - **Background removal + blur** (ONNX/DirectML; TASK 3 item 19) β€” β›” ruled out 2026-10-05 (*"no - not doing it... a vcam app does it free; an in-app model is a runtime, a GPU/driver matrix, per-scene lighting tuning"*). **REVERSED 2026-10-08 β€” now a v1 requirement** ([`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md)): bundled ~2–5 MB ONNX model + onnxruntime, CPU-capable, no downloads, no NVIDIA. NVIDIA VFX SDK (~1 GB per-client redistributable, RTX 2000+) remains ruled out. - **OBS-style source expansion** (game capture, browser source, media playlist, VLC, color-key, MIDI β€” the minimal source set is deliberate; creators who need more have graduated to OBS) - **Multi-layout** (auto-save, single layout; multi-layout = OBS territory) - **Cross-process stream resume** (API makes it impossible; creator ruling 2026-09-01) ## Transitions & brand logo (queued 2026-10-27) - βœ… **Fade fix** β€” swaps fade from the outgoing picture (`TransitionFadeTests`). On-screen check pending. - βœ… **Transition picker** in App Settings (type + duration), saved with the layout (`TransitionPickerTests`). - ☐ **Wipe** β€” CPU blend, direction setting. - ☐ **Move** β€” persistent layers (per-layer "carry across scenes" flag), per-scene placement, fixed z band. - ☐ **Brand logo** β€” permanent "Brand logo" layer above background in every scene; file in App Settings (imported to the asset table); per-scene eye toggle; top-left default. - ☐ **Cube** β€” GPU path gated on capability; CPU path only if it holds 60fps. - Gear: right-click opens App Settings (built, on-screen check pending). Left-click menu stays for now.