经过一番努力,我终于学会了如何使用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);
for (y=0; y<height; y++)
{
for (x=0; x<width; x++)
{
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);
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;
yuvpic->pts = i;
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);
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);
}
}
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);
if (!(fmt->flags & AVFMT_NOFILE))
avio_closep(oc->pb);
avcodec_close(stream->codec);
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)
因为这些考虑,我最终能够生成一个正常的输出容器,并且基本上与使用命令行工具获得的压缩率相同,这是在更深入地调查格式头、尾以及时间戳如何正确设置之前仍存在的两个问题。