刨根问底 | 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()

/**
 * @ingroup libavc
 * @defgroup lavc_encdec send/receive encoding and decoding API overview
 * @{
 *
 * The avcodec_send_packet()/avcodec_receive_frame()/avcodec_send_frame()/
 * avcodec_receive_packet() functions provide an encode/decode API, which
 * decouples input and output.
 * ...
 */

同时,也正是从3.1版本开始,之前的编解码API也被标注为deprecated:

  • 解码API

  • avcodec_decode_video2()

  • avcodec_decode_audio4():

  • 编码API

  • avcodec_encode_video2()

  • avcodec_encode_audio2()

attribute_deprecated
int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
                          int *got_frame_ptr, const AVPacket *avpkt);

在我们的工具中,我们采用了新的解码API:avcodec_send_packet()和avcodec_receive_frame(),实现视频帧的解码,并将解码后的数据转成YUV数据。具体的代码片段如下:

int process_frame() {
    ......
}

//decode operation.
while (!av_read_frame(fmt_ctx, pkt)) {
    if (pkt->stream_index != video_stream_idx) {
        continue;
    }

    int packet_new = 1;
    while (process_frame(fmt_ctx, dec_ctx, video_stream->codecpar, 
                         frame, pkt, &packet_new) > 0) {
        i++;
    };
    av_packet_unref(pkt);
}

从代码可以看出,i是解码帧的总数,但是我们运行之后发现,一个252帧的视频,最终只得到了248帧。

send_packet & receive_frame

为了加深对解码API的了解,以便能查出问题原因,我们查阅了FFmpeg的代码,从代码的注释中,我们发现了问题:我们没有遵循API的使用规范,同时FFmpeg在注释中也说明了为什么会出现我们遇到的问题。

/**
  * ... 
  * At the beginning of decoding or encoding, the codec might accept multiple
  * input frames/packets without returning a frame, until its internal buffers
  * are filled. This situation is handled transparently if you follow the steps
  * outlined above.
  * 
  * End of stream situations. These require "flushing" (aka draining) the codec,
  * as the codec might buffer multiple frames or packets internally for
  * performance or out of necessity (consider B-frames).
  * ...
  */

也就是说,为了提升性能或出于其他的考虑,解码器会在内部缓存多个frames/packets。因此,当流结束的时候,需要对解码器执行flushing操作,以便获取解码器缓存的frames/packets。

我们的工具中,在流结束之后,并没有执行flushing操作,因此就出现了解码过程丢帧的现象。按照FFmpeg的指导,我们补充了如下的逻辑,以便获取解码器中缓存的帧。

//Flush remaining frames that are cached in the decoder
int packet_new = 1;
av_init_packet(pkt);
pkt->data = NULL;
pkt->size = 0;
while (process_frame(fmt_ctx, dec_ctx, video_stream->codecpar, 
                     frame, pkt, &packet_new) > 0) {
    i++;
    packet_new = 1;
};

再次运行,我们发现,丢帧问题消失了。

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返回值中的介绍,可以得到正常情况下,解码过程的状态机,如下图所示。

ce8cbe211eb89c3abd9b373b5ee60758.png

在图中,节点代表状态(API的返回值),箭头代表API的调用。蓝色表示和avcodec_send_packet()相关,红色表示和avcodec_receive_frame()相关。

我们修复版本的解码实现实际上就是对如上图所示的状态机的实现。

而如果在实现的时候,没有处理如下图所示的状态,则会导致无法获取视频最后几帧的问题。

544051009f1e6c6f6c5a61ee28f0da32.png

思考 & 总结

  • 源码面前,了无秘密。侯捷老师说过“源码面前,了无秘密”。工作中发现,源码确实是我们获取知识和经验的一个非常有效的途径,尤其是那些好的开源项目的源码,更是如此。

  • 源码还是我们解决问题的强有力的手段之一。对于这些优秀的开源项目的源码而言,代码只是一个部分,源码中的注释、文档等会为我们提供足够的资源。这次问题的解决就是依赖源码,之前在Android摄像头Mock技术的研究中,也是在查阅Android相关源码后才有了思路。因此,当我们在工作中遇到问题的时候,第一手的资料还是源码(当然,要有源码才行),其次才是官方文档,最后才是网络上的其他资源。

  • 看了那么多的源码才发现:优秀的项目是那么的一致,而糟糕的项目,各有各的糟糕之处。

fad05cf545a2352c5053912a06419ea1.png

技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

fabd53b14ef31881a4ceacb9fae3b705.png

私信领取相关资料

推荐阅读:

音视频开发工作经验分享 || 视频版

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

Android NDK 免费视频在线学习!!!

你想要的音视频开发资料库来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

98118ebf3e94986bb8aaa7ee85901a15.png

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值