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

Jas*_*n C 10 c++ ffmpeg video-encoding libav

我正在编写一个应用程序,它从输入文件(任何编解码器,任何容器)解码单个视频流,进行一堆图像处理,并将结果编码为输出文件(单个视频流,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/%d\n", what, o->time_base.num, o->time_base.den);
    else
        printf("%s timebase: null object\n", 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: %s\n", 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: %s\n", 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); 

}
Run Code Online (Sandbox Code Playgroud)

关于这一点的几点说明:

  • 由于到目前为止我所有的帧定时尝试都失败了,所以我从这段代码中删除了几乎所有与时序相关的东西,从一个干净的平板开始.
  • 为简洁起见,几乎省略了所有错误检查和清理.
  • 我在新的缓冲区中分配新输出帧write_frame而不是inframe直接使用的原因是因为这更能代表我的实际应用程序在做什么.我的真实应用程序也在内部使用RGB24,因此这里的转换.
  • 之所以我生成一个奇怪的模式outframe,而不是使用eg av_copy_frame,是因为我只想要一个使用Quicktime RLE压缩得很好的测试模式(否则我的测试输入会产生1.7GB的输出文件).
  • 我正在使用的输入视频"wildlife.wmv"可以在这里找到.我已对文件名进行了硬编码.
  • 我知道avcodec_decode_video2并且avcodec_encode_video2已被弃用,但不在乎.它们工作得很好,我已经很难解决API的最新版本,ffmpeg几乎每个版本都会改​​变它们的API,而且我真的不想avcodec_send_*avcodec_receive_*现在打交道.
  • 我想我应该通过传递一个NULL帧avcodec_encode_video2来刷新一些缓冲区或其他东西,但我对此有点困惑.除非有人想解释一下,让我们现在忽略它,这是一个单独的问题.关于这一点,文档是模糊的,因为它们是关于其他一切的.
  • 我的测试输入文件的帧速率是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:

  1.  
    • 在里面 e.stream->time_base = d.stream->time_base
    • 在里面 e.codecx->time_base = d.codecx->time_base
    • 设置d.rgbframe->pts = packet.dtsread_frame
    • 设置outframe->pts = inframe->ptswrite_frame
    • 结果:警告未设置编码器时基(自d.codecx->time_base was 0/1),seg fault.
  2.  
    • 在里面 e.stream->time_base = d.stream->time_base
    • 在里面 e.codecx->time_base = d.stream->time_base
    • 设置d.rgbframe->pts = packet.dtsread_frame
    • 设置outframe->pts = inframe->ptswrite_frame
    • 结果:没有警告,但VLC报告帧速率为480.048(不知道这个数字来自哪里)和文件播放速度太快.此外,编码器将所有时序字段设置packet为0,这不是我所期望的.(编辑:原来这是因为av_interleaved_write_frame,不像av_write_frame,取得数据包的所有权并用空白交换它,我那次调用打印值.所以它们不会被忽略.)
  3.  
    • 在里面 e.stream->time_base = d.stream->time_base
    • 在里面 e.codecx->time_base = d.stream->time_base
    • 设置d.rgbframe->pts = packet.dtsread_frame
    • 将pts/dts/duration中的任何一个设置为packetin write_frame.
    • 结果:未设置有关数据包时间戳的警告.编码器似乎将所有数据包时序字段重置为0,因此这些都没有任何影响.
  4.  
    • 在里面 e.stream->time_base = d.stream->time_base
    • 在里面 e.codecx->time_base = d.stream->time_base
    • 我发现这些领域pkt_pts,pkt_dts以及pkt_durationAVFrame阅读后这个职位,所以我试图通过对复制那些一路outframe.
    • 结果:真的有了我的希望,但结果与尝试3相同(数据包时间戳没有设置警告,结果不正确).

我尝试了上面的各种其他手动波动的排列,没有任何效果.我想要做的是创建一个输出文件,以与输入相同的时间和帧速率播放(在这种情况下为29.97恒定帧速率).

那我该怎么做?在这里有数以万计的时序相关字段,我该怎么做才能使输出与输入相同?我如何处理任意视频输入格式,可以在不同的地方存储时间戳和时基?我需要这个才能一直工作.


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

And*_*kin 14

我认为你的问题是时间基础,起初有点令人困惑.

  • 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相同

所以你需要做以下事情:

  • 打开demuxer.那部分有效
  • 将解码器时基设置为一些"理智"值,因为解码器可能不会这样做,而0/1是坏的.如果没有设置任何组件的任何时基,事情将无法正常工作.最简单的方法是从分离器中复制时基
  • 打开解码器.它可能会改变它的时基,也可能不会.
  • 设置编码器时基.最简单的是从(现在打开)解码器复制时基,因为你没有改变帧速率或任何东西.
  • 打开编码器.它可能会改变它的时基
  • 设置muxer时基.同样,最简单的方法是从编码器复制时基
  • 打开muxer.它也可能会改变它的时基.

现在每帧:

  • 从分离器中读取它
  • 将时间戳从解复用器转换为解码器时基.有av_packet_rescale_ts帮助你做到这一点
  • 解码数据包
  • 将frame timestamp(pts)设置为返回的值av_frame_get_best_effort_timestamp
  • 将帧时间戳从解码器转换为编码器时基.使用av_rescale_qav_rescale_q_rnd
  • 编码包
  • 将时间戳从编码器转换为多路复用器时基.再次,使用av_packet_rescale_ts

这可能是一种矫枉过正,特别是编码器可能不会在打开时更改其时基(在这种情况下,您不需要转换原始帧pts).


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

  • 我忘记提及的一件事是,您需要在解码帧之后执行`frame-&gt; pts = av_frame_get_best_effort_timestamp(frame);`。不要设置输出数据包的时间戳-编码器应该根据其输入帧的pts值为您完成此操作。是的,在“ avcodec_open2”之前设置解码器时基。 (2认同)
  • 这确实是一个很好的答案,其中大部分都困扰我。感谢您发布,请继续发布更多相同的内容!如果您的要点是,我加了+100的鼓励。 (2认同)

Jas*_*n C 7

因此,感谢100%的Andrey Turkin给出了非常清晰而有用的答案,我已经使它正常运行,我想分享一下我所做的确切工作:

在初始化期间,应了解libav可能会在某些时候更改任何这些初始时基:

  • 分配编解码器上下文后,立即将解码器编解码器上下文时基初始化为合理的值。我进行了亚毫秒级的解析:

    d.codecx->time_base = { 1, 10000 };
    
    Run Code Online (Sandbox Code Playgroud)
  • 创建新流后立即初始化编码器流时基(注意:在QtRLE情况下,如果我保留此{0,0},则在写入标头后,编码器会将其设置为{0,90000},但我不知道其他情况是否会合作,所以我在这里初始化它)。此时,仅从输入流进行复制是安全的,尽管我注意到我也可以任意初始化它(例如{1,10000}),并且稍后仍可以使用:

    e.stream->time_base = d.stream->time_base;
    
    Run Code Online (Sandbox Code Playgroud)
  • 分配编码器后立即初始化编码器上下文时基。从解码器复制到流时基相同:

    e.codecx->time_base = d.codecx->time_base;
    
    Run Code Online (Sandbox Code Playgroud)

我缺少的一件事是我可以设置这些时间戳,而libav将服从。没有限制,这取决于我,并且无论我设置什么,解码时间戳都将在我选择的时基内。我没有意识到这一点。

然后在解码时:

然后,编码:

  • 只需要设置框架的点。Libav将处理该数据包。因此,在编码帧之前:

    outframe->pts = inframe->pts;
    
    Run Code Online (Sandbox Code Playgroud)
  • 但是,我仍然必须手动转换数据包时间戳,这似乎很奇怪,但是所有这些都非常奇怪,因此我认为这对课程而言是相当的。帧时间戳仍然在解码器流时基中,因此在对帧进行编码之后但在写入数据包之前:

    av_packet_rescale_ts(&packet, d.stream->time_base, e.stream->time_base);
    
    Run Code Online (Sandbox Code Playgroud)

而且它的工作原理很像是一种魅力:我注意到VLC报告的输入为29.97 FPS,但输出为30.03 FPS,我不太清楚。但是,在我测试过的所有媒体播放器中,一切似乎都能正常运行。