copy from zhujiamin
一、介绍
FFmpeg4.2支持AV1、AVS2等视频编码格式,但本身并不包含解码器,需要自己集成。集成的编解码器要避开GPL开源协议(–enable-gpl),因此不能用x264、AVS2等编解码器
我在研究FFmpeg升级时,寻找能提升多媒体系统表现力的新特性,发现FFmpeg支持的基于BSD协议的dav1d解码器比较有价值,能大幅度提高AV1软解码性能,没有代码开源的风险,并且能持续迭代更新
AV1是由AOM(Alliance for Open Media,开放媒体联盟)制定的一个开源、免版权费的视频编码格式,目标是解决H265昂贵的专利费用和复杂的专利授权问题并成为新一代领先的免版权费的编码标准。AV1是google制定的VP9标准的继任者,也是H265强有力的竞争者
目前youtube、nextflix,以及国内的爱奇艺都在推广AV1编码视频
google从Android Q开始提供了gav1软解码器,最高支持1080P,播放高码率视频容易卡顿
FFmpeg支持libaom和libdav1d两种解码器
–enable-libaom enable AV1 video encoding/decoding via libaom [no]
–enable-libdav1d enable AV1 decoding via libdav1d [no]
dav1d由VideoLAN,VLC和FFmpeg联合开发,项目由AOM联盟赞助,和libaom相比,dav1d性能普遍提升100%,最高提升400%
二、编译
1.下载源码:git clone https://code.videolan.org/videolan/dav1d.git
2.dav1d用meson和ninja编译,不是常见的Makefile。安装meson(版本>=0.47)、Ninja、nasm (版本>=2.13.02) 安装参考mesonbuild.com/Quick-guide
由于公司的研发编译环境没有安装meson,我是在ubuntu虚拟机上编译的
3.交叉编译:FFmpeg配置–enable-libdav1d后,libavcodec.so会依赖libdav1d.so,meson默认用linux系统环境编译,而libavcodec.so是用ndk环境编译,会有各种依赖的系统库定义报错,因此dav1d也要用相同ndk编译,需要编写交叉编译文件,以编译arm64库为例:
4.执行编译命令,生成libdav1d.so
meson build --buildtype release --cross-file=cross_file_64.txt && ninja -C build
编译后的源码目录如图,cross_file_32.txt、cross_file_64.txt是自己写的交叉编译文件,build目录是编译生成的,libdav1d.so在build/src目录下:
由于dav1d的meson.build脚本有版本信息,因此会生成带版本号的so
三、FFmpeg集成dav1d
1.放置libdav1d.so和头文件
在FFmpeg目录创建extend/dav1d/目录存放so和头文件,头文件在dav1d源码include目录中
2.FFmpeg编译集成dav1d
FFmpeg默认通过.pc文件获取扩展项的头文件和库路径,要求编译环境安装了pkg-config,不便于共同维护
研究configure文件后,注释掉libdav1d的pkg_config检查,并通过–extra-cflags、–extra-ldflags、–extra-libs配置dav1d的头文件、库路径和库名称
用readelf -d libavcodec.so查看它依赖了libdav1d.so.4这个库,于是将其cp到prebuilt目录,预编译到system和vendor,并在代码中添加OMX.ffmpeg.av1.decoder
FFmpeg集成dav1d,在动态库依赖层面的结构如图:
由于dav1d解码库是集成在FFmpeg中的,只要掌握FFmpeg编译、dav1d编译、FFmpeg集成dav1d的方法即可,Android大版本升级时没有额外的适配移植工作量
3.更新FFmpeg解码API
测试dav1d解码时发现有丢帧现象,每秒最高解码15帧,打印log提示废弃的解码函数会丢帧
01-01 06:44:29.139 I 1407 12739 FFMPEG : [libdav1d @ 0xe5f10c00] The deprecated avcodec_decode_* API cannot return all the frames for this decoder. Some frames will be dropped. Update your code to the new decoding API to fix this.
需要用avcodec_send_packet和avcodec_receive_frame替代avcodec_decode_video2
看源码时,发现avcodec_decode_video2源码内部也调用了avcodec_send_packet和avcodec_receive_frame
(1)avcodec_decode_video2一个函数完成了解码的输入和输出,对于capabilities支持CODEC_CAP_DELAY的解码器,可能只输入并延迟输出,最后要填空buffer获取剩余output buffer,否则丢失最后几帧;
对于不支持CODEC_CAP_DELAY的解码器,它会阻塞式地完成一帧解码
(2)avcodec_send_packet和avcodec_receive_frame从逻辑上类似于Android MediaCodec的queueInputBuffer和dequeueOutputBuffer,分别给解码器输入和输出,形式上更加灵活,最后也要送空buffer;
实测发现大部分情况下解码工作发生在avcodec_send_packet输入buffer时,少部分情况在avcodec_receive_frame输出buffer时
更新解码API:
avcodec_decode_video2和avcodec_send_packet、avcodec_receive_frame的返回值和状态判断方式不同,且SoftFFmpegVideo继承Android OMX组件,很多状态判断工作是结合OMX的状态信息完成的,因此修改解码逻辑必须加log调试观察,充分理解解码框架的流程
SoftFFmpegVideo的主体解码流程如图,inQueue和outQueue是OMX的解码器输入输出队列
四、dav1d解码接口简介
FFmpeg的dav1d接口文件为libdav1d.c,解码流程如图
作为开源库的调用者,我们通常关注解码参数设置,先看一下参数设置内容Dav1dSettings,和默认设置参数
其中影响解码性能(速度、负载)的主要是tile线程数、帧线程数,看一下在FFmpeg中是如何赋值的
(AV1标准中tile的定义与HEVC标准中tile的定义类似:是一帧图像中能被独立解码和编码的矩形区域,尽管环路滤波时可以跨越tile的边界)
(1)定义线程数threads
c→thread_count:是AVCodecContext的thread_count对象,调用avcodec_open2打开编解码器时传入FFmpeg
在OPPO的SoftFFmpegVideo中,thread_count的值是0,因此会通过av_cpu_count获取手机CPU核数,再乘以1.5赋值给threads
将thread_count设置为0传给FFmpeg时,FFmpeg都会调用av_cpu_count设置为手机CPU核数。我在8核手机调试过thread_count,设置0或8解码速度是一样的
(2)n_tile_threads和n_frame_threads的计算
还有一个比较有意思的参数是apply_grain,负责获取和叠加film grain(胶片颗粒)
film grain在电视和电影内容中广泛存在,它经常是创作内容的一部分,在编码过程中需要保留下来,因为film grain的随机性,导致很难用传统的压缩算法进行压缩,AV1对film grain做了专门处理
film grain在去噪音过程中会从视频中去除掉,grain参数会通过噪音视频序列和去噪视频序列的差异中获得,这些参数会和压缩视频流一起传输到解码端,解码后,film grain会被叠加到重建视。模型和整体框架如图所示
参考资料:http://lazybing.github.io/blog/2018/10/17/av1-film-grain-synthesis/