本片為轉(zhuǎn)載內(nèi)容,主要是以后自己看起來方便一些
原文地址: iOS音視頻實現(xiàn)邊下載邊播放
其實音視頻本地緩存的思想都差不多,都需要一個中間對象來連接播放器和服務(wù)器汉操。
近段時間制作視頻播放社區(qū)的功能坪蚁,期間查找了不少資料,做過很多嘗試豹休,現(xiàn)在來整理一下其中遇到的一些坑.由于考慮到AVPlayer對視頻有更高自由度的控制炊昆,而且能夠使用它自定義視頻播放界面,iOS中所使用的視頻播放控件為AVPlayer威根,而拋棄了高層次的MediaPlayer框架凤巨,現(xiàn)在想想挺慶幸當(dāng)初使用了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對應(yīng)著一個視頻資源症歇。
iOS視頻實現(xiàn)邊下載邊播放的幾種實現(xiàn)
1.本地實現(xiàn)http server
在iOS本地開啟Local Server服務(wù)郎笆,然后使用播放控件請求本地Local Server服務(wù),本地的服務(wù)再不斷請求視頻地址獲取視頻流忘晤,本地服務(wù)請求的過程中把視頻緩存到本地宛蚓,這種方法在網(wǎng)上有很多例子,有興趣了解的人可自己下載例子查看设塔。
2.使用AVPlayer的方法開啟下載服務(wù)
1.AVURLAsset *urlAsset = [[AVURLAsset alloc]initWithURL:url options:nil];
2.AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
3.[self.avPlayer replaceCurrentItemWithPlayerItem:item];
4.[self addObserverToPlayerItem: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)絡(luò)url下載下來的視頻沒有保存視頻的原始數(shù)據(jù)(或者蘋果沒有暴露接口給我們獲壤鳌)随抠,下載后播放的avasset不能使用AVAssetExportSession輸出到本地文件裁着,要曲線地把下載下來的視頻通過重構(gòu)成另外一個AVAsset實例才能輸出。代碼例子如下:
NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *myPathDocument = [documentDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4",[_source.videoUrl MD5]]];
NSURL *fileUrl = [NSURL fileURLWithPath:myPathDocument];
if (asset != nil) {
AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];
AVMutableCompositionTrack *firstTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
[firstTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:0] atTime:kCMTimeZero error:nil];
AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0] atTime:kCMTimeZero error:nil];
AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality];
exporter.outputURL = fileUrl;
if (exporter.supportedFileTypes) {
exporter.outputFileType = [exporter.supportedFileTypes objectAtIndex:0] ;
exporter.shouldOptimizeForNetworkUse = YES;
[exporter exportAsynchronouslyWithCompletionHandler:^{
}];
}
}
3.使用AVAssetResourceLoader回調(diào)下載拱她,也是最終決定使用的技術(shù)
AVAssetResourceLoader通過你提供的委托對象去調(diào)節(jié)AVURLAsset所需要的加載資源跨算。而很重要的一點是,AVAssetResourceLoader僅在AVURLAsset不知道如何去加載這個URL資源時才會被調(diào)用椭懊,就是說你提供的委托對象在AVURLAsset不知道如何加載資源時才會得到調(diào)用诸蚕。所以我們又要通過一些方法來曲線解決這個問題,把我們目標(biāo)視頻URL地址的scheme替換為系統(tǒng)不能識別的scheme氧猬,然后在我們調(diào)用網(wǎng)絡(luò)請求去處理這個URL時把scheme切換為原來的scheme背犯。
實現(xiàn)邊下邊播功能AVResourceLoader的委托對象必須要實現(xiàn)AVAssetResourceLoaderDelegate下五個協(xié)議的其中兩個:
1//在系統(tǒng)不知道如何處理URLAsset資源時回調(diào)
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 6_0);
2//在取消加載資源后回調(diào)
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 7_0);
以下來說說具體要怎么做處理
第一步,創(chuàng)建一個AVURLAsset盅抚,并且用它來初始化一個AVPlayerItem
#define kCustomVideoScheme @"yourScheme"
NSURL *currentURL = [NSURL URLWithString:@"http://***.***.***"];
NSURLComponents *components = [[NSURLComponents alloc]initWithURL:currentURL resolvingAgainstBaseURL:NO];
1////注意漠魏,不加這一句不能執(zhí)行到回調(diào)操作
components.scheme = kCustomVideoScheme;
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:components.URL
options:nil];
2//_resourceManager在接下來講述
[urlAsset.resourceLoader setDelegate:_resourceManager queue:dispatch_get_main_queue()];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
_playerItem = item;
if (IOS9_OR_LATER) {
item.canUseNetworkResourcesForLiveStreamingWhilePaused = YES;
}
[self.avPlayer replaceCurrentItemWithPlayerItem:item];
self.playerLayer.player = self.avPlayer;
[self addObserverToPlayerItem:item];**
第二步,創(chuàng)建AVResourceManager實現(xiàn)AVResourceLoader協(xié)議
1 @interface AVAResourceLoaderManager : NSObject < AVAssetResourceLoaderDelegate >
第三步妄均,實現(xiàn)兩個必須的回調(diào)協(xié)議柱锹,實現(xiàn)中有幾件需要做的事情
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
1//獲取系統(tǒng)中不能處理的URL
NSURL *resourceURL = [loadingRequest.request URL];
2//判斷這個URL是否遵守URL規(guī)范和其是否是我們所設(shè)定的URL
if ([self checkIsLegalURL:resourceURL] && [resourceURL.scheme isEqualToString:kCustomVideoScheme]){
3//判斷當(dāng)前的URL網(wǎng)絡(luò)請求是否已經(jīng)被加載過了,如果緩存中里面有URL對應(yīng)的網(wǎng)絡(luò)加載器(自己封裝丰包,也可以直接使用NSURLRequest)禁熏,則取出來添加請求,每一個URL對應(yīng)一個網(wǎng)絡(luò)加載器邑彪,loader的實現(xiàn)接下來會說明
AVResourceLoaderForASI *loader = [self asiresourceLoaderForRequest:loadingRequest];
if (loader == nil){
loader = [[AVResourceLoaderForASI alloc] initWithResourceURL:resourceURL];
loader.delegate = self;
4//緩存網(wǎng)絡(luò)加載器
[self.resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]];
}
5//加載器添加請求
[loader addRequest:loadingRequest];
6//返回YES則表明使用我們的代碼對AVAsset中請求網(wǎng)絡(luò)資源做處理
return YES;
}else{
return NO;
}
}
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
//如果用戶在下載的過程中調(diào)用者取消了獲取視頻,則從緩存中取消這個請求
NSURL *resourceURL = [loadingRequest.request URL];
NSString *actualURLString = [self actualURLStringWithURL: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.resourceFileManager resourceFileWithURL: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請求的偏移量
long long requestedOffset = loadingRequest.dataRequest.requestedOffset;
//3.4請求總長度
NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
//3.5取出本地文件中從偏移量到請求長度的數(shù)據(jù)
NSData *subData = [resourceFile.data subdataWithRange:NSMakeRange(@(requestedOffset).unsignedIntegerValue, requestedLength)];
//3.6返回數(shù)據(jù)給請求
[loadingRequest.dataRequest respondWithData:subData];
[loadingRequest finishLoading];
}else{
//4如果沒有本地文件释漆,則開啟網(wǎng)絡(luò)請求,從網(wǎng)絡(luò)中獲取 ,見第五步
[self startWithRequest:loadingRequest];
}
}
else{
//5如果已經(jīng)取消請求篮迎,并且請求沒有完成男图,則封裝錯誤給請求,可自己實現(xiàn)
if(loadingRequest.isFinished==NO){
[loadingRequest finishLoadingWithError:[self loaderCancelledError]];
}
}
}
第五步柑潦,添加loadingRequest到網(wǎng)絡(luò)文件加載器享言,這部分的操作比較長
- (void)startWithRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
1//判斷當(dāng)前請求是否已經(jīng)開啟,由于蘋果系統(tǒng)原因渗鬼,會有兩次回調(diào)到AVResourceLoaderDelegate,我們對其進(jìn)行判斷荧琼,只開啟一次請求
if (self.dataTask == nil){
2//根據(jù)loadingRequest中的URL創(chuàng)建NSURLRequest譬胎,注意在此把URL中的scheme修改為原先的scheme
NSURLRequest *request = [self requestWithLoadingRequest:loadingRequest];
__weak __typeof(self)weakSelf = self;
3//獲取url的絕對路徑差牛,并使用ASIHttpRequest進(jìn)行網(wǎng)絡(luò)請求,下面的請求方法經(jīng)過封裝堰乔,就不詳說如何對ASI進(jìn)行封裝了偏化,但是每一步需要做的事情能以block的形式更好說明
NSString *urlString = request.URL.absoluteString;
self.dataTask = [self GET: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)絡(luò)下載下來的視頻信息
strongSelf.tempData = [NSMutableData data];
}
4.2//把頭部信息內(nèi)容寫入到AVAssetResourceLoadingRequest,即loadingRequest中
[strongSelf processPendingRequests];
}
else if (req.recving//請求接受中狀態(tài)){
NSLog(@"### %s recving ###", __func__);
__strong __typeof(weakSelf)strongSelf = weakSelf;
5//此處需多次調(diào)用把請求的信息寫入到loadingRequest的步驟苟翻,實現(xiàn)下載的過程中數(shù)據(jù)能輸出到loadingRequest播放
if (urlString == req.originalURL.absoluteString) {
5.1//這個處理是判斷此時返回的頭部信息是重定向還是實際視頻的頭部信息韵卤,如果是重定向信息,則不作處理
if (!_contentInformation && req.responseHeaders) {
if ([req.responseHeaders objectForKey:@"Location"] ) {
NSLog(@" ### %s redirection URL ###", __func__);
}else{
//5.2如果不是重定向信息崇猫,則把需要用到的信息提取出來
_contentInformation = [[RLContentInformationForASI alloc]init];
long long numer = [[req.responseHeaders objectForKey:@"Content-Length"]longLongValue];
_contentInformation.contentLength = numer;
_contentInformation.byteRangeAccessSupported = YES;
_contentInformation.contentType = [req.responseHeaders objectForKey:@"Content-type"];
}
}
//5.3開始從請求中獲取返回數(shù)據(jù)
NSLog(@"### %s before tempData length = %lu ###", __FUNCTION__, (unsigned long)self.tempData.length);
strongSelf.tempData = [NSMutableData dataWithData:req.rawResponseData];
NSLog(@"### %s after tempData length = %lu ###",__FUNCTION__, (unsigned long)self.tempData.length);
//5.4把返回數(shù)據(jù)輸出到loadingRequest中
[strongSelf processPendingRequests];
}
}else if (req.succeed){
6//請求返回成功沈条,在這里做最后一次把數(shù)據(jù)輸出到loadingRequest,且做一些成功后的事情
NSLog(@"### %s succeed ###", __func__);
NSLog(@"### %s tempData length = %lu ###", __func__, (unsigned long)self.tempData.length);
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
[strongSelf processPendingRequests];
7//保存緩存文件诅炉,我在保存文件這里做了一次偷懶蜡歹,如果有人參考我寫的文件可對保存文件作改進(jìn),在每次返回數(shù)據(jù)時把數(shù)據(jù)追加寫到文件涕烧,而不是下載成功之后才保存月而,這請求時也可以使用這個來實現(xiàn)斷點重輸?shù)墓δ?AVAResourceFile *resourceFile = [[AVAResourceFile alloc]initWithContentType:strongSelf.contentInformation.contentType date:strongSelf.tempData];
[strongSelf.resourceFileManager saveResourceFile:resourceFile withURL:self.resourceURL];
8//在此做一些清理緩存、釋放對象和回調(diào)到上層的操作
[strongSelf complete];
if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(resourceLoader:didLoadResource:)]) {
[strongSelf.delegate resourceLoader:strongSelf didLoadResource:strongSelf.resourceURL];
}
}
}else if (req.failed){
//9如果請求返回失敗议纯,則向上層拋出錯誤景鼠,且清理緩存等操作
NSLog(@"### %s failed ###" , __func__);
[self completeWithError:req.error];
}
}];
}
[self.pendingRequests addObject:loadingRequest];
}
第六步,把請求返回數(shù)據(jù)輸出到loadingRequest的操作
- (void)processPendingRequests
{
__weak __typeof(self)weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
NSMutableArray *requestsCompleted = [NSMutableArray array];
1//從緩存信息中找出當(dāng)前正在請求中的loadingRequest
for (AVAssetResourceLoadingRequest *loadingRequest in strongSelf.pendingRequests){
2//把頭部信息輸出到loadingRequest中
[strongSelf fillInContentInformation:loadingRequest.contentInformationRequest];
3//把視頻數(shù)據(jù)輸出到loadingRequest中
BOOL didRespondCompletely = [strongSelf respondWithDataForRequest:loadingRequest.dataRequest];
4//在success狀態(tài)中做最后一次調(diào)用的時候痹扇,檢測到請求已經(jīng)完成铛漓,則從緩存信息中清除loadingRequest,并且把loadingRequest標(biāo)志為完成處理狀態(tài)
if (didRespondCompletely){
[requestsCompleted addObject:loadingRequest];
[loadingRequest finishLoading];
}
}
5//清理緩存
[strongSelf.pendingRequests removeObjectsInArray: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
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != 0){
startOffset = dataRequest.currentOffset;
}
// Don't have any data at all for this request
if (self.tempData.length < startOffset){
return NO;
}
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.tempData.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.tempData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]];
long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = self.tempData.length >= endOffset;
return didRespondFully;
}
視頻邊下邊播的流程大致上已經(jīng)描述完畢,本博文中沒有說到的代碼有錯誤處理方式结笨、緩存文件的讀寫和保存格式包晰、部分內(nèi)存緩存使用說明、
參考鏈接:http://www.codeproject.com/Articles/875105/Audio-streaming-and-caching-in-iOS-usinghttp://www.cnblogs.com/kenshincui/p/4186022.html#mpMoviePlayerController
補充:在開發(fā)過程中遇到的一些坑在這里補充一下1.在iOS9后炕吸,AVPlayer的replaceCurrentItemWithPlayerItem方法在切換視頻時底層會調(diào)用信號量等待然后導(dǎo)致當(dāng)前線程卡頓伐憾,如果在UITableViewCell中切換視頻播放使用這個方法,會導(dǎo)致當(dāng)前線程凍結(jié)幾秒鐘赫模。遇到這個坑還真不好在系統(tǒng)層面對它做什么树肃,后來找到的解決方法是在每次需要切換視頻時,需重新創(chuàng)建AVPlayer和AVPlayerItem瀑罗。2.iOS9后胸嘴,AVFoundation框架還做了幾點修改雏掠,如果需要切換視頻播放的時間,或需要控制視頻從頭播放調(diào)用seekToDate方法劣像,需要保持視頻的播放rate大于0才能修改乡话,還有canUseNetworkResourcesForLiveStreamingWhilePaused這個屬性,在iOS9前默認(rèn)為YES耳奕,之后默認(rèn)為NO绑青。3.AVPlayer的replaceCurrentItemWithPlayerItem方法正常是會引用住參數(shù)AVPlayerItem的,但在某些情況下導(dǎo)致視頻播放失敗屋群,它會馬上釋放對這個對象的持有闸婴,假如你對AVPlayerItem的實例對象添加了監(jiān)聽,但是自己沒有對item的計數(shù)進(jìn)行管理谓晌,不知道什么時候釋放這個監(jiān)聽掠拳,則會導(dǎo)致程序崩潰。4.為什么我選擇第三種方法實現(xiàn)邊下邊播纸肉,第一種方法需要程序引入LocalServer庫溺欧,需增加大量app包大小,且需要開啟本地服務(wù)柏肪,從性能方面考慮也是不合適姐刁。第二種方式存在的缺陷很多,一來只能播放網(wǎng)絡(luò)上返回格式contentType為public/mpeg4等視頻格式的url視頻地址烦味,若保存下來之后聂使,文件的格式也需要保存為.mp4或.mov等格式的本地文件才能從本地中讀取,三來使用AVMutableComposition對視頻進(jìn)行重構(gòu)后保存谬俄,經(jīng)過檢驗會對視頻源數(shù)據(jù)產(chǎn)生變化柏靶,對于程序開發(fā)人員來說,需要保證各端存在的視頻數(shù)據(jù)一致溃论。第三種邊下邊播的方法其實是對第二種方法的擴(kuò)展屎蜓,能夠解決上面所說的三種問題,可操控的自由度更高钥勋。