docs: record take-two outcomes — arrival-stamping rule, resolver key fix, top-bar model, TASK 18 verification status

Map corrections: ai.md encoded the webcam lookup bug verbatim (GetLatestFrame(WebcamId))
— code wins, line rewritten; frame-pipeline section gains the rawvideo arrival-stamping
lesson + stats log; TASK 18 status = running-app verification IN PROGRESS with take-3
pending on the pump starvation; state-model paragraph matches the new always-Start bar.
This commit is contained in:
2026-09-01 22:27:08 -07:00
parent 89fee6ca4a
commit af0d372b65
2 changed files with 23 additions and 10 deletions
+1 -1
View File
@@ -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. **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) 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` 2. ✅ `FfmpegArgs.Build` reworked into per-output blocks (stream `-f flv`, record `-f mp4`) via `AddVideoTags`
+22 -9
View File
@@ -125,8 +125,9 @@ FfmpegEncoderTests, FramePumpTests, the TASK 8/11/12 audio chain (AudioPipelineT
the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegrationTests (TASK 24), the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegrationTests (TASK 24),
GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests
(TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests (TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests
(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests (2026-09-01 (TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests +
rollback) — **ZERO known failures as of 2026-09-01. `AudioPipelineTests` 25/25 green in 26ms — 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 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 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 (`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. 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 - **State model (`MainViewModel`):** pills `RecordPillOn`/`OnAirPillOn` (ON-AIR setter no-ops if
`!CanToggleOnAir`), derived `PrimaryButtonText` ("Start Streaming" vs "Start Recording"), `ShowPrimaryStartButton` `!CanToggleOnAir`), `ShowPrimaryStartButton` = **always the idle face** (`IsOffline && !IsRecording`,
(offline + [connected OR record-only]), `IsStreamingStart`, `ShowEndStreamButton`, `CanStartSession`, 2026-09-01 — the old connected/record-only gate blanked the bar after stopping signed-out),
`AccountStatusLightToolTip`. `IsEditMode` now also requires `!IsRecording` (lock scrubbing while recording). `ShowEndStreamButton`, `CanStartSession`, `AccountStatusLightToolTip`. Sign-in is a **context-menu
`StartSession()` routes: ON-AIR on → GoLive dialog then stream (± record); only REC → `BeginRecordOnly()` item on Start** ("Sign in to YouTube" → `SignInCommand`, visible while disconnected) — the standalone
starts the pipeline directly, no YouTube dialog. 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-<yyyyMMdd>-<HHmm start>-0000.mp4` at start; - **Filenames** (pure `Services/RecordingFile`): auto-name `ty-<yyyyMMdd>-<HHmm start>-0000.mp4` at start;
**rename-on-stop** to `ty-…-<hh2mm2 actual length>.mp4` (`FinalizeRecordingAsync` after the pump stops & **rename-on-stop** to `ty-…-<hh2mm2 actual length>.mp4` (`FinalizeRecordingAsync` after the pump stops &
the file closes), numeric `-2`/`-3` suffix on collision (`UniquePath`). Length comes from `_liveElapsed`, 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 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, 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<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<CompositorOptions>`, seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<CompositorOptions>`,
`Func<EncoderOptions?>`, `Func<IFfmpegEncoder>`, `Action<string>` log, and an injectable pacing delay `Func<EncoderOptions?>`, `Func<IFfmpegEncoder>`, `Action<string>` log, and an injectable pacing delay
(default `Task.Delay`; tests inject `Task.Yield`). The pump is free of WPF and of the capture managers. (default `Task.Delay`; tests inject `Task.Yield`). The pump is free of WPF and of the capture managers.
@@ -690,7 +700,10 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
from a previous session; bitrate/FPS stay on the tier's targets (`ApplyStreamQuality`). The bar shows from a previous session; bitrate/FPS stay on the tier's targets (`ApplyStreamQuality`). The bar shows
real encoder values since TASK 9 (2026-08-16) wired the reusable stream URL into `_rtmpUrlProvider`. real encoder values since TASK 9 (2026-08-16) wired the reusable stream URL into `_rtmpUrlProvider`.
- **`MainViewModel` owns the resolver** (`ResolveOutputFrame`): `WebcamSceneConfig` → - **`MainViewModel` owns the resolver** (`ResolveOutputFrame`): `WebcamSceneConfig` →
`CameraManager.GetLatestFrame(WebcamId)`, `Source { IsLiveCapture, CaptureKey }` → `CameraManager.GetLatestFrame(DeviceKeyForWebcam(WebcamId))` — **device-keyed** (configs carry the
identity GUID; sessions key on the Windows device id — the old direct-GUID lookup returned null
forever and the webcam was structurally absent from output; take-2 fix 2026-09-01,
`WebcamOutputKeyTests`), `Source { IsLiveCapture, CaptureKey }` →
`ScreenCaptureManager.GetLatestFrame(CaptureKey)` (the new accessor mirroring `CameraManager`), image/ `ScreenCaptureManager.GetLatestFrame(CaptureKey)` (the new accessor mirroring `CameraManager`), image/
background → `StaticPixelCache.Get(AssetId)`. `BuildCompositorOptions` rounds the VM's `OutputRect*` background → `StaticPixelCache.Get(AssetId)`. `BuildCompositorOptions` rounds the VM's `OutputRect*`
doubles to ints — the vertical 607.5 half-pixel crop rounds to a perfectly-centered **608** (`Math.Round`, doubles to ints — the vertical 607.5 half-pixel crop rounds to a perfectly-centered **608** (`Math.Round`,