react-native-video: Video doesn't work with new cameraRoll uri path with prefix "ph://" on iOS RN 0.59.5

Current behavior

video playback doesn’t work with RN 0.59.5 with new cameraRoll URI format using prefix “ph://” on iOS

Reproduction steps

get videos from cameraRoll on iOS and use the URI path with prefix “ph://”

Expected behavior

Video should play

Platform

Which player are you experiencing the problem on: iOS

Details: With RN 0.58.6 cameraRoll returned videos in this format for example:

{ node: 
   { timestamp: 1515357324,
     location: 
      { altitude: 0,
        heading: -1,
        speed: -1 },
     group_name: 'Camera Roll',
     type: 'ALAssetTypeVideo',
     image: 
      { width: 720,
        height: 1280,
        isStored: true,
        filename: 'IMG_0013.mp4',
        uri: 'assets-library://asset/asset.mp4?id=D34D1148-2CC7-4AE9-BB2E-67ACBCB60A10&ext=mp4',
        playableDuration: 10 } } }

Passing the ‘asset-library’ URI to react-native-video v4.4.0 worked.

However in React Native v0.59.x the cameraRoll API returns videos in the format:

 node: 
   { timestamp: 1554232151,
     type: 'video',
     group_name: 'Camera Roll',
     location: {},
     image: 
      { width: 1080,
        uri: 'ph://D916EEFD-317B-403D-B589-B3ED9B17E784/L0/001',
        height: 1920,
        isStored: true,
        playableDuration: 4.365 } } }

react-native-video does not work with the new URI format.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 7
  • Comments: 17 (1 by maintainers)

Most upvoted comments

This is what I did to get it working

  const appleId = image.uri.substring(5, 41);
  const uri = `assets-library://asset/asset.${ext}?id=${appleId}&ext=${ext}`;`

This should get it working until react-native-video adds a fix

Converting assets from PH:// to assets-library:// seems to work if the file is local, but in instances where you fetch ‘All’ from CameraRoll you can at times find a video that is stored on iCloud. Trying to load these videos will not load (onLoad) and does not throw an error (onError).

I’m setting up a timeout to notify users if the file does not load right away (not local file) but has anyone uncovered a way to load iCloud assets into react-native-video?

@talal7860 nice snippet, I use your code and it’s work perfect at Image, but when i preview a video, it gives me an NSURLDominError with code -1100, which react-native-video say the uri is not exist. and I noticed my video is on my iCloud instead of local. do you have any advice?

Long time since last comment, but I just ran into this problem using the camera roll directly.

Not sure I if I feel comfortable relying on all this string wrangling, but basically this works if the filename of the file is predictable:

      const appleId = selected.node.image.uri.substring(5, 41);
      const fileNameLength = selected.node.image.filename.length;
      const ext = selected.node.image.filename.substring(fileNameLength - 3);
      const uri = `assets-library://asset/asset.${ext}?id=${appleId}&ext=${ext}`;

in use (don’t mind the props in the player, it’s a HoC built for my purposes, the url prop maps directly to the source={{uri: 'some url'}}) prop in the react-native-video component…

const renderVideo = () => {
    if (selected) {
      const appleId = selected.node.image.uri.substring(5, 41);
      const fileNameLength = selected.node.image.filename.length;
      const ext = selected.node.image.filename.substring(fileNameLength - 3);
      const uri = `assets-library://asset/asset.${ext}?id=${appleId}&ext=${ext}`;
      return (
        <VideoPlayer
          url={uri}
          fixedPlayerWidth={cachedDeviceWidth}
          assetMaxWidth={selected.node.image.width}
          assetMaxHeight={selected.node.image.height}
          playerMaxHeight={300}
        />
      );
    }

    return null;
  };

You need to patch the react-native-video lin, using asset-library uris hacky and was very error-prone. Here is our patch, based on @fishkingsin fork (updated it). With this ph:// uris work flawless regardless of where the asset is saved (local/iCloud):

diff --git a/node_modules/react-native-video/Video.js b/node_modules/react-native-video/Video.js
index 6b84021..a9ca472 100644
--- a/node_modules/react-native-video/Video.js
+++ b/node_modules/react-native-video/Video.js
@@ -275,7 +275,7 @@ export default class Video extends Component {
     }

     const isNetwork = !!(uri && uri.match(/^https?:/));
-    const isAsset = !!(uri && uri.match(/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/));
+    const isAsset = !!(uri && uri.match(/^(assets-library|ph|ipod-library|file|content|ms-appx|ms-appdata):/));

     let nativeResizeMode;
     const RCTVideoInstance = this.getViewManagerConfig('RCTVideo');
diff --git a/node_modules/react-native-video/ios/Video/RCTVideo.m b/node_modules/react-native-video/ios/Video/RCTVideo.m
index 8780f48..f47fd9f 100644
--- a/node_modules/react-native-video/ios/Video/RCTVideo.m
+++ b/node_modules/react-native-video/ios/Video/RCTVideo.m
@@ -5,6 +5,7 @@
 #import <React/UIView+React.h>
 #include <MediaAccessibility/MediaAccessibility.h>
 #include <AVFoundation/AVFoundation.h>
+#include "RNPhotosFrameworkExtension.h"

 static NSString *const statusKeyPath = @"status";
 static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp";
@@ -87,6 +88,7 @@ @implementation RCTVideo
 #if __has_include(<react-native-video/RCTVideoCache.h>)
   RCTVideoCache * _videoCache;
 #endif
+    RNPhotosFrameworkExtension *photosFrameworkExtension;
 #if TARGET_OS_IOS
   void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL);
   AVPictureInPictureController *_pipController;
@@ -505,6 +507,18 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye
     return;
   }

+    if ([uri hasPrefix:@"ph://"]) {
+        if(!photosFrameworkExtension) {
+            photosFrameworkExtension = [RNPhotosFrameworkExtension new];
+        }
+        NSString *loadedIdentifier = [photosFrameworkExtension startLoadingPhotosAsset:source bufferingCallback:self.onVideoBuffer andReactTag:self.reactTag andCompleteBlock:^(NSDictionary *source, AVAsset *asset, PHImageRequestID imageRequestID) {
+            NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init];
+            [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler];
+        }];
+
+        return;
+    }
+
   NSURL *url = isNetwork || isAsset
     ? [NSURL URLWithString:uri]
     : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
diff --git a/node_modules/react-native-video/ios/Video/RNPhotosFrameworkExtension.h b/node_modules/react-native-video/ios/Video/RNPhotosFrameworkExtension.h
new file mode 100644
index 0000000..b1431bc
--- /dev/null
+++ b/node_modules/react-native-video/ios/Video/RNPhotosFrameworkExtension.h
@@ -0,0 +1,20 @@
+//
+//  RNPhotosFrameworkExtension.h
+//  react-native-video
+//
+//  Created by Hanno  Gödecke on 30.12.20.
+//
+#import <Foundation/Foundation.h>
+#import <React/RCTView.h>
+#import <React/RCTConvert.h>
+
+@import Photos;
+@interface RNPhotosFrameworkExtension : NSObject
+typedef void (^RNPFVIdeoLoadCompleteBlock) (NSDictionary *source, AVAsset *asset, PHImageRequestID imageRequestID);
+
+-(NSString *)startLoadingPhotosAsset:(NSDictionary *)source bufferingCallback:(RCTBubblingEventBlock) onVideoBuffer andReactTag:(NSString *)reactTag andCompleteBlock:(RNPFVIdeoLoadCompleteBlock)completeBlock;
+
+@property (nonatomic, strong) NSString *loadedPhotosLocalIdentifier;
+@property (nonatomic) PHImageRequestID imageRequestID;
+
+@end
diff --git a/node_modules/react-native-video/ios/Video/RNPhotosFrameworkExtension.m b/node_modules/react-native-video/ios/Video/RNPhotosFrameworkExtension.m
new file mode 100644
index 0000000..36d9ecd
--- /dev/null
+++ b/node_modules/react-native-video/ios/Video/RNPhotosFrameworkExtension.m
@@ -0,0 +1,110 @@
+//
+//  RNPhotosFrameworkExtension.m
+//  react-native-video
+//
+//  Created by Hanno  Gödecke on 30.12.20.
+//
+
+#import "RNPhotosFrameworkExtension.h"
+
+@implementation RNPhotosFrameworkExtension
+
+-(NSString *)startLoadingPhotosAsset:(NSDictionary *)source bufferingCallback:(RCTBubblingEventBlock) onVideoBuffer andReactTag:(NSString *)reactTag andCompleteBlock:(RNPFVIdeoLoadCompleteBlock)completeBlock {
+    NSURL *url = [RCTConvert NSURL:source[@"uri"]];
+    if(self.imageRequestID != PHInvalidImageRequestID) {
+        [[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID];
+        self.imageRequestID = PHInvalidImageRequestID;
+    }
+    if([url.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) {
+        NSString *localIdentifier = [url.absoluteString substringFromIndex:@"ph://".length];
+        self.loadedPhotosLocalIdentifier = localIdentifier;
+        PHVideoRequestOptions *videoRequestOptions = [self getVideoRequestOptionsFromUrl:url];
+
+        videoRequestOptions.progressHandler = ^void (double progress, NSError *__nullable error, BOOL *stop, NSDictionary *__nullable info)
+        {
+            if(onVideoBuffer && self.loadedPhotosLocalIdentifier != nil && [localIdentifier isEqualToString:self.loadedPhotosLocalIdentifier] ) {
+                onVideoBuffer(@{
+                                @"isBuffering": @(YES),
+                                @"progress" : @(progress),
+                                @"target": reactTag
+                                });
+            }
+        };
+        [self loadPhotosVideoPlayerItem:localIdentifier andPhVideoRequestOptions:videoRequestOptions andSource:source andCompleteBlock:^(AVAsset * _Nullable asset, PHImageRequestID imageRequestID) {
+            if(asset && completeBlock && self.loadedPhotosLocalIdentifier != nil && [localIdentifier isEqualToString:self.loadedPhotosLocalIdentifier]) {
+                completeBlock(source, asset, imageRequestID);
+            }
+        }];
+        return localIdentifier;
+    }
+    return nil;
+}
+
+-(void) loadPhotosVideoPlayerItem:(NSString *)localIdentifier andPhVideoRequestOptions:(PHVideoRequestOptions *)requestOptions andSource:(NSDictionary *)source andCompleteBlock:(void(^)(AVAsset * _Nullable avAsset, PHImageRequestID imageRequestID))completeBlock  {
+
+    PHAsset *asset = [self getAssetFromOfLocalIdentifier:localIdentifier];
+    if(asset == nil) {
+        return [NSException raise:@"Invalid localIdentifier" format:@"Could not locate photos-asset with localIdentifier %@", localIdentifier];
+    }
+    self.imageRequestID = [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:requestOptions resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
+        completeBlock(avAsset, self.imageRequestID);
+    }];
+}
+
+-(PHVideoRequestOptions *)getVideoRequestOptionsFromUrl:(NSURL *)url {
+    PHVideoRequestOptions *videoRequestOptions = [PHVideoRequestOptions new];
+    videoRequestOptions.networkAccessAllowed = YES;
+
+    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
+                                                resolvingAgainstBaseURL:NO];
+    NSArray *queryItems = urlComponents.queryItems;
+    NSString *deliveryModeQuery = [self valueForKey:@"deliveryMode"
+                                     fromQueryItems:queryItems];
+    NSString *versionQuery = [self valueForKey:@"version"
+                                fromQueryItems:queryItems];
+
+    PHVideoRequestOptionsVersion version = PHVideoRequestOptionsVersionCurrent;
+
+    if(versionQuery) {
+        if([versionQuery isEqualToString:@"original"]) {
+            version = PHVideoRequestOptionsVersionOriginal;
+        }
+    }
+
+    PHVideoRequestOptionsDeliveryMode deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
+    if(deliveryModeQuery != nil) {
+        if([deliveryModeQuery isEqualToString:@"mediumQuality"]) {
+            deliveryMode = PHVideoRequestOptionsDeliveryModeMediumQualityFormat;
+        }
+        else if([deliveryModeQuery isEqualToString:@"highQuality"]) {
+            deliveryMode = PHVideoRequestOptionsDeliveryModeHighQualityFormat;
+        }
+        else if([deliveryModeQuery isEqualToString:@"fast"]) {
+            deliveryMode = PHVideoRequestOptionsDeliveryModeFastFormat;
+        }
+    }
+    videoRequestOptions.deliveryMode = deliveryMode;
+    videoRequestOptions.version = version;
+
+
+    return videoRequestOptions;
+}
+
+-(PHAsset *) getAssetFromOfLocalIdentifier:(NSString *)localIdentifier {
+    PHFetchOptions *fetchOptions = [PHFetchOptions new];
+    fetchOptions.includeHiddenAssets = YES;
+    fetchOptions.includeAllBurstAssets = YES;
+    return [[PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:fetchOptions] firstObject];
+}
+
+- (NSString *)valueForKey:(NSString *)key
+           fromQueryItems:(NSArray *)queryItems
+{
+    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name=%@", key];
+    NSURLQueryItem *queryItem = [[queryItems
+                                  filteredArrayUsingPredicate:predicate]
+                                 firstObject];
+    return queryItem.value;
+}
+
+@end

@ramon90 thanks. Does ‘react-native-gallery-manager’ have any other specific advantages over using the default ‘CameraRoll’ API? I’m wondering if we need to add other library in the stack just to make loading and playing video from camera roll work. 😃 It would be good if CameraRoll and ‘react-native-video’ are compatible to each other as both are so fundamental.