TASK 21 slice 2b: native-FPS pacing in MediaVideoSource (probe -> delay seam)

- MediaVideoSource takes optional IFrameRateProbe? + Func<TimeSpan,CancellationToken,Task>? delay
  seams (default Task.Delay); probes FPS once in RunAsync, delays by 1/fps after each
  emitted frame. No probe/unknown fps -> no pacing (ffmpeg pipe backpressure throttles).
- Test: MediaVideoSource_PacesFramesByProbedFps (fake probe returns 1000fps + recording
  delay; one delay per frame ~= 1ms). Media tests 5/5, build 0 warnings.

Derivative reference: per-frame delay pacing of decoded output, standard in media playback.
This commit is contained in:
2026-08-31 19:47:38 -07:00
parent 8fa78423e6
commit a2219be198
5 changed files with 68 additions and 12 deletions
+35
View File
@@ -90,6 +90,41 @@ public class MediaVideoSourceTests
public void Dispose() => _stream.Dispose();
}
[Fact]
public async Task MediaVideoSource_PacesFramesByProbedFps()
{
const int w = 2, h = 1; // frame bytes = 8
var payload = new byte[16]; // two frames
var delays = new List<TimeSpan>();
using var source = new MediaVideoSource(
"clip.mp4", w, h,
new FakeLocator(),
new FakeDecodeProcess(payload),
frameRateProbe: new FakeFrameRateProbe(1000.0),
delay: (_d, _) => { delays.Add(_d); return Task.CompletedTask; });
var completed = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
source.Completed += () => completed.TrySetResult(true);
await source.StartAsync();
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
// One pacing delay per emitted frame, each = 1/1000s.
Assert.Equal(2, delays.Count);
foreach (var d in delays)
Assert.True(Math.Abs((d - TimeSpan.FromMilliseconds(1)).TotalMilliseconds) < 0.001,
$"expected ~1ms, got {d.TotalMilliseconds}ms");
}
private sealed class FakeFrameRateProbe : IFrameRateProbe
{
private readonly double? _fps;
public FakeFrameRateProbe(double? fps) => _fps = fps;
public Task<double?> ProbeAsync(string path, CancellationToken ct = default)
=> Task.FromResult(_fps);
}
private sealed class FakeLocator : IFfmpegLocator
{
public Task<string> LocateAsync(CancellationToken ct = default) => Task.FromResult("ffmpeg.exe");