Creator 2026-09-26: "the ticker should appear over the stream alerts video, not over
the entire preview window". It was a GLOBAL 1920x48 bar blitted last at a hardcoded
(0, 0) -- it covered the whole preview width, not the alert video.
The strip is now rendered at the alert box's own size and the box's origin travels on
the frame in a new VideoFrame.Placement ((int X, int Y)?). OriginX/OriginY default to
0 when Placement is null, so every full-canvas overlay (the branding flash) is
unaffected. Every ticker blit site now reads tickerFrame.OriginX/OriginY instead of a
literal 0, 0 -- SceneCompositor x2 and FramePump's static-bake Overlay path -- and
PreviewPane.xaml's AlertTickerElement binds the same rect (AlertTickerLeft/Top/
Width/Height), so the preview cannot drift from the recording the creator is judging.
Why the position rides on the frame rather than in a full-canvas frame: a 1920x1080
overlay is 8.3MB of large-object-heap garbage per tick, ~2.5GB churned over one 10s
alert at 30fps. A box-sized strip is ~370KB. The branding flash does render
full-canvas but RECYCLES one 8MB buffer and bumps Epoch, so it never allocates per
frame; the ticker allocates per call, so it has to stay small.
Also fixed: CopyStrip now clips ROWS to the target height. The pill rasterises at its
natural 48px, so an alert box shorter than that would have written past the end of the
target buffer once the strip became box-sized.
No alert box in the scene now means no ticker at all -- there is no global position
left for it, and this is the guard against the old bar quietly coming back.
Tests (3 new facts, 27 in the file):
ComposedOutput_PutsTheTickerInsideTheAlertBox_NotAtTheTopEdge -- renders through
the real SceneCompositor and asserts the pixels land at (620, 430) and NOT at the
top-left corner. The creator is judging compositing from local recordings, so the
OUTPUT side is the side that needed proving.
ASceneWithNoAlertBox_PublishesNoTicker
ABoxShorterThanTheStrip_ClipsRowsInsteadOfOverrunning
Updated AlertLayer_PublishesARealTickerFrameToThePreviewSink, which asserted the old
1920 width from a box with no geometry (defaulted to 1px); it now uses the product's
own default box (680x200 at 620,430, MainViewModel.Sources.cs:54-57) and pins the
frame size and origin.
Full suite 367/367.
21 KiB
ytLlive — Task List
Catalog: task files live in
TASKS/(one file per task). This file is the index. Memory-map conventions:schema.md; architecture/decisions:ai.md.
Checklist markers — every task's Status line uses the same states:
- ✅ — completed / shipped
- 🔶 — in progress
- ⏳ — blocked / waiting
- ☐ — not started / queued
- ❌ — 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 |
| 02 | YouTube OAuth2 Authentication | ✅ Done | 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 |
| 04 | RTMP Ingest to YouTube | ✅ Done — all 7 items shipped | TASKS/task-04-rtmp-ingest.md |
| 05 | Layout Persistence (SQLite) | ✅ Done | TASKS/task-05-layout-persistence.md |
| 06 | UI polish batch | ✅ Done | TASKS/task-06-ui-polish-batch.md |
| 07 | Meter scaling amplification | ✅ Done | TASKS/task-07-meter-scaling.md |
| 08 | Audio milestone | ✅ SHIPPED 2026-08-14 | 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 |
| 10 | Monetization: watermark-only one-time license + Polar billing | 🔶 In progress — steps 1–7 shipped | 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 |
| 12 | Master limiter on the live mix | ☐ Queued | TASKS/task-12-master-limiter.md |
| 13 | Social media launch kit | 🔶 Scoped — queued after v1 | TASKS/task-13-social-launch-kit.md |
| 14 | Creator feedback batch | 🔶 In progress — Branch 2 shipped | TASKS/task-14-creator-feedback-batch.md |
| 15 | Stock scene background images | ✅ Shipped | TASKS/task-15-scene-backgrounds.md |
| 16 | Kill infinity display | ✅ Shipped | TASKS/task-16-kill-infinity-display.md |
| 17 | Web source rendering (WebView2) | ✅ Done | TASKS/task-17-web-source-rendering.md |
| 18 | Local recording | ✅ Shipped a9eb360 (2026-08-29) |
TASKS/task-18-local-recording.md |
| 19 | Scene transitions | MERGED → TASK 23 (Control Surface UX) | TASKS/task-19-scene-transitions.md |
| 20 | Hotkeys (keyboard shortcuts) | ✅ Done — shipped 2026-08-26 | TASKS/task-20-hotkeys.md |
| 21 | Media source (video file playback) | 🔶 In progress — UI picker remaining | TASKS/task-21-media-source.md |
| 22 | Audio sync offset | ✅ Done (2026-08-31) | TASKS/task-22-audio-sync-offset.md |
| 23 | Studio mode | MERGED → TASK 23 (Control Surface UX) | TASKS/task-23-studio-mode.md |
| 24 | Toast notifications | ✅ Done (2026-08-23) | TASKS/task-24-toast-notifications.md |
| 25 | Background consolidation | ✅ Done (2026-08-23) | 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 |
| 31 | SceneGraph component + static/dynamic compositor optimization | ✅ Done (2026-08-31) | TASKS/task-31-scenegraph-optimization.md |
| 32 | Stream resilience | ☐ Queued (2026-09-01) | TASKS/task-32-stream-resilience.md |
| 33 | Bandwidth auto step-down | ☐ Queued (2026-09-01) | TASKS/task-33-bandwidth-step-down.md |
| 34 | Scheduled streams (Text-drawer version) | ☐ Queued (2026-09-01) | TASKS/task-34-scheduled-streams.md |
| 35 | Scene-linked audio | ☐ Queued (2026-09-01) | TASKS/task-35-scene-linked-audio.md |
| 36 | Gold pass | ☐ Queued (2026-09-01) | TASKS/task-36-gold-pass.md |
| 37 | Defaults vs current layout split | ☐ Queued (2026-09-20) | 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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.
- ✅ 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. - ✅ Brand flash in ALL scenes + in recordings — was gated on
IsLiveviaUpdateLiveVisuals(), so a recording made without ever going live carried no credit. The presenter is nowStart()ed once in theMainViewModelctor and never stopped on live-state churn; the licence gate rides onIsPremium→Enabledas before. Also fixed the premium-edge/restart trap that app-lifetime exposed. Tests:BrandFlashOutputTests(12 facts) incl.Credit_IsComposited_WithNoLiveSession_AndSoARecordingCarriesItandADowngradeMidSession_RestartsTheCadenceTimer. - ✅ 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 readsOriginX/OriginYinstead of a literal0, 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;CopyStripclips rows so a short box can't overrun the buffer. Tests: 3 new facts incl. an output-sideSceneCompositor.Rendercheck that the pixels land in the box and NOT at the top-left corner. - ☐ Two instances still refuse to run.
InstanceProfileisolates layout/auth/log/WebView, butFfmpegLocator._toolsDiris still the hardcoded shared%APPDATA%\ytLlive\toolsand both instances canDirectory.CreateDirectory+ExtractBinariesinto it. Also the launch method is unconfirmed:dotnet runwhile the first app holdsbin/…/ytLive.exedies withMSB3027before any instance starts — that is a build-output lock, not a log conflict. - ☐ Post-session efficacy report — on end of stream or recording, report what worked and what
failed, including the dropped frames the creator saw.
CurrentHealthalready tracksDroppedFrames/StreamDuration;SessionTeardownTestsis 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
- Shipping / release build (creator-queued 2026-09-26) — no publish config exists yet:
the csproj has only
OutputType=WinExe+TargetFramework, so a plaindotnet publishis framework-dependent (customer needs the .NET 8 Desktop Runtime preinstalled).Distribution.mdtargets a self-contained ~80–120MBLlamaCasty.exeuploaded to Polar as a File Download benefit. Two deliverables:scripts/publish.shwrappingdotnet 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\**" />inytLive.csproj; the risk is packaging:ytLive.Tests/bin/.../ytLive.exeexists and would ship a Debug build + test DLLs if anyone ever zips a bin folder by hand. Never copy folders; always publish.)- Condition
InternalsVisibleTo("ytLive.Tests")to Debug only (ytLive.csproj:78-82is unconditional, so it ships in Release) — it hands the test assembly name to anyone decompiling and advertises thatinternalmembers are externally reachable, which conflicts with the obfuscation posture inDistribution.md§1.2. - Decide the shipped exe name.
Distribution.mdpromises the customerLlamaCasty.exe;<AssemblyName>ytLive</AssemblyName>actually publishesytLive.exe. Renaming the AssemblyName is 4 lines (1 csproj + 3 pack URIs that spell the assembly name —MainWindow.xaml:14is the window icon, so missing it ships a broken taskbar/Alt-Tab icon). Namespaces stayytLive.*; the 230-file re-brand is NOT wanted. Do NOT touch the%APPDATA%\ytLlivefolder — breaking (orphans users' layout DB). See the naming table inai.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.PublishReadyToRunis 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)
- 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.
- 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 viaICameraControlProbe, 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
defaultset (the established DB) and acurrentset carrying the build-id; a savedcurrentis 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 withIsTestStreamset, and the TEST drawer (right rail, third tab) hosts Mock Chat Input (realliveChat/messages.insert→ ~2s poll round-trip renders on the overlay) + simulated Subscriber/New Member/Super Chat events that inject through the poller'sMessageReceivedseam (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:YouTubeChatServicenow decodes all SIXliveChat/messagesevent types (ChatEventKindonChatMessage) — the four formerly-empty event rows fixed — and re-arms its poll on the server'spollingIntervalMillis(streamList semantics, clamp 1000–6000ms,maxResults=2000). Alerts half: a newSourceType.AlertBox("Stream Alerts", one per layout) is an OBS-style celebration zone where the six events play six unique branded animations (AlertRenderer+ theAlertOverlayLayercomponent; 33ms ticker, deterministicAdvancetest 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:GetBroadcastLiveChatIdAsyncreadcontentDetails.liveChatId(lifeChatId lives insnippet) 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 readingsnippet.liveChatIdand 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 fixed140px/s(~17s per pass, so a 10s alert showed the text once). Now: atickerPreviewSinkpublishes it to a globalAlertTickerElementinPreviewPane.xaml(mirroringSocialBarElement), 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 theAssettable at startup) that the creator can swap for their own file via the new Stream Alerts section (path only, never stored in the DB; sixAlertRendereranimations remain the fallback). Per-alert decode via a newIAlertClipDecoderfx (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.
1.0 gates (do these before the first shipped build)
- Kill the dev multi-instance affordance.
Helpers/InstanceProfile.csis already entirely#if DEBUG, so a Release build ships asDataRoot => 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 theInstanceVariablefield is absent from metadata. ⚠️ Do NOT grep the binary forYTLIVE_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-onlyInternalsVisibleTo, no WPF trimming), and theLlamaCasty.exeassembly rename (three pack URIs incl.MainWindow.xaml:14— do not rename%APPDATA%\ytLlive). - Decide the open business question:
OverlayHost.xamlcopy says Polar unlocks alerts too, but alerts are not gated byIsPremium. Either fix the copy or gate the alerts — do not leave it.
YouTube Live API research facts
See 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 (ONNX/DirectML; TASK 3 item 19)
- 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)