using System.Threading; using System.Windows.Media.Imaging; using System.Windows.Threading; using Xunit; using ytLive.Services; namespace ytLive.Tests; /// /// MediaVideoSourceManager owns media-file decode sessions, refcounted by file /// path and coalesced onto the UI dispatcher — a structural mirror of /// ScreenCaptureManager. These tests pin that contract with a fake /// IMediaFrameSource (the real ffmpeg decode is exercised on Windows at runtime). /// public class MediaVideoSourceManagerTests { private sealed class FakeMediaSource : IMediaFrameSource { private readonly List? _started; private readonly List? _stopped; public string Key { get; } public bool Looping { get; set; } public event Action? FrameAvailable; public event Action? Completed; public FakeMediaSource(string key, List? started = null, List? stopped = null) { Key = key; _started = started; _stopped = stopped; } public Task StartAsync(CancellationToken cancellationToken = default) { _started?.Add(Key); return Task.CompletedTask; } public Task StopAsync() { _stopped?.Add(Key); return Task.CompletedTask; } public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame); public void Complete() => Completed?.Invoke(); public void Dispose() { } } private sealed class FailingMediaSource : IMediaFrameSource { public string Key { get; } public bool Looping { get; set; } public event Action? FrameAvailable; public event Action? Completed; public FailingMediaSource(string key) => Key = key; public Task StartAsync(CancellationToken cancellationToken = default) => throw new InvalidOperationException("access denied"); public Task StopAsync() => Task.CompletedTask; public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame); public void Complete() => Completed?.Invoke(); public void Dispose() { } } private sealed class DispatcherPump : IDisposable { private readonly Thread _thread; private readonly ManualResetEventSlim _ready = new(false); private Dispatcher? _dispatcher; public Dispatcher Dispatcher => _dispatcher!; public DispatcherPump() { _thread = new Thread(() => { _dispatcher = Dispatcher.CurrentDispatcher; _ready.Set(); Dispatcher.Run(); }); _thread.IsBackground = true; _thread.SetApartmentState(ApartmentState.STA); _thread.Start(); _ready.Wait(); } public void Drain() => Dispatcher.Invoke(() => { }, DispatcherPriority.ContextIdle); public void Dispose() { Dispatcher.InvokeShutdown(); _thread.Join(3000); } } private static byte[] Pixels(params byte[] raw) => raw; [Fact] public async Task Refcount_TwoAcquires_OneDecoderUntilLastRelease() { var started = new List(); var stopped = new List(); var manager = new MediaVideoSourceManager(key => new FakeMediaSource(key, started, stopped)); Assert.True(await manager.AcquireAsync(@"C:\videos\clip.mp4")); Assert.True(await manager.AcquireAsync(@"C:\videos\clip.mp4")); Assert.Single(started); await manager.ReleaseAsync(@"C:\videos\clip.mp4"); Assert.Empty(stopped); await manager.ReleaseAsync(@"C:\videos\clip.mp4"); Assert.Single(stopped); } [Fact] public async Task ReleaseAll_StopsEvenWithMultipleRefs() { var started = new List(); var stopped = new List(); var manager = new MediaVideoSourceManager(key => new FakeMediaSource(key, started, stopped)); await manager.AcquireAsync(@"C:\videos\clip.mp4"); await manager.AcquireAsync(@"C:\videos\clip.mp4"); await manager.ReleaseAllAsync(@"C:\videos\clip.mp4"); Assert.Single(stopped); } [Fact] public async Task Acquire_FailingSource_ReturnsFalseAndRaisesMediaFailed() { var manager = new MediaVideoSourceManager(key => new FailingMediaSource(key)); string? failedKey = null; manager.MediaFailed += (key, _) => failedKey = key; Assert.False(await manager.AcquireAsync(@"C:\videos\clip.mp4")); Assert.Equal(@"C:\videos\clip.mp4", failedKey); } [Fact] public async Task Acquire_EmptyKey_ReturnsFalse() { var manager = new MediaVideoSourceManager(key => new FakeMediaSource(key)); Assert.False(await manager.AcquireAsync(" ")); } [Fact] public async Task GetLatestFrame_ReturnsLatestPump_UntilReleased() { FakeMediaSource? captured = null; var manager = new MediaVideoSourceManager(key => captured = new FakeMediaSource(key)); Assert.Null(manager.GetLatestFrame(@"C:\videos\clip.mp4")); Assert.True(await manager.AcquireAsync(@"C:\videos\clip.mp4")); Assert.Null(manager.GetLatestFrame(@"C:\videos\clip.mp4")); var first = new VideoFrame(2, 2, Pixels(1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255)); var second = new VideoFrame(2, 2, Pixels(5, 0, 0, 255, 6, 0, 0, 255, 7, 0, 0, 255, 8, 0, 0, 255)); captured!.Pump(first); captured.Pump(second); Assert.Same(second, manager.GetLatestFrame(@"C:\videos\clip.mp4")); await manager.ReleaseAsync(@"C:\videos\clip.mp4"); Assert.Null(manager.GetLatestFrame(@"C:\videos\clip.mp4")); } // The one integration test for this branch: one file creates one shared // WriteableBitmap, published once, and back-to-back frames coalesce to the // latest (a single pending UI copy per session) — mirror of ScreenCapture. [Fact] public async Task Frames_ShareOneBitmap_AndCoalesceToLatest() { using var pump = new DispatcherPump(); FakeMediaSource? captured = null; var manager = new MediaVideoSourceManager(key => captured = new FakeMediaSource(key), pump.Dispatcher); WriteableBitmap? published = null; var publishedCount = 0; manager.PreviewBitmapChanged += (_, bitmap) => { published = bitmap; publishedCount++; }; Assert.True(await manager.AcquireAsync(@"C:\videos\clip.mp4")); var first = new VideoFrame(2, 2, Pixels(1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255)); captured!.Pump(first); pump.Drain(); Assert.NotNull(published); Assert.Equal(1, publishedCount); var dims = pump.Dispatcher.Invoke(() => new[] { published!.PixelWidth, published.PixelHeight }); Assert.Equal(new[] { 2, 2 }, dims); var second = new VideoFrame(2, 2, Pixels(5, 0, 0, 255, 6, 0, 0, 255, 7, 0, 0, 255, 8, 0, 0, 255)); var third = new VideoFrame(2, 2, Pixels(9, 0, 0, 255, 10, 0, 0, 255, 11, 0, 0, 255, 12, 0, 0, 255)); captured.Pump(second); captured.Pump(third); pump.Drain(); var result = pump.Dispatcher.Invoke(() => { var bytes = new byte[16]; published!.CopyPixels(new System.Windows.Int32Rect(0, 0, 2, 2), bytes, 8, 0); return bytes; }); Assert.Equal(third.BgraPixels, result); } }