如何在不将单独的帧图像写入磁盘的情况下对 C++ 程序中生成的多个图像进行视频编码?

How to encode a video from several images generated in a C++ program without writing the separate frame images to disk?

我正在编写一个 C++ 代码,其中在执行其中实现的一些操作后生成 N 个不同帧的序列。每一帧完成后,我将其作为IMG_%d.png写入磁盘,最后使用x264编解码器通过ffmpeg将它们编码为视频。

程序主体部分的总结伪代码如下:

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 等编码器(无需将中间图像文件写入磁盘)的文档。尽管发现了一些有趣的主题,但其中 none 解决了我真正想要解决的问题,因为其中许多涉及使用磁盘上现有图像文件执行编码器,而其他一些则为其他编程语言提供解决方案,例如Python(here您可以找到适合该平台的完全满意的解决方案)。

我想得到的伪代码是这样的:

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();

根据我阅读的相关主题,x264 C++ API 可能可以做到这一点,但是,如上所述,我没有为我的具体问题找到满意的答案。我尝试直接学习和使用 ffmpeg 源代码,但它的低易用性和编译问题迫使我放弃这种可能性,因为我只是一个非专业程序员(我把它当作一种爱好,不幸的是我不能浪费这么多时间学习如此苛刻的东西)。

我想到的另一种可能的解决方案是找到一种在C++代码中调用ffmpeg二进制文件的方法,并以某种方式设法将每次迭代的图像数据(存储在B中)传输到编码器,让每一帧的添加(即不是"closing"要写入的视频文件)直到最后一帧,这样就可以添加更多的帧直到到达第N个,这里的视频文件将是"closed".换句话说,通过 C++ 程序调用 ffmpeg.exe 将第一帧写入视频,但让编码器 "wait" 用于更多帧。然后再次调用 ffmpeg 以添加第二帧并再次使编码器 "wait" 以获得更多帧,依此类推直到到达最后一帧,视频将在此处完成。但是,我不知道如何进行,也不知道是否真的可行。

编辑 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

然而,当 "fopen" 行的 i=0 时(即第一个 fopen 调用),它仍然停留在循环中。如果我没有调用 ffmpeg,那将很自然,因为服务器(我的程序)会等待客户端程序连接到管道的 "other side",但事实并非如此。看起来它们无法以某种方式通过管道连接,但我无法找到更多文档来解决此问题。有什么建议吗?

经过一番激烈的斗争,在学习了一些如何将 FFmpeg 和 libx264 C API 用于我的特定目的之后,我终于设法让它工作了,这要感谢一些用户在这个网站和其他网站上提供的有用信息,以及一些 FFmpeg 的文档示例。为了便于说明,接下来会详细介绍。

首先,编译了 libx264 C 库,然后是带有配置选项 --enable-gpl --enable-libx264 的 FFmpeg。现在让我们开始编码。实现请求目的的代码的相关部分如下:

包括:

#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>
}

Makefile 上的 LDFLAGS:

-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 数据以特定方式存储在 AVFrame 中。因此,为了便于说明,让我们假设我们有一个指向形式为 uint8_t[3] Matrix(int, int) 的结构的指针,其访问给定像素颜色值的方式坐标(x, y)为Matrix(x, y)->Red, Matrix(x, y)->Green and 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)(pre-encoding阶段),两种与数据包相关(PTS和DTS) ) (post-encoding 阶段)。在第一种情况下,看起来可以使用自定义参考单位分配帧 PTS 值(唯一的限制是如果想要恒定的 FPS,它们必须等距),因此我们可以将帧号作为例子在上面的代码中做了。在第二个中,我们必须考虑以下参数:

  • 输出格式容器的时基,在我们的例子中是 mp4 (=12800 Hz),其信息保存在流中->time_base.
  • 视频所需的 FPS。
  • 编码器是否生成B-frames(在第二种情况下帧的PTS和DTS值必须设置相同,但如果我们在第一种情况下会更复杂,如这个例子)。请参阅此 answer 到另一个相关问题以获取更多参考。

这里的关键是,幸运的是,不必为计算这些量而苦苦挣扎,因为 libav 提供了一个函数,可以通过了解上述数据来计算与数据包关联的正确时间戳:

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

由于这些考虑,我终于能够生成一个合理的输出容器和与使用命令行工具获得的压缩率基本相同的压缩率,这是在更深入地研究格式头和预告片以及如何正确设置时间戳。

感谢 ksb496,我设法完成了这项任务,但就我而言,我需要更改一些代码才能按预期工作。我想也许它可以帮助其他人所以我决定分享(延迟两年 :D)。

我有一个 RGB 缓冲区,由 directshow 样本采集器填充,我需要从中获取视频。从给定答案 RGBYUV 的转换对我没有帮助。我是这样做的:

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*),sizedata 缓冲区大小(以字节为单位)。从左下角到右上角开始填充 RGB AVFrame

另一件事是我的 FFMPEG 版本没有 av_packet_rescale_ts 功能。它是最新版本,但 FFMPEG 文档并没有说此功能在任何地方都已弃用,我想这可能仅适用于 windows。无论如何,我使用 av_rescale_q 代替它做同样的工作。像这样:

AVPacket pkt;
pkt.pts = av_rescale_q(pkt.pts, { 1, 25 }, m_stream->time_base);

最后一件事,使用这种格式转换,我需要将我的 swsContext 更改为 BGR24 而不是像这样的 RGB24 :

m_convert_ctx = sws_getContext(width, height, AV_PIX_FMT_BGR24, width, height,
        AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);

感谢@ksb496 的出色工作!

一个小改进:

c=avcodec_alloc_context3(codec);

最好写成:

c = stream->codec;

以避免内存泄漏。

如果您不介意,我已经将完整的准备部署库上传到 GitHub:https://github.com/apc-llc/moviemaker-cpp.git

avcodec_encode_video2 & avcodec_encode_audio2 似乎已被弃用。当前版本 (4.2) 的 FFmpeg 有新的 API: avcodec_send_frame & avcodec_receive_packet.