TASK 21 slice 2a: native-FPS probe seam (ffprobe parse + derive sibling)

- FfmpegFrameRateParser (pure): prefers avg_frame_rate= then r_frame_rate=,
  rational N/N/M, unknown/0 -> null.
- IFrameRateProbe + FfmpegFrameRateProbe: derives sibling ffprobe.exe from the
  located ffmpeg dir, reuses the IDecodeProcess seam for the ffprobe subprocess
  text; null if ffprobe absent.
- FfmpegLocator now also extracts ffprobe.exe (ProbeFileName) from the pinned
  archive, conditional so old caches without it degrade to no pacing.
- Tests: 6 pure parser units + 1 probe integration via fake locator/process;
  FfmpegLocatorTests still green. 15/15, 0 warnings.

Pacing (probe->delay) is slice 2b. Derivative reference: standard ffprobe
avg_frame_rate probing used across OBS/media tooling.
This commit is contained in:
2026-08-31 19:45:48 -07:00
parent 071ec8be0c
commit 8fa78423e6
7 changed files with 267 additions and 4 deletions
+6 -1
View File
@@ -683,7 +683,12 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
the pipe produces them. Wired into the live pipeline (slice 1 step 4): `ResolveOutputFrame` reads
`_mediaManager.GetLatestFrame(MediaPath)` for a `MediaSource`, and `Source.DisplaySource`
(`Models/Source.cs`) routes `VideoImageSource` for `MediaSource` so the preview/canvas show decoded video.
Still open: session acquisition on add/remove (UI picker), native-FPS pacing (ffprobe probe), loop control.
Slice 2a shipped the probe seam: `FfmpegFrameRateParser` (pure, `avg_frame_rate=` then `r_frame_rate=`
rational N/N/M, unknown→null) + `IFrameRateProbe`/`FfmpegFrameRateProbe` (derives sibling `ffprobe.exe`
from the located ffmpeg dir, reuses the `IDecodeProcess` seam for the ffprobe subprocess text; null if
ffprobe absent) + `FfmpegLocator.ProbeFileName` now also extracts `ffprobe.exe` from the pinned archive
(conditional — old caches without it just get no pacing).
Still open: session acquisition on add/remove (UI picker), native-FPS pacing injection (probe→delay, slice 2b), loop control.
- **Social bar on the output (bar bug-fix branch):** the `FramePump` takes an optional
`socialBar: Func<(VideoFrame? Frame, SocialBarPosition Position)>?` seam, re-read **every frame** (so a
mid-stream position flip applies immediately). The strip is pre-rasterized by `Compositor/SocialBarRenderer.cs`