The creator's locked table (2026-10-17) gives the Chat scene its own rules in the 9:16 tier: RECT ONLY - the circle option is not offered - and a size that is either the normal small tile or ENLARGED to fill one whole 607x540 area (top or bottom). Unit 1 shipped the per-scene placement; this unit is the Chat scene's own variant of it. The rule lives on the MODEL, not on the button: - Models/SceneElement.cs: ClipShape and ToggleClipShape() are now virtual. - Models/WebcamSceneConfig.cs: VerticalShapeLocked (set per scene by the VM) OVERRIDES the ClipShape property, so no writer can slip a circle in - the toggle, the Shape button and the placement swap all go through it. The lock is cleared BEFORE the 16:9 layout comes back, so the desktop shape returns. - ViewModels/MainViewModel.Webcam.cs: CanToggleClipShape (the circle is NOT offered in the vertical Chat layout - the Shape button disappears, it is not merely greyed out), IsChatSceneCamera, ResizeWebcamInVerticalTier. - Models/VerticalCameraLayout.cs: FillsArea (the "enlarged" state, derived - no new column) and SizeFor - the Chat camera's TWO sizes, the tile or the whole area, whichever is nearer the requested width, so the resize handle has two detents instead of any size in between. - An enlarged camera FILLS the area its centre is in (top or bottom), so dragging it up/down swaps the areas. - The Chat camera is auto-tiled like the Live one when the vertical tier is picked - otherwise the vertical chat stream has no camera at all. - Controls/PreviewPane.xaml.cs: the resize routes through the VM; the drag assigns, lets the model clamp into the band, then snaps (so the enlarged camera can be dragged from the top area to the bottom one). Test: ytLive.Tests/VerticalChatCameraLayoutTests.cs - the ONE Good Dog test for the unit: the vertical tier is picked -> the Chat camera comes into the band as a small RECT tile -> the circle option is not offered and cannot be set even on the model -> dragged up and enlarged it fills the TOP area exactly -> back to 16:9 the desktop layout is restored exactly and the circle option is offered again. 430/430 tests pass, 0 warnings. Research (the creator's table is the established practice, not our invention): - Streamlabs' vertical guide: "splitting the screen in half horizontally, placing the camera feed in the top half and the main content on the bottom (or vice versa) may be the bet", and a just-chatting vertical wants the camera "to take up most of the screen" (streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming) - Bloopbot's vertical overlay docs describe the same per-layer model we use at scene granularity: "move or resize a layer on Vertical and it stops being Auto-placed. It keeps the place you gave it, and the horizontal layout is not touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format) - our swap-at-tier-change, stored per scene. - Landmine recorded: "about the bottom 560 px of the vertical video can be covered by Twitch's chat and player controls on phones" (Bloopbot) - the Chat camera's enlarged BOTTOM area may be half-hidden by the platform chrome; TASK 53's safe-zone work decides that. Memory in the same commit: ai.md (the Chat rules, the shape lock, the tests), TASKS.md + TASKS/task-57-vertical-camera-layout.md (TASK 57 complete), HANDOFF.md, MyMistakes.md (3 recipes: lock the model property not the UI button; two detents for a discrete size set; the edit tool cannot match an oldString with an em dash), Models/ViewModels/Controls index rows.
427 lines
39 KiB
Markdown
427 lines
39 KiB
Markdown
# 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 | ☐ Queued | [`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** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28** | [`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** | ☐ Queued (2026-10-09) — `startup.log` appears uncapped; also temp alert-clip cleanup + free-space check before recording | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
|
||
| 56 | **Pre-publication secret scan (full git history)** | ☐ Queued (2026-10-09) — gate before the Gitea repo goes public; report commit/path only, never values | [`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) |
|
||
|
||
---
|
||
|
||
## 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** — Velopack update URL pending → **now owned by TASK 48** (retires if the Store handles updates)
|
||
- **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
|
||
`<Compile Remove="ytLive.Tests\**" />` 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`; `<AssemblyName>ytLive</AssemblyName>` 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 `ai.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 `ai.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 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)
|