iOS 视频边下边播(缓存,预加载)


背景

在有多个视频链接需要连续切换播放时,视频播放之前要等待视频资源加载完成,切换视频时需要等待很久,已经播放过的视频也需要重新加载才能再次播放,影响用户体验。

优化点:

  • 边下边播:视频播放时,不受网络状况限制,播放流畅
  • 缓存:已经播放过的视频,将视频资源缓存在本地,再次播放时直接读取缓存
  • 预加载:切换视频时,无缝衔接,视频秒播

实现方案

本地代理服务器

在iOS本地开启Local Server服务,然后使用播放控件请求本地Local Server服务,本地的服务再不断请求视频地址获取视频流,本地服务请求的过程中把视频缓存到本地。

唱吧开源库:KTVHTTPCache

使用AVAssetResourceLoader回调下载

AVAssetResourceLoader通过提供的委托对象去调节AVURLAsset所需要的加载资源,同时可以进行数据的缓存和读取操作。大致流程如图:


image.png

具体实现

1.给AVURLAsset设置资源加载代理

AVPlayer在执行播放的时候,就回去问这个delegate,是能能够播放这个asset。于是就可以进行自定义的一些操作

AVURLAsset *asset = [AVURLAsset URLAssetWithURL:assetURL options:nil];
//设置代理
[asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];

2.资源下载及数据填充

找一个对象实现 AVAssetResourceLoaderDelegate 这个协议的方法

//在加载URLAsset资源时回调
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;

在加载资源的代理方法中看看 request 里面的 url 是不是我们支持的,如果能支持就返回 YES!然后就可以一边下视频数据,一边塞数据给 AVPlayer 让它显示视频画面。数据交互流程图如下:


image.png
下载视频数据

在上面的回调方法中,得到了一个AVAssetResourceLoadingRequest对象,它的主要属性和方法:

@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 代表原始的请求。dataRequest是数据请求,包含数据起始偏移量,数据长度等信息。
AVPlayer 是会触发分片下载的策略,需要从dataRequest 中得到请求范围的信息。
有了请求地址和请求范围,我们就可以重新创建一个设置了请求 Range 头的 NSURLRequest 对象,让下载器去下载这个文件的 Range 范围内的数据。

塞数据给AVPLayer

当 AVPlayer 触发下载时,总是会先发起一个 Range 为 0-2 的数据请求,这个请求的作用其实是用来确认视频数据的信息,如文件类型、文件数据长度。当下载器发起这个请求,收到服务端返回的 response 后,我们要把视频的信息填充到 AVAssetResourceLoadingRequest 的 contentInformationRequest 属性中,告知下载的视频格式以及视频长度。

获取完视频信息后,AVAssetResourceLoader 会继续发起之后的数据片段的请求,下载到的数据就可以塞给 AVAssetResourceLoadingRequest 里的 dataRequest 。 dataRequest 调动下面的方法接收下载的数据,这个方法可以调用多次,接收增量连续的 data 数据。与此同时对下载数据进行本地缓存。

 - (void)respondWithData:(NSData *)data;

当 AVAssetResourceLoadingRequest 要求的所有数据都下载完毕,调用 - (void)finishLoading 完成下载。如果本次请求失败,可以直接调用 - (void)finishLoadingWithError:(NSError *)error; 结束下载。

AVAssetResourceLoadingRequest 在 - (void)finishLoading 的时候,会根据 contentInformationRequest 中的信息,去判断接下去要怎么处理。例如:下载 AVURLAsset 中 URL 指向的文件,获取到的文件的 contentType 是系统不支持的类型,这个 AVURLAsset 将无法正常播放。

下载重试
//在取消加载资源后回调
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;

AVAssetResourceLoader 在执行加载的时候,会时不时的触发取消下载,在这个回调里面,需要取消当前正在进行中的下载任务。然后重新发起加载请求的策略。如果下载了部分,那么重新发起的下载请求会从还没有下载的部分开始。

3.缓存

根据上面的 AVAssetResourceLoaderDelegate 的实现机制,当 AVAsset 需要加载数据时会通过 delegate 告诉外部,外部接管整个视频下载过程。
当我们接管了视频下载,便可以对视频数据做任何事情。比如:缓存、记录下载速度、获得下载进度等等。

实现一个下载器,用 URLSession 开启一个 DataTask 请求数据,把接收到的数据塞给 DataRequest 并写入本地磁盘。

分片下载

在每次的loadingRequest中,都包含着本次加载请求的dataRequest,他是一个AVAssetResourceLoadingDataRequest对象,看下他的属性:

@interface AVAssetResourceLoadingDataRequest : NSObject

@property (nonatomic, readonly) long long requestedOffset;
@property (nonatomic, readonly) NSInteger requestedLength;

- (void)respondWithData:(NSData *)data;

@end

根据dataRequest中的信息,在创建下载数据的 URLRequest 时需要设置 HTTPHeader 的 Range 值

NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, (fromOffset + length -1)];
[request setValue:range forHTTPHeaderField:@"Range"];
取消下载

AVAsset 在加载视频时,经常会在某次数据请求还没有完成时触发取消下载,然后发起一个新的 LoadingReqeust。所以在接到取消下载的代理回调时,需要立刻停止当前正在进行中的下载。由于 DataRequest 的 cancel 操作是异步的,就有可能在 cancel 还未完成时,下一个 LoadingRequest 就已经到来,所以还需要需要保证同一个 URL 同时只存在一个下载器在下载,否则会出现数据混乱的问题。

分片缓存

由于AVAsset请求资源数据的时候,不是完整的视频数据,但是为了方便数据管理和魂村读取,对于同一个视频URL的数据我们应该缓存到同一个文件中,根据range将下载到的数据拼接完整即可。
对于更复杂的场景,比如用户seek操作,还要处理播放进度和已缓存数据以及还未缓存的远程数据之间的协调。(我们的业务暂时不涉及到此场景,具体的处理方案可参考:VIMediaCache文档

4.预加载

在当前视频播放时,开启下载任务,提前将后面的视频资源下载并缓存到本地。需要切换视频时,根据loadingRequest的url判断本地是否已经缓存了这个视频的数据,根据range从本地读取数据填充到dataRequest中。如果本地没有缓存,从上面第2步,走边下边播逻辑。

不足与展望

现在的预加载处理方式是,提前下载后续几条视频完整的视频数据,因此预加载的任务量大,耗时长。切换视频时,可能预加载的任务还没有完成就被提前终止,然后又开始新的预加载。
最好的处理方式是,预加载的视频,只下载开头的一部分数据缓存,到播放这条视频的时候再边下边播剩余的数据。这里就涉及到这样一个场景,如下图示:


image.png

对于这次的loadingRequest,我们需要从本地缓存中读取一段数据,再从远端下载一部分数据,最后将两部分数据合并填充给dataRequest。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349