Как кодировать видео из нескольких образов, сгенерированных в программе на C++, не записывая отдельные образы кадров на диск?

Я пишу код на C++, где последовательность из N различных кадров генерируется после выполнения некоторых операций, реализованных в нем. После завершения каждого кадра я записываю его на диск как IMG_%d.png, и, наконец, я кодирую их в видео через ffmpeg, используя кодек x264.

обобщенный псевдокод основной части программы следующий:

std::vector<int> B(width*height*3);
for (i=0; i<N; i++)
{
  // void generateframe(std::vector<int> &, int)
  generateframe(B, i); // Returns different images for different i values.
  sprintf(s, "IMG_%d.png", i+1);
  WriteToDisk(B, s); // void WriteToDisk(std::vector<int>, char[])
}

проблема этой реализации заключается в том, что количество желаемых кадров, N, обычно высокий (n~100000), а также разрешение изображений (1920x1080), что приводит к перегрузке диска, производя циклы записи десятков ГБ после каждого выполнения.

чтобы избежать этого, я пытался найти документацию о разборе непосредственно каждого изображения, хранящегося в векторе B, на кодировщик, такой как x264 (без необходимости записывать промежуточные файлы изображений на диск). Хотя некоторые интересные темы были найдены, ни один из них не решил конкретно, что именно я хочу, как многие из них касаются исполнения энкодера с существующими файлами изображений на диске, в то время как другие решения для других языков программирования, таких как Python (здесь вы можете найти полностью удовлетворительное решение для этой платформы).

псевдокод того, что я хотел бы получить, похож на это:

std::vector<int> B(width*height*3);
video_file=open_video("Generated_Video.mp4", ...[encoder options]...);
for (i=0; i<N; i++)
{
  generateframe(B, i+1);
  add_frame(video_file, B);
}
video_file.close();

согласно тому, что я прочитал по связанным темам, API x264 C++ может это сделать, но, как указано выше я не нашел удовлетворительного ответа на свой конкретный вопрос. Я попытался изучить и использовать непосредственно исходный код ffmpeg, но как его низкая простота использования, так и проблемы компиляции заставили меня отказаться от этой возможности как простой непрофессиональный программист (я воспринимаю это как хобби, и, к сожалению, я не могу тратить столько времени на изучение чего-то столь требовательного).

еще одно возможное решение, которое пришло мне на ум, - найти способ вызвать двоичный файл ffmpeg в C++ код, и каким-то образом удается перенести данные изображения каждой итерации (хранящиеся в B) в кодер, позволяя добавлять каждый кадр (то есть не "закрывать" видеофайл для записи) до последнего кадра, чтобы можно было добавить больше кадров до достижения N-го, где видеофайл будет "закрыт". Другими словами, вызовите ffmpeg.exe через программу C++ для записи первого кадра в видео, но заставьте кодировщик "ждать" больше кадров. Затем снова вызовите ffmpeg, чтобы добавить второй кадр и сделайте кодировщик "ждать" снова для Больше кадров, и так далее до достижения последнего кадра, где видео будет закончено. Однако я не знаю, как действовать дальше и возможно ли это на самом деле.

Edit 1:

как было предложено в ответах, я документировал именованные каналы и пытался использовать их в своем коде. Прежде всего, следует отметить, что я работаю с Cygwin, поэтому мои именованные каналы создаются так, как они были бы созданы под Linux. Этот модифицированный псевдокод, который я использовал (включая соответствующие системные библиотеки), следующий:

FILE *fd;
mkfifo("myfifo", 0666);

for (i=0; i<N; i++)
{
  fd=fopen("myfifo", "wb");
  generateframe(B, i+1);
  WriteToPipe(B, fd); // void WriteToPipe(std::vector<int>, FILE *&fd)
  fflush(fd);
  fd=fclose("myfifo");
}
unlink("myfifo");

WriteToPipe-это небольшая модификация предыдущей функции WriteToFile, где я убедился, что буфер записи для отправки данных изображения достаточно мал, чтобы соответствовать ограничениям буферизации канала.

затем я компилирую и пишу следующую команду в терминале Cygwin:

./myprogram | ffmpeg -i pipe:myfifo -c:v libx264 -preset slow -crf 20 Video.mp4

однако он остается застрявшим в цикле, когда i=0 в строке " fopen (то есть, первый вызов fopen). Если бы я не вызвал ffmpeg, это было бы естественно, так как сервер (моя программа) ожидал бы подключения клиентской программы к "другой стороне" канала, но это не так. Похоже, что они не могут быть подключены через трубу каким-то образом, но я не смог найти дополнительную документацию для преодоления этой проблемы. Есть предложения?

3 ответов


после некоторой напряженной борьбы мне, наконец, удалось заставить его работать, узнав немного, как использовать API ffmpeg и libx264 C для моей конкретной цели, благодаря полезной информации, которую некоторые пользователи предоставили на этом сайте и некоторые другие, а также некоторые примеры документации FFmpeg. Для иллюстрации ниже будут представлены подробности.

прежде всего, была скомпилирована библиотека libx264 C, а затем ffmpeg с параметрами configure --включение-ГПЛ --включить-поддержкой libx264. Теперь перейдем к кодированию. Соответствующая часть кода, которая достигла запрошенной цели, является следующей:

включает в себя:

#include <stdint.h>
extern "C"{
#include <x264.h>
#include <libswscale/swscale.h>
#include <libavcodec/avcodec.h>
#include <libavutil/mathematics.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
}

LDFLAGS на Makefile:

-lx264 -lswscale -lavutil -lavformat -lavcodec

внутренний код (для простоты проверки ошибок будут опущены, а объявления переменных будут сделаны при необходимости вместо начала для лучшего понимания):

av_register_all(); // Loads the whole database of available codecs and formats.

struct SwsContext* convertCtx = sws_getContext(width, height, AV_PIX_FMT_RGB24, width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL); // Preparing to convert my generated RGB images to YUV frames.

// Preparing the data concerning the format and codec in order to write properly the header, frame data and end of file.
char *fmtext="mp4";
char *filename;
sprintf(filename, "GeneratedVideo.%s", fmtext);
AVOutputFormat * fmt = av_guess_format(fmtext, NULL, NULL);
AVFormatContext *oc = NULL;
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
AVStream * stream = avformat_new_stream(oc, 0);
AVCodec *codec=NULL;
AVCodecContext *c= NULL;
int ret;

codec = avcodec_find_encoder_by_name("libx264");

// Setting up the codec:
av_dict_set( &opt, "preset", "slow", 0 );
av_dict_set( &opt, "crf", "20", 0 );
avcodec_get_context_defaults3(stream->codec, codec);
c=avcodec_alloc_context3(codec);
c->width = width;
c->height = height;
c->pix_fmt = AV_PIX_FMT_YUV420P;

// Setting up the format, its stream(s), linking with the codec(s) and write the header:
if (oc->oformat->flags & AVFMT_GLOBALHEADER) // Some formats require a global header.
    c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
avcodec_open2( c, codec, &opt );
av_dict_free(&opt);
stream->time_base=(AVRational){1, 25};
stream->codec=c; // Once the codec is set up, we need to let the container know which codec are the streams using, in this case the only (video) stream.
av_dump_format(oc, 0, filename, 1);
avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
ret=avformat_write_header(oc, &opt);
av_dict_free(&opt); 

// Preparing the containers of the frame data:
AVFrame *rgbpic, *yuvpic;

// Allocating memory for each RGB frame, which will be lately converted to YUV:
rgbpic=av_frame_alloc();
rgbpic->format=AV_PIX_FMT_RGB24;
rgbpic->width=width;
rgbpic->height=height;
ret=av_frame_get_buffer(rgbpic, 1);

// Allocating memory for each conversion output YUV frame:
yuvpic=av_frame_alloc();
yuvpic->format=AV_PIX_FMT_YUV420P;
yuvpic->width=width;
yuvpic->height=height;
ret=av_frame_get_buffer(yuvpic, 1);

// After the format, code and general frame data is set, we write the video in the frame generation loop:
// std::vector<uint8_t> B(width*height*3);

вышеуказанный прокомментированный Вектор имеет та же структура, что и в моем вопросе; однако данные RGB хранятся на AVFrames определенным образом. Поэтому для удобства изложения предположим, что вместо этого у нас есть указатель на структуру вида uint8_t[3] Matrix(int, int), способ доступа к цветовым значениям пикселей для данной координаты (x, y) - Матрица (x, y)- > Red, Матрица (x, y) - >Green и Матрица (x, y) - >Blue, чтобы получить соответственно красный, зеленые и синие значения координаты (x, y). Этот первый аргумент обозначает горизонтальное положение слева направо по мере увеличения x, а второй-вертикальное положение сверху вниз по мере увеличения Y.

будучи сказанным,на петли для передачи данных, кодирования и записи каждого кадра будет следующим:

Matrix B(width, height);
int got_output;
AVPacket pkt;
for (i=0; i<N; i++)
{
    generateframe(B, i); // This one is the function that generates a different frame for each i.
    // The AVFrame data will be stored as RGBRGBRGB... row-wise, from left to right and from top to bottom, hence we have to proceed as follows:
    for (y=0; y<height; y++)
    {
        for (x=0; x<width; x++)
        {
            // rgbpic->linesize[0] is equal to width.
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x]=B(x, y)->Red;
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x+1]=B(x, y)->Green;
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x+2]=B(x, y)->Blue;
        }
    }
    sws_scale(convertCtx, rgbpic->data, rgbpic->linesize, 0, height, yuvpic->data, yuvpic->linesize); // Not actually scaling anything, but just converting the RGB data to YUV and store it in yuvpic.
    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;
    yuvpic->pts = i; // The PTS of the frame are just in a reference unit, unrelated to the format we are using. We set them, for instance, as the corresponding frame number.
    ret=avcodec_encode_video2(c, &pkt, yuvpic, &got_output);
    if (got_output)
    {
        fflush(stdout);
        av_packet_rescale_ts(&pkt, (AVRational){1, 25}, stream->time_base); // We set the packet PTS and DTS taking in the account our FPS (second argument) and the time base that our selected format uses (third argument).
        pkt.stream_index = stream->index;
        printf("Write frame %6d (size=%6d)\n", i, pkt.size);
        av_interleaved_write_frame(oc, &pkt); // Write the encoded frame to the mp4 file.
        av_packet_unref(&pkt);
    }
}
// Writing the delayed frames:
for (got_output = 1; got_output; i++) {
    ret = avcodec_encode_video2(c, &pkt, NULL, &got_output);
    if (got_output) {
        fflush(stdout);
        av_packet_rescale_ts(&pkt, (AVRational){1, 25}, stream->time_base);
        pkt.stream_index = stream->index;
        printf("Write frame %6d (size=%6d)\n", i, pkt.size);
        av_interleaved_write_frame(oc, &pkt);
        av_packet_unref(&pkt);
    }
}
av_write_trailer(oc); // Writing the end of the file.
if (!(fmt->flags & AVFMT_NOFILE))
    avio_closep(oc->pb); // Closing the file.
avcodec_close(stream->codec);
// Freeing all the allocated memory:
sws_freeContext(convertCtx);
av_frame_free(&rgbpic);
av_frame_free(&yuvpic);
avformat_free_context(oc);

побочные Примечания:

для дальнейшего использования, поскольку имеющаяся в Сети информация о временных метках (PTS/DTS) выглядит так далее я объясню, как мне удалось решить проблемы, установив правильные значения. Неправильная установка этих значений привела к тому, что размер вывода был намного больше, чем тот, который был получен с помощью инструмента двоичной командной строки ffmpeg, потому что данные кадра избыточно записывались через меньшие интервалы времени, чем фактически установленные FPS.

прежде всего, следует отметить, что при кодировании существует два вида меток времени: один связанный с кадром (PTS) (этап предварительного кодирования) и два связанных с пакетом (PTS и DTS) (этап после кодирования). В первом случае, похоже, что значения Pts кадра могут быть назначены с помощью пользовательской единицы отсчета (с единственным ограничением, что они должны быть одинаково разнесены, если требуется постоянный FPS), поэтому можно взять, например, номер кадра, как мы сделали в приведенном выше коде. Во втором случае мы должны учитывать следующие параметры:

  • время база контейнера выходного формата, в нашем случае mp4 (=12800 Гц), информация о котором хранится в stream->time_base.
  • желаемый FPS видео.
  • если кодировщик генерирует B-кадры или нет (во втором случае значения PTS и DTS для кадра должны быть установлены одинаковыми, но это сложнее, если мы находимся в первом случае, как в этом примере). Смотрите это ответ к другому связанному вопросу для получения дополнительных ссылок.

ключ вот что, к счастью, не нужно бороться с вычислением этих величин, поскольку libav предоставляет функцию для вычисления правильных временных меток, связанных с пакетом, зная вышеупомянутые данные:

av_packet_rescale_ts(AVPacket *pkt, AVRational FPS, AVRational time_base)

благодаря этим соображениям я, наконец, смог создать нормальный выходной контейнер и, по существу, ту же скорость сжатия, что и при использовании инструмента командной строки, которые были двумя оставшимися проблемами, прежде чем исследовать более глубоко, как заголовок и трейлер формата и как правильно установлены метки времени.


благодаря ksb496 мне удалось выполнить эту задачу, но в моем случае мне нужно изменить некоторые коды для работы, как ожидалось. Я подумал, может быть, это может помочь другим, поэтому я решил поделиться (с двухлетней задержкой :D).

Я RGB буфер, заполненный directshow sample grabber, из которого мне нужно было взять видео. RGB до YUV преобразование из данного ответа не сделало работу для меня. Я сделал это так:

int stride = m_width * 3;
int index = 0;
for (int y = 0; y < m_height; y++) {
    for (int x = 0; x < stride; x++) {
        int j = (size - ((y + 1)*stride)) + x;
        m_rgbpic->data[0][j] = data[index];
        ++index;
    }
}

data переменная вот моя RGB буфера (просто BYTE*) и size и data размер буфера в байтах. Это начало заполнения RGB AVFrame снизу слева направо.

другое дело, что у моей версии FFMPEG не было


Спасибо за отличную работу, @ksb496 !

одно небольшое улучшение:

c=avcodec_alloc_context3(codec);

должно быть лучше написать так:

c = stream->codec;

чтобы избежать утечки памяти.

Если вы не возражаете, я загрузил полную готовую к развертыванию библиотеку на GitHub:https://github.com/apc-llc/moviemaker-cpp.git