Как сгенерировать фиксированную продолжительность и частоту кадров для видео с помощью библиотек FFmpeg на C++?

Вопрос или проблема

Я следую официальному примеру 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 и соответствующие библиотеки поддерживают указанные вами параметры кодирования. С помощью указанных примеров и необходимых изменений, вы сможете создать видео с фиксированной продолжительностью и частотой кадров.

Оцените материал
Добавить комментарий

Капча загружается...