using System.Diagnostics; using System.IO; using ytLive.Services.Encoder; namespace ytLive.Services; /// /// TASK 21: decodes a local media file (any format ffmpeg handles) into /// s for a MediaSource. Spawns ffmpeg with /// -f rawvideo -pix_fmt bgra, reads the raw BGRA pipe via /// , and raises per /// frame. The decoder process is a seam () 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). /// public sealed class MediaVideoSource : IDisposable { public event Action? 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 */ } } }