ffmpeg超详细综合教程(二)——为直播流添加滤镜

在上一篇文章中,讲解了如何利用ffmpeg实现摄像头直播,本文将在此基础上,实现一个能够选择各类视频滤镜的摄像头直播示例。本文包含如下内容
一、AVFilter的基本介绍
二、如何利用ffmpeg命令行工具实现各类视频滤镜
三、如何利用libavfilter编程实如今摄像头直播流中加入各种不一样滤镜的功能
具备较强的综合性。

AVFilter的基本介绍

AVFilter的功能十分强大,能够实现对多媒体数据的各类处理,包括时间线编辑、视音频特效滤镜的添加或信号处理,还能够实现多路媒体流的合并或叠加,其丰富程度使人叹为观止。这里主要以视频滤镜为例进行介绍。使用AVFilter能够为单路视频添加单个或多个滤镜,也能够为多路视频分别添加不一样的滤镜而且在最后将多路视频合并为一路视频,AVFilter为实现这些功能定义了如下几个概念:
Filter:表明单个filter
FilterPad:表明一个filter的输入或输出端口,每一个filter均可以有多个输入和多个输出,只有输出pad的filter称为source,只有输入pad的filter称为sink
FilterLink:若一个filter的输出pad和另外一个filter的输入pad名字相同,即认为两个filter之间创建了link
FilterChain:表明一串相互链接的filters,除了source和sink外,要求每一个filter的输入输出pad都有对应的输出和输入pad
FilterGraph:FilterChain的集合
基本和DirectShow相似,也与视频后期调色软件中的节点等概念相似。具体来看,如下面的命令为例
[in]split[main][tmp];[tmp]crop=iw:ih/2,vflip[flip];[main][flip]overlay=0:H/2[out]

在该命令中,输入流[in]首先被分[split]为两个流[main]和[tmp],而后[tmp]流通过了剪切[crop]和翻转[vflip]两个滤镜后变为[flip],这时咱们将[flip]叠加[overlay]到最开始的[main]上造成最后的输出流[out],最后呈现出的是镜像的效果。下图清晰地表示了以上过程

咱们能够认为图中每个节点就是一个Filter,每个方括号所表明的就是FilterPad,能够看到split的输出pad中有一个叫tmp的,而crop的输入pad中也有一个tmp,由此在两者之间创建了link,固然input和output表明的就是source和sink,此外,图中有三条FilterChain,第一条由input和split组成,第二条由crop和vflip组成,第三条由overlay和output组成,整张图便是一个拥有三个FilterChain的FilterGraph。
上面的图是人工画出来的,也能够在代码中调用avfilter_graph_dump函数自动将FilterGraph画出来,以下

能够看到,多出来了一个scale滤镜,这是由ffmpeg自动添加的用于格式转换的滤镜。

在FFmpeg命令行工具中使用AVFilter

在命令行中使用AVFilter须要遵循专门的语法,简单来讲,就是每一个Filter之间以逗号分隔,每一个Filter本身的属性之间以冒号分隔,属性和Filter以等号相连,多个Filter组成一个FilterChain,每一个FilterChain之间以分号相隔。AVFilter在命令行工具中对应的是-vf或-af或-filter_complex,前两个分别对应于单路输入的视频滤镜和音频滤镜,最后的filter_complex则对应于有多路输入的状况。除了在FFMpeg命令行工具中使用外,在FFplay中一样也可使用AVFilter。其余一些关于单双引号、转义符号等更详细的语法参考Filter
Documentation
下面举几个例子
一、叠加水印
ffmpeg -i test.flv -vf movie=test.jpg[wm];[in][wm]overlay=5:5[out] out.flv
将test.jpg做为水印叠加到test.flv的坐标为(5,5)的位置上,效果以下

二、镜像
ffmpeg -i test.flv -vf crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left]pad=iw*2[a];[a][right]overlay=w out.flv
输入[in]和输出[out]能够省略不写,pad用于填充画面,效果以下

三、调整曲线
ffmpeg -i test.flv -vf curves=vintage out.flv
相似Photoshop里面的曲线调整,这里的vintage是ffmpeg自带的预设,实现复古画风,还能够直接加载其余的Photoshop预设文件并在其基础上加以调整,以下
ffmpeg -i test.flv -vf curves=psfile='test.acv':green='0.45/0.53' out.flv
其中的acv预设文件实现的是增强对比度,再次基础上调整绿色的显示效果,以上两个命令的最终效果以下


四、多路输入拼接
ffmpeg -i test1.mp4 -i test2.mp4 -i test3.mp4 -i test4.mp4 -filter_complex "[0:v]pad=iw*2:ih*2[a];[a][1:v]overlay=w[b];[b][2:v]overlay=0:h[c];[c][3:v]overlay=w:h" out.mp4
正如前面所说的,当有多个输入时,须要使用filter_complex,效果以下

经过以上几个例子,基本能够明白在命令行中使用AVFilter时须要遵循的语法。

使用libavfilter编程为直播流添加滤镜

要使用libavfilter,首先要注册相关组件
avfilter_register_all();
首先须要构造出一个完整可用的FilterGraph,须要用到输入流的解码参数,参见上一篇文章,以下
AVFilterContext *buffersink_ctx;//看名字好像AVFilterContext是什么很厉害的东西,但其实只要认为它是AVFilter的一个实例就OK了
AVFilterContext *buffersrc_ctx;
AVFilterGraph *filter_graph;
AVFilter *buffersrc=avfilter_get_by_name("buffer");//Filter的具体定义,只要是libavfilter中已注册的filter,就能够直接经过查询filter名字的方法得到其具体定义,所谓定义即filter的名称、功能描述、输入输出pad、相关回调函数等
AVFilter *buffersink=avfilter_get_by_name("buffersink");
AVFilterInOut *outputs = avfilter_inout_alloc();//AVFilterInOut对应buffer和buffersink这两个首尾端的filter的输入输出
AVFilterInOut *inputs = avfilter_inout_alloc();
filter_graph = avfilter_graph_alloc();

	/* buffer video source: the decoded frames from the decoder will be inserted here. */
	snprintf(args, sizeof(args),
		"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
		ifmt_ctx->streams[0]->codec->width, ifmt_ctx->streams[0]->codec->height, ifmt_ctx->streams[0]->codec->pix_fmt,
		ifmt_ctx->streams[0]->time_base.num, ifmt_ctx->streams[0]->time_base.den,
		ifmt_ctx->streams[0]->codec->sample_aspect_ratio.num, ifmt_ctx->streams[0]->codec->sample_aspect_ratio.den);

	ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
		args, NULL, filter_graph);//根据指定的Filter,这里就是buffer,构造对应的初始化参数args,两者结合便可建立Filter的示例,并放入filter_graph中
	if (ret < 0) {
		printf("Cannot create buffer source\n");
		return ret;
	}

	/* buffer video sink: to terminate the filter chain. */
	ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
		NULL, NULL, filter_graph);
	if (ret < 0) {
		printf("Cannot create buffer sink\n");
		return ret;
	}

	/* Endpoints for the filter graph. */
	outputs->name = av_strdup("in");//对应buffer这个filter的output
	outputs->filter_ctx = buffersrc_ctx;
	outputs->pad_idx = 0;
	outputs->next = NULL;

	inputs->name = av_strdup("out");//对应buffersink这个filter的input
	inputs->filter_ctx = buffersink_ctx;
	inputs->pad_idx = 0;
	inputs->next = NULL;

	if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr,
		&inputs, &outputs, NULL)) < 0)//filter_descr是一个filter命令,例如"overlay=iw:ih",该函数能够解析这个命令,而后自动完成FilterGraph中各个Filter之间的联接
		return ret;

	if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)//检查当前所构造的FilterGraph的完整性与可用性
		return ret;

	avfilter_inout_free(&inputs);
	avfilter_inout_free(&outputs);
上面介绍的是FilterGraph的构造方法之一,即根据filter命令使用avfilter_graph_parse_ptr自动进行构造,固然也能够由咱们本身将各个filter一一联接起来,以下,这里假设咱们已经有了buffersrc_ctx、 buffersink_ctx和一个filter_ctx
// connect inputs and outputs
    if (err >= 0) err = avfilter_link(buffersrc_ctx, 0, filter_ctx, 0);
    if (err >= 0) err = avfilter_link(filter_ctx, 0, buffersink_ctx, 0);
    if (err < 0) {
        av_log(NULL, AV_LOG_ERROR, "error connecting filters\n");
        return err;
    }
    err = avfilter_graph_config(filter_graph, NULL);
    if (err < 0) {
        av_log(NULL, AV_LOG_ERROR, "error configuring the filter graph\n");
        return err;
    }
    return 0;
不过在filter较多的状况下,仍是直接使用avfilter_graph_parse_ptr比较方便
在构造好FilterGraph以后,就能够开始使用了,使用流程也很简单,先将一个AVFrame帧推入FIlterGraph中,在将处理后的AVFrame从FilterGraph中拉出来便可,这里以上一篇文章的编解码核心模块的代码为例看一下实现过程。能够看到,是将解码获得的pFrame推入filter_graph,将处理后的数据写入picref中,他也是一个AVFrame。须要注意的是,这里依然要将picref转换为YUV420的帧以后再进行编码,一方面是由于咱们这里用的是摄像头数据,是RGB格式的,另外一方面,诸如curves这样的filter是在RGB空间进行处理的,最后获得的也是对应像素格式的帧,因此须要进行转换。其余部分基本和原来同样。
//start decode and encode
	int64_t start_time=av_gettime();
	while (av_read_frame(ifmt_ctx, dec_pkt) >= 0){	
		if (exit_thread)
			break;
		av_log(NULL, AV_LOG_DEBUG, "Going to reencode the frame\n");
		pframe = av_frame_alloc();
		if (!pframe) {
			ret = AVERROR(ENOMEM);
			return -1;
		}
		//av_packet_rescale_ts(dec_pkt, ifmt_ctx->streams[dec_pkt->stream_index]->time_base,
		//	ifmt_ctx->streams[dec_pkt->stream_index]->codec->time_base);
		ret = avcodec_decode_video2(ifmt_ctx->streams[dec_pkt->stream_index]->codec, pframe,
			&dec_got_frame, dec_pkt);
		if (ret < 0) {
			av_frame_free(&pframe);
			av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
			break;
		}
		if (dec_got_frame){
#if USEFILTER
				pframe->pts = av_frame_get_best_effort_timestamp(pframe);

				if (filter_change)
					apply_filters(ifmt_ctx);
				filter_change = 0;
				/* push the decoded frame into the filtergraph */
				if (av_buffersrc_add_frame(buffersrc_ctx, pframe) < 0) {
					printf("Error while feeding the filtergraph\n");
					break;
				}
				picref = av_frame_alloc();

				/* pull filtered pictures from the filtergraph */
				while (1) {
					ret = av_buffersink_get_frame_flags(buffersink_ctx, picref, 0);
					if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
						break;
					if (ret < 0)
						return ret;

					if (picref) {
						img_convert_ctx = sws_getContext(picref->width, picref->height, (AVPixelFormat)picref->format, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
						sws_scale(img_convert_ctx, (const uint8_t* const*)picref->data, picref->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
						sws_freeContext(img_convert_ctx);
						pFrameYUV->width = picref->width;
						pFrameYUV->height = picref->height;
						pFrameYUV->format = PIX_FMT_YUV420P;
#else
						sws_scale(img_convert_ctx, (const uint8_t* const*)pframe->data, pframe->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
						pFrameYUV->width = pframe->width;
						pFrameYUV->height = pframe->height;
						pFrameYUV->format = PIX_FMT_YUV420P;
#endif					
						enc_pkt.data = NULL;
						enc_pkt.size = 0;
						av_init_packet(&enc_pkt);
						ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);
						av_frame_free(&pframe);
						if (enc_got_frame == 1){
							//printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, enc_pkt.size);
							framecnt++;
							enc_pkt.stream_index = video_st->index;

							//Write PTS
							AVRational time_base = ofmt_ctx->streams[videoindex]->time_base;//{ 1, 1000 };
							AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;// { 50, 2 };
							AVRational time_base_q = { 1, AV_TIME_BASE };
							//Duration between 2 frames (us)
							int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));	//内部时间戳
							//Parameters
							//enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));
							enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);
							enc_pkt.dts = enc_pkt.pts;
							enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base); //(double)(calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));
							enc_pkt.pos = -1;

							//Delay
							int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
							int64_t now_time = av_gettime() - start_time;
							if (pts_time > now_time)
								av_usleep(pts_time - now_time);

							ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
							av_free_packet(&enc_pkt);
						}
#if USEFILTER
						av_frame_unref(picref);
					}
				}		
#endif
		}
		else {
			av_frame_free(&pframe);
		}
		av_free_packet(dec_pkt);
	}

这里咱们还能够实现一个按下不一样的数字键就添加不一样的滤镜的功能,以下
能够看到,首先写好一些要用的filter命令,而后在多线程的回调函数里监视用户的按键状况,根据不一样的按键使用对应的filter命令初始化filter_graph,这里“null”也是一个filter命令,用于将输入视频原样输出
#if USEFILTER
int filter_change = 1;
const char *filter_descr="null";
const char *filter_mirror = "crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right]; \
												[left]pad=iw*2[a];[a][right]overlay=w";
const char *filter_watermark = "movie=test.jpg[wm];[in][wm]overlay=5:5[out]";
const char *filter_negate = "negate[out]";
const char *filter_edge = "edgedetect[out]";
const char *filter_split4 = "scale=iw/2:ih/2[in_tmp];[in_tmp]split=4[in_1][in_2][in_3][in_4];[in_1]pad=iw*2:ih*2[a];[a][in_2]overlay=w[b];[b][in_3]overlay=0:h[d];[d][in_4]overlay=w:h[out]";
const char *filter_vintage = "curves=vintage";
typedef enum{
	FILTER_NULL =48,
	FILTER_MIRROR ,
	FILTER_WATERMATK,
	FILTER_NEGATE,
	FILTER_EDGE,
	FILTER_SPLIT4,
	FILTER_VINTAGE
}FILTERS;

AVFilterContext *buffersink_ctx;
AVFilterContext *buffersrc_ctx;
AVFilterGraph *filter_graph;
AVFilter *buffersrc;
AVFilter *buffersink;
AVFrame* picref;
#endif

DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{	
#if USEFILTER
	int ch = getchar();
	while (ch != '\n')
	{		
		switch (ch){
			case FILTER_NULL:
			{
				printf("\nnow using null filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = "null";
				getchar();
				ch = getchar();
				break;
			}
			case FILTER_MIRROR:
			{
				printf("\nnow using mirror filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = filter_mirror;
				getchar();
				ch = getchar();
				break;
			}
			case FILTER_WATERMATK:
			{
				printf("\nnow using watermark filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = filter_watermark;
				getchar();
				ch = getchar();
				break;
			}
			case FILTER_NEGATE:
			{
				printf("\nnow using negate filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = filter_negate;
				getchar();
				ch = getchar();
				break;
			}
			case FILTER_EDGE:
			{
				printf("\nnow using edge filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = filter_edge;
				getchar();
				ch = getchar();
				break;
			}
			case FILTER_SPLIT4:
			{
				printf("\nnow using split4 filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = filter_split4;
				getchar();
				ch = getchar();
				break;
			}
			case FILTER_VINTAGE:
			{
				printf("\nnow using vintage filter\nPress other numbers for other filters:");
				filter_change = 1;
				filter_descr = filter_vintage;
				getchar();
				ch = getchar();
				break;
			}
			default:
			{
				getchar();
				ch = getchar();
				break;
			}
		}
#else
	while ((getchar())!='\n')
	{
	;
#endif
	}
	exit_thread = 1;
	return 0;
}

除了在API层面调用AVFilter以外,还能够本身写一个FIlter,实现本身想要的功能,好比前面用到的反相功能,就是用255减去原来的像素数据值实现的,在后面的文章中会专门介绍如何本身编写一个Filter。
此外,针对多输入的Filter使用也是一个比较难的点,期待你们的交流。
本项目源代码 下载地址
github地址:https://github.com/zhanghuicuc/ffmpeg_camera_streamer