using System.Threading; using System.Windows.Threading; using System.Windows.Media.Imaging; using Xunit; using ytLive.Services; namespace ytLive.Tests; /// /// ScreenCaptureManager is the app-wide owner of screen-capture sessions, /// refcounted by target key and coalesced onto the UI dispatcher. These tests /// pin that contract with a fake IScreenCaptureSource (the WinRT pool/session /// layer is exercised only on Windows at runtime). /// public class ScreenCaptureManagerTests { private sealed class FakeScreenSource : IScreenCaptureSource { private readonly List? _started; private readonly List? _stopped; public string Key { get; } public event Action? FrameAvailable; public FakeScreenSource(string key, List? started = null, List? stopped = null) { Key = key; _started = started; _stopped = stopped; } public Task StartAsync() { _started?.Add(Key); return Task.CompletedTask; } public Task StopAsync() { _stopped?.Add(Key); return Task.CompletedTask; } public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame); } private sealed class FailingScreenSource : IScreenCaptureSource { public string Key { get; } public event Action? FrameAvailable; public FailingScreenSource(string key) => Key = key; public Task StartAsync() => throw new InvalidOperationException("access denied"); public Task StopAsync() => Task.CompletedTask; public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame); } // A real Dispatcher pumping on a background STA thread, so the manager's // BeginInvoke queue can be drained deterministically from the test thread. 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(); } // Queued behind anything already posted at Render/higher, so pending // bitmap copies run first. 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_OneCaptureUntilLastRelease() { var started = new List(); var stopped = new List(); var manager = new ScreenCaptureManager(key => new FakeScreenSource(key, started, stopped)); Assert.True(await manager.AcquireAsync("monitor:0")); Assert.True(await manager.AcquireAsync("monitor:0")); Assert.Single(started); await manager.ReleaseAsync("monitor:0"); Assert.Empty(stopped); await manager.ReleaseAsync("monitor:0"); Assert.Single(stopped); } [Fact] public async Task ReleaseAll_StopsEvenWithMultipleRefs() { var started = new List(); var stopped = new List(); var manager = new ScreenCaptureManager(key => new FakeScreenSource(key, started, stopped)); await manager.AcquireAsync("monitor:0"); await manager.AcquireAsync("monitor:0"); await manager.ReleaseAllAsync("monitor:0"); Assert.Single(stopped); } [Fact] public async Task Acquire_FailingSource_ReturnsFalseAndRaisesCaptureFailed() { var manager = new ScreenCaptureManager(key => new FailingScreenSource(key)); string? failedKey = null; manager.CaptureFailed += (key, _) => failedKey = key; Assert.False(await manager.AcquireAsync("monitor:0")); Assert.Equal("monitor:0", failedKey); } [Fact] public async Task Acquire_EmptyKey_ReturnsFalse() { var manager = new ScreenCaptureManager(key => new FakeScreenSource(key)); Assert.False(await manager.AcquireAsync(" ")); } // The one integration test for this branch: one target creates one shared // WriteableBitmap, published once, and back-to-back frames coalesce to the // latest (a single pending UI copy per session). [Fact] public async Task Frames_ShareOneBitmap_AndCoalesceToLatest() { using var pump = new DispatcherPump(); FakeScreenSource? captured = null; var manager = new ScreenCaptureManager(key => captured = new FakeScreenSource(key), pump.Dispatcher); WriteableBitmap? published = null; var publishedCount = 0; manager.PreviewBitmapChanged += (_, bitmap) => { published = bitmap; publishedCount++; }; Assert.True(await manager.AcquireAsync("monitor:0")); 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(); // The bitmap is frozen to the dispatcher thread; copy its pixels there. 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); } }