Libav(ffmpeg)将解码的视频时间戳复制到编码器

我正在编写一个应用程序,用于解码来自输入文件(任何编解码器,任何容器)的单个视频流,执行一堆图像处理,并将结果编码为输出文件(单视频流,Quicktime RLE,MOV)。 我正在使用ffmpeg的libav 3.1.5(Windows现在构建,但应用程序将是跨平台的)。

输入和输出帧之间有1:1的对应关系,我希望输出中的帧定时与输入相同。 我真的很难完成这个任务。 所以我的一般问题是: 我如何可靠地(如在所有输入情况下)将输出帧定时设置为与输入相同?

我花了很长时间来浏览API并达到了我现在的目标。 我把一个最小的测试程序放在一起工作:

#include <cstdio>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

using namespace std;


struct DecoderStuff {
    AVFormatContext *formatx;
    int nstream;
    AVCodec *codec;
    AVStream *stream;
    AVCodecContext *codecx;
    AVFrame *rawframe;
    AVFrame *rgbframe;
    SwsContext *swsx;
};


struct EncoderStuff {
    AVFormatContext *formatx;
    AVCodec *codec;
    AVStream *stream;
    AVCodecContext *codecx;
};


template <typename T>
static void dump_timebase (const char *what, const T *o) {
    if (o)
        printf("%s timebase: %d/%dn", what, o->time_base.num, o->time_base.den);
    else
        printf("%s timebase: null objectn", what);
}


// reads next frame into d.rawframe and d.rgbframe. returns false on error/eof.
static bool read_frame (DecoderStuff &d) {

    AVPacket packet;
    int err = 0, haveframe = 0;

    // read
    while (!haveframe && err >= 0 && ((err = av_read_frame(d.formatx, &packet)) >= 0)) {
       if (packet.stream_index == d.nstream) {
           err = avcodec_decode_video2(d.codecx, d.rawframe, &haveframe, &packet);
       }
       av_packet_unref(&packet);
    }

    // error output
    if (!haveframe && err != AVERROR_EOF) {
        char buf[500];
        av_strerror(err, buf, sizeof(buf) - 1);
        buf[499] = 0;
        printf("read_frame: %sn", buf);
    }

    // convert to rgb
    if (haveframe) {
        sws_scale(d.swsx, d.rawframe->data, d.rawframe->linesize, 0, d.rawframe->height,
                  d.rgbframe->data, d.rgbframe->linesize);
    }

    return haveframe;

}


// writes an output frame, returns false on error.
static bool write_frame (EncoderStuff &e, AVFrame *inframe) {

    // see note in so post about outframe here
    AVFrame *outframe = av_frame_alloc();
    outframe->format = inframe->format;
    outframe->width = inframe->width;
    outframe->height = inframe->height;
    av_image_alloc(outframe->data, outframe->linesize, outframe->width, outframe->height,
                   AV_PIX_FMT_RGB24, 1);
    //av_frame_copy(outframe, inframe);
    static int count = 0;
    for (int n = 0; n < outframe->width * outframe->height; ++ n) {
        outframe->data[0][n*3+0] = ((n+count) % 100) ? 0 : 255;
        outframe->data[0][n*3+1] = ((n+count) % 100) ? 0 : 255;
        outframe->data[0][n*3+2] = ((n+count) % 100) ? 0 : 255;
    }
    ++ count;

    AVPacket packet;
    av_init_packet(&packet);
    packet.size = 0;
    packet.data = NULL;

    int err, havepacket = 0;
    if ((err = avcodec_encode_video2(e.codecx, &packet, outframe, &havepacket)) >= 0 && havepacket) {
        packet.stream_index = e.stream->index;
        err = av_interleaved_write_frame(e.formatx, &packet);
    }

    if (err < 0) {
        char buf[500];
        av_strerror(err, buf, sizeof(buf) - 1);
        buf[499] = 0;
        printf("write_frame: %sn", buf);
    }

    av_packet_unref(&packet);
    av_freep(&outframe->data[0]);
    av_frame_free(&outframe);

    return err >= 0;

}


int main (int argc, char *argv[]) {

    const char *infile = "wildlife.wmv";
    const char *outfile = "test.mov";
    DecoderStuff d = {};
    EncoderStuff e = {};

    av_register_all();

    // decoder
    avformat_open_input(&d.formatx, infile, NULL, NULL);
    avformat_find_stream_info(d.formatx, NULL);
    d.nstream = av_find_best_stream(d.formatx, AVMEDIA_TYPE_VIDEO, -1, -1, &d.codec, 0);
    d.stream = d.formatx->streams[d.nstream];
    d.codecx = avcodec_alloc_context3(d.codec);
    avcodec_parameters_to_context(d.codecx, d.stream->codecpar);
    avcodec_open2(d.codecx, NULL, NULL);
    d.rawframe = av_frame_alloc();
    d.rgbframe = av_frame_alloc();
    d.rgbframe->format = AV_PIX_FMT_RGB24;
    d.rgbframe->width = d.codecx->width;
    d.rgbframe->height = d.codecx->height;
    av_frame_get_buffer(d.rgbframe, 1);
    d.swsx = sws_getContext(d.codecx->width, d.codecx->height, d.codecx->pix_fmt,
                            d.codecx->width, d.codecx->height, AV_PIX_FMT_RGB24,
                            SWS_POINT, NULL, NULL, NULL);
    //av_dump_format(d.formatx, 0, infile, 0);
    dump_timebase("in stream", d.stream);
    dump_timebase("in stream:codec", d.stream->codec); // note: deprecated
    dump_timebase("in codec", d.codecx);

    // encoder
    avformat_alloc_output_context2(&e.formatx, NULL, NULL, outfile);
    e.codec = avcodec_find_encoder(AV_CODEC_ID_QTRLE);
    e.stream = avformat_new_stream(e.formatx, e.codec);
    e.codecx = avcodec_alloc_context3(e.codec);
    e.codecx->bit_rate = 4000000; // arbitrary for qtrle
    e.codecx->width = d.codecx->width;
    e.codecx->height = d.codecx->height;
    e.codecx->gop_size = 30; // 99% sure this is arbitrary for qtrle
    e.codecx->pix_fmt = AV_PIX_FMT_RGB24;
    e.codecx->time_base = d.stream->time_base; // ???
    e.codecx->flags |= (e.formatx->flags & AVFMT_GLOBALHEADER) ? AV_CODEC_FLAG_GLOBAL_HEADER : 0;
    avcodec_open2(e.codecx, NULL, NULL);
    avcodec_parameters_from_context(e.stream->codecpar, e.codecx); 
    //av_dump_format(e.formatx, 0, outfile, 1);
    dump_timebase("out stream", e.stream);
    dump_timebase("out stream:codec", e.stream->codec); // note: deprecated
    dump_timebase("out codec", e.codecx);

    // open file and write header
    avio_open(&e.formatx->pb, outfile, AVIO_FLAG_WRITE); 
    avformat_write_header(e.formatx, NULL);

    // frames
    while (read_frame(d) && write_frame(e, d.rgbframe))
        ;

    // write trailer and close file
    av_write_trailer(e.formatx);
    avio_closep(&e.formatx->pb); 

}

关于这个的一些注意事项:

  • 由于我迄今为止在帧定时上的所有尝试都失败了,因此我已经从该代码中删除了几乎所有与时序相关的内容,以便以干净的版本开始。
  • 为简洁起见,几乎所有的错误检查和清理都省略了。
  • 我分配一个新的输出帧与一个新的缓冲区的原因write_frame ,而不是使用inframe直接,是因为这是比较有代表性的就是我真正的应用程序在做。 我真正的应用程序也使用RGB24内部,因此在这里转换。
  • 之所以我在outframe生成一个奇怪的模式,而不是使用例如av_copy_frame ,是因为我只是想要一个能够与Quicktime RLE一起压缩的测试模式(否则我的测试输入最终会生成一个1.7GB的输出文件)。
  • 我使用的输入视频“wildlife.wmv”可以在这里找到。 我对文件名进行了硬编码。
  • 我知道, avcodec_decode_video2avcodec_encode_video2已弃用,但不在意。 他们工作得很好,我已经挣扎了太多的avcodec_send_* API的最新版本,ffmpeg几乎每次发布都会改变他们的API,而我现在真的不想处理avcodec_send_*avcodec_receive_*
  • 我想我应该通过向avcodec_encode_video2传递一个NULL帧来刷新一些缓冲区或其他内容,但我对此有点困惑。 除非有人认为现在让我们忽略它,这是一个单独的问题。 关于这一点,文档是模糊的,因为它们涉及其他所有内容。
  • 我的测试输入文件的帧频是29.97。

  • 现在,我目前的尝试。 上面的代码中提供了以下与时间相关的字段,其中以粗体显示细节/混淆。 其中有很多,因为API令人难以置信地令人费解:

  • main: d.stream->time_base :输入视频流时基。 对于我的测试输入文件,这是1/1000。
  • main: d.stream->codec->time_base :不知道这是什么(当我总是使用自己的新上下文时,我从来没有理解为什么AVStream有一个AVCodecContext字段),而且不推荐使用codec字段。 对于我的测试输入文件,这是1/1000。
  • main: d.codecx->time_base :输入编解码器上下文时基。 对于我的测试输入文件,这是0/1。 我应该设置它吗?
  • main: e.stream->time_base :我创建的输出流的时基。 我该如何设置?
  • main: e.stream->codec->time_base :我创建的输出流的弃用和神秘的编解码器字段的时基。 我是否将此设置为任何内容?
  • main: e.codecx->time_base :我创建的编码器上下文的时基。 我该如何设置?
  • read_frame: packet.dts :数据包读取的解码时间戳。
  • read_frame: packet.pts :数据包读取的演示时间戳。
  • read_frame: packet.duration :数据包读取的持续时间。
  • read_frame: d.rawframe->pts :解码原始帧的演示时间戳。 这总是0.为什么它不被解码器读取......?
  • read_frame: d.rgbframe->pts / write_frame: inframe->pts :转换为RGB的解码帧的演示时间戳。 目前没有设置任何内容。
  • read_frame: d.rawframe->pkt_* :从包中复制的字段,在阅读本文后发现。 它们设置正确,但我不知道它们是否有用。
  • write_frame: outframe->pts :正在编码的帧的演示时间戳。 我应该把它设置成什么?
  • write_frame: outframe->pkt_* :来自数据包的计时字段。 我应该设置这些吗? 他们似乎被编码器忽略。
  • write_frame: packet.dts :正在编码的数据包的解码时间戳。 我该如何设置它?
  • write_frame: packet.pts :正在编码的数据包的显示时间戳。 我该如何设置它?
  • write_frame: packet.duration :正在编码的数据包的持续时间。 我该如何设置它?
  • 我已经尝试了以下描述的结果。 需要注意的是inframed.rgbframe

  • Init e.stream->time_base = d.stream->time_base
  • Init e.codecx->time_base = d.codecx->time_base
  • read_frame设置d.rgbframe->pts = packet.dts read_frame
  • 设置outframe->pts = inframe->ptswrite_frame
  • 结果:警告编码器时基未设置(因为d.codecx->time_base was 0/1 ),seg故障。
  • Init e.stream->time_base = d.stream->time_base
  • Init e.codecx->time_base = d.stream->time_base
  • read_frame设置d.rgbframe->pts = packet.dts read_frame
  • 设置outframe->pts = inframe->ptswrite_frame
  • 结果:没有警告,但VLC报告帧速率为480.048(不知道这个数字来自哪里)和文件播放速度太快。 此外,编码器将packet所有时序字段设置为0,这不是我所期望的。 (编辑:原来,这是因为av_interleaved_write_frameav_write_frame不同,它拥有数据包的所有权并将其与空白av_write_frame交换,并且在打电话后我打印这些数值,因此不会被忽略。)
  • Init e.stream->time_base = d.stream->time_base
  • Init e.codecx->time_base = d.stream->time_base
  • read_frame设置d.rgbframe->pts = packet.dts read_frame
  • write_frame任何packet / pts /持续时间write_frame为任何值。
  • 结果:关于未设置数据包时间戳的警告。 编码器似乎将所有数据包计时字段重置为0,所以这些都没有任何影响。
  • Init e.stream->time_base = d.stream->time_base
  • Init e.codecx->time_base = d.stream->time_base
  • 在阅读这篇文章后,我在AVFrame发现了这些字段, pkt_ptspkt_dtspkt_duration ,所以我试图将这些字段pkt_pts拷贝到outframe
  • 结果:真的有我的希望,但结果与尝试3相同(包时间戳未设置警告,结果不正确)。
  • 我尝试了上面的各种其他手动变换,没有任何工作。 我想要做的是创建一个输出文件,以与输入相同的时间和帧速率播放(在这种情况下为29.97恒定帧速率)。

    那么,我该如何做到这一点? 在这里的时序相关领域中,我做了什么来使输出与输入相同? 我该如何处理任意视频输入格式,以便在不同的地方存储时间戳和时间基准? 我需要这个始终工作。


    作为参考,下面是从我的测试输入文件的视频流中读取的所有数据包和帧时间戳的表格,以便了解我的测试文件的外观。 没有输入数据包pts'被设置,与帧pts相同,并且由于某种原因,前108帧的持续时间为0.VLC播放文件并将帧速率报告为29.9700089:

  • 表格在这里,因为这篇文章太大了。

  • 我认为你的问题在于时间基础,最初有点混乱。

  • d.stream->time_base: Input video stream time base 。 这是输入容器中时间戳的分辨率。 从av_read_frame返回的编码帧将在此分辨率中具有其时间戳。
  • d.stream->codec->time_base: Not sure what this is 。 这是API兼容性的旧API; 你正在使用编解码器参数,所以忽略它。
  • d.codecx->time_base: Input codec context time-base. For my test input file this is 0/1. Am I supposed to set it? 这是解码器的时间戳(与容器相对)。 编解码器将假定其输入编码帧在该分辨率中具有其时间戳,并且还将在该分辨率中设置输出解码帧中的时间戳。
  • e.stream->time_base: Time base of the output stream I create 。 与解码器相同
  • e.stream->codec->time_base 。 和demuxer一样 - 忽略这个。
  • e.codecx->time_base - 与分路器相同
  • 所以你需要做以下事情:

  • 打开demuxer。 该部分起作用
  • 将解码器时基设置为一些“理智”的值,因为解码器可能不会那样做,并且0/1是不好的 。 如果没有设置任何组件的时间基准,事情就不会如他们应该的那样工作。 最简单的方法就是从分路器复制时间基准
  • 打开解码器。 它可能会改变它的时基,或者它可能不会。
  • 设置编码器时基。 最简单的方法是复制(现在打开的)解码器的时基,因为你不会改变帧率或任何东西。
  • 打开编码器。 它可能会改变它的时基
  • 设置复用器时基。 再一次,最简单的方法是从编码器复制时基
  • 打开复合器。 它也可能改变它的时基。
  • 现在对于每一帧:

  • 从分路器读取它
  • 将时间戳从分路器转换为解码器时基。 有av_packet_rescale_ts可以帮助你做到这一点
  • 解码数据包
  • 将帧时间戳( pts )设置为由av_frame_get_best_effort_timestamp返回的值
  • 将帧时间戳从解码器转换为编码器时基。 使用av_rescale_qav_rescale_q_rnd
  • 编码数据包
  • 将编码器时间戳转换为复用器时基。 再次使用av_packet_rescale_ts
  • 这可能是一种矫枉过正,尤其是编码器在打开时不会改变它们的时基(在这种情况下,您不需要转换原始帧的pts )。


    关于刷新帧,你传递给编码器不一定是编码和输出,所以是的,你应该调用avcodec_encode_video2 NULL作为一个框架,让编码器知道你已经完成,并输出所有剩余的数据(你需要像所有其他数据包一样通过复用器)。 事实上,你应该这样做,直到它停止喷出数据包。 对于某些示例,请参阅ffmpeg中doc/examples文件夹中的编码示例之一。


    所以,非常感谢Andrey Turkin的非常清晰和有用的答案,我已经正确地工作了,我想分享我做的确切的事情:

    在初始化过程中,了解到这些初始时间基准中的任何一个可能会在某些时候被libav改变:

  • 在分配编解码器上下文后立即将解码器编解码器上下文时基初始化为合理的值。 我去亚毫秒分辨率:

    d.codecx->time_base = { 1, 10000 };
    
  • 在创建新流之后立即初始化编码器流时基(注意:在QtRLE的情况下,如果我离开这个{0,0},在编写标题之后它将由编码器设置为{0,90000},但是I不知道其他情况是否合作,所以我在这里初始化它)。 此时,从输入流中复制是安全的,但我注意到我也可以任意初始化它(例如{1,10000}),并且稍后它仍然可以工作:

    e.stream->time_base = d.stream->time_base;
    
  • 分配编码器编解码器上下文时基后立即初始化。 就来自解码器的复制而言,与流时基相同:

    e.codecx->time_base = d.codecx->time_base;
    
  • 我错过的其中一件事是我可以设置这些时间戳,并且libav会服从。 没有限制,取决于我,不管我设置的解码时间戳是在我选择的时间基础上。 我没有意识到这一点。

    然后在解码时:

  • 我所要做的就是手动填入解码帧数。 pkt_*字段是可以忽略的:

    d.rawframe->pts = av_frame_get_best_effort_timestamp(d.rawframe);
    
  • 由于我正在转换格式,我还将其复制到转换后的帧:

    d.rgbframe->pts = d.rawframe->pts;
    
  • 然后,编码:

  • 只有框架的pts需要设置。 Libav将处理数据包。 所以就在编码帧之前:

    outframe->pts = inframe->pts;
    
  • 但是,我仍然需要手动转换数据包时间戳,这看起来很奇怪,但所有这些都非常奇怪,所以我猜这是相当于课程的标准。 帧时间戳仍然在解码器流时基中,因此在编码帧之后但在写入数据包之前:

    av_packet_rescale_ts(&packet, d.stream->time_base, e.stream->time_base);
    
  • 它的作用就像一个魅力,主要是:我注意到VLC报告输入为29.97 FPS,但输出为30.03 FPS,这是我无法想象的。 但是,在我测试过的所有媒体播放器中,一切看起来都不错。

    链接地址: http://www.djcxy.com/p/67559.html

    上一篇: Libav (ffmpeg) copying decoded video timestamps to encoder

    下一篇: C++ h264 ffmpeg/libav encode/decode(lossless) issues