/*
* VideoKit
* Copyright © 2025 Yusuf Olokoba. All Rights Reserved.
*/
#nullable enable
namespace VideoKit {
using AOT;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using UnityEngine;
using Internal;
using MediaDeviceFlags = Internal.VideoKit.MediaDeviceFlags;
using Status = Internal.VideoKit.Status;
///
/// Camera device.
///
public sealed class CameraDevice : MediaDevice {
#region --Types--
///
/// Exposure mode.
///
public enum ExposureMode : int {
///
/// Continuous auto exposure.
///
Continuous = 0,
///
/// Locked auto exposure.
///
Locked = 1,
///
/// Manual exposure.
///
Manual = 2
}
///
/// Photo flash mode.
///
public enum FlashMode : int {
///
/// Never use flash.
///
Off = 0,
///
/// Always use flash.
///
On = 1,
///
/// Let the sensor detect if it needs flash.
///
Auto = 2
}
///
/// Focus mode.
///
public enum FocusMode : int {
///
/// Continuous autofocus.
///
Continuous = 0,
///
/// Locked focus.
///
Locked = 1,
}
///
/// Torch mode.
///
public enum TorchMode : int {
///
/// Disabled torch.
///
Off = 0,
///
/// Maximum supported torch level.
///
Maximum = 100
}
///
/// Video stabilization mode.
///
public enum VideoStabilizationMode : int {
///
/// Disabled video stabilization.
///
Off = 0,
///
/// Standard video stabilization.
///
Standard = 1
}
///
/// White balance mode.
///
public enum WhiteBalanceMode : int {
///
/// Continuous auto white balance.
///
Continuous = 0,
///
/// Locked auto white balance.
///
Locked = 1,
}
#endregion
#region --Properties--
///
/// Whether this camera is front facing.
///
public bool frontFacing =>
device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok &&
flags.HasFlag(MediaDeviceFlags.FrontFacing);
///
/// Whether setting the flash mode for photo capture is supported.
///
public bool flashSupported =>
device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok &&
flags.HasFlag(MediaDeviceFlags.Flash);
///
/// Whether setting the torch level is supported.
///
public bool torchSupported =>
device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok &&
flags.HasFlag(MediaDeviceFlags.Torch);
///
/// Whether setting the exposure point is supported.
///
public bool exposurePointSupported =>
device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok &&
flags.HasFlag(MediaDeviceFlags.ExposurePoint);
///
/// Whether setting the focus point is supported.
///
public bool focusPointSupported =>
device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok &&
flags.HasFlag(MediaDeviceFlags.FocusPoint);
///
/// Whether depth streaming is supported.
///
public bool depthStreamingSupported =>
device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok &&
flags.HasFlag(MediaDeviceFlags.Depth);
///
/// Field of view in degrees.
///
public (float width, float height) fieldOfView => device.GetCameraDeviceFieldOfView(out var x, out var y) == Status.Ok ? (x, y) : default;
///
/// Exposure bias range in EV.
///
public (float min, float max) exposureBiasRange => device.GetCameraDeviceExposureBiasRange(out var min, out var max) == Status.Ok ? (min, max) : default;
///
/// Exposure duration range in seconds.
///
public (float min, float max) exposureDurationRange => device.GetCameraDeviceExposureDurationRange(out var min, out var max) == Status.Ok ? (min, max) : default;
///
/// Sensor sensitivity range.
///
public (float min, float max) ISORange => device.GetCameraDeviceISORange(out var min, out var max) == Status.Ok ? (min, max) : default;
///
/// Zoom ratio range.
///
public (float min, float max) zoomRange => device.GetCameraDeviceZoomRange(out var min, out var max) == Status.Ok ? (min, max) : (1f, 1f);
///
/// Get or set the preview resolution.
///
public (int width, int height) previewResolution {
get => device.GetCameraDevicePreviewResolution(out var w, out var h).Throw() == Status.Ok ? (w, h) : default;
set => device.SetCameraDevicePreviewResolution(value.width, value.height);
}
///
/// Get or set the photo resolution.
///
public (int width, int height) photoResolution {
get => device.GetCameraDevicePhotoResolution(out var w, out var h) == Status.Ok ? (w, h) : default;
set => device.SetCameraDevicePhotoResolution(value.width, value.height); // don't throw
}
///
/// Get or set the preview framerate.
///
public float frameRate {
get => device.GetCameraDeviceFrameRate(out var frameRate) == Status.Ok ? frameRate : default;
set => device.SetCameraDeviceFrameRate(value);
}
///
/// Get or set the exposure mode.
/// If the requested exposure mode is not supported, the camera device will ignore.
///
public ExposureMode exposureMode {
get => device.GetCameraDeviceExposureMode(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceExposureMode(value).Throw();
}
///
/// Get or set the exposure bias.
/// This value must be in the range returned by `exposureRange`.
///
public float exposureBias {
get => device.GetCameraDeviceExposureBias(out var bias) == Status.Ok ? bias : default;
set => device.SetCameraDeviceExposureBias(value).Throw();
}
///
/// Get or set the current exposure duration in seconds.
///
public float exposureDuration => device.GetCameraDeviceExposureDuration(out var duration) == Status.Ok ? duration : default;
///
/// Get or set the current exposure sensitivity.
///
public float ISO => device.GetCameraDeviceISO(out var ISO) == Status.Ok ? ISO : default;
///
/// Get or set the photo flash mode.
///
public FlashMode flashMode {
get => device.GetCameraDeviceFlashMode(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceFlashMode(value).Throw();
}
///
/// Get or set the focus mode.
///
public FocusMode focusMode {
get => device.GetCameraDeviceFocusMode(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceFocusMode(value).Throw();
}
///
/// Get or set the torch mode.
///
public TorchMode torchMode {
get => device.GetCameraDeviceTorchMode(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceTorchMode(value).Throw();
}
///
/// Get or set the white balance mode.
///
public WhiteBalanceMode whiteBalanceMode {
get => device.GetCameraDeviceWhiteBalanceMode(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceWhiteBalanceMode(value).Throw();
}
///
/// Get or set the video stabilization mode.
///
public VideoStabilizationMode videoStabilizationMode {
get => device.GetCameraDeviceVideoStabilizationMode(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceVideoStabilizationMode(value).Throw();
}
///
/// Get or set the zoom ratio.
/// This value must be in the range returned by `zoomRange`.
///
public float zoomRatio {
get => device.GetCameraDeviceZoomRatio(out var mode) == Status.Ok ? mode : default;
set => device.SetCameraDeviceZoomRatio(value).Throw();
}
#endregion
#region --Controls--
///
/// Check if a given exposure mode is supported by the camera device.
///
/// Exposure mode.
public bool IsExposureModeSupported (ExposureMode mode) => mode switch {
ExposureMode.Continuous => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.ExposureContinuous),
ExposureMode.Locked => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.ExposureLock),
ExposureMode.Manual => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.ExposureManual),
_ => false
};
///
/// Check if a given focus mode is supported by the camera device.
///
/// Focus mode.
public bool IsFocusModeSupported (FocusMode mode) => mode switch {
FocusMode.Continuous => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.FocusContinuous),
FocusMode.Locked => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.FocusLock),
_ => false,
};
///
/// Check if a given white balance mode is supported by the camera device.
///
/// White balance mode.
public bool IsWhiteBalanceModeSupported (WhiteBalanceMode mode) => mode switch {
WhiteBalanceMode.Continuous => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.WhiteBalanceContinuous),
WhiteBalanceMode.Locked => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.WhiteBalanceLock),
_ => false
};
///
/// Check if a given video stabilization mode is supported by the camera device.
///
/// Video stabilization mode.
public bool IsVideoStabilizationModeSupported (VideoStabilizationMode mode) => mode switch {
VideoStabilizationMode.Off => true,
VideoStabilizationMode.Standard => device.GetMediaDeviceFlags(out var flags).Throw() == Status.Ok && flags.HasFlag(MediaDeviceFlags.VideoStabilization),
_ => false
};
///
/// Set manual exposure.
///
/// Exposure duration in seconds. MUST be in `exposureDurationRange`.
/// Sensor sensitivity ISO value. MUST be in `ISORange`.
public void SetExposureDuration (float duration, float ISO) => device.SetCameraDeviceExposureDuration(duration, ISO).Throw();
///
/// Set the exposure point of interest.
/// The point is specified in normalized coordinates in range [0.0, 1.0].
///
/// Normalized x coordinate.
/// Normalized y coordinate.
public void SetExposurePoint (float x, float y) => device.SetCameraDeviceExposurePoint(x, y).Throw();
///
/// Set the focus point of interest.
/// The point is specified in normalized coordinates in range [0.0, 1.0].
///
/// Normalized x coordinate.
/// Normalized y coordinate.
public void SetFocusPoint (float x, float y) => device.SetCameraDeviceFocusPoint(x, y).Throw();
#endregion
#region --Streaming--
///
/// Start the camera preview.
///
/// Delegate to receive preview image frames. Note that this delegate is invoked on a dedicated thread.
public void StartRunning (Action handler) => StartRunning((IntPtr sampleBuffer) => {
handler(new PixelBuffer(sampleBuffer));
});
///
/// Capture a photo.
///
/// Delegate to receive high-resolution photo. Note that this delegate is invoked on a dedicated thread.
public void CapturePhoto (Action handler) {
var handle = GCHandle.Alloc(handler, GCHandleType.Normal);
device.CapturePhoto(OnCapturePhoto, (IntPtr)handle).Throw();
}
#endregion
#region --Discovery--
///
/// Check the current camera permission status.
///
/// Request permissions if the user has not yet been asked.
/// Camera permission status.
public static Task CheckPermissions (bool request = true) => CheckPermissions(
VideoKit.PermissionType.Camera,
request
);
///
/// Discover available camera devices.
///
public static async Task Discover () {
// Check session
await VideoKitClient.Instance!.CheckSession();
// Discover
var tcs = new TaskCompletionSource();
var handle = GCHandle.Alloc(tcs, GCHandleType.Normal);
try {
VideoKit.DiscoverCameraDevices(OnDiscoverDevices, (IntPtr)handle).Throw();
return await tcs.Task;
} catch {
handle.Free();
throw;
}
}
#endregion
#region --Operations--
private int priority { // #24
get {
var order = 0;
if (!defaultForMediaType)
order += 1;
if (location == Location.External)
order += 10;
if (location == Location.Unknown)
order += 100;
return order;
}
}
internal CameraDevice (IntPtr device, bool strong = true) : base(device, strong: strong) { }
public override string ToString () => $"CameraDevice(uniqueId=\"{uniqueId}\", name=\"{name}\")";
[MonoPInvokeCallback(typeof(VideoKit.MediaDeviceDiscoveryHandler))]
private static unsafe void OnDiscoverDevices (IntPtr context, IntPtr devices, int count) {
try {
// Check
if (!VideoKit.IsAppDomainLoaded)
return;
// Get tcs
var handle = (GCHandle)context;
var tcs = handle.Target as TaskCompletionSource;
handle.Free();
// Complete
var cameras = Enumerable
.Range(0, count)
.Select(idx => ((IntPtr*)devices)[idx])
.Select(device => new CameraDevice(device, strong: true))
.OrderBy(device => device.priority)
.ToArray();
tcs?.SetResult(cameras);
} catch (Exception ex) {
Debug.LogException(ex);
}
}
[MonoPInvokeCallback(typeof(VideoKit.SampleBufferHandler))]
private static unsafe void OnCapturePhoto (IntPtr context, IntPtr sampleBuffer) {
try {
// Check
if (!VideoKit.IsAppDomainLoaded)
return;
// Get handler
var handle = (GCHandle)context;
var handler = handle.Target as Action;
handle.Free();
// Invoke
handler?.Invoke(new PixelBuffer(sampleBuffer));
} catch (Exception ex) {
Debug.LogException(ex);
}
}
#endregion
}
}