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