iOS AVPlayer 视频缓存的设计与实现
概述
最近一直在研究 iOS 平台的视频缓存设计方案。目标是实现视频边播边下载。后续再次播放时,则读取本地缓存数据,从而节省用户流量,提升用户体验。
基于 AVPlayer
的视频缓存方案 BCQMediaCache。 【源码传送门】
基本原理
下图所示为 iOS AVPlayer 视频缓存的原理示意图。左边为原始的 AVPlayer
在播放时视频时资源的请求过程。右边为实现视频缓存的方案,其中的关键是为
AVAssetResourceLoader
设置代理,并实现
AVAssetResourceLoaderDelegate
协议所声明的两个方法。通过在这两个方法中捕获所有的
AVAssetResourceLoadingRequest
请求,并为所有的原始请求创建对应的自定义网络请求。使用自定义网络请求向远端多媒体服务器请求资源,当数据返回时,将数据返回给原始请求,并在本地进行数据缓存。
技术细节
AVAssetResourceLoaderDelegate
首先,我们需要为 AVAssetResourceLoader
设置代理。
1
2let urlAsset = AVURLAsset(url: xxx, options: nil)
urlAsset.resourceLoader.setDelegate(self, queue: DispatchQueue.main)AVAssetResourceLoaderDelegate
协议时,URL 必须是自定义的 URLScheme。我们需要把原始 URL 的
http://
或 https://
替换成
xxx://
,协议方法才会生效。
然后,我们需要实现 AVAssetResourceLoaderDelegate
所声明的相关方法。对于视频缓存功能,我们仅需要实现以下两个方法即可。
1
2
3
4
5func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool;
func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
didCancel loadingRequest: AVAssetResourceLoadingRequest)
resourceLoader(_:shouldWaitForLoadingOfRequestedResource:)
方法表示代理类是否可以处理该请求。我们通过在这个方法中捕获每个原始请求,并创建对应的自定义网络请求。
resourceLoader(_:didCancel:)
方法表示
AVAssetResourceLoader
主动放弃了某个原始请求。对此,我们需要将原始请求删除,并取消对应的自定义网络请求。
自定义网络请求的创建
在上述
resourceLoader(_:shouldWaitForLoadingOfRequestedResource:)
代理方法中,我们能够捕获到一个原始请求,即一个
AVAssetResourceLoadingRequest
对象。如下所示,为
AVAssetResourceLoadingRequest
中的一些重要的属性和方法。
1 | open class AVAssetResourceLoadingRequest : NSObject { |
其中,request
代表原始请求,由于 AVPlayer
会触发分片下载的策略,request
请求会从
dataRequest
中获取请求的分片范围。因此,根据请求地址和请求分片,我们就可以创建自定义的网络请求。请求分片需要在
HTTP Header 中进行设置。
自定义网络请求的响应
下图所示为视频播放时的一次网络请求的时序图。
我们根据 dataRequest
中的分片信息,创建并发起自定义网络请求。当远端的服务器响应该请求后,客户端会经历一下三个步骤,并调用相应的代理方法。
- 处理响应
- 处理数据(多次)
- 请求结束
1 | func urlSession(_ session: URLSession, |
处理响应
请求响应时,我们从响应头部中获取资源相关信息,如:
ContentType
表示文件类型Content-Range
包含文件长度信息Accept-Ranges
包含是否支持分片请求
我们需要把视频的信息填充到 AVAssetResourceLoadingRequest
的 contentInformationRequest
中,从而通知
AVAssetResourceLoader
要下载视频的视频格式、视频长度等。
处理数据
当请求的分片范围较大时,客户端分多次顺序调用数据处理代理方法。我们可以在此时对接收到的数据进行缓存。当然,还要将数据返回给
dataRequest
,可以通过调用 respond(with:)
方法将数据返回给 dataRequest
。
请求结束
当数据传输完毕后,我们需要手动调用 finishLoading()
方法通知 AVAssetResourceLoader
数据下载完毕。如果请求失败,我们也需要手动调用
finishLoading(with:)
方法告诉
AVAssetResourceLoader
数据下载失败。
重试机制
当一个网络请求未完成时,我们拖动视频的进度条,AVAssetResourceLoader
会自动取消前一次的网络请求,从而发起一个新的网络请求。
在上述 resourceLoader(_:didCancel:)
代理方法中,我们可以取消某一次下载请求。
分片下载
一般情况下,视频播放支持进度拖拽的功能,即 seek
功能。因此,网络请求的分片与本地的分片数据可能存在如下关系:
- 本地缺失分片数据
- 本地包含完整分片数据
- 本地包含部分分片数据
通过定义一个类来表示这两种分片信息。我们对请求的分片进行检查和拆分,并按顺序进行处理。如果本地已缓存,则直接返回本地分片数据;如果本地未缓存,则创建自定义网络请求,请求分片数据。
1
2
3
4
5
6
7
8
9enum BCQResourceFragmentType {
case local // 已缓存本地
case remote // 未缓存本地
}
final class BCQResourceFragment {
let type: BCQResourceFragmentType // 数据分片类型
let range: SVRange // 数据分片范围
}
设计实现
如图所示为 BCQMediaCache 的类图。
BCQMediaCache 使用四个类将其核心功能分为四层:
BCQResourceLoaderManager
BCQResourceLoader
BCQResourceFragmentDownloader
BCQResourceFragmentRequest
BCQResourceLoaderManager
作为
AVAssetResourceLoader
的代理,实现了
AVAssetResourceLoaderDelegate
协议的两个方法。通过这两个方法实现对原始请求
AVAssetResourceLoadingRequest
的管理,包括:保存、取消。BCQResourceLoaderManager
还可以管理多个 URL,针对不同的 URL,它将创建对应的
BCQResourceLoader
。具体的资源下载任务则由
BCQResourceLoader
及以下分层来完成。
BCQResourceLoader
管理单个 URL
的资源下载。对于单个
URL,同一时刻可能存在多个网络请求,为此,BCQResourceLoader
维护一个网络请求的列表。
BCQResourceFragmentDownloader
内部包含两个属性:originRequest
和
customRequest
,分别表示原始网络请求和自定义网络请求。BCQResourceFragmentDownloader
将两者进行了绑定,负责处理两者之间的交互,如:
- 根据本地保存的分片信息,对
originRequest
的请求分片进行详细拆分,得到BCQResourceFragment
数组 - 使用
BCQResourceFragment
数组创建并启动customRequest
- 根据自定义请求的响应信息配置
originRequest
的contentInformationRequest
- 将自定义请求的返回数据返回给
originRequest
的dataRequest
- 通过自定义请求的结束调用通知
originRequest
的dataRequest
BCQResourceFragmentRequest
是数据请求的真正执行者。它根据分片的 BCQResourceFragment
数组,按顺序进行更细粒度的数据请求(远端请求或本地读取)。当从远端获取到数据时,首先向上层转发,其次异步写入本地。每个
BCQResourceFragmentRequest
单独占用一个线程,可并发执行。
BCQResourceInfo
会在初始化时从本地读取元数据
BCQResourceMeta
,元数据记录了本地已缓存数据的分片信息。
BCQResourceUtils
则包含一些工具方法,如:创建缓存目录、日志打印方法等。
注意问题
自定义Scheme
实现 AVAssetResourceLoaderDelegate
协议时,URL
必须是自定义的 URLScheme。我们需要把原始 URL 的 http://
或
https://
替换成 xxx://
,协议方法才会生效。
服务器信任证书
在请求资源时,我们可能会遇到 Challenge
验证。此时,我们需要在如下代理方法中进行 Challenge 验证。
1
2
3func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
Swift HTTPURLResponse Content-Range 天坑
使用 Swift 实现视频缓存方法,调试过程中遇到了一个 Swift URLHTTPResponse 天坑:关于 HTTP Header 中的 Content-Range 字段。
正常情况下或者连接 Charles 并且 Disable SSL Proxying
情况下,Content-Range
为小写,即
content-type
;连接 Charles 并且 Enable SSL Proxying
情况下,Content-Range
为大写,即
Content-Range
。
总结
在方案设计阶段,调研了多个开源库,包括:ShortMediaCache、VIMediaCache。详细阅读了
VIMediaCache
源码,绘制其设计类图,分析其设计优点和缺点。汲取 VIMediaCache
的设计优点,最后重新设计了一套方案。
在开发调试过程中,遇到了一些坑,花了不少时间解决。具体的实现涉及到不少细节,开发过程中也花费了不少时间。
总体而言,得到了很好的锻炼,值得~