視頻邊下邊播--緩存播放數據流-b


google搜索“iOS視頻變下邊播”,有好幾篇博客寫到了實現方法,其實只有一篇,其他都是copy的,不過他們都是使用的本地代理服務器的方式。

 

原理很簡單,但是缺點也很明顯,需要自己寫一個本地代理服務器或者使用第三方庫httpSever。

 

如果使用httpSever作為本地代理服務器,如果只緩存一個視頻是沒有問題的,如果緩存多個視頻互相切換,本地代理服務器提供的數據很不穩定,crash概率非常大。

 


這里我采用ios7以后系統自帶的方法實現視頻邊下邊播,這里的邊下邊播不是單獨開一個子線程去下載,而是把視頻播放的數據給保存到本地。

 

用到的框架:<AVFoundation/AVFoundation.h> 用到的播放器:AVplayer

 

先說一下avplayer自身的播放原理,當我們給播放器設置好url等一些參數后,播放器就會向url所在的服務器發送請求

 

(請求參數有兩個值,一個是offset偏移量,另一個是length長度,其實就相當於NSRange一樣),

 

服務器就根據range參數給播放器返回數據。

 

這就是大致的原理,當然實際的過程還是略微比較復雜。


 

下面進入主題

 

產品需求:

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

 

2.如果視頻加載完成且完整,將視頻文件保存到本地cache,下一次播放本地cache中的視頻,不再請求網絡數據

 

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

實現方案:

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

 

2.AVURLAsset中的resourceLoader可以實現這個機制,resourceLoader的delegate就是上述的代理對象。

 

3.視頻播放器在開始播放之前首先檢測是本地cache中是否有此視頻,如果沒有才通過代理獲得數據,如果有,則直接播放本地cache中的視頻即可。

視頻播放器需要實現的功能

1.有開始暫停按鈕

2.顯示播放進度及總時長

3.可以通過拖拽從任意位置開始播放視頻

4.視頻加載中的過程和加載失敗需要有相應的提示
<br/>

 

代理對象需要實現的功能

1.接收視頻播放器的請求,並根據請求的range向服務器請求本地沒有獲得的數據

2.緩存向服務器請求回的數據到本地

3.如果向服務器的請求出現錯誤,需要通知給視頻播放器,以便視頻播放器對用戶進行提示

<br/>

具體流程圖

1.png

 

視頻播放器處理流程

  1. 當開始播放視頻時,通過視頻url判斷本地cache中是否已經緩存當前視頻,如果有,則直接播放本地cache中視頻

 

2.如果本地cache中沒有視頻,則視頻播放器向代理請求數據

 

3.加載視頻時展示正在加載的提示(菊花轉)

 

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

 

5.在播放過程中如果由於網絡過慢或拖拽原因導致沒有播放數據時,要展示加載提示,跳轉到第4步

 

代理對象處理流程

1.當視頻播放器向代理請求dataRequest時,判斷代理是否已經向服務器發起了請求,如果沒有,則發起下載整個視頻文件的請求

 

2.如果代理已經和服務器建立鏈接,則判斷當前的dataRequest請求的offset是否大於當前已經緩存的文件的offset,

 

如果大於則取消當前與服務器的請求,並從offset開始到文件尾向服務器發起請求(此時應該是由於播放器向后拖拽,並且超過了已緩存的數據時才會出現)

 

3.如果當前的dataRequest請求的offset小於已經緩存的文件的offset,同時大於代理向服務器請求的range的offset,

 

說明有一部分已經緩存的數據可以傳給播放器,則將這部分數據返回給播放器(此時應該是由於播放器向前拖拽,請求的數據已經緩存過才會出現)

 

4.如果當前的dataRequest請求的offset小於代理向服務器請求的range的offset,則取消當前與服務器的請求,

 

並從offset開始到文件尾向服務器發起請求(此時應該是由於播放器向前拖拽,並且超過了已緩存的數據時才會出現)

 

5.只要代理重新向服務器發起請求,就會導致緩存的數據不連續,則加載結束后不用將緩存的數據放入本地cache

 

6.如果代理和服務器的鏈接超時,重試一次,如果還是錯誤則通知播放器網絡錯誤

 

7.如果服務器返回其他錯誤,則代理通知播放器網絡錯誤


 

resourceLoader的難點處理

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shou

ldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest

*)loadingRequest
{    
[self.pendingRequests addObject:loadingRequest];    
[self dealWithLoadingRequest:loadingRequest];    

return YES;
}

播放器發出的數據請求從這里開始,我們保存從這里發出的所有請求存放到數組,自己來處理這些請求,當一個請求完成后,對請求發出finishLoading消息,並從數組中移除。

 

正常狀態下,當播放器發出下一個請求的時候,會把上一個請求給finish。

 

下面這個方法發出的請求說明播放器自己關閉了這個請求,我們不需要再對這個請求進行處理,系統每次結束一個舊的請求,

 

便必然會發出一個或多個新的請求,除了播放器已經獲得整個視頻完整的數據,這時候就不會再發起請求。

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didC

ancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{    
[self.pendingRequests removeObject:loadingRequest]; }

 

下面這個方法是對播放器發出的請求進行填充數據

- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataReques

t *)dataRequest
{    long long startOffset = dataRequest.requestedOffset;    if
(dataRequest.currentOffset != 0) {        
startOffset = dataRequest.currentOffset;    
}    if ((self.task.offset +self.task.downLoadingOffset) < startO

ffset)    
{        //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; }

 

這是對存放所有的請求的數組進行處理

- (void)processPendingRequests
{    NSMutableArray *requestsCompleted = [NSMutableArray array];  
//請求完成的數組    
//每次下載一塊數據都是一次請求,把這些請求放到數組,遍歷數組    
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingR

equests)    
{        
[self fillInContentInformation:loadingRequest.contentInformationReq

uest]; //對每次請求加上長度,文件類型等信息       
BOOL didRespondCompletely = [self respondWithDataForRequest:loading

Request.dataRequest]; //判斷此次請求的數據是否處理完全if (didRespondCompletely) {
[requestsCompleted addObject:loadingRequest];  

//如果完整,把此次請求放進 請求完成的數組            
[loadingRequest finishLoading];        
}    
} [self.pendingRequests removeObjectsInArray:requestsCompleted];   /

/在所有請求的數組中移除已經完成的}

resourceLoader的難點基本上就是上面這點了,說到播放器,下面便順便講下AVPlayer的難點。

 

難點:對播放器狀態的捕獲

 

舉個簡單的例子,視頻總長度60分,現在緩沖的數據才10分鍾,然后拖動到20分鍾的位置進行播放。

 

在網速較慢的時候,視頻從當前位置開始播放,必然會出現一段時間的卡頓。

 

為了有一個更好的用戶體驗,在卡頓的時候,我們需要加一個菊花轉的狀態,現在問題就來了。

 

在拖動到未緩沖區域內,是否需要加菊花轉,如果加,要顯示多久再消失,而且如果在網速很慢的時候,播放器如果等了太久,哪怕最后有數據了,播放器也已經“死”了,它自己無法恢復播放。

 

這個時候需要我們人為的去恢復播放,如果恢復播放不成功,那么過一段時間需要再次恢復播放,是否恢復播放成功,這里也需要捕獲其狀態。

 

所以,如果要有一個好的用戶體驗,我們需要時時知道播放器的狀態。

 

 

有兩個狀態需要捕獲,一個是正在緩沖,一個是正在播放,監聽播放的“playbackBufferEmpty”屬性就可以捕獲正在緩沖狀態,播放器的時間監聽器則可以捕獲正在播放狀態,我的demo中一共有4個狀態:

typedef NS_ENUM(NSInteger, TBPlayerState) {
TBPlayerStateBuffering = 1,    
TBPlayerStatePlaying   = 2,    
TBPlayerStateStopped   = 3,    
TBPlayerStatePause     = 4};

 

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


然后說一說在緩沖時候的處理,以及緩沖后多久去播放,處理方法:


進入緩沖狀態后,緩沖2秒后去手動播放,如果播放不成功(緩沖的數據太少,還不足以播放),那就再緩沖2秒再次播放,如此循環,看詳細代碼:

- (void)bufferingSomeSecond {    

// playbackBufferEmpty會反復進入,因此在bufferingOneSecond延時播放

執行完之前再調用bufferingSomeSecond都忽略    
static BOOL isBuffering = NO;    if (isBuffering) {        return;    
}    
isBuffering = YES;    // 需要先暫停一小會之后再播放,否則網絡狀況

不好的時候時間在走,聲音播放不出來    
[self.player pause];    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{        

// 如果此時用戶已經暫停了,則不再需要開啟播放了        
if (self.isPauseByUser) {            
isBuffering = NO;            return;        
}
[self.player play];        

// 如果執行了play還是沒有播放則說明還沒有緩存好,則再次緩存一段時間        
isBuffering = NO;        

if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp)
{            
[self bufferingSomeSecond];        
}    
});
}

這個demo花了我很長的時間,實現這個demo我也遇到了很多坑最后才完成的,現在我奉獻出來,也許對你會有所幫助。

 

如果你覺得不錯,還請為我Star一個,也算是對我的支持和鼓勵。

 

demo下載地址【https://github.com/suifengqjn/TBPlayer】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM