203 lines
5.8 KiB
C#
203 lines
5.8 KiB
C#
using FlashCap;
|
|
|
|
namespace server.Services;
|
|
|
|
/// <summary>
|
|
/// Simple USB camera capture service following Linus principles:
|
|
/// - Single responsibility: just capture frames
|
|
/// - No special cases: uniform error handling
|
|
/// - Good taste: clean data structures
|
|
/// </summary>
|
|
public class UsbCameraCapture : IDisposable
|
|
{
|
|
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
|
|
|
private readonly CaptureDevices _captureDevices;
|
|
private CaptureDevice? _device;
|
|
private CaptureDeviceDescriptor? _descriptor;
|
|
private VideoCharacteristics? _characteristics;
|
|
|
|
// Single source of truth for latest frame - no redundant buffering
|
|
private volatile byte[]? _latestFrame;
|
|
private volatile bool _isCapturing;
|
|
private bool _disposed;
|
|
|
|
public event Action<byte[]>? FrameReady;
|
|
public event Action<Exception>? Error;
|
|
|
|
public bool IsCapturing => _isCapturing;
|
|
public VideoCharacteristics? CurrentCharacteristics => _characteristics;
|
|
public CaptureDeviceDescriptor? CurrentDevice => _descriptor;
|
|
|
|
public UsbCameraCapture()
|
|
{
|
|
_captureDevices = new CaptureDevices();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all available camera devices
|
|
/// </summary>
|
|
public IReadOnlyList<CaptureDeviceDescriptor> GetDevices()
|
|
{
|
|
return _captureDevices.EnumerateDescriptors().ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start capturing from specified device with best matching characteristics
|
|
/// </summary>
|
|
public async Task StartAsync(int deviceIndex, int width = 640, int height = 480, int frameRate = 30)
|
|
{
|
|
var devices = GetDevices();
|
|
if (deviceIndex >= devices.Count)
|
|
throw new ArgumentOutOfRangeException(nameof(deviceIndex));
|
|
|
|
var descriptor = devices[deviceIndex];
|
|
var characteristics = FindBestMatch(descriptor, width, height, frameRate);
|
|
|
|
await StartAsync(descriptor, characteristics);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start capturing with exact device and characteristics
|
|
/// </summary>
|
|
public async Task StartAsync(CaptureDeviceDescriptor descriptor, VideoCharacteristics characteristics)
|
|
{
|
|
if (_isCapturing)
|
|
await StopAsync();
|
|
|
|
try
|
|
{
|
|
_descriptor = descriptor;
|
|
_characteristics = characteristics;
|
|
_device = await descriptor.OpenAsync(
|
|
characteristics, TranscodeFormats.DoNotTranscode, true, 10, OnFrameCaptured);
|
|
|
|
await _device.StartAsync();
|
|
_isCapturing = true;
|
|
logger.Debug("Started capturing");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await CleanupAsync();
|
|
Error?.Invoke(ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop capturing and cleanup
|
|
/// </summary>
|
|
public async Task StopAsync()
|
|
{
|
|
if (!_isCapturing)
|
|
return;
|
|
|
|
_isCapturing = false;
|
|
await CleanupAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the latest captured frame (returns copy for thread safety)
|
|
/// </summary>
|
|
public byte[]? GetLatestFrame()
|
|
{
|
|
return _latestFrame;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get supported video characteristics for current device
|
|
/// </summary>
|
|
public IReadOnlyList<VideoCharacteristics> GetSupportedCharacteristics()
|
|
{
|
|
return _descriptor?.Characteristics.ToArray() ?? Array.Empty<VideoCharacteristics>();
|
|
}
|
|
|
|
private VideoCharacteristics FindBestMatch(CaptureDeviceDescriptor descriptor, int width, int height, int frameRate)
|
|
{
|
|
var characteristics = descriptor.Characteristics;
|
|
|
|
// Exact match first
|
|
var exact = characteristics.FirstOrDefault(c =>
|
|
c.Width == width && c.Height == height && Math.Abs(c.FramesPerSecond - frameRate) < 1);
|
|
if (exact != null)
|
|
return exact;
|
|
|
|
// Resolution match with best framerate
|
|
var resolution = characteristics
|
|
.Where(c => c.Width == width && c.Height == height)
|
|
.OrderByDescending(c => c.FramesPerSecond)
|
|
.FirstOrDefault();
|
|
if (resolution != null)
|
|
return resolution;
|
|
|
|
// Closest resolution
|
|
try
|
|
{
|
|
var closest = characteristics
|
|
.OrderBy(c => Math.Abs(c.Width - width) + Math.Abs(c.Height - height))
|
|
.ThenByDescending(c => c.FramesPerSecond)
|
|
.First();
|
|
|
|
return closest;
|
|
}
|
|
catch
|
|
{
|
|
for (int i = 0; i < characteristics.Length; i++)
|
|
logger.Error($"Characteristics[{i}]: {characteristics[i].Width}x{characteristics[i].Height} @ {characteristics[i].FramesPerSecond}fps");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void OnFrameCaptured(PixelBufferScope bufferScope)
|
|
{
|
|
if (!_isCapturing)
|
|
return;
|
|
|
|
try
|
|
{
|
|
// Simple: extract and store. No queues, no locks, no complexity.
|
|
var imageData = bufferScope.Buffer.CopyImage();
|
|
_latestFrame = imageData;
|
|
FrameReady?.Invoke(imageData);
|
|
// logger.Info("USB Camera frame captured");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Error?.Invoke(ex);
|
|
}
|
|
}
|
|
|
|
private async Task CleanupAsync()
|
|
{
|
|
try
|
|
{
|
|
if (_device != null)
|
|
{
|
|
await _device.StopAsync();
|
|
_device.Dispose();
|
|
_device = null;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Error?.Invoke(ex);
|
|
}
|
|
finally
|
|
{
|
|
_latestFrame = null;
|
|
_descriptor = null;
|
|
_characteristics = null;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
|
|
if (_isCapturing) StopAsync().Wait();
|
|
|
|
_device?.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|