如何在使用 libav 将视频混合到 mp4 容器时补偿帧速率不足

How to compensate frame rate underrun while muxing video to mp4 container with libav

我有一个实时生成视频帧的过程。我正在将生成的视频帧流混合到一个视频文件中(x264 编解码器在 mp4 容器上)。

我正在使用 ffmpeg-libav,并且我自己基于 muxing.c 示例。该示例的问题在于,这不是真实世界的场景,因为在给定的流持续时间内在 while 循环上生成帧,从不丢失帧。

在我的程序中,帧应该以 FPS 生成,但是,根据硬件容量,它可能会生成低于 FPS 的帧。当我初始化视频流上下文时,我声明帧速率为 FPS:

AVRational r = { 1, FPS };
ost->st->time_base = r;

这指定视频将具有 FPS 帧速率,但如果生成的帧数较少,播放速度会更快,因为它仍会像每秒所有声明的帧数一样再现视频。

在谷歌上搜索了很多关于这个主题的内容后,我明白解决这个问题的关键是操纵 pts 和 dts,但我仍然没有找到有效的解决方案。

在muxing.c示例中编写视频帧时有两个关键函数,我在程序中使用的例程:

AVFrame* get_video_frame(int timestamp, OutputStream *ost, const QImage &image)
{
    /* when we pass a frame to the encoder, it may keep a reference to it
     * internally; make sure we do not overwrite it here */
    if (av_frame_make_writable(ost->frame) < 0)
        exit(1);

    av_image_fill_arrays(ost->tmp_frame->data, ost->tmp_frame->linesize, image.bits(), AV_PIX_FMT_RGBA, ost->frame->width, ost->frame->height, 8);
    libyuv::ABGRToI420(ost->tmp_frame->data[0], ost->tmp_frame->linesize[0], ost->frame->data[0], ost->frame->linesize[0], ost->frame->data[1], ost->frame->linesize[1], ost->frame->data[2], ost->frame->linesize[2], ost->tmp_frame->width, -ost->tmp_frame->height);

    #if 1 // this is my attempt to rescale pts, but crashes with pts<dts
    ost->frame->pts = av_rescale_q(timestamp, AVRational{1, 1000}, ost->st->time_base);
    #else
    ost->frame->pts = ost->next_pts++;
    #endif

    return ost->frame;
}

在原始代码中,pts只是每帧递增的整数。我想要做的是在记录开始后以毫秒为单位传递时间戳,以便我可以重新调整点。当我重新缩放 pts 时,程序崩溃并抱怨 pts 低于 dts。

据我所读,pts/dts 操作应该在数据包级别完成,因此我也尝试在 write_frame 例程上操作但没有成功。

int write_frame(AVFormatContext *fmt_ctx, AVCodecContext *c, AVStream *st, AVFrame *frame)
{
    int ret;

    // send the frame to the encoder
    ret = avcodec_send_frame(c, frame);

    if (ret<0)
    {
        fprintf(stderr, "Error sending a frame to the encoder\n");
        exit(1);
    }

    while (ret >= 0)
    {
        AVPacket pkt = { 0 };

        ret = avcodec_receive_packet(c, &pkt);

        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        {
            break;
        }
        else if (ret<0)
        {
            //fprintf(stderr, "Error encoding a frame: %s\n", av_err2str(ret));
            exit(1);
        }

        /* rescale output packet timestamp values from codec to stream timebase */
        av_packet_rescale_ts(&pkt, c->time_base, st->time_base);
        pkt.stream_index = st->index;

        /* Write the compressed frame to the media file. */
        //log_packet(fmt_ctx, &pkt);
        ret = av_interleaved_write_frame(fmt_ctx, &pkt);
        av_packet_unref(&pkt);

        if (ret < 0)
        {
            //fprintf(stderr, "Error while writing output packet: %s\n", av_err2str(ret));
            exit(1);
        }
    }

    return ret == AVERROR_EOF ? 1 : 0;
}

我应该如何操作 dts 和 pts 才能在特定帧上获得视频,而该帧不具有流初始化中指定的所有帧?我应该在哪里进行操作?在 get_video_frame 上?在 write_frame 上?在两者上?

我的方向是否正确?我错过了什么?

看起来你做的大部分都是正确的,但你的time_base对于你的目的来说太小了。

您告诉复用器您的帧是以 1/FPS 的增量生成的,例如 1/25,并且在任何情况下都不会小于它。 如果有时帧之间的时间可以更短或更长(可变帧率),请增加 time_base.

我不知道为什么,但很多视频软件(包括 FFmpeg 客户端)似乎选择 1/12800 作为 MP4 的 time_base。这也是我在 VFR 应用程序(通过 UDP 接收视频)中使用的,并且效果很好。

不要忘记在设置帧的 pts 值时使用带有 av_rescale() 的代码版本,并且在初始化 AVCodecContext 之后,您也必须在其上设置 time_base。永远不要对缩放 AVRational 进行硬编码,总是从 AVCodecContext 重新读取它,因为 libav 内部可以限制该值。

至于“为什么”必须设置这些字段,我建议阅读 avcodec.h 和 avformat.h 的 header Doxygen。在所有有用的功能之上,它们都有关于可以设置哪些字段以及必须设置哪些字段才能工作的描述。这对于了解图书馆对您作为用户的期望非常有用。