背景
在有多個(gè)視頻鏈接需要連續(xù)切換播放時(shí)巡揍,視頻播放之前要等待視頻資源加載完成,切換視頻時(shí)需要等待很久菌瘪,已經(jīng)播放過的視頻也需要重新加載才能再次播放腮敌,影響用戶體驗(yàn)阱当。
優(yōu)化點(diǎn):
- 邊下邊播:視頻播放時(shí),不受網(wǎng)絡(luò)狀況限制糜工,播放流暢
- 緩存:已經(jīng)播放過的視頻弊添,將視頻資源緩存在本地,再次播放時(shí)直接讀取緩存
- 預(yù)加載:切換視頻時(shí)捌木,無縫銜接油坝,視頻秒播
實(shí)現(xiàn)方案
本地代理服務(wù)器
在iOS本地開啟Local Server服務(wù),然后使用播放控件請(qǐng)求本地Local Server服務(wù)刨裆,本地的服務(wù)再不斷請(qǐng)求視頻地址獲取視頻流澈圈,本地服務(wù)請(qǐng)求的過程中把視頻緩存到本地。
唱吧開源庫:KTVHTTPCache
使用AVAssetResourceLoader回調(diào)下載
AVAssetResourceLoader通過提供的委托對(duì)象去調(diào)節(jié)AVURLAsset所需要的加載資源帆啃,同時(shí)可以進(jìn)行數(shù)據(jù)的緩存和讀取操作瞬女。大致流程如圖:
具體實(shí)現(xiàn)
1.給AVURLAsset設(shè)置資源加載代理
AVPlayer在執(zhí)行播放的時(shí)候,就回去問這個(gè)delegate努潘,是能能夠播放這個(gè)asset诽偷。于是就可以進(jìn)行自定義的一些操作
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:assetURL options:nil];
//設(shè)置代理
[asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
2.資源下載及數(shù)據(jù)填充
找一個(gè)對(duì)象實(shí)現(xiàn) AVAssetResourceLoaderDelegate 這個(gè)協(xié)議的方法
//在加載URLAsset資源時(shí)回調(diào)
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
在加載資源的代理方法中看看 request 里面的 url 是不是我們支持的,如果能支持就返回 YES疯坤!然后就可以一邊下視頻數(shù)據(jù)报慕,一邊塞數(shù)據(jù)給 AVPlayer 讓它顯示視頻畫面。數(shù)據(jù)交互流程圖如下:
下載視頻數(shù)據(jù)
在上面的回調(diào)方法中压怠,得到了一個(gè)AVAssetResourceLoadingRequest對(duì)象眠冈,它的主要屬性和方法:
@interface AVAssetResourceLoadingRequest : NSObject
@property (nonatomic, readonly) NSURLRequest *request;
@property (nonatomic, readonly, nullable) AVAssetResourceLoadingContentInformationRequest *contentInformationRequest;
@property (nonatomic, readonly, nullable) AVAssetResourceLoadingDataRequest *dataRequest ;
- (void)finishLoading;
- (void)finishLoadingWithError:(nullable NSError *)error;
@end
在 AVAssetResourceLoadingRequest 里面,request 代表原始的請(qǐng)求刑峡。dataRequest是數(shù)據(jù)請(qǐng)求洋闽,包含數(shù)據(jù)起始偏移量,數(shù)據(jù)長(zhǎng)度等信息突梦。
AVPlayer 是會(huì)觸發(fā)分片下載的策略诫舅,需要從dataRequest 中得到請(qǐng)求范圍的信息。
有了請(qǐng)求地址和請(qǐng)求范圍宫患,我們就可以重新創(chuàng)建一個(gè)設(shè)置了請(qǐng)求 Range 頭的 NSURLRequest 對(duì)象刊懈,讓下載器去下載這個(gè)文件的 Range 范圍內(nèi)的數(shù)據(jù)。
塞數(shù)據(jù)給AVPLayer
當(dāng) AVPlayer 觸發(fā)下載時(shí)娃闲,總是會(huì)先發(fā)起一個(gè) Range 為 0-2 的數(shù)據(jù)請(qǐng)求虚汛,這個(gè)請(qǐng)求的作用其實(shí)是用來確認(rèn)視頻數(shù)據(jù)的信息,如文件類型皇帮、文件數(shù)據(jù)長(zhǎng)度卷哩。當(dāng)下載器發(fā)起這個(gè)請(qǐng)求,收到服務(wù)端返回的 response 后属拾,我們要把視頻的信息填充到 AVAssetResourceLoadingRequest 的 contentInformationRequest 屬性中将谊,告知下載的視頻格式以及視頻長(zhǎng)度冷溶。
獲取完視頻信息后,AVAssetResourceLoader 會(huì)繼續(xù)發(fā)起之后的數(shù)據(jù)片段的請(qǐng)求尊浓,下載到的數(shù)據(jù)就可以塞給 AVAssetResourceLoadingRequest 里的 dataRequest 逞频。 dataRequest 調(diào)動(dòng)下面的方法接收下載的數(shù)據(jù),這個(gè)方法可以調(diào)用多次栋齿,接收增量連續(xù)的 data 數(shù)據(jù)苗胀。與此同時(shí)對(duì)下載數(shù)據(jù)進(jìn)行本地緩存。
- (void)respondWithData:(NSData *)data;
當(dāng) AVAssetResourceLoadingRequest 要求的所有數(shù)據(jù)都下載完畢瓦堵,調(diào)用 - (void)finishLoading 完成下載基协。如果本次請(qǐng)求失敗,可以直接調(diào)用 - (void)finishLoadingWithError:(NSError *)error; 結(jié)束下載谷丸。
AVAssetResourceLoadingRequest 在 - (void)finishLoading 的時(shí)候堡掏,會(huì)根據(jù) contentInformationRequest 中的信息,去判斷接下去要怎么處理刨疼。例如:下載 AVURLAsset 中 URL 指向的文件,獲取到的文件的 contentType 是系統(tǒng)不支持的類型鹅龄,這個(gè) AVURLAsset 將無法正常播放揩慕。
下載重試
//在取消加載資源后回調(diào)
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
AVAssetResourceLoader 在執(zhí)行加載的時(shí)候,會(huì)時(shí)不時(shí)的觸發(fā)取消下載扮休,在這個(gè)回調(diào)里面迎卤,需要取消當(dāng)前正在進(jìn)行中的下載任務(wù)。然后重新發(fā)起加載請(qǐng)求的策略玷坠。如果下載了部分蜗搔,那么重新發(fā)起的下載請(qǐng)求會(huì)從還沒有下載的部分開始肘交。
3.緩存
根據(jù)上面的 AVAssetResourceLoaderDelegate 的實(shí)現(xiàn)機(jī)制抢腐,當(dāng) AVAsset 需要加載數(shù)據(jù)時(shí)會(huì)通過 delegate 告訴外部,外部接管整個(gè)視頻下載過程薄坏。
當(dāng)我們接管了視頻下載兄渺,便可以對(duì)視頻數(shù)據(jù)做任何事情缝龄。比如:緩存、記錄下載速度挂谍、獲得下載進(jìn)度等等叔壤。
實(shí)現(xiàn)一個(gè)下載器,用 URLSession 開啟一個(gè) DataTask 請(qǐng)求數(shù)據(jù)口叙,把接收到的數(shù)據(jù)塞給 DataRequest 并寫入本地磁盤炼绘。
分片下載
在每次的loadingRequest中,都包含著本次加載請(qǐng)求的dataRequest妄田,他是一個(gè)AVAssetResourceLoadingDataRequest對(duì)象俺亮,看下他的屬性:
@interface AVAssetResourceLoadingDataRequest : NSObject
@property (nonatomic, readonly) long long requestedOffset;
@property (nonatomic, readonly) NSInteger requestedLength;
- (void)respondWithData:(NSData *)data;
@end
根據(jù)dataRequest中的信息驮捍,在創(chuàng)建下載數(shù)據(jù)的 URLRequest 時(shí)需要設(shè)置 HTTPHeader 的 Range 值
NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, (fromOffset + length -1)];
[request setValue:range forHTTPHeaderField:@"Range"];
取消下載
AVAsset 在加載視頻時(shí),經(jīng)常會(huì)在某次數(shù)據(jù)請(qǐng)求還沒有完成時(shí)觸發(fā)取消下載铅辞,然后發(fā)起一個(gè)新的 LoadingReqeust厌漂。所以在接到取消下載的代理回調(diào)時(shí),需要立刻停止當(dāng)前正在進(jìn)行中的下載斟珊。由于 DataRequest 的 cancel 操作是異步的苇倡,就有可能在 cancel 還未完成時(shí),下一個(gè) LoadingRequest 就已經(jīng)到來囤踩,所以還需要需要保證同一個(gè) URL 同時(shí)只存在一個(gè)下載器在下載旨椒,否則會(huì)出現(xiàn)數(shù)據(jù)混亂的問題。
分片緩存
由于AVAsset請(qǐng)求資源數(shù)據(jù)的時(shí)候堵漱,不是完整的視頻數(shù)據(jù)综慎,但是為了方便數(shù)據(jù)管理和魂村讀取,對(duì)于同一個(gè)視頻URL的數(shù)據(jù)我們應(yīng)該緩存到同一個(gè)文件中勤庐,根據(jù)range將下載到的數(shù)據(jù)拼接完整即可示惊。
對(duì)于更復(fù)雜的場(chǎng)景,比如用戶seek操作愉镰,還要處理播放進(jìn)度和已緩存數(shù)據(jù)以及還未緩存的遠(yuǎn)程數(shù)據(jù)之間的協(xié)調(diào)米罚。(我們的業(yè)務(wù)暫時(shí)不涉及到此場(chǎng)景,具體的處理方案可參考:VIMediaCache文檔)
4.預(yù)加載
在當(dāng)前視頻播放時(shí)丈探,開啟下載任務(wù)录择,提前將后面的視頻資源下載并緩存到本地。需要切換視頻時(shí)碗降,根據(jù)loadingRequest的url判斷本地是否已經(jīng)緩存了這個(gè)視頻的數(shù)據(jù)隘竭,根據(jù)range從本地讀取數(shù)據(jù)填充到dataRequest中。如果本地沒有緩存讼渊,從上面第2步动看,走邊下邊播邏輯。
不足與展望
現(xiàn)在的預(yù)加載處理方式是精偿,提前下載后續(xù)幾條視頻完整的視頻數(shù)據(jù)弧圆,因此預(yù)加載的任務(wù)量大,耗時(shí)長(zhǎng)笔咽。切換視頻時(shí)搔预,可能預(yù)加載的任務(wù)還沒有完成就被提前終止,然后又開始新的預(yù)加載叶组。
最好的處理方式是拯田,預(yù)加載的視頻,只下載開頭的一部分?jǐn)?shù)據(jù)緩存甩十,到播放這條視頻的時(shí)候再邊下邊播剩余的數(shù)據(jù)船庇。這里就涉及到這樣一個(gè)場(chǎng)景吭产,如下圖示:
對(duì)于這次的loadingRequest,我們需要從本地緩存中讀取一段數(shù)據(jù)鸭轮,再從遠(yuǎn)端下載一部分?jǐn)?shù)據(jù)臣淤,最后將兩部分?jǐn)?shù)據(jù)合并填充給dataRequest。