Skip to content

Instantly share code, notes, and snippets.

@bclymer
Created March 9, 2015 21:50
Show Gist options
  • Select an option

  • Save bclymer/909ab3095eb3d0d36125 to your computer and use it in GitHub Desktop.

Select an option

Save bclymer/909ab3095eb3d0d36125 to your computer and use it in GitHub Desktop.

Revisions

  1. bclymer created this gist Mar 9, 2015.
    205 changes: 205 additions & 0 deletions AVPlayer Cache Layer
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,205 @@
    //
    // 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