Skip to content
Permalink
Browse files

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 #7988

Differential Revision: D3401251

Pulled By: nicklockwood

fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
  • Loading branch information
corbt authored and Facebook Github Bot 7 committed Jun 7, 2016
1 parent d4e7c8a commit 7357ccc37067133859c663f0b907c58801ac25c9
@@ -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);
}

/**
@@ -82,28 +82,42 @@ @implementation RCTCameraRollManager
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,
@@ -117,37 +117,42 @@ public String getName() {
* @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
protected void doInBackgroundGuarded(Void... params) {
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 @@ protected void doInBackgroundGuarded(Void... params) {
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();

0 comments on commit 7357ccc

Please sign in to comment.
You can’t perform that action at this time.