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
+16 -1
View File
@@ -25,6 +25,8 @@ public sealed class MediaVideoSource : IMediaFrameSource
private readonly int _height;
private readonly IFfmpegLocator _locator;
private readonly IDecodeProcess _process;
private readonly IFrameRateProbe? _probe;
private readonly Func<TimeSpan, CancellationToken, Task> _delay;
private readonly CancellationTokenSource _cts = new();
private IDisposable? _run;
@@ -33,13 +35,17 @@ public sealed class MediaVideoSource : IMediaFrameSource
int width,
int height,
IFfmpegLocator locator,
IDecodeProcess process)
IDecodeProcess process,
IFrameRateProbe? frameRateProbe = null,
Func<TimeSpan, CancellationToken, Task>? delay = null)
{
_path = path;
_width = width;
_height = height;
_locator = locator;
_process = process;
_probe = frameRateProbe;
_delay = delay ?? ((t, ct) => Task.Delay(t, ct));
}
public Task StartAsync(CancellationToken cancellationToken = default)
@@ -70,6 +76,11 @@ public sealed class MediaVideoSource : IMediaFrameSource
{
try
{
double? frameRate = null;
if (_probe != null)
frameRate = await _probe.ProbeAsync(_path, _cts.Token).ConfigureAwait(false);
var frameDuration = frameRate is > 0 ? TimeSpan.FromSeconds(1.0 / frameRate.Value) : TimeSpan.Zero;
var ffmpegPath = await _locator.LocateAsync(_cts.Token).ConfigureAwait(false);
var args =
$"-hide_banner -loglevel error -i \"{_path}\" " +
@@ -93,7 +104,11 @@ public sealed class MediaVideoSource : IMediaFrameSource
var read = await stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token).ConfigureAwait(false);
if (read <= 0) break;
foreach (var frame in reader.Feed(buffer.AsSpan(0, read).ToArray()))
{
FrameAvailable?.Invoke(frame);
if (frameDuration > TimeSpan.Zero)
await _delay(frameDuration, _cts.Token).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)