1. 程式人生 > >iOS開發之音視訊邊下邊播快取方案

iOS開發之音視訊邊下邊播快取方案

我還真沒看到目前有哪個公開的實現方案有做的更好的,可能是我孤陋寡聞,如果你知道更好的方案,一定要留言告訴我,鞠躬..

進入正題,這次的主要內容 

  • 理解 AVAssetResourceLoaderDelegate 的使用 

  • 快取下載的實現 

  • VIMediaCache 提供了哪些 API

接下來會介紹通過使用 AVAssetResourceLoader,在不改變 AVPlayer API 的情況下,對播放的音視訊進行快取。

前戲

現在市場上各種各樣的應用,充滿了多媒體資訊,而聲音和視訊又是體積最大的檔案,如果直接使用 URL 通過 AVPlayer 播放,系統並不會做快取處理,等下次再播又要重新下載,對網路狀況差的使用者來說這就是災難。若是下載好再播,同樣要等待全部下載完成,也是很痛苦。

我們最理想的快取方案是:邊播放,邊快取。

我在早期加入美拍團隊的時候,實際上已經有了邊下邊播的功能,當時選擇了使用 HTTPServer,在本地開啟一個 http 伺服器,把需要快取的請求地址指向本地伺服器,並帶上真正的 url 地址。

早期的美拍都是不到 20s 的短視訊,後面加長了視訊時間,但考慮到使用者裝置容量問題,我們只對短視訊做視訊快取。一直髮展到現在,平臺上現在大多數的視訊都是長視訊,真正使用到快取功能的頻率已經很低。那麼問題就來了,HTTPServer 不管我們有沒有使用快取功能,都要在應用開啟的時候默默開啟,這真的是很浪費了。並且我們引入 HTTPServer 庫也會增加一些包體積。

理解 AVAssetResourceLoaderDelegate 的使用

那麼在一段尋覓之下,發現了最適合做邊下邊播快取的工具。AVAssetResourceLoaderDelegate:一個 iOS 6 就被開放出來,專門用來處理 AVAsset 載入的工具。

AVURLAsset *urlAsset = ...
[urlAsset.resourceLoader setDelegate:<AVAssetResourceLoaderDelegate> queue:dispatch_get_main_queue()];

只要找一個物件實現了 AVAssetResourceLoaderDelegate 這個協議的方法,丟給 asset,再把 asset 丟給 AVPlayer,AVPlayer 在執行播放的時候就會去問這個 delegate:喂,你能不能播放這個 url 啊?然後會觸發下面這個方法:

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest 

我們在這個方法中看看 request 裡面的 url 是不是我們支援的,如果能支援就返回 YES!然後就可以開心的一邊下視訊資料,一邊塞資料給 AVPlayer 讓它顯示視訊畫面。

先不管下載和快取,實現上,可以分為兩步:1. 需要知道如何請求資料,url 是什麼,下載多少資料。2. 下載好的資料怎麼塞給 AVPlayer

1. 如何請求資料

在上面的回撥方法中,會得到一個 AVAssetResourceLoadingRequest 物件,它裡面的屬性和方法不多,為了減少干擾,我精簡了一下這個類的標頭檔案,只留下我們會用到以及需要解釋的屬性和方法:

@interface AVAssetResourceLoadingRequest : NSObject 
 
 @property (nonatomic, readonly) NSURLRequest *request;
 
 @property (nonatomic, readonly, nullable) AVAssetResourceLoadingContentInformationRequest *contentInformationRequest NS_AVAILABLE(10_9, 7_0);
 
 @property (nonatomic, readonly, nullable) AVAssetResourceLoadingDataRequest *dataRequest NS_AVAILABLE(10_9, 7_0);
 
 - (void)finishLoading NS_AVAILABLE(10_9, 7_0);
 
 - (void)finishLoadingWithError:(nullable NSError *)error;
 
 @end 

在 AVAssetResourceLoadingRequest 裡面,request 代表原始的請求,由於 AVPlayer 是會觸發分片下載的策略,還需要從dataRequest 中得到請求範圍的資訊。有了請求地址和請求範圍,我們就可以重新建立一個設定了請求 Range 頭的 NSURLRequest 物件,讓下載器去下載這個檔案的 Range 範圍內的資料。

2. 塞資料給 AVPlayer

當 AVPlayer 觸發下載時,總是會先發起一個 Range 為 0-2 的資料請求,這個請求的作用其實是用來確認視訊資料的資訊,如檔案型別、檔案資料長度。當下載器發起這個請求,收到服務端返回的 response 後,我們要把視訊的資訊填充到  AVAssetResourceLoadingRequest 的 contentInformationRequest 屬性中,告知下載的視訊格式以及視訊長度。

AVAssetResourceLoadingRequest 在 - (void)finishLoading 的時候,會根據 contentInformationRequest 中的資訊,去判斷接下去要怎麼處理。例如:下載 AVURLAsset 中 URL 指向的檔案,獲取到的檔案的 contentType 是系統不支援的型別,這個 AVURLAsset 將無法正常播放。

獲取完視訊資訊後,會收到剛才指定的 2 Byte 的 data 資料,下載到的資料怎麼辦? 可以塞給 AVAssetResourceLoadingRequest 裡的 dataRequest 。 dataRequest 裡面用 - (void)respondWithData:(NSData *)data; 專門用來接收下載的資料,這個方法可以呼叫多次,接收增量連續的 data 資料。

當 AVAssetResourceLoadingRequest 要求的所有資料都下載完畢,呼叫 - (void)finishLoading 完成下載,AVAssetResourceLoader 會繼續發起之後的資料片段的請求。如果本次請求失敗,可以直接呼叫 - (void)finishLoadingWithError:(nullable NSError *)error; 結束下載。

流程圖

完整實現的主流程是這樣的

重試機制

在實際的測試中,發現AVAssetResourceLoader 在執行載入的時候,會時不時的觸發取消下載呼叫 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest,然後重新發起載入請求的策略。如果下載了部分,那麼重新發起的下載請求會從還沒有下載的部分開始。

AVAssetResourceLoaderDelegate 中還有 3 個方法可以針對特殊場景做處理,不過在目前的環境中都用不到所以可以選擇不實現這些方法。

快取下載的實現

我們已經知道 AVAssetResourceLoaderDelegate 的實現機制,當 AVAsset 需要載入資料時會通過 delegate 告訴外部,外部接管整個視訊下載過程。

接管了視訊下載,便可以對視訊資料做任何事情。比如:快取、記錄下載速度、獲得下載進度等等。

實現一個下載器,就是用 URLSession 開啟一個 DataTask 請求資料,把接收到的資料塞給 DataRequest 並寫入本地磁碟。在實現下載器時主要有三個注意的點:1. Range 請求2. 可取消下載3. 分片快取

1. Range 請求

每次得到的 LoadingRequest 帶有請求資料範圍的資訊,比如期望請求第 100 位元組到 500 位元組,在建立 URLRequest 時需要設定 HTTPHeader 的 Range 值。

NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];

2. 可取消下載

AVAsset 在載入視訊時,經常會在某次資料請求還沒有完成時觸發取消下載,然後發起一個新的 LoadingReqeust。這個機制是 AVAsset 裡的黑盒,具體邏輯無法得知,比較像是 AVAsset 的一種重試機制。 作為下載器,在收到取消通知時,需要立刻停止下載。由於 DataRequest 的 cancel 操作是非同步的,就有可能在 cancel 還未完成時,下一個 LoadingRequest 就已經到來,所以還需要需要保證同一個 URL 只能同時存在一個下載器在下載,否則會出現資料混亂的問題。

3. 分片快取

如果只是單純的下載視訊,資料單調遞增,快取處理還是比較容易。然而現實是使用者對 player 的 seek 操作給視訊的快取管理帶來了巨大的挑戰,一旦涉及到使用者操作,可能性就越多,複雜度也會越高。

沒有 seek 的情況:網速正常時快取資料比播放時間走得開,正常播放;網速慢時,播放器 loading,直到有足夠的資料量進行播放,如果網速一直很慢就會播幾秒卡一下。

當加入 seek 後會有三種可能:

  • 視訊完全下載好,這時 seek 只需讀取相應快取

  • 視訊下載一半,使用者 seek 到未下載部分,LoadingRequest 請求的部分全部都是未下載的資料。這時需要取消正在下載的資料,然後從 seek 的點開始下載資料。為了支援 seek 操作,下載器就需要支援分片快取。目前使用的解決方案是下載的視訊資料會根據請求的 Range 值,把資料儲存到檔案中對應的偏移值位置,並且每個視訊檔案都會另外再儲存一個與之對應的下載資訊檔案。這個資訊檔案會記錄當前下載了多少資料,總共有多少資料,下載了哪些片段的資料等資訊,之後的快取管理會非常依賴這個配置檔案。

  • 視訊被 seek 了多次,使用者 seek 到一個時間點,LoadingRequest 請求的部分包含了已下載和未下載的部分。

這種情況是最複雜的!簡單的做法是,當成上面的情況來處理,全部都重新下載,雖然邏輯簡單,但這個方案會下載多次同樣的資料,不是最最優解。 

我的目標當然是做最優的解決方案,但也是複雜高很多的解決方案。

在收到 LoadingRequest 的請求範圍後,下載器會先獲取已經下載的資料資訊,把已下載的分片資訊分別建立一個 action,再把需要遠端下載的分片資料分別建立一個 action。最終組合就可能是 LocalAction(50-100 bytes) + RemoteAction(101-200 bytes) + LocalAction(201-300 bytes) + RemoteAction(300-400 bytes)。每一個 action 會按順序獲取資料再返回給 LoadingRequest。

VIMediaCache 提供了哪些 API

基本使用

VIMediaCache 主要提供了 VIResourceLoaderManager,這個類實現了 AVAssetResourceLoaderDelegate,並且提供了初始化一個 AVPlayerItem 的方法,平時使用時,只需用 VIResourceLoaderManager 建立一個 AVPlayerItem ,AVPlayer 再用這個 playerItem 初始化,AVPlayer 在播放的時候就會自動快取了。

VIResourceLoaderManager *resourceLoaderManager = [VIResourceLoaderManager new];
self.resourceLoaderManager = resourceLoaderManager;
AVPlayerItem *playerItem = [resourceLoaderManager playerItemWithURL:url];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];

快取管理

所有快取相關的資訊都在 VICacheManager 類中。目前提供了下載進度通知、修改快取目錄、根據 url 獲取快取地址、根據 url 獲取快取資訊、計算快取大小、清除快取等功能。詳情可看標頭檔案

錯誤回撥 

在下載視訊時,出現錯誤無法正常下載是比較容易出現的。我們自己實現了 AVAssetResourceLoaderDelegate 在第一次請求就丟擲錯誤的話,播放器會馬上提示錯誤狀態,而如果是已經響應了部分資料,再拋錯誤,AVAssetResourceLoader 會忽略錯誤而一直處於 loading,直到超時。這種情況就比較尷尬,所以 VIResourceLoaderManager 提供了 delegate,如果內部出現錯誤,就會丟擲錯誤,再又外部業務決定是如何處理。

注意:同一時間同一個 url 不能有多次下載: 由於快取內部實現是對每一個 url 都共用同一個下載配置檔案,如果同時有多次對同一個 url 進行下載,這個檔案下載資訊會被同時修改,下載資訊會變得混亂。 MediaCache 內部做了簡單的處理,如果正在下載某 url,這時再想嘗試下載同樣的 url 會直接丟擲錯誤,提示無法開始下載。

已知問題

播到一半聲音停了,視訊正常播

比較低概率,在美拍上測試時有短視訊會出現

弱網下一直loading到超時,但是檔案都是已經下載好了

沒有呼叫 AVPlayer 的 play 在弱網下會造成,AVPlayerLayer 一直無法達到 readyForDisplay 的情況

以上問題暫時沒有很好的解決方案,因為 ResourceLoader 的實現只能做到控制快取,但 AVPlayer 內的具體實現機制並不清楚,在快取沒有問題的情況下出現問題,很難去追根溯源尋找問題的根本原因。

吐槽

在實現 AVAssetResourceLoaderDelegate 的時候,文件非常少,幾乎只能一邊看標頭檔案中的文件一邊執行測試才能知道  AVAssetResourceLoaderDelegate 真正的執行機制。

另外最大的坑是 AVAssetResourceLoaderDelegate 的內部機制是個沙盒, 因為這個沙盒裡面做了很多視訊播放處理,導致遇到播放時出問題很難排查是什麼原因引起,只能不斷嘗試去找規律....

小結

回顧全文,理解 AVAssetResourceLoaderDelegate 的原理和實現機制,再到自己實現一個 Downloader,講了會遇到的幾個坑以及如何解決,最後簡單介紹了 MTMediaCache 如何使用。啊嘞,你都看完了,來來來快把 VIMediaCache 用起來

完整的專案地址:點選閱讀原文訪問

閱讀原文