轉(zhuǎn)自博客:iOS視頻邊下邊播--緩存播放數(shù)據(jù)流
google搜索“iOS視頻變下邊播”,有好幾篇博客寫到了實(shí)現(xiàn)方法固额,其實(shí)只有一篇头谜,其他都是copy的狐蜕,不過他們都是使用的本地代理服務(wù)器的方式宠纯,原理很簡(jiǎn)單,但是缺點(diǎn)也很明顯层释,需要自己寫一個(gè)本地代理服務(wù)器或者使用第三方庫httpSever婆瓜。如果使用httpSever作為本地代理服務(wù)器,如果只緩存一個(gè)視頻是沒有問題的贡羔,如果緩存多個(gè)視頻互相切換廉白,本地代理服務(wù)器提供的數(shù)據(jù)很不穩(wěn)定,crash概率非常大乖寒。
這里我采用ios7以后系統(tǒng)自帶的方法實(shí)現(xiàn)視頻邊下邊播猴蹂,這里的邊下邊播不是單獨(dú)開一個(gè)子線程去下載,而是把視頻播放的數(shù)據(jù)給保存到本地楣嘁。簡(jiǎn)而言之磅轻,就是使用一遍的流量,既播放了視頻逐虚,也保存了視頻聋溜。
用到的框架:<AVFoundation/AVFoundation.h>
用到的播放器:AVplayer
先說一下avplayer自身的播放原理,當(dāng)我們給播放器設(shè)置好url等一些參數(shù)后叭爱,播放器就會(huì)向url所在的服務(wù)器發(fā)送請(qǐng)求(請(qǐng)求參數(shù)有兩個(gè)值撮躁,一個(gè)是offset偏移量,另一個(gè)是length長(zhǎng)度涤伐,其實(shí)就相當(dāng)于NSRange一樣)馒胆,服務(wù)器就根據(jù)range參數(shù)給播放器返回?cái)?shù)據(jù)缨称。這就是大致的原理凝果,當(dāng)然實(shí)際的過程還是略微比較復(fù)雜。
下面進(jìn)入主題
產(chǎn)品需求:
1.支持正常播放器的一切功能睦尽,包括暫停器净、播放和拖拽
2.如果視頻加載完成且完整,將視頻文件保存到本地cache当凡,下一次播放本地cache中的視頻山害,不再請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)
3.如果視頻沒有加載完(半路關(guān)閉或者拖拽)就不用保存到本地cache
實(shí)現(xiàn)方案:
1.需要在視頻播放器和服務(wù)器之間添加一層類似代理的機(jī)制,視頻播放器不再直接訪問服務(wù)器沿量,而是訪問代理對(duì)象浪慌,代理對(duì)象去訪問服務(wù)器獲得數(shù)據(jù),之后返回給視頻播放器朴则,同時(shí)代理對(duì)象根據(jù)一定的策略緩存數(shù)據(jù)权纤。
2.AVURLAsset中的resourceLoader可以實(shí)現(xiàn)這個(gè)機(jī)制,resourceLoader的delegate就是上述的代理對(duì)象。
3.視頻播放器在開始播放之前首先檢測(cè)是本地cache中是否有此視頻汹想,如果沒有才通過代理獲得數(shù)據(jù)外邓,如果有,則直接播放本地cache中的視頻即可古掏。
視頻播放器需要實(shí)現(xiàn)的功能
1.有開始暫停按鈕
2.顯示播放進(jìn)度及總時(shí)長(zhǎng)
3.可以通過拖拽從任意位置開始播放視頻
4.視頻加載中的過程和加載失敗需要有相應(yīng)的提示
代理對(duì)象需要實(shí)現(xiàn)的功能
1.接收視頻播放器的請(qǐng)求损话,并根據(jù)請(qǐng)求的range向服務(wù)器請(qǐng)求本地沒有獲得的數(shù)據(jù)
2.緩存向服務(wù)器請(qǐng)求回的數(shù)據(jù)到本地
3.如果向服務(wù)器的請(qǐng)求出現(xiàn)錯(cuò)誤,需要通知給視頻播放器槽唾,以便視頻播放器對(duì)用戶進(jìn)行提示
具體流程圖
視頻播放器處理流程
1.當(dāng)開始播放視頻時(shí)丧枪,通過視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻,如果有夏漱,則直接播放本地cache中視頻
2.如果本地cache中沒有視頻豪诲,則視頻播放器向代理請(qǐng)求數(shù)據(jù)
3.加載視頻時(shí)展示正在加載的提示(菊花轉(zhuǎn))
4.如果可以正常播放視頻,則去掉加載提示挂绰,播放視頻屎篱,如果加載失敗,去掉加載提示并顯示失敗提示
5.在播放過程中如果由于網(wǎng)絡(luò)過慢或拖拽原因?qū)е聸]有播放數(shù)據(jù)時(shí)葵蒂,要展示加載提示交播,跳轉(zhuǎn)到第4步
代理對(duì)象處理流程
1.當(dāng)視頻播放器向代理請(qǐng)求dataRequest時(shí),判斷代理是否已經(jīng)向服務(wù)器發(fā)起了請(qǐng)求践付,如果沒有秦士,則發(fā)起下載整個(gè)視頻文件的請(qǐng)求
2.如果代理已經(jīng)和服務(wù)器建立鏈接,則判斷當(dāng)前的dataRequest請(qǐng)求的offset是否大于當(dāng)前已經(jīng)緩存的文件的offset永高,如果大于則取消當(dāng)前與服務(wù)器的請(qǐng)求隧土,并從offset開始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向后拖拽,并且超過了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
3.如果當(dāng)前的dataRequest請(qǐng)求的offset小于已經(jīng)緩存的文件的offset命爬,同時(shí)大于代理向服務(wù)器請(qǐng)求的range的offset曹傀,說明有一部分已經(jīng)緩存的數(shù)據(jù)可以傳給播放器,則將這部分?jǐn)?shù)據(jù)返回給播放器(此時(shí)應(yīng)該是由于播放器向前拖拽饲宛,請(qǐng)求的數(shù)據(jù)已經(jīng)緩存過才會(huì)出現(xiàn))
4.如果當(dāng)前的dataRequest請(qǐng)求的offset小于代理向服務(wù)器請(qǐng)求的range的offset皆愉,則取消當(dāng)前與服務(wù)器的請(qǐng)求,并從offset開始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向前拖拽艇抠,并且超過了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
5.只要代理重新向服務(wù)器發(fā)起請(qǐng)求幕庐,就會(huì)導(dǎo)致緩存的數(shù)據(jù)不連續(xù),則加載結(jié)束后不用將緩存的數(shù)據(jù)放入本地cache
6.如果代理和服務(wù)器的鏈接超時(shí)家淤,重試一次异剥,如果還是錯(cuò)誤則通知播放器網(wǎng)絡(luò)錯(cuò)誤
7.如果服務(wù)器返回其他錯(cuò)誤,則代理通知播放器網(wǎng)絡(luò)錯(cuò)誤
resourceLoader的難點(diǎn)處理
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
[self.pendingRequests addObject:loadingRequest];
[self dealWithLoadingRequest:loadingRequest];
return YES;
}
播放器發(fā)出的數(shù)據(jù)請(qǐng)求從這里開始絮重,我們保存從這里發(fā)出的所有請(qǐng)求存放到數(shù)組冤寿,自己來處理這些請(qǐng)求错妖,當(dāng)一個(gè)請(qǐng)求完成后,對(duì)請(qǐng)求發(fā)出finishLoading消息疚沐,并從數(shù)組中移除暂氯。正常狀態(tài)下,當(dāng)播放器發(fā)出下一個(gè)請(qǐng)求的時(shí)候亮蛔,會(huì)把上一個(gè)請(qǐng)求給finish痴施。
下面這個(gè)方法發(fā)出的請(qǐng)求說明播放器自己關(guān)閉了這個(gè)請(qǐng)求,我們不需要再對(duì)這個(gè)請(qǐng)求進(jìn)行處理究流,系統(tǒng)每次結(jié)束一個(gè)舊的請(qǐng)求辣吃,便必然會(huì)發(fā)出一個(gè)或多個(gè)新的請(qǐng)求,除了播放器已經(jīng)獲得整個(gè)視頻完整的數(shù)據(jù)芬探,這時(shí)候就不會(huì)再發(fā)起請(qǐng)求神得。
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
[self.pendingRequests removeObject:loadingRequest];
}
下面這個(gè)方法是對(duì)播放器發(fā)出的請(qǐng)求進(jìn)行填充數(shù)據(jù)
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != 0) {
startOffset = dataRequest.currentOffset;
}
if ((self.task.offset +self.task.downLoadingOffset) < startOffset)
{
//NSLog(@"NO DATA FOR REQUEST");
return NO;
}
if (startOffset < self.task.offset) {
return NO;
}
NSData *filedata = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:_videoPath] options:NSDataReadingMappedIfSafe error:nil];
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.task.downLoadingOffset - ((NSInteger)startOffset - self.task.offset);
// Respond with whatever is available if we can't satisfy the request fully yet
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
[dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.task.offset, (NSUInteger)numberOfBytesToRespondWith)]];
long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = (self.task.offset + self.task.downLoadingOffset) >= endOffset;
return didRespondFully;
}
這是對(duì)存放所有的請(qǐng)求的數(shù)組進(jìn)行處理
- (void)processPendingRequests
{
NSMutableArray *requestsCompleted = [NSMutableArray array]; //請(qǐng)求完成的數(shù)組
//每次下載一塊數(shù)據(jù)都是一次請(qǐng)求,把這些請(qǐng)求放到數(shù)組偷仿,遍歷數(shù)組
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
{
[self fillInContentInformation:loadingRequest.contentInformationRequest]; //對(duì)每次請(qǐng)求加上長(zhǎng)度哩簿,文件類型等信息
BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判斷此次請(qǐng)求的數(shù)據(jù)是否處理完全
if (didRespondCompletely) {
[requestsCompleted addObject:loadingRequest]; //如果完整,把此次請(qǐng)求放進(jìn) 請(qǐng)求完成的數(shù)組
[loadingRequest finishLoading];
}
}
[self.pendingRequests removeObjectsInArray:requestsCompleted]; //在所有請(qǐng)求的數(shù)組中移除已經(jīng)完成的
}
resourceLoader的難點(diǎn)基本上就是上面這點(diǎn)了酝静,說到播放器节榜,下面便順便講下AVPlayer的難點(diǎn)。
難點(diǎn):對(duì)播放器狀態(tài)的捕獲
舉個(gè)簡(jiǎn)單的例子别智,視頻總長(zhǎng)度60分宗苍,現(xiàn)在緩沖的數(shù)據(jù)才10分鐘,然后拖動(dòng)到20分鐘的位置進(jìn)行播放薄榛,在網(wǎng)速較慢的時(shí)候讳窟,視頻從當(dāng)前位置開始播放,必然會(huì)出現(xiàn)一段時(shí)間的卡頓敞恋,為了有一個(gè)更好的用戶體驗(yàn)丽啡,在卡頓的時(shí)候,我們需要加一個(gè)菊花轉(zhuǎn)的狀態(tài)耳舅,現(xiàn)在問題就來了碌上。
在拖動(dòng)到未緩沖區(qū)域內(nèi)倚评,是否需要加菊花轉(zhuǎn)浦徊,如果加,要顯示多久再消失天梧,而且如果在網(wǎng)速很慢的時(shí)候盔性,播放器如果等了太久,哪怕最后有數(shù)據(jù)了呢岗,播放器也已經(jīng)“死”了冕香,它自己無法恢復(fù)播放蛹尝,這個(gè)時(shí)候需要我們?nèi)藶榈娜セ謴?fù)播放,如果恢復(fù)播放不成功悉尾,那么過一段時(shí)間需要再次恢復(fù)播放突那,是否恢復(fù)播放成功,這里也需要捕獲其狀態(tài)构眯。所以愕难,如果要有一個(gè)好的用戶體驗(yàn),我們需要時(shí)時(shí)知道播放器的狀態(tài)惫霸。
有兩個(gè)狀態(tài)需要捕獲猫缭,一個(gè)是正在緩沖,一個(gè)是正在播放壹店,監(jiān)聽播放的“playbackBufferEmpty”屬性就可以捕獲正在緩沖狀態(tài)猜丹,播放器的時(shí)間監(jiān)聽器則可以捕獲正在播放狀態(tài),我的demo中一共有4個(gè)狀態(tài):
typedef NS_ENUM(NSInteger, TBPlayerState) {
TBPlayerStateBuffering = 1,
TBPlayerStatePlaying = 2,
TBPlayerStateStopped = 3,
TBPlayerStatePause = 4
};
這樣可以對(duì)播放器更好的把握和處理了硅卢。
然后說一說在緩沖時(shí)候的處理射窒,以及緩沖后多久去播放,處理方法:
進(jìn)入緩沖狀態(tài)后将塑,緩沖2秒后去手動(dòng)播放轮洋,如果播放不成功(緩沖的數(shù)據(jù)太少,還不足以播放)抬旺,那就再緩沖2秒再次播放弊予,如此循環(huán),看詳細(xì)代碼:
- (void)bufferingSomeSecond
{
// playbackBufferEmpty會(huì)反復(fù)進(jìn)入开财,因此在bufferingOneSecond延時(shí)播放執(zhí)行完之前再調(diào)用bufferingSomeSecond都忽略
static BOOL isBuffering = NO;
if (isBuffering) {
return;
}
isBuffering = YES;
// 需要先暫停一小會(huì)之后再播放汉柒,否則網(wǎng)絡(luò)狀況不好的時(shí)候時(shí)間在走,聲音播放不出來
[self.player pause];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 如果此時(shí)用戶已經(jīng)暫停了责鳍,則不再需要開啟播放了
if (self.isPauseByUser) {
isBuffering = NO;
return;
}
[self.player play];
// 如果執(zhí)行了play還是沒有播放則說明還沒有緩存好碾褂,則再次緩存一段時(shí)間
isBuffering = NO;
if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp) {
[self bufferingSomeSecond];
}
});
}
這個(gè)demo花了我很長(zhǎng)的時(shí)間,實(shí)現(xiàn)這個(gè)demo我也遇到了很多坑最后才完成的历葛,現(xiàn)在我奉獻(xiàn)出來正塌,也許對(duì)你會(huì)有所幫助。如果你覺得不錯(cuò)恤溶,還請(qǐng)為我Star一個(gè)乓诽,也算是對(duì)我的支持和鼓勵(lì)。
參考文章
作者:夜千尋墨
鏈接:http://www.reibang.com/p/990ee3db0563
來源:簡(jiǎn)書
著作權(quán)歸作者所有咒程。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)鸠天,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。