TASK 3 milestone 1: webcam capture (MediaCapture CPU-first, CameraManager refcount, picker, clip/mirror, schema v2, tests)

This commit is contained in:
2026-08-06 14:35:34 -07:00
parent d97b5d375c
commit 98cdc4b3f4
23 changed files with 1099 additions and 40 deletions
+122
View File
@@ -0,0 +1,122 @@
using Xunit;
using ytLive.Services;
namespace ytLive.Tests;
public class CameraManagerTests
{
private sealed class FakeFrameSource : ICameraFrameSource
{
private readonly List<string> _started;
private readonly List<string> _stopped;
public string DeviceId { get; }
public event Action<VideoFrame>? FrameAvailable;
public FakeFrameSource(string deviceId, List<string> started, List<string> stopped)
{
DeviceId = deviceId;
_started = started;
_stopped = stopped;
}
public Task StartAsync()
{
_started.Add(DeviceId);
return Task.CompletedTask;
}
public Task StopAsync()
{
_stopped.Add(DeviceId);
return Task.CompletedTask;
}
public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame);
}
private sealed class FailingFrameSource : ICameraFrameSource
{
public string DeviceId { get; }
public event Action<VideoFrame>? FrameAvailable;
public bool Stopped;
public FailingFrameSource(string deviceId) => DeviceId = deviceId;
public Task StartAsync() => throw new InvalidOperationException("camera in use");
public Task StopAsync()
{
Stopped = true;
return Task.CompletedTask;
}
public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame);
}
private static CameraManager CreateManager(List<string> started, List<string> stopped)
=> new(new FakeEnumerator(), id => new FakeFrameSource(id, started, stopped));
private sealed class FakeEnumerator : ICameraEnumerator
{
public Task<IReadOnlyList<CameraDeviceInfo>> GetCamerasAsync()
=> Task.FromResult<IReadOnlyList<CameraDeviceInfo>>(new[]
{
new CameraDeviceInfo("dev1", "Logitech C920"),
});
}
[Fact]
public async Task Refcount_TwoAcquires_OneCaptureUntilLastRelease()
{
var started = new List<string>();
var stopped = new List<string>();
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<string>();
var stopped = new List<string>();
FakeFrameSource? captured = null;
var manager = new CameraManager(
new FakeEnumerator(),
id => captured = new FakeFrameSource(id, started, stopped));
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 FailingFrameSource(id));
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<string>(), new List<string>());
Assert.False(await manager.AcquireAsync(" "));
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>