using System.Diagnostics; using System.Threading.Channels; using OpenCvSharp; using VisionBuilder.UI.Common.Processing; using VisionBuilder.UI.RaspberryAcquisition.Models; namespace VisionBuilder.UI.RaspberryAcquisition.Services; public class ConfigurableLibcameraSource : IImageSource, IDisposable { private static readonly byte[] JpegHeader = [0xff, 0xd8]; private static readonly byte[] JpegFooter = [0xff, 0xd9]; private const int ChunkSize = 1024; private readonly Channel _imageChannel = Channel.CreateBounded( new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest }); private CaptureSettings _settings; private CancellationTokenSource? _cts; private Thread? _captureThread; public int FrameCount { get; private set; } public event Action? StatusChanged; public ConfigurableLibcameraSource(CaptureSettings settings) { _settings = settings; } public Task GetImage(CancellationToken token) { return _imageChannel.Reader.ReadAsync(token).AsTask(); } public void Start() { Stop(); FrameCount = 0; _cts = new CancellationTokenSource(); _captureThread = new Thread(CaptureLoop) { IsBackground = true }; _captureThread.Start(); StatusChanged?.Invoke("Running"); } public void Stop() { _cts?.Cancel(); _captureThread?.Join(2000); _cts?.Dispose(); _cts = null; _captureThread = null; StatusChanged?.Invoke("Stopped"); } public void UpdateSettings(CaptureSettings newSettings) { _settings = newSettings; Start(); } private void CaptureLoop() { var psi = new ProcessStartInfo { FileName = "rpicam-vid", Arguments = _settings.BuildArguments(), RedirectStandardOutput = true, UseShellExecute = false }; Process? process = null; try { process = Process.Start(psi); if (process == null) { StatusChanged?.Invoke("Error: failed to start rpicam-vid"); return; } using var br = new BinaryReader(process.StandardOutput.BaseStream); var imageBuffer = new byte[1024 * 1024]; var buff = br.ReadBytes(ChunkSize); while (!_cts!.Token.IsCancellationRequested) { var imageStart = Find(buff, JpegHeader); if (imageStart == -1) { StatusChanged?.Invoke("Error: JPEG header not found"); break; } var size = buff.Length - imageStart; Array.Copy(buff, imageStart, imageBuffer, 0, size); while (!_cts.Token.IsCancellationRequested) { buff = br.ReadBytes(ChunkSize); var imageEnd = Find(buff, JpegFooter); if (imageEnd != -1) { imageEnd += JpegFooter.Length; var frame = new byte[size + imageEnd]; Array.Copy(imageBuffer, frame, size); Array.Copy(buff, 0, frame, size, imageEnd); var decoded = Mat.ImDecode(frame); FrameCount++; _imageChannel.Writer.TryWrite(decoded); var remaining = buff.Length - imageEnd; var newBuff = new byte[ChunkSize]; Array.Copy(buff, imageEnd, newBuff, 0, remaining); var temp = br.ReadBytes(imageEnd); Array.Copy(temp, 0, newBuff, remaining, temp.Length); buff = newBuff; break; } Array.Copy(buff, 0, imageBuffer, size, buff.Length); size += buff.Length; } } } catch (Exception ex) when (ex is not OperationCanceledException) { StatusChanged?.Invoke($"Error: {ex.Message}"); } finally { if (process is { HasExited: false }) { process.Kill(); process.Dispose(); } } } private static int Find(byte[] buff, byte[] search) { for (int start = 0; start <= buff.Length - search.Length; start++) { if (buff[start] == search[0]) { int next; for (next = 1; next < search.Length; next++) { if (buff[start + next] != search[next]) break; } if (next == search.Length) return start; } } return -1; } public void Dispose() { Stop(); } }