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

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

轉(zhuǎn)自:【博客】

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

2017-03-31 Vito Vito的貓屋

不過场仲,我還真沒看到目前有哪個(gè)公開的實(shí)現(xiàn)方案有做的更好的逝变,可能是我孤陋寡聞鲁猩,如果你知道更好的方案陨倡,一定要留言告訴我,鞠躬..

進(jìn)入正題性含,這次的主要內(nèi)容

  • 理解 AVAssetResourceLoaderDelegate 的使用

  • 緩存下載的實(shí)現(xiàn)

  • VIMediaCache 提供了哪些 API

接下來會(huì)介紹通過使用 AVAssetResourceLoader稽物,在不改變 AVPlayer API 的情況下,對播放的音視頻進(jìn)行緩存鸦做。

前戲

現(xiàn)在市場上各種各樣的應(yīng)用励烦,充滿了多媒體信息,而聲音和視頻又是體積最大的文件泼诱,如果直接使用 URL 通過 AVPlayer 播放坛掠,系統(tǒng)并不會(huì)做緩存處理,等下次再播又要重新下載治筒,對網(wǎng)絡(luò)狀況差的用戶來說這就是災(zāi)難屉栓。若是下載好再播,同樣要等待全部下載完成矢炼,也是很痛苦系瓢。

我們最理想的緩存方案是:邊播放,邊緩存句灌。

我在早期加入美拍團(tuán)隊(duì)的時(shí)候夷陋,實(shí)際上已經(jīng)有了邊下邊播的功能,當(dāng)時(shí)選擇了使用 HTTPServer胰锌,在本地開啟一個(gè) http 服務(wù)器骗绕,把需要緩存的請求地址指向本地服務(wù)器,并帶上真正的 url 地址资昧。

早期的美拍都是不到 20s 的短視頻酬土,后面加長了視頻時(shí)間,但考慮到用戶設(shè)備容量問題格带,我們只對短視頻做視頻緩存撤缴。一直發(fā)展到現(xiàn)在,平臺(tái)上現(xiàn)在大多數(shù)的視頻都是長視頻叽唱,真正使用到緩存功能的頻率已經(jīng)很低屈呕。那么問題就來了,HTTPServer 不管我們有沒有使用緩存功能棺亭,都要在應(yīng)用打開的時(shí)候默默開啟虎眨,這真的是很浪費(fèi)了。并且我們引入 HTTPServer 庫也會(huì)增加一些包體積。

理解 AVAssetResourceLoaderDelegate 的使用

那么在一段尋覓之下嗽桩,發(fā)現(xiàn)了最適合做邊下邊播緩存的工具岳守。AVAssetResourceLoaderDelegate:一個(gè) iOS 6 就被開放出來,專門用來處理 AVAsset 加載的工具碌冶。

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

只要找一個(gè)對象實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate 這個(gè)協(xié)議的方法湿痢,丟給 asset,再把 asset 丟給 AVPlayer种樱,AVPlayer 在執(zhí)行播放的時(shí)候就會(huì)去問這個(gè) delegate:喂蒙袍,你能不能播放這個(gè) url 翱÷薄嫩挤?然后會(huì)觸發(fā)下面這個(gè)方法:

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

我們在這個(gè)方法中看看 request 里面的 url 是不是我們支持的,如果能支持就返回 YES消恍!然后就可以開心的一邊下視頻數(shù)據(jù)岂昭,一邊塞數(shù)據(jù)給 AVPlayer 讓它顯示視頻畫面。

先不管下載和緩存狠怨,實(shí)現(xiàn)上约啊,可以分為兩步:1. 需要知道如何請求數(shù)據(jù),url 是什么佣赖,下載多少數(shù)據(jù)恰矩。2. 下載好的數(shù)據(jù)怎么塞給 AVPlayer

1. 如何請求數(shù)據(jù)

在上面的回調(diào)方法中,會(huì)得到一個(gè) AVAssetResourceLoadingRequest 對象憎蛤,它里面的屬性和方法不多外傅,為了減少干擾,我精簡了一下這個(gè)類的頭文件俩檬,只留下我們會(huì)用到以及需要解釋的屬性和方法:

@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 是會(huì)觸發(fā)分片下載的策略棚辽,還需要從dataRequest 中得到請求范圍的信息技竟。有了請求地址和請求范圍,我們就可以重新創(chuàng)建一個(gè)設(shè)置了請求 Range 頭的 NSURLRequest 對象屈藐,讓下載器去下載這個(gè)文件的 Range 范圍內(nèi)的數(shù)據(jù)榔组。

2. 塞數(shù)據(jù)給 AVPlayer

當(dāng) AVPlayer 觸發(fā)下載時(shí),總是會(huì)先發(fā)起一個(gè) Range 為 0-2 的數(shù)據(jù)請求联逻,這個(gè)請求的作用其實(shí)是用來確認(rèn)視頻數(shù)據(jù)的信息搓扯,如文件類型、文件數(shù)據(jù)長度遣妥。當(dāng)下載器發(fā)起這個(gè)請求擅编,收到服務(wù)端返回的 response 后,我們要把視頻的信息填充到 AVAssetResourceLoadingRequestcontentInformationRequest 屬性中,告知下載的視頻格式以及視頻長度爱态。

AVAssetResourceLoadingRequest- (void)finishLoading 的時(shí)候谭贪,會(huì)根據(jù) contentInformationRequest 中的信息,去判斷接下去要怎么處理锦担。例如:下載 AVURLAsset 中 URL 指向的文件俭识,獲取到的文件的 contentType 是系統(tǒng)不支持的類型,這個(gè) AVURLAsset 將無法正常播放洞渔。

獲取完視頻信息后套媚,會(huì)收到剛才指定的 2 Byte 的 data 數(shù)據(jù),下載到的數(shù)據(jù)怎么辦磁椒? 可以塞給 AVAssetResourceLoadingRequest 里的 dataRequest 堤瘤。 dataRequest 里面用 - (void)respondWithData:(NSData *)data; 專門用來接收下載的數(shù)據(jù),這個(gè)方法可以調(diào)用多次浆熔,接收增量連續(xù)的 data 數(shù)據(jù)本辐。

當(dāng) AVAssetResourceLoadingRequest 要求的所有數(shù)據(jù)都下載完畢,調(diào)用 - (void)finishLoading 完成下載医增,AVAssetResourceLoader 會(huì)繼續(xù)發(fā)起之后的數(shù)據(jù)片段的請求慎皱。如果本次請求失敗,可以直接調(diào)用 - (void)finishLoadingWithError:(nullable NSError *)error; 結(jié)束下載叶骨。

流程圖

完整實(shí)現(xiàn)的主流程是這樣的

image.gif

重試機(jī)制

在實(shí)際的測試中茫多,發(fā)現(xiàn)AVAssetResourceLoader 在執(zhí)行加載的時(shí)候,會(huì)時(shí)不時(shí)的觸發(fā)取消下載調(diào)用 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest忽刽,然后重新發(fā)起加載請求的策略天揖。如果下載了部分,那么重新發(fā)起的下載請求會(huì)從還沒有下載的部分開始缔恳。

AVAssetResourceLoaderDelegate 中還有 3 個(gè)方法可以針對特殊場景做處理宝剖,不過在目前的環(huán)境中都用不到所以可以選擇不實(shí)現(xiàn)這些方法。

緩存下載的實(shí)現(xiàn)

我們已經(jīng)知道 AVAssetResourceLoaderDelegate 的實(shí)現(xiàn)機(jī)制歉甚,當(dāng) AVAsset 需要加載數(shù)據(jù)時(shí)會(huì)通過 delegate 告訴外部万细,外部接管整個(gè)視頻下載過程。

接管了視頻下載纸泄,便可以對視頻數(shù)據(jù)做任何事情赖钞。比如:緩存、記錄下載速度聘裁、獲得下載進(jìn)度等等雪营。

實(shí)現(xiàn)一個(gè)下載器,就是用 URLSession 開啟一個(gè) DataTask 請求數(shù)據(jù)衡便,把接收到的數(shù)據(jù)塞給 DataRequest 并寫入本地磁盤献起。在實(shí)現(xiàn)下載器時(shí)主要有三個(gè)注意的點(diǎn):1. Range 請求 2. 可取消下載 3. 分片緩存

1. Range 請求

每次得到的 LoadingRequest 帶有請求數(shù)據(jù)范圍的信息洋访,比如期望請求第 100 字節(jié)到 500 字節(jié),在創(chuàng)建 URLRequest 時(shí)需要設(shè)置 HTTPHeader 的 Range 值谴餐。

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

2. 可取消下載

AVAsset 在加載視頻時(shí)姻政,經(jīng)常會(huì)在某次數(shù)據(jù)請求還沒有完成時(shí)觸發(fā)取消下載,然后發(fā)起一個(gè)新的 LoadingReqeust岂嗓。這個(gè)機(jī)制是 AVAsset 里的黑盒汁展,具體邏輯無法得知,比較像是 AVAsset 的一種重試機(jī)制厌殉。 作為下載器食绿,在收到取消通知時(shí),需要立刻停止下載公罕。由于 DataRequest 的 cancel 操作是異步的器紧,就有可能在 cancel 還未完成時(shí),下一個(gè) LoadingRequest 就已經(jīng)到來熏兄,所以還需要需要保證同一個(gè) URL 只能同時(shí)存在一個(gè)下載器在下載品洛,否則會(huì)出現(xiàn)數(shù)據(jù)混亂的問題树姨。

3. 分片緩存

如果只是單純的下載視頻摩桶,數(shù)據(jù)單調(diào)遞增,緩存處理還是比較容易帽揪。然而現(xiàn)實(shí)是用戶對 player 的 seek 操作給視頻的緩存管理帶來了巨大的挑戰(zhàn)硝清,一旦涉及到用戶操作,可能性就越多转晰,復(fù)雜度也會(huì)越高芦拿。

沒有 seek 的情況:網(wǎng)速正常時(shí)緩存數(shù)據(jù)比播放時(shí)間走得開,正常播放查邢;網(wǎng)速慢時(shí)蔗崎,播放器 loading,直到有足夠的數(shù)據(jù)量進(jìn)行播放扰藕,如果網(wǎng)速一直很慢就會(huì)播幾秒卡一下缓苛。

當(dāng)加入 seek 后會(huì)有三種可能:

  • 視頻完全下載好,這時(shí) seek 只需讀取相應(yīng)緩存
image
  • 視頻下載一半邓深,用戶 seek 到未下載部分未桥,LoadingRequest 請求的部分全部都是未下載的數(shù)據(jù)。這時(shí)需要取消正在下載的數(shù)據(jù)芥备,然后從 seek 的點(diǎn)開始下載數(shù)據(jù)冬耿。為了支持 seek 操作,下載器就需要支持分片緩存萌壳。目前使用的解決方案是下載的視頻數(shù)據(jù)會(huì)根據(jù)請求的 Range 值亦镶,把數(shù)據(jù)存儲(chǔ)到文件中對應(yīng)的偏移值位置日月,并且每個(gè)視頻文件都會(huì)另外再保存一個(gè)與之對應(yīng)的下載信息文件。這個(gè)信息文件會(huì)記錄當(dāng)前下載了多少數(shù)據(jù)缤骨,總共有多少數(shù)據(jù)山孔,下載了哪些片段的數(shù)據(jù)等信息,之后的緩存管理會(huì)非常依賴這個(gè)配置文件荷憋。
image
  • 視頻被 seek 了多次台颠,用戶 seek 到一個(gè)時(shí)間點(diǎn),LoadingRequest 請求的部分包含了已下載和未下載的部分勒庄。
image

這種情況是最復(fù)雜的串前!簡單的做法是,當(dāng)成上面的情況來處理实蔽,全部都重新下載荡碾,雖然邏輯簡單,但這個(gè)方案會(huì)下載多次同樣的數(shù)據(jù)局装,不是最最優(yōu)解坛吁。

我的目標(biāo)當(dāng)然是做最優(yōu)的解決方案,但也是復(fù)雜高很多的解決方案铐尚。

在收到 LoadingRequest 的請求范圍后拨脉,下載器會(huì)先獲取已經(jīng)下載的數(shù)據(jù)信息,把已下載的分片信息分別創(chuàng)建一個(gè) action宣增,再把需要遠(yuǎn)程下載的分片數(shù)據(jù)分別創(chuàng)建一個(gè) action玫膀。最終組合就可能是 LocalAction(50-100 bytes) + RemoteAction(101-200 bytes) + LocalAction(201-300 bytes) + RemoteAction(300-400 bytes)。每一個(gè) action 會(huì)按順序獲取數(shù)據(jù)再返回給 LoadingRequest爹脾。

image

VIMediaCache 提供了哪些 API

基本使用

VIMediaCache 主要提供了 VIResourceLoaderManager帖旨,這個(gè)類實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate,并且提供了初始化一個(gè) AVPlayerItem 的方法灵妨,平時(shí)使用時(shí)解阅,只需用 VIResourceLoaderManager 創(chuàng)建一個(gè) AVPlayerItemAVPlayer 再用這個(gè) playerItem 初始化泌霍,AVPlayer 在播放的時(shí)候就會(huì)自動(dòng)緩存了货抄。

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

緩存管理

所有緩存相關(guān)的信息都在 VICacheManager 類中。目前提供了下載進(jìn)度通知烹吵、修改緩存目錄碉熄、根據(jù) url 獲取緩存地址、根據(jù) url 獲取緩存信息肋拔、計(jì)算緩存大小锈津、清除緩存等功能。詳情可看頭文件

錯(cuò)誤回調(diào)

在下載視頻時(shí)凉蜂,出現(xiàn)錯(cuò)誤無法正常下載是比較容易出現(xiàn)的琼梆。我們自己實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate 在第一次請求就拋出錯(cuò)誤的話性誉,播放器會(huì)馬上提示錯(cuò)誤狀態(tài),而如果是已經(jīng)響應(yīng)了部分?jǐn)?shù)據(jù)茎杂,再拋錯(cuò)誤错览,AVAssetResourceLoader 會(huì)忽略錯(cuò)誤而一直處于 loading,直到超時(shí)煌往。這種情況就比較尷尬倾哺,所以 VIResourceLoaderManager 提供了 delegate,如果內(nèi)部出現(xiàn)錯(cuò)誤刽脖,就會(huì)拋出錯(cuò)誤羞海,再又外部業(yè)務(wù)決定是如何處理。

注意:同一時(shí)間同一個(gè) url 不能有多次下載: 由于緩存內(nèi)部實(shí)現(xiàn)是對每一個(gè) url 都共用同一個(gè)下載配置文件曲管,如果同時(shí)有多次對同一個(gè) url 進(jìn)行下載却邓,這個(gè)文件下載信息會(huì)被同時(shí)修改,下載信息會(huì)變得混亂院水。 MediaCache 內(nèi)部做了簡單的處理腊徙,如果正在下載某 url,這時(shí)再想嘗試下載同樣的 url 會(huì)直接拋出錯(cuò)誤檬某,提示無法開始下載撬腾。

已知問題

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

比較低概率橙喘,在美拍上測試時(shí)有短視頻會(huì)出現(xiàn)

弱網(wǎng)下一直loading到超時(shí)时鸵,但是文件都是已經(jīng)下載好了

沒有調(diào)用 AVPlayer 的 play 在弱網(wǎng)下會(huì)造成,AVPlayerLayer 一直無法達(dá)到 readyForDisplay 的情況

以上問題暫時(shí)沒有很好的解決方案厅瞎,因?yàn)?ResourceLoader 的實(shí)現(xiàn)只能做到控制緩存,但 AVPlayer 內(nèi)的具體實(shí)現(xiàn)機(jī)制并不清楚初坠,在緩存沒有問題的情況下出現(xiàn)問題和簸,很難去追根溯源尋找問題的根本原因。

吐槽

在實(shí)現(xiàn) AVAssetResourceLoaderDelegate 的時(shí)候碟刺,文檔非常少锁保,幾乎只能一邊看頭文件中的文檔一邊運(yùn)行測試才能知道 AVAssetResourceLoaderDelegate 真正的運(yùn)行機(jī)制。

另外最大的坑是 AVAssetResourceLoaderDelegate 的內(nèi)部機(jī)制是個(gè)沙盒半沽, 因?yàn)檫@個(gè)沙盒里面做了很多視頻播放處理爽柒,導(dǎo)致遇到播放時(shí)出問題很難排查是什么原因引起,只能不斷嘗試去找規(guī)律....

小結(jié)

回顧全文者填,理解 AVAssetResourceLoaderDelegate 的原理和實(shí)現(xiàn)機(jī)制浩村,再到自己實(shí)現(xiàn)一個(gè) Downloader,講了會(huì)遇到的幾個(gè)坑以及如何解決占哟,最后簡單介紹了 MTMediaCache 如何使用心墅。啊嘞酿矢,你都看完了,來來來快把 VIMediaCache 用起來

可能是目前最好的 AVPlayer 音視頻緩存方案-VIMediaCache-master.zip

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怎燥,一起剝皮案震驚了整個(gè)濱河市瘫筐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铐姚,老刑警劉巖策肝,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異隐绵,居然都是意外死亡驳糯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門氢橙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酝枢,“玉大人,你說我怎么就攤上這事悍手×蹦溃” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵坦康,是天一觀的道長竣付。 經(jīng)常有香客問我,道長滞欠,這世上最難降的妖魔是什么古胆? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮筛璧,結(jié)果婚禮上逸绎,老公的妹妹穿的比我還像新娘。我一直安慰自己夭谤,他們只是感情好棺牧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著朗儒,像睡著了一般颊乘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上醉锄,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天乏悄,我揣著相機(jī)與錄音,去河邊找鬼恳不。 笑死檩小,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妆够。 我是一名探鬼主播识啦,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼负蚊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了颓哮?” 一聲冷哼從身側(cè)響起家妆,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎冕茅,沒想到半個(gè)月后伤极,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姨伤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年哨坪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乍楚。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡当编,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徒溪,到底是詐尸還是另有隱情忿偷,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布臊泌,位于F島的核電站鲤桥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏渠概。R本人自食惡果不足惜茶凳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望播揪。 院中可真熱鬧贮喧,春花似錦、人聲如沸剪芍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罪裹。三九已至,卻和暖如春运挫,著一層夾襖步出監(jiān)牢的瞬間状共,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工谁帕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留峡继,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓匈挖,卻偏偏與公主長得像碾牌,于是被迫代替她去往敵國和親康愤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容