using Unosquare.Swan.Abstractions; using System; using Unosquare.Swan.Components; using System.IO; using System.Threading; using System.Threading.Tasks; 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. [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] 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(); MemoryStream output = new MemoryStream(); Int32 exitCode = await ProcessRunner.RunProcessAsync( settings.CommandName, settings.CreateProcessArguments(), (data, proc) => output.Write(data, 0, data.Length), null, true, ct); return exitCode != 0 ? new Byte[] { } : 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) { 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); onExitCallback?.Invoke(); } catch { // swallow } finally { Instance.CloseVideoStream(); OperationDone.Set(); } } #endregion } }