iOS AVPlayer 视频缓存的设计与实现

概述

最近一直在研究 iOS

平台的视频缓存设计方案。目标是实现视频边播边下载。后续再次播放时,则读取本地缓存数据,从而节省用户流量,提升用户体验。

基于 AVPlayer 的视频缓存方案 BCQMediaCache。 【源码传送门】

基本原理

下图所示为 iOS AVPlayer 视频缓存的原理示意图。左边为原始的 AVPlayer

在播放时视频时资源的请求过程。右边为实现视频缓存的方案,其中的关键是为

AVAssetResourceLoader 设置代理,并实现

AVAssetResourceLoaderDelegate

协议所声明的两个方法。通过在这两个方法中捕获所有的

AVAssetResourceLoadingRequest

请求,并为所有的原始请求创建对应的自定义网络请求。使用自定义网络请求向远端多媒体服务器请求资源,当数据返回时,将数据返回给原始请求,并在本地进行数据缓存。

技术细节

AVAssetResourceLoaderDelegate

首先,我们需要为 AVAssetResourceLoader 设置代理。

12let urlAsset = AVURLAsset(url: xxx, options: nil)urlAsset.resourceLoader.setDelegate(self, queue: DispatchQueue.main) > 注意:实现 AVAssetResourceLoaderDelegate

协议时,URL 必须是自定义的 URLScheme。我们需要把原始 URL 的

http:// 或 https:// 替换成

xxx://,协议方法才会生效。

然后,我们需要实现 AVAssetResourceLoaderDelegate

所声明的相关方法。对于视频缓存功能,我们仅需要实现以下两个方法即可。

12345func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool;func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest)

resourceLoader(_:shouldWaitForLoadingOfRequestedResource:)

方法表示代理类是否可以处理该请求。我们通过在这个方法中捕获每个原始请求,并创建对应的自定义网络请求。

resourceLoader(_:didCancel:) 方法表示

AVAssetResourceLoader

主动放弃了某个原始请求。对此,我们需要将原始请求删除,并取消对应的自定义网络请求。

自定义网络请求的创建

在上述

resourceLoader(_:shouldWaitForLoadingOfRequestedResource:)

代理方法中,我们能够捕获到一个原始请求,即一个

AVAssetResourceLoadingRequest 对象。如下所示,为

AVAssetResourceLoadingRequest

中的一些重要的属性和方法。

12345678open class AVAssetResourceLoadingRequest : NSObject { open var request: URLRequest { get } open var contentInformationRequest: AVAssetResourceLoadingContentInformationRequest? { get } open var dataRequest: AVAssetResourceLoadingDataRequest? { get } open func finishLoading() open func finishLoading(with error: Error?)}

其中,request 代表原始请求,由于 AVPlayer

会触发分片下载的策略,request 请求会从

dataRequest

中获取请求的分片范围。因此,根据请求地址和请求分片,我们就可以创建自定义的网络请求。请求分片需要在

HTTP Header 中进行设置。

自定义网络请求的响应

下图所示为视频播放时的一次网络请求的时序图。

我们根据 dataRequest

中的分片信息,创建并发起自定义网络请求。当远端的服务器响应该请求后,客户端会经历一下三个步骤,并调用相应的代理方法。

处理响应

处理数据(多次)

请求结束

123456789101112func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)

处理响应

请求响应时,我们从响应头部中获取资源相关信息,如:

ContentType 表示文件类型

Content-Range 包含文件长度信息

Accept-Ranges 包含是否支持分片请求

我们需要把视频的信息填充到 AVAssetResourceLoadingRequest

的 contentInformationRequest 中,从而通知

AVAssetResourceLoader

要下载视频的视频格式、视频长度等。

处理数据

当请求的分片范围较大时,客户端分多次顺序调用数据处理代理方法。我们可以在此时对接收到的数据进行缓存。当然,还要将数据返回给

dataRequest,可以通过调用 respond(with:)

方法将数据返回给 dataRequest。

请求结束

当数据传输完毕后,我们需要手动调用 finishLoading()

方法通知 AVAssetResourceLoader

数据下载完毕。如果请求失败,我们也需要手动调用

finishLoading(with:) 方法告诉

AVAssetResourceLoader 数据下载失败。

重试机制

当一个网络请求未完成时,我们拖动视频的进度条,AVAssetResourceLoader

会自动取消前一次的网络请求,从而发起一个新的网络请求。

在上述 resourceLoader(_:didCancel:)

代理方法中,我们可以取消某一次下载请求。

分片下载

一般情况下,视频播放支持进度拖拽的功能,即 seek

功能。因此,网络请求的分片与本地的分片数据可能存在如下关系:

本地缺失分片数据

本地包含完整分片数据

本地包含部分分片数据

通过定义一个类来表示这两种分片信息。我们对请求的分片进行检查和拆分,并按顺序进行处理。如果本地已缓存,则直接返回本地分片数据;如果本地未缓存,则创建自定义网络请求,请求分片数据。

123456789enum 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 验证。

123func 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

的设计优点,最后重新设计了一套方案。

在开发调试过程中,遇到了一些坑,花了不少时间解决。具体的实现涉及到不少细节,开发过程中也花费了不少时间。

总体而言,得到了很好的锻炼,值得~

参考

VIMediaCache

ShortMediaCache

可能是目前最好的

AVPlayer 音视频缓存方案

iOS音频播放

(九):边播边缓存

iOS短视频播放缓存之道

AVPlayer初体验之边下边播与视频缓存

通过Authentication

Challenge来信任自签名Https证书