// // /*=============================================================================== // // Copyright (C) 2025 PhantomsXR Ltd. All Rights Reserved. // // // // This file is part of the Phantom.XRMOD.QuestModule.Runtime. // // // // The XR-MOD cannot be copied, distributed, or made available to // // third-parties for commercial purposes without written permission of PhantomsXR Ltd. // // // // Contact nswell@phantomsxr.com for licensing requests. // // ===============================================================================*/ using System.Collections; using System.Linq; using UnityEngine; using UnityEngine.Assertions; using PCD = Phantom.XRMOD.QuestModule.Runtime.PassthroughCameraDebugger; namespace Phantom.XRMOD.QuestModule.Runtime { /// /// Manages the creation and lifecycle of a for a specific passthrough camera eye. /// /// Handles permission checks and initializes the webcam texture with the requested resolution. /// /// public class WebCamTextureManager: MonoBehaviour { /// /// Specifies which eye (Left/Right) this manager controls. /// [SerializeField] public PassthroughCameraEye Eye = PassthroughCameraEye.Left; /// /// The requested resolution of the camera. /// /// If (0,0), the highest supported resolution will be used. /// If the exact resolution is not supported, the closest available one will be chosen. /// /// [SerializeField, Tooltip("The requested resolution of the camera may not be supported by the chosen camera. In such cases, the closest available values will be used.\n\n" + "When set to (0,0), the highest supported resolution will be used.")] public Vector2Int RequestedResolution; /// /// Reference to the permissions manager. /// [SerializeField] public PassthroughCameraPermissions CameraPermissions; /// /// Returns reference if required permissions were granted and this component is enabled. Else, returns null. /// public WebCamTexture WebCamTexture { get; private set; } private bool m_hasPermission; private void Awake() { PCD.DebugMessage(LogType.Log, $"{nameof(WebCamTextureManager)}.{nameof(Awake)}() was called"); Assert.AreEqual(1, FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None).Length, $"PCA: Passthrough Camera: more than one {nameof(WebCamTextureManager)} component. Only one instance is allowed at a time. Current instance: {name}"); #if UNITY_ANDROID CameraPermissions.AskCameraPermissions(); #endif } private void OnEnable() { PCD.DebugMessage(LogType.Log, $"PCA: {nameof(OnEnable)}() was called"); if (!PassthroughCameraUtils.IsSupported) { PCD.DebugMessage(LogType.Log, "PCA: Passthrough Camera functionality is not supported by the current device." + $" Disabling {nameof(WebCamTextureManager)} object"); enabled = false; return; } m_hasPermission = PassthroughCameraPermissions.HasCameraPermission == true; if (!m_hasPermission) { PCD.DebugMessage(LogType.Error, $"PCA: Passthrough Camera requires permission(s) {string.Join(" and ", PassthroughCameraPermissions.CameraPermissions)}. Waiting for them to be granted..."); return; } PCD.DebugMessage(LogType.Log, "PCA: All permissions have been granted"); _ = StartCoroutine(InitializeWebCamTexture()); } private void OnDisable() { PCD.DebugMessage(LogType.Log, $"PCA: {nameof(OnDisable)}() was called"); StopCoroutine(InitializeWebCamTexture()); if (WebCamTexture != null) { WebCamTexture.Stop(); Destroy(WebCamTexture); WebCamTexture = null; } } private void Update() { if (!m_hasPermission) { if (PassthroughCameraPermissions.HasCameraPermission != true) return; m_hasPermission = true; _ = StartCoroutine(InitializeWebCamTexture()); } } private IEnumerator InitializeWebCamTexture() { #if !UNITY_6000_OR_NEWER // There is a bug on Unity 2022 that causes a crash if you don't wait a frame before initializing the WebCamTexture. // Waiting for one frame is important and prevents the bug. yield return new WaitForEndOfFrame(); #endif while (true) { var devices = WebCamTexture.devices; if (PassthroughCameraUtils.EnsureInitialized() && PassthroughCameraUtils.CameraEyeToCameraIdMap.TryGetValue(Eye, out var cameraData)) { if (cameraData.index < devices.Length) { var deviceName = devices[cameraData.index].name; WebCamTexture webCamTexture; if (RequestedResolution == Vector2Int.zero) { var largestResolution = PassthroughCameraUtils.GetOutputSizes(Eye).OrderBy(static size => size.x * size.y).Last(); webCamTexture = new WebCamTexture(deviceName, largestResolution.x, largestResolution.y); } else { webCamTexture = new WebCamTexture(deviceName, RequestedResolution.x, RequestedResolution.y); } webCamTexture.Play(); var currentResolution = new Vector2Int(webCamTexture.width, webCamTexture.height); if (RequestedResolution != Vector2Int.zero && RequestedResolution != currentResolution) { PCD.DebugMessage(LogType.Warning, $"WebCamTexture created, but '{nameof(RequestedResolution)}' {RequestedResolution} is not supported. Current resolution: {currentResolution}."); } WebCamTexture = webCamTexture; PCD.DebugMessage(LogType.Log, $"WebCamTexture created, texturePtr: {WebCamTexture.GetNativeTexturePtr()}, size: {WebCamTexture.width}/{WebCamTexture.height}"); yield break; } } PCD.DebugMessage(LogType.Error, $"Requested camera is not present in WebCamTexture.devices: {string.Join(", ", devices)}."); yield return null; } } } /// /// Defines the position of a passthrough camera relative to the headset /// public enum PassthroughCameraEye { Left, Right } }