对于视频播放类功能,大多采用缓存方案。实现视频边播放边下载,后续再次播放时,则读取本地缓存数据,以达到节省用户流量,提升用户体验的目的。在iOS 中视频播放多采用 AVPlayer 。下面就AVPlayer 来做具体分析。
基本原理
下图所示为iOS AVPlayer 视频缓存的原理图。左边为原始的AVPlayer 在播放时资源请求的过程;右边为实现了视频缓存的方案,关键是设置了AVAssetResourceLoader 代理,并实现AVAssetResourceLoaderDelegate 协议声明的两个代理方法。通过在这两个方法中捕获所有的AVAssetResourceLoadingRequest 请求,并为所有的原始请求创建对应的自定义网络请求。是用自定义的网络请求向远端多媒体服务器请求资源,当数据返回时,将数据返回给原始请求,并在本地进行数据的缓存。
AVAssetResourceLoaderDelegate
首先我们需要为 AVAssetResourceLoader 设置代理:
let urlAsset = AVURLAsset(url: xxx, options: nil)
urlAsset.resourceLoader.setDelegate(self, queue: DispatchQueue.main)
在实现AVAssetResourceLoaderDelegate 协议时,URL 必须是自定义的URLScheme 。我们需要把原始的URL 的 http:// 或者 https:// 替换成xxx://,协议方法才会生效。然后,我们需要实现 AVAssetResourceLoaderDelegate 所声明的相关方法。对于视频缓存功能,需要实现一下两个方法:
// shouldWaitForLoadingOfRequestedResource
//方法表示代理类是否可以处理该请求。我们通过在这个方法中捕获每个原始请求,
//并创建对应的自定义网络请求。
func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool;
//didCancel 方法表示 AVAssetResourceLoader 主动放弃了某个原始请求。
//对此,我们需要将原始请求删除,并取消对应的自定义网络请求。
func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
didCancel loadingRequest: AVAssetResourceLoadingRequest)
自定义网络请求的创建
在上面提到的resourceLoader(_:shouldWaitForLoadingOfRequestedResource:) 代理方法中,我们能够捕获到原始请求,即一个AVAssetResourceLoadingRequest 对象。如下所示,是对AVAssetResourceLoadingRequest 中一些常用属性和方法的简单介绍:
open 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 中获取请求的分片范围。因此,根据请求地址和请求分片,我们就可以创建自定义的网络请求。请求分片需要再HTTPHeader 中进行设置。
自定义网络请求的响应
上图是视频播放时的一次网络请求的时序图。根据dataRequest 中的分片信息,创建并发起自定义网络请求。当远端的服务器响应该请求后,客户端会经历三个步骤,并调用相关的代理方法:(1)处理响应 (2)处理数据(多次) (3)请求结束
// 处理响应
func 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-Range 包含是否支持分片请求。 我们需要把视频的信息填充到 AVAssetResourceLoadingRequest 对象的 contentInformationRequest 中,从而通知 AVAssetResourceLoader 要下载视频的视频格式、视频长度等。
处理数据
在请求的分片范围比较大时,客户端分多次,顺序调用数据处理代理方法。我们可以在此时对接收到的数据进行缓存,还要讲数据返回给dataRequest,可以通过 respond(with: ) 方法将数据返回给 dataRequest 。
请求结束
当数据请求完毕后,我们需要手动调用 finishLoading( ) 方法通知 AVAssetResourceLoader 数据已经下载完毕。如果请求失败,我们需要手动调用 finishLoading(with: ) 方法告诉 AVAssetResourceLoader 数据下载失败。
重试机制
当一个网络请求未完成时,我们拖动视频的进度条,AVAssetResourceLoader 会自动取消前一次的网络请求,进而发起一个新的网络请求。在上述 resourceLoader(_:didCancel:) 代理方法中,我们可以取消某一次网络下载请求。
分片下载
一般情况下,视频播放支持进度拖拽的功能,因此,网络请求的分片与本地的分片数据可能存在如下的关系:本地缺失分片数据、本地包含完整分片数据、本地包含部分分片数据。
通过定义一个类来实现这两种分片信息。我们对请求的分片进行检查和拆分,并按顺序进行处理。如果本地已经缓存,则直接返回本地分片数据;如果本地未缓存,则创建自定义网络请求,请求分片数据。
enum 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 包含一些工具方法,比如:创建缓存目录、日志打印方法等
服务器信任证书
在请求资源时,我们可能会遇到Challenge 验证。此时,我们需要在如下代理方法中进行Challenge 验证:
func 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。
作者:倪尛
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。