#import "RCTVideoCache.h" @implementation RCTVideoCache @synthesize videoCache; @synthesize cachePath; @synthesize cacheIdentifier; @synthesize temporaryCachePath; + (RCTVideoCache*)sharedInstance { static RCTVideoCache* sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (id)init { if (self = [super init]) { self.cacheIdentifier = @"rct.video.cache"; self.temporaryCachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:self.cacheIdentifier]; self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:self.cacheIdentifier]; SPTPersistentCacheOptions* options = [SPTPersistentCacheOptions new]; options.cachePath = self.cachePath; options.cacheIdentifier = self.cacheIdentifier; options.defaultExpirationPeriod = 60 * 60 * 24 * 30; options.garbageCollectionInterval = (NSUInteger)(1.5 * SPTPersistentCacheDefaultGCIntervalSec); options.sizeConstraintBytes = 1024 * 1024 * 100; options.useDirectorySeparation = NO; #ifdef DEBUG options.debugOutput = ^(NSString* string) { NSLog(@"VideoCache: debug %@", string); }; #endif [self createTemporaryPath]; self.videoCache = [[SPTPersistentCache alloc] initWithOptions:options]; [self.videoCache scheduleGarbageCollector]; } return self; } - (void)createTemporaryPath { NSError* error = nil; BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryCachePath withIntermediateDirectories:YES attributes:nil error:&error]; #ifdef DEBUG if (!success || error) { NSLog(@"VideoCache: Error while! %@", error); } #endif } - (void)storeItem:(NSData*)data forUri:(NSString*)uri withCallback:(void (^)(BOOL))handler; { 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) { #ifdef DEBUG NSLog(@"VideoCache: An error occured while saving the video into the cache: %@", [response.error localizedDescription]); #endif handler(NO); return; } handler(YES); } onQueue:dispatch_get_main_queue()]; return; } - (AVURLAsset*)getItemFromTemporaryStorage:(NSString*)key { NSString* temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:temporaryFilePath]; if (!fileExists) { return nil; } NSURL* assetUrl = [[NSURL alloc] initFileURLWithPath:temporaryFilePath]; AVURLAsset* asset = [AVURLAsset URLAssetWithURL:assetUrl options:nil]; return asset; } - (BOOL)saveDataToTemporaryStorage:(NSData*)data key:(NSString*)key { NSString* temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; [data writeToFile:temporaryFilePath atomically:YES]; return YES; } - (NSString*)generateCacheKeyForUri:(NSString*)uri { NSString* uriWithoutQueryParams = uri; // parse file extension if ([uri rangeOfString:@"?"].location != NSNotFound) { NSArray* components = [uri componentsSeparatedByString:@"?"]; uriWithoutQueryParams = [components objectAtIndex:0]; } NSString* pathExtension = [uriWithoutQueryParams pathExtension]; NSArray* supportedExtensions = @[ @"m4v", @"mp4", @"mov" ]; if ([pathExtension isEqualToString:@""]) { 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 (![supportedExtensions containsObject:pathExtension]) { // Notably, we don't currently support m3u8 (HLS playlists) NSDictionary* userInfo = @{ 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]; @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.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 { const char* cStr = [string UTF8String]; unsigned char result[CC_MD5_DIGEST_LENGTH]; CC_MD5(cStr, (CC_LONG)strlen(cStr), result); return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15]]; } @end