在iOS中使用AVAssetResourceLoader和 AVPlayer,音频流和缓存

分享于 

16分钟阅读

应用开发

  繁體

介绍

本教程的源代码在github

使用AVAssetResourceLoader时,应实现AVAssetResourceLoaderDelegate的两个方法:



- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader 


shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest; 



- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader 


didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest; 




didCancelLoadingRequest:我们取消了数据加载操作。

资源加载程序

使用自定义方案创建AVPlayer:



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]; 




前两行使用自定义网址创建AVURLAsset,并使用将在其上调用委托方法的调度队列设置AVAssetResourceLoaderDelegate。

接下来的两行从AVPlayerItem AVURLAsset和AVPlayer创建AVPlayerItem,并添加必需的观察者。

我们的LSFilePlayerResourceLoader对象将存储在字典中,而资源url将是键。

下面是AVAssetResourceLoaderDelegate实现的外观:



- (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]; 



} 




首先,我们应该检查resourceURL方案,然后获取缓存的LSFilePlayerResourceLoader或创建新的方案,在此之后,loadingRequest添加到资源加载器。

LSFilePlayerResourceLoader接口:



@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;



@protocol LSFilePlayerResourceLoaderDelegate <NSObject>



@optional



- (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader 


didFailWithError:(NSError *)error;



- (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader 


didLoadResource:(NSURL *)resourceURL;



@end




此接口有用于在loader queue和LSFilePlayerResourceLoaderDelegate协议中管理请求的方法,这个协议定义允许代码处理资源加载状态的方法。

将loadingRequest添加到队列时,它保存在pendingRequests数组中,并开始数据加载操作:



- (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]]; 


 } 


 } 



} 




首次为每一个即将到来的请求创建新的数据加载操作,这就是为什么文件从多个线程加载,但是随后我们发现,当AVAssetResourceLoader开始新的加载请求时,先前发出的请求可以被取消。在启动操作方法中,我们取消所有先前启动的操作。



- (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); 


 } 


} 


</ydsessionrequest>




当接收到contentInformation请求并且不存在缓存文件时,我们初始化数据缓存并开始获取音频文件。

使用临时文件缓存从web接收的数据,并在需要时读取它。



- (void)prepareDataCache{ 


 


 self.cachedFilePath = [[self class] pathForTemporaryFile]; 



 NSError *error = nil; 


 if ([[NSFileManager defaultManager] fileExistsAtPath:self.cachedFilePath] == YES){ 


 [[NSFileManager defaultManager] removeItemAtPath:self.cachedFilePath error:&error]; 


 } 


 


 if (error == nil && [[NSFileManager defaultManager] fileExistsAtPath:self.cachedFilePath] == NO) { 



 NSString *dirPath = [self.cachedFilePath stringByDeletingLastPathComponent]; 


 [[NSFileManager defaultManager] createDirectoryAtPath:dirPath 


 withIntermediateDirectories:YES 


 attributes:nil 


 error:&error]; 


 


 if (error == nil) { 


 [[NSFileManager defaultManager] createFileAtPath:self.cachedFilePath 


 contents:nil 


 attributes:nil]; 


 


 self.writingFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.cachedFilePath]; 


 


 @try { 


 [self.writingFileHandle truncateFileAtOffset:self.contentInformation.contentLength]; 


 [self.writingFileHandle synchronizeFile]; 


 } 


 @catch (NSException *exception) { 


 NSError *error = [[NSError alloc] initWithDomain: LSFilePlayerResourceLoaderErrorDomain 


 code: -1 


 userInfo: @{ NSLocalizedDescriptionKey : @"can not write to file" }]; 


 [self completeWithError:error]; 


 return; 


 } 


 self.readingFileHandle = [NSFileHandle fileHandleForReadingAtPath:self.cachedFilePath]; 


 } 


 } 


 


 if (error!= nil) { 


 [self completeWithError:error]; 


 } 


} 




当收到新数据时,它缓存在磁盘上,更新receivedDataLength,然后通知所有挂起的请求。



- (void)didReceiveDataResponse:(LSDataResonse *)dataResponse{ 



 [self cacheDataResponse:dataResponse]; 



 self.receivedDataLength=dataResponse.currentOffset; 



 [self processPendingRequests]; 


} 




Cache data response方法负责使用请求的偏移量缓存接收到的数据。



- (void)cacheDataResponse:(LSDataResonse *)dataResponse{ 



 unsigned long long offset = dataResponse.dataOffset; 



 @try { 


 [self.writingFileHandle seekToFileOffset:offset]; 


 [self.writingFileHandle writeData:dataResponse.data]; 


 [self.writingFileHandle synchronizeFile]; 


 } 



 @catch (NSException *exception) { 


 NSError *error = [[NSError alloc] initWithDomain: LSFilePlayerResourceLoaderErrorDomain 


 code: -1 


 userInfo: @{ NSLocalizedDescriptionKey : @"can not write to file" }]; 


 [self completeWithError:error]; 


 } 


} 




读取数据方法负责从磁盘读取缓存的数据。



- (NSData *)readCachedData:(unsigned long long)startOffset 


length:(unsigned long long)numberOfBytesToRespondWith{ 



 @try { 


 [self.readingFileHandle seekToFileOffset:startOffset]; 


 NSData *data = [self.readingFileHandle readDataOfLength:numberOfBytesToRespondWith]; 


 return data; 


 } 



 @catch (NSException *exception) {} 



 return nil; 



} 




在processPendingRequests方法中,我们填充内容信息,并写入缓存数据,当接收到所有请求的数据时,我们从队列中删除挂起的请求。



- (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]; 


} 




写入有关内容的信息,如内容长度,内容类型以及资源是否支持字节范围请求。



- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *) 


contentInformationRequest{ 



 if (contentInformationRequest == nil || self.contentInformation == nil){ 


 return; 


 } 



 contentInformationRequest.byteRangeAccessSupported = self.contentInformation.byteRangeAccessSupported; 


 contentInformationRequest.contentType = self.contentInformation.contentType; 


 contentInformationRequest.contentLength = self.contentInformation.contentLength; 



} 




从缓存中读取数据并将它传递给挂起的请求。



- (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.receivedDataLength <startOffset){ 


 return NO; 


 } 


 


 // This is the total data we have from startOffset to whatever has been downloaded so far


 NSUInteger unreadBytes = self.receivedDataLength - startOffset; 


 


 // Respond with whatever is available if we can't satisfy the request fully yet


 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; 


} 





str  音频  CAC  Avplayer  音频流  Avasset  
相关文章