一、MPMoviePlayerController 和 AVPlayer 簡(jiǎn)介
iOS 9 之前通常播放音頻、視頻都是基于MediaPlayer框架的MPMoviePlayerControler以及基于它的MPMoviePlayerViewController,后者提供了統(tǒng)一的控制界面泥栖;iOS 9后iOS推出了AVKit的播放器AVPlayer和AVPlayerViewController。AVKit提供的功能更加強(qiáng)大勋篓,所以我們基于AVKit里的AVPlayer進(jìn)行二次開(kāi)發(fā)吧享。
二、邊下邊播實(shí)現(xiàn)
關(guān)鍵字
- AVPlayer和AVURLAsset
- AVAssetResourceLoaderDelegate
- NSURLSession
邊下邊播目前搜索到的幾篇博客差不多都是參考這篇英文文章進(jìn)行實(shí)現(xiàn)的譬嚣,這里大概復(fù)述一遍基本的原理:
首先钢颂,AVPlayer可以播放AVAsset資源文件這個(gè)不用多說(shuō),而AVURLAsset專門用于播放網(wǎng)絡(luò)音視頻資源拜银,基本的播放殊鞭、暫停、拖動(dòng)操作均能提供,AVURLAsset有個(gè)方法
- (void)setDelegate:(nullable id <AVAssetResourceLoaderDelegate>)delegate queue:(nullable dispatch_queue_t)delegateQueue;
兩個(gè)傳入?yún)?shù):AVAssetResourceLoaderDelegate 實(shí)現(xiàn)了該協(xié)議的資源加載器和處理該資源加載的隊(duì)列尼桶,就是給AVURLAsset添加一個(gè)資源加載的代理對(duì)象操灿,幫助AVURLAsset加載數(shù)據(jù)。
不過(guò)需要清楚一點(diǎn)泵督,當(dāng)提供的URL協(xié)議是AVURLAsset能識(shí)別處理的時(shí)候如(https://xxx.mp4)趾盐,該資源加載代理對(duì)象是不會(huì)收到來(lái)自AVURLAsset的數(shù)據(jù)加載請(qǐng)求的;
然而如果我們把URL改成如(sevenuncle://xxx.mp4)的時(shí)候小腊,由于AVURLAsset無(wú)法識(shí)別出該協(xié)議救鲤,就會(huì)轉(zhuǎn)向資源加載代理對(duì)象詢問(wèn)數(shù)據(jù)如何加載,此時(shí)就會(huì)調(diào)用AVAssetResourceLoaderDelegate 協(xié)議里的下述方法:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
AVURLAsset將數(shù)據(jù)加載請(qǐng)求轉(zhuǎn)發(fā)給了資源加載代理對(duì)象,AVURLAsset在播放一個(gè)音視頻文件的時(shí)候會(huì)按照當(dāng)前播放需要溢豆,發(fā)送一個(gè)AVAssetResourceLoadingRequest請(qǐng)求,這個(gè)請(qǐng)求包含了播放所需數(shù)據(jù)的請(qǐng)求封裝類AVAssetResourceLoadingDataRequest瘸羡,該類主要包含三個(gè)屬性:
@property (nonatomic, readonly) long long requestedOffset //請(qǐng)求數(shù)據(jù)的起點(diǎn)
@property (nonatomic, readonly) NSInteger requestedLength //請(qǐng)求長(zhǎng)度
@property (nonatomic, readonly) long long currentOffset;// 該請(qǐng)求當(dāng)前已填充的數(shù)據(jù)位置
所以AVURLAsset播放請(qǐng)求是以片段的方式發(fā)出請(qǐng)求的漩仙,而且是同時(shí)發(fā)送多個(gè)數(shù)據(jù)片段請(qǐng)求,所以在資源加載代理對(duì)象里需要把這些數(shù)據(jù)片段請(qǐng)求都保存起來(lái)。
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
//保存數(shù)據(jù)請(qǐng)求
[self addLoadingRequest:loadingRequest];
return YES;
}
- (void)addLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
[self.pendingRequests addObject:loadingRequest];
//下載資源
[self doDownloadSource:loadingRequest];
}
同時(shí)队他,資源加載代理對(duì)象這個(gè)時(shí)候就需要清楚資源文件真正下載方式(如恢復(fù)成Https://xxx.mp4)卷仑,資源加載代理對(duì)象自己去開(kāi)啟網(wǎng)絡(luò)請(qǐng)求去下載該資源,可以自己封裝一個(gè)資源文件下載類麸折,該類負(fù)責(zé)實(shí)際的數(shù)據(jù)下載锡凝,
NSMutableRequest *request = [NSMutableURLRequest requestWithURL:effectiveURL];
NSString *value = [NSString stringWithFormat:@"bytes=%lld-%lld",requestOffset, requestLength]; //制定下載的范圍
[request addValue:value forHTTPHeaderField:@"Range"];
self.dataTask = [self.session dataTaskWithRequest:request];
[self.dataTask resume];
當(dāng)完成一段數(shù)據(jù)下載(HTTP本身已經(jīng)實(shí)現(xiàn)了資源文件請(qǐng)求時(shí)的分段傳輸,多點(diǎn)斷點(diǎn)下載等)的時(shí)候通知資源加載代理對(duì)象垢啼,就去遍歷前面保存的AVAssetResourceLoadingDataRequest窜锯,
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.temporyFilePath];
[fileHandle seekToFileOffset:self.currentOffset];
[fileHandle writeData:data];
//通知數(shù)據(jù)更新
if(self.freshDataCachedHandler) {
self.freshDataCachedHandler();
}
}
資源加載代理對(duì)象將已有數(shù)據(jù)一點(diǎn)一點(diǎn)的填充進(jìn)去,
[dataRequest respondWithData:data];
每次填充一段數(shù)據(jù)進(jìn)AVAssetResourceLoadingDataRequest,屬性currentOffset就會(huì)往后移動(dòng)到數(shù)據(jù)已經(jīng)填充到的位置芭析,當(dāng)一個(gè)AVAssetResourceLoadingDataRequest數(shù)據(jù)填充完畢之后(currentOffset+requestOffset >= requestLength)锚扎,就對(duì)其發(fā)送一個(gè)完成請(qǐng)求的消息,并將其移除隊(duì)列馁启,完成一個(gè)數(shù)據(jù)片段的請(qǐng)求驾孔。
[loadingRequest finishLoading];
AVAssetResourceLoaderDelegate 的另外一個(gè)方法時(shí)當(dāng)seek等操作之后,AVURLAsset會(huì)取消之前發(fā)出去的一些數(shù)據(jù)請(qǐng)求:
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
此時(shí)惯疙,我們可以把之前加入到等待隊(duì)列的AVAssetResourceLoadingRequest移除隊(duì)列翠勉。
這樣就達(dá)到了播放的同時(shí),自己實(shí)現(xiàn)了下載文件的需求霉颠;不過(guò)需要注意的一點(diǎn)对碌,這個(gè)過(guò)程有個(gè)缺點(diǎn),當(dāng)播放進(jìn)度拖動(dòng)了之后掉分,由于下載任務(wù)重新開(kāi)始俭缓,則拖動(dòng)之后下載的文件是從拖動(dòng)的位置開(kāi)始下載,導(dǎo)致下載的文件不完整酥郭。
三华坦、多點(diǎn)下載、拖動(dòng)播放保存實(shí)現(xiàn)
上述已經(jīng)闡述了邊下邊播的過(guò)程不从,但是有一個(gè)缺點(diǎn)惜姐;當(dāng)播放時(shí)拖動(dòng)了進(jìn)度之后,下載的文件不完整椿息,此時(shí)需要解決也簡(jiǎn)單歹袁,拖動(dòng)播放進(jìn)度之后不重新開(kāi)啟下載任務(wù),不刪除之前下載的數(shù)據(jù)寝优,所以解決方法:
- 用一個(gè)數(shù)據(jù)結(jié)構(gòu)保存間斷的數(shù)據(jù)分段范圍如(1-100条舔、300-2000);
- 分段請(qǐng)求數(shù)據(jù)(HTTP頭里面的Range字段可以指定數(shù)據(jù)的起始范圍)乏矾;
- 當(dāng)請(qǐng)求拖動(dòng)位置后面的數(shù)據(jù)下載完成后孟抗,重新回到前面下載之前未下載的數(shù)據(jù)片段迁杨。
很明顯,這個(gè)可以保存數(shù)據(jù)下載分片進(jìn)度的數(shù)據(jù)結(jié)構(gòu)是關(guān)鍵凄硼,作者按照NSRange的基本實(shí)現(xiàn)思路铅协,也實(shí)現(xiàn)了一個(gè)自己的SURange(SURangePointer),以鏈表的形式保存了各個(gè)分片進(jìn)度摊沉。
struct _SURange{
unsigned long long location; //起點(diǎn)
unsigned long long length; //長(zhǎng)度
struct _SURange *next; //下一個(gè)節(jié)點(diǎn)
};
typedef struct _SURange SURange;
typedef struct _SURange *SURangePointer;
- 新增一個(gè)下載進(jìn)度
SURangePointer SUInsertNodeIntoRange(SURangePointer src, SURangePointer node1);
新增下載進(jìn)度函數(shù)狐史,舉個(gè)例子,src為[(0,100)(300,1000)]说墨,node1為(19,150)骏全,則結(jié)果為[(0,150)(300,1000)]
- 獲取未下載進(jìn)度
SURangePointer SUGetGapRanges(SURangePointer links, SURangePointer node);
獲取未下載進(jìn)度,可以獲取分割的未下載片段婉刀,如links為[(100,300)(500,10000)(20000,50000)],node為(400,1000000),則范圍未下載的進(jìn)度為[(400,499)(10001,19999)(50001,100000)]吟温。
這樣保存數(shù)據(jù)分片進(jìn)度的數(shù)據(jù)結(jié)構(gòu)完成了。
剩下的就好辦了突颊,現(xiàn)在下載數(shù)據(jù)時(shí)鲁豪,申請(qǐng)一個(gè)所需大小的空文件存放數(shù)據(jù),每當(dāng)發(fā)出一個(gè)新帶seek位置的播放請(qǐng)求律秃,保存數(shù)據(jù)時(shí)先seek到制定的偏移位置爬橡,然后將數(shù)據(jù)保存進(jìn)臨時(shí)文件,然后更新進(jìn)度棒动。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.temporyFilePath];
//移動(dòng)到當(dāng)前請(qǐng)求的偏移位置糙申,保存數(shù)據(jù)
[fileHandle seekToFileOffset:self.currentOffset];
[fileHandle writeData:data];
//插入下載進(jìn)度分片
SURange *range;
range = malloc(sizeof(SURange));
range->location = self.currentOffset;
range->length = data.length;
range->next = NULL;
_downloadRange = SUInsertNodeIntoRange(_downloadRange, range);
free(range);
//更新已經(jīng)下載數(shù)據(jù)偏移
self.currentOffset += data.length;
//通知數(shù)據(jù)更新
if(self.freshDataCachedHandler) {
self.freshDataCachedHandler();
}
}
當(dāng)完成一個(gè)數(shù)據(jù)NSURLRequest之后,尋找未下載的片段范圍船惨,繼續(xù)下載柜裸,
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
NSHTTPURLResponse *httpURLResponse = (NSHTTPURLResponse *)task.response;
NSDictionary *dict = [httpURLResponse allHeaderFields];
NSString *content = [dict valueForKey:@"Content-Range"];
NSString *offsetString = [NSString stringWithFormat:@"%lld", _currentOffset-1];
//如果不是seek之后取消了一個(gè)下載請(qǐng)求,則尋找未下載的片段范圍
if([content rangeOfString:offsetString].length>0) {
if (self.currentOffset >= self.resourceLength) {
[self seekToDownloadAtOffset:0 withURL:self.resourceURL];
}else {
[self seekToDownloadAtOffset:self.currentOffset withURL:self.resourceURL];
}
}
}
最后粱锐,等到所需進(jìn)度都下載完成疙挺,則將臨時(shí)文件保存持久化,同時(shí)添加進(jìn)緩存索引怜浅。與此同時(shí)利用SURange铐然,可以開(kāi)啟多個(gè)下載任務(wù)同時(shí)下載,共同維護(hù)一個(gè)下載進(jìn)度恶座,則多點(diǎn)下載也可以很快的實(shí)現(xiàn)搀暑。
demo地址:https://github.com/sevenuncler/SUAdvancePlayer
四、總結(jié)
- 變下邊播:使用自定義協(xié)議的URL傳入AVURLAsset,導(dǎo)致其將數(shù)據(jù)請(qǐng)求的任務(wù)交給實(shí)現(xiàn)了AVAssetResourceLoaderDelegate協(xié)議的數(shù)據(jù)加載代理對(duì)象跨琳;
- 數(shù)據(jù)加載代理自己負(fù)責(zé)數(shù)據(jù)的下載自点,然后一點(diǎn)點(diǎn)將下載的數(shù)據(jù)返回給AVURLAsset;
- 多點(diǎn)下載和Seek之后文件完整保存:需要實(shí)現(xiàn)一個(gè)能夠保存分片不連續(xù)進(jìn)度的數(shù)據(jù)結(jié)構(gòu)脉让,當(dāng)前請(qǐng)求的進(jìn)度下載完成之后尋找下一個(gè)未下載的數(shù)據(jù)進(jìn)度繼續(xù)下載桂敛。