★★★★★ 5/5 Reviews (1284) ⚡Enjoy 25-50% Site-wide Discount Today⚡ || We Served More Than 94,000 Cross-Channel Orders Up To Date! Thank You For Choosing Us!😃🌴

AVAsset, AVPlayer, box caching, downloader, dropbox, Evermusic App, Google Drive, ios music, playback song, sound streaming, Web Dav, Yandex Disk -

How we implemented audio streaming in Evermusic App

Intro
About a week ago, we published an update for our app called Evermusic — audio player that streams your music directly from the cloud and help you free up valuable space on the device for photos, videos, and apps. In this update, we added new cloud services and now Evermusic can stream your music from Web Dav and Yandex.Disk as well as from DropBox, One Drive, Box, Google Drive. When we worked on this update we found out many interesting tricks about audio streaming in iOS and now it’s time to talk about some of them.

Audio Streaming
We use AVPlayer for audio streaming in our app. This is a great class and it plays your audio files from local storage and remote host. The interface is very simple. You only need to know direct url of your audio file when you initialize AVPlayer. This class automatically loads your file from the remote host and play it directly from there. But what if you need to set some extra headers in GET request before you send it to the server. AVPlayer does not have any public method that allows you to do this.

In this case, you can use resourceLoader object in AVURLAsset. With this API you can provide controlled access to the resource for AVPlayer. But you should remember that AVPlayer uses resourceLoader when it doesn’t know how to load audio file. In this case we can change url scheme so AVPlayer is forced to defer the loading of the resource to our application. 

There are two delegate methods you should implement in your client code when you work with AVAssetResourceLoader:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource
:(AVAssetResourceLoadingRequest *)loadingRequest;

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader
didCancelLoadingRequest
:(AVAssetResourceLoadingRequest *)loadingRequest;

When assistance is required of the application to load a resource delegates receive resourceLoader: shouldWaitForLoadingOfRequestedResource: message. At this moment we save request and start data loading operation. When delegates receive resourceLoader: didCancelLoadingRequest: message data from the resource is no longer required and we cancel data loading operation.

Here’s how we create AVPlayer with the custom scheme:
NSURL *url = [NSURL URLWithString:@"customscheme://host/audio.mp3"];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
[asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
     
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
[self addObserversForPlayerItem:item];

self.player = [AVPlayer playerWithPlayerItem:playerItem];
[self addObserversForPlayer];

In this code we create AVURLAsset from url with custom scheme and set AVAssetResourceLoaderDelegate with dispatch queue on which delegate methods will be invoked. Then we create AVPlayerItem from AVURLAsset and AVPlayer from AVPlayerItem and add required observers.

Our class LSFilePlayerResourceLoader will be responsible for loading data from requested resource and passing loaded data back to AVURLAsset. Let’s add two parameters in init method. The first parameter is file url and the second is YDSession object. This session object is responsible for getting file data from cloud service.

LSFilePlayerResourceLoader interface is below:
@interface LSFilePlayerResourceLoader : NSObject

@property
(nonatomic,readonly,strong)NSURL *resourceURL;
@property
(nonatomic,readonly)NSArray *requests;
@property
(nonatomic,readonly,strong)YDSession *session;
@property
(nonatomic,readonly,assign)BOOL isCancelled;
@property
(nonatomic,weak)id<LSFilePlayerResourceLoaderDelegate> delegate;

- (instancetype)initWithResourceURL:(NSURL *)url session:(YDSession *)session;
- (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
- (void)removeRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
- (void)cancel;

@end

@protocol
LSFilePlayerResourceLoaderDelegate <NSObject>

@optional
- (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader
didFailWithError
:(NSError *)error;

- (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader
didLoadResource
:(NSURL *)resourceURL;

@end

This interface has methods for adding and removing requests from loader queue and delegate methods that allow your code to handle resource loading status. We will store our LSFilePlayerResourceLoader objects in dictionary and resource url will be the key.

And now let's look how we handle AVAssetResourceLoaderDelegate methods:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource
:(AVAssetResourceLoadingRequest *)loadingRequest{
    NSURL
*resourceURL = [loadingRequest.request URL];
   
if([resourceURL.scheme isEqualToString:@"customscheme"]){
       
LSFilePlayerResourceLoader *loader = [self resourceLoaderForRequest:loadingRequest];
       
if(loader==nil){
            loader
= [[LSFilePlayerResourceLoader alloc] initWithResourceURL:resourceURL session:self.session];
            loader
.delegate = self;
           
[self.resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]];
       
}
       
[loader addRequest:loadingRequest];
       
return YES;
   
}
   
return NO;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader
didCancelLoadingRequest
:(AVAssetResourceLoadingRequest *)loadingRequest{
   
LSFilePlayerResourceLoader *loader = [self resourceLoaderForRequest:loadingRequest];
   
[loader removeRequest:loadingRequest];
}
First two lines check resourceURL scheme. Next lines create LSFilePlayerResourceLoader and add loadingRequest to the queue. In addRequest method we save loadingRequest in pendingRequests array and start data loading operation:
- (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
   
if(self.isCancelled==NO){
        NSURL
*interceptedURL = [loadingRequest.request URL];
       
[self startOperationFromOffset:loadingRequest.dataRequest.requestedOffset
length
:loadingRequest.dataRequest.requestedLength];
       
[self.pendingRequests addObject:loadingRequest];
   
}
   
else{
       
if(loadingRequest.isFinished==NO){
           
[loadingRequest finishLoadingWithError:[self loaderCancelledError]];
       
}
   
}
}

Our data loading operation has two parts to it — contentInfoOperation, and dataOperation. A contentInfoOperation is operation to identify the content type, content length and whether the resource supports byte range requests. With byte range requests, AVPlayer can get fancy and apply various optimizations. The second one is dataOperation which loads file data with offset.
- (void)startOperationFromOffset:(unsigned long long)requestedOffset
                          length
:(unsigned long long)requestedLength{
 
   
[self cancelAllPendingRequests];
   
[self cancelOperations];
 
    __weak
typeof (self) weakSelf = self;
 
   
void(^failureBlock)(NSError *error) = ^(NSError *error) {
       
[weakSelf performBlockOnMainThreadSync:^{
           
if(weakSelf && weakSelf.isCancelled==NO){
               
[weakSelf completeWithError:error];
           
}
       
}];
   
};
 
   
void(^loadDataBlock)(unsigned long long off, unsigned long long len) = ^(unsigned long long offset,unsigned long long length){
       
[weakSelf performBlockOnMainThreadSync:^{
           
NSString *bytesString = [NSString stringWithFormat:@"bytes=%lld-%lld",offset,(offset+length-1)];
           
NSDictionary *params = @{@"Range":bytesString};
            id
<YDSessionRequest> req =
           
[weakSelf.session partialContentForFileAtPath:weakSelf.path withParams:params response:nil
                data
:^(UInt64 recDataLength, UInt64 totDataLength, NSData *recData) {
                     
[weakSelf performBlockOnMainThreadSync:^{
                         
if(weakSelf && weakSelf.isCancelled==NO){
                             
LSDataResonse *dataResponse =
                             
[LSDataResonse responseWithRequestedOffset:offset
                                            requestedLength
:length
                                            receivedDataLength
:recDataLength
                                            data
:recData];
                             
[weakSelf didReceiveDataResponse:dataResponse];
                         
}
                     
}];
                 
}
                completion
:^(NSError *err) {
                   
if(err){
                       failureBlock
(err);
                   
}
               
}];
           weakSelf
.dataOperation = req;
       
}];
   
};
 
   
if(self.contentInformation==nil){
       
self.contentInfoOperation = [self.session fetchStatusForPath:self.path completion:^(NSError *err, YDItemStat *item) {
           
if(weakSelf && weakSelf.isCancelled==NO){
               
if(err==nil){
                   
NSString *mimeType = item.path.mimeTypeForPathExtension;
                   
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType,(__bridge CFStringRef)(mimeType),NULL);
                   
unsigned long long contentLength = item.size;
                    weakSelf
.contentInformation = [[LSContentInformation alloc] init];
                    weakSelf
.contentInformation.byteRangeAccessSupported = YES;
                    weakSelf
.contentInformation.contentType = CFBridgingRelease(contentType);
                    weakSelf
.contentInformation.contentLength = contentLength;
                   
[weakSelf prepareDataCache];
                    loadDataBlock
(requestedOffset,requestedLength);
                    weakSelf
.contentInfoOperation = nil;
               
}
               
else{
                    failureBlock
(err);
               
}
           
}
       
}];
   
}
   
else{
        loadDataBlock
(requestedOffset,requestedLength);
   
}
}

We found out that when AVAssetResourceLoader began new loading request previously issued requests can be cancelled. So in our start operation method we cancel all previously started requests.When the contentInformation request is received we init file cache and start data operation. When new data response is received we cache it on the disk, update receivedDataLength and then notify all pending requests.
- (void)didReceiveDataResponse:(LSDataResonse *)dataResponse{
   
[self cacheDataResponse:dataResponse];
   
self.receivedDataLength=dataResponse.currentOffset;
   
[self processPendingRequests];
}
In processPendingRequests method we write information about content and cached data. When all requested data is received we remove pending request from queue.
- (void)processPendingRequests{
   
NSMutableArray *requestsCompleted = [[NSMutableArray alloc] init];
   
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests){
       
[self fillInContentInformation:loadingRequest.contentInformationRequest];
        BOOL didRespondCompletely
= [self respondWithDataForRequest:loadingRequest.dataRequest];
       
if (didRespondCompletely){
           
[loadingRequest finishLoading];
           
[requestsCompleted addObject:loadingRequest];
       
}
   
}
   
[self.pendingRequests removeObjectsInArray:requestsCompleted];
}
Here’s how we read data from cache and pass it to pending requests:
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{
   
long long startOffset = dataRequest.requestedOffset;
   
if (dataRequest.currentOffset != 0){
        startOffset
= dataRequest.currentOffset;
   
}
   
if (self.receivedDataLength < startOffset){
       
return NO;
   
}
   
NSUInteger unreadBytes = self.receivedDataLength - startOffset;
   
NSUInteger numberOfBytesToRespondWith = MIN(dataRequest.requestedLength, unreadBytes);
    BOOL didRespondFully
= NO;
   
NSData *data = [self readCachedData:startOffset length:numberOfBytesToRespondWith];
   
if(data){
       
[dataRequest respondWithData:data];
       
long long endOffset = startOffset + dataRequest.requestedLength;
        didRespondFully
= self.receivedDataLength >= endOffset;
   
}
   
return didRespondFully;
}

Source Code
Link to the source code is available on GitHub.

0 comments

Leave a comment

Please note, comments must be approved before they are published

Tags
★Reviews

Let customers speak for us

1445 reviews
95%
(1376)
4%
(58)
0%
(6)
0%
(2)
0%
(3)
LOVE IT !!!

I absolutely love this case! Its slim, not bulky ... puts together very well, love the color accent. The buttons are s little stiff on the left to operate the volume, but hoping it'll get better as it gets used more. Haven't tried the waterproof feature, and hope I never have to. Only thing I don't care for is the charger port ... most cables fit, but I have one I bought on the road that has a wide sideways head and it won't fit into the phone because of the way the case is designed, so I can't use it. But other than that, EXCELLENT PRODUCT and awesome price ... I'd definitely recommend

S9+ Case purchase

Good product. Strong protection for the phone
But fingerprint is hard to detect.

Perfect service and product

Many thanks

Awesome

Love it fits perfect fast shipping great customer service best case out there for s7edge will on buy from UVIYO..COM

★★★★★ Reviews

Let customers speak for us

1445 reviews
95%
(1376)
4%
(58)
0%
(6)
0%
(2)
0%
(3)
LOVE IT !!!

I absolutely love this case! Its slim, not bulky ... puts together very well, love the color accent. The buttons are s little stiff on the left to operate the volume, but hoping it'll get better as it gets used more. Haven't tried the waterproof feature, and hope I never have to. Only thing I don't care for is the charger port ... most cables fit, but I have one I bought on the road that has a wide sideways head and it won't fit into the phone because of the way the case is designed, so I can't use it. But other than that, EXCELLENT PRODUCT and awesome price ... I'd definitely recommend

S9+ Case purchase

Good product. Strong protection for the phone
But fingerprint is hard to detect.

Perfect service and product

Many thanks

Awesome

Love it fits perfect fast shipping great customer service best case out there for s7edge will on buy from UVIYO..COM