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