iOS音視頻實現(xiàn)邊下載邊播放
近段時間制作視頻播放社區(qū)的功能,期間查找了不少資料,做過很多嘗試,現(xiàn)在來整理一下其中遇到的一些坑.由于考慮到AVPlayer對視頻有更高自由度的控制,而且能夠使用它自定義視頻播放界面,iOS中所使用的視頻播放控件為AVPlayer,而拋棄了高層次的MediaPlayer框架,現(xiàn)在想想挺慶幸當初使用了AVPlayer。
AVPlayer的基本知識
AVPlayer本身并不能顯示視頻,而且它也不像MPMoviePlayerController有一個view屬性。如果AVPlayer要顯示必須創(chuàng)建一個播放器層AVPlayerLayer用于展示,播放器層繼承于CALayer,有了AVPlayerLayer之添加到控制器視圖的layer中即可。要使用AVPlayer首先了解一下幾個常用的類:
AVAsset:主要用于獲取多媒體信息,是一個抽象類,不能直接使用。
AVURLAsset:AVAsset的子類,可以根據(jù)一個URL路徑創(chuàng)建一個包含媒體信息的AVURLAsset對象。
AVPlayerItem:一個媒體資源管理對象,管理者視頻的一些基本信息和狀態(tài),一個AVPlayerItem對應著一個視頻資源。
iOS視頻實現(xiàn)邊下載邊播放的幾種實現(xiàn)
1.本地實現(xiàn)http server
在iOS本地開啟Local Server服務,然后使用播放控件請求本地Local Server服務,本地的服務再不斷請求視頻地址獲取視頻流,本地服務請求的過程中把視頻緩存到本地,這種方法在網(wǎng)上有很多例子,有興趣了解的人可自己下載例子查看。
2.使用AVPlayer的方法開啟下載服務
1.AVURLAsset*urlAsset = [[AVURLAssetalloc]initWithURL:url options:nil];
2.AVPlayerItem*item = [AVPlayerItemplayerItemWithAsset:urlAsset];
3.[self.avPlayerreplaceCurrentItemWithPlayerItem:item];
4.[selfaddObserverToPlayerItem:item];
但由于AVPlayer是沒有提供方法給我們直接獲取它下載下來的數(shù)據(jù),所以我們只能在視頻下載完之后自己去尋找緩存視頻數(shù)據(jù)的辦法,AVFoundation框架中有一種從多媒體信息類AVAsset中提取視頻數(shù)據(jù)的類AVMutableComposition和AVAssetExportSession。
其中AVMutableComposition的作用是能夠從現(xiàn)有的asset實例中創(chuàng)建出一個新的AVComposition(它也是AVAsset的字類),使用者能夠從別的asset中提取他們的音頻軌道或視頻軌道,并且把它們添加到新建的Composition中。
AVAssetExportSession的作用是把現(xiàn)有的自己創(chuàng)建的asset輸出到本地文件中。
為什么需要把原先的AVAsset(AVURLAsset)實現(xiàn)的數(shù)據(jù)提取出來后拼接成另一個AVAsset(AVComposition)的數(shù)據(jù)后輸出呢,由于通過網(wǎng)絡url下載下來的視頻沒有保存視頻的原始數(shù)據(jù)(或者蘋果沒有暴露接口給我們獲取),下載后播放的avasset不能使用AVAssetExportSession輸出到本地文件,要曲線地把下載下來的視頻通過重構成另外一個AVAsset實例才能輸出。代碼例子如下:
NSString*documentDirectory =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)[0];
NSString*myPathDocument = [documentDirectory stringByAppendingPathComponent:[NSStringstringWithFormat:@"%@.mp4",[_source.videoUrlMD5]]];
NSURL*fileUrl = [NSURLfileURLWithPath:myPathDocument];
if(asset !=nil) {
AVMutableComposition*mixComposition = [[AVMutableCompositionalloc]init];
AVMutableCompositionTrack*firstTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideopreferredTrackID:kCMPersistentTrackID_Invalid];
[firstTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:0] atTime:kCMTimeZero error:nil];
AVMutableCompositionTrack*audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudiopreferredTrackID:kCMPersistentTrackID_Invalid];
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0] atTime:kCMTimeZero error:nil];
AVAssetExportSession*exporter = [[AVAssetExportSessionalloc]initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality];
exporter.outputURL= fileUrl;
if(exporter.supportedFileTypes) {
exporter.outputFileType= [exporter.supportedFileTypesobjectAtIndex:0] ;
exporter.shouldOptimizeForNetworkUse=YES;
[exporter exportAsynchronouslyWithCompletionHandler:^{
}];
}
}
3.使用AVAssetResourceLoader回調下載,也是最終決定使用的技術
AVAssetResourceLoader通過你提供的委托對象去調節(jié)AVURLAsset所需要的加載資源。而很重要的一點是,AVAssetResourceLoader僅在AVURLAsset不知道如何去加載這個URL資源時才會被調用,就是說你提供的委托對象在AVURLAsset不知道如何加載資源時才會得到調用。所以我們又要通過一些方法來曲線解決這個問題,把我們目標視頻URL地址的scheme替換為系統(tǒng)不能識別的scheme,然后在我們調用網(wǎng)絡請求去處理這個URL時把scheme切換為原來的scheme。
實現(xiàn)邊下邊播功能AVResourceLoader的委托對象必須要實現(xiàn)AVAssetResourceLoaderDelegate下五個協(xié)議的其中兩個:
1//在系統(tǒng)不知道如何處理URLAsset資源時回調
- (BOOL)resourceLoader:(AVAssetResourceLoader*)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest*)loadingRequestNS_AVAILABLE(10_9,6_0);
2//在取消加載資源后回調
- (void)resourceLoader:(AVAssetResourceLoader*)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest*)loadingRequestNS_AVAILABLE(10_9,7_0);
以下來說說具體要怎么做處理
第一步,創(chuàng)建一個AVURLAsset,并且用它來初始化一個AVPlayerItem
#define kCustomVideoScheme @"yourScheme"
NSURL*currentURL = [NSURLURLWithString:@"http://***.***.***"];
NSURLComponents*components = [[NSURLComponentsalloc]initWithURL:currentURL resolvingAgainstBaseURL:NO];
1////注意,不加這一句不能執(zhí)行到回調操作
components.scheme= kCustomVideoScheme;
AVURLAsset*urlAsset = [AVURLAssetURLAssetWithURL:components.URL
options:nil];
2//_resourceManager在接下來講述
[urlAsset.resourceLoadersetDelegate:_resourceManager queue:dispatch_get_main_queue()];
AVPlayerItem*item = [AVPlayerItemplayerItemWithAsset:urlAsset];
_playerItem = item;
if(IOS9_OR_LATER) {
item.canUseNetworkResourcesForLiveStreamingWhilePaused=YES;
}
[self.avPlayerreplaceCurrentItemWithPlayerItem:item];
self.playerLayer.player=self.avPlayer;
[selfaddObserverToPlayerItem:item];**
第二步,創(chuàng)建AVResourceManager實現(xiàn)AVResourceLoader協(xié)議
1
@interfaceAVAResourceLoaderManager :NSObject < AVAssetResourceLoaderDelegate >
第三步,實現(xiàn)兩個必須的回調協(xié)議,實現(xiàn)中有幾件需要做的事情
- (BOOL)resourceLoader:(AVAssetResourceLoader*)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest*)loadingRequest
{
1//獲取系統(tǒng)中不能處理的URL
NSURL*resourceURL = [loadingRequest.requestURL];
2//判斷這個URL是否遵守URL規(guī)范和其是否是我們所設定的URL
if([selfcheckIsLegalURL:resourceURL] && [resourceURL.schemeisEqualToString:kCustomVideoScheme]){
3//判斷當前的URL網(wǎng)絡請求是否已經(jīng)被加載過了,如果緩存中里面有URL對應的網(wǎng)絡加載器(自己封裝,也可以直接使用NSURLRequest),則取出來添加請求,每一個URL對應一個網(wǎng)絡加載器,loader的實現(xiàn)接下來會說明
AVResourceLoaderForASI*loader = [selfasiresourceLoaderForRequest:loadingRequest];
if(loader ==nil){
loader = [[AVResourceLoaderForASIalloc] initWithResourceURL:resourceURL];
loader.delegate=self;
4//緩存網(wǎng)絡加載器
[self.resourceLoaderssetObject:loader forKey:[selfkeyForResourceLoaderWithURL:resourceURL]];
}
5//加載器添加請求
[loader addRequest:loadingRequest];
6//返回YES則表明使用我們的代碼對AVAsset中請求網(wǎng)絡資源做處理
returnYES;
}else{
returnNO;
}
}
- (void)resourceLoader:(AVAssetResourceLoader*)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest*)loadingRequest
{
//如果用戶在下載的過程中調用者取消了獲取視頻,則從緩存中取消這個請求
NSURL*resourceURL = [loadingRequest.requestURL];
NSString*actualURLString = [selfactualURLStringWithURL:resourceURL];
AVResourceLoaderForASI*loader = [_resourceLoaders objectForKey:actualURLString];
[loader removeRequest:loadingRequest];
}
第四步,判斷緩存中是否已下載完視頻
- (void)addRequest:(AVAssetResourceLoadingRequest*)loadingRequest
{
//1判斷自身是否已經(jīng)取消加載
if(self.isCancelled==NO){
//2判斷本地中是否已經(jīng)有文件的緩存,如果有,則直接從緩存中讀取數(shù)據(jù),文件保存和讀取這里不做詳述,使用者可根據(jù)自身情況創(chuàng)建文件系統(tǒng)
AVAResourceFile*resourceFile = [self.resourceFileManagerresourceFileWithURL:self.resourceURL];
if(resourceFile) {
//3若本地文件存在,則從文件中獲取以下屬性
loadingRequest.contentInformationRequest.byteRangeAccessSupported=YES;
//3.1contentType
loadingRequest.contentInformationRequest.contentType= resourceFile.contentType;
//3.2數(shù)據(jù)長度
loadingRequest.contentInformationRequest.contentLength= resourceFile.contentLength;
//3.3請求的偏移量
longlongrequestedOffset = loadingRequest.dataRequest.requestedOffset;
//3.4請求總長度
NSIntegerrequestedLength = loadingRequest.dataRequest.requestedLength;
//3.5取出本地文件中從偏移量到請求長度的數(shù)據(jù)
NSData*subData = [resourceFile.datasubdataWithRange:NSMakeRange(@(requestedOffset).unsignedIntegerValue, requestedLength)];
//3.6返回數(shù)據(jù)給請求
[loadingRequest.dataRequestrespondWithData:subData];
[loadingRequest finishLoading];
}else{
//4如果沒有本地文件,則開啟網(wǎng)絡請求,從網(wǎng)絡中獲取 ,見第五步
[selfstartWithRequest:loadingRequest];
}
}
else{
//5如果已經(jīng)取消請求,并且請求沒有完成,則封裝錯誤給請求,可自己實現(xiàn)
if(loadingRequest.isFinished==NO){
[loadingRequest finishLoadingWithError:[selfloaderCancelledError]];
}
}
}
第五步,添加loadingRequest到網(wǎng)絡文件加載器,這部分的操作比較長
- (void)startWithRequest:(AVAssetResourceLoadingRequest*)loadingRequest
{
1//判斷當前請求是否已經(jīng)開啟,由于蘋果系統(tǒng)原因,會有兩次回調到AVResourceLoaderDelegate,我們對其進行判斷,只開啟一次請求
if(self.dataTask==nil){
2//根據(jù)loadingRequest中的URL創(chuàng)建NSURLRequest,注意在此把URL中的scheme修改為原先的scheme
NSURLRequest*request = [selfrequestWithLoadingRequest:loadingRequest];
__weak__typeof(self)weakSelf =self;
3//獲取url的絕對路徑,并使用ASIHttpRequest進行網(wǎng)絡請求,下面的請求方法經(jīng)過封裝,就不詳說如何對ASI進行封裝了,但是每一步需要做的事情能以block的形式更好說明
NSString*urlString = request.URL.absoluteString;
self.dataTask= [selfGET:urlString requestBlock:^(Request *req) {
NSLog(@"### %s %@ ###", __func__, req);
4//在接受到請求頭部信息時,說明鏈接成功,數(shù)據(jù)開始傳輸
if(req.recvingHeader//意思是請求接受到頭部信息狀態(tài)){
NSLog(@"### %s recvingHeader ###", __func__);
__strong__typeof(weakSelf)strongSelf = weakSelf;
if([urlString isEqualToString:req.originalURL.absoluteString]) {
4.1//,創(chuàng)建臨時數(shù)據(jù)保存網(wǎng)絡下載下來的視頻信息
strongSelf.tempData= [NSMutableDatadata];
}
4.2//把頭部信息內容寫入到AVAssetResourceLoadingRequest,即loadingRequest中
[strongSelf processPendingRequests];
}
elseif(req.recving//請求接受中狀態(tài)){
NSLog(@"### %s recving ###", __func__);
__strong__typeof(weakSelf)strongSelf = weakSelf;
5//此處需多次調用把請求的信息寫入到loadingRequest的步驟,實現(xiàn)下載的過程中數(shù)據(jù)能輸出到loadingRequest播放
if(urlString == req.originalURL.absoluteString) {
5.1//這個處理是判斷此時返回的頭部信息是重定向還是實際視頻的頭部信息,如果是重定向信息,則不作處理
if(!_contentInformation && req.responseHeaders) {
if([req.responseHeadersobjectForKey:@"Location"] ) {
NSLog(@" ### %s redirection URL ###", __func__);
}else{
//5.2如果不是重定向信息,則把需要用到的信息提取出來
_contentInformation = [[RLContentInformationForASI alloc]init];
longlongnumer = [[req.responseHeadersobjectForKey:@"Content-Length"]longLongValue];
_contentInformation.contentLength= numer;
_contentInformation.byteRangeAccessSupported=YES;
_contentInformation.contentType= [req.responseHeadersobjectForKey:@"Content-type"];
}
}
//5.3開始從請求中獲取返回數(shù)據(jù)
NSLog(@"### %s before tempData length = %lu ###", __FUNCTION__, (unsignedlong)self.tempData.length);
strongSelf.tempData= [NSMutableDatadataWithData:req.rawResponseData];
NSLog(@"### %s after tempData length = %lu ###",__FUNCTION__, (unsignedlong)self.tempData.length);
//5.4把返回數(shù)據(jù)輸出到loadingRequest中
[strongSelf processPendingRequests];
}
}elseif(req.succeed){
6//請求返回成功,在這里做最后一次把數(shù)據(jù)輸出到loadingRequest,且做一些成功后的事情
NSLog(@"### %s succeed ###", __func__);
NSLog(@"### %s tempData length = %lu ###", __func__, (unsignedlong)self.tempData.length);
__strong__typeof(weakSelf)strongSelf = weakSelf;
if(strongSelf) {
[strongSelf processPendingRequests];
7//保存緩存文件,我在保存文件這里做了一次偷懶,如果有人參考我寫的文件可對保存文件作改進,在每次返回數(shù)據(jù)時把數(shù)據(jù)追加寫到文件,而不是下載成功之后才保存,這請求時也可以使用這個來實現(xiàn)斷點重輸?shù)墓δ?/p>
AVAResourceFile*resourceFile = [[AVAResourceFilealloc]initWithContentType:strongSelf.contentInformation.contentTypedate:strongSelf.tempData];
[strongSelf.resourceFileManagersaveResourceFile:resourceFile withURL:self.resourceURL];
8//在此做一些清理緩存、釋放對象和回調到上層的操作
[strongSelf complete];
if(strongSelf.delegate&& [strongSelf.delegaterespondsToSelector:@selector(resourceLoader:didLoadResource:)]) {
[strongSelf.delegateresourceLoader:strongSelf didLoadResource:strongSelf.resourceURL];
}
}
}elseif(req.failed){
//9如果請求返回失敗,則向上層拋出錯誤,且清理緩存等操作
NSLog(@"### %s failed ###", __func__);
[selfcompleteWithError:req.error];
}
}];
}
[self.pendingRequestsaddObject:loadingRequest];
}
第六步,把請求返回數(shù)據(jù)輸出到loadingRequest的操作
- (void)processPendingRequests
{
__weak__typeof(self)weakSelf =self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong__typeof(weakSelf)strongSelf = weakSelf;
NSMutableArray*requestsCompleted = [NSMutableArrayarray];
1//從緩存信息中找出當前正在請求中的loadingRequest
for(AVAssetResourceLoadingRequest*loadingRequestinstrongSelf.pendingRequests){
2//把頭部信息輸出到loadingRequest中
[strongSelf fillInContentInformation:loadingRequest.contentInformationRequest];
3//把視頻數(shù)據(jù)輸出到loadingRequest中
BOOLdidRespondCompletely = [strongSelf respondWithDataForRequest:loadingRequest.dataRequest];
4//在success狀態(tài)中做最后一次調用的時候,檢測到請求已經(jīng)完成,則從緩存信息中清除loadingRequest,并且把loadingRequest標志為完成處理狀態(tài)
if(didRespondCompletely){
[requestsCompleted addObject:loadingRequest];
[loadingRequest finishLoading];
}
}
5//清理緩存
[strongSelf.pendingRequestsremoveObjectsInArray:requestsCompleted];
});
}
、
//把提取出來的頭部信息輸出到loadingRequest中,可以優(yōu)化
- (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;
}
//把緩存數(shù)據(jù)輸出到loadingRequest中
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest*)dataRequest
{
longlongstartOffset = dataRequest.requestedOffset;
if(dataRequest.currentOffset!=0){
startOffset = dataRequest.currentOffset;
}
// Don't have any data at all for this request
if(self.tempData.length< startOffset){
returnNO;
}
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUIntegerunreadBytes =self.tempData.length- (NSUInteger)startOffset;
// Respond with whatever is available if we can't satisfy the request fully yet
NSUIntegernumberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
[dataRequest respondWithData:[self.tempDatasubdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]];
longlongendOffset = startOffset + dataRequest.requestedLength;
BOOLdidRespondFully =self.tempData.length>= endOffset;
returndidRespondFully;
}
視頻邊下邊播的流程大致上已經(jīng)描述完畢,本博文中沒有說到的代碼有錯誤處理方式、緩存文件的讀寫和保存格式、部分內存緩存使用說明、
參考鏈接:
http://www.codeproject.com/Articles/875105/Audio-streaming-and-caching-in-iOS-using
http://www.cnblogs.com/kenshincui/p/4186022.html#mpMoviePlayerController
補充:
在開發(fā)過程中遇到的一些坑在這里補充一下
1.在iOS9后,AVPlayer的replaceCurrentItemWithPlayerItem方法在切換視頻時底層會調用信號量等待然后導致當前線程卡頓,如果在UITableViewCell中切換視頻播放使用這個方法,會導致當前線程凍結幾秒鐘。遇到這個坑還真不好在系統(tǒng)層面對它做什么,后來找到的解決方法是在每次需要切換視頻時,需重新創(chuàng)建AVPlayer和AVPlayerItem。
2.iOS9后,AVFoundation框架還做了幾點修改,如果需要切換視頻播放的時間,或需要控制視頻從頭播放調用seekToDate方法,需要保持視頻的播放rate大于0才能修改,還有canUseNetworkResourcesForLiveStreamingWhilePaused這個屬性,在iOS9前默認為YES,之后默認為NO。
3.AVPlayer的replaceCurrentItemWithPlayerItem方法正常是會引用住參數(shù)AVPlayerItem的,但在某些情況下導致視頻播放失敗,它會馬上釋放對這個對象的持有,假如你對AVPlayerItem的實例對象添加了監(jiān)聽,但是自己沒有對item的計數(shù)進行管理,不知道什么時候釋放這個監(jiān)聽,則會導致程序崩潰。
4.為什么我選擇第三種方法實現(xiàn)邊下邊播,第一種方法需要程序引入LocalServer庫,需增加大量app包大小,且需要開啟本地服務,從性能方面考慮也是不合適。第二種方式存在的缺陷很多,一來只能播放網(wǎng)絡上返回格式contentType為public/mpeg4等視頻格式的url視頻地址,若保存下來之后,文件的格式也需要保存為.mp4或.mov等格式的本地文件才能從本地中讀取,三來使用AVMutableComposition對視頻進行重構后保存,經(jīng)過檢驗會對視頻源數(shù)據(jù)產生變化,對于程序開發(fā)人員來說,需要保證各端存在的視頻數(shù)據(jù)一致。第三種邊下邊播的方法其實是對第二種方法的擴展,能夠解決上面所說的三種問題,可操控的自由度更高。
http://sky-weihao.github.io/2015/10/06/Video-streaming-and-caching-in-iOS/