190 lines
6.3 KiB
C#
190 lines
6.3 KiB
C#
using System.Threading;
|
|
using System.Windows.Threading;
|
|
using System.Windows.Media.Imaging;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public class ScreenCaptureManagerTests
|
|
{
|
|
private sealed class FakeScreenSource : IScreenCaptureSource
|
|
{
|
|
private readonly List<string>? _started;
|
|
private readonly List<string>? _stopped;
|
|
|
|
public string Key { get; }
|
|
public event Action<VideoFrame>? FrameAvailable;
|
|
|
|
public FakeScreenSource(string key, List<string>? started = null, List<string>? 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<VideoFrame>? 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<string>();
|
|
var stopped = new List<string>();
|
|
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<string>();
|
|
var stopped = new List<string>();
|
|
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);
|
|
}
|
|
}
|