Created
March 9, 2015 21:50
-
-
Save bclymer/909ab3095eb3d0d36125 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
// | |
// HudlHlsAvPlayerCache.m | |
// Hudl | |
// | |
// Created by Brian Clymer on 3/6/15. | |
// Copyright (c) 2015 Agile Sports Technologies, Inc. All rights reserved. | |
// | |
#import "HudlHlsAvPlayerCache.h" | |
@interface HudlHlsAvPlayerCache () | |
@property (nonatomic, strong) NSMapTable *pendingRequests; // Dictionary of NSURLConnections to HudlAssetResponses | |
@property (nonatomic, strong) NSMutableSet *cachedFragments; // Set of NSStrings (file paths) | |
@property (nonatomic, copy) NSString *cachePath; | |
@end | |
@implementation HudlHlsAvPlayerCache | |
+ (instancetype)sharedInstance | |
{ | |
static HudlHlsAvPlayerCache *_sharedInstance = nil; | |
static dispatch_once_t oncePredicate; | |
dispatch_once(&oncePredicate, ^{ | |
_sharedInstance = [HudlHlsAvPlayerCache 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 | |
{ | |
HudlAssetResponse *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 | |
{ | |
HudlAssetResponse *assetResponse = [self.pendingRequests objectForKey:connection.originalRequest]; | |
[assetResponse.data appendData:data]; | |
[self processPendingRequestsForResponse:assetResponse request:connection.originalRequest]; | |
} | |
- (void)connectionDidFinishLoading:(NSURLConnection *)connection | |
{ | |
HudlAssetResponse *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:(HudlAssetResponse *)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:(HudlAssetResponse *)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]; | |
HudlAssetResponse *assetResponse = [HudlAssetResponse 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) | |
{ | |
HudlAssetResponse *assetResponse = [self.pendingRequests objectForKey:connectionForRequest]; | |
if (assetResponse.loadingRequest == loadingRequest) | |
{ | |
[connectionForRequest cancel]; | |
found = YES; | |
} | |
} | |
if (found) | |
{ | |
[self.pendingRequests removeObjectForKey:connectionForRequest]; | |
} | |
} | |
@end | |
@implementation HudlAssetResponse | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This connection request
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:loadingRequest.request delegate:self startImmediately:NO];
This will fail immediately as loadingRequest.request contains the malformed URL as of now.
So how is this working for you, can you please explain?