Libav (ffmpeg) 将解码后的视频时间戳复制到编码器
Libav (ffmpeg) copying decoded video timestamps to encoder
我正在编写一个应用程序,它从输入文件(任何编解码器、任何容器)中解码单个视频流,进行大量图像处理,并将结果编码为输出文件(单个视频流、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_video2
和 avcodec_encode_video2
已被弃用,但不在乎。他们工作得很好,我已经费尽心思去了解最新版本的 API,ffmpeg 几乎在每个版本中都会更改他们的 API,我真的不想处理 avcodec_send_*
and avcodec_receive_*
现在。
- 我想我应该在 passing a NULL frame to
avcodec_encode_video2
之前完成以刷新一些缓冲区或其他东西,但我对此有点困惑。除非有人想解释一下让我们暂时忽略它,否则这是一个单独的问题。文档对这一点和其他所有内容一样含糊不清。
- 我的测试输入文件的帧速率是 29.97。
现在,关于我目前的尝试。上述代码中存在以下与时间相关的字段,details/confusion 以粗体显示。其中有很多,因为 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 was 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,因此 none 有任何影响。
-
- 初始化
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
.
- 结果:真的满怀希望,但结果与尝试 3 相同(数据包时间戳未设置警告,结果不正确)。
我尝试了上述的各种其他手动排列,但没有任何效果。我 想要 做的是创建一个输出文件,以与输入相同的时间和帧速率(在本例中为 29.97 恒定帧速率)播放。
那么我该怎么做呢? 在这里有无数个与时间相关的字段,我该怎么做才能使输出与输入相同?我如何以处理任意视频输入格式的方式来处理这些视频输入格式,这些格式可能将它们的时间戳和时基存储在不同的地方?我需要它始终有效。
作为参考,这里是从我的测试输入文件的视频流中读取的所有数据包和帧时间戳的 table,以了解我的测试文件的外观。输入数据包 pts 的 None 已设置,与帧 pts 相同,并且由于某种原因前 108 帧的持续时间为 0。VLC 可以很好地播放文件并将帧速率报告为 29.9700089:
- Table is here 因为它对于这个 post.
来说太大了
我认为您的问题在于时基,一开始有点令人困惑。
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
- 与解复用器相同
因此您需要执行以下操作:
- 打开解复用器。那部分有效
- 将解码器时基设置为某个 "sane" 值,因为解码器可能不会这样做,并且 0/1 不好 。如果未设置任何组件的任何时基,事情将无法正常工作。最简单的就是从多路分解器
复制时基
- 打开解码器。它可能会更改其时基,也可能不会。
- 设置编码器时基。最简单的方法是从(现在打开的)解码器复制时基,因为你没有改变帧率或任何东西。
- 打开编码器。它可能会改变它的时基
- 设置多路复用器时基。同样,最简单的方法是从编码器复制时基
- 打开混合器。它也可能会更改其时基。
现在每帧:
- 从分路器读取它
- 将时间戳从多路分解器转换为解码器时基。
av_packet_rescale_ts
可以帮助您做到这一点
- 解码数据包
- 将帧时间戳 (
pts
) 设置为 av_frame_get_best_effort_timestamp
返回的值
- 将帧时间戳从解码器转换为编码器时基。使用
av_rescale_q
或 av_rescale_q_rnd
- 编码数据包
- 将时间戳从编码器转换为多路复用器时基。再次使用
av_packet_rescale_ts
这可能有点矫枉过正,特别是编码器可能不会在打开时更改它们的时基(在这种情况下您不需要转换原始帧'pts
)。
关于刷新 - 您传递给编码器的帧不一定会立即编码和输出,所以是的,您应该使用 NULL 作为帧调用 avcodec_encode_video2
让编码器知道您已完成并完成它输出所有剩余数据(您需要像所有其他数据包一样通过复用器)。事实上,你应该重复这样做,直到它停止吐出数据包。有关一些示例,请参阅 ffmpeg 内 doc/examples
文件夹中的编码示例之一。
所以,100% 地感谢 ,我已经让它正常工作了,我想分享我所做的确切事情:
在初始化期间,了解这些初始时基中的任何一个都可能在某个时候被 libav 更改:
在分配编解码器上下文后立即将解码器编解码器上下文时基初始化为合理的值。我追求亚毫秒分辨率:
d.codecx->time_base = { 1, 10000 };
在创建新流后立即初始化编码器流时基(注意:在 QtRLE 情况下,如果我保留此 {0,0},编码器会将其设置为 {0 ,90000}写完header后,不知道其他情况会不会一样配合,所以在这里初始化)。在这一点上,从输入流中复制是安全的,尽管我注意到我也可以任意初始化它(例如 {1,10000})并且稍后它仍然可以工作:
e.stream->time_base = d.stream->time_base;
分配后立即初始化编码器编解码器上下文时基。就从解码器复制而言,与流时间基准相同:
e.codecx->time_base = d.codecx->time_base;
我遗漏的一件事是我可以设置这些时间戳,而 libav 会服从。没有任何限制,这取决于我,无论我设置什么,解码时间戳都将以我选择的时基为准。我没有意识到这一点。
然后解码时:
我只需手动填写解码帧pts即可。 pkt_*
字段可忽略:
d.rawframe->pts = av_frame_get_best_effort_timestamp(d.rawframe);
因为我正在转换格式,所以我也将它复制到转换后的框架中:
d.rgbframe->pts = d.rawframe->pts;
然后,编码:
只需要设置frame的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,我不太明白。但是,在我测试过的所有媒体播放器中似乎一切正常。
我正在编写一个应用程序,它从输入文件(任何编解码器、任何容器)中解码单个视频流,进行大量图像处理,并将结果编码为输出文件(单个视频流、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_video2
和avcodec_encode_video2
已被弃用,但不在乎。他们工作得很好,我已经费尽心思去了解最新版本的 API,ffmpeg 几乎在每个版本中都会更改他们的 API,我真的不想处理avcodec_send_*
andavcodec_receive_*
现在。 - 我想我应该在 passing a NULL frame to
avcodec_encode_video2
之前完成以刷新一些缓冲区或其他东西,但我对此有点困惑。除非有人想解释一下让我们暂时忽略它,否则这是一个单独的问题。文档对这一点和其他所有内容一样含糊不清。 - 我的测试输入文件的帧速率是 29.97。
现在,关于我目前的尝试。上述代码中存在以下与时间相关的字段,details/confusion 以粗体显示。其中有很多,因为 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
中设置 - 在
write_frame
中设置 - 结果:警告编码器时基未设置(自
d.codecx->time_base was 0/1
),段错误。
d.rgbframe->pts = packet.dts
outframe->pts = inframe->pts
- 初始化
-
- 初始化
e.stream->time_base = d.stream->time_base
- 初始化
e.codecx->time_base = d.stream->time_base
- 在
read_frame
中设置 - 在
write_frame
中设置 - 结果:没有警告,但 VLC 报告帧速率为 480.048(不知道这个数字是从哪里来的)并且文件播放速度太快。
此外,编码器将(编辑:原来这是因为packet
中的所有时序字段设置为 0,这不是我所期望的。av_interleaved_write_frame
,不像av_write_frame
,获取数据包的所有权并将其与空白数据包交换,我在调用 之后打印值 。因此它们不会被忽略。)
d.rgbframe->pts = packet.dts
outframe->pts = inframe->pts
- 初始化
-
- 初始化
e.stream->time_base = d.stream->time_base
- 初始化
e.codecx->time_base = d.stream->time_base
- 在
read_frame
中设置 - 将
write_frame
中的packet
中的 pts/dts/duration 中的任何一个设置为任何值。 - 结果:未设置有关数据包时间戳的警告。编码器似乎将所有数据包计时字段重置为 0,因此 none 有任何影响。
d.rgbframe->pts = packet.dts
- 初始化
-
- 初始化
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
. - 结果:真的满怀希望,但结果与尝试 3 相同(数据包时间戳未设置警告,结果不正确)。
- 初始化
我尝试了上述的各种其他手动排列,但没有任何效果。我 想要 做的是创建一个输出文件,以与输入相同的时间和帧速率(在本例中为 29.97 恒定帧速率)播放。
那么我该怎么做呢? 在这里有无数个与时间相关的字段,我该怎么做才能使输出与输入相同?我如何以处理任意视频输入格式的方式来处理这些视频输入格式,这些格式可能将它们的时间戳和时基存储在不同的地方?我需要它始终有效。
作为参考,这里是从我的测试输入文件的视频流中读取的所有数据包和帧时间戳的 table,以了解我的测试文件的外观。输入数据包 pts 的 None 已设置,与帧 pts 相同,并且由于某种原因前 108 帧的持续时间为 0。VLC 可以很好地播放文件并将帧速率报告为 29.9700089:
- Table is here 因为它对于这个 post. 来说太大了
我认为您的问题在于时基,一开始有点令人困惑。
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
- 与解复用器相同
因此您需要执行以下操作:
- 打开解复用器。那部分有效
- 将解码器时基设置为某个 "sane" 值,因为解码器可能不会这样做,并且 0/1 不好 。如果未设置任何组件的任何时基,事情将无法正常工作。最简单的就是从多路分解器 复制时基
- 打开解码器。它可能会更改其时基,也可能不会。
- 设置编码器时基。最简单的方法是从(现在打开的)解码器复制时基,因为你没有改变帧率或任何东西。
- 打开编码器。它可能会改变它的时基
- 设置多路复用器时基。同样,最简单的方法是从编码器复制时基
- 打开混合器。它也可能会更改其时基。
现在每帧:
- 从分路器读取它
- 将时间戳从多路分解器转换为解码器时基。
av_packet_rescale_ts
可以帮助您做到这一点 - 解码数据包
- 将帧时间戳 (
pts
) 设置为av_frame_get_best_effort_timestamp
返回的值
- 将帧时间戳从解码器转换为编码器时基。使用
av_rescale_q
或av_rescale_q_rnd
- 编码数据包
- 将时间戳从编码器转换为多路复用器时基。再次使用
av_packet_rescale_ts
这可能有点矫枉过正,特别是编码器可能不会在打开时更改它们的时基(在这种情况下您不需要转换原始帧'pts
)。
关于刷新 - 您传递给编码器的帧不一定会立即编码和输出,所以是的,您应该使用 NULL 作为帧调用 avcodec_encode_video2
让编码器知道您已完成并完成它输出所有剩余数据(您需要像所有其他数据包一样通过复用器)。事实上,你应该重复这样做,直到它停止吐出数据包。有关一些示例,请参阅 ffmpeg 内 doc/examples
文件夹中的编码示例之一。
所以,100% 地感谢
在初始化期间,了解这些初始时基中的任何一个都可能在某个时候被 libav 更改:
在分配编解码器上下文后立即将解码器编解码器上下文时基初始化为合理的值。我追求亚毫秒分辨率:
d.codecx->time_base = { 1, 10000 };
在创建新流后立即初始化编码器流时基(注意:在 QtRLE 情况下,如果我保留此 {0,0},编码器会将其设置为 {0 ,90000}写完header后,不知道其他情况会不会一样配合,所以在这里初始化)。在这一点上,从输入流中复制是安全的,尽管我注意到我也可以任意初始化它(例如 {1,10000})并且稍后它仍然可以工作:
e.stream->time_base = d.stream->time_base;
分配后立即初始化编码器编解码器上下文时基。就从解码器复制而言,与流时间基准相同:
e.codecx->time_base = d.codecx->time_base;
我遗漏的一件事是我可以设置这些时间戳,而 libav 会服从。没有任何限制,这取决于我,无论我设置什么,解码时间戳都将以我选择的时基为准。我没有意识到这一点。
然后解码时:
我只需手动填写解码帧pts即可。
pkt_*
字段可忽略:d.rawframe->pts = av_frame_get_best_effort_timestamp(d.rawframe);
因为我正在转换格式,所以我也将它复制到转换后的框架中:
d.rgbframe->pts = d.rawframe->pts;
然后,编码:
只需要设置frame的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,我不太明白。但是,在我测试过的所有媒体播放器中似乎一切正常。