2018-03-01 15:38:38 -07:00
|
|
|
#import "RCTVideoCache.h"
|
|
|
|
|
|
|
|
@implementation RCTVideoCache
|
|
|
|
|
|
|
|
@synthesize videoCache;
|
|
|
|
@synthesize cachePath;
|
|
|
|
@synthesize cacheIdentifier;
|
|
|
|
@synthesize temporaryCachePath;
|
|
|
|
|
2018-08-08 16:34:07 -06:00
|
|
|
+ (RCTVideoCache *)sharedInstance {
|
2018-03-01 15:38:38 -07:00
|
|
|
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) {
|
2023-11-13 13:36:16 -07:00
|
|
|
NSLog(@"VideoCache: debug %@", string);
|
2018-03-01 15:38:38 -07:00
|
|
|
};
|
|
|
|
#endif
|
|
|
|
[self createTemporaryPath];
|
|
|
|
self.videoCache = [[SPTPersistentCache alloc] initWithOptions:options];
|
2018-07-17 06:36:03 -06:00
|
|
|
[self.videoCache scheduleGarbageCollector];
|
2018-03-01 15:38:38 -07:00
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2018-08-08 16:34:07 -06:00
|
|
|
- (void) createTemporaryPath {
|
|
|
|
NSError *error = nil;
|
2018-03-01 15:38:38 -07:00
|
|
|
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryCachePath
|
|
|
|
withIntermediateDirectories:YES
|
|
|
|
attributes:nil
|
|
|
|
error:&error];
|
2018-03-02 02:13:12 -07:00
|
|
|
#ifdef DEBUG
|
2018-03-01 15:38:38 -07:00
|
|
|
if (!success || error) {
|
2023-11-13 13:36:16 -07:00
|
|
|
NSLog(@"VideoCache: Error while! %@", error);
|
2018-03-01 15:38:38 -07:00
|
|
|
}
|
2018-03-02 02:13:12 -07:00
|
|
|
#endif
|
2018-03-01 15:38:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)storeItem:(NSData *)data forUri:(NSString *)uri withCallback:(void(^)(BOOL))handler;
|
|
|
|
{
|
2018-08-05 15:06:25 -06:00
|
|
|
NSString *key = [self generateCacheKeyForUri:uri];
|
|
|
|
if (key == nil) {
|
|
|
|
handler(NO);
|
|
|
|
return;
|
|
|
|
}
|
2018-03-01 15:38:38 -07:00
|
|
|
[self saveDataToTemporaryStorage:data key:key];
|
|
|
|
[self.videoCache storeData:data forKey:key locked:NO withCallback:^(SPTPersistentCacheResponse * _Nonnull response) {
|
|
|
|
if (response.error) {
|
2018-03-02 02:13:12 -07:00
|
|
|
#ifdef DEBUG
|
2023-11-13 13:36:16 -07:00
|
|
|
NSLog(@"VideoCache: An error occured while saving the video into the cache: %@", [response.error localizedDescription]);
|
2018-03-05 09:37:15 -07:00
|
|
|
#endif
|
2018-03-01 15:38:38 -07:00
|
|
|
handler(NO);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
handler(YES);
|
|
|
|
} onQueue:dispatch_get_main_queue()];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (AVURLAsset *)getItemFromTemporaryStorage:(NSString *)key {
|
2018-08-08 16:34:07 -06:00
|
|
|
NSString * temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key];
|
2018-03-01 15:38:38 -07:00
|
|
|
|
|
|
|
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:temporaryFilePath];
|
|
|
|
if (!fileExists) {
|
|
|
|
return nil;
|
|
|
|
}
|
2018-08-08 16:34:07 -06:00
|
|
|
NSURL *assetUrl = [[NSURL alloc] initFileURLWithPath:temporaryFilePath];
|
2018-03-01 15:38:38 -07:00
|
|
|
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:assetUrl options:nil];
|
|
|
|
return asset;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)saveDataToTemporaryStorage:(NSData *)data key:(NSString *)key {
|
2018-08-08 16:34:07 -06:00
|
|
|
NSString *temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key];
|
2018-03-01 15:38:38 -07:00
|
|
|
[data writeToFile:temporaryFilePath atomically:YES];
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2018-08-05 15:06:25 -06:00
|
|
|
- (NSString *)generateCacheKeyForUri:(NSString *)uri {
|
|
|
|
NSString *uriWithoutQueryParams = uri;
|
2018-03-02 02:13:12 -07:00
|
|
|
|
2018-08-05 15:06:25 -06:00
|
|
|
// parse file extension
|
|
|
|
if ([uri rangeOfString:@"?"].location != NSNotFound) {
|
|
|
|
NSArray<NSString*> * components = [uri componentsSeparatedByString:@"?"];
|
|
|
|
uriWithoutQueryParams = [components objectAtIndex:0];
|
2018-03-01 15:38:38 -07:00
|
|
|
}
|
2018-08-05 15:06:25 -06:00
|
|
|
|
|
|
|
NSString * pathExtension = [uriWithoutQueryParams pathExtension];
|
|
|
|
NSArray * supportedExtensions = @[@"m4v", @"mp4", @"mov"];
|
2018-08-08 14:56:11 -06:00
|
|
|
if ([pathExtension isEqualToString:@""]) {
|
2018-08-05 15:06:25 -06:00
|
|
|
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;
|
2018-08-08 14:56:11 -06:00
|
|
|
} else if (![supportedExtensions containsObject:pathExtension]) {
|
|
|
|
// Notably, we don't currently support m3u8 (HLS playlists)
|
2018-08-05 15:06:25 -06:00
|
|
|
NSDictionary *userInfo = @{
|
2018-08-08 14:56:11 -06:00
|
|
|
NSLocalizedDescriptionKey: NSLocalizedString(@"Unsupported file extension.", nil),
|
|
|
|
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Unsupported file extension.", nil),
|
|
|
|
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Unsupported file extension.", nil)
|
2018-08-05 15:06:25 -06:00
|
|
|
};
|
|
|
|
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);
|
2018-03-01 15:38:38 -07:00
|
|
|
return;
|
|
|
|
}
|
2018-08-05 15:06:25 -06:00
|
|
|
|
|
|
|
[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;
|
|
|
|
}
|
|
|
|
}
|
2018-03-01 15:38:38 -07:00
|
|
|
}
|
|
|
|
|
2018-08-08 16:34:07 -06:00
|
|
|
- (NSString *)generateHashForUrl:(NSString *)string {
|
2018-03-01 15:38:38 -07:00
|
|
|
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
|