Skip to content

Commit

Permalink
Upload full video when passed in a PHAsset
Browse files Browse the repository at this point in the history
Summary:
@public
Right now when you pass a ph:// video asset, we only ever return the image thumbnail of it. This is useful if you're displaying the ph:// in an <Image> but bad if you're trying to upload it.
This change keeps the original behaviour of displaying a thumbnail in an Image but fixes the latter behaviour, so that ph:// videos are uploaded correctly.
NOTE: There is a terrible hack to accomplish this. It is detailed in the code but essentially, we change the URL scheme to ph-upload:// when trying to upload it so that the default image loader doesn't try to process it.

Reviewed By: JoshuaGross

Differential Revision: D15129454

fbshipit-source-id: 18f87bec18b7cfa5edc1d60a47f23ac5d00675e0
  • Loading branch information
Mehdi Mulani authored and facebook-github-bot committed Apr 29, 2019
1 parent e7e1a93 commit 458e70c
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 38 deletions.
117 changes: 79 additions & 38 deletions Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m
Expand Up @@ -15,6 +15,7 @@
#import <MobileCoreServices/MobileCoreServices.h>

#import <React/RCTBridge.h>
#import <React/RCTNetworking.h>
#import <React/RCTUtils.h>

@implementation RCTAssetsLibraryRequestHandler
Expand All @@ -30,7 +31,8 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request
}

return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame
|| [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame;
|| [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame
|| [request.URL.scheme caseInsensitiveCompare:RCTNetworkingPHUploadHackScheme] == NSOrderedSame;
}

- (id)sendRequest:(NSURLRequest *)request
Expand All @@ -40,32 +42,38 @@ - (id)sendRequest:(NSURLRequest *)request
void (^cancellationBlock)(void) = ^{
atomic_store(&cancelled, YES);
};

NSURL *requestURL = request.URL;
BOOL isPHUpload = [requestURL.scheme caseInsensitiveCompare:RCTNetworkingPHUploadHackScheme] == NSOrderedSame;
if (isPHUpload) {
requestURL = [NSURL URLWithString:[@"ph" stringByAppendingString:[requestURL.absoluteString substringFromIndex:RCTNetworkingPHUploadHackScheme.length]]];
}

if (!request.URL) {
if (!requestURL) {
NSString *const msg = [NSString stringWithFormat:@"Cannot send request without URL"];
[delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)];
return cancellationBlock;
}

PHFetchResult<PHAsset *> *fetchResult;

if ([request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) {
if ([requestURL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) {
// Fetch assets using PHAsset localIdentifier (recommended)
NSString *const localIdentifier = [request.URL.absoluteString substringFromIndex:@"ph://".length];
NSString *const localIdentifier = [requestURL.absoluteString substringFromIndex:@"ph://".length];
fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil];
} else if ([request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
} else if ([requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
// This is the older, deprecated way of fetching assets from assets-library
// using the "assets-library://" protocol
fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[request.URL] options:nil];
fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[requestURL] options:nil];
} else {
NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", request.URL];
NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", requestURL];
[delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)];
return cancellationBlock;
}

if (![fetchResult firstObject]) {
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset"
" at URL %@ with no error message.", request.URL];
" at URL %@ with no error message.", requestURL];
NSError *error = RCTErrorWithMessage(errorMessage);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return cancellationBlock;
Expand All @@ -77,37 +85,70 @@ - (id)sendRequest:(NSURLRequest *)request

PHAsset *const _Nonnull asset = [fetchResult firstObject];

// By default, allow downloading images from iCloud
PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new];
requestOptions.networkAccessAllowed = YES;

[[PHImageManager defaultManager] requestImageDataForAsset:asset
options:requestOptions
resultHandler:^(NSData * _Nullable imageData,
NSString * _Nullable dataUTI,
UIImageOrientation orientation,
NSDictionary * _Nullable info) {
NSError *const error = [info objectForKey:PHImageErrorKey];
if (error) {
// When we're uploading a video, provide the full data but in any other case,
// provide only the thumbnail of the video.
if (asset.mediaType == PHAssetMediaTypeVideo && isPHUpload) {
PHVideoRequestOptions *const requestOptions = [PHVideoRequestOptions new];
requestOptions.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:requestOptions resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
NSError *error = [info objectForKey:PHImageErrorKey];
if (error) {
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}

if (![avAsset isKindOfClass:[AVURLAsset class]]) {
error = [NSError errorWithDomain:RCTErrorDomain code:0 userInfo:
@{
NSLocalizedDescriptionKey: @"Unable to load AVURLAsset",
}];
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}

NSData *data = [NSData dataWithContentsOfURL:((AVURLAsset *)avAsset).URL
options:(NSDataReadingOptions)0
error:&error];
if (data) {
NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:nil expectedContentLength:data.length textEncodingName:nil];
[delegate URLRequest:cancellationBlock didReceiveResponse:response];
[delegate URLRequest:cancellationBlock didReceiveData:data];
}
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}

NSInteger const length = [imageData length];
CFStringRef const dataUTIStringRef = (__bridge CFStringRef _Nonnull)(dataUTI);
CFStringRef const mimeType = UTTypeCopyPreferredTagWithClass(dataUTIStringRef, kUTTagClassMIMEType);

NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL
MIMEType:(__bridge NSString *)(mimeType)
expectedContentLength:length
textEncodingName:nil];
CFRelease(mimeType);

[delegate URLRequest:cancellationBlock didReceiveResponse:response];

[delegate URLRequest:cancellationBlock didReceiveData:imageData];
[delegate URLRequest:cancellationBlock didCompleteWithError:nil];
}];
}];
} else {
// By default, allow downloading images from iCloud
PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new];
requestOptions.networkAccessAllowed = YES;

[[PHImageManager defaultManager] requestImageDataForAsset:asset
options:requestOptions
resultHandler:^(NSData * _Nullable imageData,
NSString * _Nullable dataUTI,
UIImageOrientation orientation,
NSDictionary * _Nullable info) {
NSError *const error = [info objectForKey:PHImageErrorKey];
if (error) {
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}

NSInteger const length = [imageData length];
CFStringRef const dataUTIStringRef = (__bridge CFStringRef _Nonnull)(dataUTI);
CFStringRef const mimeType = UTTypeCopyPreferredTagWithClass(dataUTIStringRef, kUTTagClassMIMEType);

NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL
MIMEType:(__bridge NSString *)(mimeType)
expectedContentLength:length
textEncodingName:nil];
CFRelease(mimeType);

[delegate URLRequest:cancellationBlock didReceiveResponse:response];

[delegate URLRequest:cancellationBlock didReceiveData:imageData];
[delegate URLRequest:cancellationBlock didCompleteWithError:nil];
}];
}

return cancellationBlock;
}
Expand Down
11 changes: 11 additions & 0 deletions Libraries/Network/RCTNetworking.h
Expand Up @@ -60,3 +60,14 @@
@property (nonatomic, readonly) RCTNetworking *networking;

@end

// HACK: When uploading images/video from PHAssetLibrary, we change the URL scheme to be
// ph-upload://. This is to ensure that we upload a full video when given a ph-upload:// URL,
// instead of just the thumbnail. Consider the following problem:
// The user has a video in their camera roll with URL ph://1B3E2DDB-0AD3-4E33-A7A1-9F4AA9A762AA/L0/001
// 1. We want to display that video in an <Image> and show the thumbnail
// 2. We later want to upload that video.
// At this point, if we use the same URL for both uses, there is no way to distinguish the intent
// and we will either upload the thumbnail (bad!) or try to show the video in an <Image> (bad!).
// Our solution is to change the URL scheme in the uploader.
extern NSString *const RCTNetworkingPHUploadHackScheme;
12 changes: 12 additions & 0 deletions Libraries/Network/RCTNetworking.mm
Expand Up @@ -20,6 +20,8 @@

typedef RCTURLRequestCancellationBlock (^RCTHTTPQueryResult)(NSError *error, NSDictionary<NSString *, id> *result);

NSString *const RCTNetworkingPHUploadHackScheme = @"ph-upload";

@interface RCTNetworking ()

- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary<NSString *, id> *)data
Expand Down Expand Up @@ -76,6 +78,16 @@ - (RCTURLRequestCancellationBlock)process:(NSArray<NSDictionary *> *)formData
_multipartBody = [NSMutableData new];
_boundary = RCTGenerateFormBoundary();

for (NSUInteger i = 0; i < _parts.count; i++) {
NSString *uri = _parts[i][@"uri"];
if (uri && [[uri substringToIndex:@"ph:".length] caseInsensitiveCompare:@"ph:"] == NSOrderedSame) {
uri = [RCTNetworkingPHUploadHackScheme stringByAppendingString:[uri substringFromIndex:@"ph".length]];
NSMutableDictionary *mutableDict = [_parts[i] mutableCopy];
mutableDict[@"uri"] = uri;
_parts[i] = mutableDict;
}
}

return [_networker processDataForHTTPQuery:_parts[0] callback:^(NSError *error, NSDictionary<NSString *, id> *result) {
return [self handleResult:result error:error];
}];
Expand Down

0 comments on commit 458e70c

Please sign in to comment.