TASK 21 slice 3: loop control in MediaVideoSource (process factory + Loop flag)
- IMediaFrameSource gains bool Looping. - MediaVideoSource ctor takes Func<IDecodeProcess> processFactory instead of a single IDecodeProcess: a System.Diagnostics.Process can't be re-Start()ed, so each loop pass creates a fresh decoder. Decode wrapped in do-while(Looping): restart on natural EOF instead of raising Completed. - Production wiring (MainViewModel media factory): passes the process factory AND FfmpegFrameRateProbe -- closes the slice-2b gap where production had no probe and therefore no pacing. - Tests: loop test (single frame re-emits across passes, Completed only when loop cleared); fakes updated for the new interface member. Media tests 12/12, build 0 warnings. Wiring Source.MediaIsLooping into the flag needs a manager-level per-path loop provider -> lands with the UI-picker (acquisition) slice. Derivative reference: looping media by restarting decode on EOF, standard in playback/overlay tooling (OBS media source repeat).
This commit is contained in:
@@ -20,6 +20,7 @@ public class MediaVideoSourceManagerTests
|
||||
private readonly List<string>? _stopped;
|
||||
|
||||
public string Key { get; }
|
||||
public bool Looping { get; set; }
|
||||
public event Action<VideoFrame>? FrameAvailable;
|
||||
public event Action? Completed;
|
||||
|
||||
@@ -50,6 +51,7 @@ public class MediaVideoSourceManagerTests
|
||||
private sealed class FailingMediaSource : IMediaFrameSource
|
||||
{
|
||||
public string Key { get; }
|
||||
public bool Looping { get; set; }
|
||||
public event Action<VideoFrame>? FrameAvailable;
|
||||
public event Action? Completed;
|
||||
public FailingMediaSource(string key) => Key = key;
|
||||
|
||||
@@ -62,7 +62,7 @@ public class MediaVideoSourceTests
|
||||
using var source = new MediaVideoSource(
|
||||
"clip.mp4", w, h,
|
||||
new FakeLocator(),
|
||||
new FakeDecodeProcess(payload));
|
||||
() => new FakeDecodeProcess(payload));
|
||||
|
||||
var seen = new List<byte[]>();
|
||||
var completed = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
@@ -100,7 +100,7 @@ public class MediaVideoSourceTests
|
||||
using var source = new MediaVideoSource(
|
||||
"clip.mp4", w, h,
|
||||
new FakeLocator(),
|
||||
new FakeDecodeProcess(payload),
|
||||
() => new FakeDecodeProcess(payload),
|
||||
frameRateProbe: new FakeFrameRateProbe(1000.0),
|
||||
delay: (_d, _) => { delays.Add(_d); return Task.CompletedTask; });
|
||||
|
||||
@@ -117,6 +117,37 @@ public class MediaVideoSourceTests
|
||||
$"expected ~1ms, got {d.TotalMilliseconds}ms");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MediaVideoSource_LoopsUntilLoopDisabled()
|
||||
{
|
||||
const int w = 1, h = 1; // frame bytes = 4
|
||||
var payload = new byte[4];
|
||||
var passCount = 0;
|
||||
var frames = 0;
|
||||
var completed = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
using var source = new MediaVideoSource(
|
||||
"clip.mp4", w, h,
|
||||
new FakeLocator(),
|
||||
() => { passCount++; return new FakeDecodeProcess(payload); });
|
||||
source.Looping = true;
|
||||
source.Completed += () => completed.TrySetResult(true);
|
||||
source.FrameAvailable += _ =>
|
||||
{
|
||||
frames++;
|
||||
if (frames >= 3) source.Looping = false;
|
||||
};
|
||||
|
||||
await source.StartAsync();
|
||||
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await source.StopAsync();
|
||||
|
||||
// The single frame re-emits across passes (restart) and Completed only
|
||||
// fires once Looping is cleared.
|
||||
Assert.True(frames >= 3, $"expected the single frame to re-emit across passes, got {frames}");
|
||||
Assert.True(passCount >= 3, "Looping should have restarted the decode process");
|
||||
}
|
||||
|
||||
private sealed class FakeFrameRateProbe : IFrameRateProbe
|
||||
{
|
||||
private readonly double? _fps;
|
||||
|
||||
Reference in New Issue
Block a user