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