Вопрос или проблема
Я следую официальному примеру mux, чтобы написать класс C++, который генерирует видео с фиксированной продолжительностью (5 секунд) и фиксированным fps (60). По какой-то причине продолжительность выходного видео составляет 3-4 секунды, хотя я вызываю функцию записи кадров 300 раз и устанавливаю fps равным 60.
Можете посмотреть код ниже и сказать, что я делаю не так?
#include "ffmpeg.h"
#include <iostream>
static int writeFrame(AVFormatContext *fmt_ctx, AVCodecContext *c,
AVStream *st, AVFrame *frame, AVPacket *pkt);
static void addStream(OutputStream *ost, AVFormatContext *formatContext,
const AVCodec **codec, enum AVCodecID codec_id,
int width, int height, int fps);
static AVFrame *allocFrame(enum AVPixelFormat pix_fmt, int width, int height);
static void openVideo(AVFormatContext *formatContext, const AVCodec *codec,
OutputStream *ost, AVDictionary *opt_arg);
static AVFrame *getVideoFrame(OutputStream *ost,
const std::vector<GLubyte>& pixels,
int duration);
static int writeVideoFrame(AVFormatContext *formatContext,
OutputStream *ost,
const std::vector<GLubyte>& pixels,
int duration);
static void closeStream(AVFormatContext *formatContext, OutputStream *ost);
static void fillRGBImage(AVFrame *frame, int width, int height,
const std::vector<GLubyte>& pixels);
#ifdef av_err2str
#undef av_err2str
#include <string>
av_always_inline std::string av_err2string(int errnum) {
char str[AV_ERROR_MAX_STRING_SIZE];
return av_make_error_string(str, AV_ERROR_MAX_STRING_SIZE, errnum);
}
#define av_err2str(err) av_err2string(err).c_str()
#endif // av_err2str
FFmpeg::FFmpeg(int width, int height, int fps, const char *fileName)
: videoStream{ 0 }
, formatContext{ nullptr } {
const AVOutputFormat *outputFormat;
const AVCodec *videoCodec{ nullptr };
AVDictionary *opt{ nullptr };
int ret{ 0 };
av_dict_set(&opt, "crf", "17", 0);
/* Выделите контекст выходных медиа. */
avformat_alloc_output_context2(&this->formatContext, nullptr, nullptr, fileName);
if (!this->formatContext) {
std::cout << "Не удалось определить формат вывода по расширению файла: используется MPEG." << std::endl;
avformat_alloc_output_context2(&this->formatContext, nullptr, "mpeg", fileName);
if (!formatContext)
exit(-14);
}
outputFormat = this->formatContext->oformat;
/* Добавьте видео поток с использованием кодеков по умолчанию
* и инициализируйте кодеки. */
if (outputFormat->video_codec == AV_CODEC_ID_NONE) {
std::cout << "Формат вывода не имеет кодека видео по умолчанию." << std::endl;
exit(-15);
}
addStream(
&this->videoStream,
this->formatContext,
&videoCodec,
outputFormat->video_codec,
width,
height,
fps
);
openVideo(this->formatContext, videoCodec, &this->videoStream, opt);
av_dump_format(this->formatContext, 0, fileName, 1);
/* откройте выходной файл, если это необходимо */
if (!(outputFormat->flags & AVFMT_NOFILE)) {
ret = avio_open(&this->formatContext->pb, fileName, AVIO_FLAG_WRITE);
if (ret < 0) {
std::cout << "Не удалось открыть '" << fileName << "': " << std::string{ av_err2str(ret) } << std::endl;
exit(-16);
}
}
/* Запишите заголовок потока, если есть. */
ret = avformat_write_header(this->formatContext, &opt);
if (ret < 0) {
std::cout << "Произошла ошибка при открытии выходного файла: " << av_err2str(ret) << std::endl;
exit(-17);
}
av_dict_free(&opt);
}
FFmpeg::~FFmpeg() {
if (this->formatContext) {
/* Закройте кодек. */
closeStream(this->formatContext, &this->videoStream);
if (!(this->formatContext->oformat->flags & AVFMT_NOFILE)) {
/* Закройте выходной файл. */
avio_closep(&this->formatContext->pb);
}
/* освободите поток */
avformat_free_context(this->formatContext);
}
}
void FFmpeg::Record(
const std::vector<GLubyte>& pixels,
unsigned frameIndex,
int duration,
bool isLastIndex
) {
static bool encodeVideo{ true };
if (encodeVideo)
encodeVideo = !writeVideoFrame(this->formatContext,
&this->videoStream,
pixels,
duration);
if (isLastIndex) {
av_write_trailer(this->formatContext);
encodeVideo = false;
}
}
int writeFrame(AVFormatContext *fmt_ctx, AVCodecContext *c,
AVStream *st, AVFrame *frame, AVPacket *pkt) {
int ret;
// отправьте кадр кодеру
ret = avcodec_send_frame(c, frame);
if (ret < 0) {
std::cout << "Ошибка при отправке кадра кодеру: " << av_err2str(ret) << std::endl;
exit(-2);
}
while (ret >= 0) {
ret = avcodec_receive_packet(c, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
else if (ret < 0) {
std::cout << "Ошибка кодирования кадра: " << av_err2str(ret) << std::endl;
exit(-3);
}
/* пересчитываем временные метки выходного пакета с кодека на временную базу потока */
av_packet_rescale_ts(pkt, c->time_base, st->time_base);
pkt->stream_index = st->index;
/* Записываем сжатый кадр в медиофайл. */
ret = av_interleaved_write_frame(fmt_ctx, pkt);
/* pkt теперь пуст (av_interleaved_write_frame() берет на себя владение
* его содержимым и сбрасывает pkt), так что дополнительная разметка не требуется.
* Это было бы иначе, если бы использовался av_write_frame(). */
if (ret < 0) {
std::cout << "Ошибка при записи выходного пакета: " << av_err2str(ret) << std::endl;
exit(-4);
}
}
return ret == AVERROR_EOF ? 1 : 0;
}
void addStream(OutputStream *ost, AVFormatContext *formatContext,
const AVCodec **codec, enum AVCodecID codec_id,
int width, int height, int fps) {
AVCodecContext *c;
int i;
/* найти кодировщик */
*codec = avcodec_find_encoder(codec_id);
if (!(*codec)) {
std::cout << "Не удалось найти кодировщик для " << avcodec_get_name(codec_id) << "." << std::endl;
exit(-5);
}
ost->tmpPkt = av_packet_alloc();
if (!ost->tmpPkt) {
std::cout << "Не удалось выделить AVPacket." << std::endl;
exit(-6);
}
ost->st = avformat_new_stream(formatContext, nullptr);
if (!ost->st) {
std::cout << "Не удалось выделить поток." << std::endl;
exit(-7);
}
ost->st->id = formatContext->nb_streams-1;
c = avcodec_alloc_context3(*codec);
if (!c) {
std::cout << "Не удалось выделить кодирующий контекст." << std::endl;
exit(-8);
}
ost->enc = c;
switch ((*codec)->type) {
case AVMEDIA_TYPE_VIDEO:
c->codec_id = codec_id;
c->bit_rate = 6000000;
/* Разрешение должно быть кратным двум. */
c->width = width;
c->height = height;
/* timebase: Это основная единица времени (в секундах), в которой
* представляются временные метки кадров. Для контента с фиксированным fps,
* timebase должен быть 1/частотой кадров, и временные метки должны быть
* идентичны 1. */
ost->st->time_base = { 1, fps };
c->time_base = ost->st->time_base;
c->framerate = { fps, 1 };
c->gop_size = 0; /* выдает один внутрикодируемый кадр не чаще одного раза за двенадцать кадров */
c->pix_fmt = AV_PIX_FMT_YUV420P;
if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO) {
/* просто для тестирования, мы также добавляем B-кадры */
c->max_b_frames = 2;
}
if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO) {
/* Необходимо, чтобы избежать использования макроблоков, в которых некоторые коэффициенты переполняются.
* Это не происходит с обычным видео, это происходит здесь, так как
* движение хроматической плоскости не соответствует плоскости яркости. */
c->mb_decision = 2;
}
break;
default:
break;
}
/* Некоторые форматы требуют, чтобы заголовки потоков были отдельными. */
if (formatContext->oformat->flags & AVFMT_GLOBALHEADER)
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
AVFrame *allocFrame(enum AVPixelFormat pix_fmt, int width, int height) {
AVFrame *frame{ av_frame_alloc() };
int ret;
if (!frame)
return nullptr;
frame->format = pix_fmt;
frame->width = width;
frame->height = height;
/* выделите буферы для данных кадра */
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
std::cout << "Не удалось выделить данные кадра." << std::endl;
exit(-8);
}
return frame;
}
void openVideo(AVFormatContext *formatContext, const AVCodec *codec,
OutputStream *ost, AVDictionary *opt_arg) {
int ret;
AVCodecContext *c{ ost->enc };
AVDictionary *opt{ nullptr };
av_dict_copy(&opt, opt_arg, 0);
/* откройте кодек */
ret = avcodec_open2(c, codec, &opt);
av_dict_free(&opt);
if (ret < 0) {
std::cout << "Не удалось открыть видео кодек: " << av_err2str(ret) << std::endl;
exit(-9);
}
/* Выделите и инициализируйте повторно используемый кадр. */
ost->frame = allocFrame(c->pix_fmt, c->width, c->height);
if (!ost->frame) {
std::cout << "Не удалось выделить видео кадр." << std::endl;
exit(-10);
}
/* Если формат вывода не RGB24, то также потребуется временное изображение RGB24.
* Оно затем конвертируется в требуемый
* выходной формат. */
ost->tmpFrame = nullptr;
if (c->pix_fmt != AV_PIX_FMT_RGB24) {
ost->tmpFrame = allocFrame(AV_PIX_FMT_RGB24, c->width, c->height);
if (!ost->tmpFrame) {
std::cout << "Не удалось выделить временный видео кадр." << std::endl;
exit(-11);
}
}
/* Скопируйте параметры потока в мультиплексор. */
ret = avcodec_parameters_from_context(ost->st->codecpar, c);
if (ret < 0) {
std::cout << "Не удалось скопировать параметры потока." << std::endl;
exit(-12);
}
}
AVFrame *getVideoFrame(OutputStream *ost,
const std::vector<GLubyte>& pixels,
int duration) {
AVCodecContext *c{ ost->enc };
/* проверьте, хотим ли мы сгенерировать больше кадров */
if (av_compare_ts(ost->nextPts, c->time_base,
duration, { 1, 1 }) > 0) {
return nullptr;
}
/* когда мы передаем кадр кодеру, он может сохранить на него ссылку
* внутри; убедитесь, что мы не перезаписываем его здесь */
if (av_frame_make_writable(ost->frame) < 0) {
std::cout << "Не удалось сделать кадр перезаписываемым." << std::endl;
exit(-12);
}
if (c->pix_fmt != AV_PIX_FMT_RGB24) {
/* так как мы генерируем только пиктограмму YUV420P, мы должны конвертировать ее
* в пиксельный формат кодека, если это необходимо */
if (!ost->swsContext) {
ost->swsContext = sws_getContext(c->width, c->height,
AV_PIX_FMT_RGB24,
c->width, c->height,
c->pix_fmt,
SWS_BICUBIC, nullptr, nullptr, nullptr);
if (!ost->swsContext) {
std::cout << "Не удалось инициализировать контекст конвертации." << std::endl;
exit(-13);
}
}
fillRGBImage(ost->tmpFrame, c->width, c->height, pixels);
sws_scale(ost->swsContext, (const uint8_t * const *) ost->tmpFrame->data,
ost->tmpFrame->linesize, 0, c->height, ost->frame->data,
ost->frame->linesize);
} else
fillRGBImage(ost->frame, c->width, c->height, pixels);
ost->frame->pts = ost->nextPts++;
return ost->frame;
}
int writeVideoFrame(AVFormatContext *formatContext,
OutputStream *ost,
const std::vector<GLubyte>& pixels,
int duration) {
return writeFrame(formatContext,
ost->enc,
ost->st,
getVideoFrame(ost, pixels, duration),
ost->tmpPkt);
}
void closeStream(AVFormatContext *formatContext, OutputStream *ost) {
avcodec_free_context(&ost->enc);
av_frame_free(&ost->frame);
av_frame_free(&ost->tmpFrame);
av_packet_free(&ost->tmpPkt);
sws_freeContext(ost->swsContext);
}
static void fillRGBImage(AVFrame *frame, int width, int height,
const std::vector<GLubyte>& pixels) {
// Скопируйте данные пикселей в кадр
int inputLineSize{ 3 * width }; // 3 байта на пиксель для RGB
for (int y{ 0 }; y < height; ++y) {
memcpy(frame->data[0] + y * frame->linesize[0],
pixels.data() + y * inputLineSize,
inputLineSize);
}
}
.
Ответ или решение
Чтобы создать видео фиксированной продолжительности (например, 5 секунд) и с фиксированной частотой кадров (например, 60 fps) с помощью библиотек FFmpeg на C++, вам необходимо внести несколько изменений и уточнений в вашем коде. Прежде всего, важно понимать, как правильно устанавливать временные метки (PTS) для кадров и как управлять их количеством, чтобы гарантировать нужную продолжительность.
1. Подсчет количества кадров
Для видео продолжительностью 5 секунд при 60 кадрах в секунду вам нужно сгенерировать ровно 300 кадров. Убедитесь, что ваш код вызывает функцию writeVideoFrame
300 раз.
2. Правильная установка PTS
Ваша текущая реализация, где PTS увеличивается на 1 в getVideoFrame
на каждом кадре, может быть причиной краткосрочной длительности. Вам нужно установить PTS с учетом времени, в течение которого кадры должны быть отображены. В вашем случае вычисление PTS должно выглядеть следующим образом:
ost->frame->pts = ost->nextPts++;
Это значение будет расширяться в зависимости от времени, за которое должно быть отображено заданное количество кадров. Убедитесь, что вы правильно вычисляете nextPts
с учетом time_base
.
3. Время в функции getVideoFrame
Убедитесь, что вы правильно настраиваете проверку в getVideoFrame
. Возможно, вам нужно изменить условие на следующее:
if (ost->nextPts >= (duration * c->framerate.num) / c->framerate.den) {
return nullptr; // Условие завершения генерирования кадров
}
Таким образом, вы будете сравнивать количество сгенерированных кадров с общим количеством кадров, которое равно продолжительности умноженной на частоту кадров.
4. Корректировка функции Record
Ваша реализация функции Record
должна убедиться, что все 300 кадров записаны, и только затем вызывать av_write_trailer
. Для этого ваш цикл записи кадров должен выглядеть следующим образом:
for (unsigned frameIndex = 0; frameIndex < 300; ++frameIndex) {
bool isLastIndex = (frameIndex == 299);
Record(pixels, frameIndex, 300, isLastIndex); // здесь 300 - это общее количество кадров
}
5. Пример кода создания видео
Ниже приведен более заключительный фрагмент кода, который демонстрирует, как объединить все вышеперечисленное. Убедитесь, что вы настроили правильные параметры для кодека, а также вызовы функций для записи и завершения:
FFmpeg ffmpeg(1920, 1080, 60, "output.mp4"); // открываем выходной файл
// Ваши пиксельные данные
std::vector<GLubyte> pixels(1920 * 1080 * 3); // Пример для RGB
// Заполняем pixels нужной информацией (например, черный экран)
for (auto& pixel : pixels) {
pixel = 0; // черный цвет
}
// Запись кадров
for (int i = 0; i < 300; ++i) {
ffmpeg.Record(pixels, i, 5, (i == 299)); // устанавливаем длительность
}
Заключение
Обратите внимание, правильная установка временных меток является ключевым фактором для достижения желаемой длительности. Не забудьте проверить, что используемая вами версия FFmpeg и соответствующие библиотеки поддерживают указанные вами параметры кодирования. С помощью указанных примеров и необходимых изменений, вы сможете создать видео с фиксированной продолжительностью и частотой кадров.