using FlashCap; namespace server.Services; /// /// Simple USB camera capture service following Linus principles: /// - Single responsibility: just capture frames /// - No special cases: uniform error handling /// - Good taste: clean data structures /// 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? FrameReady; public event Action? Error; public bool IsCapturing => _isCapturing; public VideoCharacteristics? CurrentCharacteristics => _characteristics; public CaptureDeviceDescriptor? CurrentDevice => _descriptor; public UsbCameraCapture() { _captureDevices = new CaptureDevices(); } /// /// Get all available camera devices /// public IReadOnlyList GetDevices() { return _captureDevices.EnumerateDescriptors().ToArray(); } /// /// Start capturing from specified device with best matching characteristics /// 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); } /// /// Start capturing with exact device and characteristics /// 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; } } /// /// Stop capturing and cleanup /// public async Task StopAsync() { if (!_isCapturing) return; _isCapturing = false; await CleanupAsync(); } /// /// Get the latest captured frame (returns copy for thread safety) /// public byte[]? GetLatestFrame() { return _latestFrame; } /// /// Get supported video characteristics for current device /// public IReadOnlyList GetSupportedCharacteristics() { return _descriptor?.Characteristics.ToArray() ?? Array.Empty(); } 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; } }