FFMpeg 解码 API 以及在解码过程中存在的丢帧问题
背景
在优化视频客观全参考算法(主要是 PSNR, SSIM, MS-SSIM)时,我们首先利用 FFMpeg 提供的 API(avcodec_send_packet()
,avcodec_receive_frame()
)对输入的两个 MP4 文件转成对应的 YUV 格式的数据文件,然后再基于这两份 YUV 数据文件进行计算,得到对应的结果。
但是,我们发现,MP4 文件转成 YUV 数据后,总是会发生丢失视频最后几帧的现象。
为了弄清楚这个问题,查阅了 FFMpeg 的源码,并参考了网络上的资料,然后总结出了这篇文章。
FFMpeg 的编解码 API
从3.1版本开始,FFMpeg 提供了新的编解码API来对音视频数据进行编解码操作,从而实现对输入和输出的解耦:
解码 API
avcodec_send_packet()
avcodec_receive_frame()
编码 API
avcodec_send_frame()
avcodec_receive_packet()
同时,也正是从 3.1 版本开始,之前的编解码 API 也被标注为deprecated
:
解码 API
avcodec_decode_video2()
avcodec_decode_audio4():
编码 API
avcodec_encode_video2()
avcodec_encode_audio2()
在我们的工具中,我们采用了新的解码 API:avcodec_send_packet()
和avcodec_receive_frame()
,实现视频帧的解码,并将解码后的数据转成 YUV 数据。具体的代码片段如下,点击可查看完整测试代码。
从代码可以看出,i
是解码帧的总数,但是我们运行之后发现,一个 252 帧的视频,最终只得到了 248 帧。
send_packet & receive_frame
为了加深对解码 API 的了解,以便能查出问题原因,我们查阅了FFMpeg的代码,从代码的注释中,我们发现了问题:我们没有遵循 API 的使用规范,同时 FFMpeg 在注释中也说明了为什么会出现我们遇到的问题。
也就是说,为了提升性能或出于其他的考虑,解码器会在内部缓存多个frames
/packets
。因此,当流结束的时候,需要对解码器执行flushing
操作,以便获取解码器缓存的frames
/packets
。
我们的工具中,在流结束之后,并没有执行flushing
操作,因此就出现了解码过程丢帧的现象。按照 FFMpeg 的指导,我们补充了如下的逻辑,以便获取解码器中缓存的帧,点击可查看完整测试代码。
再次运行,我们发现,丢帧问题消失了。
FFMPeg 解码 API 状态机
avcodec_send_packet 返回值
从 FFMpeg 的源码中,我们会发现,正常情况下,avcodec_send_packet()
函数的返回值主要有以下三种:
0
: on success.EAGAIN
: input is not accepted in the current state - user must read output with avcodec_receive_frame() (once all output is read, the packet should be resent, and the call will not fail with EAGAIN).EOF
: the decoder has been flushed, and no new packets can be sent to it (also returned if more than 1 flush packet is sent).
avcodec_receive_frame 返回值
同样的,正常情况下,avcodec_receive_frame()
函数的返回值主要有以下三种:
0
: success, a frame was returned.EAGAIN
: output is not available in this state - user must try to send new input.EOF
: the decoder has been fully flushed, and there will be no more output frames.
解码 API 状态机
avcodec_send_packet()
和avcodec_receive_frame()
不同的返回值代表了解码器的不同的状态。
对 API 的调用实际上是一种动作,而 API 的返回值则用来标志当前解码器的状态。因此,解码 API 的整个过程实际上就是一个状态机。
根据 avcodec_send_packet 返回值和 avcodec_receive_frame 返回值中的介绍,可以得到正常情况下,解码过程的状态机,如下图所示。
在图中,节点
代表状态(API 的返回值),箭头
代表 API 的调用。蓝色表示和avcodec_send_packet()
相关,红色表示和avcodec_receive_frame()
相关。
我们修复版本的解码实现实际上就是对如上图所示的状态机的实现。
而如果在实现的时候,没有处理如下图所示的状态,则会导致无法获取视频最后几帧的问题。
思考
源码面前,了无秘密。侯捷老师说过“源码面前,了无秘密”。工作中发现,源码确实是我们获取知识和经验的一个非常有效的途径,尤其是那些好的开源项目的源码,更是如此。
源码还是我们解决问题的强有力的手段之一。对于这些优秀的开源项目的源码而言,代码只是一个部分,源码中的注释、文档等会为我们提供足够的资源。这次问题的解决就是依赖源码,之前在 Android 摄像头 Mock 技术的研究中,也是在查阅 Android 相关源码后才有了思路。因此,当我们在工作中遇到问题的时候,第一手的资料还是源码(当然,要有源码才行),其次才是官方文档,最后才是网络上的其他资源。
版权声明: 本文为 InfoQ 作者【wangwei1237】的原创文章。
原文链接:【http://xie.infoq.cn/article/1296652533092d54716df589c】。文章转载请联系作者。
评论