libav 生成具有极高帧率的 MP4 文件

libav producing MP4 file with extremely high frame rate

我正在尝试编写一个程序来生成要通过 ffmpeg/libav 编码为具有单个 h264 流的 mp4 文件的帧。我找到了这两个示例,并试图将它们合并在一起以制作我想要的东西:[video transcoder] [raw MPEG1 encoder]

我已经能够获得视频输出(绿色圆圈改变大小),但无论我如何设置帧的 PTS 值或 time_base 我在 AVCodecContextAVStream,我得到的帧速率约为 7000-15000 而不是 60,导致视频文件持续 70 毫秒而不是 1000 帧/60 fps = 166 秒。每次我更改一些代码时,帧速率都会发生一点变化,几乎就像是从未初始化的内存中读取一样。 Whosebug 上对此类问题的其他引用似乎与错误设置 PTS 值有关;然而,我已经尝试打印出我能找到的所有 PTS、DTS 和时基值,它们看起来都很正常。这是我的概念验证代码(为清楚起见,删除了 libav 调用周围的错误捕获内容):

#include <iostream>
#include <opencv2/opencv.hpp>
#include <math.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/timestamp.h>
}

using namespace cv;

int main(int argc, char *argv[]) {
    const char *filename = "testvideo.mp4";
    
    AVFormatContext *avfc;
    avformat_alloc_output_context2(&avfc, NULL, NULL, filename);
    
    AVStream *stream = avformat_new_stream(avfc, NULL);
    AVCodec *h264 = avcodec_find_encoder(AV_CODEC_ID_H264);
    AVCodecContext *avcc = avcodec_alloc_context3(h264);
    
    av_opt_set(avcc->priv_data, "preset", "fast", 0);
    av_opt_set(avcc->priv_data, "crf", "20", 0);
    avcc->thread_count = 1;
    avcc->width = 1920;
    avcc->height = 1080;
    avcc->pix_fmt = AV_PIX_FMT_YUV420P;
    avcc->time_base = av_make_q(1, 60);
    stream->time_base = avcc->time_base;
    
    if(avfc->oformat->flags & AVFMT_GLOBALHEADER)
        avcc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    
    avcodec_open2(avcc, h264, NULL);
    avcodec_parameters_from_context(stream->codecpar, avcc);
    
    avio_open(&avfc->pb, filename, AVIO_FLAG_WRITE);
    
    avformat_write_header(avfc, NULL);
    
    Mat frame, nothing = Mat::zeros(1080, 1920, CV_8UC1);
    AVFrame *avf = av_frame_alloc();
    AVPacket *avp = av_packet_alloc();
    int ret;
    
    avf->format = AV_PIX_FMT_YUV420P;
    avf->width = 1920;
    avf->height = 1080;
    avf->linesize[0] = 1920;
    avf->linesize[1] = 1920;
    avf->linesize[2] = 1920;
    
    for(int x=0; x<1000; x++) {
        frame = Mat::zeros(1080, 1920, CV_8UC1);
        circle(frame, Point(1920/2, 1080/2), 250*(sin(2*M_PI*x/1000*3)+1.01), Scalar(255), 10);
        
        avf->data[0] = frame.data;
        avf->data[1] = nothing.data;
        avf->data[2] = nothing.data;
        avf->pts = x;
        
        ret = 0;
        do {
            if(ret == AVERROR(EAGAIN)) {
                av_packet_unref(avp);
                ret = avcodec_receive_packet(avcc, avp);
                if(ret) break; // deal with error
                av_write_frame(avfc, avp);
            } //else if(ret) deal with error
            ret = avcodec_send_frame(avcc, avf);
        } while(ret);
    }
    
    // flush the rest of the packets
    avcodec_send_frame(avcc, NULL);
    do {
        av_packet_unref(avp);
        ret = avcodec_receive_packet(avcc, avp);
        if(!ret)
            av_write_frame(avfc, avp);
    } while(!ret);
    
    av_frame_free(&avf);
    av_packet_free(&avp);
    
    av_write_trailer(avfc);
    avformat_close_input(&avfc);
    avformat_free_context(avfc);
    avcodec_free_context(&avcc);
    return 0;
}

这是 ffprobe 运行 在输出视频文件上的输出

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'testvideo.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.76.100
  Duration: 00:00:00.07, start: 0.000000, bitrate: 115192 kb/s
  Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080, 115389 kb/s, 15375.38 fps, 15360 tbr, 15360 tbn, 120 tbc (default)
    Metadata:
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]

什么可能导致我的帧率如此之高?在此先感谢您的帮助。

由于您未能设置 数据包持续时间

,您的帧率很高
  • time_base 设置为更高分辨率(如 1/60000),如所述 here:

     avcc->time_base = av_make_q(1, 60000);
    
  • 按照说明设置avp->durationhere:

     AVRational avg_frame_rate = av_make_q(60, 1);   //60 fps
     avp->duration = avcc->time_base.den / avcc->time_base.num / avg_frame_rate.num * avg_frame_rate.den;    //avp->duration = 1000 (60000/60)
    

    并相应地设置 pts


完整代码:

#include <iostream>
#include <opencv2/opencv.hpp>
#include <math.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/timestamp.h>
}

using namespace cv;

int main(int argc, char* argv[]) {
    const char* filename = "testvideo.mp4";

    AVFormatContext* avfc;
    avformat_alloc_output_context2(&avfc, NULL, NULL, filename);

    AVStream* stream = avformat_new_stream(avfc, NULL);
    AVCodec* h264 = avcodec_find_encoder(AV_CODEC_ID_H264);
    AVCodecContext* avcc = avcodec_alloc_context3(h264);

    av_opt_set(avcc->priv_data, "preset", "fast", 0);
    av_opt_set(avcc->priv_data, "crf", "20", 0);
    avcc->thread_count = 1;
    avcc->width = 1920;
    avcc->height = 1080;
    avcc->pix_fmt = AV_PIX_FMT_YUV420P;
    //Sey the time_base to higher resolution like 1/60000
    avcc->time_base = av_make_q(1, 60000); //avcc->time_base = av_make_q(1, 60);
    stream->time_base = avcc->time_base;

    if (avfc->oformat->flags & AVFMT_GLOBALHEADER)
        avcc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    avcodec_open2(avcc, h264, NULL);
    avcodec_parameters_from_context(stream->codecpar, avcc);

    avio_open(&avfc->pb, filename, AVIO_FLAG_WRITE);

    avformat_write_header(avfc, NULL);

    Mat frame, nothing = Mat::zeros(1080, 1920, CV_8UC1);
    AVFrame* avf = av_frame_alloc();
    AVPacket* avp = av_packet_alloc();
    int ret;

    avf->format = AV_PIX_FMT_YUV420P;
    avf->width = 1920;
    avf->height = 1080;
    avf->linesize[0] = 1920;
    avf->linesize[1] = 1920;
    avf->linesize[2] = 1920;

    for (int x = 0; x < 1000; x++) {
        frame = Mat::zeros(1080, 1920, CV_8UC1);
        circle(frame, Point(1920 / 2, 1080 / 2), (int)(250.0 * (sin(2 * M_PI * x / 1000 * 3) + 1.01)), Scalar(255), 10);

        AVRational avg_frame_rate = av_make_q(60, 1);   //60 fps

        int64_t avp_duration = avcc->time_base.den / avcc->time_base.num / avg_frame_rate.num * avg_frame_rate.den;

        avf->data[0] = frame.data;
        avf->data[1] = nothing.data;
        avf->data[2] = nothing.data;
        avf->pts = (int64_t)x * avp_duration; // avp->duration = 1000

        ret = 0;
        do {
            if (ret == AVERROR(EAGAIN)) {
                av_packet_unref(avp);
                ret = avcodec_receive_packet(avcc, avp);
                if (ret) break; // deal with error

                ////////////////////////////////////////////////////////////////
                //avp->duration was zero.
                avp->duration = avp_duration;    //avp->duration = 1000 (60000/60)

                //avp->pts = (int64_t)x * avp->duration;
                ////////////////////////////////////////////////////////////////

                av_write_frame(avfc, avp);
            } //else if(ret) deal with error
            ret = avcodec_send_frame(avcc, avf);
        } while (ret);
    }

    // flush the rest of the packets
    avcodec_send_frame(avcc, NULL);
    do {
        av_packet_unref(avp);
        ret = avcodec_receive_packet(avcc, avp);
        if (!ret)
            av_write_frame(avfc, avp);
    } while (!ret);

    av_frame_free(&avf);
    av_packet_free(&avp);

    av_write_trailer(avfc);
    avformat_close_input(&avfc);
    avformat_free_context(avfc);
    avcodec_free_context(&avcc);
    return 0;
}

FFprobe 的结果:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'testvideo.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.76.100
  Duration: 00:00:16.65, start: 0.000000, bitrate: 456 kb/s
  Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080, 450 kb/s, 60.06 fps, 60 tbr, 60k tbn, 120k tbc (default)
    Metadata:
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]

备注:

  • 我不知道为什么fps是60.06而不是60
  • 有一条警告消息 MB rate (734400000) > level limit (16711680) 我没有修复。

虽然我接受的答案解决了我遇到的问题,但这里有一些我认为可能有用的更多信息:

time_base 字段根据容器格式对其值有一些限制(例如 1/10000 有效,但 1/9999 无效),这似乎是我的根本问题曾经有。当时基设置为 1/60 时,调用 avformat_write_header() 将其更改为 1/15360。因为我已经将 PTS 增量硬编码为 1,所以这导致了 15360 FPS 视频。奇怪的分母 1​​5360 似乎是由给定的分母反复乘以 2 直到达到某个最小值而产生的。我不知道这个算法实际上是如何工作的。 This SO question 让我想到了这个。

通过将时基设置为1/60000并使PTS每帧递增1000,解决了视频过快的问题。设置数据包持续时间似乎不是必需的,但可能是个好主意。

这里的主要教训是使用任何 time_base libav 给你的东西,而不是假设你设置的值保持不变。 @Rotem 的更新代码执行此操作,因此将以 1/60 的时基“工作”,因为 PTS 和数据包持续时间实际上将基于 1/15360 值 time_base 更改为。