// Copyright (c) 2021 homuler // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; #if UNITY_ANDROID using UnityEngine.Android; #endif namespace Mediapipe.Unity { public class WebCamSource : ImageSource { [Tooltip("For the default resolution, the one whose width is closest to this value will be chosen")] [SerializeField] private int _preferableDefaultWidth = 1280; private const string _TAG = nameof(WebCamSource); [SerializeField] private ResolutionStruct[] _defaultAvailableResolutions = new ResolutionStruct[] { new ResolutionStruct(176, 144, 30), new ResolutionStruct(320, 240, 30), new ResolutionStruct(424, 240, 30), new ResolutionStruct(640, 360, 30), new ResolutionStruct(640, 480, 30), new ResolutionStruct(848, 480, 30), new ResolutionStruct(960, 540, 30), new ResolutionStruct(1280, 720, 30), new ResolutionStruct(1600, 896, 30), new ResolutionStruct(1920, 1080, 30), }; private static readonly object _PermissionLock = new object(); private static bool _IsPermitted = false; private WebCamTexture _webCamTexture; private WebCamTexture webCamTexture { get => _webCamTexture; set { if (_webCamTexture != null) { _webCamTexture.Stop(); } _webCamTexture = value; } } public override int textureWidth => !isPrepared ? 0 : webCamTexture.width; public override int textureHeight => !isPrepared ? 0 : webCamTexture.height; public override bool isVerticallyFlipped => isPrepared && webCamTexture.videoVerticallyMirrored; public override bool isFrontFacing => isPrepared && (webCamDevice is WebCamDevice valueOfWebCamDevice) && valueOfWebCamDevice.isFrontFacing; public override RotationAngle rotation => !isPrepared ? RotationAngle.Rotation0 : (RotationAngle)webCamTexture.videoRotationAngle; private WebCamDevice? _webCamDevice; private WebCamDevice? webCamDevice { get => _webCamDevice; set { if (_webCamDevice is WebCamDevice valueOfWebCamDevice) { if (value is WebCamDevice valueOfValue && valueOfValue.name == valueOfWebCamDevice.name) { // not changed return; } } else if (value == null) { // not changed return; } _webCamDevice = value; resolution = GetDefaultResolution(); } } public override string sourceName => (webCamDevice is WebCamDevice valueOfWebCamDevice) ? valueOfWebCamDevice.name : null; private WebCamDevice[] _availableSources; private WebCamDevice[] availableSources { get { if (_availableSources == null) { _availableSources = WebCamTexture.devices; } return _availableSources; } set => _availableSources = value; } public override string[] sourceCandidateNames => availableSources?.Select(device => device.name).ToArray(); #pragma warning disable IDE0025 public override ResolutionStruct[] availableResolutions { get { #if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR if (webCamDevice is WebCamDevice valueOfWebCamDevice) { return valueOfWebCamDevice.availableResolutions.Select(resolution => new ResolutionStruct(resolution)).ToArray(); } #endif return webCamDevice == null ? null : _defaultAvailableResolutions; } } #pragma warning restore IDE0025 public override bool isPrepared => webCamTexture != null; public override bool isPlaying => webCamTexture != null && webCamTexture.isPlaying; private bool _isInitialized; private IEnumerator Start() { yield return GetPermission(); if (!_IsPermitted) { _isInitialized = true; yield break; } availableSources = WebCamTexture.devices; if (availableSources != null && availableSources.Length > 0) { webCamDevice = availableSources[0]; } _isInitialized = true; } private IEnumerator GetPermission() { lock (_PermissionLock) { if (_IsPermitted) { yield break; } #if UNITY_ANDROID if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { Permission.RequestUserPermission(Permission.Camera); yield return new WaitForSeconds(0.1f); } #elif UNITY_IOS if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) { yield return Application.RequestUserAuthorization(UserAuthorization.WebCam); } #endif #if UNITY_ANDROID if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { Logger.LogWarning(_TAG, "Not permitted to use Camera"); yield break; } #elif UNITY_IOS if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) { Logger.LogWarning(_TAG, "Not permitted to use WebCam"); yield break; } #endif _IsPermitted = true; yield return new WaitForEndOfFrame(); } } public override void SelectSource(int sourceId) { if (sourceId < 0 || sourceId >= availableSources.Length) { throw new ArgumentException($"Invalid source ID: {sourceId}"); } webCamDevice = availableSources[sourceId]; } public override IEnumerator Play() { yield return new WaitUntil(() => _isInitialized); if (!_IsPermitted) { throw new InvalidOperationException("Not permitted to access cameras"); } InitializeWebCamTexture(); webCamTexture.Play(); yield return WaitForWebCamTexture(); } public override IEnumerator Resume() { if (!isPrepared) { throw new InvalidOperationException("WebCamTexture is not prepared yet"); } if (!webCamTexture.isPlaying) { webCamTexture.Play(); } yield return WaitForWebCamTexture(); } public override void Pause() { if (isPlaying) { webCamTexture.Pause(); } } public override void Stop() { if (webCamTexture != null) { webCamTexture.Stop(); } webCamTexture = null; } public override Texture GetCurrentTexture() { return webCamTexture; } private ResolutionStruct GetDefaultResolution() { var resolutions = availableResolutions; return resolutions == null || resolutions.Length == 0 ? new ResolutionStruct() : resolutions.OrderBy(resolution => resolution, new ResolutionStructComparer(_preferableDefaultWidth)).First(); } private void InitializeWebCamTexture() { Stop(); if (webCamDevice is WebCamDevice valueOfWebCamDevice) { webCamTexture = new WebCamTexture(valueOfWebCamDevice.name, resolution.width, resolution.height, (int)resolution.frameRate); return; } throw new InvalidOperationException("Cannot initialize WebCamTexture because WebCamDevice is not selected"); } private IEnumerator WaitForWebCamTexture() { const int timeoutFrame = 2000; var count = 0; Logger.LogVerbose("Waiting for WebCamTexture to start"); yield return new WaitUntil(() => count++ > timeoutFrame || webCamTexture.width > 16); if (webCamTexture.width <= 16) { throw new TimeoutException("Failed to start WebCam"); } } private class ResolutionStructComparer : IComparer { private readonly int _preferableDefaultWidth; public ResolutionStructComparer(int preferableDefaultWidth) { _preferableDefaultWidth = preferableDefaultWidth; } public int Compare(ResolutionStruct a, ResolutionStruct b) { var aDiff = Mathf.Abs(a.width - _preferableDefaultWidth); var bDiff = Mathf.Abs(b.width - _preferableDefaultWidth); if (aDiff != bDiff) { return aDiff - bDiff; } if (a.height != b.height) { // prefer smaller height return a.height - b.height; } // prefer smaller frame rate return (int)(a.frameRate - b.frameRate); } } } }