本节介绍FFmpeg在处理音视频文件时常见的处理流程,首先描述如何复制音视频编解码器中的参数信息,然后叙述如何创建并写入音视频文件,最后阐述如何使用滤镜加工音视频文件。
对于现成的音视频文件,其内部音视频的解码器藏在数据流的codecpar字段中,该字段为AVCodecParameters结构的指针类型,解码器编号就来自AVCodecParameters结构的codec_id字段。然而编解码器的AVCodec结构仅仅是规格定义,并不足以执行真正的编解码操作,还需调用avcodec_alloc_context3函数根据编解码器分配对应的实例AVCodecContext才行。解码器的实例分配之后,接着调用avcodec_parameters_to_context函数把数据流的编解码参数复制给解码器的实例,然后调用avcodec_open2函数才算成功打开实例。在关闭解码器的实例时,要先调用avcodec_close函数关闭实例,再调用avcodec_free_context函数释放实例资源。
总结一下,打开编解码器实例的完整流程为avcodec_alloc_context3→avcodec_parameters_to_context→avcodec_open2,关闭编解码器实例的完整流程为avcodec_close→avcodec_free_context。在编解码器实例打开之后,才能对数据包或者数据帧进行编解码操作。具体而言,就是数据包AVPacket经过解码生成数据帧AVFrame;反之,数据帧AVFrame经过编码生成数据包AVPacket。下面的FFmpeg代码片段演示了如何打开编解码器的实例(完整代码见chapter02/para.c)。
AVCodecContext *video_decode_ctx = NULL; video_decode_ctx = avcodec_alloc_context3(video_codec); // 分配解码器的实例 if (!video_decode_ctx) { av_log(NULL, AV_LOG_ERROR, "video_decode_ctx is null\n"); return -1; } // 把视频流中的编解码参数复制给解码器的实例 avcodec_parameters_to_context(video_decode_ctx, video_stream->codecpar); av_log(NULL, AV_LOG_INFO, "Success copy video parameters_to_context.\n"); ret = avcodec_open2(video_decode_ctx, video_codec, NULL); // 打开解码器的实例 av_log(NULL, AV_LOG_INFO, "Success open video codec.\n"); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't open video_decode_ctx.\n"); return -1; } avcodec_close(video_decode_ctx); // 关闭解码器的实例 avcodec_free_context(&video_decode_ctx); // 释放解码器的实例
其实只要调用了avcodec_parameters_to_context函数,就能获取音视频文件的详细编码参数,具体参数保存在AVCodecContext结构中,该结构的常见字段说明如下。
· codec_id:编解码器的编号。编号说明参见表2-5。
· codec_type:编解码器的类型。类型说明参见表2-2。
· width:视频画面的宽度。
· height:视频画面的高度。
· gop_size:每两个关键帧(I帧)间隔多少帧。
· max_b_frames:双向预测帧(B帧)的最大数量。
· pix_fmt:视频的像素格式。像素格式的定义来自AVPixelFormat枚举,详细的像素格式类型及其说明见表2-6。
表2-6 视频的像素格式及其说明
· profile:音频的规格类型,主要用于细分AAC音频的种类,详细的AAC种类定义及其说明见表2-7。
表2-7 AAC音频种类及其说明
· ch_layout:音频的声道布局,该字段为AVChannelLayout结构,声道数量为AVChannelLayout结构的nb_channels字段。声道数量的定义及其说明见表2-8。
表2-8 音频的声道数量及其说明
· sample_fmt:音频的采样格式。采样格式的定义来自AVSampleFormat枚举,详细的采样格式定义及其说明见表2-9。
表2-9 音频的采样格式及其说明
表2-9提到了交错模式和平面模式,它们之间的区别在于:平面模式把左右声道的音频数据分开存储,比如左声道的数据都保存在data[0]中(类似于LLLLL这样),右声道的数据都保存在data[1]中(类似于RRRRR这样);而交错模式把左右声道的音频数据都存储在data[0]中,数据分布形如LRLRLRLRLR这样。通常音频在播放时采用平面模式,方便扬声器设备取数,而保存文件时采用交错模式。
· sample_rate:音频的采样频率,单位为赫兹(次每秒)。
· frame_size:音频的帧大小,也叫采样个数,即每个音频帧采集的样本数量。
· bit_rate:码率,也叫比特率,单位为比特每秒。
· framerate:视频的帧率,该字段为AVRational结构。
· time_base:音视频的时间基,该字段为AVRational结构。
接下来把AVStream到AVCodec再到AVCodecContext的完整流程串起来,分别在视频流和音频流中寻找它们的解码器实例,并执行实例的打开和关闭操作。据此编写的FFmpeg示例代码如下(完整的代码见chapter02/para.c)。
接着执行下面的编译命令:
gcc para.c -o para -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm
编译完成后,执行以下命令启动测试程序,期望查看指定文件的编解码参数。
./para ../fuzhou.mp4
程序运行完毕,发现控制台输出如下日志信息:
Success open input_file ../fuzhou.mp4. video_codec name=h264 Success copy video parameters_to_context. video_decode_ctx width=1440 video_decode_ctx height=810 Success open video codec. audio_codec name=aac Success copy audio parameters_to_context. audio_decode_ctx profile=1 audio_decode_ctx nb_channels=2 Success open audio codec.
由日志信息可见,视频流和音频流的解码器实例都被找到并且成功打开,还发现目标文件的视频宽高为1440×810,并且音频规格为AAC-LC(profile=1,根据表2-7找到规格说明),声道类型为双声道(立体声)。
前面介绍的音视频处理都属于对文件的读操作,如果是写操作,那又是另一套流程。写入音视频文件的总体步骤说明如下:
01 调用avformat_alloc_output_context2函数分配音视频文件的封装实例。
02 调用avio_open函数打开音视频文件的输出流。
03 调用avformat_write_header函数写入音视频的文件头。
04 多次调用av_write_frame函数写入音视频的数据帧。
05 调用av_write_trailer函数写入音视频的文件尾。
06 调用avio_close函数关闭音视频文件的输出流。
07 调用avformat_free_context函数释放音视频文件的封装实例。
由此可见,FFmpeg对于音视频的处理操作很多是对称的,有分配就有释放,有打开就有关闭,有写文件头就有写文件尾。包含上述写文件步骤的FFmpeg示例代码片段如下。
AVFormatContext *out_fmt_ctx; // 输出文件的封装器实例 // 分配音视频文件的封装实例 int ret = avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, filename); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't alloc output_file %s.\n", filename); return -1; } // 打开输出流 ret = avio_open(&out_fmt_ctx->pb, filename, AVIO_FLAG_READ_WRITE); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't open output_file %s.\n", filename); return -1; } av_log(NULL, AV_LOG_INFO, "Success open output_file %s.\n", filename); ret = avformat_write_header(out_fmt_ctx, NULL); // 写文件头 if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "write file_header occur error %d.\n", ret); return -1; } av_log(NULL, AV_LOG_INFO, "Success write file_header.\n"); av_write_trailer(out_fmt_ctx); // 写文件尾 avio_close(&out_fmt_ctx->pb); // 关闭输出流 avformat_free_context(out_fmt_ctx); // 释放封装器的实例
不过,如果直接编译运行上述步骤的代码,会发现程序运行失败。究其原因,缘于音视频文件要求至少封装一路数据流,要么封装单路视频,要么封装单路音频,要么封装包括音频和视频在内的两路数据流。总之,不允许连一路数据流都没有。以下是音视频文件封装数据流的总体步骤说明。
01 调用avcodec_find_encoder函数查找指定编号的编码器。
02 调用avcodec_alloc_context3函数根据编码器分配对应的编码器实例。对于视频来说,还要设置编码器实例的width和height字段,指定视频画面的宽高。
03 调用avformat_new_stream函数,给输出文件创建采用指定编码器的数据流。
04 调用avcodec_parameters_from_context函数把编码器实例的参数复制给数据流。
上述的封装步骤虽然没有写入真实的视频帧,但是不影响测试程序的正常运行,反正已经创建了一路视频流,无非是视频内容为空而已。综合音视频文件的写入步骤,以及数据流的封装步骤,编写完整的FFmpeg代码如下(完整代码见chapter02/write.c)。
#include <stdio.h> // 之所以增加__cplusplus的宏定义,是为了同时兼容GCC编译器和G++编译器 #ifdef __cplusplus extern "C" { #endif #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/avutil.h> #ifdef __cplusplus }; #endif int main(int argc, char **argv) { const char *filename = "output.mp4"; if (argc > 1) { filename = argv[1]; } AVFormatContext *out_fmt_ctx; // 输出文件的封装器实例 // 分配音视频文件的封装实例 int ret = avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, filename); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't alloc output_file %s.\n", filename); return -1; } // 打开输出流 ret = avio_open(&out_fmt_ctx->pb, filename, AVIO_FLAG_READ_WRITE); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't open output_file %s.\n", filename); return -1; } av_log(NULL, AV_LOG_INFO, "Success open output_file %s.\n", filename); // 查找编码器 AVCodec *video_codec = (AVCodec*) avcodec_find_encoder(AV_CODEC_ID_H264); if (!video_codec) { av_log(NULL, AV_LOG_ERROR, "AV_CODEC_ID_H264 not found\n"); return -1; } AVCodecContext *video_encode_ctx = NULL; video_encode_ctx = avcodec_alloc_context3(video_codec); // 分配编解码器的实例 if (!video_encode_ctx) { av_log(NULL, AV_LOG_ERROR, "video_encode_ctx is null\n"); return -1; } video_encode_ctx->width = 320; // 视频画面的宽度 video_encode_ctx->height = 240; // 视频画面的高度 // 创建指定编码器的数据流 AVStream * video_stream = avformat_new_stream(out_fmt_ctx, video_codec); // 把编码器实例中的参数复制给数据流 avcodec_parameters_from_context(video_stream->codecpar, video_encode_ctx); video_stream->codecpar->codec_tag = 0; // 非特殊情况都填0 ret = avformat_write_header(out_fmt_ctx, NULL); // 写文件头 if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "write file_header occur error %d.\n", ret); return -1; } av_log(NULL, AV_LOG_INFO, "Success write file_header.\n"); av_write_trailer(out_fmt_ctx); // 写文件尾 avio_close(&out_fmt_ctx->pb); // 关闭输出流 avformat_free_context(out_fmt_ctx); // 释放封装器的实例 return 0; }
接着执行下面的编译命令:
gcc write.c -o write -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm
编译完成后,执行以下命令启动测试程序,期望生成一个全新的视频文件。
./write output.mp4
程序运行完毕,发现控制台输出如下日志信息,可知已成功写入了音视频文件。
Success open output_file output.mp4. Success write file_header.
音视频文件的修改操作与文本文件不同,修改音视频数据需要借助滤镜,由FFmpeg提供的各类滤镜实现相应的更改处理。使用滤镜加工音视频的总体步骤说明如下。
01 根据源文件的数据流、解码器实例、过滤字符串来初始化滤镜,得到输入滤镜的实例和输出滤镜的实例。
02 调用av_buffersrc_add_frame_flags函数把一个数据帧添加到输入滤镜的实例。
03 调用av_buffersink_get_frame函数从输出滤镜的实例获取加工后的数据帧。
04 把加工后的数据帧压缩编码后保存到目标文件中。
05 重复前面的第2~4步,直到源文件的所有数据帧都处理完毕。
关于av_buffersrc_add_frame_flags和av_buffersink_get_frame两个函数的用法留待以后详述,本小节只介绍第1步的滤镜初始化操作,初始化滤镜的具体步骤说明如下。
01 声明滤镜的各种实例资源,除输入滤镜的实例、输出滤镜的实例、滤镜图外,还要调用avfilter_get_by_name函数分别获取输入滤镜和输出滤镜,调用avfilter_inout_alloc函数各自分配滤镜的输入输出参数,调用avfilter_graph_alloc函数分配一个滤镜图。
02 拼接输入源的媒体参数信息字符串,以视频为例,参数字符串需要包括视频宽高、像素格式、时间基等。
03 调用avfilter_graph_create_filter函数,根据输入滤镜和第2步的参数字符串,创建输入滤镜的实例,并将其添加到现有的滤镜图中。
04 调用avfilter_graph_create_filter函数,根据输出滤镜创建输出滤镜的实例,并将其添加到现有的滤镜图中。
05 调用av_opt_set_int_list函数设置额外的选项参数,比如加工视频要给输出滤镜的实例设置像素格式。
06 设置滤镜的输入输出参数,给AVFilterInOut结构的filter_ctx字段填写输入滤镜的实例或者输出滤镜的实例。
07 调用avfilter_graph_parse_ptr函数,把采用过滤字符串描述的图形添加到滤镜图中,这个过滤字符串指定了滤镜的种类名称及其参数取值。
08 调用avfilter_graph_config函数检查过滤字符串的有效性,并配置滤镜图中的所有前后连接和图像格式。
09 调用avfilter_inout_free函数分别释放滤镜的输入参数和输出参数。
综合上述的滤镜初始化步骤说明,编写FFmpeg对滤镜的初始化函数代码如下(完整代码见chapter02/filter.c):
AVFilterContext *buffersrc_ctx = NULL; // 输入滤镜的实例 AVFilterContext *buffersink_ctx = NULL; // 输出滤镜的实例 AVFilterGraph *filter_graph = NULL; // 滤镜图 // 初始化滤镜(也称过滤器、滤波器)。第一个参数是视频流,第二个参数是解码器实例,第三个参数是过滤字符串 int init_filter(AVStream *video_stream, AVCodecContext *video_decode_ctx, const char *filters_desc) { int ret = 0; const AVFilter *buffersrc = avfilter_get_by_name("buffer"); // 获取输入滤镜 const AVFilter *buffersink = avfilter_get_by_name("buffersink"); // 获取输出滤镜 AVFilterInOut *inputs = avfilter_inout_alloc(); // 分配滤镜的输入输出参数 AVFilterInOut *outputs = avfilter_inout_alloc(); // 分配滤镜的输入输出参数 AVRational time_base = video_stream->time_base; enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE }; filter_graph = avfilter_graph_alloc(); // 分配一个滤镜图 if (!outputs || !inputs || !filter_graph) { ret = AVERROR(ENOMEM); return ret; } char args[512]; //临时字符串,存放输入源的媒体参数信息,比如视频宽高、像素格式等 snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", video_decode_ctx->width, video_decode_ctx->height, video_decode_ctx->pix_fmt, time_base.num, time_base.den, video_decode_ctx->sample_aspect_ratio.num, video_decode_ctx-> sample_aspect_ratio.den); av_log(NULL, AV_LOG_INFO, "args : %s\n", args); // 创建输入滤镜的实例,并将其添加到现有的滤镜图中 ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n"); return ret; } // 创建输出滤镜的实例,并将其添加到现有的滤镜图中 ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n"); return ret; } // 将二进制选项设置为整数列表,此处给输出滤镜的实例设置像素格式 ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n"); return ret; } // 设置滤镜的输入输出参数 outputs->name = av_strdup("in"); outputs->filter_ctx = buffersrc_ctx; outputs->pad_idx = 0; outputs->next = NULL; // 设置滤镜的输入输出参数 inputs->name = av_strdup("out"); inputs->filter_ctx = buffersink_ctx; inputs->pad_idx = 0; inputs->next = NULL; // 把采用过滤字符串描述的图形添加到滤镜图中 ret = avfilter_graph_parse_ptr(filter_graph, filters_desc, &inputs, &outputs, NULL); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot parse graph string\n"); return ret; } // 检查过滤字符串的有效性,并配置滤镜图中的所有前后连接和图像格式 ret = avfilter_graph_config(filter_graph, NULL); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot config filter graph\n"); return ret; } avfilter_inout_free(&inputs); // 释放滤镜的输入参数 avfilter_inout_free(&outputs); // 释放滤镜的输出参数 av_log(NULL, AV_LOG_INFO, "Success initialize filter.\n"); return ret; }
以上代码定义了一个名为init_filter的滤镜初始化函数,接着回到读取源文件的FFmpeg代码中,增加调用init_filter函数即可正常初始化滤镜。滤镜使用结束后,要记得调用avfilter_free函数分别释放输入滤镜的实例和输出滤镜的实例,还要调用avfilter_graph_free函数释放滤镜图资源。下面是针对视频流初始化视频滤镜的FFmpeg代码片段:
// 找到视频流的索引 int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (video_index >= 0) { AVStream *video_stream = fmt_ctx->streams[video_index]; enum AVCodecID video_codec_id = video_stream->codecpar->codec_id; // 查找视频解码器 AVCodec *video_codec = (AVCodec*) avcodec_find_decoder(video_codec_id); if (!video_codec) { av_log(NULL, AV_LOG_INFO, "video_codec not found\n"); return -1; } av_log(NULL, AV_LOG_INFO, "video_codec name=%s\n", video_codec->name); AVCodecContext *video_decode_ctx = NULL; // 视频解码器的实例 video_decode_ctx = avcodec_alloc_context3(video_codec); // 分配解码器的实例 if (!video_decode_ctx) { av_log(NULL, AV_LOG_INFO, "video_decode_ctx is null\n"); return -1; } // 把视频流中的编解码器参数复制给解码器的实例 avcodec_parameters_to_context(video_decode_ctx, video_stream->codecpar); av_log(NULL, AV_LOG_INFO, "Success copy video parameters_to_context.\n"); ret = avcodec_open2(video_decode_ctx, video_codec, NULL); // 打开解码器的实例 av_log(NULL, AV_LOG_INFO, "Success open video codec.\n"); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't open video_decode_ctx.\n"); return -1; } // 初始化滤镜 init_filter(video_stream, video_decode_ctx, "fps=25"); avcodec_close(video_decode_ctx); // 关闭解码器的实例 avcodec_free_context(&video_decode_ctx); // 释放解码器的实例 avfilter_free(buffersrc_ctx); // 释放输入滤镜的实例 avfilter_free(buffersink_ctx); // 释放输出滤镜的实例 avfilter_graph_free(&filter_graph); // 释放滤镜图资源 }
接着执行下面的编译命令:
gcc filter.c -o filter -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm
编译完成后,执行以下命令启动测试程序,期望初始化视频滤镜。
./filter ../fuzhou.mp4
程序运行完毕,发现控制台输出如下日志信息,可知已成功初始化视频滤镜。
Success open input_file ../fuzhou.mp4. video_codec name=h264 Success copy video parameters_to_context. Success open video codec. args : video_size=1440x810:pix_fmt=0:time_base=1/12800:pixel_aspect=1/1 Success initialize filter.
当然,这里的示例程序仅仅演示了如何初始化滤镜,并没有真正加工视频文件,也没将加工结果另存为新的文件,有关滤镜的加工处理操作会在第6章详细介绍。