using System; using System.Globalization; using System.IO; using UnityEngine; #if UNITY_2018_4_OR_NEWER && !NATIVE_CAMERA_DISABLE_ASYNC_FUNCTIONS using System.Threading.Tasks; using Unity.Collections; using UnityEngine.Networking; #endif #if UNITY_ANDROID || UNITY_IOS using NativeCameraNamespace; #endif using Object = UnityEngine.Object; public static class NativeCamera { public struct ImageProperties { public readonly int width; public readonly int height; public readonly string mimeType; public readonly ImageOrientation orientation; public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation ) { this.width = width; this.height = height; this.mimeType = mimeType; this.orientation = orientation; } } public struct VideoProperties { public readonly int width; public readonly int height; public readonly long duration; public readonly float rotation; public VideoProperties( int width, int height, long duration, float rotation ) { this.width = width; this.height = height; this.duration = duration; this.rotation = rotation; } } public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 }; public enum Quality { Default = -1, Low = 0, Medium = 1, High = 2 }; public enum PreferredCamera { Default = -1, Rear = 0, Front = 1 } // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered) public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; public delegate void PermissionCallback( Permission permission ); public delegate void CameraCallback( string path ); #region Platform Specific Elements #if !UNITY_EDITOR && UNITY_ANDROID private static AndroidJavaClass m_ajc = null; private static AndroidJavaClass AJC { get { if( m_ajc == null ) m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeCamera" ); return m_ajc; } } private static AndroidJavaObject m_context = null; private static AndroidJavaObject Context { get { if( m_context == null ) { using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) ) { m_context = unityClass.GetStatic( "currentActivity" ); } } return m_context; } } #elif !UNITY_EDITOR && UNITY_IOS [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern int _NativeCamera_CheckPermission(); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern int _NativeCamera_RequestPermission( int asyncMode ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern int _NativeCamera_CanOpenSettings(); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern void _NativeCamera_OpenSettings(); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern int _NativeCamera_HasCamera(); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern void _NativeCamera_TakePicture( string imageSavePath, int maxSize, int preferredCamera ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern string _NativeCamera_GetImageProperties( string path ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern string _NativeCamera_GetVideoProperties( string path ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern string _NativeCamera_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern string _NativeCamera_LoadImageAtPath( string path, string temporaryFilePath, int maxSize ); #endif #if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS ) private static string m_temporaryImagePath = null; private static string TemporaryImagePath { get { if( m_temporaryImagePath == null ) { m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" ); Directory.CreateDirectory( Application.temporaryCachePath ); } return m_temporaryImagePath; } } #endif #if !UNITY_EDITOR && UNITY_IOS private static string m_iOSSelectedImagePath = null; private static string IOSSelectedImagePath { get { if( m_iOSSelectedImagePath == null ) { m_iOSSelectedImagePath = Path.Combine( Application.temporaryCachePath, "CameraImg" ); Directory.CreateDirectory( Application.temporaryCachePath ); } return m_iOSSelectedImagePath; } } #endif #endregion #region Runtime Permissions public static Permission CheckPermission( bool isPicturePermission ) { #if !UNITY_EDITOR && UNITY_ANDROID Permission result = (Permission) AJC.CallStatic( "CheckPermission", Context, isPicturePermission ); if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeCameraPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk ) result = Permission.ShouldAsk; return result; #elif !UNITY_EDITOR && UNITY_IOS return (Permission) _NativeCamera_CheckPermission(); #else return Permission.Granted; #endif } public static Permission RequestPermission( bool isPicturePermission ) { // Don't block the main thread if the permission is already granted if( CheckPermission( isPicturePermission ) == Permission.Granted ) return Permission.Granted; #if !UNITY_EDITOR && UNITY_ANDROID object threadLock = new object(); lock( threadLock ) { NCPermissionCallbackAndroid nativeCallback = new NCPermissionCallbackAndroid( threadLock ); AJC.CallStatic( "RequestPermission", Context, nativeCallback, isPicturePermission, (int) Permission.ShouldAsk ); if( nativeCallback.Result == -1 ) System.Threading.Monitor.Wait( threadLock ); if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "NativeCameraPermission", -1 ) != nativeCallback.Result ) { PlayerPrefs.SetInt( "NativeCameraPermission", nativeCallback.Result ); PlayerPrefs.Save(); } return (Permission) nativeCallback.Result; } #elif !UNITY_EDITOR && UNITY_IOS return (Permission) _NativeCamera_RequestPermission( 0 ); #else return Permission.Granted; #endif } public static void RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission ) { #if !UNITY_EDITOR && UNITY_ANDROID NCPermissionCallbackAsyncAndroid nativeCallback = new NCPermissionCallbackAsyncAndroid( callback ); AJC.CallStatic( "RequestPermission", Context, nativeCallback, isPicturePermission, (int) Permission.ShouldAsk ); #elif !UNITY_EDITOR && UNITY_IOS NCPermissionCallbackiOS.Initialize( callback ); _NativeCamera_RequestPermission( 1 ); #else callback( Permission.Granted ); #endif } #if UNITY_2018_4_OR_NEWER && !NATIVE_CAMERA_DISABLE_ASYNC_FUNCTIONS public static Task RequestPermissionAsync( bool isPicturePermission ) { TaskCompletionSource tcs = new TaskCompletionSource(); RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), isPicturePermission ); return tcs.Task; } #endif public static bool CanOpenSettings() { #if !UNITY_EDITOR && UNITY_IOS return _NativeCamera_CanOpenSettings() == 1; #else return true; #endif } public static void OpenSettings() { #if !UNITY_EDITOR && UNITY_ANDROID AJC.CallStatic( "OpenSettings", Context ); #elif !UNITY_EDITOR && UNITY_IOS _NativeCamera_OpenSettings(); #endif } #endregion #region Camera Functions public static Permission TakePicture( CameraCallback callback, int maxSize = -1, bool saveAsJPEG = true, PreferredCamera preferredCamera = PreferredCamera.Default ) { Permission result = RequestPermission( true ); if( result == Permission.Granted && !IsCameraBusy() ) { #if UNITY_EDITOR string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select image", "", new string[] { "Image files", "png,jpg,jpeg", "All files", "*" } ); if( callback != null ) callback( pickedFile != "" ? pickedFile : null ); #elif UNITY_ANDROID AJC.CallStatic( "TakePicture", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera ); #elif UNITY_IOS if( maxSize <= 0 ) maxSize = SystemInfo.maxTextureSize; NCCameraCallbackiOS.Initialize( callback ); _NativeCamera_TakePicture( IOSSelectedImagePath + ( saveAsJPEG ? ".jpeg" : ".png" ), maxSize, (int) preferredCamera ); #else if( callback != null ) callback( null ); #endif } return result; } public static Permission RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default ) { Permission result = RequestPermission( false ); if( result == Permission.Granted && !IsCameraBusy() ) { #if UNITY_EDITOR string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select video", "", new string[] { "Video files", "mp4,mov,wav,avi", "All files", "*" } ); if( callback != null ) callback( pickedFile != "" ? pickedFile : null ); #elif UNITY_ANDROID AJC.CallStatic( "RecordVideo", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera, (int) quality, maxDuration, maxSizeBytes ); #elif UNITY_IOS NCCameraCallbackiOS.Initialize( callback ); _NativeCamera_RecordVideo( (int) quality, maxDuration, (int) preferredCamera ); #else if( callback != null ) callback( null ); #endif } return result; } public static bool DeviceHasCamera() { #if !UNITY_EDITOR && UNITY_ANDROID return AJC.CallStatic( "HasCamera", Context ); #elif !UNITY_EDITOR && UNITY_IOS return _NativeCamera_HasCamera() == 1; #else return true; #endif } public static bool IsCameraBusy() { #if !UNITY_EDITOR && UNITY_IOS return NCCameraCallbackiOS.IsBusy; #else return false; #endif } #endregion #region Utility Functions public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) { if( string.IsNullOrEmpty( imagePath ) ) throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); if( !File.Exists( imagePath ) ) throw new FileNotFoundException( "File not found at " + imagePath ); if( maxSize <= 0 ) maxSize = SystemInfo.maxTextureSize; #if !UNITY_EDITOR && UNITY_ANDROID string loadPath = AJC.CallStatic( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize ); #elif !UNITY_EDITOR && UNITY_IOS string loadPath = _NativeCamera_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize ); #else string loadPath = imagePath; #endif string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace ); try { if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) { Debug.LogWarning( "Couldn't load image at path: " + loadPath ); Object.DestroyImmediate( result ); return null; } } catch( Exception e ) { Debug.LogException( e ); Object.DestroyImmediate( result ); return null; } finally { if( loadPath != imagePath ) { try { File.Delete( loadPath ); } catch { } } } return result; } #if UNITY_2018_4_OR_NEWER && !NATIVE_CAMERA_DISABLE_ASYNC_FUNCTIONS public static async Task LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true ) { if( string.IsNullOrEmpty( imagePath ) ) throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); if( !File.Exists( imagePath ) ) throw new FileNotFoundException( "File not found at " + imagePath ); if( maxSize <= 0 ) maxSize = SystemInfo.maxTextureSize; #if !UNITY_EDITOR && UNITY_ANDROID string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) ); #elif !UNITY_EDITOR && UNITY_IOS string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread string loadPath = await Task.Run( () => _NativeCamera_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) ); #else string loadPath = imagePath; #endif Texture2D result = null; using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) ) { UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest(); while( !asyncOperation.isDone ) await Task.Yield(); #if UNITY_2020_1_OR_NEWER if( www.result != UnityWebRequest.Result.Success ) #else if( www.isNetworkError || www.isHttpError ) #endif { Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error ); } else result = DownloadHandlerTexture.GetContent( www ); } if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong { string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; result = new Texture2D( 2, 2, format, true, false ); try { if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) { Debug.LogWarning( "Couldn't load image at path: " + loadPath ); Object.DestroyImmediate( result ); return null; } } catch( Exception e ) { Debug.LogException( e ); Object.DestroyImmediate( result ); return null; } finally { if( loadPath != imagePath ) { try { File.Delete( loadPath ); } catch { } } } } return result; } #endif public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) { if( maxSize <= 0 ) maxSize = SystemInfo.maxTextureSize; #if !UNITY_EDITOR && UNITY_ANDROID string thumbnailPath = AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ); #elif !UNITY_EDITOR && UNITY_IOS string thumbnailPath = _NativeCamera_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds ); #else string thumbnailPath = null; #endif if( !string.IsNullOrEmpty( thumbnailPath ) ) return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace ); else return null; } #if UNITY_2018_4_OR_NEWER && !NATIVE_CAMERA_DISABLE_ASYNC_FUNCTIONS public static async Task GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true ) { if( maxSize <= 0 ) maxSize = SystemInfo.maxTextureSize; #if !UNITY_EDITOR && UNITY_ANDROID string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) ); #elif !UNITY_EDITOR && UNITY_IOS string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread string thumbnailPath = await Task.Run( () => _NativeCamera_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) ); #else string thumbnailPath = null; #endif if( !string.IsNullOrEmpty( thumbnailPath ) ) return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable ); else return null; } private static async Task TryCallNativeAndroidFunctionOnSeparateThread( Func function ) { T result = default( T ); bool hasResult = false; await Task.Run( () => { if( AndroidJNI.AttachCurrentThread() != 0 ) Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" ); else { try { result = function(); hasResult = true; } finally { AndroidJNI.DetachCurrentThread(); } } } ); return hasResult ? result : function(); } #endif public static ImageProperties GetImageProperties( string imagePath ) { if( !File.Exists( imagePath ) ) throw new FileNotFoundException( "File not found at " + imagePath ); #if !UNITY_EDITOR && UNITY_ANDROID string value = AJC.CallStatic( "GetImageProperties", Context, imagePath ); #elif !UNITY_EDITOR && UNITY_IOS string value = _NativeCamera_GetImageProperties( imagePath ); #else string value = null; #endif int width = 0, height = 0; string mimeType = null; ImageOrientation orientation = ImageOrientation.Unknown; if( !string.IsNullOrEmpty( value ) ) { string[] properties = value.Split( '>' ); if( properties != null && properties.Length >= 4 ) { if( !int.TryParse( properties[0].Trim(), out width ) ) width = 0; if( !int.TryParse( properties[1].Trim(), out height ) ) height = 0; mimeType = properties[2].Trim(); if( mimeType.Length == 0 ) { string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); if( extension == ".png" ) mimeType = "image/png"; else if( extension == ".jpg" || extension == ".jpeg" ) mimeType = "image/jpeg"; else if( extension == ".gif" ) mimeType = "image/gif"; else if( extension == ".bmp" ) mimeType = "image/bmp"; else mimeType = null; } int orientationInt; if( int.TryParse( properties[3].Trim(), out orientationInt ) ) orientation = (ImageOrientation) orientationInt; } } return new ImageProperties( width, height, mimeType, orientation ); } public static VideoProperties GetVideoProperties( string videoPath ) { if( !File.Exists( videoPath ) ) throw new FileNotFoundException( "File not found at " + videoPath ); #if !UNITY_EDITOR && UNITY_ANDROID string value = AJC.CallStatic( "GetVideoProperties", Context, videoPath ); #elif !UNITY_EDITOR && UNITY_IOS string value = _NativeCamera_GetVideoProperties( videoPath ); #else string value = null; #endif int width = 0, height = 0; long duration = 0L; float rotation = 0f; if( !string.IsNullOrEmpty( value ) ) { string[] properties = value.Split( '>' ); if( properties != null && properties.Length >= 4 ) { if( !int.TryParse( properties[0].Trim(), out width ) ) width = 0; if( !int.TryParse( properties[1].Trim(), out height ) ) height = 0; if( !long.TryParse( properties[2].Trim(), out duration ) ) duration = 0L; if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) ) rotation = 0f; } } if( rotation == -90f ) rotation = 270f; return new VideoProperties( width, height, duration, rotation ); } #endregion }