TASK 21 slice 1 step 3: IMediaFrameSource + MediaVideoSourceManager
Refactor MediaVideoSource onto IMediaFrameSource (Key/FrameAvailable/Completed/ StartAsync/StopAsync, renaming Start->StartAsync, FrameReady->FrameAvailable), behavior preserved. Add MediaVideoSourceManager: app-wide decode-session owner refcounted by MediaPath with Func<string,IMediaFrameSource?> factory seam, Acquire/Release/ReleaseAll/GetLatestFrame, coalescing each file's frames onto the UI dispatcher onto one shared WriteableBitmap; MediaFailed + PreviewBitmapChanged. Mirrors ScreenCaptureManager (screen-capture session ownership + dispatcher coalescing), per the codebase precedent and derivative-work rule. Tests: MediaVideoSourceManagerTests (5 unit + 1 integration: single shared bitmap, coalesce-to-latest); MediaVideoSourceTests updated for renames. 10/10 media tests pass, build 0 warnings. Docs (TASKS/HANDOFF/ai.md) updated.
This commit is contained in:
@@ -6,19 +6,20 @@ namespace ytLive.Services;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 21: decodes a local media file (any format ffmpeg handles) into
|
||||
/// <see cref="VideoFrame"/>s for a MediaSource. Spawns ffmpeg with
|
||||
/// <c>-f rawvideo -pix_fmt bgra</c>, reads the raw BGRA pipe via
|
||||
/// <see cref="RawVideoFrameReader"/>, and raises <see cref="FrameReady"/> per
|
||||
/// frame. The decoder process is a seam (<see cref="IDecodeProcess"/>) so tests
|
||||
/// run without a real codec or audio device. Frames are emitted as fast as the
|
||||
/// pipe produces them; native-FPS pacing is wired by the caller via a probe in a
|
||||
/// later unit (TASK 21 step 5 pacing).
|
||||
/// <see cref="VideoFrame"/>s for a MediaSource. Implements
|
||||
/// <see cref="IMediaFrameSource"/>: raises <see cref="FrameAvailable"/> per
|
||||
/// frame and <see cref="IMediaFrameSource.Completed"/> on natural EOF. Spawns
|
||||
/// ffmpeg with <c>-f rawvideo -pix_fmt bgra</c> and drains the raw BGRA pipe via
|
||||
/// <see cref="RawVideoFrameReader"/>. The decoder process and emitters are seams
|
||||
/// so tests run without a real codec or audio device.
|
||||
/// </summary>
|
||||
public sealed class MediaVideoSource : IDisposable
|
||||
public sealed class MediaVideoSource : IMediaFrameSource
|
||||
{
|
||||
public event Action<VideoFrame>? FrameReady;
|
||||
public event Action<VideoFrame>? FrameAvailable;
|
||||
public event Action? Completed;
|
||||
|
||||
public string Key => _path;
|
||||
|
||||
private readonly string _path;
|
||||
private readonly int _width;
|
||||
private readonly int _height;
|
||||
@@ -41,17 +42,19 @@ public sealed class MediaVideoSource : IDisposable
|
||||
_process = process;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_run != null) return;
|
||||
if (_run != null) return Task.CompletedTask;
|
||||
_run = _cts.Token.Register(StopInternal);
|
||||
_ = Task.Run(RunAsync);
|
||||
_ = Task.Run(RunAsync, CancellationToken.None);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
public Task StopAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_run = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -85,14 +88,12 @@ public sealed class MediaVideoSource : IDisposable
|
||||
var reader = new RawVideoFrameReader(_width, _height);
|
||||
var buffer = new byte[65536];
|
||||
var stream = _process.StandardOutput;
|
||||
byte[] readChunk;
|
||||
while (!_cts.IsCancellationRequested && !_process.HasExited)
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token).ConfigureAwait(false);
|
||||
if (read <= 0) break;
|
||||
readChunk = buffer.AsSpan(0, read).ToArray();
|
||||
foreach (var frame in reader.Feed(readChunk))
|
||||
FrameReady?.Invoke(frame);
|
||||
foreach (var frame in reader.Feed(buffer.AsSpan(0, read).ToArray()))
|
||||
FrameAvailable?.Invoke(frame);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
Reference in New Issue
Block a user