diff --git a/TASKS.md b/TASKS.md index 7576b18..bcd9695 100644 --- a/TASKS.md +++ b/TASKS.md @@ -973,7 +973,7 @@ click (volume sliders keep their manual `SetSliderValueFromClick`, harmless dupl **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; remaining = running-app verification +### Status: ✅ Shipped `a9eb360` (2026-08-29) — code done (incl. manual-rename modal), build 0 warnings, 244/246 tests; **running-app verification IN PROGRESS (2026-09-01 takes 1–2)**: files now actually land (dead ffmpeg pin re-pinned month-end, locator wrap), webcam-in-output + top-bar reachability + audio-loop NREs fixed en route; take 3 pending with pump stage-timing to resolve the ~2fps producer starvation (time-lapse/short-file/garbled-audio = one root cause, `FramePump stats` will name the stage) 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` diff --git a/ai.md b/ai.md index e5540ca..c80da0f 100644 --- a/ai.md +++ b/ai.md @@ -125,8 +125,9 @@ FfmpegEncoderTests, FramePumpTests, the TASK 8/11/12 audio chain (AudioPipelineT the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegrationTests (TASK 24), GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests (TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests -(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests (2026-09-01 -rollback) — **ZERO known failures as of 2026-09-01. `AudioPipelineTests` 25/25 green in 26ms — +(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests + +WebcamOutputKeyTests + GameMeterHonestyTests (2026-09-01 recording-verification fixes) — **ZERO +known failures as of 2026-09-01. `AudioPipelineTests` 25/25 green in 26ms — the "known failing" `Mix_HonorsProviderGains…` and the class's notorious STANDALONE HANG shared one root cause: TASK 22's `_delayedMix` (nullable, never initialized) was dereferenced (`delayed.Length` on null) every live-mix tick — a swallowed NRE starved the pipe the tests @@ -539,11 +540,17 @@ until `IsConnected`). The pill is intent; `IsRecording`/`IsLive` are reality — green when a session is actually recording, the ON-AIR dot when actually live. - **State model (`MainViewModel`):** pills `RecordPillOn`/`OnAirPillOn` (ON-AIR setter no-ops if - `!CanToggleOnAir`), derived `PrimaryButtonText` ("Start Streaming" vs "Start Recording"), `ShowPrimaryStartButton` - (offline + [connected OR record-only]), `IsStreamingStart`, `ShowEndStreamButton`, `CanStartSession`, - `AccountStatusLightToolTip`. `IsEditMode` now also requires `!IsRecording` (lock scrubbing while recording). - `StartSession()` routes: ON-AIR on → GoLive dialog then stream (± record); only REC → `BeginRecordOnly()` - starts the pipeline directly, no YouTube dialog. + `!CanToggleOnAir`), `ShowPrimaryStartButton` = **always the idle face** (`IsOffline && !IsRecording`, + 2026-09-01 — the old connected/record-only gate blanked the bar after stopping signed-out), + `ShowEndStreamButton`, `CanStartSession`, `AccountStatusLightToolTip`. Sign-in is a **context-menu + item on Start** ("Sign in to YouTube" → `SignInCommand`, visible while disconnected) — the standalone + Sign In button is gone (TASK 30's single-button rule, completed). `StartSession()` routes: ON-AIR on + → GoLive dialog then stream (± record); REC on or **nothing armed** → `BeginRecordOnly()` (unarmed + Start lights the REC pill and records — no dead-end no-ops; local recording needs no account). + `IsEditMode` also requires `!IsRecording` (lock scrubbing while recording). + **Top bar order (creator spec 2026-09-01):** `[sign light] REC [pill] [sign light] ON-AIR [pill]` + — each reality lamp sits before its own intent switch. **Stop ends everything:** `StopStream` + clears both pills (see "Stop ends everything; failures roll back" below). - **Filenames** (pure `Services/RecordingFile`): auto-name `ty---0000.mp4` at start; **rename-on-stop** to `ty-…-.mp4` (`FinalizeRecordingAsync` after the pump stops & the file closes), numeric `-2`/`-3` suffix on collision (`UniquePath`). Length comes from `_liveElapsed`, @@ -666,7 +673,10 @@ devices, no timers). The **`FramePump`** (`Services/Encoder/`) is the live frame producer: while live it snapshots the active scene each tick, resolves every element to its latest frame, composites it into the tier's output frame, -and paces frames into the encoder at the tier's FPS. **Pattern — everything is a constructor-injected +and paces frames into the encoder at the tier's FPS. **rawvideo is stamped by ARRIVAL:** ffmpeg assigns +pts from frame order at the declared fps — supply rate = output speed. A starved producer (pump < fps) +ships a time-lapse, truncated file with NO error (take-2 lesson, 2026-09-01); the pump therefore logs +`FramePump stats: n/target frames per 5s, avg render Xms submit Yms` so a slow stage names itself. **Pattern — everything is a constructor-injected seam:** `Func`, `Func` resolver, `Func`, `Func`, `Func`, `Action` log, and an injectable pacing delay (default `Task.Delay`; tests inject `Task.Yield`). The pump is free of WPF and of the capture managers. @@ -690,7 +700,10 @@ seam:** `Func`, `Func` resolver, `Func