Skip to content

Instantly share code, notes, and snippets.

Created May 20, 2014 08:30
Show Gist options
  • Save anonymous/83a93746d1ea52e9d23f to your computer and use it in GitHub Desktop.
Save anonymous/83a93746d1ea52e9d23f to your computer and use it in GitHub Desktop.
//
// ViewController.m
// AVPlayerCaching
//
// Created by Anurag Mishra on 5/19/14.
// Sample code to demonstrate how to cache a remote audio file while streaming it with AVPlayer
//
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <MobileCoreServices/MobileCoreServices.h>
@interface ViewController () <NSURLConnectionDataDelegate, AVAssetResourceLoaderDelegate>
@property (nonatomic, strong) NSMutableData *songData;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, strong) NSHTTPURLResponse *response;
@property (nonatomic, strong) NSMutableArray *pendingRequests;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (NSURL *)songURL
{
return [NSURL URLWithString:@"http://sampleswap.org/mp3/artist/earthling/Chuck-Silva_Ninety-Nine-Percent-320.mp3"];
}
- (NSURL *)songURLWithCustomScheme:(NSString *)scheme
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[self songURL] resolvingAgainstBaseURL:NO];
components.scheme = scheme;
return [components URL];
}
- (IBAction)playSong:(id)sender
{
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[self songURLWithCustomScheme:@"streaming"] options:nil];
[asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
self.pendingRequests = [NSMutableArray array];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
self.player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:NULL];
}
#pragma mark - NSURLConnection delegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.songData = [NSMutableData data];
self.response = (NSHTTPURLResponse *)response;
[self processPendingRequests];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.songData appendData:data];
[self processPendingRequests];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self processPendingRequests];
NSString *cachedFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cached.mp3"];
[self.songData writeToFile:cachedFilePath atomically:YES];
}
#pragma mark - AVURLAsset resource loading
- (void)processPendingRequests
{
NSMutableArray *requestsCompleted = [NSMutableArray array];
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
{
[self fillInContentInformation:loadingRequest.contentInformationRequest];
BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest];
if (didRespondCompletely)
{
[requestsCompleted addObject:loadingRequest];
[loadingRequest finishLoading];
}
}
[self.pendingRequests removeObjectsInArray:requestsCompleted];
}
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
{
if (contentInformationRequest == nil || self.response == nil)
{
return;
}
NSString *mimeType = [self.response MIMEType];
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
contentInformationRequest.byteRangeAccessSupported = YES;
contentInformationRequest.contentType = CFBridgingRelease(contentType);
contentInformationRequest.contentLength = [self.response expectedContentLength];
}
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != 0)
{
startOffset = dataRequest.currentOffset;
}
// Don't have any data at all for this request
if (self.songData.length < startOffset)
{
return NO;
}
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.songData.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:[self.songData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]];
long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = self.songData.length >= endOffset;
return didRespondFully;
}
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
if (self.connection == nil)
{
NSURL *interceptedURL = [loadingRequest.request URL];
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = @"http";
NSURLRequest *request = [NSURLRequest requestWithURL:[actualURLComponents URL]];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection setDelegateQueue:[NSOperationQueue mainQueue]];
[self.connection start];
}
[self.pendingRequests addObject:loadingRequest];
return YES;
}
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
[self.pendingRequests removeObject:loadingRequest];
}
#pragma KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay)
{
[self.player play];
}
}
@end
@Ry-Fi
Copy link

Ry-Fi commented Jan 7, 2015

This is great - thanks so much!!

As an addition, and this might be obvious but I'll say it anyways:

If you're using an NSURLCache (ie. [NSURLCache setSharedURLCache:[NSURLCache allocWithMemoryCapacity:x diskCapacity:y diskPath:@"blah"]]), then you can forego manually writing the asset to a file since the NSURLConnection will automatically cache the asset for you. 😄

In this scenario, on subsequent requests when the asset is already in the NSURLCache, there's one issue that your code doesn't address: connectionDidFinishLoading: will most likely get called before the shouldWaitForLoadingOfRequestedResource: is called for the data request, leading to the [self processPendingRequests]; never getting called to handle the request and thus the request forever sitting in limbo. A simple fix is to add another [self processPendingRequests]; at the end of shouldWaitForLoadingOfRequestedResource:. Bingo, bongo!

@darkengine
Copy link

As mentioned by @Ry-Fi, connectionDidFinishLoading will be called before all request from player when network is good enough even you did not use NSURLCache. We need to add a [self processPendingRequests] in shouldWaitForLoadingOfRequestedResource in this case or the latter request will never be satisfied. BTW, the code works perfectly with my app. Thanks a lot.

@ndbroadbent
Copy link

This is a great start, but unfortunately this code doesn't handle simultaneous or multiple requests. Not too hard to fix though, just need to adjust the processPendingRequests method to start the next request once the first is finished.

@Nikolozi
Copy link

I've tried the above code, but it seems the playback starts only after the file is fully downloaded. I thought the playback is supposed to start while it's still downloading the file? Or am I wrong?

@sparkinson
Copy link

This looks absolutely brilliant! I have been hunting around for something like this for a long time.

I have one question. When a user watches a video by streaming directly over http they can jump to the middle of the video, and streaming now starts from that position. Using the code above, how is this affected. Does the user have to wait for downloading to reach the new position before they can view that part of the video?

Thanks

@aaronscherbing
Copy link

This is also a great solution for streaming from memory instead of disk for small-sized videos.

@inorganik
Copy link

This is a tremendous bit of code. Using it in the wild, I've discovered a couple quirks. Here's what they are and how I've handled them:

  1. If you log it, you'll see the track getting saved in connectionDidFinishLoading multiple times, causing cellular data to be wasted. That's because shouldWaitForLoadingOfRequestedResource get's called multiple times, and inside that method a new connection can get initiated, even after you've already downloaded the entire track. To fix this you must deny it the opportunity to create a new connection if the file is available locally. In connectionDidFinishLoading, simply assign self.songData to the local data, so it can be used in respondWithDataForRequest. As a side effect of this, self.songData can get re-used even after you load up a new player item with a different track. Make sure you re-init songData before you load up the next song.
  2. As @sparkinson pointed out, if you wanted to start streaming a file from some timestamp, this code forces it to start downloading from the beginning. AVPlayer will wait to start playing until the track as been downloaded up to the point they want to start at. To handle this, I've done the following: If the user started listening from the beginning and wants to skip ahead, allow them to seek no further than the downloaded progress. If the user wants to start streaming from somewhere in the middle of the track, abandon the custom protocol and let AVURLAsset handle the loading of the resource itself.

@JoeyBodnar
Copy link

@ndbroadbent could you further explain how you modified processPendingRequests to make this work for simultaneous downloads ?

@tsheaff
Copy link

tsheaff commented Oct 4, 2016

@aaronscherbing true that this forces the entire binary asset into memory

Anyone have a solution to this problem, to support this solution for large binary assets that may not fit in memory.

@gsabran
Copy link

gsabran commented Dec 31, 2016

It seems that the AVPlayerItemStatusReadyToPlay status hits only once the video is entirely downloaded. Is that correct?

@tsheaff
Copy link

tsheaff commented Jan 5, 2017

I've written a wrapper over this implementation, with some significant new features including the solution to the in-memory issue noted above. I released it as a cocoapod

Feel free to use or fork! I didn't find any license on this code so I have not attributed it explicitly. If the author (anonymous?) contacts me, I'll be happy to attribute this.

@tsheaff
Copy link

tsheaff commented Jan 5, 2017

@gsabran not sure what you mean. Feel free to open an issue at https://github.com/calm/PersistentStreamPlayer

I also commented on your Stack Overflow comment

@jamilshazad
Copy link

jamilshazad commented Oct 7, 2017

@ndbroadbent i am facing the same issue how did you resolve it? any one can help me please to figure it out .

@vitoziv
Copy link

vitoziv commented Nov 29, 2017

Check out this repo https://github.com/vitoziv/VIMediaCache, it's a relatively complete implementation of AVAssetResourceLoaderDelegate

This repo can handle video seek and multiple requests

@satyres
Copy link

satyres commented Apr 26, 2018

Hi , thanks for this amazing work.
Is there any version written in Swift please ?
Best regards

@mohsinulhaq
Copy link

Does this work for HLS video?

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