Skip to content

Instantly share code, notes, and snippets.

@nathanhillyer
Created April 3, 2015 14:52
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save nathanhillyer/84e46152d7c4c88183b6 to your computer and use it in GitHub Desktop.
Save nathanhillyer/84e46152d7c4c88183b6 to your computer and use it in GitHub Desktop.
//
// 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
@end
@ndbroadbent
Copy link

Hi @nathanhillyer, did you ever figure out a way to get this code to work, or maybe an alternative approach to caching HLS for iOS?

@knowsudhanshu
Copy link

How to use it for HLS caching?

@azickh
Copy link

azickh commented Jan 1, 2017

Seems to great job! But, where is the declaration for AssetResponse?

@ykshin
Copy link

ykshin commented Dec 12, 2018

can't cache m3u8 file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment