我正在编写一个 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。
为了避免这种情况,我一直在尝试查找有关将 vector B 中存储的每个图像直接解析到 x264 等编码器的文档(无需将中间图像文件写入磁盘)。尽管发现了一些有趣的话题,但没有一个能具体解决我真正想要的问题,因为其中许多涉及使用磁盘上现有图像文件执行编码器,而其他一些则为其他编程语言(如 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 中)传输到编码器,让加法每一帧(即不“关闭”要写入的视频文件)直到最后一帧,这样可以添加更多帧,直到到达第 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
但是,当 i=0 在“fopen”行(即第一个 fopen 调用)时,它仍然停留在循环中。如果我没有调用 ffmpeg,那很自然,因为服务器(我的程序)会等待客户端程序连接到管道的“另一端”,但事实并非如此。看起来它们无法以某种方式通过管道连接,但我无法找到进一步的文档来解决这个问题。有什么建议吗?
最佳答案
经过一番激烈的斗争,在学习了一些如何将 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);
上面注释的 vector 与我在问题中公开的 vector 具有相同的结构;但是,RGB 数据以特定方式存储在 AVFrames 上。因此,为了说明起见,让我们假设我们有一个指向 uint8_t[3] Matrix(int, int) 形式的结构的指针,它访问给定坐标 (x, y) 是 Matrix(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)(后-编码阶段)。在第一种情况下,看起来可以使用自定义的引用单位来分配帧 PTS 值(唯一的限制是,如果想要恒定 FPS,它们必须等间距),因此可以将帧号作为我们的例子在上面的代码中做了。在第二个中,我们必须考虑以下参数:
这里的关键是幸运的是,不必为这些数量的计算而苦恼,因为 libav 提供了一个函数,可以通过了解上述数据来计算与数据包相关的正确时间戳:
av_packet_rescale_ts(AVPacket *pkt, AVRational FPS, AVRational time_base)
多亏了这些考虑,我终于能够生成一个健全的输出容器,并且与使用命令行工具获得的压缩率基本相同,这是在更深入地研究格式头和尾如何以及如何正确设置时间戳。
关于c++ - 如何在不将单独的帧图像写入磁盘的情况下从 C++ 程序中生成的多个图像中编码视频?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34511312/
出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby中使用两个参数异步运行exe吗?我已经尝试过ruby命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何rubygems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除
我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server
这是在Ruby中设置默认值的常用方法:classQuietByDefaultdefinitialize(opts={})@verbose=opts[:verbose]endend这是一个容易落入的陷阱:classVerboseNoMatterWhatdefinitialize(opts={})@verbose=opts[:verbose]||trueendend正确的做法是:classVerboseByDefaultdefinitialize(opts={})@verbose=opts.include?(:verbose)?opts[:verbose]:trueendend编写Verb
鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信
我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"
我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re