google搜尋“iOS視訊變下邊播”,有好幾篇部落格寫到了實現方法,其實只有一篇,其他都是copy的,不過他們都是使用的本地代理伺服器的方式,原理很簡單,但是缺點也很明顯,需要自己寫一個本地代理伺服器或者使用第三方庫httpSever。如果使用httpSever作為本地代理伺服器,如果只快取一個視訊是沒有問題的,如果快取多個視訊互相切換,本地代理伺服器提供的資料很不穩定,crash概率非常大。
這裡我採用ios7以後系統自帶的方法實現視訊邊下邊播,這裡的邊下邊播不是單獨開一個子執行緒去下載,而是把視訊播放的資料給儲存到本地。簡而言之,就是使用一遍的流量,既播放了視訊,也儲存了視訊。
1 2 3 |
用到的框架: 用到的播放器: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.視訊載入中的過程和載入失敗需要有相應的提示
代理物件需要實現的功能
- 1.接收視訊播放器的請求,並根據請求的range向伺服器請求本地沒有獲得的資料
- 2.快取向伺服器請求回的資料到本地
- 3.如果向伺服器的請求出現錯誤,需要通知給視訊播放器,以便視訊播放器對使用者進行提示
具體流程圖
視訊播放器處理流程
- 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的難點處理
1 2 3 4 5 6 7 |
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { [self.pendingRequests addObject:loadingRequest]; [self dealWithLoadingRequest:loadingRequest]; return YES; } |
播放器發出的資料請求從這裡開始,我們儲存從這裡發出的所有請求存放到陣列,自己來處理這些請求,當一個請求完成後,對請求發出finishLoading訊息,並從陣列中移除。正常狀態下,當播放器發出下一個請求的時候,會把上一個請求給finish。
下面這個方法發出的請求說明播放器自己關閉了這個請求,我們不需要再對這個請求進行處理,系統每次結束一箇舊的請求,便必然會發出一個或多個新的請求,除了播放器已經獲得整個視訊完整的資料,這時候就不會再發起請求。
1 2 3 4 5 |
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { [self.pendingRequests removeObject:loadingRequest]; } |
下面這個方法是對播放器發出的請求進行填充資料
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest { long long startOffset = dataRequest.requestedOffset; if (dataRequest.currentOffset != 0) { startOffset = dataRequest.currentOffset; } if ((self.task.offset +self.task.downLoadingOffset) = endOffset; return didRespondFully; } |
這是對存放所有的請求的陣列進行處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)processPendingRequests { NSMutableArray *requestsCompleted = [NSMutableArray array]; //請求完成的陣列 //每次下載一塊資料都是一次請求,把這些請求放到陣列,遍歷陣列 for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests) { [self fillInContentInformation:loadingRequest.contentInformationRequest]; //對每次請求加上長度,檔案型別等資訊 BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判斷此次請求的資料是否處理完全 if (didRespondCompletely) { [requestsCompleted addObject:loadingRequest]; //如果完整,把此次請求放進 請求完成的陣列 [loadingRequest finishLoading]; } } [self.pendingRequests removeObjectsInArray:requestsCompleted]; //在所有請求的陣列中移除已經完成的 } |
resourceLoader的難點基本上就是上面這點了,說到播放器,下面便順便講下AVPlayer的難點。
難點:對播放器狀態的捕獲
舉個簡單的例子,視訊總長度60分,現在緩衝的資料才10分鐘,然後拖動到20分鐘的位置進行播放,在網速較慢的時候,視訊從當前位置開始播放,必然會出現一段時間的卡頓,為了有一個更好的使用者體驗,在卡頓的時候,我們需要加一個菊花轉的狀態,現在問題就來了。
在拖動到未緩衝區域內,是否需要加菊花轉,如果加,要顯示多久再消失,而且如果在網速很慢的時候,播放器如果等了太久,哪怕最後有資料了,播放器也已經“死”了,它自己無法恢復播放,這個時候需要我們人為的去恢復播放,如果恢復播放不成功,那麼過一段時間需要再次恢復播放,是否恢復播放成功,這裡也需要捕獲其狀態。所以,如果要有一個好的使用者體驗,我們需要時時知道播放器的狀態。
有兩個狀態需要捕獲,一個是正在緩衝,一個是正在播放,監聽播放的“playbackBufferEmpty”屬性就可以捕獲正在緩衝狀態,播放器的時間監聽器則可以捕獲正在播放狀態,我的demo中一共有4個狀態:
1 2 3 4 5 6 |
typedef NS_ENUM(NSInteger, TBPlayerState) { TBPlayerStateBuffering = 1, TBPlayerStatePlaying = 2, TBPlayerStateStopped = 3, TBPlayerStatePause = 4 }; |
這樣可以對播放器更好的把握和處理了。
然後說一說在緩衝時候的處理,以及緩衝後多久去播放,處理方法:
進入緩衝狀態後,緩衝2秒後去手動播放,如果播放不成功(緩衝的資料太少,還不足以播放),那就再緩衝2秒再次播放,如此迴圈,看詳細程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
- (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一個,也算是對我的支援和鼓勵。