up
This commit is contained in:
8
Assets/3rd/NativeCamera/Plugins.meta
Normal file
8
Assets/3rd/NativeCamera/Plugins.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bb6a665b7d9041bab99a6efdbc705ca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/3rd/NativeCamera/Plugins/Android.meta
Normal file
9
Assets/3rd/NativeCamera/Plugins/Android.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fc918beb0f7cfe49a7f596d5b6b8768
|
||||
folderAsset: yes
|
||||
timeCreated: 1525098661
|
||||
licenseType: Free
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Assets/3rd/NativeCamera/Plugins/Android/NCCallbackHelper.cs
Normal file
31
Assets/3rd/NativeCamera/Plugins/Android/NCCallbackHelper.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
using UnityEngine;
|
||||
|
||||
namespace NativeCameraNamespace
|
||||
{
|
||||
public class NCCallbackHelper : MonoBehaviour
|
||||
{
|
||||
private System.Action mainThreadAction = null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
DontDestroyOnLoad( gameObject );
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if( mainThreadAction != null )
|
||||
{
|
||||
System.Action temp = mainThreadAction;
|
||||
mainThreadAction = null;
|
||||
temp();
|
||||
}
|
||||
}
|
||||
|
||||
public void CallOnMainThread( System.Action function )
|
||||
{
|
||||
mainThreadAction = function;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2bbe0051e738ea4585119c46d863f19
|
||||
timeCreated: 1545147258
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
using UnityEngine;
|
||||
|
||||
namespace NativeCameraNamespace
|
||||
{
|
||||
public class NCCameraCallbackAndroid : AndroidJavaProxy
|
||||
{
|
||||
private readonly NativeCamera.CameraCallback callback;
|
||||
private readonly NCCallbackHelper callbackHelper;
|
||||
|
||||
public NCCameraCallbackAndroid( NativeCamera.CameraCallback callback ) : base( "com.yasirkula.unity.NativeCameraMediaReceiver" )
|
||||
{
|
||||
this.callback = callback;
|
||||
callbackHelper = new GameObject( "NCCallbackHelper" ).AddComponent<NCCallbackHelper>();
|
||||
}
|
||||
|
||||
public void OnMediaReceived( string path )
|
||||
{
|
||||
callbackHelper.CallOnMainThread( () => MediaReceiveCallback( path ) );
|
||||
}
|
||||
|
||||
private void MediaReceiveCallback( string path )
|
||||
{
|
||||
if( string.IsNullOrEmpty( path ) )
|
||||
path = null;
|
||||
|
||||
try
|
||||
{
|
||||
if( callback != null )
|
||||
callback( path );
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.Destroy( callbackHelper );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cc8df584d2a4344b929a4f13a53723a
|
||||
timeCreated: 1519060539
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NativeCameraNamespace
|
||||
{
|
||||
public class NCPermissionCallbackAndroid : AndroidJavaProxy
|
||||
{
|
||||
private object threadLock;
|
||||
public int Result { get; private set; }
|
||||
|
||||
public NCPermissionCallbackAndroid( object threadLock ) : base( "com.yasirkula.unity.NativeCameraPermissionReceiver" )
|
||||
{
|
||||
Result = -1;
|
||||
this.threadLock = threadLock;
|
||||
}
|
||||
|
||||
public void OnPermissionResult( int result )
|
||||
{
|
||||
Result = result;
|
||||
|
||||
lock( threadLock )
|
||||
{
|
||||
Monitor.Pulse( threadLock );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bafa24bbc8c455f44a2b98dcbe6451bd
|
||||
timeCreated: 1519060539
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/3rd/NativeCamera/Plugins/Android/NativeCamera.aar
Normal file
BIN
Assets/3rd/NativeCamera/Plugins/Android/NativeCamera.aar
Normal file
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 284037eba2526f54d9cf51b5d9bffcfa
|
||||
timeCreated: 1569764737
|
||||
licenseType: Free
|
||||
PluginImporter:
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
platformData:
|
||||
data:
|
||||
first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
data:
|
||||
first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
data:
|
||||
first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/3rd/NativeCamera/Plugins/Editor.meta
Normal file
9
Assets/3rd/NativeCamera/Plugins/Editor.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16fe39fd709533a4eba946790a8e3123
|
||||
folderAsset: yes
|
||||
timeCreated: 1521452097
|
||||
licenseType: Free
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
66
Assets/3rd/NativeCamera/Plugins/Editor/NCPostProcessBuild.cs
Normal file
66
Assets/3rd/NativeCamera/Plugins/Editor/NCPostProcessBuild.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if UNITY_IOS
|
||||
using UnityEditor.Callbacks;
|
||||
using UnityEditor.iOS.Xcode;
|
||||
#endif
|
||||
|
||||
public class NCPostProcessBuild
|
||||
{
|
||||
private const bool ENABLED = true;
|
||||
|
||||
private const string CAMERA_USAGE_DESCRIPTION = "Capture media with camera";
|
||||
private const string MICROPHONE_USAGE_DESCRIPTION = "Capture microphone input in videos";
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
public static void ValidatePlugin()
|
||||
{
|
||||
string jarPath = "Assets/Plugins/NativeCamera/Android/NativeCamera.jar";
|
||||
if( File.Exists( jarPath ) )
|
||||
{
|
||||
Debug.Log( "Deleting obsolete " + jarPath );
|
||||
AssetDatabase.DeleteAsset( jarPath );
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_IOS
|
||||
#pragma warning disable 0162
|
||||
[PostProcessBuild]
|
||||
public static void OnPostprocessBuild( BuildTarget target, string buildPath )
|
||||
{
|
||||
if( !ENABLED )
|
||||
return;
|
||||
|
||||
if( target == BuildTarget.iOS )
|
||||
{
|
||||
string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath );
|
||||
string plistPath = Path.Combine( buildPath, "Info.plist" );
|
||||
|
||||
PBXProject pbxProject = new PBXProject();
|
||||
pbxProject.ReadFromFile( pbxProjectPath );
|
||||
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
string targetGUID = pbxProject.GetUnityFrameworkTargetGuid();
|
||||
#else
|
||||
string targetGUID = pbxProject.TargetGuidByName( PBXProject.GetUnityTargetName() );
|
||||
#endif
|
||||
|
||||
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" );
|
||||
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" );
|
||||
|
||||
File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
|
||||
|
||||
PlistDocument plist = new PlistDocument();
|
||||
plist.ReadFromString( File.ReadAllText( plistPath ) );
|
||||
|
||||
PlistElementDict rootDict = plist.root;
|
||||
rootDict.SetString( "NSCameraUsageDescription", CAMERA_USAGE_DESCRIPTION );
|
||||
rootDict.SetString( "NSMicrophoneUsageDescription", MICROPHONE_USAGE_DESCRIPTION );
|
||||
|
||||
File.WriteAllText( plistPath, plist.WriteToString() );
|
||||
}
|
||||
}
|
||||
#pragma warning restore 0162
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa3b57e342928704cb910789ae4dde20
|
||||
timeCreated: 1521452119
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "NativeCamera.Editor",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31117d0234af0084b91a7e53b3d9e0a3
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "NativeCamera.Runtime"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b107fd1956cb3e04985108f5ee29e115
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
442
Assets/3rd/NativeCamera/Plugins/NativeCamera.cs
Normal file
442
Assets/3rd/NativeCamera/Plugins/NativeCamera.cs
Normal file
@@ -0,0 +1,442 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
|
||||
using NativeCameraNamespace;
|
||||
#endif
|
||||
|
||||
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 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<AndroidJavaObject>( "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();
|
||||
|
||||
[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()
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
Permission result = (Permission) AJC.CallStatic<int>( "CheckPermission", Context );
|
||||
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()
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
object threadLock = new object();
|
||||
lock( threadLock )
|
||||
{
|
||||
NCPermissionCallbackAndroid nativeCallback = new NCPermissionCallbackAndroid( threadLock );
|
||||
|
||||
AJC.CallStatic( "RequestPermission", Context, nativeCallback, PlayerPrefs.GetInt( "NativeCameraPermission", (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();
|
||||
#else
|
||||
return Permission.Granted;
|
||||
#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();
|
||||
if( result == Permission.Granted && !IsCameraBusy() )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
AJC.CallStatic( "TakePicture", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera );
|
||||
#elif !UNITY_EDITOR && 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();
|
||||
if( result == Permission.Granted && !IsCameraBusy() )
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
AJC.CallStatic( "RecordVideo", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera, (int) quality, maxDuration, maxSizeBytes );
|
||||
#elif !UNITY_EDITOR && 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<bool>( "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<string>( "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 ) )
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0 )
|
||||
{
|
||||
if( maxSize <= 0 )
|
||||
maxSize = SystemInfo.maxTextureSize;
|
||||
|
||||
#if !UNITY_EDITOR && UNITY_ANDROID
|
||||
string thumbnailPath = AJC.CallStatic<string>( "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 );
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
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<string>( "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<string>( "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(), out rotation ) )
|
||||
rotation = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
if( rotation == -90f )
|
||||
rotation = 270f;
|
||||
|
||||
return new VideoProperties( width, height, duration, rotation );
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
12
Assets/3rd/NativeCamera/Plugins/NativeCamera.cs.meta
Normal file
12
Assets/3rd/NativeCamera/Plugins/NativeCamera.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff758a73b21d4a04aa6f95679b3da605
|
||||
timeCreated: 1498722610
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
90
Assets/3rd/NativeCamera/Plugins/README.txt
Normal file
90
Assets/3rd/NativeCamera/Plugins/README.txt
Normal file
@@ -0,0 +1,90 @@
|
||||
= Native Camera for Android & iOS =
|
||||
|
||||
Online documentation & example code available at: https://github.com/yasirkula/UnityNativeCamera
|
||||
E-mail: yasirkula@gmail.com
|
||||
|
||||
1. ABOUT
|
||||
This plugin helps you take pictures/record videos natively with your device's camera on Android & iOS.
|
||||
|
||||
2. HOW TO
|
||||
NativeCamera no longer requires any manual setup on Android. If you were using an older version of the plugin, you need to remove NativeCamera's "<provider ... />" from your AndroidManifest.xml.
|
||||
|
||||
For reference, the legacy documentation is available at: https://github.com/yasirkula/UnityNativeCamera/wiki/Manual-Setup-for-Android
|
||||
|
||||
2.2. iOS Setup
|
||||
There are two ways to set up the plugin on iOS:
|
||||
|
||||
2.2.a. Automated Setup for iOS
|
||||
- (optional) change the value of CAMERA_USAGE_DESCRIPTION in Plugins/NativeCamera/Editor/NCPostProcessBuild.cs
|
||||
|
||||
2.2.b. Manual Setup for iOS
|
||||
- set the value of ENABLED to false in NCPostProcessBuild.cs
|
||||
- build your project
|
||||
- enter a Camera Usage Description to Info.plist in Xcode
|
||||
- insert "-framework MobileCoreServices -framework ImageIO" to the "Other Linker Flags" of Unity-iPhone Target
|
||||
|
||||
3. FAQ
|
||||
- Can't use the camera, it says "Can't find ContentProvider, camera is inaccessible!" in Logcat
|
||||
After building your project, verify that NativeCamera's "<provider ... />" tag is inserted in-between the "<application>...</application>" tags of PROJECT_PATH/Temp/StagingArea/AndroidManifest.xml. If not, please contact me.
|
||||
|
||||
- Can't use the camera, it says "java.lang.ClassNotFoundException: com.yasirkula.unity.NativeCamera" in Logcat
|
||||
If your project uses ProGuard, try adding the following line to ProGuard filters: -keep class com.yasirkula.unity.* { *; }
|
||||
|
||||
4. SCRIPTING API
|
||||
Please see the online documentation for a more in-depth documentation of the Scripting API: https://github.com/yasirkula/UnityNativeCamera
|
||||
|
||||
enum NativeCamera.Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
|
||||
enum NativeCamera.Quality { Default = -1, Low = 0, Medium = 1, High = 2 };
|
||||
enum NativeCamera.ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
|
||||
|
||||
delegate void CameraCallback( string path );
|
||||
|
||||
//// Accessing Camera ////
|
||||
|
||||
// This operation is asynchronous! After user takes a picture or cancels the operation, the callback is called (on main thread)
|
||||
// CameraCallback takes a string parameter which stores the path of the captured image, or null if the operation is canceled
|
||||
// maxSize: determines the maximum size of the returned image in pixels on iOS. A larger image will be down-scaled for better performance. If untouched, its value will be set to SystemInfo.maxTextureSize. Has no effect on Android
|
||||
// saveAsJPEG: determines whether the image is saved as JPEG or PNG. Has no effect on Android
|
||||
// preferredCamera: determines whether the rear camera or the front camera should be opened by default
|
||||
NativeCamera.Permission NativeCamera.TakePicture( CameraCallback callback, int maxSize = -1, bool saveAsJPEG = true, PreferredCamera preferredCamera = PreferredCamera.Default );
|
||||
|
||||
// quality: determines the quality of the recorded video
|
||||
// maxDuration: determines the maximum duration, in seconds, for the recorded video. If untouched, there will be no limit. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices
|
||||
// maxSizeBytes: determines the maximum size, in bytes, for the recorded video. If untouched, there will be no limit. This parameter has no effect on iOS. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices
|
||||
NativeCamera.Permission NativeCamera.RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default );
|
||||
|
||||
bool NativeCamera.DeviceHasCamera(); // returns false if the device doesn't have a camera. In this case, TakePicture and RecordVideo functions will not execute
|
||||
|
||||
bool NativeCamera.IsCameraBusy(); // returns true if the camera is currently open. In that case, another TakePicture or RecordVideo request will simply be ignored
|
||||
|
||||
|
||||
//// Runtime Permissions ////
|
||||
|
||||
// Accessing camera is only possible when permission state is Permission.Granted. TakePicture and RecordVideo functions request permission internally (and return the result) but you can also check/request the permissions manually
|
||||
NativeCamera.Permission NativeCamera.CheckPermission();
|
||||
NativeCamera.Permission NativeCamera.RequestPermission();
|
||||
|
||||
// If permission state is Permission.Denied, user must grant the necessary permission(s) manually from the Settings (Android requires Storage and, if declared in AndroidManifest, Camera permissions; iOS requires Camera permission). These functions help you open the Settings directly from within the app
|
||||
void NativeCamera.OpenSettings();
|
||||
bool NativeCamera.CanOpenSettings();
|
||||
|
||||
|
||||
//// Utility Functions ////
|
||||
|
||||
// Creates a Texture2D from the specified image file in correct orientation and returns it. Returns null, if something goes wrong
|
||||
// maxSize: determines the maximum size of the returned Texture2D in pixels. Larger textures will be down-scaled. If untouched, its value will be set to SystemInfo.maxTextureSize. It is recommended to set a proper maxSize for better performance
|
||||
// markTextureNonReadable: marks the generated texture as non-readable for better memory usage. If you plan to modify the texture later (e.g. GetPixels/SetPixels), set its value to false
|
||||
// generateMipmaps: determines whether texture should have mipmaps or not
|
||||
// linearColorSpace: determines whether texture should be in linear color space or sRGB color space
|
||||
Texture2D NativeCamera.LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false );
|
||||
|
||||
// Creates a Texture2D thumbnail from a video file and returns it. Returns null, if something goes wrong
|
||||
// maxSize: determines the maximum size of the returned Texture2D in pixels. Larger thumbnails will be down-scaled. If untouched, its value will be set to SystemInfo.maxTextureSize. It is recommended to set a proper maxSize for better performance
|
||||
// captureTimeInSeconds: determines the frame of the video that the thumbnail is captured from. If untouched, OS will decide this value
|
||||
Texture2D NativeCamera.GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0 );
|
||||
|
||||
// Returns an ImageProperties instance that holds the width, height and mime type information of an image file without creating a Texture2D object. Mime type will be null, if it can't be determined
|
||||
NativeCamera.ImageProperties NativeCamera.GetImageProperties( string imagePath );
|
||||
|
||||
// Returns a VideoProperties instance that holds the width, height, duration (in milliseconds) and rotation information of a video file. To play a video in correct orientation, you should rotate it by rotation degrees clockwise. For a 90-degree or 270-degree rotated video, values of width and height should be swapped to get the display size of the video
|
||||
NativeCamera.VideoProperties NativeCamera.GetVideoProperties( string videoPath );
|
||||
8
Assets/3rd/NativeCamera/Plugins/README.txt.meta
Normal file
8
Assets/3rd/NativeCamera/Plugins/README.txt.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a88d1b1b9d7b904b862304c20ed4db4
|
||||
timeCreated: 1563308465
|
||||
licenseType: Free
|
||||
TextScriptImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/3rd/NativeCamera/Plugins/iOS.meta
Normal file
9
Assets/3rd/NativeCamera/Plugins/iOS.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5576acd2a06eb72409e6ec4b7f204a4e
|
||||
folderAsset: yes
|
||||
timeCreated: 1498722622
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
72
Assets/3rd/NativeCamera/Plugins/iOS/NCCameraCallbackiOS.cs
Normal file
72
Assets/3rd/NativeCamera/Plugins/iOS/NCCameraCallbackiOS.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
#if !UNITY_EDITOR && UNITY_IOS
|
||||
using UnityEngine;
|
||||
|
||||
namespace NativeCameraNamespace
|
||||
{
|
||||
public class NCCameraCallbackiOS : MonoBehaviour
|
||||
{
|
||||
private static NCCameraCallbackiOS instance;
|
||||
private NativeCamera.CameraCallback callback;
|
||||
|
||||
private float nextBusyCheckTime;
|
||||
|
||||
public static bool IsBusy { get; private set; }
|
||||
|
||||
[System.Runtime.InteropServices.DllImport( "__Internal" )]
|
||||
private static extern int _NativeCamera_IsCameraBusy();
|
||||
|
||||
public static void Initialize( NativeCamera.CameraCallback callback )
|
||||
{
|
||||
if( IsBusy )
|
||||
return;
|
||||
|
||||
if( instance == null )
|
||||
{
|
||||
instance = new GameObject( "NCCameraCallbackiOS" ).AddComponent<NCCameraCallbackiOS>();
|
||||
DontDestroyOnLoad( instance.gameObject );
|
||||
}
|
||||
|
||||
instance.callback = callback;
|
||||
|
||||
instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
|
||||
IsBusy = true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if( IsBusy )
|
||||
{
|
||||
if( Time.realtimeSinceStartup >= nextBusyCheckTime )
|
||||
{
|
||||
nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
|
||||
|
||||
if( _NativeCamera_IsCameraBusy() == 0 )
|
||||
{
|
||||
IsBusy = false;
|
||||
|
||||
if( callback != null )
|
||||
{
|
||||
callback( null );
|
||||
callback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnMediaReceived( string path )
|
||||
{
|
||||
IsBusy = false;
|
||||
|
||||
if( string.IsNullOrEmpty( path ) )
|
||||
path = null;
|
||||
|
||||
if( callback != null )
|
||||
{
|
||||
callback( path );
|
||||
callback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8f19d5713752dc41bd377562677d8ee
|
||||
timeCreated: 1519060539
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
507
Assets/3rd/NativeCamera/Plugins/iOS/NativeCamera.mm
Normal file
507
Assets/3rd/NativeCamera/Plugins/iOS/NativeCamera.mm
Normal file
@@ -0,0 +1,507 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#ifdef UNITY_4_0 || UNITY_5_0
|
||||
#import "iPhone_View.h"
|
||||
#else
|
||||
extern UIViewController* UnityGetGLViewController();
|
||||
#endif
|
||||
|
||||
@interface UNativeCamera:NSObject
|
||||
+ (int)checkPermission;
|
||||
+ (int)requestPermission;
|
||||
+ (int)canOpenSettings;
|
||||
+ (void)openSettings;
|
||||
+ (int)hasCamera;
|
||||
+ (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration;
|
||||
+ (int)isCameraBusy;
|
||||
+ (char *)getImageProperties:(NSString *)path;
|
||||
+ (char *)getVideoProperties:(NSString *)path;
|
||||
+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime;
|
||||
+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize;
|
||||
@end
|
||||
|
||||
@implementation UNativeCamera
|
||||
|
||||
static NSString *pickedMediaSavePath;
|
||||
static UIImagePickerController *imagePicker;
|
||||
static int cameraMaxImageSize = -1;
|
||||
static int imagePickerState = 0; // 0 -> none, 1 -> showing, 2 -> finished
|
||||
|
||||
// Credit: https://stackoverflow.com/a/20464727/2373034
|
||||
+ (int)checkPermission {
|
||||
if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending)
|
||||
{
|
||||
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
if (status == AVAuthorizationStatusAuthorized)
|
||||
return 1;
|
||||
else if (status == AVAuthorizationStatusNotDetermined )
|
||||
return 2;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/20464727/2373034
|
||||
+ (int)requestPermission {
|
||||
if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending)
|
||||
{
|
||||
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
if (status == AVAuthorizationStatusAuthorized)
|
||||
return 1;
|
||||
|
||||
if (status == AVAuthorizationStatusNotDetermined) {
|
||||
__block BOOL authorized = NO;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
||||
authorized = granted;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (authorized)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/25453667/2373034
|
||||
+ (int)canOpenSettings {
|
||||
if (&UIApplicationOpenSettingsURLString != NULL)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/25453667/2373034
|
||||
+ (void)openSettings {
|
||||
if (&UIApplicationOpenSettingsURLString != NULL)
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
|
||||
}
|
||||
|
||||
+ (int)hasCamera {
|
||||
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/10531752/2373034
|
||||
+ (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration {
|
||||
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
|
||||
{
|
||||
NSLog(@"Device has no registered cameras!");
|
||||
|
||||
UnitySendMessage("NCCameraCallbackiOS", "OnMediaReceived", "");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeImage]) ||
|
||||
(!imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeMovie]))
|
||||
{
|
||||
NSLog(@"Camera does not support this operation!");
|
||||
|
||||
UnitySendMessage("NCCameraCallbackiOS", "OnMediaReceived", "");
|
||||
return;
|
||||
}
|
||||
|
||||
imagePicker = [[UIImagePickerController alloc] init];
|
||||
imagePicker.delegate = self;
|
||||
imagePicker.allowsEditing = NO;
|
||||
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
||||
|
||||
if (imageMode)
|
||||
imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
|
||||
else
|
||||
{
|
||||
imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeMovie];
|
||||
|
||||
if (maxVideoDuration > 0)
|
||||
imagePicker.videoMaximumDuration = maxVideoDuration;
|
||||
|
||||
if (videoQuality == 0)
|
||||
imagePicker.videoQuality = UIImagePickerControllerQualityTypeLow;
|
||||
else if (videoQuality == 1)
|
||||
imagePicker.videoQuality = UIImagePickerControllerQualityTypeMedium;
|
||||
else if (videoQuality == 2)
|
||||
imagePicker.videoQuality = UIImagePickerControllerQualityTypeHigh;
|
||||
}
|
||||
|
||||
if (defaultCamera == 0 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])
|
||||
imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
|
||||
else if (defaultCamera == 1 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])
|
||||
imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront;
|
||||
|
||||
pickedMediaSavePath = imageSavePath;
|
||||
cameraMaxImageSize = maxImageSize;
|
||||
|
||||
imagePickerState = 1;
|
||||
[UnityGetGLViewController() presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];
|
||||
}
|
||||
|
||||
+ (int)isCameraBusy {
|
||||
if (imagePickerState == 2)
|
||||
return 1;
|
||||
|
||||
if (imagePicker != nil) {
|
||||
if (imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController())
|
||||
return 1;
|
||||
|
||||
imagePicker = nil;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/4170099/2373034
|
||||
+ (NSArray *)getImageMetadata:(NSString *)path {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int orientation = -1;
|
||||
|
||||
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], nil);
|
||||
if (imageSource != nil) {
|
||||
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache];
|
||||
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
|
||||
CFRelease(imageSource);
|
||||
|
||||
CGFloat widthF = 0.0f, heightF = 0.0f;
|
||||
if (imageProperties != nil) {
|
||||
if (CFDictionaryContainsKey(imageProperties, kCGImagePropertyPixelWidth))
|
||||
CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth), kCFNumberCGFloatType, &widthF);
|
||||
|
||||
if (CFDictionaryContainsKey(imageProperties, kCGImagePropertyPixelHeight))
|
||||
CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight), kCFNumberCGFloatType, &heightF);
|
||||
|
||||
if (CFDictionaryContainsKey(imageProperties, kCGImagePropertyOrientation)) {
|
||||
CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(imageProperties, kCGImagePropertyOrientation), kCFNumberIntType, &orientation);
|
||||
|
||||
if (orientation > 4) { // landscape image
|
||||
CGFloat temp = widthF;
|
||||
widthF = heightF;
|
||||
heightF = temp;
|
||||
}
|
||||
}
|
||||
|
||||
CFRelease(imageProperties);
|
||||
}
|
||||
|
||||
width = (int)roundf(widthF);
|
||||
height = (int)roundf(heightF);
|
||||
}
|
||||
|
||||
return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil];
|
||||
}
|
||||
|
||||
+ (char *)getImageProperties:(NSString *)path {
|
||||
NSArray *metadata = [self getImageMetadata:path];
|
||||
|
||||
int orientationUnity;
|
||||
int orientation = [metadata[2] intValue];
|
||||
|
||||
// To understand the magic numbers, see ImageOrientation enum in NativeCamera.cs
|
||||
// and http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
if (orientation == 1)
|
||||
orientationUnity = 0;
|
||||
else if (orientation == 2)
|
||||
orientationUnity = 4;
|
||||
else if (orientation == 3)
|
||||
orientationUnity = 2;
|
||||
else if (orientation == 4)
|
||||
orientationUnity = 6;
|
||||
else if (orientation == 5)
|
||||
orientationUnity = 5;
|
||||
else if (orientation == 6)
|
||||
orientationUnity = 1;
|
||||
else if (orientation == 7)
|
||||
orientationUnity = 7;
|
||||
else if (orientation == 8)
|
||||
orientationUnity = 3;
|
||||
else
|
||||
orientationUnity = -1;
|
||||
|
||||
return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]];
|
||||
}
|
||||
|
||||
+ (char *)getVideoProperties:(NSString *)path {
|
||||
CGSize size = CGSizeZero;
|
||||
float rotation = 0;
|
||||
long long duration = 0;
|
||||
|
||||
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
|
||||
if (asset != nil) {
|
||||
duration = (long long) round(CMTimeGetSeconds([asset duration]) * 1000);
|
||||
CGAffineTransform transform = [asset preferredTransform];
|
||||
NSArray<AVAssetTrack *>* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
|
||||
if (videoTracks != nil && [videoTracks count] > 0) {
|
||||
size = [[videoTracks objectAtIndex:0] naturalSize];
|
||||
transform = [[videoTracks objectAtIndex:0] preferredTransform];
|
||||
}
|
||||
|
||||
rotation = atan2(transform.b, transform.a) * (180.0 / M_PI);
|
||||
}
|
||||
|
||||
return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int)roundf(size.width), (int)roundf(size.height), duration, rotation]];
|
||||
}
|
||||
|
||||
+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime {
|
||||
AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]];
|
||||
thumbnailGenerator.appliesPreferredTrackTransform = YES;
|
||||
thumbnailGenerator.maximumSize = CGSizeMake((CGFloat) maximumSize, (CGFloat) maximumSize);
|
||||
thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero;
|
||||
thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero;
|
||||
|
||||
if (captureTime < 0.0)
|
||||
captureTime = 0.0;
|
||||
else {
|
||||
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
|
||||
if (asset != nil) {
|
||||
double videoDuration = CMTimeGetSeconds([asset duration]);
|
||||
if (videoDuration > 0.0 && captureTime >= videoDuration - 0.1) {
|
||||
if (captureTime > videoDuration)
|
||||
captureTime = videoDuration;
|
||||
|
||||
thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds(1.0, 600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds(captureTime, 600) actualTime:nil error:&error];
|
||||
if (image == nil) {
|
||||
if (error != nil)
|
||||
NSLog(@"Error generating video thumbnail: %@", error);
|
||||
else
|
||||
NSLog(@"Error generating video thumbnail...");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
|
||||
CGImageRelease(image);
|
||||
|
||||
if (![UIImagePNGRepresentation(thumbnail) writeToFile:savePath atomically:YES]) {
|
||||
NSLog(@"Error saving thumbnail image");
|
||||
return "";
|
||||
}
|
||||
|
||||
return [self getCString:savePath];
|
||||
}
|
||||
|
||||
+ (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize {
|
||||
CGFloat width = image.size.width;
|
||||
CGFloat height = image.size.height;
|
||||
|
||||
UIImageOrientation orientation = image.imageOrientation;
|
||||
if (width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown &&
|
||||
orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight &&
|
||||
orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored &&
|
||||
orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored)
|
||||
return image;
|
||||
|
||||
CGFloat scaleX = 1.0f;
|
||||
CGFloat scaleY = 1.0f;
|
||||
if (width > maxSize)
|
||||
scaleX = maxSize / width;
|
||||
if (height > maxSize)
|
||||
scaleY = maxSize / height;
|
||||
|
||||
// Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m
|
||||
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(image.CGImage);
|
||||
BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast;
|
||||
|
||||
CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY;
|
||||
CGRect imageRect = CGRectMake(0, 0, width * scaleRatio, height * scaleRatio);
|
||||
UIGraphicsBeginImageContextWithOptions(imageRect.size, !hasAlpha, image.scale);
|
||||
[image drawInRect:imageRect];
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize {
|
||||
// Check if the image can be loaded by Unity without requiring a conversion to PNG
|
||||
// Credit: https://stackoverflow.com/a/12048937/2373034
|
||||
NSString *extension = [path pathExtension];
|
||||
BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame;
|
||||
|
||||
if (!conversionNeeded) {
|
||||
// Check if the image needs to be processed at all
|
||||
NSArray *metadata = [self getImageMetadata:path];
|
||||
int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range
|
||||
if (orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize)
|
||||
return [self getCString:path];
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithContentsOfFile:path];
|
||||
if (image == nil)
|
||||
return [self getCString:path];
|
||||
|
||||
UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize];
|
||||
if (conversionNeeded || scaledImage != image) {
|
||||
if (![UIImagePNGRepresentation(scaledImage) writeToFile:tempFilePath atomically:YES]) {
|
||||
NSLog(@"Error creating scaled image");
|
||||
return [self getCString:path];
|
||||
}
|
||||
|
||||
return [self getCString:tempFilePath];
|
||||
}
|
||||
else
|
||||
return [self getCString:path];
|
||||
}
|
||||
|
||||
+ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
|
||||
NSString *path = nil;
|
||||
if ([info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage]) { // took picture
|
||||
// Temporarily save image as PNG
|
||||
UIImage *image = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage];
|
||||
if (image == nil)
|
||||
path = nil;
|
||||
else {
|
||||
NSString *extension = [pickedMediaSavePath pathExtension];
|
||||
BOOL saveAsJPEG = [extension caseInsensitiveCompare:@"jpg"] == NSOrderedSame || [extension caseInsensitiveCompare:@"jpeg"] == NSOrderedSame;
|
||||
|
||||
// Try to save the image with metadata
|
||||
// CANCELED: a number of users reported that this method results in 90-degree rotated images, uncomment at your own risk
|
||||
// Credit: https://stackoverflow.com/a/15858955
|
||||
/*NSDictionary *metadata = [info objectForKey:UIImagePickerControllerMediaMetadata];
|
||||
NSMutableDictionary *mutableMetadata = nil;
|
||||
CFDictionaryRef metadataRef;
|
||||
CFStringRef imageType;
|
||||
|
||||
if (saveAsJPEG) {
|
||||
mutableMetadata = [metadata mutableCopy];
|
||||
[mutableMetadata setObject:@(1.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
|
||||
|
||||
metadataRef = (__bridge CFDictionaryRef)mutableMetadata;
|
||||
imageType = kUTTypeJPEG;
|
||||
}
|
||||
else {
|
||||
metadataRef = (__bridge CFDictionaryRef)metadata;
|
||||
imageType = kUTTypePNG;
|
||||
}
|
||||
|
||||
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:pickedMediaSavePath], imageType , 1, NULL);
|
||||
if (imageDestination == NULL )
|
||||
NSLog(@"Failed to create image destination");
|
||||
else {
|
||||
CGImageDestinationAddImage(imageDestination, image.CGImage, metadataRef);
|
||||
if (CGImageDestinationFinalize(imageDestination))
|
||||
path = pickedMediaSavePath;
|
||||
else
|
||||
NSLog(@"Failed to finalize the image");
|
||||
|
||||
CFRelease(imageDestination);
|
||||
}*/
|
||||
|
||||
if (path == nil) {
|
||||
//NSLog(@"Attempting to save the image without metadata as fallback");
|
||||
|
||||
if ((saveAsJPEG && [UIImageJPEGRepresentation([self scaleImage:image maxSize:cameraMaxImageSize], 1.0) writeToFile:pickedMediaSavePath atomically:YES]) ||
|
||||
(!saveAsJPEG && [UIImagePNGRepresentation([self scaleImage:image maxSize:cameraMaxImageSize]) writeToFile:pickedMediaSavePath atomically:YES]) )
|
||||
path = pickedMediaSavePath;
|
||||
else {
|
||||
NSLog(@"Error saving image without metadata");
|
||||
path = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else { // recorded video
|
||||
NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
|
||||
if (mediaUrl == nil)
|
||||
path = nil;
|
||||
else
|
||||
path = [mediaUrl path];
|
||||
}
|
||||
|
||||
imagePicker = nil;
|
||||
imagePickerState = 2;
|
||||
UnitySendMessage("NCCameraCallbackiOS", "OnMediaReceived", [self getCString:path]);
|
||||
|
||||
[picker dismissViewControllerAnimated:NO completion:nil];
|
||||
}
|
||||
|
||||
+ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
|
||||
{
|
||||
imagePicker = nil;
|
||||
UnitySendMessage("NCCameraCallbackiOS", "OnMediaReceived", "");
|
||||
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/37052118/2373034
|
||||
+ (char *)getCString:(NSString *)source {
|
||||
if (source == nil)
|
||||
source = @"";
|
||||
|
||||
const char *sourceUTF8 = [source UTF8String];
|
||||
char *result = (char*) malloc(strlen(sourceUTF8) + 1);
|
||||
strcpy(result, sourceUTF8);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
extern "C" int _NativeCamera_CheckPermission() {
|
||||
return [UNativeCamera checkPermission];
|
||||
}
|
||||
|
||||
extern "C" int _NativeCamera_RequestPermission() {
|
||||
return [UNativeCamera requestPermission];
|
||||
}
|
||||
|
||||
extern "C" int _NativeCamera_CanOpenSettings() {
|
||||
return [UNativeCamera canOpenSettings];
|
||||
}
|
||||
|
||||
extern "C" void _NativeCamera_OpenSettings() {
|
||||
[UNativeCamera openSettings];
|
||||
}
|
||||
|
||||
extern "C" int _NativeCamera_HasCamera() {
|
||||
return [UNativeCamera hasCamera];
|
||||
}
|
||||
|
||||
extern "C" void _NativeCamera_TakePicture(const char* imageSavePath, int maxSize, int preferredCamera) {
|
||||
[UNativeCamera openCamera:YES defaultCamera:preferredCamera savePath:[NSString stringWithUTF8String:imageSavePath] maxImageSize:maxSize videoQuality:-1 maxVideoDuration:-1];
|
||||
}
|
||||
|
||||
extern "C" void _NativeCamera_RecordVideo(int quality, int maxDuration, int preferredCamera) {
|
||||
[UNativeCamera openCamera:NO defaultCamera:preferredCamera savePath:nil maxImageSize:4096 videoQuality:quality maxVideoDuration:maxDuration];
|
||||
}
|
||||
|
||||
extern "C" int _NativeCamera_IsCameraBusy() {
|
||||
return [UNativeCamera isCameraBusy];
|
||||
}
|
||||
|
||||
extern "C" char* _NativeCamera_GetImageProperties(const char* path) {
|
||||
return [UNativeCamera getImageProperties:[NSString stringWithUTF8String:path]];
|
||||
}
|
||||
|
||||
extern "C" char* _NativeCamera_GetVideoProperties(const char* path) {
|
||||
return [UNativeCamera getVideoProperties:[NSString stringWithUTF8String:path]];
|
||||
}
|
||||
|
||||
extern "C" char* _NativeCamera_GetVideoThumbnail(const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds) {
|
||||
return [UNativeCamera getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];
|
||||
}
|
||||
|
||||
extern "C" char* _NativeCamera_LoadImageAtPath(const char* path, const char* temporaryFilePath, int maxSize) {
|
||||
return [UNativeCamera loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];
|
||||
}
|
||||
33
Assets/3rd/NativeCamera/Plugins/iOS/NativeCamera.mm.meta
Normal file
33
Assets/3rd/NativeCamera/Plugins/iOS/NativeCamera.mm.meta
Normal file
@@ -0,0 +1,33 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f71ce2f3d3a5dbd46af575e628ed9d6e
|
||||
timeCreated: 1498722774
|
||||
licenseType: Pro
|
||||
PluginImporter:
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
platformData:
|
||||
data:
|
||||
first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
data:
|
||||
first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
data:
|
||||
first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user