输入和输出帧之间存在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”可以在这里找到。我已经硬编码了文件名。
- 我知道
avcodec_decode_video2
和avcodec_encode_video2
已经被弃用,但我不在意。它们很好用,我已经花费了太多时间来理解最新版本的API,ffmpeg几乎每次发布都会更改他们的API,而我现在真的不想处理avcodec_send_*
和avcodec_receive_*
。 - 我认为我应该通过向
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_*
: 从数据包复制的字段,在阅读this post后发现。它们被正确设置,但我不知道它们是否有用。write_frame: outframe->pts
: 正在编码的帧的显示时间戳。 我应该将其设置为什么?write_frame: outframe->pkt_*
: 来自数据包的定时字段。 我应该设置它们吗?编码器似乎忽略了它们。write_frame: packet.dts
: 正在编码的数据包的解码时间戳。 我应该将其设置为什么?write_frame: packet.pts
: 正在编码的数据包的显示时间戳。 我应该将其设置为什么?write_frame: packet.duration
: 正在编码的数据包的持续时间。 我应该将其设置为什么?
inframe
是d.rgbframe
:
-
- 初始化
e.stream->time_base = d.stream->time_base
- 初始化
e.codecx->time_base = d.codecx->time_base
- 在
read_frame
中设置d.rgbframe->pts = packet.dts
- 在
write_frame
中设置outframe->pts = inframe->pts
- 结果:发出警告,提示编码器时间基数未设置(由于
d.codecx->time_base为0/1
),导致段错误。
- 初始化
-
- 初始化
e.stream->time_base = d.stream->time_base
- 初始化
e.codecx->time_base = d.stream->time_base
- 在
read_frame
中设置d.rgbframe->pts = packet.dts
- 在
write_frame
中设置outframe->pts = inframe->pts
- 结果:没有警告,但是VLC报告帧速率为480.048(不知道这个数字来自哪里),文件播放过快。
此外,编码器将(编辑:事实证明,这是因为packet
中的所有时序字段设置为0,这不是我期望的。av_interleaved_write_frame
与av_write_frame
不同,它接管了数据包并将其与空白数据包进行交换,而我是在该调用之后打印这些值的。因此它们不被忽略。)
- 初始化
-
- 初始化
e.stream->time_base = d.stream->time_base
- 初始化
e.codecx->time_base = d.stream->time_base
- 在
read_frame
中设置d.rgbframe->pts = packet.dts
- 在
write_frame
中设置packet
中的任何一个pts/dts/duration
为任何值。 - 结果:警告未设置包时间戳。编码器似乎重置所有数据包时间字段为0,因此这些都没有任何影响。
- 初始化
-
- 初始化
e.stream->time_base = d.stream->time_base
- 初始化
e.codecx->time_base = d.stream->time_base
- 在阅读this post后,我发现
AVFrame
中有这些字段:pkt_pts
,pkt_dts
和pkt_duration
,因此我尝试将这些字段全部复制到outframe
中。 - 结果:真的很有希望,但最终结果与第三次尝试相同(未设置数据包时间戳警告,结果不正确)。
- 初始化
那么我该怎么做呢?在这里有数不清的与时间相关的字段,我该如何使输出与输入相同?而且我要如何以处理任意视频输入格式,这些格式可能将它们的时间戳和时间基准存储在不同的位置?我需要这始终可行。
为了参考,这里是我测试输入文件的视频流中读取的所有数据包和帧时间戳的表格,以便了解我的测试文件的情况。没有设置任何输入数据包pts,同样适用于帧pts,并且由于某种原因,前108帧的持续时间为0。VLC可以正常播放该文件,并将帧速率报告为29.9700089:
- 表格在此处,因为它太大而无法在此帖子中显示。