可能是目前最好的 AVPlayer 音視頻緩存方案 AVAssetResourceLoaderDelegate

可能是目前最好的 AVPlayer 音視頻緩存方案

轉自:【博客】

可下載:緩存視頻播放demo地址

2017-03-31 Vito Vito的貓屋

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

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

  • 理解 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 后,我們要把視頻的信息填充到 AVAssetResourceLoadingRequestcontentInformationRequest 屬性中,告知下載的視頻格式以及視頻長度。

AVAssetResourceLoadingRequest- (void)finishLoading 的時候,會根據 contentInformationRequest 中的信息,去判斷接下去要怎么處理。例如:下載 AVURLAsset 中 URL 指向的文件,獲取到的文件的 contentType 是系統不支持的類型,這個 AVURLAsset 將無法正常播放。

獲取完視頻信息后,會收到剛才指定的 2 Byte 的 data 數據,下載到的數據怎么辦? 可以塞給 AVAssetResourceLoadingRequest 里的 dataRequestdataRequest 里面用 - (void)respondWithData:(NSData *)data; 專門用來接收下載的數據,這個方法可以調用多次,接收增量連續的 data 數據。

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

流程圖

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

image.gif

重試機制

在實際的測試中,發現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 只需讀取相應緩存
image
  • 視頻下載一半,用戶 seek 到未下載部分,LoadingRequest 請求的部分全部都是未下載的數據。這時需要取消正在下載的數據,然后從 seek 的點開始下載數據。為了支持 seek 操作,下載器就需要支持分片緩存。目前使用的解決方案是下載的視頻數據會根據請求的 Range 值,把數據存儲到文件中對應的偏移值位置,并且每個視頻文件都會另外再保存一個與之對應的下載信息文件。這個信息文件會記錄當前下載了多少數據,總共有多少數據,下載了哪些片段的數據等信息,之后的緩存管理會非常依賴這個配置文件。
image
  • 視頻被 seek 了多次,用戶 seek 到一個時間點,LoadingRequest 請求的部分包含了已下載和未下載的部分。
image

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

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

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

image

VIMediaCache 提供了哪些 API

基本使用

VIMediaCache 主要提供了 VIResourceLoaderManager,這個類實現了 AVAssetResourceLoaderDelegate,并且提供了初始化一個 AVPlayerItem 的方法,平時使用時,只需用 VIResourceLoaderManager 創建一個 AVPlayerItemAVPlayer 再用這個 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內容