The MVE below is a simple test program that should encode 6 s (360 frames) of video. When I import the exported video (output.mkv
) into a video editor, I see that it is only a fraction of a second. Why is this? How do I fix it?
#include <cstddef>
#include <cstdio>
#include <iostream>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
namespace av {
// ERROR HANDLING
// ==============
class av_category_t final : public std::error_category {
public:
inline const char* name() const noexcept override { return "av_category"; }
inline std::string message(int code) const noexcept override {
thread_local static char msg_buffer[AV_ERROR_MAX_STRING_SIZE];
av_strerror(code, msg_buffer, sizeof(msg_buffer));
return std::string(msg_buffer);
}
};
inline const std::error_category& av_category() {
static av_category_t result;
return result;
}
// helper function to create a std::system_error.
inline std::system_error av_error(int code) {
return std::system_error(code, av_category());
}
// Allocate or reallocate buffers in a video frame.
inline void alloc_video_frame(
AVFrame* frame, int width, int height, AVPixelFormat pix_fmt
) {
int err;
if (frame->width != width || frame->height != height ||
frame->format != pix_fmt || !av_frame_is_writable(frame)) {
// unref the old data if any
if (frame->buf[0] != nullptr) {
av_frame_unref(frame);
}
// reset parameters
frame->width = width;
frame->height = height;
frame->format = pix_fmt;
// reallocate
if ((err = av_frame_get_buffer(frame, 0)) < 0)
throw av_error(err);
}
}
inline void alloc_video_frame(AVFrame* frame, const AVCodecContext* ctx) {
alloc_video_frame(frame, ctx->width, ctx->height, ctx->pix_fmt);
}
} // namespace av
void fill_rgb(AVFrame* tmp, AVFrame* dst, uint32_t col) {
static SwsContext* sws = nullptr;
int err;
av::alloc_video_frame(tmp, dst->width, dst->height, AV_PIX_FMT_0RGB32);
for (int i = 0; i < tmp->height; i++) {
for (int j = 0; j < tmp->width; j++) {
void* p = tmp->data[0] + (i * tmp->linesize[0]) + (j * 4);
*((uint32_t*) p) = col;
}
}
sws = sws_getCachedContext(sws,
// src params
tmp->width, tmp->height, (AVPixelFormat) tmp->format,
// dst params
dst->width, dst->height, (AVPixelFormat) dst->format,
// stuff
0, nullptr, nullptr, nullptr
);
if ((err = sws_scale_frame(sws, dst, tmp)) < 0)
throw av::av_error(err);
}
int32_t hue_c(int deg) {
deg %= 360;
int rem = deg % 60;
switch (deg / 60) {
case 0: return 0xFF0000 | ((deg * 256 / 60) << 8);
case 1: return 0x00FF00 | (((60 - deg) * 256 / 60) << 16);
case 2: return 0x00FF00 | ((deg * 256 / 60) << 0);
case 3: return 0x0000FF | (((60 - deg) * 256 / 60) << 8);
case 4: return 0x00FF00 | ((deg * 256 / 60) << 16);
case 5: return 0xFF0000 | (((60 - deg) * 256 / 60) << 0);
}
return 0xFFFFFF;
}
int main() {
int res;
AVFormatContext* fmt_ctx;
AVStream* vstream;
const AVCodec* vcodec;
AVCodecContext* vcodec_ctx;
AVFrame* vframe;
AVFrame* vframe2;
AVPacket* vpacket;
int64_t pts = 0;
// init frame
res = avformat_alloc_output_context2(&fmt_ctx, NULL, NULL, "output.mkv");
if (res < 0)
return 1;
// get encoder
vcodec = avcodec_find_encoder(AV_CODEC_ID_VP8);
// allocate everything else
vstream = avformat_new_stream(fmt_ctx, vcodec);
vcodec_ctx = avcodec_alloc_context3(vcodec);
vframe = av_frame_alloc();
vframe2 = av_frame_alloc();
vpacket = av_packet_alloc();
if (!vstream || !vcodec_ctx || !vframe || !vpacket) {
return 1;
}
// set PTS counter
pts = 0;
// codec: size parameters
vcodec_ctx->width = 640;
vcodec_ctx->height = 480;
vcodec_ctx->sample_aspect_ratio = {1, 1};
// codec: pixel formats
vcodec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
// codec: bit rate et al.
vcodec_ctx->bit_rate = 2e6;
vcodec_ctx->rc_buffer_size = 4e6;
vcodec_ctx->rc_max_rate = 2e6;
vcodec_ctx->rc_min_rate = 2.5e6;
// codec: frame rate
vcodec_ctx->time_base = {1, 60};
vstream->time_base = {1, 60};
vstream->avg_frame_rate = {60, 1};
// open codec
res = avcodec_open2(vcodec_ctx, vcodec, NULL);
if (res < 0)
throw av::av_error(res);
res = avcodec_parameters_from_context(vstream->codecpar, vcodec_ctx);
if (res < 0)
throw av::av_error(res);
// open file
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
res = avio_open(&fmt_ctx->pb, "output.mkv", AVIO_FLAG_WRITE);
if (res < 0)
throw av::av_error(res);
}
// write format header
res = avformat_write_header(fmt_ctx, NULL);
if (res < 0)
throw av::av_error(res);
// encode loop
for (int i = 0; i < 360; i++) {
av::alloc_video_frame(vframe, vcodec_ctx);
// gen data and store to vframe
fill_rgb(vframe2, vframe, hue_c(i));
// set timing info
vframe->time_base = vcodec_ctx->time_base;
vframe->pts = i;
// send to encoder
avcodec_send_frame(vcodec_ctx, vframe);
while ((res = avcodec_receive_packet(vcodec_ctx, vpacket)) == 0) {
printf("DTS: %ld, PTS: %ld\n", vpacket->dts, vpacket->pts);
if ((res = av_interleaved_write_frame(fmt_ctx, vpacket)) < 0)
throw av::av_error(res);
}
if (res != AVERROR_EOF && res != AVERROR(EAGAIN))
return -1;
}
if ((res = av_write_trailer(fmt_ctx)) < 0)
throw av::av_error(res);
av_frame_free(&vframe);
av_frame_free(&vframe2);
av_packet_free(&vpacket);
avcodec_free_context(&vcodec_ctx);
avformat_free_context(fmt_ctx);
}
It looks like MKV with VP8 video stream forces the timebase to be 1/1000
.
According to the following answer, the 1/1000
is required and enforced by the WebM muxer, and was not something should be changed.
(It's hard to find references for that limitation - it could be FFmpeg specific limitation).
By definition, VP8 video codec in MKV container is equivalent to WebM container with VP8 video codec.
I assume that the above configuration uses "WebM muxer" and has the same 1/1000
timebase limitation as VP9 codec in WebM container.
Note:
I don't know if there is a way for forcing different timebase in the above configuration.
A simple solution for getting 60fps is setting the pts
to i*16
.
vframe->pts = (int64_t)i*16; //16 = 1000/60 (approximately).
Executing ffprobe -show_packets output.mkv
always outputs:
time_base=1/1000
When setting pts
to i*16
, FFprobe reports:
Stream #0:0: Video: vp8, yuv420p(progressive), 640x480, 60 fps, 60 tbr, 1k tbn, 1k tbc
MediaInfo tool also reports Frame rate : 60.000 FPS
.
I don't know if this is the best solution...