这个公众号会路线图式的遍历分享音视频技术:音视频基础 → 音视频工具 → 音视频工程示例 → 音视频工业实战。关注一下成本不高,错过干货损失不小 ↓↓↓
在视频编辑场景中,涉及到的模块很多,比如:抽帧模块、预览播放模块、视频编辑模块、特效合成模块、视频转码模块等等。这些模块各自都有对应的性能指标,这些指标影响着编辑场景的用户体验。这里我们先介绍一下抽帧模块和预览播放模块相关的优化。
在抽帧模块和预览播放模块我们关注的指标主要有:
- 视频抽帧相关:
- 视频抽帧成功率,对视频进行抽帧时的成功率。
- 视频抽帧平均时长,对视频进行抽帧时的获取每帧截图的平均耗时。
- 视频播放相关:
- 视频 Seek 平均时长,从拖动视频进度到对应时间点到图像显示出来的平均耗时。
1、视频抽帧优化
抽帧模块主要用于提取和展示视频画面缩略图的场景。
视频缩略图展示
1.1、抽帧接口异步逐帧回调
通常展示视频画面缩略图是需要一定数量的缩略图,这时候可能有两种做法:一种是等成功获取到所有缩略图后,再一起展示出来;另一种是每获取到一帧缩略图就先展示出来。
从体验上来讲,通常处理完一帧后立即展示出来的体验优于等所有帧都处理完成后才展示的体验,前者的效果能更快的给用户反馈,告诉用户事情正在发生,而不是要等很久。所以,在设计抽帧模块的接口时,就需要将其设计为异步调用且逐帧回调的方式。
1.2、精准抽帧与非精准抽帧
由于编码采用的参数不同,不同视频的关键帧数量和关键帧间隔差别很大,目前很多短视频产品为了提高压缩率,转码时设置的关键帧数量都比较少。抽帧模块在抽取视频帧时,如果仅解码关键帧,处理是最快的,但是当关键帧数量少于需要的抽帧数量时又不能满足显示视频缩略图的需求,这时候就需要解码其他非关键帧。所以,应对不同的视频,抽帧的具体处理方式也不同。
- 精准抽帧:要按照给定的时间点列表,抽取并返回对应时间点的图像。
- 采用跳跃的方式进行解码。首先计算待解码各帧的时间戳位于哪个 GOP,从对应的 GOP 的 IDR 帧开始解码,直到解码到准确的位置。如果待解码的帧中有两帧或多帧在一个 GOP 内,则这两帧或多帧在一次 GOP 顺序解码中完成,不要重复多次从头开始解码该 GOP。这样可以提升抽帧的速度。
- 非精准抽帧:抽取并返回给定数量的图像,但是可以不设置各帧的时间点,或者允许抽取帧的时间点和给定的时间点存在一定的误差。-仅解码关键帧,并可重复使用。仅解码关键帧的好处是速度最快,但如果需要的抽帧数量比视频的关键帧数量多,那就要根据时间点靠近的原则来返回最近的关键帧,这样会出现重复的关键帧作为返回值。比如,现在需要抽取 10 幅缩略图,但视频中仅 2 个 I 帧,则返回的前 5 幅为第 1 个 I 帧,后 5 幅为第 2 个 I 帧。
- 设置非精准误差范围。比如接受误差范围为给定时间点 100ms 左右,则可以先查找给点时间点前后 100ms 左右是否存在关键帧,如果存在,则解码该关键帧返回即可;如果不存在则继续向左查找最近的 IDR 帧开始解码,解码至进入给定时间点左边 100ms 的范围即可停止解码,并返回最近的图像。
1.3、数据转换和缩放优化
视频帧解码后的 YUV 数据通常是非常大的,在抽帧时往往需要将 YUV 数据转换为 RGB 进行处理,并且常常还需要进行裁剪、缩放、旋转。在通过数据格式判断是否需要数据转换或者缩放等操作至指定分辨率时,使用指令加速的 libyuv 替换手写的内存拷贝移动方法能缩短转换时间。
1.4、解码丢弃非参考帧
非参考帧就是其他帧在解码过程中不需要参考此帧。在解码目标帧时,可以丢弃掉关键帧和目标帧之间的非参考帧不进行解码,从而节省解码时间,提升抽帧速度。
1.5、解码性能测试和适配
不同设备的软解、硬解性能有较大的差异,在 Android 设备上硬解还包括 ByteBuffer 和 Surface 方式,它们的解码的性能也表现不同,解码方式有同步也有异步,对于 H.264 和 H.265 以及不同分辨率的支持情况也不同,最好对机型进行 Benchmark 后选择最优解码方式来提高解码速度。
1.6、解码器复用池
在整个视频编辑的工作流中,抽帧模块、预览播放和转码模块都有可能需要使用解码器,由于操作对象大多情况下是同一个视频,所以解码器的参数几乎都是一致的。为了能够更快的获取解码器,可以实现一个解码器复用池来优化解码器的使用性能。
当外界请求解码器池的时候,解码器池会在池中寻找属性匹配(宽、高、H.264/H.265、硬解/软解等)并且处于空闲状态的解码器。如果当前没有符合条件的解码器实例,解码器池会创建解码器并设置解码器为非空闲状态。解码器池也会定时清理空闲的解码器实例,优化内存。
1.7、抽帧缩略图缓存
可以存储解码后的 BitMap 作为缩略图缓存,通过包含视频内容的 hash 值、抽帧尺寸、抽帧位置等参数的信息作为缓存缩略图的 key。当用户对同一个视频进行操作,在进入不同页面需要抽帧时,则可直接从缓存中获取数据来展示,不过这里需要注意 控制缓存的总大小和及时清理缓存。
1.8、多线程并发
可将多个抽帧目标时间戳划分到多个 GOP,由于 GOP 是可以独立解码的单元,所以可以对这些 GOP 进行并发解码抽帧,每组用一个解码器解码,这样可以时限并行解码。
需要注意硬解码器是有限制的,所以一般使用 2-3 个线程并发即可。这里可以结合上面提到的解码器复用池复用解码器,避免解码器的频繁创建和超过数量限制。
1.9、解封装层优化
可以在解封装层就过滤出目标解码帧所在的数据包(AVPacket),而不是等到解码时做 Seek,因为 Seek 是需要 flush 解码器,这样会有耗时。
比如,当待抽帧的视频中关键帧的数量大于或等于目标抽帧数,直接在 Demuxer 中就准备好对应的关键帧包给解码器解码出帧即可;当待抽帧的视频中关键帧的数量小于目标抽帧数,则可以在 Demuxer 中找到所有待解码帧及其依赖帧送给解码器解码出帧。这样就可以避免在解码时还需要做 Seek 操作耗时。
2、视频 Seek 优化
在视频编辑的场景中,用户有大部分时间会停留在编辑页面,在这个页面对视频进度进行拖动来预览视频是一个高频的操作,这样依赖对视频 Seek 体验的优化就显得尤为重要了。
视频 Seek 的流程一般分为:解封装器(Demuxer)Seek、解码器解码、音视频丢弃、渲染这四个步骤。首先播放器根据用户操作拿到目标的 Seek 位置,利用解封装器跳到视频文件距离目标位置左边最近的 IDR 帧开始读取数据,将之后的视频 AVPacket 数据送给解码器解码得到帧(AVFrame)数据,将音频 AVPacket 直接丢弃到目标位置。解码出来的视频帧(AVFrame)数据是从 IDR 帧开始的,所以需要丢弃目标位置之前的帧数据,从而渲染从目标位置开始之后的帧。
2.1、精准 Seek 和非精准 Seek
Seek 分为精准和非精准。精准 Seek 是指 Seek 到给定时间点的位置;非精准 Seek 是指允许 Seek 到给定时间点附近一定误差范围内的位置。
如果不做优化,精准 Seek 的速度可能会比较慢的,原因主要包括:
- Demuxer 解封装耗时:部分格式导致 Demuxer Seek 的过程很慢。比如,MP4 可以从 moov box 的关键帧索引信息中快速精准查到各 IDR 帧的位置,但是 HLS 就需要先找到 ts 切片下载下来,然后只能从这个切片开始读取。
- 解码耗时:解码一帧的速度,以及由于帧之间的解码依赖关系导致要解码多帧才能得到目标帧,是影响 Seek 速度最主要的因素。
- 渲染逻辑耗时:需要丢弃一个 GOP 中的 IDR 帧到目标帧前的其他帧来直接渲染目标帧。需要注意一些线程和锁的等待耗时。
非精准 Seek 可以 Seek 到目标帧左侧最近 IDR 帧的位置,解码器可以直接解码这一帧而不需要依赖其他帧,并随即完成渲染,所以非精准 Seek 的速度可以相对比较快。
2.2、多线程并发
将解封装和解码拆分成两个模块放到不同线程处理,并设置缓冲区。读取数据完成解封装后将数据存储到缓冲区,解码线程从缓冲区取数据解码,形成一个生产者消费者模式。
2.3、减少解码不必要的帧
减少解码不必要的帧包括下面几种情况:
- 解码丢弃目标帧之前的音频帧:由于渲染视频帧的时候,需要丢弃一个 GOP 中的 IDR 帧到目标帧前一帧的数据来直接渲染目标帧。所以解码时可以直接丢弃与这段视频帧对应的音频帧,不必解码。
- 解码丢弃非参考帧:解码可以丢弃非参考帧。准确判断参考帧是方式是:H.264 通过判断 NALU Header 结构中的
nal_ref_idc
字段,该字段为 0 表示非参考帧;H.265 直接判断 NALU Header 的type
字段来确定是否为参考帧。 - 根据最优解码序列解码:最优解码序列是指在当前 GOP 内解码目标帧及目标帧之后所有帧的最小需要依赖帧的集合。基于这个最优解码序列来解码,可以少解码一些帧,对于包含 B 帧较多的视频以及 GOP 长度较大的视频,效果很好。
2.4、向右 Seek 充分利用缓存数据
在做向右 Seek 时,如果当前解封装后的 AVPacket 缓冲区中有目标帧,则不必调用 Demuxer Seek 操作和解码,直接继续解码后面的帧直到目标帧即可。如果目标帧跟当前帧不在一个 GOP,则直接跳到目标帧所在的 GOP 的 IDR 帧开始解码。
2.5、解码性能测试和适配
同『视频抽帧优化』一节中的解码性能测试和适配
讲到的一样,不同设备上的解码性能受到多种因素的影响,最好能做好 Benchmark 再根据性能情况选择最后的编解码配置。
2.6、交互体验优化
用户 Seek 时交互体验优化需要注意以下几点:
- 将影响用户交互的代码移到异步线程:如果 Seek 操作涉及的代码性能影响到了主线程的用户交互,需要将 Seek 操作拆分,将耗时较大的代码放到异步线程,不能影响主线程用户的 UI 操作。
- 用户连续滑动时体验优化:如果用户连续滑动,可以展示滑动中已解码好的帧,即使与当前手指的位置不一致,等滑动停止后再展示停止时刻的帧。连续滑动会触发连续的 Seek,新的 Seek 来了,但是老的 Seek 的帧这时候已经解码完成或者已解码到的帧在上一次目标帧和新的目标帧之间,可以展示当前已解码到的帧,这样可以给用户连续滑动的效果,而不是画面卡住跳动的感觉。