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
+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),
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-<yyyyMMdd>-<HHmm start>-0000.mp4` at start;
**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`,
@@ -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<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<CompositorOptions>`,
`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.
@@ -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
real encoder values since TASK 9 (2026-08-16) wired the reusable stream URL into `_rtmpUrlProvider`.
- **`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/
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`,