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

Я пишу код на С++, где последовательность 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), что приводит к перегрузке диска, что создает циклы записи десятки GB после каждого исполнения.

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

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

Изменить 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

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

Ответ 1

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

Прежде всего, библиотека libx264 C была скомпилирована, а затем FFmpeg с параметрами configure --enable-gpl --enable-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, Matrix (x, y) → Green и Matrix (x, y) → Blue, чтобы получить соответственно красные, зеленые и синие значения координата (x, y). Первый аргумент означает горизонтальное положение слева направо по мере увеличения x, а второе для вертикального положения сверху вниз с увеличением y.

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

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) (post- этап кодирования). В первом случае похоже, что значения PTS фрейма могут быть назначены с использованием настраиваемой единицы ссылки (с единственным ограничением, что они должны быть равномерно распределены, если требуется постоянный FPS), поэтому можно взять, например, номер кадра, поскольку мы в приведенном выше коде. Во втором мы должны учитывать следующие параметры:

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

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

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

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