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