Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
//
// 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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented May 9, 2015

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

This comment has been minimized.

Copy link

commented May 31, 2015

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

This comment has been minimized.

Copy link

commented Jun 22, 2015

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

This comment has been minimized.

Copy link

commented Oct 22, 2015

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

This comment has been minimized.

Copy link

commented Jan 21, 2016

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

@inorganik

This comment has been minimized.

Copy link

commented Mar 10, 2016

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

This comment has been minimized.

Copy link

commented Apr 17, 2016

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

@tsheaff

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Dec 31, 2016

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

@tsheaff

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Apr 26, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.