├── .github ├── AAR Source (Android) │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── yasirkula │ │ │ └── unity │ │ │ ├── NativeCamera.java │ │ │ ├── NativeCameraContentProvider.java │ │ │ ├── NativeCameraMediaReceiver.java │ │ │ ├── NativeCameraPermissionFragment.java │ │ │ ├── NativeCameraPermissionReceiver.java │ │ │ ├── NativeCameraPictureFragment.java │ │ │ ├── NativeCameraUtils.java │ │ │ └── NativeCameraVideoFragment.java │ └── proguard.txt ├── README.md └── screenshots │ ├── 1.png │ ├── 2.png │ └── AndroidManifest.png ├── LICENSE.txt ├── LICENSE.txt.meta ├── Plugins.meta ├── Plugins ├── NativeCamera.meta └── NativeCamera │ ├── Android.meta │ ├── Android │ ├── NCCallbackHelper.cs │ ├── NCCallbackHelper.cs.meta │ ├── NCCameraCallbackAndroid.cs │ ├── NCCameraCallbackAndroid.cs.meta │ ├── NCPermissionCallbackAndroid.cs │ ├── NCPermissionCallbackAndroid.cs.meta │ ├── NativeCamera.aar │ └── NativeCamera.aar.meta │ ├── Editor.meta │ ├── Editor │ ├── NCPostProcessBuild.cs │ ├── NCPostProcessBuild.cs.meta │ ├── NativeCamera.Editor.asmdef │ └── NativeCamera.Editor.asmdef.meta │ ├── NativeCamera.Runtime.asmdef │ ├── NativeCamera.Runtime.asmdef.meta │ ├── NativeCamera.cs │ ├── NativeCamera.cs.meta │ ├── README.txt │ ├── README.txt.meta │ ├── iOS.meta │ └── iOS │ ├── NCCameraCallbackiOS.cs │ ├── NCCameraCallbackiOS.cs.meta │ ├── NCPermissionCallbackiOS.cs │ ├── NCPermissionCallbackiOS.cs.meta │ ├── NativeCamera.mm │ └── NativeCamera.mm.meta ├── package.json └── package.json.meta /.github/AAR Source (Android)/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCamera.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.app.Activity; 6 | import android.app.Fragment; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.net.Uri; 11 | import android.os.Build; 12 | import android.os.Bundle; 13 | import android.provider.Settings; 14 | import android.util.Log; 15 | 16 | /** 17 | * Created by yasirkula on 22.04.2018. 18 | */ 19 | 20 | public class NativeCamera 21 | { 22 | public static boolean KeepGalleryReferences = false; // false: if camera app saves a copy of the image/video in Gallery, automatically delete it 23 | public static boolean QuickCapture = true; // true: the Confirm/Delete screen after the capture is skipped 24 | public static boolean UseDefaultCameraApp = true; // false: Intent.createChooser is used to pick the camera app 25 | public static boolean PermissionFreeMode = false; // true: Permissions for reading/writing media elements won't be requested. It might cause undesired side effects like a copy of the captured image/video being saved to Gallery or the captured image having a very low resolution 26 | 27 | public static boolean HasCamera( Context context ) 28 | { 29 | PackageManager pm = context.getPackageManager(); 30 | return pm.hasSystemFeature( PackageManager.FEATURE_CAMERA ) || pm.hasSystemFeature( PackageManager.FEATURE_CAMERA_FRONT ); 31 | } 32 | 33 | public static void TakePicture( Context context, NativeCameraMediaReceiver mediaReceiver, int defaultCamera ) 34 | { 35 | if( !CanAccessCamera( context, mediaReceiver, true ) ) 36 | return; 37 | 38 | Bundle bundle = new Bundle(); 39 | bundle.putInt( NativeCameraPictureFragment.DEFAULT_CAMERA_ID, defaultCamera ); 40 | bundle.putString( NativeCameraPictureFragment.AUTHORITY_ID, NativeCameraUtils.GetAuthority( context ) ); 41 | 42 | final Fragment request = new NativeCameraPictureFragment( mediaReceiver ); 43 | request.setArguments( bundle ); 44 | 45 | ( (Activity) context ).getFragmentManager().beginTransaction().add( 0, request ).commitAllowingStateLoss(); 46 | } 47 | 48 | public static void RecordVideo( Context context, NativeCameraMediaReceiver mediaReceiver, int defaultCamera, int quality, int maxDuration, long maxSize ) 49 | { 50 | if( !CanAccessCamera( context, mediaReceiver, false ) ) 51 | return; 52 | 53 | Bundle bundle = new Bundle(); 54 | bundle.putInt( NativeCameraVideoFragment.DEFAULT_CAMERA_ID, defaultCamera ); 55 | bundle.putString( NativeCameraVideoFragment.AUTHORITY_ID, NativeCameraUtils.GetAuthority( context ) ); 56 | bundle.putInt( NativeCameraVideoFragment.QUALITY_ID, quality ); 57 | bundle.putInt( NativeCameraVideoFragment.MAX_DURATION_ID, maxDuration ); 58 | bundle.putLong( NativeCameraVideoFragment.MAX_SIZE_ID, maxSize ); 59 | 60 | final Fragment request = new NativeCameraVideoFragment( mediaReceiver ); 61 | request.setArguments( bundle ); 62 | 63 | ( (Activity) context ).getFragmentManager().beginTransaction().add( 0, request ).commitAllowingStateLoss(); 64 | } 65 | 66 | // Credit: https://stackoverflow.com/a/35456817/2373034 67 | public static void OpenSettings( Context context ) 68 | { 69 | Uri uri = Uri.fromParts( "package", context.getPackageName(), null ); 70 | 71 | Intent intent = new Intent(); 72 | intent.setAction( Settings.ACTION_APPLICATION_DETAILS_SETTINGS ); 73 | intent.setData( uri ); 74 | 75 | context.startActivity( intent ); 76 | } 77 | 78 | @TargetApi( Build.VERSION_CODES.M ) 79 | public static int CheckPermission( Context context, final boolean isPicturePermission ) 80 | { 81 | if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M ) 82 | return 1; 83 | 84 | if( !PermissionFreeMode ) 85 | { 86 | if( Build.VERSION.SDK_INT < 30 && context.checkSelfPermission( Manifest.permission.WRITE_EXTERNAL_STORAGE ) != PackageManager.PERMISSION_GRANTED ) 87 | return 0; 88 | 89 | if( Build.VERSION.SDK_INT < 33 || context.getApplicationInfo().targetSdkVersion < 33 ) 90 | { 91 | if( context.checkSelfPermission( Manifest.permission.READ_EXTERNAL_STORAGE ) != PackageManager.PERMISSION_GRANTED ) 92 | return 0; 93 | } 94 | } 95 | 96 | // If CAMERA permission is declared, we must request it: https://developer.android.com/reference/android/provider/MediaStore#ACTION_IMAGE_CAPTURE 97 | if( NativeCameraUtils.IsPermissionDefinedInManifest( context, Manifest.permission.CAMERA ) && context.checkSelfPermission( Manifest.permission.CAMERA ) != PackageManager.PERMISSION_GRANTED ) 98 | return 0; 99 | 100 | return 1; 101 | } 102 | 103 | // Credit: https://github.com/Over17/UnityAndroidPermissions/blob/0dca33e40628f1f279decb67d901fd444b409cd7/src/UnityAndroidPermissions/src/main/java/com/unity3d/plugin/UnityAndroidPermissions.java 104 | public static void RequestPermission( Context context, final NativeCameraPermissionReceiver permissionReceiver, final boolean isPicturePermission ) 105 | { 106 | if( CheckPermission( context, isPicturePermission ) == 1 ) 107 | { 108 | permissionReceiver.OnPermissionResult( 1 ); 109 | return; 110 | } 111 | 112 | Bundle bundle = new Bundle(); 113 | bundle.putBoolean( NativeCameraPermissionFragment.PICTURE_PERMISSION_ID, isPicturePermission ); 114 | 115 | final Fragment request = new NativeCameraPermissionFragment( permissionReceiver ); 116 | request.setArguments( bundle ); 117 | 118 | ( (Activity) context ).getFragmentManager().beginTransaction().add( 0, request ).commitAllowingStateLoss(); 119 | } 120 | 121 | public static String LoadImageAtPath( Context context, String path, final String temporaryFilePath, final int maxSize ) 122 | { 123 | return NativeCameraUtils.LoadImageAtPath( context, path, temporaryFilePath, maxSize ); 124 | } 125 | 126 | public static String GetImageProperties( Context context, final String path ) 127 | { 128 | return NativeCameraUtils.GetImageProperties( context, path ); 129 | } 130 | 131 | public static String GetVideoProperties( Context context, final String path ) 132 | { 133 | return NativeCameraUtils.GetVideoProperties( context, path ); 134 | } 135 | 136 | public static String GetVideoThumbnail( Context context, final String path, final String savePath, final boolean saveAsJpeg, final int maxSize, final double captureTime ) 137 | { 138 | return NativeCameraUtils.GetVideoThumbnail( context, path, savePath, saveAsJpeg, maxSize, captureTime ); 139 | } 140 | 141 | private static boolean CanAccessCamera( Context context, NativeCameraMediaReceiver mediaReceiver, final boolean isPictureMode ) 142 | { 143 | if( !HasCamera( context ) ) 144 | { 145 | Log.e( "Unity", "Device has no registered cameras!" ); 146 | 147 | mediaReceiver.OnMediaReceived( "" ); 148 | return false; 149 | } 150 | 151 | if( CheckPermission( context, isPictureMode ) != 1 ) 152 | { 153 | Log.e( "Unity", "Can't access camera, permission denied!" ); 154 | 155 | mediaReceiver.OnMediaReceived( "" ); 156 | return false; 157 | } 158 | 159 | if( NativeCameraUtils.GetAuthority( context ) == null ) 160 | { 161 | Log.e( "Unity", "Can't find ContentProvider, camera is inaccessible!" ); 162 | 163 | mediaReceiver.OnMediaReceived( "" ); 164 | return false; 165 | } 166 | 167 | return true; 168 | } 169 | } -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraContentProvider.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.content.pm.ProviderInfo; 8 | import android.content.res.XmlResourceParser; 9 | import android.database.Cursor; 10 | import android.database.MatrixCursor; 11 | import android.net.Uri; 12 | import android.os.ParcelFileDescriptor; 13 | import android.provider.OpenableColumns; 14 | import android.text.TextUtils; 15 | import android.webkit.MimeTypeMap; 16 | 17 | import org.xmlpull.v1.XmlPullParserException; 18 | 19 | import java.io.File; 20 | import java.io.FileNotFoundException; 21 | import java.io.IOException; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | /* 26 | * Copyright (C) 2013 The Android Open Source Project 27 | * 28 | * Licensed under the Apache License, Version 2.0 (the "License"); 29 | * you may not use this file except in compliance with the License. 30 | * You may obtain a copy of the License at 31 | * 32 | * http://www.apache.org/licenses/LICENSE-2.0 33 | * 34 | * Unless required by applicable law or agreed to in writing, software 35 | * distributed under the License is distributed on an "AS IS" BASIS, 36 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | * See the License for the specific language governing permissions and 38 | * limitations under the License. 39 | */ 40 | 41 | /** 42 | * Edited by yasirkula on 22.06.2017. 43 | */ 44 | 45 | public class NativeCameraContentProvider extends ContentProvider 46 | { 47 | private static final String[] COLUMNS = { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; 48 | private static final String META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; 49 | private static final String TAG_ROOT_PATH = "root-path"; 50 | private static final String TAG_FILES_PATH = "files-path"; 51 | private static final String TAG_CACHE_PATH = "cache-path"; 52 | private static final String TAG_EXTERNAL = "external-path"; 53 | private static final String ATTR_NAME = "name"; 54 | private static final String ATTR_PATH = "path"; 55 | private static final File DEVICE_ROOT = new File("/"); 56 | // @GuardedBy("sCache") 57 | private static HashMap sCache = new HashMap(); 58 | private PathStrategy mStrategy; 59 | /** 60 | * The default FileProvider implementation does not need to be initialized. If you want to 61 | * override this method, you must provide your own subclass of FileProvider. 62 | */ 63 | @Override 64 | public boolean onCreate() { 65 | return true; 66 | } 67 | /** 68 | * After the FileProvider is instantiated, this method is called to provide the system with 69 | * information about the provider. 70 | * 71 | * @param context A {@link Context} for the current component. 72 | * @param info A {@link ProviderInfo} for the new provider. 73 | */ 74 | @Override 75 | public void attachInfo(Context context, ProviderInfo info) { 76 | super.attachInfo(context, info); 77 | // Sanity check our security 78 | if (info.exported) { 79 | throw new SecurityException("Provider must not be exported"); 80 | } 81 | if (!info.grantUriPermissions) { 82 | throw new SecurityException("Provider must grant uri permissions"); 83 | } 84 | mStrategy = getPathStrategy(context, info.authority); 85 | } 86 | 87 | public static Uri getUriForFile(Context context, String authority, File file) { 88 | final PathStrategy strategy = getPathStrategy(context, authority); 89 | return strategy.getUriForFile(file); 90 | } 91 | /** 92 | * Use a content URI returned by 93 | * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file 94 | * managed by the FileProvider. 95 | * FileProvider reports the column names defined in {@link OpenableColumns}: 96 | * 100 | * For more information, see 101 | * {@link ContentProvider#query(Uri, String[], String, String[], String) 102 | * ContentProvider.query()}. 103 | * 104 | * @param uri A content URI returned by {@link #getUriForFile}. 105 | * @param projection The list of columns to put into the {@link Cursor}. If null all columns are 106 | * included. 107 | * @param selection Selection criteria to apply. If null then all data that matches the content 108 | * URI is returned. 109 | * @param selectionArgs An array of {@link String}, containing arguments to bind to 110 | * the selection parameter. The query method scans selection from left to 111 | * right and iterates through selectionArgs, replacing the current "?" character in 112 | * selection with the value at the current position in selectionArgs. The 113 | * values are bound to selection as {@link String} values. 114 | * @param sortOrder A {@link String} containing the column name(s) on which to sort 115 | * the resulting {@link Cursor}. 116 | * @return A {@link Cursor} containing the results of the query. 117 | * 118 | */ 119 | @Override 120 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 121 | String sortOrder) { 122 | // ContentProvider has already checked granted permissions 123 | final File file = mStrategy.getFileForUri(uri); 124 | if (projection == null) { 125 | projection = COLUMNS; 126 | } 127 | String[] cols = new String[projection.length]; 128 | Object[] values = new Object[projection.length]; 129 | int i = 0; 130 | for (String col : projection) { 131 | if (OpenableColumns.DISPLAY_NAME.equals(col)) { 132 | cols[i] = OpenableColumns.DISPLAY_NAME; 133 | values[i++] = file.getName(); 134 | } else if (OpenableColumns.SIZE.equals(col)) { 135 | cols[i] = OpenableColumns.SIZE; 136 | values[i++] = file.length(); 137 | } 138 | } 139 | cols = copyOf(cols, i); 140 | values = copyOf(values, i); 141 | final MatrixCursor cursor = new MatrixCursor(cols, 1); 142 | cursor.addRow(values); 143 | return cursor; 144 | } 145 | /** 146 | * Returns the MIME type of a content URI returned by 147 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 148 | * 149 | * @param uri A content URI returned by 150 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 151 | * @return If the associated file has an extension, the MIME type associated with that 152 | * extension; otherwise application/octet-stream. 153 | */ 154 | @Override 155 | public String getType(Uri uri) { 156 | // ContentProvider has already checked granted permissions 157 | final File file = mStrategy.getFileForUri(uri); 158 | final int lastDot = file.getName().lastIndexOf('.'); 159 | if (lastDot >= 0) { 160 | final String extension = file.getName().substring(lastDot + 1); 161 | final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 162 | if (mime != null) { 163 | return mime; 164 | } 165 | } 166 | return "application/octet-stream"; 167 | } 168 | /** 169 | * By default, this method throws an {@link UnsupportedOperationException}. You must 170 | * subclass FileProvider if you want to provide different functionality. 171 | */ 172 | @Override 173 | public Uri insert(Uri uri, ContentValues values) { 174 | throw new UnsupportedOperationException("No external inserts"); 175 | } 176 | /** 177 | * By default, this method throws an {@link UnsupportedOperationException}. You must 178 | * subclass FileProvider if you want to provide different functionality. 179 | */ 180 | @Override 181 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 182 | throw new UnsupportedOperationException("No external updates"); 183 | } 184 | /** 185 | * Deletes the file associated with the specified content URI, as 186 | * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this 187 | * method does not throw an {@link IOException}; you must check its return value. 188 | * 189 | * @param uri A content URI for a file, as returned by 190 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 191 | * @param selection Ignored. Set to {@code null}. 192 | * @param selectionArgs Ignored. Set to {@code null}. 193 | * @return 1 if the delete succeeds; otherwise, 0. 194 | */ 195 | @Override 196 | public int delete(Uri uri, String selection, String[] selectionArgs) { 197 | // ContentProvider has already checked granted permissions 198 | final File file = mStrategy.getFileForUri(uri); 199 | return file.delete() ? 1 : 0; 200 | } 201 | /** 202 | * By default, FileProvider automatically returns the 203 | * {@link ParcelFileDescriptor} for a file associated with a content:// 204 | * {@link Uri}. To get the {@link ParcelFileDescriptor}, call 205 | * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) 206 | * ContentResolver.openFileDescriptor}. 207 | * 208 | * To override this method, you must provide your own subclass of FileProvider. 209 | * 210 | * @param uri A content URI associated with a file, as returned by 211 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 212 | * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and 213 | * write access, or "rwt" for read and write access that truncates any existing file. 214 | * @return A new {@link ParcelFileDescriptor} with which you can access the file. 215 | */ 216 | @Override 217 | public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 218 | // ContentProvider has already checked granted permissions 219 | final File file = mStrategy.getFileForUri(uri); 220 | final int fileMode = modeToMode(mode); 221 | return ParcelFileDescriptor.open(file, fileMode); 222 | } 223 | /** 224 | * Return {@link PathStrategy} for given authority, either by parsing or 225 | * returning from cache. 226 | */ 227 | private static PathStrategy getPathStrategy(Context context, String authority) { 228 | PathStrategy strat; 229 | synchronized (sCache) { 230 | strat = sCache.get(authority); 231 | if (strat == null) { 232 | try { 233 | strat = parsePathStrategy(context, authority); 234 | } catch (IOException e) { 235 | throw new IllegalArgumentException( 236 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); 237 | } catch (XmlPullParserException e) { 238 | throw new IllegalArgumentException( 239 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); 240 | } 241 | sCache.put(authority, strat); 242 | } 243 | } 244 | return strat; 245 | } 246 | /** 247 | * Parse and return {@link PathStrategy} for given authority as defined in 248 | * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}. 249 | * 250 | * @see #getPathStrategy(Context, String) 251 | */ 252 | private static PathStrategy parsePathStrategy(Context context, String authority) 253 | throws IOException, XmlPullParserException { 254 | final SimplePathStrategy strat = new SimplePathStrategy(authority); 255 | final ProviderInfo info = context.getPackageManager() 256 | .resolveContentProvider(authority, PackageManager.GET_META_DATA); 257 | final XmlResourceParser in = info.loadXmlMetaData( 258 | context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS); 259 | //if (in == null) { 260 | // throw new IllegalArgumentException( 261 | // "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data"); 262 | //} 263 | 264 | File target = null; 265 | target = buildPath(DEVICE_ROOT, "."); 266 | if (target != null) { 267 | strat.addRoot("devroot", target); 268 | } 269 | 270 | return strat; 271 | } 272 | /** 273 | * Strategy for mapping between {@link File} and {@link Uri}. 274 | *

275 | * Strategies must be symmetric so that mapping a {@link File} to a 276 | * {@link Uri} and then back to a {@link File} points at the original 277 | * target. 278 | *

279 | * Strategies must remain consistent across app launches, and not rely on 280 | * dynamic state. This ensures that any generated {@link Uri} can still be 281 | * resolved if your process is killed and later restarted. 282 | * 283 | * @see SimplePathStrategy 284 | */ 285 | interface PathStrategy { 286 | /** 287 | * Return a {@link Uri} that represents the given {@link File}. 288 | */ 289 | public Uri getUriForFile( File file ); 290 | /** 291 | * Return a {@link File} that represents the given {@link Uri}. 292 | */ 293 | public File getFileForUri( Uri uri ); 294 | } 295 | /** 296 | * Strategy that provides access to files living under a narrow whitelist of 297 | * filesystem roots. It will throw {@link SecurityException} if callers try 298 | * accessing files outside the configured roots. 299 | *

300 | * For example, if configured with 301 | * {@code addRoot("myfiles", context.getFilesDir())}, then 302 | * {@code context.getFileStreamPath("foo.txt")} would map to 303 | * {@code content://myauthority/myfiles/foo.txt}. 304 | */ 305 | static class SimplePathStrategy implements PathStrategy { 306 | private final String mAuthority; 307 | private final HashMap mRoots = new HashMap(); 308 | public SimplePathStrategy(String authority) { 309 | mAuthority = authority; 310 | } 311 | /** 312 | * Add a mapping from a name to a filesystem root. The provider only offers 313 | * access to files that live under configured roots. 314 | */ 315 | public void addRoot(String name, File root) { 316 | if (TextUtils.isEmpty(name)) { 317 | throw new IllegalArgumentException("Name must not be empty"); 318 | } 319 | try { 320 | // Resolve to canonical path to keep path checking fast 321 | root = root.getCanonicalFile(); 322 | } catch (IOException e) { 323 | throw new IllegalArgumentException( 324 | "Failed to resolve canonical path for " + root, e); 325 | } 326 | mRoots.put(name, root); 327 | } 328 | @Override 329 | public Uri getUriForFile(File file) { 330 | String path; 331 | try { 332 | path = file.getCanonicalPath(); 333 | } catch (IOException e) { 334 | throw new IllegalArgumentException("Failed to resolve canonical path for " + file); 335 | } 336 | // Find the most-specific root path 337 | Map.Entry mostSpecific = null; 338 | for (Map.Entry root : mRoots.entrySet()) { 339 | final String rootPath = root.getValue().getPath(); 340 | if (path.startsWith(rootPath) && (mostSpecific == null 341 | || rootPath.length() > mostSpecific.getValue().getPath().length())) { 342 | mostSpecific = root; 343 | } 344 | } 345 | if (mostSpecific == null) { 346 | throw new IllegalArgumentException( 347 | "Failed to find configured root that contains " + path); 348 | } 349 | // Start at first char of path under root 350 | final String rootPath = mostSpecific.getValue().getPath(); 351 | if (rootPath.endsWith("/")) { 352 | path = path.substring(rootPath.length()); 353 | } else { 354 | path = path.substring(rootPath.length() + 1); 355 | } 356 | // Encode the tag and path separately 357 | path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/"); 358 | return new Uri.Builder().scheme("content") 359 | .authority(mAuthority).encodedPath(path).build(); 360 | } 361 | @Override 362 | public File getFileForUri(Uri uri) { 363 | String path = uri.getEncodedPath(); 364 | final int splitIndex = path.indexOf('/', 1); 365 | final String tag = Uri.decode(path.substring(1, splitIndex)); 366 | path = Uri.decode(path.substring(splitIndex + 1)); 367 | final File root = mRoots.get(tag); 368 | if (root == null) { 369 | throw new IllegalArgumentException("Unable to find configured root for " + uri ); 370 | } 371 | File file = new File(root, path); 372 | try { 373 | file = file.getCanonicalFile(); 374 | } catch (IOException e) { 375 | throw new IllegalArgumentException("Failed to resolve canonical path for " + file); 376 | } 377 | if (!file.getPath().startsWith(root.getPath())) { 378 | throw new SecurityException("Resolved path jumped beyond configured root"); 379 | } 380 | return file; 381 | } 382 | } 383 | /** 384 | * Copied from ContentResolver.java 385 | */ 386 | private static int modeToMode(String mode) { 387 | int modeBits; 388 | if ("r".equals(mode)) { 389 | modeBits = ParcelFileDescriptor.MODE_READ_ONLY; 390 | } else if ("w".equals(mode) || "wt".equals(mode)) { 391 | modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY 392 | | ParcelFileDescriptor.MODE_CREATE 393 | | ParcelFileDescriptor.MODE_TRUNCATE; 394 | } else if ("wa".equals(mode)) { 395 | modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY 396 | | ParcelFileDescriptor.MODE_CREATE 397 | | ParcelFileDescriptor.MODE_APPEND; 398 | } else if ("rw".equals(mode)) { 399 | modeBits = ParcelFileDescriptor.MODE_READ_WRITE 400 | | ParcelFileDescriptor.MODE_CREATE; 401 | } else if ("rwt".equals(mode)) { 402 | modeBits = ParcelFileDescriptor.MODE_READ_WRITE 403 | | ParcelFileDescriptor.MODE_CREATE 404 | | ParcelFileDescriptor.MODE_TRUNCATE; 405 | } else { 406 | throw new IllegalArgumentException("Invalid mode: " + mode); 407 | } 408 | return modeBits; 409 | } 410 | private static File buildPath(File base, String... segments) { 411 | File cur = base; 412 | for (String segment : segments) { 413 | if (segment != null) { 414 | cur = new File(cur, segment); 415 | } 416 | } 417 | return cur; 418 | } 419 | private static String[] copyOf(String[] original, int newLength) { 420 | final String[] result = new String[newLength]; 421 | System.arraycopy(original, 0, result, 0, newLength); 422 | return result; 423 | } 424 | private static Object[] copyOf(Object[] original, int newLength) { 425 | final Object[] result = new Object[newLength]; 426 | System.arraycopy(original, 0, result, 0, newLength); 427 | return result; 428 | } 429 | } -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraMediaReceiver.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | /** 4 | * Created by yasirkula on 22.04.2018. 5 | */ 6 | 7 | public interface NativeCameraMediaReceiver 8 | { 9 | void OnMediaReceived( String path ); 10 | } 11 | -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraPermissionFragment.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | // Original work Copyright (c) 2017 Yury Habets 4 | // Modified work Copyright 2018 yasirkula 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import android.Manifest; 25 | import android.annotation.TargetApi; 26 | import android.app.Fragment; 27 | import android.content.Intent; 28 | import android.content.pm.PackageManager; 29 | import android.os.Build; 30 | import android.os.Bundle; 31 | import android.util.Log; 32 | 33 | import java.util.ArrayList; 34 | 35 | @TargetApi( Build.VERSION_CODES.M ) 36 | public class NativeCameraPermissionFragment extends Fragment 37 | { 38 | public static final String PICTURE_PERMISSION_ID = "NC_PictureMode"; 39 | private static final int PERMISSIONS_REQUEST_CODE = 120645; 40 | 41 | private final NativeCameraPermissionReceiver permissionReceiver; 42 | 43 | public NativeCameraPermissionFragment() 44 | { 45 | permissionReceiver = null; 46 | } 47 | 48 | public NativeCameraPermissionFragment( final NativeCameraPermissionReceiver permissionReceiver ) 49 | { 50 | this.permissionReceiver = permissionReceiver; 51 | } 52 | 53 | @Override 54 | public void onCreate( Bundle savedInstanceState ) 55 | { 56 | super.onCreate( savedInstanceState ); 57 | if( permissionReceiver == null ) 58 | onRequestPermissionsResult( PERMISSIONS_REQUEST_CODE, new String[0], new int[0] ); 59 | else 60 | { 61 | ArrayList permissions = new ArrayList( 3 ); 62 | if( NativeCameraUtils.IsPermissionDefinedInManifest( getActivity(), Manifest.permission.CAMERA ) ) 63 | permissions.add( Manifest.permission.CAMERA ); 64 | 65 | if( Build.VERSION.SDK_INT < 30 ) 66 | permissions.add( Manifest.permission.WRITE_EXTERNAL_STORAGE ); 67 | 68 | if( Build.VERSION.SDK_INT < 33 || getActivity().getApplicationInfo().targetSdkVersion < 33 ) 69 | permissions.add( Manifest.permission.READ_EXTERNAL_STORAGE ); 70 | 71 | if( permissions.size() > 0 ) 72 | { 73 | String[] permissionsArray = new String[permissions.size()]; 74 | permissions.toArray( permissionsArray ); 75 | 76 | requestPermissions( permissionsArray, PERMISSIONS_REQUEST_CODE ); 77 | } 78 | else 79 | onRequestPermissionsResult( PERMISSIONS_REQUEST_CODE, new String[0], new int[0] ); 80 | } 81 | } 82 | 83 | @Override 84 | public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults ) 85 | { 86 | if( requestCode != PERMISSIONS_REQUEST_CODE ) 87 | return; 88 | 89 | if( permissionReceiver == null ) 90 | { 91 | Log.e( "Unity", "Fragment data got reset while asking permissions!" ); 92 | 93 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss(); 94 | return; 95 | } 96 | 97 | // 0 -> denied, must go to settings 98 | // 1 -> granted 99 | // 2 -> denied, can ask again 100 | int result = 1; 101 | if( permissions.length == 0 || grantResults.length == 0 ) 102 | result = 2; 103 | else 104 | { 105 | for( int i = 0; i < permissions.length && i < grantResults.length; ++i ) 106 | { 107 | if( grantResults[i] == PackageManager.PERMISSION_DENIED ) 108 | { 109 | if( !shouldShowRequestPermissionRationale( permissions[i] ) ) 110 | { 111 | result = 0; 112 | break; 113 | } 114 | 115 | result = 2; 116 | } 117 | } 118 | } 119 | 120 | permissionReceiver.OnPermissionResult( result ); 121 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss(); 122 | 123 | // Resolves a bug in Unity 2019 where the calling activity 124 | // doesn't resume automatically after the fragment finishes 125 | // Credit: https://stackoverflow.com/a/12409215/2373034 126 | try 127 | { 128 | Intent resumeUnityActivity = new Intent( getActivity(), getActivity().getClass() ); 129 | resumeUnityActivity.setFlags( Intent.FLAG_ACTIVITY_REORDER_TO_FRONT ); 130 | getActivity().startActivityIfNeeded( resumeUnityActivity, 0 ); 131 | } 132 | catch( Exception e ) 133 | { 134 | Log.e( "Unity", "Exception (resume):", e ); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraPermissionReceiver.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | /** 4 | * Created by yasirkula on 5.03.2018. 5 | */ 6 | 7 | public interface NativeCameraPermissionReceiver 8 | { 9 | void OnPermissionResult( int result ); 10 | } 11 | -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraPictureFragment.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.ActivityNotFoundException; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.database.Cursor; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.provider.MediaStore; 13 | import android.util.Log; 14 | import android.widget.Toast; 15 | 16 | import java.io.File; 17 | 18 | /** 19 | * Created by yasirkula on 22.04.2018. 20 | */ 21 | 22 | public class NativeCameraPictureFragment extends Fragment 23 | { 24 | private static final int CAMERA_PICTURE_CODE = 554776; 25 | 26 | private static final String IMAGE_NAME = "IMG_camera.jpg"; 27 | public static final String DEFAULT_CAMERA_ID = "UNCP_DEF_CAMERA"; 28 | public static final String AUTHORITY_ID = "UNCP_AUTHORITY"; 29 | 30 | private final NativeCameraMediaReceiver mediaReceiver; 31 | private String fileTargetPath; 32 | private int lastImageId = Integer.MAX_VALUE; 33 | 34 | public NativeCameraPictureFragment() 35 | { 36 | mediaReceiver = null; 37 | } 38 | 39 | public NativeCameraPictureFragment( final NativeCameraMediaReceiver mediaReceiver ) 40 | { 41 | this.mediaReceiver = mediaReceiver; 42 | } 43 | 44 | @Override 45 | public void onCreate( Bundle savedInstanceState ) 46 | { 47 | super.onCreate( savedInstanceState ); 48 | if( mediaReceiver == null ) 49 | { 50 | Log.e( "Unity", "NativeCameraPictureFragment.mediaReceiver became null in onCreate!" ); 51 | onActivityResult( CAMERA_PICTURE_CODE, Activity.RESULT_CANCELED, null ); 52 | } 53 | else 54 | { 55 | int defaultCamera = getArguments().getInt( DEFAULT_CAMERA_ID ); 56 | String authority = getArguments().getString( AUTHORITY_ID ); 57 | 58 | File photoFile = new File( getActivity().getCacheDir(), IMAGE_NAME ); 59 | try 60 | { 61 | if( photoFile.exists() ) 62 | NativeCameraUtils.ClearFileContents( photoFile ); 63 | else 64 | photoFile.createNewFile(); 65 | } 66 | catch( Exception e ) 67 | { 68 | Log.e( "Unity", "Exception:", e ); 69 | onActivityResult( CAMERA_PICTURE_CODE, Activity.RESULT_CANCELED, null ); 70 | return; 71 | } 72 | 73 | fileTargetPath = photoFile.getAbsolutePath(); 74 | 75 | // Credit: https://stackoverflow.com/a/8555925/2373034 76 | // Get the id of the newest image in the Gallery 77 | Cursor imageCursor = null; 78 | try 79 | { 80 | imageCursor = getActivity().getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 81 | new String[] { MediaStore.Images.Media._ID }, null, null, MediaStore.Images.Media._ID + " DESC" ); 82 | if( imageCursor != null ) 83 | { 84 | if( imageCursor.moveToFirst() ) 85 | lastImageId = imageCursor.getInt( imageCursor.getColumnIndex( MediaStore.Images.Media._ID ) ); 86 | else if( imageCursor.getCount() <= 0 ) 87 | { 88 | // If there are currently no images in the Gallery, after the image is captured, querying the Gallery with 89 | // "_ID > lastImageId" will return the newly captured image since its ID will always be greater than Integer.MIN_VALUE 90 | lastImageId = Integer.MIN_VALUE; 91 | } 92 | } 93 | } 94 | catch( Exception e ) 95 | { 96 | Log.e( "Unity", "Exception:", e ); 97 | } 98 | finally 99 | { 100 | if( imageCursor != null ) 101 | imageCursor.close(); 102 | } 103 | 104 | Intent intent = new Intent( MediaStore.ACTION_IMAGE_CAPTURE ); 105 | NativeCameraUtils.SetOutputUri( getActivity(), intent, authority, photoFile ); 106 | 107 | if( defaultCamera == 0 ) 108 | NativeCameraUtils.SetDefaultCamera( intent, true ); 109 | else if( defaultCamera == 1 ) 110 | NativeCameraUtils.SetDefaultCamera( intent, false ); 111 | 112 | if( NativeCamera.QuickCapture ) 113 | intent.putExtra( "android.intent.extra.quickCapture", true ); 114 | 115 | try 116 | { 117 | // MIUI devices have issues with Intent.createChooser on at least Android 11 (https://stackoverflow.com/questions/67785661/taking-and-picking-photos-on-poco-x3-with-android-11-does-not-work) 118 | if( NativeCamera.UseDefaultCameraApp || ( Build.VERSION.SDK_INT == 30 && NativeCameraUtils.IsXiaomiOrMIUI() ) ) 119 | startActivityForResult( intent, CAMERA_PICTURE_CODE ); 120 | else 121 | startActivityForResult( Intent.createChooser( intent, "" ), CAMERA_PICTURE_CODE ); 122 | } 123 | catch( ActivityNotFoundException e ) 124 | { 125 | Toast.makeText( getActivity(), "No apps can perform this action.", Toast.LENGTH_LONG ).show(); 126 | onActivityResult( CAMERA_PICTURE_CODE, Activity.RESULT_CANCELED, null ); 127 | } 128 | } 129 | } 130 | 131 | @Override 132 | public void onActivityResult( int requestCode, int resultCode, Intent data ) 133 | { 134 | if( requestCode != CAMERA_PICTURE_CODE ) 135 | return; 136 | 137 | File result = null; 138 | if( resultCode == Activity.RESULT_OK ) 139 | { 140 | result = new File( fileTargetPath ); 141 | 142 | if( lastImageId != 0L ) // it got reset somehow? 143 | { 144 | // Credit: https://stackoverflow.com/a/8555925/2373034 145 | // Check if the image is saved to the Gallery instead of the specified path 146 | Cursor imageCursor = null; 147 | try 148 | { 149 | final String[] imageColumns = { MediaStore.Images.Media.DATA, MediaStore.Images.Media.SIZE, MediaStore.Images.Media._ID }; 150 | imageCursor = getActivity().getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageColumns, 151 | MediaStore.Images.Media._ID + ">?", new String[] { "" + lastImageId }, MediaStore.Images.Media._ID + " DESC" ); 152 | if( imageCursor != null && imageCursor.moveToNext() ) 153 | { 154 | String path = imageCursor.getString( imageCursor.getColumnIndex( MediaStore.Images.Media.DATA ) ); 155 | if( path != null && path.length() > 0 ) 156 | { 157 | boolean shouldDeleteImage = false; 158 | 159 | String id = "" + imageCursor.getInt( imageCursor.getColumnIndex( MediaStore.Images.Media._ID ) ); 160 | long size = imageCursor.getLong( imageCursor.getColumnIndex( MediaStore.Images.Media.SIZE ) ); 161 | if( size > result.length() ) 162 | { 163 | shouldDeleteImage = true; 164 | 165 | Uri contentUri; 166 | try 167 | { 168 | contentUri = Uri.withAppendedPath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id ); 169 | } 170 | catch( Exception e ) 171 | { 172 | Log.e( "Unity", "Exception:", e ); 173 | contentUri = null; 174 | } 175 | 176 | NativeCameraUtils.CopyFile( getActivity(), new File( path ), result, contentUri ); 177 | } 178 | else 179 | { 180 | try 181 | { 182 | if( !new File( path ).getCanonicalPath().equals( result.getCanonicalPath() ) ) 183 | shouldDeleteImage = true; 184 | } 185 | catch( Exception e ) 186 | { 187 | Log.e( "Unity", "Exception:", e ); 188 | } 189 | } 190 | 191 | if( shouldDeleteImage && !NativeCamera.KeepGalleryReferences ) 192 | { 193 | Log.d( "Unity", "Trying to delete duplicate gallery item: " + path ); 194 | 195 | getActivity().getContentResolver().delete( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 196 | MediaStore.Images.Media._ID + "=?", new String[] { id } ); 197 | } 198 | } 199 | } 200 | } 201 | catch( Exception e ) 202 | { 203 | Log.e( "Unity", "Exception:", e ); 204 | } 205 | finally 206 | { 207 | if( imageCursor != null ) 208 | imageCursor.close(); 209 | } 210 | } 211 | } 212 | 213 | Log.d( "Unity", "NativeCameraPictureFragment.onActivityResult: " + ( ( result == null ) ? "null" : ( ( result.exists() ? result.length() : -1 ) + " " + result.getAbsolutePath() ) ) ); 214 | if( mediaReceiver != null ) 215 | mediaReceiver.OnMediaReceived( ( result != null && result.exists() && result.length() > 1L ) ? result.getAbsolutePath() : "" ); 216 | else 217 | Log.e( "Unity", "NativeCameraPictureFragment.mediaReceiver became null in onActivityResult!" ); 218 | 219 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss(); 220 | } 221 | } -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraUtils.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.ClipData; 5 | import android.content.ContentResolver; 6 | import android.content.ContentUris; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.pm.PackageInfo; 10 | import android.content.pm.PackageManager; 11 | import android.content.pm.ProviderInfo; 12 | import android.database.Cursor; 13 | import android.graphics.Bitmap; 14 | import android.graphics.BitmapFactory; 15 | import android.graphics.Matrix; 16 | import android.media.ExifInterface; 17 | import android.media.MediaMetadataRetriever; 18 | import android.media.ThumbnailUtils; 19 | import android.net.Uri; 20 | import android.os.Build; 21 | import android.os.Environment; 22 | import android.provider.DocumentsContract; 23 | import android.provider.MediaStore; 24 | import android.provider.OpenableColumns; 25 | import android.util.Log; 26 | import android.util.Size; 27 | import android.webkit.MimeTypeMap; 28 | 29 | import java.io.BufferedReader; 30 | import java.io.File; 31 | import java.io.FileInputStream; 32 | import java.io.FileOutputStream; 33 | import java.io.IOException; 34 | import java.io.InputStream; 35 | import java.io.InputStreamReader; 36 | import java.io.OutputStream; 37 | import java.io.RandomAccessFile; 38 | import java.util.Locale; 39 | 40 | /** 41 | * Created by yasirkula on 30.04.2018. 42 | */ 43 | 44 | public class NativeCameraUtils 45 | { 46 | private static String authority = null; 47 | private static String secondaryStoragePath = null; 48 | private static int isXiaomiOrMIUI = 0; // 1: true, -1: false 49 | 50 | public static String GetAuthority( Context context ) 51 | { 52 | if( authority == null ) 53 | { 54 | // Find the authority of ContentProvider first 55 | // Credit: https://stackoverflow.com/a/2001769/2373034 56 | try 57 | { 58 | PackageInfo packageInfo = context.getPackageManager().getPackageInfo( context.getPackageName(), PackageManager.GET_PROVIDERS ); 59 | ProviderInfo[] providers = packageInfo.providers; 60 | if( providers != null ) 61 | { 62 | for( ProviderInfo provider : providers ) 63 | { 64 | if( provider.name != null && provider.packageName != null && provider.authority != null && 65 | provider.name.equals( NativeCameraContentProvider.class.getName() ) && provider.packageName.equals( context.getPackageName() ) 66 | && provider.authority.length() > 0 ) 67 | { 68 | authority = provider.authority; 69 | break; 70 | } 71 | } 72 | } 73 | } 74 | catch( Exception e ) 75 | { 76 | Log.e( "Unity", "Exception:", e ); 77 | } 78 | } 79 | 80 | return authority; 81 | } 82 | 83 | public static boolean IsXiaomiOrMIUI() 84 | { 85 | if( isXiaomiOrMIUI > 0 ) 86 | return true; 87 | else if( isXiaomiOrMIUI < 0 ) 88 | return false; 89 | 90 | if( "xiaomi".equalsIgnoreCase( android.os.Build.MANUFACTURER ) ) 91 | { 92 | isXiaomiOrMIUI = 1; 93 | return true; 94 | } 95 | 96 | // Check if device is using MIUI 97 | // Credit: https://gist.github.com/Muyangmin/e8ec1002c930d8df3df46b306d03315d 98 | String line; 99 | BufferedReader inputStream = null; 100 | try 101 | { 102 | java.lang.Process process = Runtime.getRuntime().exec( "getprop ro.miui.ui.version.name" ); 103 | inputStream = new BufferedReader( new InputStreamReader( process.getInputStream() ), 1024 ); 104 | line = inputStream.readLine(); 105 | 106 | if( line != null && line.length() > 0 ) 107 | { 108 | isXiaomiOrMIUI = 1; 109 | return true; 110 | } 111 | else 112 | { 113 | isXiaomiOrMIUI = -1; 114 | return false; 115 | } 116 | } 117 | catch( Exception e ) 118 | { 119 | isXiaomiOrMIUI = -1; 120 | return false; 121 | } 122 | finally 123 | { 124 | if( inputStream != null ) 125 | { 126 | try 127 | { 128 | inputStream.close(); 129 | } 130 | catch( Exception e ) 131 | { 132 | } 133 | } 134 | } 135 | } 136 | 137 | // Credit: https://github.com/jamesmontemagno/MediaPlugin/issues/307#issuecomment-356199135 138 | // Credit: https://stackoverflow.com/a/67288087/2373034 139 | public static void SetDefaultCamera( Intent intent, boolean useRearCamera ) 140 | { 141 | if( useRearCamera ) 142 | { 143 | intent.putExtra( "android.intent.extras.LENS_FACING_BACK", 1 ); 144 | intent.putExtra( "android.intent.extras.CAMERA_FACING", 0 ); 145 | intent.putExtra( "android.intent.extra.USE_FRONT_CAMERA", false ); 146 | intent.putExtra( "com.google.assistant.extra.USE_FRONT_CAMERA", false ); 147 | } 148 | else 149 | { 150 | intent.putExtra( "android.intent.extras.LENS_FACING_FRONT", 1 ); 151 | intent.putExtra( "android.intent.extras.CAMERA_FACING", 1 ); 152 | intent.putExtra( "android.intent.extra.USE_FRONT_CAMERA", true ); 153 | intent.putExtra( "com.google.assistant.extra.USE_FRONT_CAMERA", true ); 154 | 155 | intent.putExtra( "camerafacing", "front" ); 156 | intent.putExtra( "default_camera", "1" ); 157 | } 158 | } 159 | 160 | @TargetApi( Build.VERSION_CODES.JELLY_BEAN ) 161 | public static void SetOutputUri( Context context, Intent intent, String authority, File output ) 162 | { 163 | Uri uri; 164 | if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ) 165 | uri = NativeCameraContentProvider.getUriForFile( context, authority, output ); 166 | else 167 | uri = Uri.fromFile( output ); 168 | 169 | intent.putExtra( MediaStore.EXTRA_OUTPUT, uri ); 170 | 171 | // Credit: https://medium.com/@quiro91/sharing-files-through-intents-part-2-fixing-the-permissions-before-lollipop-ceb9bb0eec3a 172 | if( Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ) 173 | { 174 | intent.setClipData( ClipData.newRawUri( "", uri ) ); 175 | intent.setFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION ); 176 | } 177 | } 178 | 179 | // Credit: https://stackoverflow.com/a/6994668/2373034 180 | public static void ClearFileContents( File file ) throws IOException 181 | { 182 | RandomAccessFile stream = null; 183 | try 184 | { 185 | stream = new RandomAccessFile( file, "rw" ); 186 | stream.setLength( 0 ); 187 | } 188 | finally 189 | { 190 | if( stream != null ) 191 | { 192 | try 193 | { 194 | stream.close(); 195 | } 196 | catch( Exception e ) 197 | { 198 | } 199 | } 200 | } 201 | } 202 | 203 | // Credit: https://stackoverflow.com/a/9293885/2373034 204 | public static void CopyFile( Context context, File src, File dst, Uri rawUri ) 205 | { 206 | try 207 | { 208 | if( !src.exists() ) 209 | return; 210 | 211 | if( dst.exists() ) 212 | ClearFileContents( dst ); 213 | 214 | InputStream in = new FileInputStream( src ); 215 | try 216 | { 217 | OutputStream out = new FileOutputStream( dst ); 218 | try 219 | { 220 | byte[] buf = new byte[1024]; 221 | int len; 222 | while( ( len = in.read( buf ) ) > 0 ) 223 | out.write( buf, 0, len ); 224 | } 225 | finally 226 | { 227 | out.close(); 228 | } 229 | } 230 | finally 231 | { 232 | in.close(); 233 | } 234 | } 235 | catch( Exception e ) 236 | { 237 | if( rawUri == null ) 238 | Log.e( "Unity", "Exception:", e ); 239 | else 240 | { 241 | // Try to save the file via contentResolver (can happen e.g. on Android 10 where raw file system access is restricted) 242 | try 243 | { 244 | InputStream input = context.getContentResolver().openInputStream( rawUri ); 245 | if( input == null ) 246 | return; 247 | 248 | OutputStream output = null; 249 | try 250 | { 251 | output = new FileOutputStream( dst, false ); 252 | 253 | byte[] buf = new byte[4096]; 254 | int len; 255 | while( ( len = input.read( buf ) ) > 0 ) 256 | output.write( buf, 0, len ); 257 | } 258 | finally 259 | { 260 | if( output != null ) 261 | output.close(); 262 | 263 | input.close(); 264 | } 265 | } 266 | catch( Exception e2 ) 267 | { 268 | Log.e( "Unity", "Exception:", e ); 269 | Log.e( "Unity", "Exception2:", e2 ); 270 | } 271 | } 272 | } 273 | } 274 | 275 | public static boolean IsPermissionDefinedInManifest( Context context, String permission ) 276 | { 277 | try 278 | { 279 | String[] requestedPermissions = context.getPackageManager().getPackageInfo( context.getPackageName(), PackageManager.GET_PERMISSIONS ).requestedPermissions; 280 | if( requestedPermissions != null ) 281 | { 282 | for( String requestedPermission : requestedPermissions ) 283 | { 284 | if( permission.equals( requestedPermission ) ) 285 | return true; 286 | } 287 | } 288 | } 289 | catch( PackageManager.NameNotFoundException e ) 290 | { 291 | } 292 | 293 | return false; 294 | } 295 | 296 | public static String GetPathFromURIOrCopyFile( Context context, Uri uri, String defaultPath ) 297 | { 298 | if( uri == null ) 299 | return null; 300 | 301 | String path = GetPathFromURI( context, uri ); 302 | if( path == null || path.length() == 0 ) 303 | { 304 | Log.d( "Unity", "Filepath of '" + uri + "' couldn't be determined. Falling back to: " + defaultPath ); 305 | path = defaultPath; 306 | } 307 | 308 | if( path != null && path.length() > 0 ) 309 | { 310 | // Check if file is accessible 311 | FileInputStream inputStream = null; 312 | try 313 | { 314 | inputStream = new FileInputStream( new File( path ) ); 315 | inputStream.read(); 316 | 317 | return path; 318 | } 319 | catch( Exception e ) 320 | { 321 | Log.e( "Unity", "Media uri isn't accessible via File API: " + uri, e ); 322 | } 323 | finally 324 | { 325 | if( inputStream != null ) 326 | { 327 | try 328 | { 329 | inputStream.close(); 330 | } 331 | catch( Exception e ) 332 | { 333 | } 334 | } 335 | } 336 | } 337 | 338 | // File path couldn't be determined, copy the file to an accessible temporary location 339 | // Credit: https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo 340 | ContentResolver resolver = context.getContentResolver(); 341 | Cursor returnCursor = null; 342 | String filename = null; 343 | 344 | try 345 | { 346 | returnCursor = resolver.query( uri, null, null, null, null ); 347 | if( returnCursor != null && returnCursor.moveToFirst() ) 348 | filename = returnCursor.getString( returnCursor.getColumnIndex( OpenableColumns.DISPLAY_NAME ) ); 349 | } 350 | catch( Exception e ) 351 | { 352 | Log.e( "Unity", "Exception:", e ); 353 | } 354 | finally 355 | { 356 | if( returnCursor != null ) 357 | returnCursor.close(); 358 | } 359 | 360 | String extension = null; 361 | int filenameExtensionIndex = ( filename != null ) ? filename.lastIndexOf( '.' ) : -1; 362 | if( filenameExtensionIndex > 0 && filenameExtensionIndex < filename.length() - 1 ) 363 | extension = filename.substring( filenameExtensionIndex ).toLowerCase( Locale.US ); 364 | else 365 | { 366 | String mime = resolver.getType( uri ); 367 | if( mime != null ) 368 | { 369 | String mimeExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType( mime ); 370 | if( mimeExtension != null && mimeExtension.length() > 0 ) 371 | extension = "." + mimeExtension; 372 | } 373 | } 374 | 375 | if( extension == null ) 376 | extension = ".mp4"; 377 | 378 | try 379 | { 380 | InputStream input = resolver.openInputStream( uri ); 381 | if( input == null ) 382 | { 383 | Log.w( "Unity", "Couldn't open input stream: " + uri ); 384 | return null; 385 | } 386 | 387 | File tempFile = new File( context.getCacheDir().getAbsolutePath(), "VID_copy" + extension ); 388 | OutputStream output = null; 389 | try 390 | { 391 | output = new FileOutputStream( tempFile, false ); 392 | 393 | byte[] buf = new byte[4096]; 394 | int len; 395 | while( ( len = input.read( buf ) ) > 0 ) 396 | { 397 | output.write( buf, 0, len ); 398 | } 399 | 400 | Log.d( "Unity", "Copied media from " + uri + " to: " + tempFile.getAbsolutePath() ); 401 | return tempFile.getAbsolutePath(); 402 | } 403 | finally 404 | { 405 | if( output != null ) 406 | output.close(); 407 | 408 | input.close(); 409 | } 410 | } 411 | catch( Exception e ) 412 | { 413 | Log.e( "Unity", "Exception:", e ); 414 | } 415 | 416 | return null; 417 | } 418 | 419 | // Credit: https://stackoverflow.com/a/36714242/2373034 420 | public static String GetPathFromURI( Context context, Uri uri ) 421 | { 422 | if( uri == null ) 423 | return null; 424 | 425 | Log.d( "Unity", "Media uri: " + uri.toString() ); 426 | 427 | String selection = null; 428 | String[] selectionArgs = null; 429 | 430 | try 431 | { 432 | if( Build.VERSION.SDK_INT >= 19 && DocumentsContract.isDocumentUri( context.getApplicationContext(), uri ) ) 433 | { 434 | if( "com.android.externalstorage.documents".equals( uri.getAuthority() ) ) 435 | { 436 | final String docId = DocumentsContract.getDocumentId( uri ); 437 | final String[] split = docId.split( ":" ); 438 | 439 | if( "primary".equalsIgnoreCase( split[0] ) ) 440 | return Environment.getExternalStorageDirectory() + File.separator + split[1]; 441 | else if( "raw".equalsIgnoreCase( split[0] ) ) // https://stackoverflow.com/a/51874578/2373034 442 | return split[1]; 443 | 444 | return getSecondaryStoragePathFor( split[1] ); 445 | } 446 | else if( "com.android.providers.downloads.documents".equals( uri.getAuthority() ) ) 447 | { 448 | final String id = DocumentsContract.getDocumentId( uri ); 449 | if( id.startsWith( "raw:" ) ) // https://stackoverflow.com/a/51874578/2373034 450 | return id.substring( 4 ); 451 | else if( id.indexOf( ':' ) < 0 ) // Don't attempt to parse stuff like "msf:NUMBER" (newer Android versions) 452 | uri = ContentUris.withAppendedId( Uri.parse( "content://downloads/public_downloads" ), Long.parseLong( id ) ); 453 | else 454 | return null; 455 | } 456 | else if( "com.android.providers.media.documents".equals( uri.getAuthority() ) ) 457 | { 458 | final String docId = DocumentsContract.getDocumentId( uri ); 459 | final String[] split = docId.split( ":" ); 460 | final String type = split[0]; 461 | if( "image".equals( type ) ) 462 | uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 463 | else if( "video".equals( type ) ) 464 | uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 465 | else if( "audio".equals( type ) ) 466 | uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 467 | else if( "raw".equals( type ) ) // https://stackoverflow.com/a/51874578/2373034 468 | return split[1]; 469 | 470 | selection = "_id=?"; 471 | selectionArgs = new String[] { split[1] }; 472 | } 473 | } 474 | 475 | if( "content".equalsIgnoreCase( uri.getScheme() ) ) 476 | { 477 | String[] projection = { MediaStore.Images.Media.DATA }; 478 | Cursor cursor = null; 479 | 480 | try 481 | { 482 | cursor = context.getContentResolver().query( uri, projection, selection, selectionArgs, null ); 483 | if( cursor != null ) 484 | { 485 | int column_index = cursor.getColumnIndexOrThrow( MediaStore.Images.Media.DATA ); 486 | if( cursor.moveToFirst() ) 487 | { 488 | String columnValue = cursor.getString( column_index ); 489 | if( columnValue != null && columnValue.length() > 0 ) 490 | return columnValue; 491 | } 492 | } 493 | } 494 | catch( Exception e ) 495 | { 496 | } 497 | finally 498 | { 499 | if( cursor != null ) 500 | cursor.close(); 501 | } 502 | } 503 | else if( "file".equalsIgnoreCase( uri.getScheme() ) ) 504 | return uri.getPath(); 505 | 506 | // File path couldn't be determined 507 | return null; 508 | } 509 | catch( Exception e ) 510 | { 511 | Log.e( "Unity", "Exception:", e ); 512 | return null; 513 | } 514 | } 515 | 516 | private static String getSecondaryStoragePathFor( String localPath ) 517 | { 518 | if( secondaryStoragePath == null ) 519 | { 520 | String primaryPath = Environment.getExternalStorageDirectory().getAbsolutePath(); 521 | 522 | // Try paths saved at system environments 523 | // Credit: https://stackoverflow.com/a/32088396/2373034 524 | String strSDCardPath = System.getenv( "SECONDARY_STORAGE" ); 525 | if( strSDCardPath == null || strSDCardPath.length() == 0 ) 526 | strSDCardPath = System.getenv( "EXTERNAL_SDCARD_STORAGE" ); 527 | 528 | if( strSDCardPath != null && strSDCardPath.length() > 0 ) 529 | { 530 | if( !strSDCardPath.contains( ":" ) ) 531 | strSDCardPath += ":"; 532 | 533 | String[] externalPaths = strSDCardPath.split( ":" ); 534 | for( int i = 0; i < externalPaths.length; i++ ) 535 | { 536 | String path = externalPaths[i]; 537 | if( path != null && path.length() > 0 ) 538 | { 539 | File file = new File( path ); 540 | if( file.exists() && file.isDirectory() && file.canRead() && !file.getAbsolutePath().equalsIgnoreCase( primaryPath ) ) 541 | { 542 | String absolutePath = file.getAbsolutePath() + File.separator + localPath; 543 | if( new File( absolutePath ).exists() ) 544 | { 545 | secondaryStoragePath = file.getAbsolutePath(); 546 | return absolutePath; 547 | } 548 | } 549 | } 550 | } 551 | } 552 | 553 | // Try most common possible paths 554 | // Credit: https://gist.github.com/PauloLuan/4bcecc086095bce28e22 555 | String[] possibleRoots = new String[] { "/storage", "/mnt", "/storage/removable", 556 | "/removable", "/data", "/mnt/media_rw", "/mnt/sdcard0" }; 557 | for( String root : possibleRoots ) 558 | { 559 | try 560 | { 561 | File fileList[] = new File( root ).listFiles(); 562 | for( File file : fileList ) 563 | { 564 | if( file.exists() && file.isDirectory() && file.canRead() && !file.getAbsolutePath().equalsIgnoreCase( primaryPath ) ) 565 | { 566 | String absolutePath = file.getAbsolutePath() + File.separator + localPath; 567 | if( new File( absolutePath ).exists() ) 568 | { 569 | secondaryStoragePath = file.getAbsolutePath(); 570 | return absolutePath; 571 | } 572 | } 573 | } 574 | } 575 | catch( Exception e ) 576 | { 577 | } 578 | } 579 | 580 | secondaryStoragePath = "_NulL_"; 581 | } 582 | else if( !secondaryStoragePath.equals( "_NulL_" ) ) 583 | return secondaryStoragePath + File.separator + localPath; 584 | 585 | return null; 586 | } 587 | 588 | private static BitmapFactory.Options GetImageMetadata( final String path ) 589 | { 590 | try 591 | { 592 | BitmapFactory.Options result = new BitmapFactory.Options(); 593 | result.inJustDecodeBounds = true; 594 | BitmapFactory.decodeFile( path, result ); 595 | 596 | return result; 597 | } 598 | catch( Exception e ) 599 | { 600 | Log.e( "Unity", "Exception:", e ); 601 | return null; 602 | } 603 | } 604 | 605 | // Credit: https://stackoverflow.com/a/30572852/2373034 606 | private static int GetImageOrientation( Context context, final String path ) 607 | { 608 | try 609 | { 610 | ExifInterface exif = new ExifInterface( path ); 611 | int orientationEXIF = exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED ); 612 | if( orientationEXIF != ExifInterface.ORIENTATION_UNDEFINED ) 613 | return orientationEXIF; 614 | } 615 | catch( Exception e ) 616 | { 617 | } 618 | 619 | Cursor cursor = null; 620 | try 621 | { 622 | cursor = context.getContentResolver().query( Uri.fromFile( new File( path ) ), new String[] { MediaStore.Images.Media.ORIENTATION }, null, null, null ); 623 | if( cursor != null && cursor.moveToFirst() ) 624 | { 625 | int orientation = cursor.getInt( cursor.getColumnIndex( MediaStore.Images.Media.ORIENTATION ) ); 626 | if( orientation == 90 ) 627 | return ExifInterface.ORIENTATION_ROTATE_90; 628 | if( orientation == 180 ) 629 | return ExifInterface.ORIENTATION_ROTATE_180; 630 | if( orientation == 270 ) 631 | return ExifInterface.ORIENTATION_ROTATE_270; 632 | 633 | return ExifInterface.ORIENTATION_NORMAL; 634 | } 635 | } 636 | catch( Exception e ) 637 | { 638 | } 639 | finally 640 | { 641 | if( cursor != null ) 642 | cursor.close(); 643 | } 644 | 645 | return ExifInterface.ORIENTATION_UNDEFINED; 646 | } 647 | 648 | // Credit: https://gist.github.com/aviadmini/4be34097dfdb842ae066fae48501ed41 649 | private static Matrix GetImageOrientationCorrectionMatrix( final int orientation, final float scale ) 650 | { 651 | Matrix matrix = new Matrix(); 652 | 653 | switch( orientation ) 654 | { 655 | case ExifInterface.ORIENTATION_ROTATE_270: 656 | { 657 | matrix.postRotate( 270 ); 658 | matrix.postScale( scale, scale ); 659 | 660 | break; 661 | } 662 | case ExifInterface.ORIENTATION_ROTATE_180: 663 | { 664 | matrix.postRotate( 180 ); 665 | matrix.postScale( scale, scale ); 666 | 667 | break; 668 | } 669 | case ExifInterface.ORIENTATION_ROTATE_90: 670 | { 671 | matrix.postRotate( 90 ); 672 | matrix.postScale( scale, scale ); 673 | 674 | break; 675 | } 676 | case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 677 | { 678 | matrix.postScale( -scale, scale ); 679 | break; 680 | } 681 | case ExifInterface.ORIENTATION_FLIP_VERTICAL: 682 | { 683 | matrix.postScale( scale, -scale ); 684 | break; 685 | } 686 | case ExifInterface.ORIENTATION_TRANSPOSE: 687 | { 688 | matrix.postRotate( 90 ); 689 | matrix.postScale( -scale, scale ); 690 | 691 | break; 692 | } 693 | case ExifInterface.ORIENTATION_TRANSVERSE: 694 | { 695 | matrix.postRotate( 270 ); 696 | matrix.postScale( -scale, scale ); 697 | 698 | break; 699 | } 700 | default: 701 | { 702 | matrix.postScale( scale, scale ); 703 | break; 704 | } 705 | } 706 | 707 | return matrix; 708 | } 709 | 710 | public static String LoadImageAtPath( Context context, String path, final String temporaryFilePath, final int maxSize ) 711 | { 712 | BitmapFactory.Options metadata = GetImageMetadata( path ); 713 | if( metadata == null ) 714 | return path; 715 | 716 | boolean shouldCreateNewBitmap = false; 717 | if( metadata.outWidth > maxSize || metadata.outHeight > maxSize ) 718 | shouldCreateNewBitmap = true; 719 | 720 | if( metadata.outMimeType != null && !metadata.outMimeType.equals( "image/jpeg" ) && !metadata.outMimeType.equals( "image/png" ) ) 721 | shouldCreateNewBitmap = true; 722 | 723 | int orientation = GetImageOrientation( context, path ); 724 | if( orientation != ExifInterface.ORIENTATION_NORMAL && orientation != ExifInterface.ORIENTATION_UNDEFINED ) 725 | shouldCreateNewBitmap = true; 726 | 727 | if( shouldCreateNewBitmap ) 728 | { 729 | Bitmap bitmap = null; 730 | FileOutputStream out = null; 731 | 732 | try 733 | { 734 | // Credit: https://developer.android.com/topic/performance/graphics/load-bitmap.html 735 | int sampleSize = 1; 736 | int halfHeight = metadata.outHeight / 2; 737 | int halfWidth = metadata.outWidth / 2; 738 | while( ( halfHeight / sampleSize ) >= maxSize || ( halfWidth / sampleSize ) >= maxSize ) 739 | sampleSize *= 2; 740 | 741 | BitmapFactory.Options options = new BitmapFactory.Options(); 742 | options.inSampleSize = sampleSize; 743 | options.inJustDecodeBounds = false; 744 | bitmap = BitmapFactory.decodeFile( path, options ); 745 | 746 | float scaleX = 1f, scaleY = 1f; 747 | if( bitmap.getWidth() > maxSize ) 748 | scaleX = maxSize / (float) bitmap.getWidth(); 749 | if( bitmap.getHeight() > maxSize ) 750 | scaleY = maxSize / (float) bitmap.getHeight(); 751 | 752 | // Create a new bitmap if it should be scaled down or if its orientation is wrong 753 | float scale = scaleX < scaleY ? scaleX : scaleY; 754 | if( scale < 1f || ( orientation != ExifInterface.ORIENTATION_NORMAL && orientation != ExifInterface.ORIENTATION_UNDEFINED ) ) 755 | { 756 | Matrix transformationMatrix = GetImageOrientationCorrectionMatrix( orientation, scale ); 757 | Bitmap transformedBitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), transformationMatrix, true ); 758 | if( transformedBitmap != bitmap ) 759 | { 760 | bitmap.recycle(); 761 | bitmap = transformedBitmap; 762 | } 763 | } 764 | 765 | out = new FileOutputStream( temporaryFilePath ); 766 | if( metadata.outMimeType == null || !metadata.outMimeType.equals( "image/jpeg" ) ) 767 | bitmap.compress( Bitmap.CompressFormat.PNG, 100, out ); 768 | else 769 | bitmap.compress( Bitmap.CompressFormat.JPEG, 100, out ); 770 | 771 | path = temporaryFilePath; 772 | } 773 | catch( Exception e ) 774 | { 775 | Log.e( "Unity", "Exception:", e ); 776 | 777 | try 778 | { 779 | File temporaryFile = new File( temporaryFilePath ); 780 | if( temporaryFile.exists() ) 781 | temporaryFile.delete(); 782 | } 783 | catch( Exception e2 ) 784 | { 785 | } 786 | } 787 | finally 788 | { 789 | if( bitmap != null ) 790 | bitmap.recycle(); 791 | 792 | try 793 | { 794 | if( out != null ) 795 | out.close(); 796 | } 797 | catch( Exception e ) 798 | { 799 | } 800 | } 801 | } 802 | 803 | return path; 804 | } 805 | 806 | public static String GetImageProperties( Context context, final String path ) 807 | { 808 | BitmapFactory.Options metadata = GetImageMetadata( path ); 809 | if( metadata == null ) 810 | return ""; 811 | 812 | int width = metadata.outWidth; 813 | int height = metadata.outHeight; 814 | 815 | String mimeType = metadata.outMimeType; 816 | if( mimeType == null ) 817 | mimeType = ""; 818 | 819 | int orientationUnity; 820 | int orientation = GetImageOrientation( context, path ); 821 | if( orientation == ExifInterface.ORIENTATION_UNDEFINED ) 822 | orientationUnity = -1; 823 | else if( orientation == ExifInterface.ORIENTATION_NORMAL ) 824 | orientationUnity = 0; 825 | else if( orientation == ExifInterface.ORIENTATION_ROTATE_90 ) 826 | orientationUnity = 1; 827 | else if( orientation == ExifInterface.ORIENTATION_ROTATE_180 ) 828 | orientationUnity = 2; 829 | else if( orientation == ExifInterface.ORIENTATION_ROTATE_270 ) 830 | orientationUnity = 3; 831 | else if( orientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL ) 832 | orientationUnity = 4; 833 | else if( orientation == ExifInterface.ORIENTATION_TRANSPOSE ) 834 | orientationUnity = 5; 835 | else if( orientation == ExifInterface.ORIENTATION_FLIP_VERTICAL ) 836 | orientationUnity = 6; 837 | else if( orientation == ExifInterface.ORIENTATION_TRANSVERSE ) 838 | orientationUnity = 7; 839 | else 840 | orientationUnity = -1; 841 | 842 | if( orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270 || 843 | orientation == ExifInterface.ORIENTATION_TRANSPOSE || orientation == ExifInterface.ORIENTATION_TRANSVERSE ) 844 | { 845 | int temp = width; 846 | width = height; 847 | height = temp; 848 | } 849 | 850 | return width + ">" + height + ">" + mimeType + ">" + orientationUnity; 851 | } 852 | 853 | @TargetApi( Build.VERSION_CODES.JELLY_BEAN_MR1 ) 854 | public static String GetVideoProperties( Context context, final String path ) 855 | { 856 | MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); 857 | try 858 | { 859 | metadataRetriever.setDataSource( path ); 860 | 861 | String width = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH ); 862 | String height = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT ); 863 | String duration = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_DURATION ); 864 | String rotation = "0"; 865 | if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) 866 | rotation = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION ); 867 | 868 | if( width == null ) 869 | width = "0"; 870 | if( height == null ) 871 | height = "0"; 872 | if( duration == null ) 873 | duration = "0"; 874 | if( rotation == null ) 875 | rotation = "0"; 876 | 877 | return width + ">" + height + ">" + duration + ">" + rotation; 878 | } 879 | catch( Exception e ) 880 | { 881 | Log.e( "Unity", "Exception:", e ); 882 | return ""; 883 | } 884 | finally 885 | { 886 | try 887 | { 888 | metadataRetriever.release(); 889 | } 890 | catch( Exception e ) 891 | { 892 | } 893 | } 894 | } 895 | 896 | @TargetApi( Build.VERSION_CODES.Q ) 897 | public static String GetVideoThumbnail( Context context, final String path, final String savePath, final boolean saveAsJpeg, int maxSize, double captureTime ) 898 | { 899 | Bitmap bitmap = null; 900 | FileOutputStream out = null; 901 | 902 | try 903 | { 904 | if( captureTime < 0.0 && maxSize <= 1024 ) 905 | { 906 | try 907 | { 908 | if( Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ) 909 | bitmap = ThumbnailUtils.createVideoThumbnail( path, maxSize > 512 ? MediaStore.Video.Thumbnails.FULL_SCREEN_KIND : MediaStore.Video.Thumbnails.MINI_KIND ); 910 | else 911 | bitmap = ThumbnailUtils.createVideoThumbnail( new File( path ), maxSize > 512 ? new Size( 1024, 786 ) : new Size( 512, 384 ), null ); 912 | } 913 | catch( Exception e ) 914 | { 915 | Log.e( "Unity", "Exception:", e ); 916 | } 917 | } 918 | 919 | if( bitmap == null ) 920 | { 921 | MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); 922 | try 923 | { 924 | metadataRetriever.setDataSource( path ); 925 | 926 | try 927 | { 928 | int width = Integer.parseInt( metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH ) ); 929 | int height = Integer.parseInt( metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT ) ); 930 | if( maxSize > width && maxSize > height ) 931 | maxSize = width > height ? width : height; 932 | } 933 | catch( Exception e ) 934 | { 935 | } 936 | 937 | if( captureTime < 0.0 ) 938 | captureTime = 0.0; 939 | else 940 | { 941 | try 942 | { 943 | double duration = Long.parseLong( metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_DURATION ) ) / 1000.0; 944 | if( captureTime > duration ) 945 | captureTime = duration; 946 | } 947 | catch( Exception e ) 948 | { 949 | } 950 | } 951 | 952 | long frameTime = (long) ( captureTime * 1000000.0 ); 953 | if( Build.VERSION.SDK_INT < 27 ) 954 | bitmap = metadataRetriever.getFrameAtTime( frameTime, MediaMetadataRetriever.OPTION_CLOSEST_SYNC ); 955 | else 956 | bitmap = metadataRetriever.getScaledFrameAtTime( frameTime, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, maxSize, maxSize ); 957 | } 958 | finally 959 | { 960 | try 961 | { 962 | metadataRetriever.release(); 963 | } 964 | catch( Exception e ) 965 | { 966 | } 967 | } 968 | } 969 | 970 | if( bitmap == null ) 971 | return ""; 972 | 973 | out = new FileOutputStream( savePath ); 974 | if( saveAsJpeg ) 975 | bitmap.compress( Bitmap.CompressFormat.JPEG, 100, out ); 976 | else 977 | bitmap.compress( Bitmap.CompressFormat.PNG, 100, out ); 978 | 979 | return savePath; 980 | } 981 | catch( Exception e ) 982 | { 983 | Log.e( "Unity", "Exception:", e ); 984 | return ""; 985 | } 986 | finally 987 | { 988 | if( bitmap != null ) 989 | bitmap.recycle(); 990 | 991 | try 992 | { 993 | if( out != null ) 994 | out.close(); 995 | } 996 | catch( Exception e ) 997 | { 998 | } 999 | } 1000 | } 1001 | } -------------------------------------------------------------------------------- /.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraVideoFragment.java: -------------------------------------------------------------------------------- 1 | package com.yasirkula.unity; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.ActivityNotFoundException; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.database.Cursor; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.os.Environment; 13 | import android.provider.MediaStore; 14 | import android.util.Log; 15 | import android.widget.Toast; 16 | 17 | import java.io.File; 18 | import java.util.Locale; 19 | 20 | /** 21 | * Created by yasirkula on 22.04.2018. 22 | */ 23 | 24 | public class NativeCameraVideoFragment extends Fragment 25 | { 26 | private static final int CAMERA_VIDEO_CODE = 554777; 27 | 28 | public static final String VIDEO_NAME = "VID_camera"; 29 | public static final String DEFAULT_CAMERA_ID = "UNCV_DEF_CAMERA"; 30 | public static final String AUTHORITY_ID = "UNCV_AUTHORITY"; 31 | public static final String QUALITY_ID = "UNCV_QUALITY"; 32 | public static final String MAX_DURATION_ID = "UNCV_DURATION"; 33 | public static final String MAX_SIZE_ID = "UNCV_SIZE"; 34 | 35 | public static boolean provideExtraOutputOnAndroidQ = true; 36 | 37 | private final NativeCameraMediaReceiver mediaReceiver; 38 | private String fileTargetPath; 39 | private int lastVideoId = Integer.MAX_VALUE; 40 | 41 | public NativeCameraVideoFragment() 42 | { 43 | mediaReceiver = null; 44 | } 45 | 46 | public NativeCameraVideoFragment( final NativeCameraMediaReceiver mediaReceiver ) 47 | { 48 | this.mediaReceiver = mediaReceiver; 49 | } 50 | 51 | @Override 52 | public void onCreate( Bundle savedInstanceState ) 53 | { 54 | super.onCreate( savedInstanceState ); 55 | if( mediaReceiver == null ) 56 | { 57 | Log.e( "Unity", "NativeCameraVideoFragment.mediaReceiver became null in onCreate!" ); 58 | onActivityResult( CAMERA_VIDEO_CODE, Activity.RESULT_CANCELED, null ); 59 | } 60 | else 61 | { 62 | int defaultCamera = getArguments().getInt( DEFAULT_CAMERA_ID ); 63 | String authority = getArguments().getString( AUTHORITY_ID ); 64 | int quality = getArguments().getInt( QUALITY_ID ); 65 | int maxDuration = getArguments().getInt( MAX_DURATION_ID ); 66 | long maxSize = getArguments().getLong( MAX_SIZE_ID ); 67 | 68 | // Credit: https://stackoverflow.com/a/8555925/2373034 69 | // Get the id of the newest video in the Gallery 70 | Cursor videoCursor = null; 71 | try 72 | { 73 | videoCursor = getActivity().getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 74 | new String[] { MediaStore.Video.Media._ID }, null, null, MediaStore.Video.Media._ID + " DESC" ); 75 | if( videoCursor != null ) 76 | { 77 | if( videoCursor.moveToFirst() ) 78 | lastVideoId = videoCursor.getInt( videoCursor.getColumnIndex( MediaStore.Video.Media._ID ) ); 79 | else if( videoCursor.getCount() <= 0 ) 80 | { 81 | // If there are currently no videos in the Gallery, after the video is captured, querying the Gallery with 82 | // "_ID > lastVideoId" will return the newly captured video since its ID will always be greater than Integer.MIN_VALUE 83 | lastVideoId = Integer.MIN_VALUE; 84 | } 85 | } 86 | } 87 | catch( Exception e ) 88 | { 89 | Log.e( "Unity", "Exception:", e ); 90 | } 91 | finally 92 | { 93 | if( videoCursor != null ) 94 | videoCursor.close(); 95 | } 96 | 97 | Intent intent = new Intent( MediaStore.ACTION_VIDEO_CAPTURE ); 98 | 99 | // Setting a "EXTRA_OUTPUT" can stop the video from also appearing in the Gallery 100 | // but it is reported that while doing so, the camera app may stop functioning properly 101 | // on old devices or Nexus devices. Since we can delete any file from the Gallery on devices 102 | // that run on Android 9 or earlier, don't use EXTRA_OUTPUT on those devices for maximum compatibility 103 | // with older devices. Use EXTRA_OUTPUT on only Android 10 which restricts our access to the Gallery, 104 | // otherwise we can't delete the copies of the captured video on those devices 105 | if( provideExtraOutputOnAndroidQ && android.os.Build.VERSION.SDK_INT >= 29 && !Environment.isExternalStorageLegacy() ) 106 | { 107 | File videoFile = new File( getActivity().getCacheDir(), VIDEO_NAME + ".mp4" ); 108 | try 109 | { 110 | if( videoFile.exists() ) 111 | NativeCameraUtils.ClearFileContents( videoFile ); 112 | else 113 | videoFile.createNewFile(); 114 | } 115 | catch( Exception e ) 116 | { 117 | Log.e( "Unity", "Exception:", e ); 118 | onActivityResult( CAMERA_VIDEO_CODE, Activity.RESULT_CANCELED, null ); 119 | return; 120 | } 121 | 122 | fileTargetPath = videoFile.getAbsolutePath(); 123 | NativeCameraUtils.SetOutputUri( getActivity(), intent, authority, videoFile ); 124 | } 125 | else 126 | fileTargetPath = null; 127 | 128 | if( quality >= 0 ) 129 | intent.putExtra( MediaStore.EXTRA_VIDEO_QUALITY, quality <= 1 ? quality : 1 ); 130 | if( maxDuration > 0 ) 131 | intent.putExtra( MediaStore.EXTRA_DURATION_LIMIT, maxDuration ); 132 | if( maxSize > 0L ) 133 | intent.putExtra( MediaStore.EXTRA_SIZE_LIMIT, maxSize ); 134 | 135 | if( defaultCamera == 0 ) 136 | NativeCameraUtils.SetDefaultCamera( intent, true ); 137 | else if( defaultCamera == 1 ) 138 | NativeCameraUtils.SetDefaultCamera( intent, false ); 139 | 140 | if( NativeCamera.QuickCapture ) 141 | intent.putExtra( "android.intent.extra.quickCapture", true ); 142 | 143 | try 144 | { 145 | // MIUI devices have issues with Intent.createChooser on at least Android 11 (https://stackoverflow.com/questions/67785661/taking-and-picking-photos-on-poco-x3-with-android-11-does-not-work) 146 | if( NativeCamera.UseDefaultCameraApp || ( Build.VERSION.SDK_INT == 30 && NativeCameraUtils.IsXiaomiOrMIUI() ) ) 147 | startActivityForResult( intent, CAMERA_VIDEO_CODE ); 148 | else 149 | startActivityForResult( Intent.createChooser( intent, "" ), CAMERA_VIDEO_CODE ); 150 | } 151 | catch( ActivityNotFoundException e ) 152 | { 153 | Toast.makeText( getActivity(), "No apps can perform this action.", Toast.LENGTH_LONG ).show(); 154 | onActivityResult( CAMERA_VIDEO_CODE, Activity.RESULT_CANCELED, null ); 155 | } 156 | } 157 | } 158 | 159 | @Override 160 | public void onActivityResult( int requestCode, int resultCode, Intent data ) 161 | { 162 | if( requestCode != CAMERA_VIDEO_CODE ) 163 | return; 164 | 165 | File result = null; 166 | if( resultCode == Activity.RESULT_OK ) 167 | { 168 | if( data != null ) 169 | { 170 | String path = NativeCameraUtils.GetPathFromURIOrCopyFile( getActivity(), data.getData(), fileTargetPath ); 171 | if( path != null && path.length() > 0 ) 172 | result = new File( path ); 173 | } 174 | 175 | if( ( result == null || !result.exists() || result.length() == 0 ) && fileTargetPath != null && fileTargetPath.length() > 0 ) 176 | result = new File( fileTargetPath ); 177 | 178 | if( lastVideoId != 0L ) // it got reset somehow? 179 | { 180 | // Credit: https://stackoverflow.com/a/8555925/2373034 181 | // Check if the video is saved to the Gallery 182 | Cursor videoCursor = null; 183 | try 184 | { 185 | final String[] videoColumns = { MediaStore.Video.Media.DATA, MediaStore.Video.Media.SIZE, MediaStore.Video.Media._ID }; 186 | videoCursor = getActivity().getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoColumns, 187 | MediaStore.Video.Media._ID + ">?", new String[] { "" + lastVideoId }, MediaStore.Video.Media._ID + " DESC" ); 188 | while( videoCursor != null && videoCursor.moveToNext() ) 189 | { 190 | String path = videoCursor.getString( videoCursor.getColumnIndex( MediaStore.Video.Media.DATA ) ); 191 | if( path != null && path.length() > 0 ) 192 | { 193 | long size = videoCursor.getLong( videoCursor.getColumnIndex( MediaStore.Video.Media.SIZE ) ); 194 | if( result == null || !result.exists() || size == result.length() ) 195 | { 196 | try 197 | { 198 | String id = "" + videoCursor.getInt( videoCursor.getColumnIndex( MediaStore.Video.Media._ID ) ); 199 | String extension = ""; 200 | int extensionIndex = path.lastIndexOf( '.' ); 201 | if( extensionIndex > 0 && extensionIndex < path.length() - 1 && extensionIndex > path.lastIndexOf( File.separatorChar ) ) 202 | extension = path.substring( extensionIndex ).toLowerCase( Locale.US ); 203 | 204 | File copiedFile = new File( getActivity().getCacheDir(), VIDEO_NAME + extension ); 205 | Uri contentUri; 206 | try 207 | { 208 | contentUri = Uri.withAppendedPath( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id ); 209 | } 210 | catch( Exception e ) 211 | { 212 | Log.e( "Unity", "Exception:", e ); 213 | contentUri = null; 214 | } 215 | 216 | NativeCameraUtils.CopyFile( getActivity(), new File( path ), copiedFile, contentUri ); 217 | 218 | if( copiedFile.length() > 1L ) 219 | { 220 | result = copiedFile; 221 | 222 | if( !NativeCamera.KeepGalleryReferences ) 223 | { 224 | Log.d( "Unity", "Trying to delete duplicate gallery item: " + path ); 225 | 226 | getActivity().getContentResolver().delete( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 227 | MediaStore.Video.Media._ID + "=?", new String[] { id } ); 228 | } 229 | } 230 | } 231 | catch( Exception e ) 232 | { 233 | Log.e( "Unity", "Exception:", e ); 234 | } 235 | 236 | break; 237 | } 238 | } 239 | } 240 | } 241 | catch( Exception e ) 242 | { 243 | Log.e( "Unity", "Exception:", e ); 244 | } 245 | finally 246 | { 247 | if( videoCursor != null ) 248 | videoCursor.close(); 249 | } 250 | } 251 | } 252 | 253 | Log.d( "Unity", "NativeCameraVideoFragment.onActivityResult: " + ( ( result == null ) ? "null" : ( ( result.exists() ? result.length() : -1 ) + " " + result.getAbsolutePath() ) ) ); 254 | if( mediaReceiver != null ) 255 | mediaReceiver.OnMediaReceived( ( result != null && result.exists() && result.length() > 1L ) ? result.getAbsolutePath() : "" ); 256 | else 257 | Log.e( "Unity", "NativeCameraVideoFragment.mediaReceiver became null in onActivityResult!" ); 258 | 259 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss(); 260 | } 261 | } -------------------------------------------------------------------------------- /.github/AAR Source (Android)/proguard.txt: -------------------------------------------------------------------------------- 1 | -keep class com.yasirkula.unity.* { *; } -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # Unity Native Camera Plugin 2 | 3 | **Available on Asset Store:** https://assetstore.unity.com/packages/tools/integration/native-camera-for-android-ios-117802 4 | 5 | **Forum Thread:** https://forum.unity.com/threads/native-camera-for-android-ios-open-source.529560/ 6 | 7 | **Discord:** https://discord.gg/UJJt549AaV 8 | 9 | **[GitHub Sponsors ☕](https://github.com/sponsors/yasirkula)** 10 | 11 | This plugin helps you take pictures/record videos natively with your device's camera on Android & iOS (other platforms aren't supported). It has built-in support for runtime permissions, as well. 12 | 13 | ## INSTALLATION 14 | 15 | There are 5 ways to install this plugin: 16 | 17 | - import [NativeCamera.unitypackage](https://github.com/yasirkula/UnityNativeCamera/releases) via *Assets-Import Package* 18 | - clone/[download](https://github.com/yasirkula/UnityNativeCamera/archive/master.zip) this repository and move the *Plugins* folder to your Unity project's *Assets* folder 19 | - import it from [Asset Store](https://assetstore.unity.com/packages/tools/integration/native-camera-for-android-ios-117802) 20 | - *(via Package Manager)* click the + button and install the package from the following git URL: 21 | - `https://github.com/yasirkula/UnityNativeCamera.git` 22 | - *(via [OpenUPM](https://openupm.com))* after installing [openupm-cli](https://github.com/openupm/openupm-cli), run the following command: 23 | - `openupm add com.yasirkula.nativecamera` 24 | 25 | ### Android Setup 26 | 27 | NativeCamera no longer requires any manual setup on Android. For reference, the legacy manual setup documentation is available at: https://github.com/yasirkula/UnityNativeCamera/wiki/Manual-Setup-for-Android 28 | 29 | ### iOS Setup 30 | 31 | There are two ways to set up the plugin on iOS: 32 | 33 | - **a. Automated Setup:** *(optional)* change the values of **Camera Usage Description** and **Microphone Usage Description** at *Project Settings/yasirkula/Native Camera* 34 | - **b. Manual Setup:** see: https://github.com/yasirkula/UnityNativeCamera/wiki/Manual-Setup-for-iOS 35 | 36 | ## FAQ 37 | 38 | - **Audio is muted on iOS after calling NativeCamera.RecordVideo** 39 | 40 | Please see: https://forum.unity.com/threads/native-camera-for-android-ios-open-source.529560/page-9#post-8207157 41 | 42 | - **Plugin doesn't work in a Windows/Mac/Linux build** 43 | 44 | Only Android & iOS platforms are supported. Editor functionality is for preview purposes only and uses Unity's [Editor-only API](https://docs.unity3d.com/ScriptReference/EditorUtility.OpenFilePanelWithFilters.html). 45 | 46 | - **Can't use the camera, it says "java.lang.ClassNotFoundException: com.yasirkula.unity.NativeCamera" in Logcat** 47 | 48 | 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.* { *; }` 49 | 50 | - **NativeCamera functions return Permission.Denied even though I've granted the permission"** 51 | 52 | Declare `WRITE_EXTERNAL_STORAGE` permission manually in your **Plugins/Android/AndroidManifest.xml** with the `tools:node="replace"` attribute as follows: ``. 53 | 54 | ## HOW TO 55 | 56 | ### A. Accessing Camera 57 | 58 | `NativeCamera.TakePicture( CameraCallback callback, int maxSize = -1, PreferredCamera preferredCamera = PreferredCamera.Default )`: opens the camera and waits for user to take a picture. 59 | - This operation is **asynchronous**! After user takes a picture or cancels the operation, the **callback** is called (on main thread). **CameraCallback** takes a *string* parameter which stores the path of the captured image, or *null* if the operation is canceled 60 | - **maxSize** determines the maximum size of the returned image in pixels on iOS. A larger image will be down-scaled for better performance. If untouched, its value will be set to *SystemInfo.maxTextureSize*. Has no effect on Android 61 | - **saveAsJPEG** determines whether the image is saved as JPEG or PNG. Has no effect on Android 62 | - **preferredCamera** determines whether the rear camera or the front camera should be opened by default. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices (see https://github.com/yasirkula/UnityNativeCamera/issues/126) 63 | 64 | `NativeCamera.RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default )`: opens the camera and waits for user to record a video. 65 | - **quality** determines the quality of the recorded video. Available values are: *Default*, *Low*, *Medium*, *High* 66 | - **maxDuration** determines the maximum duration, in seconds, for the recorded video. If untouched, there will be no limit. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices 67 | - **maxSizeBytes** determines the maximum size, in bytes, for the recorded video. If untouched, there will be no limit. This parameter has no effect on iOS. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices 68 | 69 | `NativeCamera.DeviceHasCamera()`: returns false if the device doesn't have a camera. In this case, TakePicture and RecordVideo functions will not execute. 70 | 71 | `NativeCamera.IsCameraBusy()`: returns true if the camera is currently open. In that case, another TakePicture or RecordVideo request will simply be ignored. 72 | 73 | Note that TakePicture and RecordVideo functions automatically call *NativeCamera.RequestPermissionAsync*. More details available below. 74 | 75 | ### B. Runtime Permissions 76 | 77 | Beginning with *6.0 Marshmallow*, Android apps must request runtime permissions before accessing certain services, similar to iOS. There are two functions to handle permissions with this plugin: 78 | 79 | `bool NativeCamera.CheckPermission( bool isPicturePermission )`: checks whether the app has access to camera or not. 80 | - **isPicturePermission** determines whether we're checking permission to take a picture or record a video. Has no effect on iOS 81 | 82 | `void NativeCamera.RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission )`: requests permission to access the camera from the user and returns the result asynchronously. It is recommended to show a brief explanation before asking the permission so that user understands why the permission is needed and doesn't click Deny or worse, "Don't ask again". Note that TakePicture and RecordVideo functions call RequestPermissionAsync internally and execute only if the permission is granted. 83 | - **PermissionCallback** takes `NativeCamera.Permission permission` parameter 84 | 85 | **NativeCamera.Permission** is an enum that can take 3 values: 86 | - **Granted**: we have the permission to access the camera 87 | - **ShouldAsk**: permission is denied but we can ask the user for permission once again. On Android, as long as the user doesn't select "Don't ask again" while denying the permission, ShouldAsk is returned 88 | - **Denied**: we don't have permission and we can't ask the user for permission. In this case, user has to give the permission from Settings. This happens when user denies the permission on iOS (can't request permission again on iOS), when user selects "Don't ask again" while denying the permission on Android or when user is not allowed to give that permission (parental controls etc.) 89 | 90 | `Task NativeCamera.RequestPermissionAsync( bool isPicturePermission )`: Task-based overload of *RequestPermissionAsync*. 91 | 92 | `NativeCamera.OpenSettings()`: opens the settings for this app, from where the user can manually grant permission in case current permission state is *Permission.Denied* (Android requires *Storage* and, if declared in AndroidManifest, *Camera* permissions; iOS requires *Camera* permission). 93 | 94 | ### C. Utility Functions 95 | 96 | `NativeCamera.ImageProperties NativeCamera.GetImageProperties( string imagePath )`: returns an *ImageProperties* instance that holds the width, height, mime type and EXIF orientation information of an image file without creating a *Texture2D* object. Mime type will be *null*, if it can't be determined. 97 | 98 | `NativeCamera.VideoProperties NativeCamera.GetVideoProperties( string videoPath )`: 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. 99 | 100 | `Texture2D NativeCamera.LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )`: creates a Texture2D from the specified image file in correct orientation and returns it. Returns *null*, if something goes wrong. 101 | - **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 102 | - **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* 103 | - **generateMipmaps** determines whether texture should have mipmaps or not 104 | - **linearColorSpace** determines whether texture should be in linear color space or sRGB color space 105 | 106 | `async Task NativeCamera.LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true )`: asynchronous variant of *LoadImageAtPath*. Whether or not the returned Texture2D has mipmaps enabled depends on *UnityWebRequestTexture*'s implementation on the target Unity version. Note that it isn't possible to load multiple images simultaneously using this function. 107 | 108 | `Texture2D NativeCamera.GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, 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. 109 | - **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 110 | - **captureTimeInSeconds** determines the frame of the video that the thumbnail is captured from. If untouched, OS will decide this value 111 | - **markTextureNonReadable** (see *LoadImageAtPath*) 112 | 113 | `async Task NativeCamera.GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true )`: asynchronous variant of *GetVideoThumbnail*. Whether or not the returned Texture2D has mipmaps enabled depends on *UnityWebRequestTexture*'s implementation on the target Unity version. Note that it isn't possible to generate multiple video thumbnails simultaneously using this function. 114 | 115 | ## EXAMPLE CODE 116 | 117 | The following code has two functions: 118 | 119 | - if you click left half of the screen, the camera is opened and after a picture is taken, it is displayed on a temporary quad that is placed in front of the camera 120 | - if you click right half of the screen, the camera is opened and after a video is recorded, it is played using the *Handheld.PlayFullScreenMovie* function 121 | 122 | ```csharp 123 | void Update() 124 | { 125 | if( Input.GetMouseButtonDown( 0 ) ) 126 | { 127 | // Don't attempt to use the camera if it is already open 128 | if( NativeCamera.IsCameraBusy() ) 129 | return; 130 | 131 | if( Input.mousePosition.x < Screen.width / 2 ) 132 | { 133 | // Take a picture with the camera 134 | // If the captured image's width and/or height is greater than 512px, down-scale it 135 | TakePicture( 512 ); 136 | } 137 | else 138 | { 139 | // Record a video with the camera 140 | RecordVideo(); 141 | } 142 | } 143 | } 144 | 145 | private void TakePicture( int maxSize ) 146 | { 147 | NativeCamera.TakePicture( ( path ) => 148 | { 149 | Debug.Log( "Image path: " + path ); 150 | if( path != null ) 151 | { 152 | // Create a Texture2D from the captured image 153 | Texture2D texture = NativeCamera.LoadImageAtPath( path, maxSize ); 154 | if( texture == null ) 155 | { 156 | Debug.Log( "Couldn't load texture from " + path ); 157 | return; 158 | } 159 | 160 | // Assign texture to a temporary quad and destroy it after 5 seconds 161 | GameObject quad = GameObject.CreatePrimitive( PrimitiveType.Quad ); 162 | quad.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2.5f; 163 | quad.transform.forward = Camera.main.transform.forward; 164 | quad.transform.localScale = new Vector3( 1f, texture.height / (float) texture.width, 1f ); 165 | 166 | Material material = quad.GetComponent().material; 167 | if( !material.shader.isSupported ) // happens when Standard shader is not included in the build 168 | material.shader = Shader.Find( "Legacy Shaders/Diffuse" ); 169 | 170 | material.mainTexture = texture; 171 | 172 | Destroy( quad, 5f ); 173 | 174 | // If a procedural texture is not destroyed manually, 175 | // it will only be freed after a scene change 176 | Destroy( texture, 5f ); 177 | } 178 | }, maxSize ); 179 | } 180 | 181 | private void RecordVideo() 182 | { 183 | NativeCamera.RecordVideo( ( path ) => 184 | { 185 | Debug.Log( "Video path: " + path ); 186 | if( path != null ) 187 | { 188 | // Play the recorded video 189 | Handheld.PlayFullScreenMovie( "file://" + path ); 190 | } 191 | } ); 192 | } 193 | ``` 194 | -------------------------------------------------------------------------------- /.github/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/.github/screenshots/1.png -------------------------------------------------------------------------------- /.github/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/.github/screenshots/2.png -------------------------------------------------------------------------------- /.github/screenshots/AndroidManifest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/.github/screenshots/AndroidManifest.png -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Süleyman Yasir KULA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5c24a0aae91808f4587811abccea7bed 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4b3220d76ab685149aefa67134758726 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/NativeCamera.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e2f21cd7d33ee2b4a9aa49675b556cb4 3 | folderAsset: yes 4 | timeCreated: 1525098654 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5fc918beb0f7cfe49a7f596d5b6b8768 3 | folderAsset: yes 4 | timeCreated: 1525098661 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NCCallbackHelper.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR || UNITY_ANDROID 2 | using System; 3 | using UnityEngine; 4 | 5 | namespace NativeCameraNamespace 6 | { 7 | public class NCCallbackHelper : MonoBehaviour 8 | { 9 | private bool autoDestroyWithCallback; 10 | private Action mainThreadAction = null; 11 | 12 | public static NCCallbackHelper Create( bool autoDestroyWithCallback ) 13 | { 14 | NCCallbackHelper result = new GameObject( "NCCallbackHelper" ).AddComponent(); 15 | result.autoDestroyWithCallback = autoDestroyWithCallback; 16 | DontDestroyOnLoad( result.gameObject ); 17 | return result; 18 | } 19 | 20 | public void CallOnMainThread( Action function ) 21 | { 22 | lock( this ) 23 | { 24 | mainThreadAction += function; 25 | } 26 | } 27 | 28 | private void Update() 29 | { 30 | if( mainThreadAction != null ) 31 | { 32 | try 33 | { 34 | Action temp; 35 | lock( this ) 36 | { 37 | temp = mainThreadAction; 38 | mainThreadAction = null; 39 | } 40 | 41 | temp(); 42 | } 43 | finally 44 | { 45 | if( autoDestroyWithCallback ) 46 | Destroy( gameObject ); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | #endif -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NCCallbackHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b2bbe0051e738ea4585119c46d863f19 3 | timeCreated: 1545147258 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR || UNITY_ANDROID 2 | using UnityEngine; 3 | 4 | namespace NativeCameraNamespace 5 | { 6 | public class NCCameraCallbackAndroid : AndroidJavaProxy 7 | { 8 | private readonly NativeCamera.CameraCallback callback; 9 | private readonly NCCallbackHelper callbackHelper; 10 | 11 | public NCCameraCallbackAndroid( NativeCamera.CameraCallback callback ) : base( "com.yasirkula.unity.NativeCameraMediaReceiver" ) 12 | { 13 | this.callback = callback; 14 | callbackHelper = NCCallbackHelper.Create( true ); 15 | } 16 | 17 | [UnityEngine.Scripting.Preserve] 18 | public void OnMediaReceived( string path ) 19 | { 20 | callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) ); 21 | } 22 | } 23 | } 24 | #endif -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cc8df584d2a4344b929a4f13a53723a 3 | timeCreated: 1519060539 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR || UNITY_ANDROID 2 | using UnityEngine; 3 | 4 | namespace NativeCameraNamespace 5 | { 6 | public class NCPermissionCallbackAndroid : AndroidJavaProxy 7 | { 8 | private readonly NativeCamera.PermissionCallback callback; 9 | private readonly NCCallbackHelper callbackHelper; 10 | 11 | public NCPermissionCallbackAndroid( NativeCamera.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeCameraPermissionReceiver" ) 12 | { 13 | this.callback = callback; 14 | callbackHelper = NCCallbackHelper.Create( true ); 15 | } 16 | 17 | [UnityEngine.Scripting.Preserve] 18 | public void OnPermissionResult( int result ) 19 | { 20 | callbackHelper.CallOnMainThread( () => callback( (NativeCamera.Permission) result ) ); 21 | } 22 | } 23 | } 24 | #endif -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bafa24bbc8c455f44a2b98dcbe6451bd 3 | timeCreated: 1519060539 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NativeCamera.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/Plugins/NativeCamera/Android/NativeCamera.aar -------------------------------------------------------------------------------- /Plugins/NativeCamera/Android/NativeCamera.aar.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 284037eba2526f54d9cf51b5d9bffcfa 3 | timeCreated: 1569764737 4 | licenseType: Free 5 | PluginImporter: 6 | serializedVersion: 2 7 | iconMap: {} 8 | executionOrder: {} 9 | isPreloaded: 0 10 | isOverridable: 0 11 | platformData: 12 | data: 13 | first: 14 | Android: Android 15 | second: 16 | enabled: 1 17 | settings: {} 18 | data: 19 | first: 20 | Any: 21 | second: 22 | enabled: 0 23 | settings: {} 24 | data: 25 | first: 26 | Editor: Editor 27 | second: 28 | enabled: 0 29 | settings: 30 | DefaultValueInitialized: true 31 | userData: 32 | assetBundleName: 33 | assetBundleVariant: 34 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 16fe39fd709533a4eba946790a8e3123 3 | folderAsset: yes 4 | timeCreated: 1521452097 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Editor/NCPostProcessBuild.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using UnityEngine; 3 | using UnityEditor; 4 | #if UNITY_IOS 5 | using UnityEditor.Callbacks; 6 | using UnityEditor.iOS.Xcode; 7 | #endif 8 | 9 | namespace NativeCameraNamespace 10 | { 11 | [System.Serializable] 12 | public class Settings 13 | { 14 | private const string SAVE_PATH = "ProjectSettings/NativeCamera.json"; 15 | 16 | public bool AutomatedSetup = true; 17 | public string CameraUsageDescription = "The app requires access to the camera to take pictures or record videos with it."; 18 | public string MicrophoneUsageDescription = "The app will capture microphone input in the recorded video."; 19 | 20 | private static Settings m_instance = null; 21 | public static Settings Instance 22 | { 23 | get 24 | { 25 | if( m_instance == null ) 26 | { 27 | try 28 | { 29 | if( File.Exists( SAVE_PATH ) ) 30 | m_instance = JsonUtility.FromJson( File.ReadAllText( SAVE_PATH ) ); 31 | else 32 | m_instance = new Settings(); 33 | } 34 | catch( System.Exception e ) 35 | { 36 | Debug.LogException( e ); 37 | m_instance = new Settings(); 38 | } 39 | } 40 | 41 | return m_instance; 42 | } 43 | } 44 | 45 | public void Save() 46 | { 47 | File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) ); 48 | } 49 | 50 | [SettingsProvider] 51 | public static SettingsProvider CreatePreferencesGUI() 52 | { 53 | return new SettingsProvider( "Project/yasirkula/Native Camera", SettingsScope.Project ) 54 | { 55 | guiHandler = ( searchContext ) => PreferencesGUI(), 56 | keywords = new System.Collections.Generic.HashSet() { "Native", "Camera", "Android", "iOS" } 57 | }; 58 | } 59 | 60 | public static void PreferencesGUI() 61 | { 62 | EditorGUI.BeginChangeCheck(); 63 | 64 | Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup ); 65 | 66 | EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup ); 67 | Instance.CameraUsageDescription = EditorGUILayout.DelayedTextField( "Camera Usage Description", Instance.CameraUsageDescription ); 68 | Instance.MicrophoneUsageDescription = EditorGUILayout.DelayedTextField( "Microphone Usage Description", Instance.MicrophoneUsageDescription ); 69 | EditorGUI.EndDisabledGroup(); 70 | 71 | if( EditorGUI.EndChangeCheck() ) 72 | Instance.Save(); 73 | } 74 | } 75 | 76 | public class NCPostProcessBuild 77 | { 78 | #if UNITY_IOS 79 | [PostProcessBuild] 80 | public static void OnPostprocessBuild( BuildTarget target, string buildPath ) 81 | { 82 | if( !Settings.Instance.AutomatedSetup ) 83 | return; 84 | 85 | if( target == BuildTarget.iOS ) 86 | { 87 | string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath ); 88 | string plistPath = Path.Combine( buildPath, "Info.plist" ); 89 | 90 | PBXProject pbxProject = new PBXProject(); 91 | pbxProject.ReadFromFile( pbxProjectPath ); 92 | 93 | string targetGUID = pbxProject.GetUnityFrameworkTargetGuid(); 94 | pbxProject.AddFrameworkToProject( targetGUID, "MobileCoreServices.framework", false ); 95 | pbxProject.AddFrameworkToProject( targetGUID, "ImageIO.framework", false ); 96 | 97 | File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() ); 98 | 99 | PlistDocument plist = new PlistDocument(); 100 | plist.ReadFromString( File.ReadAllText( plistPath ) ); 101 | 102 | PlistElementDict rootDict = plist.root; 103 | if( !string.IsNullOrEmpty( Settings.Instance.CameraUsageDescription ) ) 104 | rootDict.SetString( "NSCameraUsageDescription", Settings.Instance.CameraUsageDescription ); 105 | if( !string.IsNullOrEmpty( Settings.Instance.MicrophoneUsageDescription ) ) 106 | rootDict.SetString( "NSMicrophoneUsageDescription", Settings.Instance.MicrophoneUsageDescription ); 107 | 108 | File.WriteAllText( plistPath, plist.WriteToString() ); 109 | } 110 | } 111 | #endif 112 | } 113 | } -------------------------------------------------------------------------------- /Plugins/NativeCamera/Editor/NCPostProcessBuild.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa3b57e342928704cb910789ae4dde20 3 | timeCreated: 1521452119 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/Editor/NativeCamera.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NativeCamera.Editor", 3 | "references": [], 4 | "includePlatforms": [ 5 | "Editor" 6 | ], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /Plugins/NativeCamera/Editor/NativeCamera.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 31117d0234af0084b91a7e53b3d9e0a3 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/NativeCamera.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NativeCamera.Runtime" 3 | } 4 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/NativeCamera.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b107fd1956cb3e04985108f5ee29e115 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/NativeCamera.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using UnityEngine; 5 | using System.Threading.Tasks; 6 | using UnityEngine.Networking; 7 | #if UNITY_ANDROID || UNITY_IOS 8 | using NativeCameraNamespace; 9 | #endif 10 | using Object = UnityEngine.Object; 11 | 12 | public static class NativeCamera 13 | { 14 | public struct ImageProperties 15 | { 16 | public readonly int width; 17 | public readonly int height; 18 | public readonly string mimeType; 19 | public readonly ImageOrientation orientation; 20 | 21 | public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation ) 22 | { 23 | this.width = width; 24 | this.height = height; 25 | this.mimeType = mimeType; 26 | this.orientation = orientation; 27 | } 28 | } 29 | 30 | public struct VideoProperties 31 | { 32 | public readonly int width; 33 | public readonly int height; 34 | public readonly long duration; 35 | public readonly float rotation; 36 | 37 | public VideoProperties( int width, int height, long duration, float rotation ) 38 | { 39 | this.width = width; 40 | this.height = height; 41 | this.duration = duration; 42 | this.rotation = rotation; 43 | } 44 | } 45 | 46 | public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 }; 47 | public enum Quality { Default = -1, Low = 0, Medium = 1, High = 2 }; 48 | public enum PreferredCamera { Default = -1, Rear = 0, Front = 1 } 49 | 50 | // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered) 51 | public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; 52 | 53 | public delegate void PermissionCallback( Permission permission ); 54 | public delegate void CameraCallback( string path ); 55 | 56 | #region Platform Specific Elements 57 | #if !UNITY_EDITOR && UNITY_ANDROID 58 | private static AndroidJavaClass m_ajc = null; 59 | private static AndroidJavaClass AJC 60 | { 61 | get 62 | { 63 | if( m_ajc == null ) 64 | m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeCamera" ); 65 | 66 | return m_ajc; 67 | } 68 | } 69 | 70 | private static AndroidJavaObject m_context = null; 71 | private static AndroidJavaObject Context 72 | { 73 | get 74 | { 75 | if( m_context == null ) 76 | { 77 | using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) ) 78 | { 79 | m_context = unityClass.GetStatic( "currentActivity" ); 80 | } 81 | } 82 | 83 | return m_context; 84 | } 85 | } 86 | #elif !UNITY_EDITOR && UNITY_IOS 87 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 88 | private static extern int _NativeCamera_CheckPermission(); 89 | 90 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 91 | private static extern void _NativeCamera_RequestPermission(); 92 | 93 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 94 | private static extern void _NativeCamera_OpenSettings(); 95 | 96 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 97 | private static extern int _NativeCamera_HasCamera(); 98 | 99 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 100 | private static extern void _NativeCamera_TakePicture( string imageSavePath, int maxSize, int preferredCamera ); 101 | 102 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 103 | private static extern void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera ); 104 | 105 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 106 | private static extern string _NativeCamera_GetImageProperties( string path ); 107 | 108 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 109 | private static extern string _NativeCamera_GetVideoProperties( string path ); 110 | 111 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 112 | private static extern string _NativeCamera_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds ); 113 | 114 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 115 | private static extern string _NativeCamera_LoadImageAtPath( string path, string temporaryFilePath, int maxSize ); 116 | #endif 117 | 118 | #if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS ) 119 | private static string m_temporaryImagePath = null; 120 | private static string TemporaryImagePath 121 | { 122 | get 123 | { 124 | if( m_temporaryImagePath == null ) 125 | { 126 | m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" ); 127 | Directory.CreateDirectory( Application.temporaryCachePath ); 128 | } 129 | 130 | return m_temporaryImagePath; 131 | } 132 | } 133 | #endif 134 | 135 | #if !UNITY_EDITOR && UNITY_IOS 136 | private static string m_iOSSelectedImagePath = null; 137 | private static string IOSSelectedImagePath 138 | { 139 | get 140 | { 141 | if( m_iOSSelectedImagePath == null ) 142 | { 143 | m_iOSSelectedImagePath = Path.Combine( Application.temporaryCachePath, "CameraImg" ); 144 | Directory.CreateDirectory( Application.temporaryCachePath ); 145 | } 146 | 147 | return m_iOSSelectedImagePath; 148 | } 149 | } 150 | #endif 151 | #endregion 152 | 153 | #region Runtime Permissions 154 | public static bool CheckPermission( bool isPicturePermission ) 155 | { 156 | #if !UNITY_EDITOR && UNITY_ANDROID 157 | return AJC.CallStatic( "CheckPermission", Context, isPicturePermission ) == 1; 158 | #elif !UNITY_EDITOR && UNITY_IOS 159 | return _NativeCamera_CheckPermission() == 1; 160 | #else 161 | return true; 162 | #endif 163 | } 164 | 165 | public static void RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission ) 166 | { 167 | #if !UNITY_EDITOR && UNITY_ANDROID 168 | NCPermissionCallbackAndroid nativeCallback = new( callback ); 169 | AJC.CallStatic( "RequestPermission", Context, nativeCallback, isPicturePermission ); 170 | #elif !UNITY_EDITOR && UNITY_IOS 171 | NCPermissionCallbackiOS.Initialize( callback ); 172 | _NativeCamera_RequestPermission(); 173 | #else 174 | callback( Permission.Granted ); 175 | #endif 176 | } 177 | 178 | public static Task RequestPermissionAsync( bool isPicturePermission ) 179 | { 180 | TaskCompletionSource tcs = new TaskCompletionSource(); 181 | RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), isPicturePermission ); 182 | return tcs.Task; 183 | } 184 | 185 | public static void OpenSettings() 186 | { 187 | #if !UNITY_EDITOR && UNITY_ANDROID 188 | AJC.CallStatic( "OpenSettings", Context ); 189 | #elif !UNITY_EDITOR && UNITY_IOS 190 | _NativeCamera_OpenSettings(); 191 | #endif 192 | } 193 | #endregion 194 | 195 | #region Camera Functions 196 | public static void TakePicture( CameraCallback callback, int maxSize = -1, bool saveAsJPEG = true, PreferredCamera preferredCamera = PreferredCamera.Default ) 197 | { 198 | RequestPermissionAsync( ( permission ) => 199 | { 200 | if( permission != Permission.Granted || IsCameraBusy() ) 201 | { 202 | callback?.Invoke( null ); 203 | return; 204 | } 205 | 206 | #if UNITY_EDITOR 207 | string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select image", "", new string[] { "Image files", "png,jpg,jpeg", "All files", "*" } ); 208 | 209 | if( callback != null ) 210 | callback( pickedFile != "" ? pickedFile : null ); 211 | #elif UNITY_ANDROID 212 | AJC.CallStatic( "TakePicture", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera ); 213 | #elif UNITY_IOS 214 | if( maxSize <= 0 ) 215 | maxSize = SystemInfo.maxTextureSize; 216 | 217 | NCCameraCallbackiOS.Initialize( callback ); 218 | _NativeCamera_TakePicture( IOSSelectedImagePath + ( saveAsJPEG ? ".jpeg" : ".png" ), maxSize, (int) preferredCamera ); 219 | #else 220 | if( callback != null ) 221 | callback( null ); 222 | #endif 223 | }, true ); 224 | } 225 | 226 | public static void RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default ) 227 | { 228 | RequestPermissionAsync( ( permission ) => 229 | { 230 | if( permission != Permission.Granted || IsCameraBusy() ) 231 | { 232 | callback?.Invoke( null ); 233 | return; 234 | } 235 | 236 | #if UNITY_EDITOR 237 | string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select video", "", new string[] { "Video files", "mp4,mov,webm,avi", "All files", "*" } ); 238 | 239 | if( callback != null ) 240 | callback( pickedFile != "" ? pickedFile : null ); 241 | #elif UNITY_ANDROID 242 | AJC.CallStatic( "RecordVideo", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera, (int) quality, maxDuration, maxSizeBytes ); 243 | #elif UNITY_IOS 244 | NCCameraCallbackiOS.Initialize( callback ); 245 | _NativeCamera_RecordVideo( (int) quality, maxDuration, (int) preferredCamera ); 246 | #else 247 | if( callback != null ) 248 | callback( null ); 249 | #endif 250 | }, false ); 251 | } 252 | 253 | public static bool DeviceHasCamera() 254 | { 255 | #if !UNITY_EDITOR && UNITY_ANDROID 256 | return AJC.CallStatic( "HasCamera", Context ); 257 | #elif !UNITY_EDITOR && UNITY_IOS 258 | return _NativeCamera_HasCamera() == 1; 259 | #else 260 | return true; 261 | #endif 262 | } 263 | 264 | public static bool IsCameraBusy() 265 | { 266 | #if !UNITY_EDITOR && UNITY_IOS 267 | return NCCameraCallbackiOS.IsBusy; 268 | #else 269 | return false; 270 | #endif 271 | } 272 | #endregion 273 | 274 | #region Utility Functions 275 | public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) 276 | { 277 | if( string.IsNullOrEmpty( imagePath ) ) 278 | throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); 279 | 280 | if( !File.Exists( imagePath ) ) 281 | throw new FileNotFoundException( "File not found at " + imagePath ); 282 | 283 | if( maxSize <= 0 ) 284 | maxSize = SystemInfo.maxTextureSize; 285 | 286 | #if !UNITY_EDITOR && UNITY_ANDROID 287 | string loadPath = AJC.CallStatic( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize ); 288 | #elif !UNITY_EDITOR && UNITY_IOS 289 | string loadPath = _NativeCamera_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize ); 290 | #else 291 | string loadPath = imagePath; 292 | #endif 293 | 294 | string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); 295 | TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; 296 | 297 | Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace ); 298 | 299 | try 300 | { 301 | if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) 302 | { 303 | Debug.LogWarning( "Couldn't load image at path: " + loadPath ); 304 | 305 | Object.DestroyImmediate( result ); 306 | return null; 307 | } 308 | } 309 | catch( Exception e ) 310 | { 311 | Debug.LogException( e ); 312 | 313 | Object.DestroyImmediate( result ); 314 | return null; 315 | } 316 | finally 317 | { 318 | if( loadPath != imagePath ) 319 | { 320 | try 321 | { 322 | File.Delete( loadPath ); 323 | } 324 | catch { } 325 | } 326 | } 327 | 328 | return result; 329 | } 330 | 331 | public static async Task LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true ) 332 | { 333 | if( string.IsNullOrEmpty( imagePath ) ) 334 | throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); 335 | 336 | if( !File.Exists( imagePath ) ) 337 | throw new FileNotFoundException( "File not found at " + imagePath ); 338 | 339 | if( maxSize <= 0 ) 340 | maxSize = SystemInfo.maxTextureSize; 341 | 342 | #if !UNITY_EDITOR && UNITY_ANDROID 343 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread 344 | string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) ); 345 | #elif !UNITY_EDITOR && UNITY_IOS 346 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread 347 | string loadPath = await Task.Run( () => _NativeCamera_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) ); 348 | #else 349 | string loadPath = imagePath; 350 | #endif 351 | 352 | Texture2D result = null; 353 | 354 | using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) ) 355 | { 356 | UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest(); 357 | while( !asyncOperation.isDone ) 358 | await Task.Yield(); 359 | 360 | if( www.result != UnityWebRequest.Result.Success ) 361 | Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error ); 362 | else 363 | result = DownloadHandlerTexture.GetContent( www ); 364 | } 365 | 366 | if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong 367 | { 368 | string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); 369 | TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; 370 | 371 | result = new Texture2D( 2, 2, format, true, false ); 372 | 373 | try 374 | { 375 | if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) 376 | { 377 | Debug.LogWarning( "Couldn't load image at path: " + loadPath ); 378 | 379 | Object.DestroyImmediate( result ); 380 | return null; 381 | } 382 | } 383 | catch( Exception e ) 384 | { 385 | Debug.LogException( e ); 386 | 387 | Object.DestroyImmediate( result ); 388 | return null; 389 | } 390 | finally 391 | { 392 | if( loadPath != imagePath ) 393 | { 394 | try 395 | { 396 | File.Delete( loadPath ); 397 | } 398 | catch { } 399 | } 400 | } 401 | } 402 | 403 | return result; 404 | } 405 | 406 | public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) 407 | { 408 | if( maxSize <= 0 ) 409 | maxSize = SystemInfo.maxTextureSize; 410 | 411 | #if !UNITY_EDITOR && UNITY_ANDROID 412 | string thumbnailPath = AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ); 413 | #elif !UNITY_EDITOR && UNITY_IOS 414 | string thumbnailPath = _NativeCamera_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds ); 415 | #else 416 | string thumbnailPath = null; 417 | #endif 418 | 419 | if( !string.IsNullOrEmpty( thumbnailPath ) ) 420 | return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace ); 421 | else 422 | return null; 423 | } 424 | 425 | public static async Task GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true ) 426 | { 427 | if( maxSize <= 0 ) 428 | maxSize = SystemInfo.maxTextureSize; 429 | 430 | #if !UNITY_EDITOR && UNITY_ANDROID 431 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread 432 | string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) ); 433 | #elif !UNITY_EDITOR && UNITY_IOS 434 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread 435 | string thumbnailPath = await Task.Run( () => _NativeCamera_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) ); 436 | #else 437 | string thumbnailPath = null; 438 | #endif 439 | 440 | if( !string.IsNullOrEmpty( thumbnailPath ) ) 441 | return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable ); 442 | else 443 | return null; 444 | } 445 | 446 | #if UNITY_ANDROID 447 | private static async Task TryCallNativeAndroidFunctionOnSeparateThread( Func function ) 448 | { 449 | T result = default( T ); 450 | bool hasResult = false; 451 | 452 | await Task.Run( () => 453 | { 454 | if( AndroidJNI.AttachCurrentThread() != 0 ) 455 | Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" ); 456 | else 457 | { 458 | try 459 | { 460 | result = function(); 461 | hasResult = true; 462 | } 463 | finally 464 | { 465 | AndroidJNI.DetachCurrentThread(); 466 | } 467 | } 468 | } ); 469 | 470 | return hasResult ? result : function(); 471 | } 472 | #endif 473 | 474 | public static ImageProperties GetImageProperties( string imagePath ) 475 | { 476 | if( !File.Exists( imagePath ) ) 477 | throw new FileNotFoundException( "File not found at " + imagePath ); 478 | 479 | #if !UNITY_EDITOR && UNITY_ANDROID 480 | string value = AJC.CallStatic( "GetImageProperties", Context, imagePath ); 481 | #elif !UNITY_EDITOR && UNITY_IOS 482 | string value = _NativeCamera_GetImageProperties( imagePath ); 483 | #else 484 | string value = null; 485 | #endif 486 | 487 | int width = 0, height = 0; 488 | string mimeType = null; 489 | ImageOrientation orientation = ImageOrientation.Unknown; 490 | if( !string.IsNullOrEmpty( value ) ) 491 | { 492 | string[] properties = value.Split( '>' ); 493 | if( properties != null && properties.Length >= 4 ) 494 | { 495 | if( !int.TryParse( properties[0].Trim(), out width ) ) 496 | width = 0; 497 | if( !int.TryParse( properties[1].Trim(), out height ) ) 498 | height = 0; 499 | 500 | mimeType = properties[2].Trim(); 501 | if( mimeType.Length == 0 ) 502 | { 503 | string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); 504 | if( extension == ".png" ) 505 | mimeType = "image/png"; 506 | else if( extension == ".jpg" || extension == ".jpeg" ) 507 | mimeType = "image/jpeg"; 508 | else if( extension == ".gif" ) 509 | mimeType = "image/gif"; 510 | else if( extension == ".bmp" ) 511 | mimeType = "image/bmp"; 512 | else 513 | mimeType = null; 514 | } 515 | 516 | int orientationInt; 517 | if( int.TryParse( properties[3].Trim(), out orientationInt ) ) 518 | orientation = (ImageOrientation) orientationInt; 519 | } 520 | } 521 | 522 | return new ImageProperties( width, height, mimeType, orientation ); 523 | } 524 | 525 | public static VideoProperties GetVideoProperties( string videoPath ) 526 | { 527 | if( !File.Exists( videoPath ) ) 528 | throw new FileNotFoundException( "File not found at " + videoPath ); 529 | 530 | #if !UNITY_EDITOR && UNITY_ANDROID 531 | string value = AJC.CallStatic( "GetVideoProperties", Context, videoPath ); 532 | #elif !UNITY_EDITOR && UNITY_IOS 533 | string value = _NativeCamera_GetVideoProperties( videoPath ); 534 | #else 535 | string value = null; 536 | #endif 537 | 538 | int width = 0, height = 0; 539 | long duration = 0L; 540 | float rotation = 0f; 541 | if( !string.IsNullOrEmpty( value ) ) 542 | { 543 | string[] properties = value.Split( '>' ); 544 | if( properties != null && properties.Length >= 4 ) 545 | { 546 | if( !int.TryParse( properties[0].Trim(), out width ) ) 547 | width = 0; 548 | if( !int.TryParse( properties[1].Trim(), out height ) ) 549 | height = 0; 550 | if( !long.TryParse( properties[2].Trim(), out duration ) ) 551 | duration = 0L; 552 | if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) ) 553 | rotation = 0f; 554 | } 555 | } 556 | 557 | if( rotation == -90f ) 558 | rotation = 270f; 559 | 560 | return new VideoProperties( width, height, duration, rotation ); 561 | } 562 | #endregion 563 | } -------------------------------------------------------------------------------- /Plugins/NativeCamera/NativeCamera.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff758a73b21d4a04aa6f95679b3da605 3 | timeCreated: 1498722610 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/README.txt: -------------------------------------------------------------------------------- 1 | = Native Camera for Android & iOS (v1.5.0) = 2 | 3 | Documentation: https://github.com/yasirkula/UnityNativeCamera 4 | FAQ: https://github.com/yasirkula/UnityNativeCamera#faq 5 | Example code: https://github.com/yasirkula/UnityNativeCamera#example-code 6 | E-mail: yasirkula@gmail.com -------------------------------------------------------------------------------- /Plugins/NativeCamera/README.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5a88d1b1b9d7b904b862304c20ed4db4 3 | timeCreated: 1563308465 4 | licenseType: Free 5 | TextScriptImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5576acd2a06eb72409e6ec4b7f204a4e 3 | folderAsset: yes 4 | timeCreated: 1498722622 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS/NCCameraCallbackiOS.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR || UNITY_IOS 2 | using UnityEngine; 3 | 4 | namespace NativeCameraNamespace 5 | { 6 | public class NCCameraCallbackiOS : MonoBehaviour 7 | { 8 | private static NCCameraCallbackiOS instance; 9 | private NativeCamera.CameraCallback callback; 10 | 11 | private float nextBusyCheckTime; 12 | 13 | public static bool IsBusy { get; private set; } 14 | 15 | [System.Runtime.InteropServices.DllImport( "__Internal" )] 16 | private static extern int _NativeCamera_IsCameraBusy(); 17 | 18 | public static void Initialize( NativeCamera.CameraCallback callback ) 19 | { 20 | if( IsBusy ) 21 | return; 22 | 23 | if( instance == null ) 24 | { 25 | instance = new GameObject( "NCCameraCallbackiOS" ).AddComponent(); 26 | DontDestroyOnLoad( instance.gameObject ); 27 | } 28 | 29 | instance.callback = callback; 30 | 31 | instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f; 32 | IsBusy = true; 33 | } 34 | 35 | private void Update() 36 | { 37 | if( IsBusy ) 38 | { 39 | if( Time.realtimeSinceStartup >= nextBusyCheckTime ) 40 | { 41 | nextBusyCheckTime = Time.realtimeSinceStartup + 1f; 42 | 43 | if( _NativeCamera_IsCameraBusy() == 0 ) 44 | { 45 | IsBusy = false; 46 | 47 | NativeCamera.CameraCallback _callback = callback; 48 | callback = null; 49 | 50 | if( _callback != null ) 51 | _callback( null ); 52 | } 53 | } 54 | } 55 | } 56 | 57 | [UnityEngine.Scripting.Preserve] 58 | public void OnMediaReceived( string path ) 59 | { 60 | IsBusy = false; 61 | 62 | if( string.IsNullOrEmpty( path ) ) 63 | path = null; 64 | 65 | NativeCamera.CameraCallback _callback = callback; 66 | callback = null; 67 | 68 | if( _callback != null ) 69 | _callback( path ); 70 | } 71 | } 72 | } 73 | #endif -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS/NCCameraCallbackiOS.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d8f19d5713752dc41bd377562677d8ee 3 | timeCreated: 1519060539 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR || UNITY_IOS 2 | using UnityEngine; 3 | 4 | namespace NativeCameraNamespace 5 | { 6 | public class NCPermissionCallbackiOS : MonoBehaviour 7 | { 8 | private static NCPermissionCallbackiOS instance; 9 | private NativeCamera.PermissionCallback callback; 10 | 11 | public static void Initialize( NativeCamera.PermissionCallback callback ) 12 | { 13 | if( instance == null ) 14 | { 15 | instance = new GameObject( "NCPermissionCallbackiOS" ).AddComponent(); 16 | DontDestroyOnLoad( instance.gameObject ); 17 | } 18 | else if( instance.callback != null ) 19 | instance.callback( NativeCamera.Permission.ShouldAsk ); 20 | 21 | instance.callback = callback; 22 | } 23 | 24 | [UnityEngine.Scripting.Preserve] 25 | public void OnPermissionRequested( string message ) 26 | { 27 | NativeCamera.PermissionCallback _callback = callback; 28 | callback = null; 29 | 30 | if( _callback != null ) 31 | _callback( (NativeCamera.Permission) int.Parse( message ) ); 32 | } 33 | } 34 | } 35 | #endif -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1efd0cf9fb7457142b76fb1bc8672e8d 3 | timeCreated: 1519060539 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS/NativeCamera.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | 7 | extern UIViewController* UnityGetGLViewController(); 8 | 9 | @interface UNativeCamera:NSObject 10 | + (int)checkPermission; 11 | + (void)requestPermission; 12 | + (void)openSettings; 13 | + (int)hasCamera; 14 | + (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration; 15 | + (int)isCameraBusy; 16 | + (char *)getImageProperties:(NSString *)path; 17 | + (char *)getVideoProperties:(NSString *)path; 18 | + (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime; 19 | + (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize; 20 | @end 21 | 22 | @implementation UNativeCamera 23 | 24 | static NSString *pickedMediaSavePath; 25 | static UIImagePickerController *imagePicker; 26 | static int cameraMaxImageSize = -1; 27 | static int imagePickerState = 0; // 0 -> none, 1 -> showing, 2 -> finished 28 | static BOOL recordingVideo = NO; 29 | static AVAudioSessionCategory unityAudioSessionCategory = AVAudioSessionCategoryAmbient; 30 | static NSUInteger unityAudioSessionCategoryOptions = 1; 31 | static AVAudioSessionMode unityAudioSessionMode = AVAudioSessionModeDefault; 32 | 33 | // Credit: https://stackoverflow.com/a/20464727/2373034 34 | + (int)checkPermission 35 | { 36 | AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 37 | if( status == AVAuthorizationStatusAuthorized ) 38 | return 1; 39 | else if( status == AVAuthorizationStatusNotDetermined ) 40 | return 2; 41 | else 42 | return 0; 43 | } 44 | 45 | // Credit: https://stackoverflow.com/a/20464727/2373034 46 | + (void)requestPermission 47 | { 48 | AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 49 | if( status == AVAuthorizationStatusAuthorized ) 50 | UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", "1" ); 51 | else if( status == AVAuthorizationStatusNotDetermined ) 52 | { 53 | [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) 54 | { 55 | UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", granted ? "1" : "0" ); 56 | }]; 57 | } 58 | else 59 | UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", "0" ); 60 | } 61 | 62 | // Credit: https://stackoverflow.com/a/25453667/2373034 63 | + (void)openSettings 64 | { 65 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; 66 | } 67 | 68 | + (int)hasCamera 69 | { 70 | return [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] ? 1 : 0; 71 | } 72 | 73 | // Credit: https://stackoverflow.com/a/10531752/2373034 74 | + (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration 75 | { 76 | if( ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] ) 77 | { 78 | NSLog( @"Device has no registered cameras!" ); 79 | 80 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" ); 81 | return; 82 | } 83 | 84 | if( ( imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeImage] ) || 85 | ( !imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeMovie] ) ) 86 | { 87 | NSLog( @"Camera does not support this operation!" ); 88 | 89 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" ); 90 | return; 91 | } 92 | 93 | imagePicker = [[UIImagePickerController alloc] init]; 94 | imagePicker.delegate = (id) self; 95 | imagePicker.allowsEditing = NO; 96 | imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; 97 | 98 | if( imageMode ) 99 | imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage]; 100 | else 101 | { 102 | imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeMovie]; 103 | 104 | if( maxVideoDuration > 0 ) 105 | imagePicker.videoMaximumDuration = maxVideoDuration; 106 | 107 | if( videoQuality == 0 ) 108 | imagePicker.videoQuality = UIImagePickerControllerQualityTypeLow; 109 | else if( videoQuality == 1 ) 110 | imagePicker.videoQuality = UIImagePickerControllerQualityTypeMedium; 111 | else if( videoQuality == 2 ) 112 | imagePicker.videoQuality = UIImagePickerControllerQualityTypeHigh; 113 | } 114 | 115 | if( defaultCamera == 0 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear] ) 116 | imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear; 117 | else if( defaultCamera == 1 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront] ) 118 | imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront; 119 | 120 | // Bugfix for https://github.com/yasirkula/UnityNativeCamera/issues/45 121 | if( !imageMode ) 122 | { 123 | unityAudioSessionCategory = [[AVAudioSession sharedInstance] category]; 124 | unityAudioSessionCategoryOptions = [[AVAudioSession sharedInstance] categoryOptions]; 125 | unityAudioSessionMode = [[AVAudioSession sharedInstance] mode]; 126 | } 127 | 128 | recordingVideo = !imageMode; 129 | pickedMediaSavePath = imageSavePath; 130 | cameraMaxImageSize = maxImageSize; 131 | 132 | imagePickerState = 1; 133 | [UnityGetGLViewController() presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }]; 134 | } 135 | 136 | + (int)isCameraBusy 137 | { 138 | if( imagePickerState == 2 ) 139 | return 1; 140 | 141 | if( imagePicker != nil ) 142 | { 143 | if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() ) 144 | return 1; 145 | 146 | imagePicker = nil; 147 | [self restoreAudioSession]; 148 | 149 | return 0; 150 | } 151 | 152 | return 0; 153 | } 154 | 155 | + (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info 156 | { 157 | NSString *path = nil; 158 | if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] ) 159 | { 160 | NSLog( @"UIImagePickerController finished taking picture" ); 161 | 162 | UIImage *image = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage]; 163 | if( image == nil ) 164 | path = nil; 165 | else 166 | { 167 | NSString *extension = [pickedMediaSavePath pathExtension]; 168 | BOOL saveAsJPEG = [extension caseInsensitiveCompare:@"jpg"] == NSOrderedSame || [extension caseInsensitiveCompare:@"jpeg"] == NSOrderedSame; 169 | 170 | // Try to save the image with metadata 171 | // CANCELED: a number of users reported that this method results in 90-degree rotated images, uncomment at your own risk 172 | // Credit: https://stackoverflow.com/a/15858955 173 | /*NSDictionary *metadata = [info objectForKey:UIImagePickerControllerMediaMetadata]; 174 | NSMutableDictionary *mutableMetadata = nil; 175 | CFDictionaryRef metadataRef; 176 | CFStringRef imageType; 177 | 178 | if( saveAsJPEG ) 179 | { 180 | mutableMetadata = [metadata mutableCopy]; 181 | [mutableMetadata setObject:@(1.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality]; 182 | 183 | metadataRef = (__bridge CFDictionaryRef) mutableMetadata; 184 | imageType = kUTTypeJPEG; 185 | } 186 | else 187 | { 188 | metadataRef = (__bridge CFDictionaryRef) metadata; 189 | imageType = kUTTypePNG; 190 | } 191 | 192 | CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:pickedMediaSavePath], imageType , 1, NULL ); 193 | if( imageDestination == NULL ) 194 | NSLog( @"Failed to create image destination" ); 195 | else 196 | { 197 | CGImageDestinationAddImage( imageDestination, image.CGImage, metadataRef ); 198 | if( CGImageDestinationFinalize( imageDestination ) ) 199 | path = pickedMediaSavePath; 200 | else 201 | NSLog( @"Failed to finalize the image" ); 202 | 203 | CFRelease( imageDestination ); 204 | }*/ 205 | 206 | if( path == nil ) 207 | { 208 | //NSLog( @"Attempting to save the image without metadata as fallback" ); 209 | 210 | if( ( saveAsJPEG && [UIImageJPEGRepresentation( [self scaleImage:image maxSize:cameraMaxImageSize], 1.0 ) writeToFile:pickedMediaSavePath atomically:YES] ) || 211 | ( !saveAsJPEG && [UIImagePNGRepresentation( [self scaleImage:image maxSize:cameraMaxImageSize] ) writeToFile:pickedMediaSavePath atomically:YES] ) ) 212 | path = pickedMediaSavePath; 213 | else 214 | { 215 | NSLog( @"Error saving image without metadata" ); 216 | path = nil; 217 | } 218 | } 219 | } 220 | } 221 | else 222 | { 223 | NSLog( @"UIImagePickerController finished recording video" ); 224 | 225 | #pragma clang diagnostic push 226 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 227 | NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL]; 228 | #pragma clang diagnostic pop 229 | if( mediaUrl == nil ) 230 | path = nil; 231 | else 232 | path = [mediaUrl path]; 233 | } 234 | 235 | imagePicker = nil; 236 | imagePickerState = 2; 237 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", [self getCString:path] ); 238 | 239 | [picker dismissViewControllerAnimated:NO completion:nil]; 240 | [self restoreAudioSession]; 241 | } 242 | 243 | + (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker 244 | { 245 | NSLog( @"UIImagePickerController cancelled" ); 246 | 247 | imagePicker = nil; 248 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" ); 249 | 250 | [picker dismissViewControllerAnimated:NO completion:nil]; 251 | [self restoreAudioSession]; 252 | } 253 | 254 | // Bugfix for https://github.com/yasirkula/UnityNativeCamera/issues/45 255 | + (void)restoreAudioSession 256 | { 257 | if( recordingVideo ) 258 | { 259 | NSError *error = nil; 260 | if( ![[AVAudioSession sharedInstance] setCategory:unityAudioSessionCategory mode:unityAudioSessionMode options:unityAudioSessionCategoryOptions error:&error] ) 261 | { 262 | if( error != nil ) 263 | NSLog( @"Error setting audio session category back to %@ with mode %@ and options %lu: %@", unityAudioSessionCategory, unityAudioSessionMode, (unsigned long) unityAudioSessionCategoryOptions, error ); 264 | else 265 | NSLog( @"Error setting audio session category back to %@ with mode %@ and options %lu", unityAudioSessionCategory, unityAudioSessionMode, (unsigned long) unityAudioSessionCategoryOptions ); 266 | } 267 | } 268 | } 269 | 270 | // Credit: https://stackoverflow.com/a/4170099/2373034 271 | + (NSArray *)getImageMetadata:(NSString *)path 272 | { 273 | int width = 0; 274 | int height = 0; 275 | int orientation = -1; 276 | 277 | CGImageSourceRef imageSource = CGImageSourceCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:path], nil ); 278 | if( imageSource != nil ) 279 | { 280 | NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache]; 281 | CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex( imageSource, 0, (__bridge CFDictionaryRef) options ); 282 | CFRelease( imageSource ); 283 | 284 | CGFloat widthF = 0.0f, heightF = 0.0f; 285 | if( imageProperties != nil ) 286 | { 287 | if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelWidth ) ) 288 | CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelWidth ), kCFNumberCGFloatType, &widthF ); 289 | 290 | if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelHeight ) ) 291 | CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelHeight ), kCFNumberCGFloatType, &heightF ); 292 | 293 | if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyOrientation ) ) 294 | { 295 | CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyOrientation ), kCFNumberIntType, &orientation ); 296 | 297 | if( orientation > 4 ) 298 | { 299 | // Landscape image 300 | CGFloat temp = widthF; 301 | widthF = heightF; 302 | heightF = temp; 303 | } 304 | } 305 | 306 | CFRelease( imageProperties ); 307 | } 308 | 309 | width = (int) roundf( widthF ); 310 | height = (int) roundf( heightF ); 311 | } 312 | 313 | return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil]; 314 | } 315 | 316 | + (char *)getImageProperties:(NSString *)path 317 | { 318 | NSArray *metadata = [self getImageMetadata:path]; 319 | 320 | int orientationUnity; 321 | int orientation = [metadata[2] intValue]; 322 | 323 | // To understand the magic numbers, see ImageOrientation enum in NativeCamera.cs 324 | // and http://sylvana.net/jpegcrop/exif_orientation.html 325 | if( orientation == 1 ) 326 | orientationUnity = 0; 327 | else if( orientation == 2 ) 328 | orientationUnity = 4; 329 | else if( orientation == 3 ) 330 | orientationUnity = 2; 331 | else if( orientation == 4 ) 332 | orientationUnity = 6; 333 | else if( orientation == 5 ) 334 | orientationUnity = 5; 335 | else if( orientation == 6 ) 336 | orientationUnity = 1; 337 | else if( orientation == 7 ) 338 | orientationUnity = 7; 339 | else if( orientation == 8 ) 340 | orientationUnity = 3; 341 | else 342 | orientationUnity = -1; 343 | 344 | return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]]; 345 | } 346 | 347 | + (char *)getVideoProperties:(NSString *)path 348 | { 349 | CGSize size = CGSizeZero; 350 | float rotation = 0; 351 | long long duration = 0; 352 | 353 | AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil]; 354 | if( asset != nil ) 355 | { 356 | duration = (long long) round( CMTimeGetSeconds( [asset duration] ) * 1000 ); 357 | CGAffineTransform transform = [asset preferredTransform]; 358 | NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo]; 359 | if( videoTracks != nil && [videoTracks count] > 0 ) 360 | { 361 | size = [[videoTracks objectAtIndex:0] naturalSize]; 362 | transform = [[videoTracks objectAtIndex:0] preferredTransform]; 363 | } 364 | 365 | rotation = atan2( transform.b, transform.a ) * ( 180.0 / M_PI ); 366 | } 367 | 368 | return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int) roundf( size.width ), (int) roundf( size.height ), duration, rotation]]; 369 | } 370 | 371 | + (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime 372 | { 373 | AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]]; 374 | thumbnailGenerator.appliesPreferredTrackTransform = YES; 375 | thumbnailGenerator.maximumSize = CGSizeMake( (CGFloat) maximumSize, (CGFloat) maximumSize ); 376 | thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero; 377 | thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero; 378 | 379 | if( captureTime < 0.0 ) 380 | captureTime = 0.0; 381 | else 382 | { 383 | AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil]; 384 | if( asset != nil ) 385 | { 386 | double videoDuration = CMTimeGetSeconds( [asset duration] ); 387 | if( videoDuration > 0.0 && captureTime >= videoDuration - 0.1 ) 388 | { 389 | if( captureTime > videoDuration ) 390 | captureTime = videoDuration; 391 | 392 | thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds( 1.0, 600 ); 393 | } 394 | } 395 | } 396 | 397 | NSError *error = nil; 398 | CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds( captureTime, 600 ) actualTime:nil error:&error]; 399 | if( image == nil ) 400 | { 401 | if( error != nil ) 402 | NSLog( @"Error generating video thumbnail: %@", error ); 403 | else 404 | NSLog( @"Error generating video thumbnail..." ); 405 | 406 | return [self getCString:@""]; 407 | } 408 | 409 | UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image]; 410 | CGImageRelease( image ); 411 | 412 | if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] ) 413 | { 414 | NSLog( @"Error saving thumbnail image" ); 415 | return [self getCString:@""]; 416 | } 417 | 418 | return [self getCString:savePath]; 419 | } 420 | 421 | + (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize 422 | { 423 | CGFloat width = image.size.width; 424 | CGFloat height = image.size.height; 425 | 426 | UIImageOrientation orientation = image.imageOrientation; 427 | if( width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown && 428 | orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight && 429 | orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored && 430 | orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored ) 431 | return image; 432 | 433 | CGFloat scaleX = 1.0f; 434 | CGFloat scaleY = 1.0f; 435 | if( width > maxSize ) 436 | scaleX = maxSize / width; 437 | if( height > maxSize ) 438 | scaleY = maxSize / height; 439 | 440 | // Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m 441 | CGImageAlphaInfo alpha = CGImageGetAlphaInfo( image.CGImage ); 442 | BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast; 443 | 444 | CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY; 445 | CGRect imageRect = CGRectMake( 0, 0, width * scaleRatio, height * scaleRatio ); 446 | UIGraphicsImageRendererFormat *format = [image imageRendererFormat]; 447 | format.opaque = !hasAlpha; 448 | format.scale = image.scale; 449 | UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format]; 450 | image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext ) 451 | { 452 | [image drawInRect:imageRect]; 453 | }]; 454 | 455 | return image; 456 | } 457 | 458 | + (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize 459 | { 460 | // Check if the image can be loaded by Unity without requiring a conversion to PNG 461 | // Credit: https://stackoverflow.com/a/12048937/2373034 462 | NSString *extension = [path pathExtension]; 463 | BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame; 464 | 465 | if( !conversionNeeded ) 466 | { 467 | // Check if the image needs to be processed at all 468 | NSArray *metadata = [self getImageMetadata:path]; 469 | int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range 470 | if( orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize ) 471 | return [self getCString:path]; 472 | } 473 | 474 | UIImage *image = [UIImage imageWithContentsOfFile:path]; 475 | if( image == nil ) 476 | return [self getCString:path]; 477 | 478 | UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize]; 479 | if( conversionNeeded || scaledImage != image ) 480 | { 481 | if( ![UIImagePNGRepresentation( scaledImage ) writeToFile:tempFilePath atomically:YES] ) 482 | { 483 | NSLog( @"Error creating scaled image" ); 484 | return [self getCString:path]; 485 | } 486 | 487 | return [self getCString:tempFilePath]; 488 | } 489 | else 490 | return [self getCString:path]; 491 | } 492 | 493 | // Credit: https://stackoverflow.com/a/37052118/2373034 494 | + (char *)getCString:(NSString *)source 495 | { 496 | if( source == nil ) 497 | source = @""; 498 | 499 | const char *sourceUTF8 = [source UTF8String]; 500 | char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 ); 501 | strcpy( result, sourceUTF8 ); 502 | 503 | return result; 504 | } 505 | 506 | @end 507 | 508 | extern "C" int _NativeCamera_CheckPermission() 509 | { 510 | return [UNativeCamera checkPermission]; 511 | } 512 | 513 | extern "C" void _NativeCamera_RequestPermission() 514 | { 515 | [UNativeCamera requestPermission]; 516 | } 517 | 518 | extern "C" void _NativeCamera_OpenSettings() 519 | { 520 | [UNativeCamera openSettings]; 521 | } 522 | 523 | extern "C" int _NativeCamera_HasCamera() 524 | { 525 | return [UNativeCamera hasCamera]; 526 | } 527 | 528 | extern "C" void _NativeCamera_TakePicture( const char* imageSavePath, int maxSize, int preferredCamera ) 529 | { 530 | [UNativeCamera openCamera:YES defaultCamera:preferredCamera savePath:[NSString stringWithUTF8String:imageSavePath] maxImageSize:maxSize videoQuality:-1 maxVideoDuration:-1]; 531 | } 532 | 533 | extern "C" void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera ) 534 | { 535 | [UNativeCamera openCamera:NO defaultCamera:preferredCamera savePath:nil maxImageSize:4096 videoQuality:quality maxVideoDuration:maxDuration]; 536 | } 537 | 538 | extern "C" int _NativeCamera_IsCameraBusy() 539 | { 540 | return [UNativeCamera isCameraBusy]; 541 | } 542 | 543 | extern "C" char* _NativeCamera_GetImageProperties( const char* path ) 544 | { 545 | return [UNativeCamera getImageProperties:[NSString stringWithUTF8String:path]]; 546 | } 547 | 548 | extern "C" char* _NativeCamera_GetVideoProperties( const char* path ) 549 | { 550 | return [UNativeCamera getVideoProperties:[NSString stringWithUTF8String:path]]; 551 | } 552 | 553 | extern "C" char* _NativeCamera_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds ) 554 | { 555 | return [UNativeCamera getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds]; 556 | } 557 | 558 | extern "C" char* _NativeCamera_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize ) 559 | { 560 | return [UNativeCamera loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize]; 561 | } -------------------------------------------------------------------------------- /Plugins/NativeCamera/iOS/NativeCamera.mm.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f71ce2f3d3a5dbd46af575e628ed9d6e 3 | timeCreated: 1498722774 4 | licenseType: Pro 5 | PluginImporter: 6 | serializedVersion: 2 7 | iconMap: {} 8 | executionOrder: {} 9 | isPreloaded: 0 10 | isOverridable: 0 11 | platformData: 12 | data: 13 | first: 14 | Any: 15 | second: 16 | enabled: 0 17 | settings: {} 18 | data: 19 | first: 20 | Editor: Editor 21 | second: 22 | enabled: 0 23 | settings: 24 | DefaultValueInitialized: true 25 | data: 26 | first: 27 | iPhone: iOS 28 | second: 29 | enabled: 1 30 | settings: {} 31 | userData: 32 | assetBundleName: 33 | assetBundleVariant: 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.yasirkula.nativecamera", 3 | "displayName": "Native Camera", 4 | "version": "1.5.0", 5 | "documentationUrl": "https://github.com/yasirkula/UnityNativeCamera", 6 | "changelogUrl": "https://github.com/yasirkula/UnityNativeCamera/releases", 7 | "licensesUrl": "https://github.com/yasirkula/UnityNativeCamera/blob/master/LICENSE.txt", 8 | "description": "This plugin helps you take pictures/record videos natively with your device's camera on Android and iOS. It has built-in support for runtime permissions, as well." 9 | } 10 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 16b0316fc31bcf540a33a6b4363ec8bf 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------