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.
This commit is contained in:
2026-09-05 16:31:46 -07:00
parent 98d30c2056
commit 87509bcf99
36 changed files with 1698 additions and 1615 deletions
+58
View File
@@ -0,0 +1,58 @@
# TASK 18 — Local recording
> Catalog: [`TASKS.md`](../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.