轉(zhuǎn)自:博客
【demo下載地址】
近段時間制作視頻播放社區(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對應(yīng)著一個視頻資源。
iOS視頻實現(xiàn)邊下載邊播放的幾種實現(xiàn)
方案一.本地實現(xiàn)http server
在iOS本地開啟Local Server服務(wù)墓阀,然后使用播放控件請求本地Local Server服務(wù)毡惜,本地的服務(wù)再不斷請求視頻地址獲取視頻流,本地服務(wù)請求的過程中把視頻緩存到本地斯撮,這種方法在網(wǎng)上有很多例子经伙,有興趣了解的人可自己下載例子查看。
方案二.使用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:^{
}];
}
}
方案三.使用AVAssetResourceLoader回調(diào)下載,也是最終決定使用的技術(shù)
AVAssetResourceLoader通過你提供的委托對象去調(diào)節(jié)AVURLAsset所需要的加載資源螃征。而很重要的一點是搪桂,AVAssetResourceLoader僅在AVURLAsset不知道如何去加載這個URL資源時才會被調(diào)用,就是說你提供的委托對象在AVURLAsset不知道如何加載資源時才會得到調(dià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é)議
@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//判斷當前的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//判斷當前請求是否已經(jīng)開啟罩抗,由于蘋果系統(tǒng)原因,會有兩次回調(diào)到AVResourceLoaderDelegate灿椅,我們對其進行判斷套蒂,只開啟一次請求
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進行網(wǎng)絡(luò)請求操刀,下面的請求方法經(jīng)過封裝,就不詳說如何對ASI進行封裝了婴洼,但是每一步需要做的事情能以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//保存緩存文件,我在保存文件這里做了一次偷懶盈包,如果有人參考我寫的文件可對保存文件作改進沸呐,在每次返回數(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//從緩存信息中找出當前正在請求中的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標志為完成處理狀態(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-using
http://www.cnblogs.com/kenshincui/p/4186022.html#mpMoviePlayerController
補充:
在開發(fā)過程中遇到的一些坑在這里補充一下
1.在iOS9后颇象,AVPlayer的replaceCurrentItemWithPlayerItem方法在切換視頻時底層會調(diào)用信號量等待然后導(dǎo)致當前線程卡頓,如果在UITableViewCell中切換視頻播放使用這個方法并徘,會導(dǎo)致當前線程凍結(jié)幾秒鐘遣钳。遇到這個坑還真不好在系統(tǒng)層面對它做什么,后來找到的解決方法是在每次需要切換視頻時麦乞,需重新創(chuàng)建AVPlayer和AVPlayerItem蕴茴。
2.iOS9后,AVFoundation框架還做了幾點修改姐直,如果需要切換視頻播放的時間倦淀,或需要控制視頻從頭播放調(diào)用seekToDate方法,需要保持視頻的播放rate大于0才能修改声畏,還有canUseNetworkResourcesForLiveStreamingWhilePaused這個屬性撞叽,在iOS9前默認為YES,之后默認為NO砰识。
3.AVPlayer的replaceCurrentItemWithPlayerItem方法正常是會引用住參數(shù)AVPlayerItem的能扒,但在某些情況下導(dǎo)致視頻播放失敗,它會馬上釋放對這個對象的持有辫狼,假如你對AVPlayerItem的實例對象添加了監(jiān)聽初斑,但是自己沒有對item的計數(shù)進行管理,不知道什么時候釋放這個監(jiān)聽膨处,則會導(dǎo)致程序崩潰见秤。
4.為什么我選擇第三種方法實現(xiàn)邊下邊播,第一種方法需要程序引入LocalServer庫真椿,需增加大量app包大小鹃答,且需要開啟本地服務(wù),從性能方面考慮也是不合適突硝。第二種方式存在的缺陷很多测摔,一來只能播放網(wǎng)絡(luò)上返回格式contentType為public/mpeg4等視頻格式的url視頻地址,若保存下來之后解恰,文件的格式也需要保存為.mp4或.mov等格式的本地文件才能從本地中讀取锋八,三來使用AVMutableComposition對視頻進行重構(gòu)后保存,經(jīng)過檢驗會對視頻源數(shù)據(jù)產(chǎn)生變化护盈,對于程序開發(fā)人員來說挟纱,需要保證各端存在的視頻數(shù)據(jù)一致。第三種邊下邊播的方法其實是對第二種方法的擴展腐宋,能夠解決上面所說的三種問題紊服,可操控的自由度更高檀轨。