Skip to content

Commit

Permalink
Fix selecting videos from library in iOS 13
Browse files Browse the repository at this point in the history
Summary:
In iOS 13, Apple made a change that results in video URLs returned by UIImagePickerController becoming invalidated as soon as the info object from the delegate callback is released. This commit works around this issue by retaining these info objects by default and giving the application a way to release them once it is done processing the video.

See also https://stackoverflow.com/questions/57798968/didfinishpickingmediawithinfo-returns-different-url-in-ios-13

Reviewed By: olegbl, mmmulani

Differential Revision: D17845889

fbshipit-source-id: 12d0e496508dafa2581ef12730f7537ef98c60e2
  • Loading branch information
fatalsun authored and grabbou committed Oct 28, 2019
1 parent d328877 commit 5051bf2
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 1 deletion.
30 changes: 29 additions & 1 deletion Libraries/CameraRoll/RCTImagePickerManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ @implementation RCTImagePickerManager
NSMutableArray<UIImagePickerController *> *_pickers;
NSMutableArray<RCTResponseSenderBlock> *_pickerCallbacks;
NSMutableArray<RCTResponseSenderBlock> *_pickerCancelCallbacks;
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *_pendingVideoInfo;
}

RCT_EXPORT_MODULE(ImagePickerIOS);
Expand Down Expand Up @@ -131,6 +132,24 @@ - (dispatch_queue_t)methodQueue
cancelCallback:cancelCallback];
}

// In iOS 13, the URLs provided when selecting videos from the library are only valid while the
// info object provided by the delegate is retained.
// This method provides a way to clear out all retained pending info objects.
RCT_EXPORT_METHOD(clearAllPendingVideos)
{
[_pendingVideoInfo removeAllObjects];
_pendingVideoInfo = [NSMutableDictionary new];
}

// In iOS 13, the URLs provided when selecting videos from the library are only valid while the
// info object provided by the delegate is retained.
// This method provides a way to release the info object for a particular file url when the application
// is done with it, for example after the video has been uploaded or copied locally.
RCT_EXPORT_METHOD(removePendingVideo:(NSString *)url)
{
[_pendingVideoInfo removeObjectForKey:url];
}

- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info
{
Expand All @@ -146,7 +165,15 @@ - (void)imagePickerController:(UIImagePickerController *)picker
width = @(image.size.width);
}
if (imageURL) {
[self _dismissPicker:picker args:@[imageURL.absoluteString, RCTNullIfNil(height), RCTNullIfNil(width)]];
NSString *imageURLString = imageURL.absoluteString;
// In iOS 13, video URLs are only valid while info dictionary is retained
if (@available(iOS 13.0, *)) {
if (isMovie) {
_pendingVideoInfo[imageURLString] = info;
}
}

[self _dismissPicker:picker args:@[imageURLString, RCTNullIfNil(height), RCTNullIfNil(width)]];
return;
}

Expand Down Expand Up @@ -174,6 +201,7 @@ - (void)_presentPicker:(UIImagePickerController *)imagePicker
_pickers = [NSMutableArray new];
_pickerCallbacks = [NSMutableArray new];
_pickerCancelCallbacks = [NSMutableArray new];
_pendingVideoInfo = [NSMutableDictionary new];
}

[_pickers addObject:imagePicker];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,14 @@ + (RCTManagedPointer *)JS_NativeImagePickerIOS_SpecOpenSelectDialogConfig:(id)js
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "openSelectDialog", @selector(openSelectDialog:successCallback:cancelCallback:), args, count);
}

static facebook::jsi::Value __hostFunction_NativeImagePickerIOSSpecJSI_clearAllPendingVideos(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "clearAllPendingVideos", @selector(clearAllPendingVideos), args, count);
}

static facebook::jsi::Value __hostFunction_NativeImagePickerIOSSpecJSI_removePendingVideo(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "removePendingVideo", @selector(removePendingVideo:), args, count);
}


NativeImagePickerIOSSpecJSI::NativeImagePickerIOSSpecJSI(id<RCTTurboModule> instance, std::shared_ptr<JSCallInvoker> jsInvoker)
: ObjCTurboModule("ImagePickerIOS", instance, jsInvoker) {
Expand All @@ -1143,6 +1151,12 @@ + (RCTManagedPointer *)JS_NativeImagePickerIOS_SpecOpenSelectDialogConfig:(id)js

setMethodArgConversionSelector(@"openSelectDialog", 0, @"JS_NativeImagePickerIOS_SpecOpenSelectDialogConfig:");

methodMap_["clearAllPendingVideos"] = MethodMetadata {0, __hostFunction_NativeImagePickerIOSSpecJSI_clearAllPendingVideos};


methodMap_["removePendingVideo"] = MethodMetadata {1, __hostFunction_NativeImagePickerIOSSpecJSI_removePendingVideo};



}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,8 @@ namespace JS {
- (void)openSelectDialog:(JS::NativeImagePickerIOS::SpecOpenSelectDialogConfig &)config
successCallback:(RCTResponseSenderBlock)successCallback
cancelCallback:(RCTResponseSenderBlock)cancelCallback;
- (void)clearAllPendingVideos;
- (void)removePendingVideo:(NSString *)url;

@end
namespace facebook {
Expand Down
20 changes: 20 additions & 0 deletions Libraries/Image/ImagePickerIOS.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ const ImagePickerIOS = {
cancelCallback,
);
},
/**
* In iOS 13, the video URLs returned by the Image Picker are invalidated when
* the picker is dismissed, unless reference to it is held. This API allows
* the application to signal when it's finished with the video so that the
* reference can be cleaned up.
* It is safe to call this method for urlsthat aren't video URLs;
* it will be a no-op.
*/
removePendingVideo: function(url: string): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
NativeImagePickerIOS.removePendingVideo(url);
},
/**
* WARNING: In most cases, removePendingVideo should be used instead because
* clearAllPendingVideos could clear out pending videos made by other callers.
*/
clearAllPendingVideos: function(): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
NativeImagePickerIOS.clearAllPendingVideos();
},
};

module.exports = ImagePickerIOS;
2 changes: 2 additions & 0 deletions Libraries/Image/NativeImagePickerIOS.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface Spec extends TurboModule {
successCallback: (imageURL: string, height: number, width: number) => void,
cancelCallback: () => void,
) => void;
+clearAllPendingVideos: () => void;
+removePendingVideo: (url: string) => void;
}

export default (TurboModuleRegistry.get<Spec>('ImagePickerIOS'): ?Spec);

0 comments on commit 5051bf2

Please sign in to comment.