using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Swan; namespace Unosquare.RaspberryIO.Camera { /// /// 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 Boolean 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 MemoryStream output = new MemoryStream(); Int32 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) => this.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(Int32 width, Int32 height, CancellationToken ct = default) { CameraStillSettings settings = new CameraStillSettings { CaptureWidth = width, CaptureHeight = height, CaptureJpegQuality = 90, CaptureTimeoutMilliseconds = 300, }; return this.CaptureImageAsync(settings, ct); } /// /// Captures a JPEG encoded image at 90% quality. /// /// The width. /// The height. /// The image bytes. public Byte[] CaptureImageJpeg(Int32 width, Int32 height) => this.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) { CameraVideoSettings settings = new CameraVideoSettings { CaptureTimeoutMilliseconds = 0, CaptureDisplayPreview = false, CaptureWidth = 1920, CaptureHeight = 1080, }; this.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(this.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 } }