可能是目前最好的 AVPlayer 音視頻緩存方案
阿勒,沒想到第一篇文章我就標題黨了...
不過,我還真沒看到目前有哪個公開的實現方案有做的更好的,可能是我孤陋寡聞,如果你知道更好的方案,一定要留言告訴我,鞠躬..
進入正題,這次的主要內容
-
理解 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
用起來
可能是目前最好的 AVPlayer 音視頻緩存方案-VIMediaCache-master.zip