-
-
Save nathanhillyer/e027ae7c031054ac9bd7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// HlsAvPlayerCache.m | |
// | |
// Copyright (c) 2015 Agile Sports Technologies, Inc. All rights reserved. | |
// | |
#import "HlsAvPlayerCache.h" | |
@interface HlsAvPlayerCache () | |
@property (nonatomic, strong) NSMapTable *pendingRequests; // Dictionary of NSURLConnections to AssetResponses | |
@property (nonatomic, strong) NSMutableSet *cachedFragments; // Set of NSStrings (file paths) | |
@property (nonatomic, copy) NSString *cachePath; | |
@end | |
@implementation HlsAvPlayerCache | |
+ (instancetype)sharedInstance | |
{ | |
static HlsAvPlayerCache *_sharedInstance = nil; | |
static dispatch_once_t oncePredicate; | |
dispatch_once(&oncePredicate, ^{ | |
_sharedInstance = [HlsAvPlayerCache new]; | |
_sharedInstance.cachePath = ^NSString*() { | |
NSString *basePath = [appDelegate applicationCachesDirectory]; | |
NSString *iden = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; | |
basePath = [basePath stringByAppendingPathComponent:iden]; | |
basePath = [basePath stringByAppendingPathComponent:@"hlsFragmentCache"]; | |
if (![[NSFileManager defaultManager] fileExistsAtPath:basePath]) | |
{ | |
[[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:NULL]; | |
} | |
return basePath; | |
}(); | |
_sharedInstance.cachedFragments = [NSMutableSet setWithArray:[[NSFileManager defaultManager] contentsOfDirectoryAtPath:_sharedInstance.cachePath error:nil]]; | |
_sharedInstance.pendingRequests = [NSMapTable new]; | |
}); | |
return _sharedInstance; | |
} | |
#pragma mark - NSURLConnection delegate | |
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response | |
{ | |
AssetResponse *assetResponse = [self.pendingRequests objectForKey:connection.originalRequest]; | |
assetResponse.response = response; | |
assetResponse.loadingRequest.response = response; | |
[self fillInContentInformation:assetResponse.loadingRequest.contentInformationRequest response:assetResponse.response]; | |
[self processPendingRequestsForResponse:assetResponse request:connection.originalRequest]; | |
} | |
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data | |
{ | |
AssetResponse *assetResponse = [self.pendingRequests objectForKey:connection.originalRequest]; | |
[assetResponse.data appendData:data]; | |
[self processPendingRequestsForResponse:assetResponse request:connection.originalRequest]; | |
} | |
- (void)connectionDidFinishLoading:(NSURLConnection *)connection | |
{ | |
AssetResponse *assetResponse = [self.pendingRequests objectForKey:connection.originalRequest]; | |
assetResponse.finished = YES; | |
[self processPendingRequestsForResponse:assetResponse request:connection.originalRequest]; | |
NSString *localName = [assetResponse.response.URL.absoluteString localStringFromRemoteString]; | |
NSString *cachedFilePath = [self.cachePath stringByAppendingPathComponent:localName]; | |
[self.cachedFragments addObject:localName]; | |
[assetResponse.data writeToFile:cachedFilePath atomically:YES]; | |
} | |
#pragma mark - AVURLAsset resource loading | |
- (void)processPendingRequestsForResponse:(AssetResponse *)assetResponse request:(NSURLRequest *)request | |
{ | |
BOOL didRespondCompletely = [self respondWithDataForRequest:assetResponse]; | |
if (didRespondCompletely) | |
{ | |
DDLogVerbose(@"Completed %@", request.URL.absoluteString); | |
[assetResponse.loadingRequest finishLoading]; | |
[self.pendingRequests removeObjectForKey:request]; | |
} | |
} | |
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest response:(NSURLResponse *)response | |
{ | |
if (contentInformationRequest == nil || response == nil) | |
{ | |
return; | |
} | |
contentInformationRequest.byteRangeAccessSupported = NO; | |
contentInformationRequest.contentType = [response MIMEType]; | |
contentInformationRequest.contentLength = [response expectedContentLength]; | |
} | |
- (BOOL)respondWithDataForRequest:(AssetResponse *)assetResponse | |
{ | |
AVAssetResourceLoadingDataRequest *dataRequest = assetResponse.loadingRequest.dataRequest; | |
long long startOffset = dataRequest.requestedOffset; | |
if (dataRequest.currentOffset != 0) | |
{ | |
startOffset = dataRequest.currentOffset; | |
} | |
// Don't have any data at all for this request | |
if (assetResponse.data.length < startOffset) | |
{ | |
return NO; | |
} | |
if (!assetResponse.finished) | |
{ | |
return NO; | |
} | |
// This is the total data we have from startOffset to whatever has been downloaded so far | |
NSUInteger unreadBytes = assetResponse.data.length - (NSUInteger)startOffset; | |
// Respond with whatever is available if we can't satisfy the request fully yet | |
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes); | |
[dataRequest respondWithData:[assetResponse.data subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]]; | |
long long endOffset = startOffset + dataRequest.requestedLength; | |
BOOL didRespondFully = assetResponse.data.length >= endOffset; | |
DDLogVerbose(@"%@ - Requested %lli to %li, have %li", assetResponse.loadingRequest.request.URL.absoluteString, dataRequest.currentOffset, (long)dataRequest.requestedLength, (unsigned long)assetResponse.data.length); | |
return didRespondFully || assetResponse.finished; | |
} | |
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest | |
{ | |
DDLogVerbose(@"shouldWaitForLoadingOfRequestedResource %@", loadingRequest.request.URL.absoluteString); | |
// start downloading the fragment. | |
NSURL *interceptedURL = [loadingRequest.request URL]; | |
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO]; | |
actualURLComponents.scheme = @"http"; | |
NSString *localFile = [actualURLComponents.URL.absoluteString localStringFromRemoteString]; | |
if ([self.cachedFragments containsObject:localFile] && ![localFile hasSuffix:@".ts"]) | |
{ | |
NSData *fileData = [[NSFileManager defaultManager] contentsAtPath:[self.cachePath stringByAppendingPathComponent:localFile]]; | |
loadingRequest.contentInformationRequest.contentLength = fileData.length; | |
loadingRequest.contentInformationRequest.contentType = @"video/mpegts"; | |
loadingRequest.contentInformationRequest.byteRangeAccessSupported = NO; | |
[loadingRequest.dataRequest respondWithData:[fileData subdataWithRange:NSMakeRange(loadingRequest.dataRequest.requestedOffset, MIN(loadingRequest.dataRequest.requestedLength, fileData.length))]]; | |
[loadingRequest finishLoading]; | |
DDLogVerbose(@"Responded with cached data for %@", actualURLComponents.URL.absoluteString); | |
return YES; | |
} | |
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:loadingRequest.request delegate:self startImmediately:NO]; | |
[connection setDelegateQueue:[NSOperationQueue mainQueue]]; | |
[connection start]; | |
AssetResponse *assetResponse = [AssetResponse new]; | |
assetResponse.data = [NSMutableData new]; | |
assetResponse.loadingRequest = loadingRequest; | |
[self.pendingRequests setObject:assetResponse forKey:loadingRequest.request]; | |
return YES; | |
} | |
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest | |
{ | |
DDLogVerbose(@"shouldWaitForRenewalOfRequestedResource %@", renewalRequest.request.URL.absoluteString); | |
return YES; | |
} | |
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest | |
{ | |
DDLogVerbose(@"Resource request cancelled for %@", loadingRequest.request.URL.absoluteString); | |
NSURLConnection *connectionForRequest = nil; | |
NSEnumerator *enumerator = self.pendingRequests.keyEnumerator; | |
BOOL found = NO; | |
while ((connectionForRequest = [enumerator nextObject]) && !found) | |
{ | |
AssetResponse *assetResponse = [self.pendingRequests objectForKey:connectionForRequest]; | |
if (assetResponse.loadingRequest == loadingRequest) | |
{ | |
[connectionForRequest cancel]; | |
found = YES; | |
} | |
} | |
if (found) | |
{ | |
[self.pendingRequests removeObjectForKey:connectionForRequest]; | |
} | |
} | |
@end | |
@implementation AssetResponse | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment