using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Windows.Media.Imaging; using System.Windows.Threading; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.BackgroundEffects; namespace ytLive.Tests; public class CameraManagerTests { private sealed class FakeFrameSource : ICameraFrameSource { private readonly List _started; private readonly List _stopped; private readonly VideoFrame? _pumpOnStart; private readonly string? _failOnStart; public string DeviceId { get; } public event Action? FrameAvailable; public event Action? SourceFailed; public FakeFrameSource(string deviceId, List started, List stopped, VideoFrame? pumpOnStart = null, string? failOnStart = null) { DeviceId = deviceId; _started = started; _stopped = stopped; _pumpOnStart = pumpOnStart; _failOnStart = failOnStart; } public Task StartAsync() { if (_failOnStart != null) throw new InvalidOperationException(_failOnStart); _started.Add(DeviceId); if (_pumpOnStart != null) FrameAvailable?.Invoke(_pumpOnStart); return Task.CompletedTask; } public Task StopAsync() { _stopped.Add(DeviceId); return Task.CompletedTask; } public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame); public void Fail(string message) => SourceFailed?.Invoke(message); } // The refcount/coalescing tests model a camera whose frame pump is driven by // the test after acquire, so they skip the first-frame proof (TimeSpan.Zero); // the proof itself is exercised by the dedicated tests below. private static CameraManager CreateManager(List started, List stopped) => new(new FakeEnumerator(), id => new FakeFrameSource(id, started, stopped), null, TimeSpan.Zero); private sealed class FakeEnumerator : ICameraEnumerator { public Task> GetCamerasAsync() => Task.FromResult>(new[] { new CameraDeviceInfo("dev1", "Logitech C920"), }); } [Fact] public async Task Refcount_TwoAcquires_OneCaptureUntilLastRelease() { var started = new List(); var stopped = new List(); var manager = CreateManager(started, stopped); Assert.True(await manager.AcquireAsync("dev1")); Assert.True(await manager.AcquireAsync("dev1")); Assert.Single(started); await manager.ReleaseAsync("dev1"); Assert.Empty(stopped); await manager.ReleaseAsync("dev1"); Assert.Single(stopped); Assert.Null(manager.GetLatestFrame("dev1")); } [Fact] public async Task FramePump_UpdatesLatestFrame() { var started = new List(); var stopped = new List(); FakeFrameSource? captured = null; var manager = new CameraManager( new FakeEnumerator(), id => captured = new FakeFrameSource(id, started, stopped), null, TimeSpan.Zero); await manager.AcquireAsync("dev1"); var frame = new VideoFrame(2, 2, new byte[16]); captured!.Pump(frame); Assert.Same(frame, manager.GetLatestFrame("dev1")); } [Fact] public async Task Acquire_FailingCamera_ReturnsFalseAndStops() { var manager = new CameraManager( new FakeEnumerator(), id => new FakeFrameSource(id, new List(), new List(), failOnStart: "camera in use"), null, TimeSpan.Zero); string? failedDevice = null; manager.CameraFailed += (device, _) => failedDevice = device; Assert.False(await manager.AcquireAsync("dev1")); Assert.Equal("dev1", failedDevice); } [Fact] public async Task Acquire_EmptyDeviceId_ReturnsFalse() { var manager = CreateManager(new List(), new List()); Assert.False(await manager.AcquireAsync(" ")); } // ─── First-frame proof (the integration test for this change) ─── [Fact] public async Task Acquire_SilentCamera_NoFirstFrame_FailsAndSurfacesCameraFailed() { var stopped = new List(); var manager = new CameraManager( new FakeEnumerator(), id => new FakeFrameSource(id, new List(), stopped), null, TimeSpan.FromMilliseconds(150)); string? failedDevice = null; string? failedMessage = null; manager.CameraFailed += (device, message) => { failedDevice = device; failedMessage = message; }; // The reader "starts" fine but never delivers a frame — the exact // silent-empty-box scenario. Must fail, be reported, and be rolled back. Assert.False(await manager.AcquireAsync("dev1")); Assert.Equal("dev1", failedDevice); Assert.Contains("no frames", failedMessage); Assert.Null(manager.GetLatestFrame("dev1")); Assert.Contains("dev1", stopped); } [Fact] public async Task Acquire_FirstFrameProvesAlive_ReturnsTrueWithoutWaitingTimeout() { var manager = new CameraManager( new FakeEnumerator(), id => new FakeFrameSource(id, new List(), new List(), pumpOnStart: new VideoFrame(2, 2, new byte[16])), null, TimeSpan.FromSeconds(10)); var started = await manager.AcquireAsync("dev1"); Assert.True(started); Assert.NotNull(manager.GetLatestFrame("dev1")); } [Fact] public async Task Acquire_SourceFailureAfterStart_SurfacesCameraFailedAndRollsBack() { var stopped = new List(); FakeFrameSource? source = null; var manager = new CameraManager( new FakeEnumerator(), id => source = new FakeFrameSource(id, new List(), stopped, pumpOnStart: new VideoFrame(2, 2, new byte[16])), null, TimeSpan.Zero); string? failedMessage = null; manager.CameraFailed += (_, message) => failedMessage = message; Assert.True(await manager.AcquireAsync("dev1")); source!.Fail("capture failed (0x8007001F): device not available"); Assert.Contains("device not available", failedMessage); Assert.Null(manager.GetLatestFrame("dev1")); Assert.Contains("dev1", stopped); } private sealed class HalfMaskSegmenter : IPersonSegmenter { public int Calls; public SegmentationMask? Segment(VideoFrame frame) { Interlocked.Increment(ref Calls); return new SegmentationMask(2, 1, new[] { 1f, 0f }); // left person, right background } } /// TASK 51 slice 4 integration test — the one processing point: with a Remove /// variant requested, a raw camera frame reaches BOTH consumers processed — the /// compositor's latest frame for that variant and the variant's preview bitmap on the UI /// dispatcher — while the raw path keeps the untouched frame. [Fact] public async Task Requested_Remove_Variant_Reaches_Compositor_And_Preview_Raw_Stays_Raw() { Dispatcher? ui = null; using var ready = new ManualResetEventSlim(); var uiThread = new Thread(() => { ui = Dispatcher.CurrentDispatcher; ready.Set(); Dispatcher.Run(); }); uiThread.SetApartmentState(ApartmentState.STA); uiThread.IsBackground = true; uiThread.Start(); ready.Wait(); try { FakeFrameSource? source = null; var segmenter = new HalfMaskSegmenter(); var manager = new CameraManager( new FakeEnumerator(), id => source = new FakeFrameSource(id, new List(), new List()), ui, TimeSpan.Zero, new BackgroundEffectProcessor(segmenter)); var key = WebcamEffectKey.For(BackgroundEffect.Remove, 0.5); manager.SetRequestedEffects("dev1", new[] { key, WebcamEffectKey.None }); var previewArrived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); manager.EffectPreviewBitmapChanged += (device, k, bitmap) => { if (device == "dev1" && k == key) previewArrived.TrySetResult(bitmap); }; Assert.True(await manager.AcquireAsync("dev1")); const int w = 16, h = 4; var pixels = new byte[w * h * 4]; for (var i = 0; i < pixels.Length; i += 4) { pixels[i] = 50; pixels[i + 1] = 60; pixels[i + 2] = 70; pixels[i + 3] = 255; } var raw = new VideoFrame(w, h, pixels) { IsOpaque = true }; source!.Pump(raw); var bitmap = await previewArrived.Task.WaitAsync(TimeSpan.FromSeconds(10)); // Raw consumer: the very same frame, untouched. Assert.Same(raw, manager.GetLatestFrame("dev1")); Assert.Same(raw, manager.GetLatestFrame("dev1", WebcamEffectKey.None)); // Compositor consumer: the processed variant. var processed = manager.GetLatestFrame("dev1", key); Assert.NotNull(processed); Assert.False(processed!.IsOpaque); Assert.Equal(255, processed.BgraPixels[(1 * w + 1) * 4 + 3]); // person side Assert.Equal(0, processed.BgraPixels[(1 * w + (w - 2)) * 4 + 3]); // background side // Preview consumer: the variant's bitmap carries the same cut-out. var previewPixels = new byte[w * h * 4]; var previewWidth = await ui!.InvokeAsync(() => { bitmap.CopyPixels(previewPixels, w * 4, 0); return bitmap.PixelWidth; }); Assert.Equal(w, previewWidth); Assert.Equal(255, previewPixels[(1 * w + 1) * 4 + 3]); Assert.Equal(0, previewPixels[(1 * w + (w - 2)) * 4 + 3]); Assert.Same(bitmap, manager.GetPreviewBitmap("dev1", key)); Assert.True(segmenter.Calls >= 1); // Dropping the request stops the variant. manager.SetRequestedEffects("dev1", new[] { WebcamEffectKey.None }); Assert.Null(manager.GetLatestFrame("dev1", key)); } finally { ui?.InvokeShutdown(); } } }