8f490102ee
Adds the codec-agnostic decoder half of the media source. Spawns ffmpeg with -f rawvideo -pix_fmt bgra (reusing the already-shipped ffmpeg via IFfmpegLocator) and drains the raw BGRA stdout pipe into VideoFrames. - Services/RawVideoFrameReader.cs: pure rawvideo BGRA stream -> frames (partial reads kept across Feed; no ffmpeg needed to test) - Services/IDecodeProcess.cs + FfmpegDecodeProcess.cs: binary-stdout subprocess seam, mirror of the encoder's IEncoderProcess - Services/MediaVideoSource.cs: owns the decode, raises FrameReady/Completed - MediaVideoSourceTests: 3 pure reader + 1 integration (fake decode process through the real source loop, frames in order) Reference (external scan): ffmpeg rawvideo pipe decode is the canonical codec-agnostic frame feeds pattern (ffmpeg docs -f rawvideo; how OBS/media pipelines push frames to a compositor). Verified: 4/4 tests, build 0 warnings. Native-FPS pacing + resolver/compositor wiring are the next slice.
118 lines
3.5 KiB
C#
118 lines
3.5 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using ytLive.Services.Encoder;
|
|
|
|
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).
|
|
/// </summary>
|
|
public sealed class MediaVideoSource : IDisposable
|
|
{
|
|
public event Action<VideoFrame>? FrameReady;
|
|
public event Action? Completed;
|
|
|
|
private readonly string _path;
|
|
private readonly int _width;
|
|
private readonly int _height;
|
|
private readonly IFfmpegLocator _locator;
|
|
private readonly IDecodeProcess _process;
|
|
private readonly CancellationTokenSource _cts = new();
|
|
private IDisposable? _run;
|
|
|
|
public MediaVideoSource(
|
|
string path,
|
|
int width,
|
|
int height,
|
|
IFfmpegLocator locator,
|
|
IDecodeProcess process)
|
|
{
|
|
_path = path;
|
|
_width = width;
|
|
_height = height;
|
|
_locator = locator;
|
|
_process = process;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (_run != null) return;
|
|
_run = _cts.Token.Register(StopInternal);
|
|
_ = Task.Run(RunAsync);
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
_cts.Cancel();
|
|
_run = null;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
_process.Dispose();
|
|
}
|
|
|
|
private void StopInternal() => KillProcess();
|
|
|
|
private async Task RunAsync()
|
|
{
|
|
try
|
|
{
|
|
var ffmpegPath = await _locator.LocateAsync(_cts.Token).ConfigureAwait(false);
|
|
var args =
|
|
$"-hide_banner -loglevel error -i \"{_path}\" " +
|
|
$"-f rawvideo -pix_fmt bgra -vf scale={_width}:{_height} -an -";
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = ffmpegPath,
|
|
Arguments = args,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true,
|
|
};
|
|
_process.Start(startInfo);
|
|
|
|
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);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Fail($"Media decode failed: {ex.Message}");
|
|
return;
|
|
}
|
|
|
|
if (!_cts.IsCancellationRequested)
|
|
Completed?.Invoke();
|
|
}
|
|
|
|
private void Fail(string message) => Console.Error.WriteLine(message);
|
|
|
|
private void KillProcess()
|
|
{
|
|
try { _process.Kill(); } catch { /* already exited */ }
|
|
}
|
|
}
|