(轉(zhuǎn))iOS視頻邊下邊播--緩存播放數(shù)據(jù)流

轉(zhuǎn)載鏈接:http://m.itdecent.cn/p/990ee3db0563

google搜索“iOS視頻變下邊播”,有好幾篇博客寫(xiě)到了實(shí)現(xiàn)方法,其實(shí)只有一篇,其他都是copy的,不過(guò)他們都是使用的本地代理服務(wù)器的方式,原理很簡(jiǎn)單,但是缺點(diǎn)也很明顯,需要自己寫(xiě)一個(gè)本地代理服務(wù)器或者使用第三方庫(kù)httpSever。如果使用httpSever作為本地代理服務(wù)器,如果只緩存一個(gè)視頻是沒(méi)有問(wèn)題的,如果緩存多個(gè)視頻互相切換,本地代理服務(wù)器提供的數(shù)據(jù)很不穩(wěn)定,crash概率非常大。

這里我采用ios7以后系統(tǒng)自帶的方法實(shí)現(xiàn)視頻邊下邊播,這里的邊下邊播不是單獨(dú)開(kāi)一個(gè)子線程去下載,而是把視頻播放的數(shù)據(jù)給保存到本地。簡(jiǎn)而言之,就是使用一遍的流量,既播放了視頻,也保存了視頻。

用到的框架:用到的播放器:AVplayer

先說(shuō)一下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í)際的過(guò)程還是略微比較復(fù)雜。

下面進(jìn)入主題

產(chǎn)品需求:

1.支持正常播放器的一切功能,包括暫停、播放和拖拽

2.如果視頻加載完成且完整,將視頻文件保存到本地cache,下一次播放本地cache中的視頻,不再請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)

3.如果視頻沒(méi)有加載完(半路關(guān)閉或者拖拽)就不用保存到本地cache

實(shí)現(xiàn)方案:

1.需要在視頻播放器和服務(wù)器之間添加一層類似代理的機(jī)制,視頻播放器不再直接訪問(wèn)服務(wù)器,而是訪問(wèn)代理對(duì)象,代理對(duì)象去訪問(wèn)服務(wù)器獲得數(shù)據(jù),之后返回給視頻播放器,同時(shí)代理對(duì)象根據(jù)一定的策略緩存數(shù)據(jù)。

2.AVURLAsset中的resourceLoader可以實(shí)現(xiàn)這個(gè)機(jī)制,resourceLoader的delegate就是上述的代理對(duì)象。

3.視頻播放器在開(kāi)始播放之前首先檢測(cè)是本地cache中是否有此視頻,如果沒(méi)有才通過(guò)代理獲得數(shù)據(jù),如果有,則直接播放本地cache中的視頻即可。

視頻播放器需要實(shí)現(xiàn)的功能

1.有開(kāi)始暫停按鈕

2.顯示播放進(jìn)度及總時(shí)長(zhǎng)

3.可以通過(guò)拖拽從任意位置開(kāi)始播放視頻

4.視頻加載中的過(guò)程和加載失敗需要有相應(yīng)的提示

代理對(duì)象需要實(shí)現(xiàn)的功能

1.接收視頻播放器的請(qǐng)求,并根據(jù)請(qǐng)求的range向服務(wù)器請(qǐng)求本地沒(méi)有獲得的數(shù)據(jù)

2.緩存向服務(wù)器請(qǐng)求回的數(shù)據(jù)到本地

3.如果向服務(wù)器的請(qǐng)求出現(xiàn)錯(cuò)誤,需要通知給視頻播放器,以便視頻播放器對(duì)用戶進(jìn)行提示

具體流程圖


971366-0a9b11be2df75aaa.png

視頻播放器處理流程

1.當(dāng)開(kāi)始播放視頻時(shí),通過(guò)視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻,如果有,則直接播放本地cache中視頻

2.如果本地cache中沒(méi)有視頻,則視頻播放器向代理請(qǐng)求數(shù)據(jù)

3.加載視頻時(shí)展示正在加載的提示(菊花轉(zhuǎn))

4.如果可以正常播放視頻,則去掉加載提示,播放視頻,如果加載失敗,去掉加載提示并顯示失敗提示

5.在播放過(guò)程中如果由于網(wǎng)絡(luò)過(guò)慢或拖拽原因?qū)е聸](méi)有播放數(shù)據(jù)時(shí),要展示加載提示,跳轉(zhuǎn)到第4步

代理對(duì)象處理流程

1.當(dāng)視頻播放器向代理請(qǐng)求dataRequest時(shí),判斷代理是否已經(jīng)向服務(wù)器發(fā)起了請(qǐng)求,如果沒(méi)有,則發(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開(kāi)始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向后拖拽,并且超過(guò)了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))

3.如果當(dāng)前的dataRequest請(qǐng)求的offset小于已經(jīng)緩存的文件的offset,同時(shí)大于代理向服務(wù)器請(qǐng)求的range的offset,說(shuō)明有一部分已經(jīng)緩存的數(shù)據(jù)可以傳給播放器,則將這部分?jǐn)?shù)據(jù)返回給播放器(此時(shí)應(yīng)該是由于播放器向前拖拽,請(qǐng)求的數(shù)據(jù)已經(jīng)緩存過(guò)才會(huì)出現(xiàn))

4.如果當(dāng)前的dataRequest請(qǐng)求的offset小于代理向服務(wù)器請(qǐng)求的range的offset,則取消當(dāng)前與服務(wù)器的請(qǐng)求,并從offset開(kāi)始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向前拖拽,并且超過(guò)了已緩存的數(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];    [selfdealWithLoadingRequest:loadingRequest];returnYES;

}

播放器發(fā)出的數(shù)據(jù)請(qǐng)求從這里開(kāi)始,我們保存從這里發(fā)出的所有請(qǐng)求存放到數(shù)組,自己來(lái)處理這些請(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)求說(shuō)明播放器自己關(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 *)resourceLoaderdidCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest

{    

[self.pendingRequestsremoveObject: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 = [NSMutableArrayarray];

//請(qǐng)求完成的數(shù)組//每次下載一塊數(shù)據(jù)都是一次請(qǐng)求,把這些請(qǐng)求放到數(shù)組,遍歷數(shù)組for(AVAssetResourceLoadingRequest *loadingRequestin 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)了,說(shuō)到播放器,下面便順便講下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)前位置開(kāi)始播放,必然會(huì)出現(xiàn)一段時(shí)間的卡頓,為了有一個(gè)更好的用戶體驗(yàn),在卡頓的時(shí)候,我們需要加一個(gè)菊花轉(zhuǎn)的狀態(tài),現(xiàn)在問(wèn)題就來(lái)了。

在拖動(dòng)到未緩沖區(qū)域內(nèi),是否需要加菊花轉(zhuǎn),如果加,要顯示多久再消失,而且如果在網(wǎng)速很慢的時(shí)候,播放器如果等了太久,哪怕最后有數(shù)據(jù)了,播放器也已經(jīng)“死”了,它自己無(wú)法恢復(fù)播放,這個(gè)時(shí)候需要我們?nèi)藶榈娜セ謴?fù)播放,如果恢復(fù)播放不成功,那么過(guò)一段時(shí)間需要再次恢復(fù)播放,是否恢復(fù)播放成功,這里也需要捕獲其狀態(tài)。所以,如果要有一個(gè)好的用戶體驗(yàn),我們需要時(shí)時(shí)知道播放器的狀態(tài)。

有兩個(gè)狀態(tài)需要捕獲,一個(gè)是正在緩沖,一個(gè)是正在播放,監(jiān)聽(tīng)播放的“playbackBufferEmpty”屬性就可以捕獲正在緩沖狀態(tài),播放器的時(shí)間監(jiān)聽(tīng)器則可以捕獲正在播放狀態(tài),我的demo中一共有4個(gè)狀態(tài):


typedef NS_ENUM(NSInteger,TBPlayerState) {

TBPlayerStateBuffering= 1,

TBPlayerStatePlaying= 2,

TBPlayerStateStopped= 3,

TBPlayerStatePause= 4

};

這樣可以對(duì)播放器更好的把握和處理了。

然后說(shuō)一說(shuō)在緩沖時(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í)間在走,聲音播放不出來(lái)

[self.player pause];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

// 如果此時(shí)用戶已經(jīng)暫停了,則不再需要開(kāi)啟播放了

if (self.isPauseByUser) {

isBuffering = NO;

return;

}

[self.player play];

// 如果執(zhí)行了play還是沒(méi)有播放則說(shuō)明還沒(méi)有緩存好,則再次緩存一段時(shí)間

isBuffering = NO;

if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp) {

[self bufferingSomeSecond];

}

});

}

這個(gè)demo花了我很長(zhǎng)的時(shí)間,實(shí)現(xiàn)這個(gè)demo我也遇到了很多坑最后才完成的,現(xiàn)在我奉獻(xiàn)出來(lái),也許對(duì)你會(huì)有所幫助。如果你覺(jué)得不錯(cuò),還請(qǐng)為我Star一個(gè),也算是對(duì)我的支持和鼓勵(lì)。

參考文章

demo下載地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容