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:
@@ -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`
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
Reference in New Issue
Block a user