共计 3600 个字符,预计需要花费 9 分钟才能阅读完成。
边下边播总结(一)
概述
最近批改了我的项目中的视频播放性能, 由之前的全量下载完再播, 改为了边下边播的形式. 因为咱们我的项目中的视频在收回时都进行了加密, 所以整个过程其实就是边下载边解密边播放.
边下边播的技术计划, 网上的博客很容易搜到, 不外乎两种形式, 内置本地代理服务器
和AVAssetResourceLoader
. 咱们采取了零碎提供的 AVAssetResourceLoader
这一计划.
计划原理
具体的 AVAssetResourceLoader
实现原理网上能够找到很多逻辑图, 如下图 (来自网络) 所示.
这里联合咱们的理论代码简略的介绍一个这个图片.
在平时应用 AVPlayer 播放 url 时, 咱们会这样创立一个播放器(简略)
let videoAsset = AVURLAsset(url: "http://resource_url/xxxxx")
let item = AVPlayerItem(asset: videoAsset)
let player = AVPlayer(playerItem: item)
如果咱们这样设置播放, 整个播放的外部流程其实都咱们都是不可见的, 视频的下载和缓存等, 咱们只能通过已知的一些办法, 来管制播放器的播放暂停等.
如果想要实现咱们我的项目中想要的成果, 边下载边播放, 同时, 咱们可能须要接手视频的缓存这一模块, 所以咱们就必须得能进入到整个播放流程中, AVAssetResourceLoader
其实就算是苹果给咱们留的一个小口子, 而后通过设置恪守 AVAssetResourceLoaderDelegate
这一协定的代理对象, 接手数据处理的这一过程(包含获取数据和向播放器填充数据).
videoAsset.resourceLoader.setDelegate(self, queue: queue)
注意事项
- 要进入到
AVAssetResourceLoader
的代理回调, 除了要给 videoAsset.resourceLoader 设置 delegate 之外, 还须要把咱们的 url 改为不能辨认的 scheme. 咱们一遍的资源门路都是 http 或者 https, 咱们须要把 url 的 scheme 改为不能辨认的 (公有的), 比方http://resource/xxx/xxx.mp4
改为http-prefix://reource/xxxx/xxx.mp4
- url 门路的最初必须要有视频的后缀, 相似.mp4, 我之前应用的资源门路是没有后缀的, 导致了播放器无奈起播.
AVAssetResourceLoaderDelegate
AVAssetResourceLoaderDelegate
有两个罕用的回调办法如下
// MARK: - AVAssetResourceLoaderDelegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {}
当播放器开始播放的时候, 会通过 shouldWaitForLoadingOfRequestedResource
这个回调办法向咱们索要数据, 具体所要数据的信息细节都封装在 loadingRequest
外面.
因为这个回调会走很屡次, 上图中示意的是要保存起来每一次的 loadingRequest, 但在理论我的项目中, 我应用了不太一样的策略, 我把每一次 loadingRequest 都对应一个 worker 对象来解决, 这样每次索要数据, 都有一个独自的 worker 来解决绝对应的网络申请 (暂不思考缓存), 这样比拟条理. 同时咱们也须要保留起咱们的 worker, 因为如果播放器须要反对进度条拖动时, 须要手动 seek 到某一个地位, 这样会触发didCancel
这个回调, 所以咱们也须要把咱们对应的 worker 外部停掉.
回调解决
当咱们收到一个回调时, 咱们次要关注这个 AVAssetResourceLoadingRequest 类型的 loadingRequest.
他外部有一个 dataRequest 属性, dataRequest 中有 requestedOffset, requestedLength 等一些有用信息. 咱们通过 requestedOffset 和 requestedLength 构建出咱们的 Range, 塞到申请头外面去, 获取相应 range 的数据.
当咱们的 player 开始播放时, 收到的第一个回调, requestedOffset=0,requestedLength=2, 也就是索要 0 - 1 这两个字节, 这次申请其实能够了解为一个嗅探申请, 目标是为了失去视频的相干信息, 文件大小, 类型等.
guard request.contentInformationRequest == nil else {
if request.dataRequest?.requestsAllDataToEndOfResource == false {request.contentInformationRequest?.contentLength = totalLen} else {request.contentInformationRequest?.contentLength = Int64(data.count)
}
request.contentInformationRequest?.isByteRangeAccessSupported = true
request.contentInformationRequest?.contentType = "video/mp4"
request.finishLoading()
return
}
上述代码就是第一个嗅探申请的解决形式, 通过request.contentInformationRequest==nil
, 判断出是第一个嗅探申请, 而后咱们须要填充 request 的 contentInformationRequest, 而后填充信息完结调用finishLoading()
, 以后的 loadingRequest 就完结了.
第一个嗅探申请完结后, 如果咱们返回的没有问题, 那播放器会立即进行下一个回调, 开始所要视频数据, 我在我的项目中测试时, 第二个申请个别都是 0 -xxx(文件大小 -1), 索要整个文件, 这时咱们 dataTask 类型的申请, 期待服务器一片一片的返回数据, 没收到一部分数据后调用dataRequest.respond(with: data)
, 全副收取结束之后调用request.finishLoading()
.
其实这就是最根本的数据填充的逻辑, 除了第一个嗅探申请非凡解决一下, 前面的就是收到数据, 就填充回 dataRequest, 索要的数据全副填充结束, 调用 finishLoading.
在咱们申请整个文件的过程中, 有时候会发现一种景象, 就是 respond 一部分数据之后, loadingRequest 被 cancel 了, 而后又开始索要很前面的 range 的数据, 其实这能够了解为一个寻找文件的 moov 的过程, 文件的 moov 可能在文件头, 也可能在文件尾部.moov 外面定义视频的时间尺度, 时长, 显示个性以及每个轨道信息等, 这一部分能够通过理解 mp4 文件头格局来多做一下理解.
咱们不论他索要的是那一部分数据, 只有咱们申请到对应的数据, respond 回去就没问题.
补充
那这么简略的逻辑对于咱们本人的我的项目来说难点是什么呢, 这里简略形容一下.
后面有说到咱们我的项目中的资源都是通过加密的, 应用了 AES 的加密算法, 这样咱们在承受到数据之后, 是不能间接返回给 dataRequest
的, 须要咱们先解密, 而后简略的说咱们应用的加密策略是每 16 字节是一个加密片段, 但申请返回的数据并不能保障每次都是 16 倍数, 所以咱们解决 16 的倍数能力进行解密这一个问题, 而后还有一个 range 的修改问题, 打比方咱们须要 1 -10 这 10 个字节的数据, 然而我申请头的 range 是不能间接写 1 -10 的, 因为依照咱们每 16 个字节是一个加密片段, 咱们须要的 1 -10, 在 0 -15 这个片段中, 所以咱们必须要先申请下来 0 -15 这一个片段, 而后解密, 再从中拿出 1 -10, 填充回去. 当然了还有一些细节就不开展叙述了, 等有机会联合我的项目独自聊一聊 AES 这个解密办法.
总结
下面就是在实现边下边播过程中总结到的一些小点, 当然每个人在理论我的项目可能会遇到不一样的问题. 同时本文没有波及到数据的缓存, github 上也有很多不错的缓存计划, 大家能够看看.
感激浏览.