namespace Unosquare.RaspberryIO.Camera { using Swan; using System; using System.IO; using System.Threading; using System.Threading.Tasks; /// /// The Raspberry Pi's camera controller wrapping raspistill and raspivid programs. /// This class is a singleton. /// public class CameraController : SingletonBase { #region Private Declarations private static readonly ManualResetEventSlim OperationDone = new ManualResetEventSlim(true); private static readonly object SyncRoot = new object(); private static CancellationTokenSource _videoTokenSource = new CancellationTokenSource(); private static Task? _videoStreamTask; #endregion #region Properties /// /// Gets a value indicating whether the camera module is busy. /// /// /// true if this instance is busy; otherwise, false. /// public bool IsBusy => OperationDone.IsSet == false; #endregion #region Image Capture Methods /// /// Captures an image asynchronously. /// /// The settings. /// The ct. /// The image bytes. /// Cannot use camera module because it is currently busy. public async Task CaptureImageAsync(CameraStillSettings settings, CancellationToken ct = default) { if (Instance.IsBusy) throw new InvalidOperationException("Cannot use camera module because it is currently busy."); if (settings.CaptureTimeoutMilliseconds <= 0) throw new ArgumentException($"{nameof(settings.CaptureTimeoutMilliseconds)} needs to be greater than 0"); try { OperationDone.Reset(); using var output = new MemoryStream(); var exitCode = await ProcessRunner.RunProcessAsync( settings.CommandName, settings.CreateProcessArguments(), (data, proc) => output.Write(data, 0, data.Length), null, true, ct).ConfigureAwait(false); return exitCode != 0 ? Array.Empty() : output.ToArray(); } finally { OperationDone.Set(); } } /// /// Captures an image. /// /// The settings. /// The image bytes. public byte[] CaptureImage(CameraStillSettings settings) => CaptureImageAsync(settings).GetAwaiter().GetResult(); /// /// Captures a JPEG encoded image asynchronously at 90% quality. /// /// The width. /// The height. /// The ct. /// The image bytes. public Task CaptureImageJpegAsync(int width, int height, CancellationToken ct = default) { var settings = new CameraStillSettings { CaptureWidth = width, CaptureHeight = height, CaptureJpegQuality = 90, CaptureTimeoutMilliseconds = 300, }; return CaptureImageAsync(settings, ct); } /// /// Captures a JPEG encoded image at 90% quality. /// /// The width. /// The height. /// The image bytes. public byte[] CaptureImageJpeg(int width, int height) => CaptureImageJpegAsync(width, height).GetAwaiter().GetResult(); #endregion #region Video Capture Methods /// /// Opens the video stream with a timeout of 0 (running indefinitely) at 1080p resolution, variable bitrate and 25 FPS. /// No preview is shown. /// /// The on data callback. /// The on exit callback. public void OpenVideoStream(Action onDataCallback, Action? onExitCallback = null) { var settings = new CameraVideoSettings { CaptureTimeoutMilliseconds = 0, CaptureDisplayPreview = false, CaptureWidth = 1920, CaptureHeight = 1080, }; OpenVideoStream(settings, onDataCallback, onExitCallback); } /// /// Opens the video stream with the supplied settings. Capture Timeout Milliseconds has to be 0 or greater. /// /// The settings. /// The on data callback. /// The on exit callback. /// Cannot use camera module because it is currently busy. /// CaptureTimeoutMilliseconds. public void OpenVideoStream(CameraVideoSettings settings, Action onDataCallback, Action? onExitCallback = null) { if (Instance.IsBusy) throw new InvalidOperationException("Cannot use camera module because it is currently busy."); if (settings.CaptureTimeoutMilliseconds < 0) throw new ArgumentException($"{nameof(settings.CaptureTimeoutMilliseconds)} needs to be greater than or equal to 0"); try { OperationDone.Reset(); _videoStreamTask = Task.Factory.StartNew(() => VideoWorkerDoWork(settings, onDataCallback, onExitCallback), _videoTokenSource.Token); } catch { OperationDone.Set(); throw; } } /// /// Closes the video stream of a video stream is open. /// public void CloseVideoStream() { lock (SyncRoot) { if (IsBusy == false) return; } if (_videoTokenSource.IsCancellationRequested == false) { _videoTokenSource.Cancel(); _videoStreamTask?.Wait(); } _videoTokenSource = new CancellationTokenSource(); } private static async Task VideoWorkerDoWork( CameraVideoSettings settings, Action onDataCallback, Action onExitCallback) { try { await ProcessRunner.RunProcessAsync( settings.CommandName, settings.CreateProcessArguments(), (data, proc) => onDataCallback?.Invoke(data), null, true, _videoTokenSource.Token).ConfigureAwait(false); onExitCallback?.Invoke(); } catch { // swallow } finally { Instance.CloseVideoStream(); OperationDone.Set(); } } #endregion } }