using System.Diagnostics; using System.IO; using Xunit; using ytLive.Services; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// TASK 21: the media decoder path. is pure /// (synthetic bytes, no ffmpeg); the one integration test drives the real /// source loop against a fake decoder process. /// public class MediaVideoSourceTests { // ─── Pure reader (unit) ─── [Fact] public void Reader_EmitsOneFramePerFrameBytes() { var w = 2; var h = 2; var reader = new RawVideoFrameReader(w, h); var frameBytes = new byte[w * h * 4]; var frames = reader.Feed(frameBytes); Assert.Single(frames); Assert.Equal(w, frames[0].Width); Assert.Equal(h, frames[0].Height); } [Fact] public void Reader_SplitsMultipleFramesFromOneFeed() { var reader = new RawVideoFrameReader(1, 1); // 4 bytes each var frames = reader.Feed(new byte[12]); // 3 frames Assert.Equal(3, frames.Count); } [Fact] public void Reader_KeepsPartialBytesAcrossFeeds() { var reader = new RawVideoFrameReader(1, 1); Assert.Empty(reader.Feed(new byte[2])); // half a frame Assert.Empty(reader.Feed(new byte[1])); // 3/4 var frames = reader.Feed(new byte[1]); // completes the frame Assert.Single(frames); Assert.Empty(reader.Feed(new byte[0])); // no leftover } // ─── The one integration test: source loop against a fake decode process ─── [Fact] public async Task MediaVideoSource_EmitsFramesInOrderFromFakeDecoder() { const int w = 2, h = 1; // frame bytes = 8 var frameA = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; var frameB = new byte[] { 9, 10, 11, 12, 13, 14, 15, 16 }; var payload = new byte[frameA.Length + frameB.Length]; Buffer.BlockCopy(frameA, 0, payload, 0, frameA.Length); Buffer.BlockCopy(frameB, 0, payload, frameA.Length, frameB.Length); using var source = new MediaVideoSource( "clip.mp4", w, h, new FakeLocator(), new FakeDecodeProcess(payload)); var seen = new List(); var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); source.FrameAvailable += f => seen.Add(((byte[])f.BgraPixels.Clone())); source.Completed += () => completed.TrySetResult(true); await source.StartAsync(); await completed.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.Equal(2, seen.Count); Assert.Equal(frameA, seen[0]); Assert.Equal(frameB, seen[1]); } private sealed class FakeDecodeProcess : IDecodeProcess { private readonly MemoryStream _stream; public FakeDecodeProcess(byte[] bytes) => _stream = new MemoryStream(bytes); public void Start(ProcessStartInfo startInfo) { } public Stream StandardOutput => _stream; public bool HasExited => _stream.Position >= _stream.Length; public int ExitCode => 0; public void Kill() { } public Task WaitForExitAsync(CancellationToken ct = default) => Task.CompletedTask; public void Dispose() => _stream.Dispose(); } private sealed class FakeLocator : IFfmpegLocator { public Task LocateAsync(CancellationToken ct = default) => Task.FromResult("ffmpeg.exe"); } }