AVPlayer缓存-AVAssetResourceLoader 的使用

1,626 阅读6分钟

前一段时间使用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。

整体流程

整体流程大概如下图

image.png

重点说下第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

  1. 如果 requestedOffset + requestedLength ≤ dataLength,说明请求到的数据已经满足此次loadingRequest需要的数据了,我们截取数据范围为[requestedOffset, requestedOffset + requestedLength)的数据调用loadingRequest.dataRequest?.respond(with: data),再调用loadingRequest.finishLoading(),然后把loadingRequest 从数组中移除。

  2. 如果 requestedOffset + requestedLength > dataLength,说明请求到的数据不能满足此次loadingRequest需要的数据,所以我们把尽量多的数据返回,在数组保留loadingRequest等请求到更多数据时再次处理。此时截取数据范围是[requestedOffset + dataLength),这里需要注意当调用完loadingRequest.dataRequest?.respond(with: data),loadingRequest.currentOffset 已经变成 requestedOffset + dataLength,下次获取loadingRequest的数据截取范围时应该从currentOffset 开始。从上面打印规律来看每次requestedOffset 都是等于 currentOffset,所以上面的公式中requestedOffset 都可以替换为currentOffset。

  3. 在图中第一步存储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 问题如果发起新的请求的话,视频数据是不完整的也需要处理。

最后

希望本文可以帮助到大家,如有问题欢迎指正。