Merge pull request #1172 from react-native-community/bugfix/ios-cache-texttracks

[WIP] Prevent errors on iOS when using text tracks with caching
This commit is contained in:
Hampton Maxwell 2018-08-28 04:19:37 +03:00 committed by GitHub
commit 03734bbe52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 76 deletions

View File

@ -9,11 +9,13 @@ The cache is backed by [SPTPersistentCache](https://github.com/spotify/SPTPersis
# How Does It Work
The caching is based on the url of the asset.
SPTPersistentCache is a LRU ([last recently used](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU))) cache.
SPTPersistentCache is a LRU ([Least Recently Used](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU))) cache.
# Restrictions
Currenly the uri of the resource that should be cached needs to have the appropriate file extension (one of `mp4`, `m4v` or `mov`). In order to be cached. In future versions (once dependencies allow access to the `content-type` header) this will no longer be necessary. You will also receive warnings in the xcode logs by using the `debug` mode. So if you are not 100% sure if your video is cached, check your xcode logs!
Currently, caching is only supported for URLs that end in a `.mp4`, `.m4v`, or `.mov` extension. In future versions, URLs that end in a query string (e.g. test.mp4?resolution=480p) will be support once dependencies allow access to the `Content-Type` header. At this time, HLS playlists (.m3u8) and videos that sideload text tracks are not supported and will bypass the cache.
You will also receive warnings in the Xcode logs by using the `debug` mode. So if you are not 100% sure if your video is cached, check your Xcode logs!
By default files expire after 30 days and the maxmimum cache size is 100mb.

View File

@ -7,7 +7,8 @@
#if __has_include(<react-native-video/RCTVideoCache.h>)
#import <react-native-video/RCTVideoCache.h>
#import "DVURLAsset.h"
#import <DVAssetLoaderDelegate/DVURLAsset.h>
#import <DVAssetLoaderDelegate/DVAssetLoaderDelegate.h>
#endif
@class RCTEventDispatcher;

View File

@ -15,6 +15,12 @@ static NSString *const timedMetadata = @"timedMetadata";
static int const RCTVideoUnset = -1;
#ifdef DEBUG
#define DebugLog(...) NSLog(__VA_ARGS__)
#else
#define DebugLog(...) (void)0
#endif
@implementation RCTVideo
{
AVPlayer *_player;
@ -312,7 +318,7 @@ static int const RCTVideoUnset = -1;
[self removePlayerTimeObserver];
[self removePlayerItemObservers];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{
// perform on next run loop, otherwise other passed react-props may not be set
[self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) {
@ -373,12 +379,13 @@ static int const RCTVideoUnset = -1;
return nil;
}
- (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSMutableDictionary * __nullable)assetOptions withCallback:(void(^)(AVPlayerItem *))handler
- (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nullable)assetOptions withCallback:(void(^)(AVPlayerItem *))handler
{
if (!_textTracks) {
handler([AVPlayerItem playerItemWithAsset:asset]);
return;
}
// sideload text tracks
AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
@ -430,53 +437,38 @@ static int const RCTVideoUnset = -1;
NSString *uri = [source objectForKey:@"uri"];
NSString *type = [source objectForKey:@"type"];
NSURL *url = (isNetwork || isAsset) ?
[NSURL URLWithString:uri] :
[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
NSURL *url = isNetwork || isAsset
? [NSURL URLWithString:uri]
: [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init];
if (isNetwork) {
#if __has_include(<react-native-video/RCTVideoCache.h>)
[_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;
/* Per #1091, this is not a public API.
* We need to either get approval from Apple to use this or use a different approach.
NSDictionary *headers = [source objectForKey:@"requestHeaders"];
if ([headers count] > 0) {
[assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"];
}
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];
[assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey];
#if __has_include(<react-native-video/RCTVideoCache.h>)
DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:assetOptions networkTimeout: 10000];
asset.loaderDelegate = self;
#else
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions];
#endif
[self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler];
#if __has_include(<react-native-video/RCTVideoCache.h>)
}];
#endif
if (!_textTracks) {
/* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying
* to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded.
* Until this is fixed, we need to bypass caching when text tracks are specified.
*/
DebugLog(@"Caching is not supported for uri '%@' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri);
[self playerItemForSourceUsingCache:uri assetOptions:assetOptions withCallback:handler];
return;
}
else if (isAsset) {
#endif
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions];
[self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler];
return;
} else if (isAsset) {
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
[self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler];
return;
@ -486,6 +478,61 @@ static int const RCTVideoUnset = -1;
[self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler];
}
#if __has_include(<react-native-video/RCTVideoCache.h>)
- (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary *)options withCallback:(void(^)(AVPlayerItem *))handler {
NSURL *url = [NSURL URLWithString:uri];
[_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) {
switch (videoCacheStatus) {
case RCTVideoCacheStatusMissingFileExtension: {
DebugLog(@"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);
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options];
[self playerItemPrepareText:asset assetOptions:options withCallback:handler];
return;
}
case RCTVideoCacheStatusUnsupportedFileExtension: {
DebugLog(@"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);
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options];
[self playerItemPrepareText:asset assetOptions:options withCallback:handler];
return;
}
default:
if (cachedAsset) {
DebugLog(@"Playing back uri '%@' from cache", uri);
// See note in playerItemForSource about not being able to support text tracks & caching
handler([AVPlayerItem playerItemWithAsset:asset]);
return;
}
}
DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000];
asset.loaderDelegate = self;
/* More granular code to have control over the DVURLAsset
DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url];
resourceLoaderDelegate.delegate = self;
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
components.scheme = [DVAssetLoaderDelegate scheme];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options];
[asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()];
*/
handler([AVPlayerItem playerItemWithAsset:asset]);
}];
}
#pragma mark - DVAssetLoaderDelegate
- (void)dvAssetLoaderDelegate:(DVAssetLoaderDelegate *)loaderDelegate
didLoadData:(NSData *)data
forURL:(NSURL *)url {
[_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) {
DebugLog(@"Cache data stored successfully 🎉");
}];
}
#endif
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == _playerItem) {
@ -1153,20 +1200,6 @@ static int const RCTVideoUnset = -1;
_playerLayer = nil;
}
#if __has_include(<react-native-video/RCTVideoCache.h>)
#pragma mark - DVAssetLoaderDelegate
- (void)dvAssetLoaderDelegate:(DVAssetLoaderDelegate *)loaderDelegate
didLoadData:(NSData *)data
forURL:(NSURL *)url {
[_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) {
#ifdef DEBUG
NSLog(@"data stored succesfully 🎉");
#endif
}];
}
#endif
#pragma mark - RCTVideoPlayerViewControllerDelegate
- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController

View File

@ -7,8 +7,7 @@
@synthesize cacheIdentifier;
@synthesize temporaryCachePath;
+ (RCTVideoCache *) sharedInstance
{
+ (RCTVideoCache *)sharedInstance {
static RCTVideoCache *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@ -41,8 +40,7 @@
return self;
}
- (void) createTemporaryPath
{
- (void) createTemporaryPath {
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryCachePath
withIntermediateDirectories:YES
@ -105,7 +103,7 @@
NSString * pathExtension = [uriWithoutQueryParams pathExtension];
NSArray * supportedExtensions = @[@"m4v", @"mp4", @"mov"];
if ([supportedExtensions containsObject:pathExtension] == NO) {
if ([pathExtension isEqualToString:@""]) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing file extension.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Missing file extension.", nil),
@ -114,11 +112,12 @@
NSError *error = [NSError errorWithDomain:@"RCTVideoCache"
code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo];
@throw error;
} else if ([pathExtension isEqualToString:@"m3u8"]) {
} else if (![supportedExtensions containsObject:pathExtension]) {
// Notably, we don't currently support m3u8 (HLS playlists)
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing file extension.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Missing file extension.", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Missing file extension.", nil)
NSLocalizedDescriptionKey: NSLocalizedString(@"Unsupported file extension.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Unsupported file extension.", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Unsupported file extension.", nil)
};
NSError *error = [NSError errorWithDomain:@"RCTVideoCache"
code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo];