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。