Allow CameraRoll to export videos
Summary: This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows: ``` // old saveImageWithTag(tag: string): Promise<string> // new saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string> ``` if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed. I've left in the `saveImageWithTag` method for now with a deprecation warning. **Test plan (required)** I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms. ```js // index.js /** * Sample React Native App * https://github.com/facebook/react-native * flow */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, CameraRoll, } from 'react-native'; import FS fro Closes https://github.com/facebook/react-native/pull/7988 Differential Revision: D3401251 Pulled By: nicklockwood fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
This commit is contained in:
parent
0a84d4a37c
commit
57af9ecbe0
|
@ -115,33 +115,45 @@ class CameraRoll {
|
|||
|
||||
static GroupTypesOptions: Array<string>;
|
||||
static AssetTypeOptions: Array<string>;
|
||||
|
||||
static saveImageWithTag(tag: string):Promise<*> {
|
||||
console.warn('CameraRoll.saveImageWithTag is deprecated. Use CameraRoll.saveToCameraRoll instead');
|
||||
return this.saveToCameraRoll(tag, 'photo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the image to the camera roll / gallery.
|
||||
* Saves the photo or video to the camera roll / gallery.
|
||||
*
|
||||
* On Android, the tag is a local URI, such as `"file:///sdcard/img.png"`.
|
||||
* On Android, the tag must be a local image or video URI, such as `"file:///sdcard/img.png"`.
|
||||
*
|
||||
* On iOS, the tag can be one of the following:
|
||||
* On iOS, the tag can be any image URI (including local, remote asset-library and base64 data URIs)
|
||||
* or a local video file URI (remote or data URIs are not supported for saving video at this time).
|
||||
*
|
||||
* - local URI
|
||||
* - assets-library tag
|
||||
* - a tag not matching any of the above, which means the image data will
|
||||
* be stored in memory (and consume memory as long as the process is alive)
|
||||
* If the tag has a file extension of .mov or .mp4, it will be inferred as a video. Otherwise
|
||||
* it will be treated as a photo. To override the automatic choice, you can pass an optional
|
||||
* `type` parameter that must be one of 'photo' or 'video'.
|
||||
*
|
||||
* Returns a Promise which when resolved will be passed the new URI.
|
||||
* Returns a Promise which will resolve with the new URI.
|
||||
*/
|
||||
static saveImageWithTag(tag) {
|
||||
static saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<*> {
|
||||
invariant(
|
||||
typeof tag === 'string',
|
||||
'CameraRoll.saveImageWithTag tag must be a valid string.'
|
||||
'CameraRoll.saveToCameraRoll must be a valid string.'
|
||||
);
|
||||
if (arguments.length > 1) {
|
||||
console.warn('CameraRoll.saveImageWithTag(tag, success, error) is deprecated. Use the returned Promise instead');
|
||||
const successCallback = arguments[1];
|
||||
const errorCallback = arguments[2] || ( () => {} );
|
||||
RCTCameraRollManager.saveImageWithTag(tag).then(successCallback, errorCallback);
|
||||
return;
|
||||
|
||||
invariant(
|
||||
type === 'photo' || type === 'video' || type === undefined,
|
||||
`The second argument to saveToCameraRoll must be 'photo' or 'video'. You passed ${type}`
|
||||
);
|
||||
|
||||
let mediaType = 'photo';
|
||||
if (type) {
|
||||
mediaType = type;
|
||||
} else if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) {
|
||||
mediaType = 'video';
|
||||
}
|
||||
return RCTCameraRollManager.saveImageWithTag(tag);
|
||||
|
||||
return RCTCameraRollManager.saveToCameraRoll(tag, mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -117,22 +117,26 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
|
|||
* @param promise to be resolved or rejected
|
||||
*/
|
||||
@ReactMethod
|
||||
public void saveImageWithTag(String uri, Promise promise) {
|
||||
new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), promise)
|
||||
public void saveToCameraRoll(String uri, String type, Promise promise) {
|
||||
MediaType parsedType = type.equals("video") ? MediaType.VIDEO : MediaType.PHOTO;
|
||||
new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), parsedType, promise)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private static class SaveImageTag extends GuardedAsyncTask<Void, Void> {
|
||||
private enum MediaType { PHOTO, VIDEO };
|
||||
private static class SaveToCameraRoll extends GuardedAsyncTask<Void, Void> {
|
||||
|
||||
private final Context mContext;
|
||||
private final Uri mUri;
|
||||
private final Promise mPromise;
|
||||
private final MediaType mType;
|
||||
|
||||
public SaveImageTag(ReactContext context, Uri uri, Promise promise) {
|
||||
public SaveToCameraRoll(ReactContext context, Uri uri, MediaType type, Promise promise) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
mUri = uri;
|
||||
mPromise = promise;
|
||||
mType = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -140,14 +144,15 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
|
|||
File source = new File(mUri.getPath());
|
||||
FileChannel input = null, output = null;
|
||||
try {
|
||||
File pictures =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
pictures.mkdirs();
|
||||
if (!pictures.isDirectory()) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available");
|
||||
File exportDir = (mType == MediaType.PHOTO)
|
||||
? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
exportDir.mkdirs();
|
||||
if (!exportDir.isDirectory()) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
|
||||
return;
|
||||
}
|
||||
File dest = new File(pictures, source.getName());
|
||||
File dest = new File(exportDir, source.getName());
|
||||
int n = 0;
|
||||
String fullSourceName = source.getName();
|
||||
String sourceName, sourceExt;
|
||||
|
@ -159,7 +164,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
|
|||
sourceExt = "";
|
||||
}
|
||||
while (!dest.createNewFile()) {
|
||||
dest = new File(pictures, sourceName + "_" + (n++) + sourceExt);
|
||||
dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
|
||||
}
|
||||
input = new FileInputStream(source).getChannel();
|
||||
output = new FileOutputStream(dest).getChannel();
|
||||
|
|
|
@ -82,28 +82,42 @@ RCT_EXPORT_MODULE()
|
|||
NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
|
||||
NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE";
|
||||
|
||||
RCT_EXPORT_METHOD(saveImageWithTag:(NSURLRequest *)imageRequest
|
||||
RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
||||
type:(NSString *)type
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
[_bridge.imageLoader loadImageWithURLRequest:imageRequest
|
||||
callback:^(NSError *loadError, UIImage *loadedImage) {
|
||||
if (loadError) {
|
||||
reject(RCTErrorUnableToLoad, nil, loadError);
|
||||
return;
|
||||
}
|
||||
// It's unclear if writeImageToSavedPhotosAlbum is thread-safe
|
||||
if ([type isEqualToString:@"video"]) {
|
||||
// It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
||||
[_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
||||
if (saveError) {
|
||||
RCTLogWarn(@"Error saving cropped image: %@", saveError);
|
||||
reject(RCTErrorUnableToSave, nil, saveError);
|
||||
} else {
|
||||
resolve(assetURL.absoluteString);
|
||||
}
|
||||
}];
|
||||
});
|
||||
}];
|
||||
} else {
|
||||
[_bridge.imageLoader loadImageWithURLRequest:request
|
||||
callback:^(NSError *loadError, UIImage *loadedImage) {
|
||||
if (loadError) {
|
||||
reject(RCTErrorUnableToLoad, nil, loadError);
|
||||
return;
|
||||
}
|
||||
// It's unclear if writeImageToSavedPhotosAlbum is thread-safe
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
||||
if (saveError) {
|
||||
RCTLogWarn(@"Error saving cropped image: %@", saveError);
|
||||
reject(RCTErrorUnableToSave, nil, saveError);
|
||||
} else {
|
||||
resolve(assetURL.absoluteString);
|
||||
}
|
||||
}];
|
||||
});
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
|
||||
|
|
Loading…
Reference in New Issue