Browse Source

修改 RTC 执行和显示逻辑、修复上传图片和视频Bug

DGJ 1 year ago
parent
commit
5d81c58670
46 changed files with 3916 additions and 34 deletions
  1. 16 3
      Assets/Patch/AgoraVideoAudioManager.cs
  2. BIN
      Assets/Plugins/Android/mylibrary-debug.aar
  3. 32 0
      Assets/Plugins/Android/mylibrary-debug.aar.meta
  4. 9 0
      Assets/Plugins/NativeGallery.meta
  5. 9 0
      Assets/Plugins/NativeGallery/Android.meta
  6. 31 0
      Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs
  7. 12 0
      Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta
  8. 92 0
      Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs
  9. 12 0
      Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta
  10. 29 0
      Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs
  11. 12 0
      Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta
  12. BIN
      Assets/Plugins/NativeGallery/Android/NativeGallery.aar
  13. 33 0
      Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta
  14. 9 0
      Assets/Plugins/NativeGallery/Editor.meta
  15. 149 0
      Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs
  16. 12 0
      Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta
  17. 15 0
      Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef
  18. 7 0
      Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta
  19. 3 0
      Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef
  20. 7 0
      Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta
  21. 1051 0
      Assets/Plugins/NativeGallery/NativeGallery.cs
  22. 12 0
      Assets/Plugins/NativeGallery/NativeGallery.cs.meta
  23. 147 0
      Assets/Plugins/NativeGallery/README.txt
  24. 8 0
      Assets/Plugins/NativeGallery/README.txt.meta
  25. 9 0
      Assets/Plugins/NativeGallery/iOS.meta
  26. 130 0
      Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs
  27. 12 0
      Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta
  28. 43 0
      Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs
  29. 12 0
      Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta
  30. 1540 0
      Assets/Plugins/NativeGallery/iOS/NativeGallery.mm
  31. 33 0
      Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta
  32. 61 0
      Assets/Remote3.1/Scripts/ImagePicker.cs
  33. 11 0
      Assets/Remote3.1/Scripts/ImagePicker.cs.meta
  34. 19 0
      Assets/Remote3.1/Scripts/ImagePickerCallback.cs
  35. 11 0
      Assets/Remote3.1/Scripts/ImagePickerCallback.cs.meta
  36. 1 1
      Assets/Remote3.1/Scripts/LoginModule/LoginForms.cs
  37. 13 0
      Assets/Remote3.1/Scripts/MyObjectReceive.cs
  38. 11 0
      Assets/Remote3.1/Scripts/MyObjectReceive.cs.meta
  39. 1 1
      Assets/Remote3.1/Scripts/RoomScripts/JieTuScripts/jietu.cs
  40. 43 18
      Assets/Remote3.1/Scripts/RoomScripts/RoomMainForms.cs
  41. 45 10
      Assets/Resources/UIPrefabs/RoomMainForms.prefab
  42. 153 0
      Assets/Scenes/Remote3.1Phone.unity
  43. 1 1
      Assets/StreamingAssets/build_info
  44. 24 0
      NativeGallery.Editor.csproj
  45. 24 0
      NativeGallery.Runtime.csproj
  46. 12 0
      remote3.1phone-master.sln

+ 16 - 3
Assets/Patch/AgoraVideoAudioManager.cs

@@ -58,6 +58,8 @@ public class AgoraVideoAudioManager : SingletonMono<AgoraVideoAudioManager>
 
     private bool isOneShow;
     // Use this for initialization
+
+    public bool isVideoStream;
     private void Start()
     {
         LoadAssetData();
@@ -195,6 +197,16 @@ public class AgoraVideoAudioManager : SingletonMono<AgoraVideoAudioManager>
         //RtcEngine.SetCameraDeviceOrientation(VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY, VIDEO_ORIENTATION.VIDEO_ORIENTATION_90);
         //RtcEngine.SetCameraDeviceOrientation(VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_SECONDARY, VIDEO_ORIENTATION.VIDEO_ORIENTATION_90);
         RtcEngine.SetLocalVideoMirrorMode(VIDEO_MIRROR_MODE_TYPE.VIDEO_MIRROR_MODE_DISABLED);
+        StartCoroutine(SetSystem(0.3f));
+    }
+
+    IEnumerator SetSystem(float times)
+    {
+        yield return new WaitForSeconds(times);
+
+        EnableLocalVideo(CustomInfo.isSendVideo);
+        EnableLoacalAudio(CustomInfo.isSendAudio);
+
     }
 
     public void OpenAgoraAudio()
@@ -286,7 +298,8 @@ public class AgoraVideoAudioManager : SingletonMono<AgoraVideoAudioManager>
     {
         if (peerid == mainViewPeerId)
         {
-            if(isOpen)
+            isVideoStream = isOpen;
+            if (isOpen)
             {
                 AddListShowView(mainViewPeerId, RoomMainForms.Instance.bigView);
             }
@@ -333,7 +346,7 @@ public class AgoraVideoAudioManager : SingletonMono<AgoraVideoAudioManager>
                     Debug.Log(listCustomPeer[i].isVideo + "   " + peerId);
                     rawImage.rectTransform.localEulerAngles = new Vector3(0, 180, 180);
                     MakeVideoView(dicPeeridAndUid[peerId], rawImage, this._channelName);
-                    
+                    isVideoStream = true;
                 }
                 else
                 {
@@ -450,7 +463,7 @@ public class AgoraVideoAudioManager : SingletonMono<AgoraVideoAudioManager>
     {
         int msg = RtcEngine.MuteLocalVideoStream(!isVideo);
         //RtcEngine.EnableVideo();
-      //  int msg = RtcEngine.EnableLocalVideo(isVideo);
+       // int msg = RtcEngine.EnableLocalVideo(isVideo);
         switch (msg)
         {
             case 0:

BIN
Assets/Plugins/Android/mylibrary-debug.aar


+ 32 - 0
Assets/Plugins/Android/mylibrary-debug.aar.meta

@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 4f271deb37b36bd4d818401443bac9cd
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Android: Android
+    second:
+      enabled: 1
+      settings: {}
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/Plugins/NativeGallery.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 5e05ed2bddbccb94e9650efb5742e452
+folderAsset: yes
+timeCreated: 1518877529
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/Plugins/NativeGallery/Android.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 0a607dcda26e7614f86300c6ca717295
+folderAsset: yes
+timeCreated: 1498722617
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 31 - 0
Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs

@@ -0,0 +1,31 @@
+#if UNITY_EDITOR || UNITY_ANDROID
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGCallbackHelper : 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

+ 12 - 0
Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2d517fd0f2f85f24698df2775bee58e9
+timeCreated: 1544889149
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 92 - 0
Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs

@@ -0,0 +1,92 @@
+#if UNITY_EDITOR || UNITY_ANDROID
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGMediaReceiveCallbackAndroid : AndroidJavaProxy
+	{
+		private readonly NativeGallery.MediaPickCallback callback;
+		private readonly NativeGallery.MediaPickMultipleCallback callbackMultiple;
+
+		private readonly NGCallbackHelper callbackHelper;
+
+		public NGMediaReceiveCallbackAndroid( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) : base( "com.yasirkula.unity.NativeGalleryMediaReceiver" )
+		{
+			this.callback = callback;
+			this.callbackMultiple = callbackMultiple;
+			callbackHelper = new GameObject( "NGCallbackHelper" ).AddComponent<NGCallbackHelper>();
+		}
+
+		public void OnMediaReceived( string path )
+		{
+			callbackHelper.CallOnMainThread( () => MediaReceiveCallback( path ) );
+		}
+
+		public void OnMultipleMediaReceived( string paths )
+		{
+			string[] result = null;
+			if( !string.IsNullOrEmpty( paths ) )
+			{
+				string[] pathsSplit = paths.Split( '>' );
+
+				int validPathCount = 0;
+				for( int i = 0; i < pathsSplit.Length; i++ )
+				{
+					if( !string.IsNullOrEmpty( pathsSplit[i] ) )
+						validPathCount++;
+				}
+
+				if( validPathCount == 0 )
+					pathsSplit = new string[0];
+				else if( validPathCount != pathsSplit.Length )
+				{
+					string[] validPaths = new string[validPathCount];
+					for( int i = 0, j = 0; i < pathsSplit.Length; i++ )
+					{
+						if( !string.IsNullOrEmpty( pathsSplit[i] ) )
+							validPaths[j++] = pathsSplit[i];
+					}
+
+					pathsSplit = validPaths;
+				}
+
+				result = pathsSplit;
+			}
+
+			callbackHelper.CallOnMainThread( () => MediaReceiveMultipleCallback( result ) );
+		}
+
+		private void MediaReceiveCallback( string path )
+		{
+			if( string.IsNullOrEmpty( path ) )
+				path = null;
+
+			try
+			{
+				if( callback != null )
+					callback( path );
+			}
+			finally
+			{
+				Object.Destroy( callbackHelper.gameObject );
+			}
+		}
+
+		private void MediaReceiveMultipleCallback( string[] paths )
+		{
+			if( paths != null && paths.Length == 0 )
+				paths = null;
+
+			try
+			{
+				if( callbackMultiple != null )
+					callbackMultiple( paths );
+			}
+			finally
+			{
+				Object.Destroy( callbackHelper.gameObject );
+			}
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4c18d702b07a63945968db47201b95c9
+timeCreated: 1519060539
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 29 - 0
Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs

@@ -0,0 +1,29 @@
+#if UNITY_EDITOR || UNITY_ANDROID
+using System.Threading;
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGPermissionCallbackAndroid : AndroidJavaProxy
+	{
+		private object threadLock;
+		public int Result { get; private set; }
+
+		public NGPermissionCallbackAndroid( object threadLock ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" )
+		{
+			Result = -1;
+			this.threadLock = threadLock;
+		}
+
+		public void OnPermissionResult( int result )
+		{
+			Result = result;
+
+			lock( threadLock )
+			{
+				Monitor.Pulse( threadLock );
+			}
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: a07afac614af1294d8e72a3c083be028
+timeCreated: 1519060539
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/Plugins/NativeGallery/Android/NativeGallery.aar


+ 33 - 0
Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: db4d55e1212537e4baa84cac66eb6645
+timeCreated: 1569764737
+licenseType: Store
+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 - 0
Assets/Plugins/NativeGallery/Editor.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 19fc6b8ce781591438a952d8aa9104f8
+folderAsset: yes
+timeCreated: 1521452097
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 149 - 0
Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs

@@ -0,0 +1,149 @@
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+#if UNITY_IOS
+using UnityEditor.Callbacks;
+using UnityEditor.iOS.Xcode;
+#endif
+
+namespace NativeGalleryNamespace
+{
+	[System.Serializable]
+	public class Settings
+	{
+		private const string SAVE_PATH = "ProjectSettings/NativeGallery.json";
+
+		public bool AutomatedSetup = true;
+#if !UNITY_2018_1_OR_NEWER
+		public bool MinimumiOSTarget8OrAbove = false;
+#endif
+		public string PhotoLibraryUsageDescription = "The app requires access to Photos to interact with it.";
+		public string PhotoLibraryAdditionsUsageDescription = "The app requires access to Photos to save media to it.";
+		public bool DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = true; // See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/
+
+		private static Settings m_instance = null;
+		public static Settings Instance
+		{
+			get
+			{
+				if( m_instance == null )
+				{
+					try
+					{
+						if( File.Exists( SAVE_PATH ) )
+							m_instance = JsonUtility.FromJson<Settings>( File.ReadAllText( SAVE_PATH ) );
+						else
+							m_instance = new Settings();
+					}
+					catch( System.Exception e )
+					{
+						Debug.LogException( e );
+						m_instance = new Settings();
+					}
+				}
+
+				return m_instance;
+			}
+		}
+
+		public void Save()
+		{
+			File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) );
+		}
+
+#if UNITY_2018_3_OR_NEWER
+		[SettingsProvider]
+		public static SettingsProvider CreatePreferencesGUI()
+		{
+			return new SettingsProvider( "Project/yasirkula/Native Gallery", SettingsScope.Project )
+			{
+				guiHandler = ( searchContext ) => PreferencesGUI(),
+				keywords = new System.Collections.Generic.HashSet<string>() { "Native", "Gallery", "Android", "iOS" }
+			};
+		}
+#endif
+
+#if !UNITY_2018_3_OR_NEWER
+		[PreferenceItem( "Native Gallery" )]
+#endif
+		public static void PreferencesGUI()
+		{
+			EditorGUI.BeginChangeCheck();
+
+			Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup );
+
+			EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup );
+#if !UNITY_2018_1_OR_NEWER
+			Instance.MinimumiOSTarget8OrAbove = EditorGUILayout.Toggle( "Deployment Target Is 8.0 Or Above", Instance.MinimumiOSTarget8OrAbove );
+#endif
+			Instance.PhotoLibraryUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Usage Description", Instance.PhotoLibraryUsageDescription );
+			Instance.PhotoLibraryAdditionsUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Additions Usage Description", Instance.PhotoLibraryAdditionsUsageDescription );
+			Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = EditorGUILayout.Toggle( new GUIContent( "Don't Ask Limited Photos Permission Automatically", "See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/. It's recommended to keep this setting enabled" ), Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 );
+			EditorGUI.EndDisabledGroup();
+
+			if( EditorGUI.EndChangeCheck() )
+				Instance.Save();
+		}
+	}
+
+	public class NGPostProcessBuild
+	{
+#if UNITY_IOS
+		[PostProcessBuild( 1 )]
+		public static void OnPostprocessBuild( BuildTarget target, string buildPath )
+		{
+			if( !Settings.Instance.AutomatedSetup )
+				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
+
+				// Minimum supported iOS version on Unity 2018.1 and later is 8.0
+#if !UNITY_2018_1_OR_NEWER
+				if( !Settings.Instance.MinimumiOSTarget8OrAbove )
+				{
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework Photos" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework PhotosUI" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework AssetsLibrary" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" );
+				}
+				else
+#endif
+				{
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework PhotosUI" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework Photos" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" );
+					pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" );
+				}
+
+				pbxProject.RemoveFrameworkFromProject( targetGUID, "Photos.framework" );
+
+				File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
+
+				PlistDocument plist = new PlistDocument();
+				plist.ReadFromString( File.ReadAllText( plistPath ) );
+
+				PlistElementDict rootDict = plist.root;
+				rootDict.SetString( "NSPhotoLibraryUsageDescription", Settings.Instance.PhotoLibraryUsageDescription );
+				rootDict.SetString( "NSPhotoLibraryAddUsageDescription", Settings.Instance.PhotoLibraryAdditionsUsageDescription );
+				if( Settings.Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 )
+					rootDict.SetBoolean( "PHPhotoLibraryPreventAutomaticLimitedAccessAlert", true );
+
+				File.WriteAllText( plistPath, plist.WriteToString() );
+			}
+		}
+#endif
+	}
+}

+ 12 - 0
Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: dff1540cf22bfb749a2422f445cf9427
+timeCreated: 1521452119
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 15 - 0
Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef

@@ -0,0 +1,15 @@
+{
+    "name": "NativeGallery.Editor",
+    "references": [],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3dffc8e654f00c545a82d0a5274d51eb
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 3 - 0
Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef

@@ -0,0 +1,3 @@
+{
+	"name": "NativeGallery.Runtime"
+}

+ 7 - 0
Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6e5063adab271564ba0098a06a8cebda
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1051 - 0
Assets/Plugins/NativeGallery/NativeGallery.cs

@@ -0,0 +1,1051 @@
+using System;
+using System.Globalization;
+using System.IO;
+using UnityEngine;
+#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS
+using System.Threading.Tasks;
+using Unity.Collections;
+using UnityEngine.Networking;
+#endif
+#if UNITY_ANDROID || UNITY_IOS
+using NativeGalleryNamespace;
+#endif
+using Object = UnityEngine.Object;
+
+public static class NativeGallery
+{
+	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 PermissionType { Read = 0, Write = 1 };
+	public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
+
+	[Flags]
+	public enum MediaType { Image = 1, Video = 2, Audio = 4 };
+
+	// 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 MediaSaveCallback( bool success, string path );
+	public delegate void MediaPickCallback( string path );
+	public delegate void MediaPickMultipleCallback( string[] paths );
+
+	#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.NativeGallery" );
+
+			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 _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern void _NativeGallery_ShowLimitedLibraryPicker();
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern int _NativeGallery_CanOpenSettings();
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern void _NativeGallery_OpenSettings();
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern int _NativeGallery_CanPickMultipleMedia();
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern int _NativeGallery_GetMediaTypeFromExtension( string extension );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern void _NativeGallery_ImageWriteToAlbum( string path, string album, int permissionFreeMode );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album, int permissionFreeMode );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern void _NativeGallery_PickMedia( string mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern string _NativeGallery_GetImageProperties( string path );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern string _NativeGallery_GetVideoProperties( string path );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern string _NativeGallery_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds );
+
+	[System.Runtime.InteropServices.DllImport( "__Internal" )]
+	private static extern string _NativeGallery_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;
+		}
+	}
+
+	private static string m_selectedMediaPath = null;
+	private static string SelectedMediaPath
+	{
+		get
+		{
+			if( m_selectedMediaPath == null )
+			{
+				m_selectedMediaPath = Path.Combine( Application.temporaryCachePath, "pickedMedia" );
+				Directory.CreateDirectory( Application.temporaryCachePath );
+			}
+
+			return m_selectedMediaPath;
+		}
+	}
+#endif
+	#endregion
+
+	#region Runtime Permissions
+	// PermissionFreeMode was initially planned to be a toggleable setting on iOS but it has its own issues when set to false, so its value is forced to true.
+	// These issues are:
+	// - Presented permission dialog will have a "Select Photos" option on iOS 14+ but clicking it will freeze and eventually crash the app (I'm guessing that
+	//   this is caused by how permissions are handled synchronously in NativeGallery)
+	// - While saving images/videos to Photos, iOS 14+ users would see the "Select Photos" option (which is irrelevant in this context, hence confusing) and
+	//   the user must grant full Photos access in order to save the image/video to a custom album
+	// The only downside of having PermissionFreeMode = true is that, on iOS 14+, images/videos will be saved to the default Photos album rather than the
+	// provided custom album
+	private const bool PermissionFreeMode = true;
+
+	public static Permission CheckPermission( PermissionType permissionType, MediaType mediaTypes )
+	{
+#if !UNITY_EDITOR && UNITY_ANDROID
+		Permission result = (Permission) AJC.CallStatic<int>( "CheckPermission", Context, permissionType == PermissionType.Read, (int) mediaTypes );
+		if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk )
+			result = Permission.ShouldAsk;
+
+		return result;
+#elif !UNITY_EDITOR && UNITY_IOS
+		// result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true
+		int result = _NativeGallery_CheckPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 );
+		return result == 3 ? Permission.Granted : (Permission) result;
+#else
+		return Permission.Granted;
+#endif
+	}
+
+	public static Permission RequestPermission( PermissionType permissionType, MediaType mediaTypes )
+	{
+#if !UNITY_EDITOR && UNITY_ANDROID
+		object threadLock = new object();
+		lock( threadLock )
+		{
+			NGPermissionCallbackAndroid nativeCallback = new NGPermissionCallbackAndroid( threadLock );
+
+			AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) );
+
+			if( nativeCallback.Result == -1 )
+				System.Threading.Monitor.Wait( threadLock );
+
+			if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "NativeGalleryPermission", -1 ) != nativeCallback.Result )
+			{
+				PlayerPrefs.SetInt( "NativeGalleryPermission", nativeCallback.Result );
+				PlayerPrefs.Save();
+			}
+
+			return (Permission) nativeCallback.Result;
+		}
+#elif !UNITY_EDITOR && UNITY_IOS
+		// result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true
+		int result = _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 );
+		return result == 3 ? Permission.Granted : (Permission) result;
+#else
+		return Permission.Granted;
+#endif
+	}
+
+	// This function isn't needed when PermissionFreeMode is set to true
+	private static void TryExtendLimitedAccessPermission()
+	{
+		if( IsMediaPickerBusy() )
+			return;
+
+#if !UNITY_EDITOR && UNITY_IOS
+		_NativeGallery_ShowLimitedLibraryPicker();
+#endif
+	}
+
+	public static bool CanOpenSettings()
+	{
+#if !UNITY_EDITOR && UNITY_IOS
+		return _NativeGallery_CanOpenSettings() == 1;
+#else
+		return true;
+#endif
+	}
+
+	public static void OpenSettings()
+	{
+#if !UNITY_EDITOR && UNITY_ANDROID
+		AJC.CallStatic( "OpenSettings", Context );
+#elif !UNITY_EDITOR && UNITY_IOS
+		_NativeGallery_OpenSettings();
+#endif
+	}
+	#endregion
+
+	#region Save Functions
+	public static Permission SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
+	{
+		return SaveToGallery( mediaBytes, album, filename, MediaType.Image, callback );
+	}
+
+	public static Permission SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
+	{
+		return SaveToGallery( existingMediaPath, album, filename, MediaType.Image, callback );
+	}
+
+	public static Permission SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null )
+	{
+		if( image == null )
+			throw new ArgumentException( "Parameter 'image' is null!" );
+
+		if( filename.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) || filename.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) )
+			return SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback );
+		else if( filename.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) )
+			return SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback );
+		else
+			return SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback );
+	}
+
+	public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
+	{
+		return SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback );
+	}
+
+	public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
+	{
+		return SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback );
+	}
+
+	private static Permission SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
+	{
+		return SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback );
+	}
+
+	private static Permission SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
+	{
+		return SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback );
+	}
+	#endregion
+
+	#region Load Functions
+	public static bool CanSelectMultipleFilesFromGallery()
+	{
+#if !UNITY_EDITOR && UNITY_ANDROID
+		return AJC.CallStatic<bool>( "CanSelectMultipleMedia" );
+#elif !UNITY_EDITOR && UNITY_IOS
+		return _NativeGallery_CanPickMultipleMedia() == 1;
+#else
+		return false;
+#endif
+	}
+
+	public static bool CanSelectMultipleMediaTypesFromGallery()
+	{
+#if UNITY_EDITOR
+		return true;
+#elif UNITY_ANDROID
+		return AJC.CallStatic<bool>( "CanSelectMultipleMediaTypes" );
+#elif UNITY_IOS
+		return true;
+#else
+		return false;
+#endif
+	}
+
+	public static Permission GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" )
+	{
+		return GetMediaFromGallery( callback, MediaType.Image, mime, title );
+	}
+
+	public static Permission GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" )
+	{
+		return GetMediaFromGallery( callback, MediaType.Video, mime, title );
+	}
+
+	public static Permission GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" )
+	{
+		return GetMediaFromGallery( callback, MediaType.Audio, mime, title );
+	}
+
+	public static Permission GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" )
+	{
+		return GetMediaFromGallery( callback, mediaTypes, "*/*", title );
+	}
+
+	public static Permission GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" )
+	{
+		return GetMultipleMediaFromGallery( callback, MediaType.Image, mime, title );
+	}
+
+	public static Permission GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" )
+	{
+		return GetMultipleMediaFromGallery( callback, MediaType.Video, mime, title );
+	}
+
+	public static Permission GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" )
+	{
+		return GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title );
+	}
+
+	public static Permission GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" )
+	{
+		return GetMultipleMediaFromGallery( callback, mediaTypes, "*/*", title );
+	}
+
+	public static bool IsMediaPickerBusy()
+	{
+#if !UNITY_EDITOR && UNITY_IOS
+		return NGMediaReceiveCallbackiOS.IsBusy;
+#else
+		return false;
+#endif
+	}
+
+	public static MediaType GetMediaTypeOfFile( string path )
+	{
+		if( string.IsNullOrEmpty( path ) )
+			return (MediaType) 0;
+
+		string extension = Path.GetExtension( path );
+		if( string.IsNullOrEmpty( extension ) )
+			return (MediaType) 0;
+
+		if( extension[0] == '.' )
+		{
+			if( extension.Length == 1 )
+				return (MediaType) 0;
+
+			extension = extension.Substring( 1 );
+		}
+
+#if UNITY_EDITOR
+		extension = extension.ToLowerInvariant();
+		if( extension == "png" || extension == "jpg" || extension == "jpeg" || extension == "gif" || extension == "bmp" || extension == "tiff" )
+			return MediaType.Image;
+		else if( extension == "mp4" || extension == "mov" || extension == "wav" || extension == "avi" )
+			return MediaType.Video;
+		else if( extension == "mp3" || extension == "aac" || extension == "flac" )
+			return MediaType.Audio;
+
+		return (MediaType) 0;
+#elif UNITY_ANDROID
+		string mime = AJC.CallStatic<string>( "GetMimeTypeFromExtension", extension.ToLowerInvariant() );
+		if( string.IsNullOrEmpty( mime ) )
+			return (MediaType) 0;
+		else if( mime.StartsWith( "image/" ) )
+			return MediaType.Image;
+		else if( mime.StartsWith( "video/" ) )
+			return MediaType.Video;
+		else if( mime.StartsWith( "audio/" ) )
+			return MediaType.Audio;
+		else
+			return (MediaType) 0;
+#elif UNITY_IOS
+		return (MediaType) _NativeGallery_GetMediaTypeFromExtension( extension.ToLowerInvariant() );
+#else
+		return (MediaType) 0;
+#endif
+	}
+	#endregion
+
+	#region Internal Functions
+	private static Permission SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
+	{
+		Permission result = RequestPermission( PermissionType.Write, mediaType );
+		if( result == Permission.Granted )
+		{
+			if( mediaBytes == null || mediaBytes.Length == 0 )
+				throw new ArgumentException( "Parameter 'mediaBytes' is null or empty!" );
+
+			if( album == null || album.Length == 0 )
+				throw new ArgumentException( "Parameter 'album' is null or empty!" );
+
+			if( filename == null || filename.Length == 0 )
+				throw new ArgumentException( "Parameter 'filename' is null or empty!" );
+
+			if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )
+				Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );
+
+			string path = GetTemporarySavePath( filename );
+#if UNITY_EDITOR
+			Debug.Log( "SaveToGallery called successfully in the Editor" );
+#else
+			File.WriteAllBytes( path, mediaBytes );
+#endif
+
+			SaveToGalleryInternal( path, album, mediaType, callback );
+		}
+
+		return result;
+	}
+
+	private static Permission SaveToGallery( string existingMediaPath, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
+	{
+		Permission result = RequestPermission( PermissionType.Write, mediaType );
+		if( result == Permission.Granted )
+		{
+			if( !File.Exists( existingMediaPath ) )
+				throw new FileNotFoundException( "File not found at " + existingMediaPath );
+
+			if( album == null || album.Length == 0 )
+				throw new ArgumentException( "Parameter 'album' is null or empty!" );
+
+			if( filename == null || filename.Length == 0 )
+				throw new ArgumentException( "Parameter 'filename' is null or empty!" );
+
+			if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )
+			{
+				string originalExtension = Path.GetExtension( existingMediaPath );
+				if( string.IsNullOrEmpty( originalExtension ) )
+					Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );
+				else
+					filename += originalExtension;
+			}
+
+			string path = GetTemporarySavePath( filename );
+#if UNITY_EDITOR
+			Debug.Log( "SaveToGallery called successfully in the Editor" );
+#else
+			File.Copy( existingMediaPath, path, true );
+#endif
+
+			SaveToGalleryInternal( path, album, mediaType, callback );
+		}
+
+		return result;
+	}
+
+	private static void SaveToGalleryInternal( string path, string album, MediaType mediaType, MediaSaveCallback callback )
+	{
+#if !UNITY_EDITOR && UNITY_ANDROID
+		string savePath = AJC.CallStatic<string>( "SaveMedia", Context, (int) mediaType, path, album );
+
+		File.Delete( path );
+
+		if( callback != null )
+			callback( !string.IsNullOrEmpty( savePath ), savePath );
+#elif !UNITY_EDITOR && UNITY_IOS
+		if( mediaType == MediaType.Audio )
+		{
+			Debug.LogError( "Saving audio files is not supported on iOS" );
+
+			if( callback != null )
+				callback( false, null );
+
+			return;
+		}
+
+		Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) );
+
+		NGMediaSaveCallbackiOS.Initialize( callback );
+		if( mediaType == MediaType.Image )
+			_NativeGallery_ImageWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );
+		else if( mediaType == MediaType.Video )
+			_NativeGallery_VideoWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );
+#else
+		if( callback != null )
+			callback( true, null );
+#endif
+	}
+
+	private static string GetTemporarySavePath( string filename )
+	{
+		string saveDir = Path.Combine( Application.persistentDataPath, "NGallery" );
+		Directory.CreateDirectory( saveDir );
+
+#if !UNITY_EDITOR && UNITY_IOS
+		// Ensure a unique temporary filename on iOS:
+		// iOS internally copies images/videos to Photos directory of the system,
+		// but the process is async. The redundant file is deleted by objective-c code
+		// automatically after the media is saved but while it is being saved, the file
+		// should NOT be overwritten. Therefore, always ensure a unique filename on iOS
+		string path = Path.Combine( saveDir, filename );
+		if( File.Exists( path ) )
+		{
+			int fileIndex = 0;
+			string filenameWithoutExtension = Path.GetFileNameWithoutExtension( filename );
+			string extension = Path.GetExtension( filename );
+
+			do
+			{
+				path = Path.Combine( saveDir, string.Concat( filenameWithoutExtension, ++fileIndex, extension ) );
+			} while( File.Exists( path ) );
+		}
+
+		return path;
+#else
+		return Path.Combine( saveDir, filename );
+#endif
+	}
+
+	private static Permission GetMediaFromGallery( MediaPickCallback callback, MediaType mediaType, string mime, string title )
+	{
+		Permission result = RequestPermission( PermissionType.Read, mediaType );
+		if( result == Permission.Granted && !IsMediaPickerBusy() )
+		{
+#if UNITY_EDITOR
+			System.Collections.Generic.List<string> editorFilters = new System.Collections.Generic.List<string>( 4 );
+
+			if( ( mediaType & MediaType.Image ) == MediaType.Image )
+			{
+				editorFilters.Add( "Image files" );
+				editorFilters.Add( "png,jpg,jpeg" );
+			}
+
+			if( ( mediaType & MediaType.Video ) == MediaType.Video )
+			{
+				editorFilters.Add( "Video files" );
+				editorFilters.Add( "mp4,mov,wav,avi" );
+			}
+
+			if( ( mediaType & MediaType.Audio ) == MediaType.Audio )
+			{
+				editorFilters.Add( "Audio files" );
+				editorFilters.Add( "mp3,aac,flac" );
+			}
+
+			editorFilters.Add( "All files" );
+			editorFilters.Add( "*" );
+
+			string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() );
+
+			if( callback != null )
+				callback( pickedFile != "" ? pickedFile : null );
+#elif UNITY_ANDROID
+			AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title );
+#elif UNITY_IOS
+			if( mediaType == MediaType.Audio )
+			{
+				Debug.LogError( "Picking audio files is not supported on iOS" );
+
+				if( callback != null ) // Selecting audio files is not supported on iOS
+					callback( null );
+			}
+			else
+			{
+				NGMediaReceiveCallbackiOS.Initialize( callback, null );
+				_NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 );
+			}
+#else
+			if( callback != null )
+				callback( null );
+#endif
+		}
+
+		return result;
+	}
+
+	private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title )
+	{
+		Permission result = RequestPermission( PermissionType.Read, mediaType );
+		if( result == Permission.Granted && !IsMediaPickerBusy() )
+		{
+			if( CanSelectMultipleFilesFromGallery() )
+			{
+#if !UNITY_EDITOR && UNITY_ANDROID
+				AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title );
+#elif !UNITY_EDITOR && UNITY_IOS
+				if( mediaType == MediaType.Audio )
+				{
+					Debug.LogError( "Picking audio files is not supported on iOS" );
+
+					if( callback != null ) // Selecting audio files is not supported on iOS
+						callback( null );
+				}
+				else
+				{
+					NGMediaReceiveCallbackiOS.Initialize( null, callback );
+					_NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 );
+				}
+#else
+				if( callback != null )
+					callback( null );
+#endif
+			}
+			else if( callback != null )
+				callback( null );
+		}
+
+		return result;
+	}
+
+	private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg )
+	{
+		try
+		{
+			return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG();
+		}
+		catch( UnityException )
+		{
+			return GetTextureBytesFromCopy( texture, isJpeg );
+		}
+		catch( ArgumentException )
+		{
+			return GetTextureBytesFromCopy( texture, isJpeg );
+		}
+
+#pragma warning disable 0162
+		return null;
+#pragma warning restore 0162
+	}
+
+	private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg )
+	{
+		// Texture is marked as non-readable, create a readable copy and save it instead
+		Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" );
+
+		Texture2D sourceTexReadable = null;
+		RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height );
+		RenderTexture activeRT = RenderTexture.active;
+
+		try
+		{
+			Graphics.Blit( texture, rt );
+			RenderTexture.active = rt;
+
+			sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false );
+			sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false );
+			sourceTexReadable.Apply( false, false );
+		}
+		catch( Exception e )
+		{
+			Debug.LogException( e );
+
+			Object.DestroyImmediate( sourceTexReadable );
+			return null;
+		}
+		finally
+		{
+			RenderTexture.active = activeRT;
+			RenderTexture.ReleaseTemporary( rt );
+		}
+
+		try
+		{
+			return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG();
+		}
+		catch( Exception e )
+		{
+			Debug.LogException( e );
+			return null;
+		}
+		finally
+		{
+			Object.DestroyImmediate( sourceTexReadable );
+		}
+	}
+
+#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS
+	private static async Task<T> TryCallNativeAndroidFunctionOnSeparateThread<T>( Func<T> 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
+	#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 = _NativeGallery_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_GALLERY_DISABLE_ASYNC_FUNCTIONS
+	public static async Task<Texture2D> LoadImageAtPathAsync( 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 temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
+		string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) );
+#elif !UNITY_EDITOR && UNITY_IOS
+		string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
+		string loadPath = await Task.Run( () => _NativeGallery_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) );
+#else
+		string loadPath = imagePath;
+#endif
+
+		Texture2D result = null;
+
+		if( !linearColorSpace )
+		{
+			using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable && !generateMipmaps ) )
+			{
+				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
+				{
+					Texture2D texture = DownloadHandlerTexture.GetContent( www );
+
+					if( !generateMipmaps )
+						result = texture;
+					else
+					{
+						Texture2D mipmapTexture = null;
+						try
+						{
+							// Generate a Texture with mipmaps enabled
+							// Credits: https://forum.unity.com/threads/generate-mipmaps-at-runtime-for-a-texture-loaded-with-unitywebrequest.644842/#post-7571809
+							NativeArray<byte> textureData = texture.GetRawTextureData<byte>();
+
+							mipmapTexture = new Texture2D( texture.width, texture.height, texture.format, true );
+#if UNITY_2019_3_OR_NEWER
+							mipmapTexture.SetPixelData( textureData, 0 );
+#else
+							NativeArray<byte> mipmapTextureData = mipmapTexture.GetRawTextureData<byte>();
+							NativeArray<byte>.Copy( textureData, mipmapTextureData, textureData.Length );
+							mipmapTexture.LoadRawTextureData( mipmapTextureData );
+#endif
+							mipmapTexture.Apply( true, markTextureNonReadable );
+
+							result = mipmapTexture;
+						}
+						catch( Exception e )
+						{
+							Debug.LogException( e );
+
+							if( mipmapTexture )
+								Object.DestroyImmediate( mipmapTexture );
+						}
+						finally
+						{
+							Object.DestroyImmediate( texture );
+						}
+					}
+				}
+			}
+		}
+
+		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, 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;
+	}
+#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<string>( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );
+#elif !UNITY_EDITOR && UNITY_IOS
+		string thumbnailPath = _NativeGallery_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_GALLERY_DISABLE_ASYNC_FUNCTIONS
+	public static async Task<Texture2D> GetVideoThumbnailAsync( 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 temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
+		string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "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( () => _NativeGallery_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) );
+#else
+		string thumbnailPath = null;
+#endif
+
+		if( !string.IsNullOrEmpty( thumbnailPath ) )
+			return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );
+		else
+			return null;
+	}
+#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<string>( "GetImageProperties", Context, imagePath );
+#elif !UNITY_EDITOR && UNITY_IOS
+		string value = _NativeGallery_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 = _NativeGallery_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
+}

+ 12 - 0
Assets/Plugins/NativeGallery/NativeGallery.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: ce1403606c3629046a0147d3e705f7cc
+timeCreated: 1498722610
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 147 - 0
Assets/Plugins/NativeGallery/README.txt

@@ -0,0 +1,147 @@
+= Native Gallery for Android & iOS (v1.7.2) =
+
+Online documentation & example code available at: https://github.com/yasirkula/UnityNativeGallery
+E-mail: yasirkula@gmail.com
+
+
+1. ABOUT
+This plugin helps you interact with Gallery/Photos on Android & iOS.
+
+
+2. HOW TO
+for Android: set "Write Permission" to "External (SDCard)" in Player Settings
+for iOS: there are two ways to set up the plugin on iOS:
+
+a. Automated Setup for iOS
+- (optional) change the values of 'Photo Library Usage Description' and 'Photo Library Additions Usage Description' at 'Project Settings/yasirkula/Native Gallery'
+- (Unity 2017.4 or earlier) if your minimum Deployment Target (iOS Version) is at least 8.0, set the value of 'Deployment Target Is 8.0 Or Above' to true at 'Project Settings/yasirkula/Native Gallery'
+
+b. Manual Setup for iOS
+- set the value of 'Automated Setup' to false at 'Project Settings/yasirkula/Native Gallery'
+- build your project
+- enter a Photo Library Usage Description to Info.plist in Xcode
+- also enter a "Photo Library Additions Usage Description" to Info.plist in Xcode, if exists
+- set Info.plist's "Prevent limited photos access alert" property's value to 1 in Xcode, if exists
+- insert "-weak_framework PhotosUI -weak_framework Photos -framework AssetsLibrary -framework MobileCoreServices -framework ImageIO" to the "Other Linker Flags" of Unity-iPhone Target (and UnityFramework Target on Unity 2019.3 or newer) (if your Deployment Target is at least 8.0, it is sufficient to insert "-weak_framework PhotosUI -framework Photos -framework MobileCoreServices -framework ImageIO")
+- lastly, remove Photos.framework and PhotosUI.framework from Link Binary With Libraries of Unity-iPhone Target (and UnityFramework Target on Unity 2019.3 or newer) in Build Phases, if exists
+
+IMPORTANT: If you are targeting iOS 14 or later, you need to build your app with Xcode 12 or later to avoid any permission issues.
+
+
+3. FAQ
+- How can I fetch the path of the saved image or the original path of the picked image on iOS?
+You can't. On iOS, these files are stored in an internal directory that we have no access to (I don't think there is even a way to fetch that internal path).
+
+- Android build fails, it says "error: attribute android:requestLegacyExternalStorage not found" in Console
+"android:requestLegacyExternalStorage" attribute in AndroidManifest.xml fixes a rare UnauthorizedAccessException on Android 10 but requires you to update your Android SDK to at least SDK 29. If this isn't possible for you, you should open NativeGallery.aar with WinRAR or 7-Zip and then remove the "<application ... />" tag from AndroidManifest.xml.
+
+- Can't access the Gallery, it says "java.lang.ClassNotFoundException: com.yasirkula.unity.NativeGallery" in Logcat
+If you are sure that your plugin is up-to-date, then enable "Custom Proguard File" option from Player Settings and add the following line to that file: -keep class com.yasirkula.unity.* { *; }
+
+- Nothing happens when I try to access the Gallery on Android
+Make sure that you've set the "Write Permission" to "External (SDCard)" in Player Settings.
+
+- NativeGallery functions return Permission.Denied even though I've set "Write Permission" to "External (SDCard)"
+Declare the WRITE_EXTERNAL_STORAGE permission manually in your Plugins/Android/AndroidManifest.xml file as follows: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="replace"/>
+You'll need to add the following attribute to the '<manifest ...>' element: xmlns:tools="http://schemas.android.com/tools"
+
+- Saving image/video doesn't work properly
+Make sure that the "filename" parameter of the Save function includes the file's extension, as well
+
+
+4. SCRIPTING API
+Please see the online documentation for a more in-depth documentation of the Scripting API: https://github.com/yasirkula/UnityNativeGallery
+
+enum NativeGallery.PermissionType { Read = 0, Write = 1 };
+enum NativeGallery.Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
+enum NativeGallery.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)
+enum MediaType { Image = 1, Video = 2, Audio = 4 };
+
+delegate void MediaSaveCallback( bool success, string path );
+delegate void NativeGallery.MediaPickCallback( string path );
+delegate void MediaPickMultipleCallback( string[] paths );
+
+//// Saving Media To Gallery/Photos ////
+
+// On Android, your images/videos are saved at DCIM/album/filename. On iOS 14+, the image/video will be saved to the default Photos album (i.e. album parameter will be ignored). On earlier iOS versions, the image/video will be saved to the target album.
+// NOTE: Make sure that the filename parameter includes the file's extension, as well
+// IMPORTANT: NativeGallery will never overwrite existing media on the Gallery. If there is a name conflict, NativeGallery will ensure a unique filename. So don't put '{0}' in filename anymore (for new users, putting {0} in filename was recommended in order to ensure unique filenames in earlier versions, this is no longer necessary).
+// MediaSaveCallback takes "bool success" and "string path" parameters. If the image/video is saved successfully, success becomes true. On Android, path stores where the image/video was saved to (is null on iOS). If the raw filepath can't be determined, an abstract Storage Access Framework path will be returned (File.Exists returns false for that path)
+NativeGallery.Permission NativeGallery.SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null );
+NativeGallery.Permission NativeGallery.SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null );
+NativeGallery.Permission NativeGallery.SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null );
+NativeGallery.Permission NativeGallery.SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null );
+NativeGallery.Permission NativeGallery.SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null );
+
+
+//// Retrieving Media From Gallery/Photos ////
+
+// This operation is asynchronous! After user selects an image/video or cancels the operation, the callback is called (on main thread)
+// MediaPickCallback takes a string parameter which stores the path of the selected image/video, or null if nothing is selected
+// MediaPickMultipleCallback takes a string[] parameter which stores the path(s) of the selected image(s)/video(s), or null if nothing is selected
+// title: determines the title of the image picker dialog on Android. Has no effect on iOS
+// mime: filters the available images/videos on Android. For example, to request a JPEG image from the user, mime can be set as "image/jpeg". Setting multiple mime types is not possible (in that case, you should leave mime as is). Has no effect on iOS
+NativeGallery.Permission NativeGallery.GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" );
+NativeGallery.Permission NativeGallery.GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" );
+NativeGallery.Permission NativeGallery.GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" );
+NativeGallery.Permission NativeGallery.GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" );
+
+// Picking audio files is supported on Android only
+NativeGallery.Permission NativeGallery.GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" );
+NativeGallery.Permission NativeGallery.GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" );
+
+// Allows you to pick images/videos/audios at the same time
+// mediaTypes: bitwise OR'ed media types to pick from (e.g. to pick an image or video, use 'MediaType.Image | MediaType.Video')
+NativeGallery.Permission NativeGallery.GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" );
+NativeGallery.Permission NativeGallery.GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" );
+
+
+// Returns true if selecting multiple images/videos from Gallery/Photos is possible on this device (only available on Android 18 and later and iOS 14 and later)
+bool NativeGallery.CanSelectMultipleFilesFromGallery();
+
+// Returns true if GetMixedMediaFromGallery/GetMixedMediasFromGallery functions are supported (available on Android 19 and later and all iOS versions)
+bool NativeGallery.CanSelectMultipleMediaTypesFromGallery();
+
+// Returns true if the user is currently picking media from Gallery/Photos. In that case, another GetImageFromGallery, GetVideoFromGallery or GetAudioFromGallery request will simply be ignored
+bool NativeGallery.IsMediaPickerBusy();
+
+
+//// Runtime Permissions ////
+
+// Interacting with Gallery/Photos is only possible when permission state is Permission.Granted. Most of the functions request permission internally (and return the result) but you can also check/request the permissions manually
+// mediaTypes: for which media type(s) we're checking the permission for. Has no effect on iOS
+NativeGallery.Permission NativeGallery.CheckPermission( PermissionType permissionType, MediaType mediaTypes );
+NativeGallery.Permission NativeGallery.RequestPermission( PermissionType permissionType, MediaType mediaTypes );
+
+// If permission state is Permission.Denied, user must grant the necessary permission (Storage on Android and Photos on iOS) manually from the Settings. These functions help you open the Settings directly from within the app
+void NativeGallery.OpenSettings();
+bool NativeGallery.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 NativeGallery.LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false );
+async Task<Texture2D> NativeGallery.LoadImageAtPathAsync( 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
+// markTextureNonReadable: see LoadImageAtPath
+// generateMipmaps: see LoadImageAtPath
+// linearColorSpace: see LoadImageAtPath
+Texture2D NativeGallery.GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false );
+async Task<Texture2D> NativeGallery.GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false );
+
+// 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
+NativeGallery.ImageProperties NativeGallery.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
+NativeGallery.VideoProperties NativeGallery.GetVideoProperties( string videoPath );
+
+// Returns the media type of the file at the specified path: Image, Video, Audio or neither of these (if media type can't be determined)
+NativeGallery.MediaType NativeGallery.GetMediaTypeOfFile( string path );

+ 8 - 0
Assets/Plugins/NativeGallery/README.txt.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: be769f45b807c40459e5bafb18e887d6
+timeCreated: 1563308465
+licenseType: Store
+TextScriptImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/Plugins/NativeGallery/iOS.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 9c623599351a41a4c84c20f73c9d8976
+folderAsset: yes
+timeCreated: 1498722622
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 130 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs

@@ -0,0 +1,130 @@
+#if UNITY_EDITOR || UNITY_IOS
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGMediaReceiveCallbackiOS : MonoBehaviour
+	{
+		private static NGMediaReceiveCallbackiOS instance;
+
+		private NativeGallery.MediaPickCallback callback;
+		private NativeGallery.MediaPickMultipleCallback callbackMultiple;
+
+		private float nextBusyCheckTime;
+
+		public static bool IsBusy { get; private set; }
+
+		[System.Runtime.InteropServices.DllImport( "__Internal" )]
+		private static extern int _NativeGallery_IsMediaPickerBusy();
+
+		public static void Initialize( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple )
+		{
+			if( IsBusy )
+				return;
+
+			if( instance == null )
+			{
+				instance = new GameObject( "NGMediaReceiveCallbackiOS" ).AddComponent<NGMediaReceiveCallbackiOS>();
+				DontDestroyOnLoad( instance.gameObject );
+			}
+
+			instance.callback = callback;
+			instance.callbackMultiple = callbackMultiple;
+
+			instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
+			IsBusy = true;
+		}
+
+		private void Update()
+		{
+			if( IsBusy )
+			{
+				if( Time.realtimeSinceStartup >= nextBusyCheckTime )
+				{
+					nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
+
+					if( _NativeGallery_IsMediaPickerBusy() == 0 )
+					{
+						IsBusy = false;
+
+						NativeGallery.MediaPickCallback _callback = callback;
+						callback = null;
+
+						NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple;
+						callbackMultiple = null;
+
+						if( _callback != null )
+							_callback( null );
+
+						if( _callbackMultiple != null )
+							_callbackMultiple( null );
+					}
+				}
+			}
+		}
+
+		public void OnMediaReceived( string path )
+		{
+			IsBusy = false;
+
+			if( string.IsNullOrEmpty( path ) )
+				path = null;
+
+			NativeGallery.MediaPickCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( path );
+		}
+
+		public void OnMultipleMediaReceived( string paths )
+		{
+			IsBusy = false;
+
+			string[] _paths = SplitPaths( paths );
+			if( _paths != null && _paths.Length == 0 )
+				_paths = null;
+
+			NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple;
+			callbackMultiple = null;
+
+			if( _callbackMultiple != null )
+				_callbackMultiple( _paths );
+		}
+
+		private string[] SplitPaths( string paths )
+		{
+			string[] result = null;
+			if( !string.IsNullOrEmpty( paths ) )
+			{
+				string[] pathsSplit = paths.Split( '>' );
+
+				int validPathCount = 0;
+				for( int i = 0; i < pathsSplit.Length; i++ )
+				{
+					if( !string.IsNullOrEmpty( pathsSplit[i] ) )
+						validPathCount++;
+				}
+
+				if( validPathCount == 0 )
+					pathsSplit = new string[0];
+				else if( validPathCount != pathsSplit.Length )
+				{
+					string[] validPaths = new string[validPathCount];
+					for( int i = 0, j = 0; i < pathsSplit.Length; i++ )
+					{
+						if( !string.IsNullOrEmpty( pathsSplit[i] ) )
+							validPaths[j++] = pathsSplit[i];
+					}
+
+					pathsSplit = validPaths;
+				}
+
+				result = pathsSplit;
+			}
+
+			return result;
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 71fb861c149c2d1428544c601e52a33c
+timeCreated: 1519060539
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 43 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs

@@ -0,0 +1,43 @@
+#if UNITY_EDITOR || UNITY_IOS
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGMediaSaveCallbackiOS : MonoBehaviour
+	{
+		private static NGMediaSaveCallbackiOS instance;
+		private NativeGallery.MediaSaveCallback callback;
+
+		public static void Initialize( NativeGallery.MediaSaveCallback callback )
+		{
+			if( instance == null )
+			{
+				instance = new GameObject( "NGMediaSaveCallbackiOS" ).AddComponent<NGMediaSaveCallbackiOS>();
+				DontDestroyOnLoad( instance.gameObject );
+			}
+			else if( instance.callback != null )
+				instance.callback( false, null );
+
+			instance.callback = callback;
+		}
+
+		public void OnMediaSaveCompleted( string message )
+		{
+			NativeGallery.MediaSaveCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( true, null );
+		}
+
+		public void OnMediaSaveFailed( string error )
+		{
+			NativeGallery.MediaSaveCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( false, null );
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 9cbb865d0913a0d47bb6d2eb3ad04c4f
+timeCreated: 1519060539
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1540 - 0
Assets/Plugins/NativeGallery/iOS/NativeGallery.mm

@@ -0,0 +1,1540 @@
+#import <Foundation/Foundation.h>
+#import <Photos/Photos.h>
+#import <MobileCoreServices/UTCoreTypes.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <ImageIO/ImageIO.h>
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+#import <AssetsLibrary/AssetsLibrary.h>
+#endif
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+#import <PhotosUI/PhotosUI.h>
+#endif
+
+#ifdef UNITY_4_0 || UNITY_5_0
+#import "iPhone_View.h"
+#else
+extern UIViewController* UnityGetGLViewController();
+#endif
+
+#define CHECK_IOS_VERSION( version )  ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedAscending)
+
+@interface UNativeGallery:NSObject
++ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;
++ (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;
++ (void)showLimitedLibraryPicker;
++ (int)canOpenSettings;
++ (void)openSettings;
++ (int)canPickMultipleMedia;
++ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode;
++ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit;
++ (int)isMediaPickerBusy;
++ (int)getMediaTypeFromExtension:(NSString *)extension;
++ (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 UNativeGallery
+
+static NSString *pickedMediaSavePath;
+static UIPopoverController *popup;
+static UIImagePickerController *imagePicker;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+static PHPickerViewController *imagePickerNew;
+#endif
+static int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finished
+static BOOL simpleMediaPickMode;
+static BOOL pickingMultipleFiles = NO;
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode
+{
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+	if( CHECK_IOS_VERSION( @"8.0" ) )
+	{
+#endif
+		// version >= iOS 8: check permission using Photos framework
+
+		// On iOS 11 and later, permission isn't mandatory to fetch media from Photos
+		if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )
+			return 1;
+		
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+		// Photos permissions has changed on iOS 14
+		if( CHECK_IOS_VERSION( @"14.0" ) )
+		{
+			// Request ReadWrite permission in 2 cases:
+			// 1) When attempting to pick media from Photos with PHPhotoLibrary (readPermission=true and permissionFreeMode=false)
+			// 2) When attempting to write media to a specific album in Photos using PHPhotoLibrary (readPermission=false and permissionFreeMode=false)
+			PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( ( readPermission || !permissionFreeMode ) ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];
+			if( status == PHAuthorizationStatusAuthorized )
+				return 1;
+			else if( status == PHAuthorizationStatusRestricted )
+				return 3;
+			else if( status == PHAuthorizationStatusNotDetermined )
+				return 2;
+			else
+				return 0;
+		}
+		else
+#endif
+		{
+			PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
+			if( status == PHAuthorizationStatusAuthorized )
+				return 1;
+			else if( status == PHAuthorizationStatusNotDetermined )
+				return 2;
+			else
+				return 0;
+		}
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+	}
+	else
+	{
+		// version < iOS 8: check permission using AssetsLibrary framework (Photos framework not available)
+		ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
+		if( status == ALAuthorizationStatusAuthorized )
+			return 1;
+		else if( status == ALAuthorizationStatusNotDetermined )
+			return 2;
+		else
+			return 0;
+	}
+#endif
+}
+#pragma clang diagnostic pop
+
++ (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode
+{
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+	if( CHECK_IOS_VERSION( @"8.0" ) )
+	{
+#endif
+		// version >= iOS 8: request permission using Photos framework
+		
+		// On iOS 11 and later, permission isn't mandatory to fetch media from Photos
+		if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )
+			return 1;
+		
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+		if( CHECK_IOS_VERSION( @"14.0" ) )
+		{
+			// Photos permissions has changed on iOS 14. There are 2 permission dialogs now:
+			// - AddOnly permission dialog: has 2 options: "Allow" and "Don't Allow". This dialog grants permission for save operations only. Unfortunately,
+			//   saving media to a custom album isn't possible with this dialog, media can only be saved to the default Photos album
+			// - ReadWrite permission dialog: has 3 options: "Allow Access to All Photos" (i.e. full permission), "Select Photos" (i.e. limited access) and
+			//   "Don't Allow". To be able to save media to a custom album, user must grant Full Photos permission. Thus, even when readPermission is false,
+			//   this dialog will be used if PermissionFreeMode is set to false. So, PermissionFreeMode determines whether or not saving to a custom album is
+			//   be supported
+			return [self requestPermissionNewest:( readPermission || !permissionFreeMode )];
+		}
+		else
+#endif
+			return [self requestPermissionNew];
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+	}
+	else
+	{
+		// version < iOS 8: request permission using AssetsLibrary framework (Photos framework not available)
+		return [self requestPermissionOld];
+	}
+#endif
+}
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+// Credit: https://stackoverflow.com/a/26933380/2373034
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (int)requestPermissionOld
+{
+	ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
+	
+	if( status == ALAuthorizationStatusAuthorized )
+		return 1;
+	else if( status == ALAuthorizationStatusNotDetermined )
+	{
+		__block BOOL authorized = NO;
+		ALAssetsLibrary *lib = [[ALAssetsLibrary alloc] init];
+		
+		dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );
+		[lib enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^( ALAssetsGroup *group, BOOL *stop )
+		{
+			*stop = YES;
+			authorized = YES;
+			dispatch_semaphore_signal( sema );
+		}
+		failureBlock:^( NSError *error )
+		{
+			dispatch_semaphore_signal( sema );
+		}];
+		dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );
+		
+		return authorized ? 1 : 0;
+	}
+
+	return 0;
+}
+#pragma clang diagnostic pop
+#endif
+
+// Credit: https://stackoverflow.com/a/32989022/2373034
++ (int)requestPermissionNew
+{
+	PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
+	
+	if( status == PHAuthorizationStatusAuthorized )
+		return 1;
+	else if( status == PHAuthorizationStatusNotDetermined )
+	{
+		__block BOOL authorized = NO;
+		
+		dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );
+		[PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status )
+		{
+			authorized = ( status == PHAuthorizationStatusAuthorized );
+			dispatch_semaphore_signal( sema );
+		}];
+		dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );
+		
+		return authorized ? 1 : 0;
+	}
+	
+	return 0;
+}
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
++ (int)requestPermissionNewest:(BOOL)readPermission
+{
+	PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];
+	
+	if( status == PHAuthorizationStatusAuthorized )
+		return 1;
+	else if( status == PHAuthorizationStatusRestricted )
+		return 3;
+	else if( status == PHAuthorizationStatusNotDetermined )
+	{
+		__block int authorized = 0;
+		
+		dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );
+		[PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status )
+		{
+			if( status == PHAuthorizationStatusAuthorized )
+				authorized = 1;
+			else if( status == PHAuthorizationStatusRestricted )
+				authorized = 3;
+
+			dispatch_semaphore_signal( sema );
+		}];
+		dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );
+		
+		return authorized;
+	}
+	
+	return 0;
+}
+#endif
+
+// When Photos permission is set to restricted, allows user to change the permission or change the list of restricted images
+// It doesn't support a deterministic callback; for example there is a photoLibraryDidChange event but it won't be invoked if
+// user doesn't change the list of restricted images
++ (void)showLimitedLibraryPicker
+{
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+	PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
+	if( status == PHAuthorizationStatusNotDetermined )
+		[self requestPermissionNewest:YES];
+	else if( status == PHAuthorizationStatusRestricted )
+		[[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:UnityGetGLViewController()];
+#endif
+}
+
+// Credit: https://stackoverflow.com/a/25453667/2373034
++ (int)canOpenSettings
+{
+	return ( &UIApplicationOpenSettingsURLString != NULL ) ? 1 : 0;
+}
+
+// Credit: https://stackoverflow.com/a/25453667/2373034
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (void)openSettings
+{
+	if( &UIApplicationOpenSettingsURLString != NULL )
+	{
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
+		if( CHECK_IOS_VERSION( @"10.0" ) )
+			[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
+		else
+#endif
+			[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
+	}
+}
+#pragma clang diagnostic pop
+
++ (int)canPickMultipleMedia
+{
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+	if( CHECK_IOS_VERSION( @"14.0" ) )
+		return 1;
+	else
+#endif
+		return 0;
+}
+
++ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode
+{
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+	if( CHECK_IOS_VERSION( @"8.0" ) )
+	{
+#endif
+		// version >= iOS 8: save to specified album using Photos framework
+		// On iOS 14+, permission workflow has changed significantly with the addition of PHAuthorizationStatusRestricted permission. On those versions,
+		// user must grant Full Photos permission to be able to save to a custom album. Hence, there are 2 workflows:
+		// - If PermissionFreeMode is enabled, save the media directly to the default album (i.e. ignore 'album' parameter). This will present a simple
+		//   permission dialog stating "The app requires access to Photos to save media to it." and the "Selected Photos" permission won't be listed in the options
+		// - Otherwise, the more complex "The app requires access to Photos to interact with it." permission dialog will be shown and if the user grants
+		//   Full Photos permission, only then the image will be saved to the specified album. If user selects "Selected Photos" permission, default album will be
+		//   used as fallback
+		[self saveMediaNew:path albumName:album isImage:isImg saveToDefaultAlbum:( permissionFreeMode && CHECK_IOS_VERSION( @"14.0" ) )];
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+	}
+	else
+	{
+		// version < iOS 8: save using AssetsLibrary framework (Photos framework not available)
+		[self saveMediaOld:path albumName:album isImage:isImg];
+	}
+#endif
+}
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
+// Credit: https://stackoverflow.com/a/22056664/2373034
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (void)saveMediaOld:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage
+{
+	ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
+	
+	if( !isImage && ![library videoAtPathIsCompatibleWithSavedPhotosAlbum:[NSURL fileURLWithPath:path]])
+	{
+		NSLog( @"Error saving video: Video format is not compatible with Photos" );
+		[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+		return;
+	}
+	
+	void (^saveBlock)(ALAssetsGroup *assetCollection) = ^void( ALAssetsGroup *assetCollection )
+	{
+		void (^saveResultBlock)(NSURL *assetURL, NSError *error) = ^void( NSURL *assetURL, NSError *error )
+		{
+			[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+			
+			if( error.code == 0 )
+			{
+				[library assetForURL:assetURL resultBlock:^( ALAsset *asset )
+				{
+					[assetCollection addAsset:asset];
+					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+				}
+				failureBlock:^( NSError* error )
+				{
+					NSLog( @"Error moving asset to album: %@", error );
+					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+				}];
+			}
+			else
+			{
+				NSLog( @"Error creating asset: %@", error );
+				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+			}
+		};
+		
+		if( !isImage )
+			[library writeImageDataToSavedPhotosAlbum:[NSData dataWithContentsOfFile:path] metadata:nil completionBlock:saveResultBlock];
+		else
+			[library writeVideoAtPathToSavedPhotosAlbum:[NSURL fileURLWithPath:path] completionBlock:saveResultBlock];
+	};
+	
+	__block BOOL albumFound = NO;
+	[library enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^( ALAssetsGroup *group, BOOL *stop )
+	{
+		if( [[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:album] )
+		{
+			*stop = YES;
+			albumFound = YES;
+			saveBlock( group );
+		}
+		else if( group == nil && albumFound==NO )
+		{
+			// Album doesn't exist
+			[library addAssetsGroupAlbumWithName:album resultBlock:^( ALAssetsGroup *group )
+			{
+				saveBlock( group );
+			}
+			failureBlock:^( NSError *error )
+			{
+				NSLog( @"Error creating album: %@", error );
+				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+			}];
+		}
+	}
+	failureBlock:^( NSError* error )
+	{
+		NSLog( @"Error listing albums: %@", error );
+		[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+	}];
+}
+#pragma clang diagnostic pop
+#endif
+
+// Credit: https://stackoverflow.com/a/39909129/2373034
++ (void)saveMediaNew:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage saveToDefaultAlbum:(BOOL)saveToDefaultAlbum
+{
+	void (^saveToPhotosAlbum)() = ^void()
+	{
+		if( isImage )
+		{
+			// Try preserving image metadata (essential for animated gif images)
+			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:
+			^{
+				[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
+			}
+			completionHandler:^( BOOL success, NSError *error )
+			{
+				if( success )
+				{
+					[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+				}
+				else
+				{
+					NSLog( @"Error creating asset in default Photos album: %@", error );
+					
+					UIImage *image = [UIImage imageWithContentsOfFile:path];
+					if( image != nil )
+						UIImageWriteToSavedPhotosAlbum( image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );
+					else
+					{
+						NSLog( @"Couldn't create UIImage from file at path: %@", path );
+						[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+						UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+					}
+				}
+			}];
+		}
+		else
+		{
+			if( UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( path ) )
+				UISaveVideoAtPathToSavedPhotosAlbum( path, self, @selector(video:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );
+			else
+			{
+				NSLog( @"Video at path isn't compatible with saved photos album: %@", path );
+				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+			}
+		}
+	};
+
+	void (^saveBlock)(PHAssetCollection *assetCollection) = ^void( PHAssetCollection *assetCollection )
+	{
+		[[PHPhotoLibrary sharedPhotoLibrary] performChanges:
+		^{
+			PHAssetChangeRequest *assetChangeRequest;
+			if( isImage )
+				assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
+			else
+				assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:path]];
+			
+			PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection];
+			[assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]];
+			
+		}
+		completionHandler:^( BOOL success, NSError *error )
+		{
+			if( success )
+			{
+				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+			}
+			else
+			{
+				NSLog( @"Error creating asset: %@", error );
+				saveToPhotosAlbum();
+			}
+		}];
+	};
+
+	if( saveToDefaultAlbum )
+		saveToPhotosAlbum();
+	else
+	{
+		PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
+		fetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", album];
+		PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:fetchOptions];
+		if( fetchResult.count > 0 )
+			saveBlock( fetchResult.firstObject);
+		else
+		{
+			__block PHObjectPlaceholder *albumPlaceholder;
+			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:
+			^{
+				PHAssetCollectionChangeRequest *changeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:album];
+				albumPlaceholder = changeRequest.placeholderForCreatedAssetCollection;
+			}
+			completionHandler:^( BOOL success, NSError *error )
+			{
+				if( success )
+				{
+					PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumPlaceholder.localIdentifier] options:nil];
+					if( fetchResult.count > 0 )
+						saveBlock( fetchResult.firstObject);
+					else
+					{
+						NSLog( @"Error creating album: Album placeholder not found" );
+						saveToPhotosAlbum();
+					}
+				}
+				else
+				{
+					NSLog( @"Error creating album: %@", error );
+					saveToPhotosAlbum();
+				}
+			}];
+		}
+	}
+}
+
++ (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
+{
+	NSString* path = (__bridge_transfer NSString *)(contextInfo);
+	[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+
+	if( error == nil )
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+	else
+	{
+		NSLog( @"Error saving image with UIImageWriteToSavedPhotosAlbum: %@", error );
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+	}
+}
+
++ (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
+{
+	NSString* path = (__bridge_transfer NSString *)(contextInfo);
+	[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+
+	if( error == nil )
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+	else
+	{
+		NSLog( @"Error saving video with UISaveVideoAtPathToSavedPhotosAlbum: %@", error );
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+	}
+}
+
+// Credit: https://stackoverflow.com/a/10531752/2373034
++ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit
+{
+	pickedMediaSavePath = mediaSavePath;
+	imagePickerState = 1;
+	simpleMediaPickMode = permissionFreeMode && CHECK_IOS_VERSION( @"11.0" );
+	
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+	if( CHECK_IOS_VERSION( @"14.0" ) )
+	{
+		// PHPickerViewController is used on iOS 14
+		PHPickerConfiguration *config = simpleMediaPickMode ? [[PHPickerConfiguration alloc] init] : [[PHPickerConfiguration alloc] initWithPhotoLibrary:[PHPhotoLibrary sharedPhotoLibrary]];
+		config.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent;
+		config.selectionLimit = selectionLimit;
+		pickingMultipleFiles = selectionLimit != 1;
+		
+		// mediaType is a bitmask:
+		// 1: image
+		// 2: video
+		// 4: audio (not supported)
+		if( mediaType == 1 )
+			config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], nil]];
+		else if( mediaType == 2 )
+			config.filter = [PHPickerFilter videosFilter];
+		else
+			config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], [PHPickerFilter videosFilter], nil]];
+		
+		imagePickerNew = [[PHPickerViewController alloc] initWithConfiguration:config];
+		imagePickerNew.delegate = (id) self;
+		[UnityGetGLViewController() presentViewController:imagePickerNew animated:YES completion:^{ imagePickerState = 0; }];
+	}
+	else
+#endif
+	{
+		// UIImagePickerController is used on previous versions
+		imagePicker = [[UIImagePickerController alloc] init];
+		imagePicker.delegate = (id) self;
+		imagePicker.allowsEditing = NO;
+		imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
+		
+		// mediaType is a bitmask:
+		// 1: image
+		// 2: video
+		// 4: audio (not supported)
+		if( mediaType == 1 )
+		{
+			if( CHECK_IOS_VERSION( @"9.1" ) )
+				imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, nil];
+			else
+				imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
+		}
+		else if( mediaType == 2 )
+			imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
+		else
+		{
+			if( CHECK_IOS_VERSION( @"9.1" ) )
+				imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
+			else
+				imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
+		}
+		
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+		if( mediaType != 1 )
+		{
+			// Don't compress picked videos if possible
+			if( CHECK_IOS_VERSION( @"11.0" ) )
+				imagePicker.videoExportPreset = AVAssetExportPresetPassthrough;
+		}
+#endif
+		
+		UIViewController *rootViewController = UnityGetGLViewController();
+		if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) // iPhone
+			[rootViewController presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];
+		else
+		{
+			// iPad
+			popup = [[UIPopoverController alloc] initWithContentViewController:imagePicker];
+			popup.delegate = (id) self;
+			[popup presentPopoverFromRect:CGRectMake( rootViewController.view.frame.size.width / 2, rootViewController.view.frame.size.height / 2, 1, 1 ) inView:rootViewController.view permittedArrowDirections:0 animated:YES];
+		}
+	}
+}
+
++ (int)isMediaPickerBusy
+{
+	if( imagePickerState == 2 )
+		return 1;
+	
+	if( imagePicker != nil )
+	{
+		if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() )
+			return 1;
+		else
+		{
+			imagePicker = nil;
+			return 0;
+		}
+	}
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+	else if( CHECK_IOS_VERSION( @"14.0" ) && imagePickerNew != nil )
+	{
+		if( imagePickerState == 1 || [imagePickerNew presentingViewController] == UnityGetGLViewController() )
+			return 1;
+		else
+		{
+			imagePickerNew = nil;
+			return 0;
+		}
+	}
+#endif
+	else
+		return 0;
+}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
+{
+	NSString *resultPath = nil;
+	
+	if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] )
+	{
+		NSLog( @"Picked an image" );
+		
+		// On iOS 8.0 or later, try to obtain the raw data of the image (which allows picking gifs properly or preserving metadata)
+		if( CHECK_IOS_VERSION( @"8.0" ) )
+		{
+			PHAsset *asset = nil;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+			if( CHECK_IOS_VERSION( @"11.0" ) )
+			{
+				// Try fetching the source image via UIImagePickerControllerImageURL
+				NSURL *mediaUrl = info[UIImagePickerControllerImageURL];
+				if( mediaUrl != nil )
+				{
+					NSString *imagePath = [mediaUrl path];
+					if( imagePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:imagePath] )
+					{
+						NSError *error;
+						NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[imagePath pathExtension]];
+						
+						if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+						{
+							if( [[NSFileManager defaultManager] copyItemAtPath:imagePath toPath:newPath error:&error] )
+							{
+								resultPath = newPath;
+								NSLog( @"Copied source image from UIImagePickerControllerImageURL" );
+							}
+							else
+								NSLog( @"Error copying image: %@", error );
+						}
+						else
+							NSLog( @"Error deleting existing image: %@", error );
+					}
+				}
+				
+				if( resultPath == nil )
+					asset = info[UIImagePickerControllerPHAsset];
+			}
+#endif
+			
+			if( resultPath == nil && !simpleMediaPickMode )
+			{
+				if( asset == nil )
+				{
+					NSURL *mediaUrl = info[UIImagePickerControllerReferenceURL] ?: info[UIImagePickerControllerMediaURL];
+					if( mediaUrl != nil )
+						asset = [[PHAsset fetchAssetsWithALAssetURLs:[NSArray arrayWithObject:mediaUrl] options:nil] firstObject];
+				}
+				
+				resultPath = [self trySavePHAsset:asset atIndex:1];
+			}
+		}
+		
+		if( resultPath == nil )
+		{
+			// Save image as PNG
+			UIImage *image = info[UIImagePickerControllerOriginalImage];
+			if( image != nil )
+			{
+				resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
+				if( ![self saveImageAsPNG:image toPath:resultPath] )
+				{
+					NSLog( @"Error creating PNG image" );
+					resultPath = nil;
+				}
+			}
+			else
+				NSLog( @"Error fetching original image from picker" );
+		}
+	}
+	else if( CHECK_IOS_VERSION( @"9.1" ) && [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeLivePhoto] )
+	{
+		NSLog( @"Picked a live photo" );
+		
+		// Save live photo as PNG
+		UIImage *image = info[UIImagePickerControllerOriginalImage];
+		if( image != nil )
+		{
+			resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
+			if( ![self saveImageAsPNG:image toPath:resultPath] )
+			{
+				NSLog( @"Error creating PNG image" );
+				resultPath = nil;
+			}
+		}
+		else
+			NSLog( @"Error fetching live photo's still image from picker" );
+	}
+	else
+	{
+		NSLog( @"Picked a video" );
+		
+		NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
+		if( mediaUrl != nil )
+		{
+			resultPath = [mediaUrl path];
+			
+			// On iOS 13, picked file becomes unreachable as soon as the UIImagePickerController disappears,
+			// in that case, copy the video to a temporary location
+			if( CHECK_IOS_VERSION( @"13.0" ) )
+			{
+				NSError *error;
+				NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[resultPath pathExtension]];
+				
+				if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+				{
+					if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error] )
+						resultPath = newPath;
+					else
+					{
+						NSLog( @"Error copying video: %@", error );
+						resultPath = nil;
+					}
+				}
+				else
+				{
+					NSLog( @"Error deleting existing video: %@", error );
+					resultPath = nil;
+				}
+			}
+		}
+	}
+	
+	popup = nil;
+	imagePicker = nil;
+	imagePickerState = 2;
+	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPath] );
+	
+	[picker dismissViewControllerAnimated:NO completion:nil];
+}
+#pragma clang diagnostic pop
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
+// Credit: https://ikyle.me/blog/2020/phpickerviewcontroller
++(void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results
+{
+	imagePickerNew = nil;
+	imagePickerState = 2;
+	
+	[picker dismissViewControllerAnimated:NO completion:nil];
+	
+	if( results != nil && [results count] > 0 )
+	{
+		NSMutableArray<NSString *> *resultPaths = [NSMutableArray arrayWithCapacity:[results count]];
+		NSLock *arrayLock = [[NSLock alloc] init];
+		dispatch_group_t group = dispatch_group_create();
+		
+		for( int i = 0; i < [results count]; i++ )
+		{
+			PHPickerResult *result = results[i];
+			NSItemProvider *itemProvider = result.itemProvider;
+			NSString *assetIdentifier = result.assetIdentifier;
+			__block NSString *resultPath = nil;
+			
+			int j = i + 1;
+			
+			//NSLog( @"result: %@", result );
+			//NSLog( @"%@", result.assetIdentifier);
+			//NSLog( @"%@", result.itemProvider);
+
+			if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage] )
+			{
+				NSLog( @"Picked an image" );
+				
+				if( !simpleMediaPickMode && assetIdentifier != nil )
+				{
+					PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetIdentifier] options:nil] firstObject];
+					resultPath = [self trySavePHAsset:asset atIndex:j];
+				}
+				
+				if( resultPath != nil )
+				{
+					[arrayLock lock];
+					[resultPaths addObject:resultPath];
+					[arrayLock unlock];
+				}
+				else
+				{
+					dispatch_group_enter( group );
+					
+					[itemProvider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeImage completionHandler:^( NSURL *url, NSError *error )
+					{
+						if( url != nil )
+						{
+							// Copy the image to a temporary location because the returned image will be deleted by the OS after this callback is completed
+							resultPath = [url path];
+							NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];
+							
+							if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+							{
+								if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])
+									resultPath = newPath;
+								else
+								{
+									NSLog( @"Error copying image: %@", error );
+									resultPath = nil;
+								}
+							}
+							else
+							{
+								NSLog( @"Error deleting existing image: %@", error );
+								resultPath = nil;
+							}
+						}
+						else
+							NSLog( @"Error getting the picked image's path: %@", error );
+						
+						if( resultPath != nil )
+						{
+							[arrayLock lock];
+							[resultPaths addObject:resultPath];
+							[arrayLock unlock];
+						}
+						else
+						{
+							if( [itemProvider canLoadObjectOfClass:[UIImage class]] )
+							{
+								dispatch_group_enter( group );
+								
+								[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
+								{
+									if( object != nil && [object isKindOfClass:[UIImage class]] )
+									{
+										resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];
+										if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )
+										{
+											NSLog( @"Error creating PNG image" );
+											resultPath = nil;
+										}
+									}
+									else
+										NSLog( @"Error generating UIImage from picked image: %@", error );
+									
+									[arrayLock lock];
+									[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+									[arrayLock unlock];
+									
+									dispatch_group_leave( group );
+								}];
+							}
+							else
+							{
+								NSLog( @"Can't generate UIImage from picked image" );
+								
+								[arrayLock lock];
+								[resultPaths addObject:@""];
+								[arrayLock unlock];
+							}
+						}
+						
+						dispatch_group_leave( group );
+					}];
+				}
+			}
+			else if( CHECK_IOS_VERSION( @"9.1" ) && [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeLivePhoto] )
+			{
+				NSLog( @"Picked a live photo" );
+				
+				if( [itemProvider canLoadObjectOfClass:[UIImage class]] )
+				{
+					dispatch_group_enter( group );
+					
+					[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
+					{
+						if( object != nil && [object isKindOfClass:[UIImage class]] )
+						{
+							resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];
+							if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )
+							{
+								NSLog( @"Error creating PNG image" );
+								resultPath = nil;
+							}
+						}
+						else
+							NSLog( @"Error generating UIImage from picked live photo: %@", error );
+						
+						[arrayLock lock];
+						[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+						[arrayLock unlock];
+						
+						dispatch_group_leave( group );
+					}];
+				}
+				else if( [itemProvider canLoadObjectOfClass:[PHLivePhoto class]] )
+				{
+					dispatch_group_enter( group );
+					
+					[itemProvider loadObjectOfClass:[PHLivePhoto class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
+					{
+						if( object != nil && [object isKindOfClass:[PHLivePhoto class]] )
+						{
+							// Extract image data from live photo
+							// Credit: https://stackoverflow.com/a/41341675/2373034
+							NSArray<PHAssetResource*>* livePhotoResources = [PHAssetResource assetResourcesForLivePhoto:(PHLivePhoto *)object];
+							
+							PHAssetResource *livePhotoImage = nil;
+							for( int k = 0; k < [livePhotoResources count]; k++ )
+							{
+								if( livePhotoResources[k].type == PHAssetResourceTypePhoto )
+								{
+									livePhotoImage = livePhotoResources[k];
+									break;
+								}
+							}
+							
+							if( livePhotoImage == nil )
+							{
+								NSLog( @"Error extracting image data from live photo" );
+							
+								[arrayLock lock];
+								[resultPaths addObject:@""];
+								[arrayLock unlock];
+							}
+							else
+							{
+								dispatch_group_enter( group );
+								
+								NSString *originalFilename = livePhotoImage.originalFilename;
+								if( originalFilename == nil || [originalFilename length] == 0 )
+									resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j];
+								else
+									resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[originalFilename pathExtension]];
+								
+								[[PHAssetResourceManager defaultManager] writeDataForAssetResource:livePhotoImage toFile:[NSURL fileURLWithPath:resultPath] options:nil completionHandler:^( NSError * _Nullable error2 )
+								{
+									if( error2 != nil )
+									{
+										NSLog( @"Error saving image data from live photo: %@", error2 );
+										resultPath = nil;
+									}
+									
+									[arrayLock lock];
+									[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+									[arrayLock unlock];
+									
+									dispatch_group_leave( group );
+								}];
+							}
+						}
+						else
+						{
+							NSLog( @"Error generating PHLivePhoto from picked live photo: %@", error );
+						
+							[arrayLock lock];
+							[resultPaths addObject:@""];
+							[arrayLock unlock];
+						}
+						
+						dispatch_group_leave( group );
+					}];
+				}
+				else
+				{
+					NSLog( @"Can't convert picked live photo to still image" );
+					
+					[arrayLock lock];
+					[resultPaths addObject:@""];
+					[arrayLock unlock];
+				}
+			}
+			else if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] || [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] )
+			{
+				NSLog( @"Picked a video" );
+				
+				// Get the video file's path
+				dispatch_group_enter( group );
+				
+				[itemProvider loadFileRepresentationForTypeIdentifier:([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] ? (NSString *)kUTTypeMovie : (NSString *)kUTTypeVideo) completionHandler:^( NSURL *url, NSError *error )
+				{
+					if( url != nil )
+					{
+						// Copy the video to a temporary location because the returned video will be deleted by the OS after this callback is completed
+						resultPath = [url path];
+						NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];
+						
+						if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+						{
+							if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])
+								resultPath = newPath;
+							else
+							{
+								NSLog( @"Error copying video: %@", error );
+								resultPath = nil;
+							}
+						}
+						else
+						{
+							NSLog( @"Error deleting existing video: %@", error );
+							resultPath = nil;
+						}
+					}
+					else
+						NSLog( @"Error getting the picked video's path: %@", error );
+					
+					[arrayLock lock];
+					[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+					[arrayLock unlock];
+					
+					dispatch_group_leave( group );
+				}];
+			}
+			else
+			{
+				// Unknown media type picked?
+				NSLog( @"Couldn't determine type of picked media: %@", itemProvider );
+				
+				[arrayLock lock];
+				[resultPaths addObject:@""];
+				[arrayLock unlock];
+			}
+		}
+		
+		dispatch_group_notify( group, dispatch_get_main_queue(),
+		^{
+			if( !pickingMultipleFiles )
+				UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPaths[0]] );
+			else
+				UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", [self getCString:[resultPaths componentsJoinedByString:@">"]] );
+		});
+	}
+	else
+	{
+		NSLog( @"No media picked" );
+		
+		if( !pickingMultipleFiles )
+			UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
+		else
+			UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", "" );
+	}
+}
+#endif
+
++ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
+{
+	NSLog( @"UIImagePickerController cancelled" );
+
+	popup = nil;
+	imagePicker = nil;
+	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
+	
+	[picker dismissViewControllerAnimated:NO completion:nil];
+}
+
++ (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
+{
+	NSLog( @"UIPopoverController dismissed" );
+
+	popup = nil;
+	imagePicker = nil;
+	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
+}
+
++ (NSString *)trySavePHAsset:(PHAsset *)asset atIndex:(int)filenameIndex
+{
+	if( asset == nil )
+		return nil;
+	
+	__block NSString *resultPath = nil;
+	
+	PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
+	options.synchronous = YES;
+	options.version = PHImageRequestOptionsVersionCurrent;
+	
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
+	if( CHECK_IOS_VERSION( @"13.0" ) )
+	{
+		[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *imageInfo )
+		{
+			if( imageData != nil )
+				resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];
+			else
+				NSLog( @"Couldn't fetch raw image data" );
+		}];
+	}
+	else 
+#endif
+	{
+		[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *imageInfo )
+		{
+			if( imageData != nil )
+				resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];
+			else
+				NSLog( @"Couldn't fetch raw image data" );
+		}];
+	}
+	
+	return resultPath;
+}
+
++ (NSString *)trySaveSourceImage:(NSData *)imageData withInfo:(NSDictionary *)info atIndex:(int)filenameIndex
+{
+	NSString *filePath = info[@"PHImageFileURLKey"];
+	if( filePath != nil ) // filePath can actually be an NSURL, convert it to NSString
+		filePath = [NSString stringWithFormat:@"%@", filePath];
+	
+	if( filePath == nil || [filePath length] == 0 )
+	{
+		filePath = info[@"PHImageFileUTIKey"];
+		if( filePath != nil )
+			filePath = [NSString stringWithFormat:@"%@", filePath];
+	}
+	
+	NSString *resultPath;
+	if( filePath == nil || [filePath length] == 0 )
+		resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex];
+	else
+		resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex] stringByAppendingPathExtension:[filePath pathExtension]];
+	
+	NSError *error;
+	if( ![[NSFileManager defaultManager] fileExistsAtPath:resultPath] || [[NSFileManager defaultManager] removeItemAtPath:resultPath error:&error] )
+	{
+		if( ![imageData writeToFile:resultPath atomically:YES] )
+		{
+			NSLog( @"Error copying source image to file" );
+			resultPath = nil;
+		}
+	}
+	else
+	{
+		NSLog( @"Error deleting existing image: %@", error );
+		resultPath = nil;
+	}
+	
+	return resultPath;
+}
+
+// Credit: https://lists.apple.com/archives/cocoa-dev/2012/Jan/msg00052.html
++ (int)getMediaTypeFromExtension:(NSString *)extension
+{
+	CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef) extension, NULL );
+	
+	// mediaType is a bitmask:
+	// 1: image
+	// 2: video
+	// 4: audio (not supported)
+	int result = 0;
+	if( UTTypeConformsTo( fileUTI, kUTTypeImage ) )
+		result = 1;
+	else if( CHECK_IOS_VERSION( @"9.1" ) && UTTypeConformsTo( fileUTI, kUTTypeLivePhoto ) )
+		result = 1;
+	else if( UTTypeConformsTo( fileUTI, kUTTypeMovie ) || UTTypeConformsTo( fileUTI, kUTTypeVideo ) )
+		result = 2;
+	else if( UTTypeConformsTo( fileUTI, kUTTypeAudio ) )
+		result = 4;
+	
+	CFRelease( fileUTI );
+	
+	return result;
+}
+
+// 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 NativeGallery.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 [self getCString:@""];
+	}
+	
+	UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
+	CGImageRelease( image );
+	
+	if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] )
+	{
+		NSLog( @"Error saving thumbnail image" );
+		return [self getCString:@""];
+	}
+	
+	return [self getCString:savePath];
+}
+
++ (BOOL)saveImageAsPNG:(UIImage *)image toPath:(NSString *)resultPath
+{
+	return [UIImagePNGRepresentation( [self scaleImage:image maxSize:16384] ) writeToFile:resultPath atomically:YES];
+}
+
++ (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 );
+	
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
+	// Resize image with UIGraphicsImageRenderer (Apple's recommended API) if possible
+	if( CHECK_IOS_VERSION( @"10.0" ) )
+	{
+		UIGraphicsImageRendererFormat *format = [image imageRendererFormat];
+		format.opaque = !hasAlpha;
+		format.scale = image.scale;
+	   
+		UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format];
+		image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext )
+		{
+			[image drawInRect:imageRect];
+		}];
+	}
+	else
+	#endif
+	{
+		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];
+}
+
+// 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 _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode )
+{
+	return [UNativeGallery checkPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode )
+{
+	return [UNativeGallery requestPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_ShowLimitedLibraryPicker()
+{
+	return [UNativeGallery showLimitedLibraryPicker];
+}
+
+extern "C" int _NativeGallery_CanOpenSettings()
+{
+	return [UNativeGallery canOpenSettings];
+}
+
+extern "C" void _NativeGallery_OpenSettings()
+{
+	[UNativeGallery openSettings];
+}
+
+extern "C" int _NativeGallery_CanPickMultipleMedia()
+{
+	return [UNativeGallery canPickMultipleMedia];
+}
+
+extern "C" void _NativeGallery_ImageWriteToAlbum( const char* path, const char* album, int permissionFreeMode )
+{
+	[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:YES permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_VideoWriteToAlbum( const char* path, const char* album, int permissionFreeMode )
+{
+	[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:NO permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_PickMedia( const char* mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit )
+{
+	[UNativeGallery pickMedia:mediaType savePath:[NSString stringWithUTF8String:mediaSavePath] permissionFreeMode:( permissionFreeMode == 1 ) selectionLimit:selectionLimit];
+}
+
+extern "C" int _NativeGallery_IsMediaPickerBusy()
+{
+	return [UNativeGallery isMediaPickerBusy];
+}
+
+extern "C" int _NativeGallery_GetMediaTypeFromExtension( const char* extension )
+{
+	return [UNativeGallery getMediaTypeFromExtension:[NSString stringWithUTF8String:extension]];
+}
+
+extern "C" char* _NativeGallery_GetImageProperties( const char* path )
+{
+	return [UNativeGallery getImageProperties:[NSString stringWithUTF8String:path]];
+}
+
+extern "C" char* _NativeGallery_GetVideoProperties( const char* path )
+{
+	return [UNativeGallery getVideoProperties:[NSString stringWithUTF8String:path]];
+}
+
+extern "C" char* _NativeGallery_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds )
+{
+	return [UNativeGallery getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];
+}
+
+extern "C" char* _NativeGallery_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize )
+{
+	return [UNativeGallery loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];
+}

+ 33 - 0
Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 953e0b740eb03144883db35f72cad8a6
+timeCreated: 1498722774
+licenseType: Store
+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: 

+ 61 - 0
Assets/Remote3.1/Scripts/ImagePicker.cs

@@ -0,0 +1,61 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+public class ImagePicker : MonoBehaviour
+{
+   
+    AndroidJavaObject picker;
+    private void Start()
+    {
+        //AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
+        //AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
+        //picker = new AndroidJavaObject("com.example.mylibrary.ImagePicker", currentActivity);
+        //picker.Call("PickImageTest");
+
+        //AndroidJavaObject unityCallAndorid = new AndroidJavaObject("com.example.mylibrary.UnityCallAndroid", currentActivity);
+
+        //string str= unityCallAndorid.CallStatic<string>("DebugLogs");
+        //Debug.Log(str);
+    }
+    public void PickImage()
+    {
+        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
+        AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
+        picker = new AndroidJavaObject("com.example.mylibrary.ImagePicker", currentActivity);
+        picker.Call("PickImage", "image/*");
+
+        // PickImageFromGallery();
+    }
+
+    public void PickVideo()
+    {
+        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
+        AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
+        picker = new AndroidJavaObject("com.example.mylibrary.ImagePicker", currentActivity);
+        picker.Call("PickImage", "video/*");
+       
+      
+    }
+    // 在Android中选择图片后,Unity中调用该方法接收选择的图片路径
+    public void ReceiveImagePath(string imagePath)
+    {
+        // 在这里处理选择的图片路径
+        Debug.Log("Selected image path: " + imagePath);
+    }
+
+    void PickImageFromGallery()
+    {
+        using (AndroidJavaClass androidClass = new AndroidJavaClass("com.example.mylibrary.ImagePicker"))
+        {
+            using (AndroidJavaObject androidObject = androidClass.CallStatic<AndroidJavaObject>("getInstance"))
+            {
+                // 创建ImagePickerCallback实例
+                ImagePickerCallback callback = new ImagePickerCallback();
+
+                // 调用Android的选择相册图片的方法,并传递ImagePickerCallback实例
+                androidObject.Call("pickImageFromGallery", callback);
+            }
+        }
+    }
+}

+ 11 - 0
Assets/Remote3.1/Scripts/ImagePicker.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 832eea4ce57c45440b74f50bcf7d9a66
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 19 - 0
Assets/Remote3.1/Scripts/ImagePickerCallback.cs

@@ -0,0 +1,19 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+public class ImagePickerCallback : AndroidJavaProxy
+{
+    public ImagePickerCallback() : base("com.example.mylibrary.ImagePickerCallback")
+    {
+    }
+
+    // 接收从Android回调的结果的方法
+    public void OnImagePicked(string imagePath)
+    {
+        Debug.Log("Selected image path: " + imagePath);
+
+        // 在这里处理返回的图片路径
+        // ...
+    }
+}

+ 11 - 0
Assets/Remote3.1/Scripts/ImagePickerCallback.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 508bef74ecab1f44cb04a7796f5afe10
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1 - 1
Assets/Remote3.1/Scripts/LoginModule/LoginForms.cs

@@ -271,7 +271,7 @@ public class LoginForms : BaseUIForms
             UserInfo.userName = sData["data"]["nickName"].ToString();
             if (UserInfo.userName == "")
             {
-                UserInfo.userName = "YCKJ" + UserInfo.Account.Substring(UserInfo.Account.Length - 4);
+                UserInfo.userName = user;
             }
             UserInfo.activateType = int.Parse(sData["data"]["activateType"].ToString());
             UserInfo.indate = double.Parse(sData["data"]["indate"].ToString());

+ 13 - 0
Assets/Remote3.1/Scripts/MyObjectReceive.cs

@@ -0,0 +1,13 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+public class MyObjectReceive : MonoBehaviour
+{
+    // 在Android中选择图片后,Unity中调用该方法接收选择的图片路径
+    public void ReceiveImagePath(string imagePath)
+    {
+        // 在这里处理选择的图片路径
+        Debug.Log("Selected image path: " + imagePath);
+    }
+}

+ 11 - 0
Assets/Remote3.1/Scripts/MyObjectReceive.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7f5e98067492c3b47af7afd9b5a3a0c7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1 - 1
Assets/Remote3.1/Scripts/RoomScripts/JieTuScripts/jietu.cs

@@ -22,7 +22,7 @@ public class jietu : MonoBehaviour
 
     public void ClickJietu()
     {
-        if (screenShotCamera && RoomMainForms.Instance)
+        if (AgoraVideoAudioManager.Instance.isVideoStream&& screenShotCamera && RoomMainForms.Instance)
         {
             //Debug.Log("截屏");
             RoomMainForms.Instance.CkickOnJieTuBtn();

+ 43 - 18
Assets/Remote3.1/Scripts/RoomScripts/RoomMainForms.cs

@@ -58,9 +58,9 @@ public class RoomMainForms : BaseUIForms
         WSHandler.Rtc.onRtcState += onRtcState;
         WSHandler.Rtc.OnChangeOwner += ChangeOwner;
         guaDuanBtn.onClick.AddListener(ClickOnGuaDuan);
-        audioOpenBtn.onClick.AddListener(()=> { sendAudio(true); } );
-        audioCloseBtn.onClick.AddListener(()=> { sendAudio(false); });
-        videoOpenBtn.onClick.AddListener(()=> { sendVideo(true); });
+        audioOpenBtn.onClick.AddListener(() => { sendAudio(true); });
+        audioCloseBtn.onClick.AddListener(() => { sendAudio(false); });
+        videoOpenBtn.onClick.AddListener(() => { sendVideo(true); });
         videoCloseBtn.onClick.AddListener(() => { sendVideo(false); });
         openCoordinateBtn.onClick.AddListener(ClickOnOpenCoordinate);
         closeCoordinateBtn.onClick.AddListener(ClickOnCloseCoordinate);
@@ -77,10 +77,10 @@ public class RoomMainForms : BaseUIForms
     private void Start()
     {
         OpenBlack();
-       
+
     }
 
-    
+
     private void Update()
     {
         if (userId != NOUSER)
@@ -110,15 +110,12 @@ public class RoomMainForms : BaseUIForms
                 }
             }
         }
-        
+
     }
 
     private void OnEnable()
     {
         Screen.orientation = ScreenOrientation.LandscapeLeft;
-
-        sendAudio(UserInfo.systemDatas.Mic == 1 ? true : false);
-        sendVideo(UserInfo.systemDatas.Camera == 1 ? true : false);
     }
 
     private void OnDisable()
@@ -195,7 +192,7 @@ public class RoomMainForms : BaseUIForms
             RoomFile.Instance.Init();
         }
 
-        if (UserInfo.systemDatas.Camera==1)
+       // if (UserInfo.systemDatas == null|| UserInfo.systemDatas.Camera==1)
             StartCoroutine(ShowLoacalVideo(2f));
     }
 
@@ -401,18 +398,42 @@ public class RoomMainForms : BaseUIForms
 
     private void CallAndroidImage()
     {
-        if (test.Instance)
-        {
-            test.Instance.CallAndroidImage();
-        }
+        
+        NativeGallery.GetVideoFromGallery((string path) => {
+
+            Debug.Log("Video path: " + path);
+
+            test.path = path;
+            WSHandler.Rtc.uploadCert();
+            if (RoomMainForms.Instance)
+            {
+                RoomMainForms.Instance.ClickOnCanel();
+            }
+
+        });
+        //if (test.Instance)
+        //{
+        //    test.Instance.CallAndroidImage();
+        //}
     }
 
     private void CallAndroidVideo()
     {
-        if (test.Instance)
-        {
-            test.Instance.CallAndroidVideo();
-        }
+
+        NativeGallery.GetImageFromGallery((string path) => {
+            Debug.Log("Image path: " + path);
+
+            test.path = path;
+            WSHandler.Rtc.uploadCert();
+            if (RoomMainForms.Instance)
+            {
+                RoomMainForms.Instance.ClickOnCanel();
+            }
+        });
+        //if (test.Instance)
+        //{
+        //    test.Instance.CallAndroidVideo();
+        //}
     }
 
     private void ClickOnUpFileBtn()
@@ -605,6 +626,10 @@ public class RoomMainForms : BaseUIForms
 
     public void CkickOnJieTuBtn()
     {
+
+        if (!AgoraVideoAudioManager.Instance.isVideoStream)
+            return;
+
         AllNeedHideUI.SetActive(false);
         //if (isSwithVideo)
         //{

+ 45 - 10
Assets/Resources/UIPrefabs/RoomMainForms.prefab

@@ -2639,7 +2639,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0, y: 1}
   m_AnchorMax: {x: 1, y: 1}
-  m_AnchoredPosition: {x: 0, y: 0.0005127184}
+  m_AnchoredPosition: {x: 0, y: 0.00007435225}
   m_SizeDelta: {x: 0, y: 300}
   m_Pivot: {x: 0, y: 1}
 --- !u!114 &1159428447
@@ -4310,7 +4310,7 @@ MonoBehaviour:
   m_HandleRect: {fileID: 250507819}
   m_Direction: 2
   m_Value: 0
-  m_Size: 0.99999917
+  m_Size: 0.99761117
   m_NumberOfSteps: 0
   m_OnValueChanged:
     m_PersistentCalls:
@@ -6091,7 +6091,18 @@ MonoBehaviour:
   m_TargetGraphic: {fileID: 5787917090794896416}
   m_OnClick:
     m_PersistentCalls:
-      m_Calls: []
+      m_Calls:
+      - m_Target: {fileID: 849058004779364420}
+        m_MethodName: PickImage
+        m_Mode: 1
+        m_Arguments:
+          m_ObjectArgument: {fileID: 0}
+          m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
+          m_IntArgument: 0
+          m_FloatArgument: 0
+          m_StringArgument: 
+          m_BoolArgument: 0
+        m_CallState: 2
 --- !u!1 &706268797804826367
 GameObject:
   m_ObjectHideFlags: 0
@@ -6939,7 +6950,18 @@ MonoBehaviour:
   m_TargetGraphic: {fileID: 4001237122267784310}
   m_OnClick:
     m_PersistentCalls:
-      m_Calls: []
+      m_Calls:
+      - m_Target: {fileID: 849058004779364420}
+        m_MethodName: PickVideo
+        m_Mode: 1
+        m_Arguments:
+          m_ObjectArgument: {fileID: 0}
+          m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
+          m_IntArgument: 0
+          m_FloatArgument: 0
+          m_StringArgument: 
+          m_BoolArgument: 0
+        m_CallState: 2
 --- !u!1 &1278122743393311197
 GameObject:
   m_ObjectHideFlags: 0
@@ -15642,6 +15664,7 @@ GameObject:
   - component: {fileID: 3609531404925778822}
   - component: {fileID: 8713848220503307295}
   - component: {fileID: 7985396393475550817}
+  - component: {fileID: 849058004779364420}
   m_Layer: 5
   m_Name: Image
   m_TagString: Untagged
@@ -15707,6 +15730,18 @@ MonoBehaviour:
   m_FillOrigin: 0
   m_UseSpriteMesh: 0
   m_PixelsPerUnitMultiplier: 1
+--- !u!114 &849058004779364420
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6249952138896274766}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 832eea4ce57c45440b74f50bcf7d9a66, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
 --- !u!1 &6315192327196717489
 GameObject:
   m_ObjectHideFlags: 0
@@ -21599,15 +21634,15 @@ PrefabInstance:
       objectReference: {fileID: 0}
     m_RemovedComponents: []
   m_SourcePrefab: {fileID: 100100000, guid: aada762ac48142a4898ba48b91a0d7e6, type: 3}
---- !u!1 &4250395272880026558 stripped
-GameObject:
-  m_CorrespondingSourceObject: {fileID: 2799213986232774072, guid: aada762ac48142a4898ba48b91a0d7e6,
-    type: 3}
-  m_PrefabInstance: {fileID: 2027923513951051270}
-  m_PrefabAsset: {fileID: 0}
 --- !u!224 &4250395272880026559 stripped
 RectTransform:
   m_CorrespondingSourceObject: {fileID: 2799213986232774073, guid: aada762ac48142a4898ba48b91a0d7e6,
     type: 3}
   m_PrefabInstance: {fileID: 2027923513951051270}
   m_PrefabAsset: {fileID: 0}
+--- !u!1 &4250395272880026558 stripped
+GameObject:
+  m_CorrespondingSourceObject: {fileID: 2799213986232774072, guid: aada762ac48142a4898ba48b91a0d7e6,
+    type: 3}
+  m_PrefabInstance: {fileID: 2027923513951051270}
+  m_PrefabAsset: {fileID: 0}

+ 153 - 0
Assets/Scenes/Remote3.1Phone.unity

@@ -408,6 +408,102 @@ MonoBehaviour:
   m_Script: {fileID: 11500000, guid: 95bd5e73b2b930240adab20b3c9619d2, type: 3}
   m_Name: 
   m_EditorClassIdentifier: 
+--- !u!1 &579147770
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 579147773}
+  - component: {fileID: 579147772}
+  - component: {fileID: 579147771}
+  m_Layer: 0
+  m_Name: Reporter
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!114 &579147771
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 579147770}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 6767a180de870304caa2013b2772dd62, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+--- !u!114 &579147772
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 579147770}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 600c02144c4813244abd262cbcbe8825, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  show: 0
+  UserData: 
+  fps: 0
+  fpsText: 
+  images:
+    clearImage: {fileID: 2800000, guid: 112c6fcf56e349449ab2e6ad76b67816, type: 3}
+    collapseImage: {fileID: 2800000, guid: 4623f326a884a2546ab39078bf7822c3, type: 3}
+    clearOnNewSceneImage: {fileID: 2800000, guid: 3a6bc61a8319b1949ab9f1f2db1302b4,
+      type: 3}
+    showTimeImage: {fileID: 2800000, guid: 782e03669fa4a614e89ef56252134250, type: 3}
+    showSceneImage: {fileID: 2800000, guid: ff4dfb29f203a174ab8e4c498afe908a, type: 3}
+    userImage: {fileID: 2800000, guid: 2bcdc012e7356f1449ce7d3a31dc458c, type: 3}
+    showMemoryImage: {fileID: 2800000, guid: f447d62f2dacf9843be7cbf168a3a9d0, type: 3}
+    softwareImage: {fileID: 2800000, guid: 6c91fc88ee6c791468318d85febfb48d, type: 3}
+    dateImage: {fileID: 2800000, guid: a7561cd0a9f62a84e99bff1abce2a222, type: 3}
+    showFpsImage: {fileID: 2800000, guid: 90b2f48155dc0e74f8e428561ac79da5, type: 3}
+    infoImage: {fileID: 2800000, guid: 2954bef266e6d794aba08ceacc887a0f, type: 3}
+    saveLogsImage: {fileID: 2800000, guid: 4a4821d80a1fc774fb76eac6213544e7, type: 3}
+    searchImage: {fileID: 2800000, guid: bfef37b5a26d2264798616d960451329, type: 3}
+    copyImage: {fileID: 2800000, guid: 7f36d71624c630046b855909c497be17, type: 3}
+    closeImage: {fileID: 2800000, guid: b65e9be99974bc94eab5d6698811d0b8, type: 3}
+    buildFromImage: {fileID: 2800000, guid: 8702be598dd9f504ca33be2afee2ca33, type: 3}
+    systemInfoImage: {fileID: 2800000, guid: e9011b1dc9256ad4d9c19a31c595f95f, type: 3}
+    graphicsInfoImage: {fileID: 2800000, guid: 999d31716332cc04eb4abc9c9270b0ca, type: 3}
+    backImage: {fileID: 2800000, guid: a0632a18e7c665641b94fea66506ab50, type: 3}
+    logImage: {fileID: 2800000, guid: e876b803a4dd5c5488078071d15aa9c0, type: 3}
+    warningImage: {fileID: 2800000, guid: 1066be8e7b994b94c8a182b8dbe30705, type: 3}
+    errorImage: {fileID: 2800000, guid: 7640ebf8b3a92124d821d3b4b8b3fd7e, type: 3}
+    barImage: {fileID: 2800000, guid: 8128d4f4c0193e34586f9631ef7d4787, type: 3}
+    button_activeImage: {fileID: 2800000, guid: 2580a2e903691e44282e56ed6e0ff37a,
+      type: 3}
+    even_logImage: {fileID: 2800000, guid: d27aad55b568c6544b0b95a95da44bc7, type: 3}
+    odd_logImage: {fileID: 2800000, guid: 8ffbb44a2c3adae45913474e4fd487f5, type: 3}
+    selectedImage: {fileID: 2800000, guid: 17117a429b08e7e43b0b6c8421de69fe, type: 3}
+    reporterScrollerSkin: {fileID: 11400000, guid: 1cc68832d00d3284a9324a4dc05be753,
+      type: 2}
+  size: {x: 32, y: 32}
+  maxSize: 20
+  numOfCircleToShow: 1
+  Initialized: 0
+--- !u!4 &579147773
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 579147770}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 6
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
 --- !u!1 &705507993
 GameObject:
   m_ObjectHideFlags: 0
@@ -948,6 +1044,62 @@ CanvasRenderer:
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 1693531575}
   m_CullTransparentMesh: 0
+--- !u!1 &1698252113
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1698252115}
+  - component: {fileID: 1698252114}
+  - component: {fileID: 1698252116}
+  m_Layer: 0
+  m_Name: MyObjectReceive
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!114 &1698252114
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1698252113}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 7f5e98067492c3b47af7afd9b5a3a0c7, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+--- !u!4 &1698252115
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1698252113}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 7
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1698252116
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1698252113}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 10ac22f354fc7354d9fe3b512a543aaf, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
 --- !u!1 &1967058811
 GameObject:
   m_ObjectHideFlags: 0
@@ -1002,3 +1154,4 @@ MonoBehaviour:
   isSwitchCamera: 0
   mainViewPeerId: 
   bg: {fileID: 2800000, guid: 895b9c4a81b23f549b0ae6f7f5d5521d, type: 3}
+  isVideoStream: 0

+ 1 - 1
Assets/StreamingAssets/build_info

@@ -1 +1 @@
-Build from SK-20211220VCWK at 2023/5/9 10:46:21
+Build from SK-20211220VCWK at 2023/6/5 10:16:21

File diff suppressed because it is too large
+ 24 - 0
NativeGallery.Editor.csproj


File diff suppressed because it is too large
+ 24 - 0
NativeGallery.Runtime.csproj


+ 12 - 0
remote3.1phone-master.sln

@@ -3,8 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 15
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{D6AED37D-DA18-7417-D784-EDDC9F5254A8}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeGallery.Runtime", "NativeGallery.Runtime.csproj", "{990D00D0-AB9B-6D36-B860-7CECC3AB4587}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{860BF539-C297-2098-CB07-AA337BBCFF2F}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeGallery.Editor", "NativeGallery.Editor.csproj", "{864FE903-2567-3A29-0375-7439C64EE485}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -15,10 +19,18 @@ Global
 		{D6AED37D-DA18-7417-D784-EDDC9F5254A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{D6AED37D-DA18-7417-D784-EDDC9F5254A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D6AED37D-DA18-7417-D784-EDDC9F5254A8}.Release|Any CPU.Build.0 = Release|Any CPU
+		{990D00D0-AB9B-6D36-B860-7CECC3AB4587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{990D00D0-AB9B-6D36-B860-7CECC3AB4587}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{990D00D0-AB9B-6D36-B860-7CECC3AB4587}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{990D00D0-AB9B-6D36-B860-7CECC3AB4587}.Release|Any CPU.Build.0 = Release|Any CPU
 		{860BF539-C297-2098-CB07-AA337BBCFF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{860BF539-C297-2098-CB07-AA337BBCFF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{860BF539-C297-2098-CB07-AA337BBCFF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{860BF539-C297-2098-CB07-AA337BBCFF2F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{864FE903-2567-3A29-0375-7439C64EE485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{864FE903-2567-3A29-0375-7439C64EE485}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{864FE903-2567-3A29-0375-7439C64EE485}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{864FE903-2567-3A29-0375-7439C64EE485}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

Some files were not shown because too many files changed in this diff