Files
LlamaCasty/TASKS/task-18-local-recording.md
T
gramps 87509bcf99 docs: restructure TASKS.md into a catalog — one file per task in TASKS/
TASKS.md is now the index (status table, open items, research pointer).
33 files: 32 task files + 1 research facts file. The full take-saga
narrative and all design decisions are preserved verbatim; the catalog
makes the queue readable without opening every task body. Schema and
AGENTS.md updated to reflect the new layout.
2026-09-05 16:31:46 -07:00

11 KiB
Raw Blame History

TASK 18 — Local recording

Catalog: TASKS.md — status and requirements live here.

Goal: record the stream output to a local file, with or without simultaneously streaming.

Status: ✅ Shipped a9eb360 (2026-08-29) — code done (incl. manual-rename modal), build 0 warnings, 244/246 tests; running-app verification (takes 1–5 done): files land (ffmpeg re-pinned month-end), webcam-in-output + social bar visually CONFIRMED from take 3's extracted frame, rename modal used for real (take 3 was named via it); take 3 exposed the ~2fps producer starvation and the pump stage-timing (97ffc42) named it in one line — avg render 258.1ms — slice 1 fixed 2026-09-03 (deadline pacing per OBS video-io.c + libyuv-style row blits in SceneCompositor, test Pump_Paces_To_The_Deadline_Compensating_Render_Cost); take 4: pacing held but render stayed 58.9ms (2M-iteration row walk + 8.3MB/tick LOH) — slice 2 shipped 2026-09-04: VideoFrame.IsOpaque producer-contract flag → full-cover backdrop is one Buffer.BlockCopy; integer fixed-point bilinear general path; pump scratch pool (release strictly post-submit, owned-by-reference so cache frames are untouchable); dead per-tick fromScene render + the fromSceneProvider seam removed (BlendFrame uses TransitionService.FromFrame — the old render fed nothing). Tests Pump_Pools_ScratchBuffers_Across_Frames_Without_Stale_Pixels (the ONE) + Composite_OpaqueFullCover_Backdrop_CopiesEveryPixel_Into_Scratch; 59/59 per-class green, clean build 0 warnings. take 5 ran: render 58.9→25.5ms (138/300 ≈ 2.2x still) — cause: the per-tick chat raster (RenderFrame hit RenderTargetBitmap every tick whenever the message buffer was non-empty — the buffer survives sessions); slice 3 shipped 2026-09-04: ChatOverlayLayer rasters on message/config change and blits a cached frame every tick (OBS text-source pattern; test ChatOverlayLayerCacheTests). **take 6 ran WITHOUT attribution (35-41ms — build provenance unknown; slice 3 effectiveness UNCONFIRMED) → build-stamp shipped instead of guessing again: Helpers/BuildStamp GUID per build (csproj GenerateBuildStamp; incremental builds can no longer lie), wordmark superscript + startup.log line; stats split render (resolve) so the next take names the stage. **takes 7–8 (stamped f190587b): chat fix CONFIRMED (resolve ≈0) but render stayed 26-27ms — compositor re-rasterizing STATIC layers every tick; slice 5 shipped: BlitCachedLayer pastes once-rasterized element-space layers (OBS surface-cache pattern; test PasteCache_RepeatRender_IsByteIdentical_And_ContentChangePropagates). **take 9 ran (c65a3cde, paste cache): render 26.5→22.4ms yet period stayed ~37ms — the gap was Task.Delay's 15.6ms sleep quantum padding every sub-tick wait: the true ceiling, hidden until then (why takes 7→9 looked like zero change). Slice 6 shipped: timeBeginPeriod(1) for the pump life + bulk-sleep + 2ms spin tail + avg wait stat (accounting closes) + webcam now routes through the paste cache. **take 10 ran (59a02a5b): the new wait stat exposed the LAST structural bug — render 22 + wait 10 against a 16.7ms deadline is impossible for a rebasing pacer: the wait was the producer QUEUED BEHIND THE LIVE PREVIEW — the pump's await-continuations inherit the UI SynchronizationContext (StartAsync fires from a command handler), so the loop had been rendering on the dispatcher all along. Slice 7: Task.Run the loop (OBS keeps media threads off-UI for this exact reason), StaticPixelCache locked + chat raster marshalled to the dispatcher on cache-miss (RTB/DrawingVisual are UI-thread objects), SustainedLowLatency GC, worst render stat, webcam routed through the paste cache; test Pump_Produces_OffTheStartingContext, 70/70 green. **take 11 ran (c10ce06c): off-UI loop WORKED — typical frames land exactly on the 16.7ms deadline (work ~10 + wait ~6.8; 212/300 best yet); the remaining gap is periodic 35-65ms spikes worsening across a take = gen2 GC pauses, fed by the capture path's fresh ~8.3MB array per DWM frame (~500MB/s). Slice 8: 4-deep capture buffer ring + VideoFrame.Epoch in the identity-keyed paste cache + gen2 +N printed per stats window; test PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits (fails on the old key), 37/37. Also this session (creator ask): post-mix master gain +40% (before the −1dBFS limiter, so no new clipping). take 12: gen2 ~0-1/window, worst render → ~16-20ms, n/300 → 300, audio ~40% louder in the file.

ROLL-FORWARD (2026-09-04): the two-loop producer (the OBS duplicate-instead-of-starve redesign) and the +40% pre-limiter gain were NOT kept — the two-loop introduced recorded-output TEARING on real hardware and was rolled back with it (full record in HANDOFF). Main sits on the slice-8 serial loop plus this unit: all shared-frame rings 4→8 (screen/camera/web — depth × source period must exceed the worst consumer hold), fresh camera + WebView2 capture pooling, and the wordmark release counter #N replacing the UI GUID (per-build GUID now logs to startup.log only). The tear-proof pacing fix — duplicate-instead-of-starve ON the serial loop — is the next in-flight unit. Known remaining churn (follow-ups, not silently done): vertical tier's final BilinearScale still allocates per frame; one slow tick (~15-25ms) per arriving chat message — debounced off-tick re-render if take 6 shows burst loss. User UX spec (2026-09-04) queued behind this: two-line top bar (LIVE rename, radio pills, Login/Logout button + avatar right-click Change Account, no account light; line 2 centered Start↔Stop grayed-until-armed) + up-front SaveFileDialog for REC (native overwrite prompt; retires the stop-time rename modal) + Go-Live dialog KEPT as preflight confirmation prefilled from the Text drawer — decisions captured in HANDOFF

  1. ✅ EncoderOptions extended with StreamEnabled / RecordEnabled / RecordPath (independent intent flags)
  2. ✅ FfmpegArgs.Build reworked into per-output blocks (stream -f flv, record -f mp4) via AddVideoTags
  3. ✅ Two modes via pills: record-only / stream-only (single ffmpeg, one output). stream+record — REMOVED by creator ruling 2026-09-01: the VOD is already the copy; dual encode drags mid-range hardware and degrades both outputs ("we're not them"). Pending implementation: pills become mutually exclusive radios + BeginGoLive(alsoRecord) param dies.
  4. ✅ Top-bar REC + ON-AIR pill toggles, status lights (REC green when recording, ON-AIR green when live), dynamic primary-button text, account status-light tooltip
  5. ✅ Record-folder persistence (LayoutStore RecordFolder key) + picker (ChooseRecordFolderCommand)
  6. 🟡 Branding flash carries into local recordings — same frame path as streaming (verify in the running app)
  7. ✅ Output folder %APPDATA%\ytLlive\recordings\ default; auto-name ty-<yyyymmdd>-<hhmm start>-0000.mp4 at start, rename-on-stop to ty-…-<hh2mm2 length>.mp4 (numeric suffix on collision)

Remaining: running-app verification of the rename + dual output.

Design decisions

  • Record OR stream, never both (creator ruling 2026-09-01): YouTube's VOD already keeps the copy (downloadable from Studio), and a second concurrent encode degrades BOTH outputs on anything short of a performant chassis. Recording's real jobs: the offline mux-check bench and the no-account session. Pills become radios (arming one disarms the other); the one-stop ruling ("Stop ends everything") already assumes a single session kind.
  • FFmpeg supports multiple outputs natively (-f flv rtmp://... -f mp4 file.mp4) — kept as generic machinery, but only one block is ever enabled now
  • MP4 is the default container; crash-safety arrives as fragmented MP4 (TASK 32 slice 4 — in record mode the local file IS the archive), retiring the old "MKV as an option" note
  • Record-only mode is useful for pre-recorded content or testing without going live
  • The branding flash carries into local recordings (free tier billboard extends to recordings)

v1 execution order

The tasks below are ordered by dependency and risk. Each task builds on the previous.

  1. ✅ TASK 9.4 — Live chat (right panel) — wire YouTubeChatService.Start(), parse messages, render in panel. Foundation for chat box source.
  2. ✅ TASK 3.18 — Chat box source — renders chat ON the stream. Depends on TASK 9.4 (same message parsing).
  3. ✅ TASK 10 — Polar billing — license key entry + watermark toggle + Velopack auto-updates (steps 1-7 shipped; Velopack update URL pending).
  4. ✅ TASK 19/23 — Control Surface UX — director's control room: thumbnails above central monitor, transitions (Cut/Fade/Move), edit mode offline only, left panel two-state (layers/props ↔ chat), right panel eliminated. Verified shipped 2026-08-24.
  5. ✅ TASK 20 — Hotkeys — global keyboard shortcuts. Shipped 2026-08-26: F1-F9 defaults + config UI with modifier chords, persistence, conflict detection, unbinding. 5b. ✅ Broadcast metadata pull-out + launch geometry — shipped 2026-08-24 (row 29 above). Live-screen "Text" tab → broadcast form, persistence + remote update; window default/minimums bumped (1920×1040 / 1366×768) with size+position restore.
  6. ✅ TASK 17 — Web source (WebView2) — shipped 2026-08-28; enables alert ecosystem.
  7. ✅ TASK 18 — Local recording — shipped 2026-08-29 (running-app verification pending in handoff).
  8. 🔶 TASK 21 — Media source — video file playback for non-Live scenes. Remaining: the UI picker slice (handoff spec in HANDOFF.md).
  9. ✅ TASK 22 — Audio sync offset — shipped 2026-08-31 (status reconciled 2026-09-01).
  10. ✅ TASK 15 — Stock bg images — all 5 scenes seeded (Starting, BRB, Live, Chat, Ending). EnsureDefaultBackdrop() runs on every scene switch.
  11. TASK 32 — Stream resilience (blip retry → grace countdown on the ON-AIR sign → one-click Back on air → crash-safe recording → pre-flight).
  12. TASK 33 — Bandwidth auto step-down (depends on TASK 32's restart machinery).
  13. TASK 34 — Scheduled streams (Text-drawer schedule + adoption at Start).
  14. TASK 35 — Scene-linked audio ("scenes remember the room").
  15. Text source (TASK 3 item 17) + the monetization-awareness chain + Alerts (TASK 10 related work → TASK 3 item 20) — capture → report → journey → alerts, one slice per integration test.
  16. TASK 13 — Social media launch kit (MARCOM) — after features, before gold.
  17. TASK 36 — Gold pass, always last: visibility unlock + flash-live enable + screens/layers audit + native verification suite + expiry reminders + signing/installer/EULA/Velopack.