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
}
}