如何在不将单独帧图像写入磁盘的情况下,从由C++程序生成的几个图像中编码视频?

33

我正在编写一个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),导致磁盘过载,在每次执行后产生几十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();
根据我在相关主题上所读到的信息,x264 C++ API 可能能够实现这一点,但如上所述,我没有找到对我的具体问题令人满意的答案。我尝试学习并直接使用ffmpeg源代码,但由于它难以使用且编译问题,我不得不放弃这个可能性,作为一个非专业程序员(我只是把它当做一种爱好,不幸的是我不能浪费那么多时间去学习如此苛刻的东西)。
我想到的另一个可能的解决方案是在C++代码中找到一种调用ffmpeg二进制文件的方法,并以某种方式将每次迭代的图像数据(存储在B中)传输到编码器,让每个帧的添加(即不“关闭”视频文件进行写入)直到最后一帧,这样可以添加更多的帧,直到达到第N帧,在那里视频文件将被“关闭”。换句话说,通过C++程序调用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
然而,在“fopen”行(也就是第一个fopen调用)时,它仍然卡在循环中,如果我没有调用ffmpeg,那么这是很自然的,因为服务器(我的程序)将等待客户机程序连接到管道的“另一侧”,但这并不是情况。看起来他们无法通过管道连接,但我还没有能够找到进一步的文档以解决这个问题。有什么建议吗?

你尝试过使用命名管道吗?对于FFMPEG,它可以接受命名管道作为输入-i pipe:pipe_name。在msdn.microsoft上的示例 - David Tsulaia
谢谢您的建议。我了解了命名管道并尝试通过这种方式进行。在尝试这种方法后出现的新问题在我的新编辑中得到了揭示。 - ksb496
4个回答

38

经过一番努力,我终于学会了如何使用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数据以特定方式存储在AVFrames中。因此,为了说明问题,假设我们有一个指向形式为uint8_t [3] Matrix(int,int)的结构体的指针,其访问给定坐标(x,y)的像素的颜色值的方式是Matrix(x,y) -&gt; Red,Matrix(x,y) -&gt; Green和Matrix(x,y) -&gt; 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)相关联(后编码阶段)。在第一种情况下,似乎可以使用自定义引用单位分配帧PTS值(唯一的限制是如果要保持恒定的FPS,则必须等间距),因此我们可以像上面的代码中所做的那样采用帧编号。在第二种情况下,我们必须考虑以下参数:

  • 输出格式容器的时间基准,对于我们的情况是mp4(= 12800 Hz),其信息保存在stream->time_base中。
  • 视频的期望FPS。
  • 编码器是否生成B帧(在第二种情况下,帧的PTS和DTS值必须设置相同,但如果我们处于第一种情况下(如本例),则更为复杂)。有关更多参考,请参见此答案另一个相关问题。

关键在于,幸运的是,不需要为这些量的计算而苦苦挣扎,因为libav提供了一种函数来通过知道上述数据来计算与包关联的正确时间戳:

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

因为这些考虑,我最终能够生成一个正常的输出容器,并且基本上与使用命令行工具获得的压缩率相同,这是在更深入地调查格式头、尾以及时间戳如何正确设置之前仍存在的两个问题。


1
我还发现了一个单头MPEG编码器,可能会有用处:http://www.jonolick.com/home/mpeg-video-writer - Jaime Ivan Cervantes
请问您能否将源文件附加在这个回答中吗? - Liang Steve

8
感谢您出色的工作,@ksb496!
一个小的改进:
c=avcodec_alloc_context3(codec);

应该改写为:

c = stream->codec;

为了避免内存泄漏。

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


1

avcodec_encode_video2avcodec_encode_audio2似乎已经过时。当前版本(4.2)的FFmpeg具有新的API:avcodec_send_frameavcodec_receive_packet


0

感谢 ksb496 的帮助,我完成了这个任务,但在我的情况下,我需要更改一些代码才能按预期工作。我想也许可以帮助其他人,所以我决定分享(两年的延迟:D)。

我有一个由 directshow sample grabber 填充的 RGB 缓冲区,我需要从中获取视频。给定答案中的 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*),size 是以字节为单位的 data 缓冲区大小。它从左下角开始填充 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);

最后一件事,使用这种格式转换,我需要将swsContextRGB24改为BGR24,像这样:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接