From c9e2ba0547a9a73f12366b4faf76c1f4ea9bb3fd Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Sun, 5 Aug 2018 23:06:25 +0200 Subject: [PATCH] Fix URLs with query strings at the end, e.g. ?size=large Fix HLS Playlists (only support mp4, m4v and mov file extension) Add debug logging for guiding library consumers about why their video is not cached --- ios/Video/RCTVideo.m | 34 +++++++++----- ios/VideoCaching/RCTVideoCache.m | 77 ++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 8bee5246..6842703c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -311,13 +311,7 @@ static int const RCTVideoUnset = -1; [self removePlayerLayer]; [self removePlayerTimeObserver]; [self removePlayerItemObservers]; - [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { - [self didSetPlayerItemWithSource:source playerItem:playerItem]; - }]; -} -- (void) didSetPlayerItemWithSource:(NSDictionary *)source playerItem:(AVPlayerItem *) playerItem -{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // perform on next run loop, otherwise other passed react-props may not be set @@ -433,10 +427,29 @@ static int const RCTVideoUnset = -1; if (isNetwork) { #if __has_include() - [_videoCache getItemForUri:uri withCallback:^(AVAsset * _Nullable cachedAsset) { - if (cachedAsset) { - [self playerItemPrepareText:cachedAsset assetOptions:assetOptions withCallback:handler]; - return; + [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { + switch (videoCacheStatus) { + case RCTVideoCacheStatusMissingFileExtension: { +#ifdef DEBUG + NSLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md.", uri); +#endif + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; + [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; + return; + } + case RCTVideoCacheStatusUnsupportedFileExtension: { +#ifdef DEBUG + NSLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md.", uri); +#endif + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; + [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; + return; + } + default: + if (cachedAsset) { + [self playerItemPrepareText:cachedAsset assetOptions:assetOptions withCallback:handler]; + return; + } } #endif NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; @@ -1138,6 +1151,7 @@ static int const RCTVideoUnset = -1; #endif }]; } + #endif #pragma mark - RCTVideoPlayerViewControllerDelegate diff --git a/ios/VideoCaching/RCTVideoCache.m b/ios/VideoCaching/RCTVideoCache.m index 94dac176..4f23a113 100644 --- a/ios/VideoCaching/RCTVideoCache.m +++ b/ios/VideoCaching/RCTVideoCache.m @@ -57,7 +57,11 @@ - (void)storeItem:(NSData *)data forUri:(NSString *)uri withCallback:(void(^)(BOOL))handler; { - NSString * key = [[self generateHashForUrl:uri] stringByAppendingPathExtension: [uri pathExtension]]; + NSString *key = [self generateCacheKeyForUri:uri]; + if (key == nil) { + handler(NO); + return; + } [self saveDataToTemporaryStorage:data key:key]; [self.videoCache storeData:data forKey:key locked:NO withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { if (response.error) { @@ -90,23 +94,68 @@ return YES; } -- (void)getItemForUri:(NSString *)uri withCallback:(void(^)(AVAsset * _Nullable)) handler { - NSString * key = [[self generateHashForUrl:uri] stringByAppendingPathExtension: [uri pathExtension]]; +- (NSString *)generateCacheKeyForUri:(NSString *)uri { + NSString *uriWithoutQueryParams = uri; - AVURLAsset * temporaryAsset = [self getItemFromTemporaryStorage:key]; - if (temporaryAsset != nil) { - handler(temporaryAsset); - return; + // parse file extension + if ([uri rangeOfString:@"?"].location != NSNotFound) { + NSArray * components = [uri componentsSeparatedByString:@"?"]; + uriWithoutQueryParams = [components objectAtIndex:0]; } - - [self.videoCache loadDataForKey:key withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { - if (response.record == nil || response.record.data == nil) { - handler(nil); + + NSString * pathExtension = [uriWithoutQueryParams pathExtension]; + NSArray * supportedExtensions = @[@"m4v", @"mp4", @"mov"]; + if ([supportedExtensions containsObject:pathExtension] == NO) { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Missing file extension.", nil), + NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Missing file extension.", nil), + NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Missing file extension.", nil) + }; + NSError *error = [NSError errorWithDomain:@"RCTVideoCache" + code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo]; + @throw error; + } else if ([pathExtension isEqualToString:@"m3u8"]) { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Missing file extension.", nil), + NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Missing file extension.", nil), + NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Missing file extension.", nil) + }; + NSError *error = [NSError errorWithDomain:@"RCTVideoCache" + code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo]; + @throw error; + } + return [[self generateHashForUrl:uri] stringByAppendingPathExtension:pathExtension]; +} + +- (void)getItemForUri:(NSString *)uri withCallback:(void(^)(RCTVideoCacheStatus, AVAsset * _Nullable)) handler { + @try { + NSString *key = [self generateCacheKeyForUri:uri]; + AVURLAsset * temporaryAsset = [self getItemFromTemporaryStorage:key]; + if (temporaryAsset != nil) { + handler(RCTVideoCacheStatusAvailable, temporaryAsset); return; } - [self saveDataToTemporaryStorage:response.record.data key:key]; - handler([self getItemFromTemporaryStorage:key]); - } onQueue:dispatch_get_main_queue()]; + + [self.videoCache loadDataForKey:key withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { + if (response.record == nil || response.record.data == nil) { + handler(RCTVideoCacheStatusNotAvailable, nil); + return; + } + [self saveDataToTemporaryStorage:response.record.data key:key]; + handler(RCTVideoCacheStatusAvailable, [self getItemFromTemporaryStorage:key]); + } onQueue:dispatch_get_main_queue()]; + } @catch (NSError * err) { + switch (err.code) { + case RCTVideoCacheStatusMissingFileExtension: + handler(RCTVideoCacheStatusMissingFileExtension, nil); + return; + case RCTVideoCacheStatusUnsupportedFileExtension: + handler(RCTVideoCacheStatusUnsupportedFileExtension, nil); + return; + default: + @throw err; + } + } } - (NSString *) generateHashForUrl:(NSString *)string {