前一段时间使用AVPlayer 实现了视频边播放边缓存的功能,通过AVAssetResourceLoader 来接管AVPlayer 的视频数据请求。把自己通过URLSession 请求到的视频数据进行缓存,同时把视频数据通过AVAssetResourceLoader 给回AVPlayer。实现方式简单来讲就是这样,但在实现过程中还是有很多不好理解的地方。
相关的类介绍
AVPlayer 正常使用非常简单通过AVPlayer(url: url) 直接加载URL 即可。如果我们需要接管AVPlayer 的请求数据则需要另一种初始化AVPlayer 方式,如下:
let asset = AVURLAsset(url: url)
asset.resourceLoader.setDelegate(
resourceLoaderDelegate,
queue:DispatchQueue.main
)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
这里通过setDelegate 设置代理,这样我们就可以在代理对象(resourceLoaderDelegate)中收到AVPlayer 的视频数据请求。代理对象需要遵守AVAssetResourceLoaderDelegate 协议,我们重点关注协议中下面的方法:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
通过这个方法的注释我们可以了解到,参数loadingRequest是用来描述请求数据的信息,在加载一个视频的过程中会产生多个loadingRequest,如果在执行这个方法时不能立刻返回loadingRequest需要的数据,那么需要保存loadingRequest一直到loadingRequest所需要的数据全部返回。下面是loadingRequest的3个关键属性。
注释中同时也提到了,只有custom scheme 才能接管资源的加载,如果scheme 是http/https 则无法接管。所以我们需要把AVPlayer 加载的URL的Scheme替换成自定义的,例如customscheme://xxxx.xxx.xx。然后还需替换回http/https 去服务端请求数据。
class AVAssetResourceLoadingRequest : NSObject {
var request: URLRequest { get }
var contentInformationRequest: AVAssetResourceLoadingContentInformationRequest? { get }
var dataRequest: AVAssetResourceLoadingDataRequest? { get }
}
-
request:数据请求,我们需要根据请求的URL去自己请求数据。
-
contentInformationRequest:关于请求资源的信息,需要我们填充。
-
dataRequest:封装了请求数据的范围,以及返回数据的方法。
我们再来看下最关键的dataRequest:
class AVAssetResourceLoadingDataRequest : NSObject {
var requestedOffset: Int64 { get }
var requestedLength: Int { get }
var currentOffset: Int64 { get }
func respond(with data: Data)
}
前3个属性都是在描述请求数据的范围,我们需要根据这几个属性对我们自己请求到的数据进行截取,然后调用respond方法返回数据给AVPlayer。
整体流程
整体流程大概如下图
重点说下第3步
填充contentInformationRequest
基本上是固定的代码如下
var isByteRangeAccessSupported = false
var contentLength: Int64 = 0
var contentTypeString = ""
if let httpResponse = response as? HTTPURLResponse {
let acceptRange = httpResponse.allHeaderFields["Accept-Ranges"] as? String
if let bytes = acceptRange?.isEqual("bytes") {
isByteRangeAccessSupported = bytes
}
// fix swift allHeaderFields NO! case insensitive
let contentRange = httpResponse.allHeaderFields["content-range"] as? String
let contentRang = httpResponse.allHeaderFields["Content-Range"] as? String
if let last = contentRange?.components(separatedBy: "/").last {
contentLength = Int64(last)!
}
if let last = contentRang?.components(separatedBy: "/").last {
contentLength = Int64(last)!
}
if contentLength == 0 {
contentLength = httpResponse.expectedContentLength
}
if let mimeType = httpResponse.mimeType {
let contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)
if let takeUnretainedValue = contentType?.takeUnretainedValue() {
contentTypeString = takeUnretainedValue as String
}
}
}
返回数据
我们在代理方法中打印loadingRequest 查看规律来获得读取数据的范围
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
print("requestedOffset-\(String(describing: loadingRequest.dataRequest?.requestedOffset))-currentOffset-\(String(describing: loadingRequest.dataRequest?.currentOffset))-requestedLength-\(String(describing: loadingRequest.dataRequest?.requestedLength))")
}
requestedOffset-Optional(4849664)-currentOffset-Optional(4849664)-requestedLength-Optional(65536)
requestedOffset-Optional(30129037)-currentOffset-Optional(30129037)-requestedLength-Optional(31445200)
requestedOffset-Optional(30146560)-currentOffset-Optional(30146560)-requestedLength-Optional(31427677)
requestedOffset-Optional(32112640)-currentOffset-Optional(32112640)-requestedLength-Optional(29461597)
requestedOffset-Optional(7798784)-currentOffset-Optional(7798784)-requestedLength-Optional(720896)
requestedOffset-Optional(8650752)-currentOffset-Optional(8650752)-requestedLength-Optional(65536)
requestedOffset-Optional(8847360)-currentOffset-Optional(8847360)-requestedLength-Optional(65536)
requestedOffset-Optional(8978432)-currentOffset-Optional(8978432)-requestedLength-Optional(65536)
requestedOffset-Optional(9109504)-currentOffset-Optional(9109504)-requestedLength-Optional(65536)
requestedOffset-Optional(9240576)-currentOffset-Optional(9240576)-requestedLength-Optional(131072)
requestedOffset-Optional(9437184)-currentOffset-Optional(9437184)-requestedLength-Optional(65536)
requestedOffset-Optional(9633792)-currentOffset-Optional(9633792)-requestedLength-Optional(65536)
requestedOffset-Optional(9764864)-currentOffset-Optional(9764864)-requestedLength-Optional(196608)
requestedOffset-Optional(10027008)-currentOffset-Optional(10027008)-requestedLength-Optional(65536)
requestedOffset-Optional(10289152)-currentOffset-Optional(10289152)-requestedLength-Optional(65536)
requestedOffset-Optional(10551296)-currentOffset-Optional(10551296)-requestedLength-Optional(65536)
requestedOffset-Optional(10813440)-currentOffset-Optional(10813440)-requestedLength-Optional(131072)
requestedOffset-Optional(11075584)-currentOffset-Optional(11075584)-requestedLength-Optional(65536)
requestedOffset-Optional(11206656)-currentOffset-Optional(11206656)-requestedLength-Optional(65536)
requestedOffset-Optional(11403264)-currentOffset-Optional(11403264)-requestedLength-Optional(65536)
requestedOffset-Optional(11665408)-currentOffset-Optional(11665408)-requestedLength-Optional(65536)
-
requestedOffset:此次获取数据的起点相对于总体数据起点的偏移量
-
currentOffset:已经获取到的数据量
-
requestedLength:此次获取数据的长度
根据这些我们就可以为每个loadingRequest 返回数据啦。假定我们已经从服务端请求到的数据长度为dataLength
-
如果
requestedOffset + requestedLength ≤ dataLength,说明请求到的数据已经满足此次loadingRequest需要的数据了,我们截取数据范围为[requestedOffset, requestedOffset + requestedLength)的数据调用loadingRequest.dataRequest?.respond(with: data),再调用loadingRequest.finishLoading(),然后把loadingRequest 从数组中移除。 -
如果
requestedOffset + requestedLength > dataLength,说明请求到的数据不能满足此次loadingRequest需要的数据,所以我们把尽量多的数据返回,在数组保留loadingRequest等请求到更多数据时再次处理。此时截取数据范围是[requestedOffset + dataLength),这里需要注意当调用完loadingRequest.dataRequest?.respond(with: data),loadingRequest.currentOffset 已经变成 requestedOffset + dataLength,下次获取loadingRequest的数据截取范围时应该从currentOffset 开始。从上面打印规律来看每次requestedOffset 都是等于 currentOffset,所以上面的公式中requestedOffset 都可以替换为currentOffset。 -
在图中第一步存储loadingRequest 时也需要执行上面的步骤,如果数据满足loadingRequest,则直接返回数据并调用
loadingRequest.finishLoading()即可,无须添加进数组。
代码如下:
if let requestedOffset = loadingRequest.dataRequest?.requestedOffset,
let requestedLength = loadingRequest.dataRequest?.requestedLength,
let currentOffset = loadingRequest.dataRequest?.currentOffset {
let start = Int(currentOffset) != 0 ? Int(currentOffset) : Int(requestedOffset)
let end = min(Int(requestedOffset) + requestedLength, downloadData.count)
if start >= end {
continue
}
let responseData = downloadData[start ..< end]
loadingRequest.dataRequest?.respond(with: responseData)
if Int(requestedOffset) + requestedLength <= downloadData.count {
loadingRequest.finishLoading()
finished.append(loadingRequest)
}
}
一些问题
视频seek
视频seek 之后还是会有新的loadingRequest产生,至于是等待请求数据满足seek 位置,还是发起新的请求,请求头设置Range 直接获取seek位置数据。就具体情况具体分析吧。
缓存策略
淘汰策略以及是否只缓存小视频大视频不缓存,也是具体情况具体分析吧。seek 问题如果发起新的请求的话,视频数据是不完整的也需要处理。
最后
希望本文可以帮助到大家,如有问题欢迎指正。