diff --git a/README.md b/README.md index 9dcd968..3a63f4c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # react-native-image-crop-picker -iOS/Android image picker with support for camera, multiple images and cropping +iOS/Android image picker with support for camera, video compression, multiple images and cropping ## Result @@ -68,7 +68,8 @@ ImagePicker.clean().then(() => { | multiple | bool (default false) | Enable or disable multiple image selection | | includeBase64 | bool (default false) | Enable or disable returning base64 data with image | | maxFiles (ios only) | number (default 5) | Max number of files to select when using `multiple` option | -| smartAlbums (ios only) | array ['UserLibrary', 'PhotoStream', 'Panoramas', 'Videos', 'Bursts'] | Remove smart albums or rearrange order | +| compressVideo (ios only) | number (default true) | When video is selected, compress it and convert it to mp4 | +| smartAlbums (ios only) | array (default ['UserLibrary', 'PhotoStream', 'Panoramas', 'Videos', 'Bursts']) | #### Response Object diff --git a/android/build.gradle b/android/build.gradle index 10c066d..23c8546 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,6 +7,7 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 23 + vectorDrawables.useSupportLibrary = true versionCode 1 } lintOptions { @@ -16,5 +17,5 @@ android { dependencies { compile 'com.facebook.react:react-native:+' - compile 'com.yalantis:ucrop:2.1.2' + compile 'com.yalantis:ucrop:2.2.0-native' } \ No newline at end of file diff --git a/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java index 1489057..73f18f2 100644 --- a/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java +++ b/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java @@ -7,6 +7,7 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.PixelFormat; +import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; @@ -28,6 +29,7 @@ import com.facebook.react.bridge.WritableNativeMap; import android.support.v4.app.ActivityCompat; import android.content.pm.PackageManager; +import android.webkit.MimeTypeMap; import com.yalantis.ucrop.UCrop; @@ -58,9 +60,9 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity private static final String E_CAMERA_IS_NOT_AVAILABLE = "E_CAMERA_IS_NOT_AVAILABLE"; private static final String E_CANNOT_LAUNCH_CAMERA = "E_CANNOT_LAUNCH_CAMERA"; private static final String E_PERMISSIONS_MISSING = "E_PERMISSIONS_MISSING"; + private static final String E_ERROR_WHILE_CLEANING_FILES = "E_ERROR_WHILE_CLEANING_FILES"; private Promise mPickerPromise; - private Activity activity; private boolean cropping = false; private boolean multiple = false; @@ -77,6 +79,15 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity mReactContext = reactContext; } + public String getTmpDir() { + String tmpDir = mReactContext.getCacheDir() + "/react-native-image-crop-picker"; + Boolean created = new File(tmpDir).mkdir(); + + System.out.println(tmpDir); + + return tmpDir; + } + @Override public String getName() { return "ImageCropPicker"; @@ -90,14 +101,52 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity cropping = options.hasKey("cropping") ? options.getBoolean("cropping") : cropping; } - @ReactMethod - public void clean(final Promise promise) { - promise.resolve(null); + private void deleteRecursive(File fileOrDirectory) { + if (fileOrDirectory.isDirectory()) { + for (File child : fileOrDirectory.listFiles()) { + deleteRecursive(child); + } + } + + fileOrDirectory.delete(); } @ReactMethod - public void cleanSingle(final String path, final Promise promise) { - promise.resolve(null); + public void clean(final Promise promise) { + try { + File file = new File(this.getTmpDir()); + if (!file.exists()) throw new Exception("File does not exist"); + + this.deleteRecursive(file); + promise.resolve(null); + } catch (Exception ex) { + ex.printStackTrace(); + promise.reject(E_ERROR_WHILE_CLEANING_FILES, ex.getMessage()); + } + } + + @ReactMethod + public void cleanSingle(String path, final Promise promise) { + if (path == null) { + promise.reject(E_ERROR_WHILE_CLEANING_FILES, "Cannot cleanup empty path"); + return; + } + + try { + final String filePrefix = "file://"; + if (path.startsWith(filePrefix)) { + path = path.substring(filePrefix.length()); + } + + File file = new File(path); + if (!file.exists()) throw new Exception("File does not exist. Path: " + path); + + this.deleteRecursive(file); + promise.resolve(null); + } catch (Exception ex) { + ex.printStackTrace(); + promise.reject(E_ERROR_WHILE_CLEANING_FILES, ex.getMessage()); + } } @ReactMethod @@ -110,7 +159,7 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity return; } - activity = getCurrentActivity(); + Activity activity = getCurrentActivity(); if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); @@ -147,7 +196,7 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity @ReactMethod public void openPicker(final ReadableMap options, final Promise promise) { - activity = getCurrentActivity(); + Activity activity = getCurrentActivity(); if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); @@ -159,7 +208,13 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity try { final Intent galleryIntent = new Intent(Intent.ACTION_PICK); - galleryIntent.setType("image/*"); + + if (cropping) { + galleryIntent.setType("image/*"); + } else { + galleryIntent.setType("image/*,video/*"); + } + galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); galleryIntent.setAction(Intent.ACTION_GET_CONTENT); galleryIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); @@ -198,7 +253,50 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity return Base64.encodeToString(bytes, Base64.NO_WRAP); } - private WritableMap getImage(Uri uri, boolean resolvePath) { + public static String getMimeType(String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + + return type; + } + + public WritableMap getSelection(Activity activity, Uri uri) throws Exception { + String path = RealPathUtil.getRealPathFromURI(activity, uri); + if (path == null || path.isEmpty()) { + throw new Exception("Cannot resolve image path."); + } + + String mime = getMimeType(path); + if (mime != null && mime.startsWith("video/")) { + return getVideo(path, mime); + } + + return getImage(activity, uri, true); + } + + public WritableMap getVideo(String path, String mime) { + WritableMap image = new WritableNativeMap(); + + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(path); + Bitmap bmp = retriever.getFrameAtTime(); + + if (bmp != null) { + image.putInt("width", bmp.getWidth()); + image.putInt("height", bmp.getHeight()); + } + + image.putString("path", "file://" + path); + image.putString("mime", mime); + image.putInt("size", (int) new File(path).length()); + + return image; + } + + private WritableMap getImage(Activity activity, Uri uri, boolean resolvePath) throws Exception { WritableMap image = new WritableNativeMap(); String path = uri.getPath(); @@ -206,20 +304,28 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity path = RealPathUtil.getRealPathFromURI(activity, uri); } + if (path == null || path.isEmpty()) { + throw new Exception("Cannot resolve image path."); + } + + if (path.startsWith("http://") || path.startsWith("https://")) { + throw new Exception("Cannot select remote files"); + } + BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - long fileLen = 0; - if (path != null) { - fileLen = new File(path).length(); + BitmapFactory.decodeFile(path, options); + + if (options.outMimeType == null || options.outWidth == 0 || options.outHeight == 0) { + throw new Exception("Invalid image selected"); } - BitmapFactory.decodeFile(path, options); image.putString("path", "file://" + path); image.putInt("width", options.outWidth); image.putInt("height", options.outHeight); image.putString("mime", options.outMimeType); - image.putInt("size", (int) fileLen); + image.putInt("size", (int) new File(path).length()); if (includeBase64) { image.putString("data", getBase64StringFromFile(path)); @@ -228,18 +334,18 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity return image; } - public void startCropping(Uri uri) { + public void startCropping(Activity activity, Uri uri) { UCrop.Options options = new UCrop.Options(); options.setCompressionFormat(Bitmap.CompressFormat.JPEG); - UCrop.of(uri, Uri.fromFile(new File(activity.getCacheDir(), UUID.randomUUID().toString() + ".jpg"))) + UCrop.of(uri, Uri.fromFile(new File(this.getTmpDir(), UUID.randomUUID().toString() + ".jpg"))) .withMaxResultSize(width, height) .withAspectRatio(width, height) .withOptions(options) .start(activity); } - public void imagePickerResult(final int requestCode, final int resultCode, final Intent data) { + public void imagePickerResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (mPickerPromise == null) { return; } @@ -251,53 +357,79 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity ClipData clipData = data.getClipData(); WritableArray result = new WritableNativeArray(); - // only one image selected - if (clipData == null) { - result.pushMap(getImage(data.getData(), true)); - } else { - for (int i = 0; i < clipData.getItemCount(); i++) { - result.pushMap(getImage(clipData.getItemAt(i).getUri(), true)); + try { + // only one image selected + if (clipData == null) { + result.pushMap(getSelection(activity, data.getData())); + } else { + for (int i = 0; i < clipData.getItemCount(); i++) { + result.pushMap(getSelection(activity, clipData.getItemAt(i).getUri())); + } } + + mPickerPromise.resolve(result); + } catch (Exception ex) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } - mPickerPromise.resolve(result); } else { Uri uri = data.getData(); - if (cropping && uri != null) { - startCropping(uri); + if (uri == null) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "Cannot resolve image url"); + } + + if (cropping) { + startCropping(activity, uri); } else { - mPickerPromise.resolve(getImage(uri, true)); + try { + mPickerPromise.resolve(getSelection(activity, uri)); + } catch (Exception ex) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); + } } } } } - public void cameraPickerResult(final int requestCode, final int resultCode, final Intent data) { + public void cameraPickerResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (mPickerPromise == null) { return; } if (resultCode == Activity.RESULT_CANCELED) { mPickerPromise.reject(E_PICKER_CANCELLED_KEY, E_PICKER_CANCELLED_MSG); - } else if (resultCode == Activity.RESULT_OK && mCameraCaptureURI != null) { + } else if (resultCode == Activity.RESULT_OK) { Uri uri = mCameraCaptureURI; + if (uri == null) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "Cannot resolve image url"); + return; + } + if (cropping) { UCrop.Options options = new UCrop.Options(); options.setCompressionFormat(Bitmap.CompressFormat.JPEG); - startCropping(uri); + startCropping(activity, uri); } else { - mPickerPromise.resolve(getImage(uri, true)); + try { + mPickerPromise.resolve(getImage(activity, uri, true)); + } catch (Exception ex) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); + } } } } - public void croppingResult(final int requestCode, final int resultCode, final Intent data) { + public void croppingResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (data != null) { final Uri resultUri = UCrop.getOutput(data); if (resultUri != null) { - mPickerPromise.resolve(getImage(resultUri, false)); + try { + mPickerPromise.resolve(getImage(activity, resultUri, false)); + } catch (Exception ex) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); + } } else { mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "Cannot find image data"); } @@ -307,13 +439,13 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity } @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { + public void onActivityResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (requestCode == IMAGE_PICKER_REQUEST) { - imagePickerResult(requestCode, resultCode, data); + imagePickerResult(activity, requestCode, resultCode, data); } else if (requestCode == CAMERA_PICKER_REQUEST) { - cameraPickerResult(requestCode, resultCode, data); + cameraPickerResult(activity, requestCode, resultCode, data); } else if (requestCode == UCrop.REQUEST_CROP) { - croppingResult(requestCode, resultCode, data); + croppingResult(activity, requestCode, resultCode, data); } } @@ -344,7 +476,7 @@ public class PickerModule extends ReactContextBaseJavaModule implements Activity private File createNewFile(final boolean forcePictureDirectory) { String filename = "image-" + UUID.randomUUID().toString() + ".jpg"; if (tmpImage && (!forcePictureDirectory)) { - return new File(mReactContext.getCacheDir(), filename); + return new File(this.getTmpDir(), filename); } else { File path = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES); diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b0827ec..effa3dc 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -88,7 +88,7 @@ android { defaultConfig { applicationId "com.example" - minSdkVersion 16 + minSdkVersion 18 targetSdkVersion 22 versionCode 1 versionName "1.0" @@ -126,6 +126,7 @@ android { } dependencies { + compile project(':react-native-video') compile project(':react-native-image-crop-picker') compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:23.1.1" diff --git a/example/android/app/src/main/java/com/example/MainApplication.java b/example/android/app/src/main/java/com/example/MainApplication.java index a03baab..d602dd6 100644 --- a/example/android/app/src/main/java/com/example/MainApplication.java +++ b/example/android/app/src/main/java/com/example/MainApplication.java @@ -3,6 +3,7 @@ package com.example; import android.app.Application; import com.facebook.react.ReactApplication; +import com.brentvatne.react.ReactVideoPackage; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; @@ -23,6 +24,7 @@ public class MainApplication extends Application implements ReactApplication { protected List getPackages() { return Arrays.asList( new MainReactPackage(), + new ReactVideoPackage(), new PickerPackage() ); } diff --git a/example/android/app/src/main/res/values/strings.xml b/example/android/app/src/main/res/values/strings.xml index d75426c..d941c05 100644 --- a/example/android/app/src/main/res/values/strings.xml +++ b/example/android/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ + example diff --git a/example/android/build.gradle b/example/android/build.gradle index 46047bd..c8978a9 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 88654f2..53fd95e 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Aug 26 00:38:15 CEST 2016 +#Sat Sep 10 03:05:34 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index b915a6c..2aa498d 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,6 +1,8 @@ rootProject.name = 'example' include ':app' +include ':react-native-video' +project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android') include ':react-native-image-crop-picker' project(':react-native-image-crop-picker').projectDir = new File(settingsDir, '../../android') \ No newline at end of file diff --git a/example/app.js b/example/app.js index 95e29a3..04848ad 100644 --- a/example/app.js +++ b/example/app.js @@ -1,7 +1,11 @@ import React, {Component} from 'react'; -import {View, Text, StyleSheet, ScrollView, Image, TouchableOpacity} from 'react-native'; +import { + View, Text, StyleSheet, ScrollView, + Image, TouchableOpacity, NativeModules, Dimensions +} from 'react-native'; + +import Video from 'react-native-video'; -import {NativeModules, Dimensions} from 'react-native'; var ImagePicker = NativeModules.ImageCropPicker; const styles = StyleSheet.create({ @@ -57,7 +61,7 @@ export default class App extends Component { image: {uri: `data:${image.mime};base64,`+ image.data, width: image.width, height: image.height}, images: null }); - }).catch(e => {}); + }).catch(e => alert(e)); } cleanupImages() { @@ -83,14 +87,15 @@ export default class App extends Component { ImagePicker.openPicker({ width: 300, height: 300, - cropping: cropit + cropping: cropit, + compressVideo: true }).then(image => { console.log('received image', image); this.setState({ - image: {uri: image.path, width: image.width, height: image.height}, + image: {uri: image.path, width: image.width, height: image.height, mime: image.mime}, images: null }); - }).catch(e => {}); + }).catch(e => alert(e)); } pickMultiple() { @@ -101,21 +106,54 @@ export default class App extends Component { image: null, images: images.map(i => { console.log('received image', i); - return {uri: i.path, width: i.width, height: i.height}; + return {uri: i.path, width: i.width, height: i.height, mime: i.mime}; }) }); - }).catch(e => {}); + }).catch(e => alert(e)); } scaledHeight(oldW, oldH, newW) { return (oldH / oldW) * newW; } + renderVideo(uri) { + return + ; + } + + renderImage(image) { + return + } + + renderAsset(image) { + if (image.mime && image.mime.toLowerCase().indexOf('video/') !== -1) { + return this.renderVideo(image.uri); + } + + return this.renderImage(image); + } + render() { return - {this.state.image ? : null} - {this.state.images ? this.state.images.map(i => ) : null} + {this.state.image ? this.renderAsset(this.state.image) : null} + {this.state.images ? this.state.images.map(i => {this.renderAsset(i)}) : null} this.pickSingleWithCamera(false)} style={styles.button}> diff --git a/example/ios/example.xcodeproj/project.pbxproj b/example/ios/example.xcodeproj/project.pbxproj index 4c60fa1..87f0202 100644 --- a/example/ios/example.xcodeproj/project.pbxproj +++ b/example/ios/example.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 34A9DDBD1D7F43320012B1F5 /* QBImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34A9DDB81D7F43220012B1F5 /* QBImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 34A9DDBE1D7F43320012B1F5 /* RSKImageCropper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34A9DDB91D7F43220012B1F5 /* RSKImageCropper.framework */; }; 34A9DDBF1D7F43320012B1F5 /* RSKImageCropper.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34A9DDB91D7F43220012B1F5 /* RSKImageCropper.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7165791E23AC464E8943EEF6 /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FE03BA1664084F9C9C188799 /* libRCTVideo.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; /* End PBXBuildFile section */ @@ -94,6 +95,13 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; + 34534DEB1D8374A6005D9519 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 69779CA3792A4BD79EBAC2CD /* RCTVideo.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTVideo; + }; 3460B9DF1D6BA58300CCEC39 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3460B9D91D6BA58300CCEC39 /* imageCropPicker.xcodeproj */; @@ -155,8 +163,10 @@ 3460B9D91D6BA58300CCEC39 /* imageCropPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = imageCropPicker.xcodeproj; path = ../../ios/imageCropPicker.xcodeproj; sourceTree = ""; }; 34A9DDB81D7F43220012B1F5 /* QBImagePicker.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = QBImagePicker.framework; sourceTree = ""; }; 34A9DDB91D7F43220012B1F5 /* RSKImageCropper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RSKImageCropper.framework; sourceTree = ""; }; + 69779CA3792A4BD79EBAC2CD /* RCTVideo.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTVideo.xcodeproj; path = "../node_modules/react-native-video/RCTVideo.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + FE03BA1664084F9C9C188799 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTVideo.a; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -185,6 +195,7 @@ 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, 34A9DDBC1D7F43320012B1F5 /* QBImagePicker.framework in Frameworks */, + 7165791E23AC464E8943EEF6 /* libRCTVideo.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -286,6 +297,14 @@ name = Products; sourceTree = ""; }; + 34534DDF1D8374A6005D9519 /* Products */ = { + isa = PBXGroup; + children = ( + 34534DEC1D8374A6005D9519 /* libRCTVideo.a */, + ); + name = Products; + sourceTree = ""; + }; 3460B9DA1D6BA58300CCEC39 /* Products */ = { isa = PBXGroup; children = ( @@ -325,6 +344,7 @@ 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, + 69779CA3792A4BD79EBAC2CD /* RCTVideo.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -405,7 +425,7 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0730; + LastUpgradeCheck = 730; ORGANIZATIONNAME = Facebook; TargetAttributes = { 00E356ED1AD99517003FC87E = { @@ -465,6 +485,10 @@ ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; }, + { + ProductGroup = 34534DDF1D8374A6005D9519 /* Products */; + ProjectRef = 69779CA3792A4BD79EBAC2CD /* RCTVideo.xcodeproj */; + }, { ProductGroup = 139FDEE71B06529A00C62182 /* Products */; ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; @@ -539,6 +563,13 @@ remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 34534DEC1D8374A6005D9519 /* libRCTVideo.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTVideo.a; + remoteRef = 34534DEB1D8374A6005D9519 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 3460B9E01D6BA58300CCEC39 /* libimageCropPicker.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -650,6 +681,10 @@ INFOPLIST_FILE = exampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/example"; @@ -664,6 +699,10 @@ INFOPLIST_FILE = exampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/example"; @@ -685,6 +724,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../node_modules/react-native-video", ); INFOPLIST_FILE = example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -713,6 +753,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../node_modules/react-native-video", ); INFOPLIST_FILE = example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -766,6 +807,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../node_modules/react-native-video", ); IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; @@ -806,6 +848,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../node_modules/react-native-video", ); IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; diff --git a/example/package.json b/example/package.json index a46ab6e..f51929a 100644 --- a/example/package.json +++ b/example/package.json @@ -6,8 +6,9 @@ "start": "node node_modules/react-native/local-cli/cli.js start" }, "dependencies": { - "react": "15.2.1", - "react-native": "0.31.0", - "react-native-image-crop-picker": "../" + "react": "^15.3.1", + "react-native": "^0.33.0", + "react-native-image-crop-picker": "../", + "react-native-video": "^0.9.0" } } diff --git a/ios/ImageCropPicker.m b/ios/ImageCropPicker.m index 780ae92..ee86f52 100644 --- a/ios/ImageCropPicker.m +++ b/ios/ImageCropPicker.m @@ -22,6 +22,9 @@ #define ERROR_CANNOT_SAVE_IMAGE_KEY @"cannot_save_image" #define ERROR_CANNOT_SAVE_IMAGE_MSG @"Cannot save image. Unable to write to tmp location." +#define ERROR_CANNOT_PROCESS_VIDEO_KEY @"cannot_process_video" +#define ERROR_CANNOT_PROCESS_VIDEO_MSG @"Cannot process video data" + @implementation ImageCropPicker RCT_EXPORT_MODULE(); @@ -33,6 +36,7 @@ RCT_EXPORT_MODULE(); @"multiple": @NO, @"cropping": @NO, @"includeBase64": @NO, + @"compressVideo": @YES, @"maxFiles": @5, @"width": @200, @"height": @200 @@ -117,8 +121,17 @@ RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options } - (NSString*) getTmpDirectory { - NSString* TMP_DIRECTORY = @"/react-native-image-crop-picker/"; - return [NSTemporaryDirectory() stringByAppendingString:TMP_DIRECTORY]; + NSString *TMP_DIRECTORY = @"react-native-image-crop-picker/"; + NSString *tmpFullPath = [NSTemporaryDirectory() stringByAppendingString:TMP_DIRECTORY]; + + BOOL isDir; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:tmpFullPath isDirectory:&isDir]; + if (!exists) { + [[NSFileManager defaultManager] createDirectoryAtPath: tmpFullPath + withIntermediateDirectories:YES attributes:nil error:nil]; + } + + return tmpFullPath; } - (BOOL)cleanTmpDirectory { @@ -172,7 +185,6 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options imagePickerController.allowsMultipleSelection = [[self.options objectForKey:@"multiple"] boolValue]; imagePickerController.maximumNumberOfSelection = [[self.options objectForKey:@"maxFiles"] intValue]; imagePickerController.showsNumberOfSelectedAssets = YES; - imagePickerController.mediaType = QBImagePickerMediaTypeImage; if ([self.options objectForKey:@"smartAlbums"] != nil) { NSDictionary *smartAlbums = @{ @@ -191,6 +203,12 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options imagePickerController.assetCollectionSubtypes = albumsToShow; } + if ([[self.options objectForKey:@"cropping"] boolValue]) { + imagePickerController.mediaType = QBImagePickerMediaTypeImage; + } else { + imagePickerController.mediaType = QBImagePickerMediaTypeAny; + } + dispatch_async(dispatch_get_main_queue(), ^{ [[self getRootVC] presentViewController:imagePickerController animated:YES completion:nil]; }); @@ -198,6 +216,121 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options }]; } +- (void)convertVideoToLowQuailtyWithInputURL:(NSURL*)inputURL + outputURL:(NSURL*)outputURL + handler:(void (^)(AVAssetExportSession*))handler { + [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil]; + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:inputURL options:nil]; + AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetMediumQuality]; + exportSession.outputURL = outputURL; + exportSession.outputFileType = AVFileTypeMPEG4; + [exportSession exportAsynchronouslyWithCompletionHandler:^(void) + { + handler(exportSession); + }]; +} + +- (void)showActivityIndicator:(void (^)(UIActivityIndicatorView*, UIView*))handler { + UIView *mainView = [[self getRootVC] view]; + + // create overlay + UIView *loadingView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds]; + loadingView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5]; + loadingView.clipsToBounds = YES; + + // create loading spinner + UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + activityView.frame = CGRectMake(65, 40, activityView.bounds.size.width, activityView.bounds.size.height); + activityView.center = loadingView.center; + [loadingView addSubview:activityView]; + + // create message + UILabel *loadingLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 115, 130, 22)]; + loadingLabel.backgroundColor = [UIColor clearColor]; + loadingLabel.textColor = [UIColor whiteColor]; + loadingLabel.adjustsFontSizeToFitWidth = YES; + CGPoint loadingLabelLocation = loadingView.center; + loadingLabelLocation.y += [activityView bounds].size.height; + loadingLabel.center = loadingLabelLocation; + loadingLabel.textAlignment = UITextAlignmentCenter; + loadingLabel.text = @"Processing assets..."; + [loadingLabel setFont:[UIFont boldSystemFontOfSize:18]]; + [loadingView addSubview:loadingLabel]; + + // show all + [mainView addSubview:loadingView]; + [activityView startAnimating]; + + handler(activityView, loadingView); +} + + +- (void) getVideoAsset:(PHAsset*)forAsset completion:(void (^)(NSDictionary* image))completion { + PHImageManager *manager = [PHImageManager defaultManager]; + PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init]; + options.version = PHVideoRequestOptionsVersionOriginal; + + [manager + requestAVAssetForVideo:forAsset + options:options + resultHandler:^(AVAsset * asset, AVAudioMix * audioMix, + NSDictionary *info) { + NSURL *sourceURL = [(AVURLAsset *)asset URL]; + AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; + CGSize dimensions = CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform); + + if (![[self.options objectForKey:@"compressVideo"] boolValue]) { + NSNumber *fileSizeValue = nil; + [sourceURL getResourceValue:&fileSizeValue + forKey:NSURLFileSizeKey + error:nil]; + + completion([self createAttachmentResponse:[sourceURL absoluteString] + withWidth:[NSNumber numberWithFloat:dimensions.width] + withHeight:[NSNumber numberWithFloat:dimensions.height] + withMime:[@"video/" stringByAppendingString:[[sourceURL pathExtension] lowercaseString]] + withSize:fileSizeValue + withData:[NSNull null]]); + return; + } + + // create temp file + NSString *tmpDirFullPath = [self getTmpDirectory]; + NSString *filePath = [tmpDirFullPath stringByAppendingString:[[NSUUID UUID] UUIDString]]; + filePath = [filePath stringByAppendingString:@".mp4"]; + NSURL *outputURL = [NSURL fileURLWithPath:filePath]; + + [self convertVideoToLowQuailtyWithInputURL:sourceURL outputURL:outputURL handler:^(AVAssetExportSession *exportSession) { + if (exportSession.status == AVAssetExportSessionStatusCompleted) { + NSNumber *fileSizeValue = nil; + [outputURL getResourceValue:&fileSizeValue + forKey:NSURLFileSizeKey + error:nil]; + + completion([self createAttachmentResponse:[outputURL absoluteString] + withWidth:[NSNumber numberWithFloat:dimensions.width] + withHeight:[NSNumber numberWithFloat:dimensions.height] + withMime:@"video/mp4" + withSize:fileSizeValue + withData:[NSNull null]]); + } else { + completion(nil); + } + }]; + }]; +} + +- (NSDictionary*) createAttachmentResponse:(NSString*)filePath withWidth:(NSNumber*)width withHeight:(NSNumber*)height withMime:(NSString*)mime withSize:(NSNumber*)size withData:(NSString*)data { + return @{ + @"path": filePath, + @"width": width, + @"height": height, + @"mime": mime, + @"size": size, + @"data": data, + }; +} + - (void)qb_imagePickerController: (QBImagePickerController *)imagePickerController didFinishPickingAssets:(NSArray *)assets { @@ -205,47 +338,101 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options PHImageManager *manager = [PHImageManager defaultManager]; if ([[[self options] objectForKey:@"multiple"] boolValue]) { - NSMutableArray *images = [[NSMutableArray alloc] init]; + NSMutableArray *selections = [[NSMutableArray alloc] init]; PHImageRequestOptions* options = [[PHImageRequestOptions alloc] init]; options.synchronous = YES; - for (PHAsset *asset in assets) { + [self showActivityIndicator:^(UIActivityIndicatorView *indicatorView, UIView *overlayView) { + NSLock *lock = [[NSLock alloc] init]; + __block int processed = 0; + + for (PHAsset *phAsset in assets) { + + if (phAsset.mediaType == PHAssetMediaTypeVideo) { + [self getVideoAsset:phAsset completion:^(NSDictionary* video) { + [lock lock]; + + if (video != nil) { + [selections addObject:video]; + } + + processed++; + [lock unlock]; + + if (processed == [assets count]) { + self.resolve(selections); + [indicatorView stopAnimating]; + [overlayView removeFromSuperview]; + [imagePickerController dismissViewControllerAnimated:YES completion:nil]; + return; + } + }]; + } else { + [manager + requestImageDataForAsset:phAsset + options:options + resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + UIImage *image = [UIImage imageWithData:imageData]; + NSData *data = UIImageJPEGRepresentation(image, 1); + + NSString *filePath = [self persistFile:data]; + if (filePath == nil) { + self.reject(ERROR_CANNOT_SAVE_IMAGE_KEY, ERROR_CANNOT_SAVE_IMAGE_MSG, nil); + [imagePickerController dismissViewControllerAnimated:YES completion:nil]; + return; + } + + [lock lock]; + [selections addObject:[self createAttachmentResponse:filePath + withWidth:@(phAsset.pixelWidth) + withHeight:@(phAsset.pixelHeight) + withMime:@"image/jpeg" + withSize:[NSNumber numberWithUnsignedInteger:data.length] + withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [data base64EncodedStringWithOptions:0] : [NSNull null] + ]]; + processed++; + [lock unlock]; + + if (processed == [assets count]) { + self.resolve(selections); + [indicatorView stopAnimating]; + [overlayView removeFromSuperview]; + [imagePickerController dismissViewControllerAnimated:YES completion:nil]; + return; + } + }]; + } + } + }]; + } else { + PHAsset *phAsset = [assets objectAtIndex:0]; + + if (phAsset.mediaType == PHAssetMediaTypeVideo) { + + [self showActivityIndicator:^(UIActivityIndicatorView *indicatorView, UIView *overlayView) { + [self getVideoAsset:phAsset completion:^(NSDictionary* video) { + if (video != nil) { + self.resolve(video); + } else { + self.reject(ERROR_CANNOT_PROCESS_VIDEO_KEY, ERROR_CANNOT_PROCESS_VIDEO_MSG, nil); + } + + + [indicatorView stopAnimating]; + [overlayView removeFromSuperview]; + [imagePickerController dismissViewControllerAnimated:YES completion:nil]; + }]; + }]; + } else { [manager - requestImageDataForAsset:asset - options:options - resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { - UIImage *image = [UIImage imageWithData:imageData]; - NSData *data = UIImageJPEGRepresentation(image, 1); - - NSString *filePath = [self persistFile:data]; - if (filePath == nil) { - self.reject(ERROR_CANNOT_SAVE_IMAGE_KEY, ERROR_CANNOT_SAVE_IMAGE_MSG, nil); - [imagePickerController dismissViewControllerAnimated:YES completion:nil]; - return; - } - - [images addObject:@{ - @"path": filePath, - @"width": @(asset.pixelWidth), - @"height": @(asset.pixelHeight), - @"mime": @"image/jpeg", - @"size": [NSNumber numberWithUnsignedInteger:data.length], - @"data": [[self.options objectForKey:@"includeBase64"] boolValue] ? [data base64EncodedStringWithOptions:0] : [NSNull null], - }]; + requestImageDataForAsset:phAsset + options:nil + resultHandler:^(NSData *imageData, NSString *dataUTI, + UIImageOrientation orientation, + NSDictionary *info) { + [self processSingleImagePick:[UIImage imageWithData:imageData] withViewController:imagePickerController]; }]; } - - self.resolve(images); - [imagePickerController dismissViewControllerAnimated:YES completion:nil]; - } else { - [manager - requestImageDataForAsset:[assets objectAtIndex:0] - options:nil - resultHandler:^(NSData *imageData, NSString *dataUTI, - UIImageOrientation orientation, - NSDictionary *info) { - [self processSingleImagePick:[UIImage imageWithData:imageData] withViewController:imagePickerController]; - }]; } } @@ -279,14 +466,13 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options return; } - self.resolve(@{ - @"path": filePath, - @"width": @(image.size.width), - @"height": @(image.size.height), - @"mime": @"image/jpeg", - @"size": [NSNumber numberWithUnsignedInteger:data.length], - @"data": [[self.options objectForKey:@"includeBase64"] boolValue] ? [data base64EncodedStringWithOptions:0] : [NSNull null], - }); + self.resolve([self createAttachmentResponse:filePath + withWidth:@(image.size.width) + withHeight:@(image.size.height) + withMime:@"image/jpeg" + withSize:[NSNumber numberWithUnsignedInteger:data.length] + withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [data base64EncodedStringWithOptions:0] : [NSNull null]]); + [viewController dismissViewControllerAnimated:YES completion:nil]; } } @@ -369,14 +555,12 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options return; } - self.resolve(@{ - @"path": filePath, - @"width": @(resizedImage.size.width), - @"height": @(resizedImage.size.height), - @"mime": @"image/jpeg", - @"size": [NSNumber numberWithUnsignedInteger:data.length], - @"data": [[self.options objectForKey:@"includeBase64"] boolValue] ? [data base64EncodedStringWithOptions:0] : [NSNull null], - }); + self.resolve([self createAttachmentResponse:filePath + withWidth:@(resizedImage.size.width) + withHeight:@(resizedImage.size.height) + withMime:@"image/jpeg" + withSize:[NSNumber numberWithUnsignedInteger:data.length] + withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [data base64EncodedStringWithOptions:0] : [NSNull null]]); [controller dismissViewControllerAnimated:YES completion:nil]; } @@ -384,15 +568,8 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options // at the moment it is not possible to upload image by reading PHAsset // we are saving image and saving it to the tmp location where we are allowed to access image later - (NSString*) persistFile:(NSData*)data { - // create tmp directory - NSString *tmpDirFullPath = [self getTmpDirectory]; - BOOL dirCreated = [[NSFileManager defaultManager] createDirectoryAtPath: tmpDirFullPath - withIntermediateDirectories:YES attributes:nil error:nil]; - if (!dirCreated) { - return nil; - } - // create temp file + NSString *tmpDirFullPath = [self getTmpDirectory]; NSString *filePath = [tmpDirFullPath stringByAppendingString:[[NSUUID UUID] UUIDString]]; filePath = [filePath stringByAppendingString:@".jpg"]; diff --git a/package.json b/package.json index 2873329..9dee150 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-image-crop-picker", - "version": "0.8.2", + "version": "0.9.1", "description": "Select single or multiple images, with croping option", "main": "index.js", "scripts": { @@ -29,6 +29,6 @@ }, "homepage": "https://github.com/ivpusic/react-native-image-crop-picker#readme", "peerDependencies": { - "react-native": ">=0.30.0" + "react-native": ">=0.33.0" } }