-
-
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 |
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.
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.
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?
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
This is also a great solution for streaming from memory instead of disk for small-sized videos.
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:
- If you log it, you'll see the track getting saved in
connectionDidFinishLoading
multiple times, causing cellular data to be wasted. That's becauseshouldWaitForLoadingOfRequestedResource
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. InconnectionDidFinishLoading
, simply assignself.songData
to the local data, so it can be used inrespondWithDataForRequest
. 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. - 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.
@ndbroadbent could you further explain how you modified processPendingRequests to make this work for simultaneous downloads ?
@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.
It seems that the AVPlayerItemStatusReadyToPlay
status hits only once the video is entirely downloaded. Is that correct?
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.
@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
@ndbroadbent i am facing the same issue how did you resolve it? any one can help me please to figure it out .
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
Hi , thanks for this amazing work.
Is there any version written in Swift please ?
Best regards
Does this work for HLS video?
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 theshouldWaitForLoadingOfRequestedResource:
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 ofshouldWaitForLoadingOfRequestedResource:
. Bingo, bongo!