/* * VideoKit * Copyright © 2025 Yusuf Olokoba. All Rights Reserved. */ #nullable enable namespace VideoKit { using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; using Internal; /// /// VideoKit camera manager for streaming video from camera devices. /// [Tooltip(@"VideoKit camera manager for streaming video from camera devices.")] [HelpURL(@"https://videokit.ai/reference/videokitcameramanager")] [DisallowMultipleComponent] public sealed class VideoKitCameraManager : MonoBehaviour { #region --Enumerations-- /// /// Camera manager capabilities. /// [Flags] public enum Capabilities : int { /// /// Stream depth data along with the camera preview data. /// This flag adds a minimal performance cost, so enable it only when necessary. /// This flag is only supported on iOS and Android. /// Depth = 0b0001, /// /// Generate a human texture from the camera preview stream. /// This flag adds a variable performance cost, so enable it only when necessary. /// HumanTexture = 0b00110, } /// /// Camera facing. /// [Flags] public enum Facing : int { /// /// User-facing camera. /// User = 0b10, /// /// World-facing camera. /// World = 0b01, } /// /// Camera resolution presets. /// public enum Resolution : int { /// /// Use the default camera resolution. /// With this preset, the camera resolution will not be set. /// Default = 0, /// /// Lowest resolution supported by the camera device. /// Lowest = 1, /// /// SD resolution. /// _640x480 = 2, /// /// HD resolution. /// [InspectorName(@"1280x720 (HD)")] _1280x720 = 3, /// /// Full HD resolution. /// [InspectorName(@"1920x1080 (Full HD)")] _1920x1080 = 4, /// /// 2K WQHD resolution. /// [InspectorName(@"2560x1440 (2K)")] _2560x1440 = 6, /// /// 4K UHD resolution. /// [InspectorName(@"3840x2160 (4K)")] _3840x2160 = 5, /// /// Highest resolution supported by the camera device. /// Using this resolution is strongly not recommended. /// Highest = 10 } /// /// public enum FrameRate : int { /// /// Use the default camera frame rate. /// With this preset, the camera frame rate will not be set. /// Default = 0, /// /// Use the lowest frame rate supported by the camera. /// Lowest = 1, /// /// 15FPS. /// _15 = 15, /// /// 30FPS. /// _30 = 30, /// /// 60FPS. /// _60 = 60, /// /// 120FPS. /// _120 = 120, /// /// 240FPS. /// _240 = 240 } #endregion #region --Inspector-- [Header(@"Configuration")] /// /// Desired camera capabilities. /// [Tooltip(@"Desired camera capabilities.")] public Capabilities capabilities = 0; /// /// Whether to start the camera preview as soon as the component awakes. /// [Tooltip(@"Whether to start the camera preview as soon as the component awakes.")] public bool playOnAwake = true; [Header(@"Camera Selection")] /// /// Desired camera facing. /// [SerializeField, Tooltip(@"Desired camera facing.")] private Facing _facing = Facing.User; /// /// Whether the specified facing is required. /// When false, the camera manager will fallback to a default camera when a camera with the requested facing is not available. /// [Tooltip(@"Whether the specified facing is required. When false, the camera manager will fallback to a default camera when a camera with the requested facing is not available.")] public bool facingRequired = false; [Header(@"Camera Settings")] /// /// Desired camera resolution. /// [Tooltip(@"Desired camera resolution.")] public Resolution resolution = Resolution._1280x720; /// /// Desired camera frame rate. /// [Tooltip(@"Desired camera frame rate.")] public FrameRate frameRate = FrameRate._30; /// /// Desired camera focus mode. /// [Tooltip(@"Desired camera focus mode.")] public CameraDevice.FocusMode focusMode = CameraDevice.FocusMode.Continuous; /// /// Desired camera exposure mode. /// [Tooltip(@"Desired camera exposure mode.")] public CameraDevice.ExposureMode exposureMode = CameraDevice.ExposureMode.Continuous; #endregion #region --Client API-- /// /// Get or set the camera device used for streaming. /// public MediaDevice? device { get => _device; set { // Switch cameras without disposing output // We deliberately skip configuring the camera like we do in `StartRunning` if (running) { _device!.StopRunning(); _device = value; if (_device != null) StartRunning(_device, OnCameraBuffer); } // Handle trivial case else _device = value; } } /// /// Get or set the desired camera facing. /// public Facing facing { get => _facing; set { if (_facing == value) return; device = GetDefaultDevice(devices, _facing = value, facingRequired); } } /// /// Whether the camera is running. /// public bool running => _device?.running ?? false; /// /// Event raised when a new pixel buffer is provided by the camera device. /// NOTE: This event is invoked on a dedicated camera thread, not the Unity main thread. /// public event Action? OnPixelBuffer; /// /// Start the camera preview. /// public async void StartRunning () => await StartRunningAsync(); /// /// Start the camera preview. /// public async Task StartRunningAsync () { // Check if (!isActiveAndEnabled) throw new InvalidOperationException(@"VideoKit: Camera manager failed to start running because component is disabled"); // Check if (running) return; // Request camera permissions var permissions = await CameraDevice.CheckPermissions(request: true); if (permissions != MediaDevice.PermissionStatus.Authorized) throw new InvalidOperationException(@"VideoKit: User did not grant camera permissions"); // Check device devices = await GetAllDevices(); _device ??= GetDefaultDevice(devices, _facing, facingRequired); if (_device == null) throw new InvalidOperationException(@"VideoKit: Camera manager failed to start running because no camera device is available"); // Configure camera(s) foreach (var cameraDevice in EnumerateCameraDevices(_device)) { if (resolution != Resolution.Default) cameraDevice.previewResolution = GetResolutionFrameSize(resolution); if (frameRate != FrameRate.Default) cameraDevice.frameRate = (int)frameRate; if (cameraDevice.IsFocusModeSupported(focusMode)) cameraDevice.focusMode = focusMode; if (cameraDevice.IsExposureModeSupported(exposureMode)) cameraDevice.exposureMode = exposureMode; } // Preload human texture predictor var fxn = VideoKitClient.Instance!.fxn; if (capabilities.HasFlag(Capabilities.HumanTexture)) { try { await fxn.Predictions.Create(HumanTextureTag, new()); } catch { // CHECK // REMOVE var predictorCachePath = Path.Join(Application.persistentDataPath, @"fxn", @"predictors"); if (Directory.Exists(predictorCachePath)) Directory.Delete(predictorCachePath, recursive: true); } await fxn.Predictions.Create(HumanTextureTag, new()); } // Start running StartRunning(_device, OnCameraBuffer); // Listen for events var events = VideoKitEvents.Instance; events.onPause += OnPause; events.onResume += OnResume; } /// /// Stop the camera preview. /// public void StopRunning () { var events = VideoKitEvents.OptionalInstance; if (events != null) { events.onPause -= OnPause; events.onResume -= OnResume; } _device?.StopRunning(); } #endregion #region --Operations-- private MediaDevice[]? devices; private MediaDevice? _device; public const string HumanTextureTag = @"@videokit/human-texture"; private void Awake () { if (playOnAwake) StartRunning(); } private static void StartRunning ( MediaDevice device, Action handler ) { if (device is CameraDevice cameraDevice) cameraDevice.StartRunning(pixelBuffer => handler(cameraDevice, pixelBuffer)); else if (device is MultiCameraDevice multiCameraDevice) multiCameraDevice.StartRunning(handler); else throw new InvalidOperationException($"Cannot start running because media device has unsupported type: {device.GetType()}"); } private unsafe void OnCameraBuffer ( CameraDevice cameraDevice, PixelBuffer pixelBuffer ) => OnPixelBuffer?.Invoke(cameraDevice, pixelBuffer); private void OnPause () => _device?.StopRunning(); private void OnResume () { if (_device != null) StartRunning(_device, OnCameraBuffer); } private void OnDestroy () => StopRunning(); #endregion #region --Utilties-- internal static IEnumerable EnumerateCameraDevices (MediaDevice? device) { if (device is CameraDevice cameraDevice) yield return cameraDevice; else if (device is MultiCameraDevice multiCameraDevice) foreach (var camera in multiCameraDevice.cameras) yield return camera; else yield break; } internal static Facing GetCameraFacing (MediaDevice mediaDevice) => mediaDevice switch { CameraDevice cameraDevice => cameraDevice.frontFacing ? Facing.User : Facing.World, MultiCameraDevice multiCameraDevice => multiCameraDevice.cameras.Select(GetCameraFacing).Aggregate((a, b) => a | b), _ => 0, }; private static async Task GetAllDevices () { var cameraDevices = await CameraDevice.Discover(); // MUST always come before multi-cameras var multiCameraDevices = await MultiCameraDevice.Discover(); var result = cameraDevices.Cast().Concat(multiCameraDevices).ToArray(); return result; } private static MediaDevice? GetDefaultDevice ( MediaDevice[]? devices, Facing facing, bool facingRequired ) { facing &= Facing.User | Facing.World; var fallbackDevice = facingRequired ? null : devices?.FirstOrDefault(); var requestedDevice = devices?.FirstOrDefault(device => GetCameraFacing(device).HasFlag(facing)); return requestedDevice ?? fallbackDevice; } private static (int width, int height) GetResolutionFrameSize (Resolution resolution) => resolution switch { Resolution.Lowest => (176, 144), Resolution._640x480 => (640, 480), Resolution._1280x720 => (1280, 720), Resolution._1920x1080 => (1920, 1080), Resolution._2560x1440 => (2560, 1440), Resolution._3840x2160 => (3840, 2160), Resolution.Highest => (5120, 2880), _ => (1280, 720), }; #endregion } }