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

标签 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); 

}

关于这一点的一些注意事项:
  • 由于到目前为止我对帧时序的所有尝试都失败了,我已经从这段代码中删除了几乎所有与时序相关的东西,以从头开始。
  • 为简洁起见,几乎省略了所有错误检查和清理。
  • 之所以在 write_frame 中分配一个新的缓冲区,而不是直接使用 inframe ,是因为这更能代表我的实际应用程序正在做的事情。我的真实应用程序也在内部使用 RGB24,因此这里进行转换。
  • 我在 outframe 中生成奇怪模式的原因,而不是使用例如av_copy_frame ,是因为我只想要一个用 Quicktime RLE 压缩得很好的测试模式(否则我的测试输入最终会生成一个 1.7GB 的输出文件)。
  • 我正在使用的输入视频“wildlife.wmv”可以找到 here 。我对文件名进行了硬编码。
  • 我知道 avcodec_decode_video2avcodec_encode_video2 已被弃用,但不在乎。他们工作得很好,我已经在最新版本的 API 上挣扎了太多,ffmpeg 几乎在每个版本中都会更改他们的 API,我现在真的不想处理 avcodec_send_* and avcodec_receive_*
  • 我想我应该在 passing a NULL frame to avcodec_encode_video2 之前完成以刷新一些缓冲区或其他东西,但我对此有点困惑。除非有人想解释让我们暂时忽略它,否则这是一个单独的问题。文档在这一点上与其他所有内容一样含糊不清。
  • 我的测试输入文件的帧率为 29.97。


  • 现在,至于我目前的尝试。上面的代码中存在以下与时序相关的字段,详细信息/混淆以粗体显示。有很多,因为 API 是令人难以置信的复杂:
  • main: d.stream->time_base :输入视频流时基。 对于我的测试输入文件,这是 1/1000。
  • main: d.stream->codec->time_base :不知道这是什么(我永远无法理解为什么 AVStream 有一个 AVCodecContext 字段,不管怎样,当你总是使用你自己的新字段时,你自己的新字段也是 18s12312312313131313131318181314131313131313131313131313131313131313131313131313123 对于我的测试输入文件,这是 1/1000。
  • codec :输入编解码器上下文时基。 对于我的测试输入文件,这是 0/1。我应该设置它吗?
  • main: d.codecx->time_base :我创建的输出流的时基。 我要把它设置成什么?
  • 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 :解码原始帧的显示时间戳。 这总是0。为什么它不被解码器读取......?
  • read_frame: d.rawframe->pts/read_frame: d.rgbframe->pts : 转换为 RGB 的解码帧的显示时间戳。当前未设置为任何内容。
  • write_frame: inframe->pts :从数据包复制的字段,在读取 this post 后发现。它们设置正确,但我不知道它们是否有用。
  • read_frame: d.rawframe->pkt_* :正在编码的帧的显示时间戳。 我应该把它设置成什么吗?
  • write_frame: outframe->pts :来自数据包的计时字段。 我应该设置这些吗?它们似乎被编码器忽略了。
  • write_frame: outframe->pkt_* :正在编码的数据包的解码时间戳。 我把它设置成什么?
  • write_frame: packet.dts :正在编码的数据包的显示时间戳。 我把它设置成什么?
  • write_frame: packet.pts :数据包被编码的持续时间。 我把它设置成什么?

  • 我尝试了以下方法,并得到了描述的结果。请注意, write_frame: packet.durationinframe :

  • 初始化 d.rgbframe
  • 初始化 e.stream->time_base = d.stream->time_base
  • e.codecx->time_base = d.codecx->time_base 设置在 d.rgbframe->pts = packet.dts
  • read_frame 设置在 outframe->pts = inframe->pts
  • 结果:警告未设置编码器时基(自 write_frame 起),段错误。

  • 初始化 d.codecx->time_base was 0/1
  • 初始化 e.stream->time_base = d.stream->time_base
  • e.codecx->time_base = d.stream->time_base 设置在 d.rgbframe->pts = packet.dts
  • read_frame 设置在 outframe->pts = inframe->pts
  • 结果:没有警告,但 VLC 报告帧速率为 480.048(不知道这个数字来自哪里)并且文件播放速度太快。此外,编码器将 write_frame 中的所有计时字段设置为 0,这不是我所期望的。 (编辑:原来这是因为 packetav_interleaved_write_frame 不同,它获取数据包的所有权并将其与空白数据交换,并且我在该调用之后打印了值。因此它们不会被忽略。) 0x2919124212334111

  • 初始化 av_write_frame
  • 初始化 e.stream->time_base = d.stream->time_base
  • e.codecx->time_base = d.stream->time_base 设置在 d.rgbframe->pts = packet.dts
  • read_framepacket 中的任何 pts/dts/duration 设置为任何内容。
  • 结果:未设置有关数据包时间戳的警告。编码器似乎将所有数据包计时字段重置为 0,因此这些都没有任何影响。

  • 初始化 write_frame
  • 初始化 e.stream->time_base = d.stream->time_base
  • 我发现这些领域,e.codecx->time_base = d.stream->time_basepkt_pts,并在pkt_dts pkt_duration阅读this post后,所以我试图通过对AVFrame复制那些一路。
  • 结果:真的让我充满希望,但结果与尝试 3 相同(数据包时间戳未设置警告,结果不正确)。

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

    那么我该怎么做呢? 在这里的无数计时相关字段中,我该怎么做才能使输出与输入相同?我该如何处理可能将时间戳和时基存储在不同位置的任意视频输入格式?我需要这个才能一直工作。

    作为引用,这里是从我的测试输入文件的视频流中读取的所有数据包和帧时间戳的表,以了解我的测试文件的外观。没有设置输入数据包 pts',与帧 pts 相同,由于某种原因,前 108 帧的持续时间为 0。VLC 可以正常播放文件并报告帧速率为 29.9700089:
  • Table is here 因为它对于这篇文章来说太大了。
  • 最佳答案

    我认为您的问题是时基起初有点令人困惑。

  • 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 - 与分路器
  • 相同

    因此,您需要执行以下操作:
  • 打开分路器。那部分工作
  • 将解码器时基设置为某个“正常”值,因为解码器可能不会这样做,而 0/1 是坏的 。如果未设置任何组件的任何时基,则事情将无法正常工作。最简单的方法是从 demuxer
  • 复制时基
  • 打开解码器。它可能会更改其时基,也可能不会。
  • 设置编码器时基。最简单的方法是从(现已打开)解码器复制时基,因为您没有更改帧速率或任何内容。
  • 打开编码器。它可能会改变它的时基
  • 设置多路复用器时基。同样,最简单的方法是从编码器
  • 复制时基
  • 打开多路复用器。它也可能会更改其时基。

  • 现在对于每一帧:
  • 从解复用器中读取它
  • 将时间戳从解复用器转换为解码器时基。有 av_packet_rescale_ts 可以帮助您做到这一点
  • 解码包
  • 将帧时间戳 ( pts ) 设置为 av_frame_get_best_effort_timestamp
  • 返回的值
  • 将帧时间戳从解码器转换为编码器时基。使用 av_rescale_qav_rescale_q_rnd
  • 编码包
  • 将时间戳从编码器转换为多路复用器时基。再次使用 av_packet_rescale_ts

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

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

    关于c++ - Libav (ffmpeg) 将解码的视频时间戳复制到编码器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40275242/

    相关文章:

    c++ - 给定一个类对象的三个“稍微”不同的拷贝,我如何有效地存储它们之间的差异?

    macos - OpenCV VideoWriter write() 函数在 C++ 中的 MAC 操作系统上失败

    node.js - 我有来自 ffmpeg 的音频数据流,如何在浏览器中播放它?

    php - 视频上传大小

    Android - 在解码视频的同时对其进行编码

    c# - C# 的流媒体视频库

    c++ - 如何从文件“HANDLE”中获取一个“HANDLE”到包含目录?

    c++ - 设置整数中的较高位,而不考虑其中的位数

    c++ - 实现迭代加深深度优先搜索

    ffmpeg - 在 Centos 7 中安装 ffmpeg