FFmpeg 视频添加水印图片

最近学习FFmpeg编程开发,想写个视频添加水印图片的demo(未对音频或字幕进行处理),代码编写中遇见很多问题,在这里进行做一个笔记来,易于自己记忆和理解。期间在网上找demo,发现很多都是ffmpeg3版本的一些demo,ffmpeg4有很大的改变,有很多方法不适用,因此写篇文章给初学者一些细微的帮助,也易于自己巩固,避免犯类似的错误。

一、总结一下编码过程:

1.初始化化封装格式上下文  并打开文件

avformat_alloc_context / avformat_alloc_output_context2

2.创建输出码流

avformat_new_stream

3.创建编码器(根据输入编码类型)

avcodec_find_encoder  

4.创建视频编码器上下文 并设置参数

avcodec_alloc_context3  

5.打开编码器

avcodec_open2

6.把编码器信息写入流中

avcodec_parameters_from_context

7.创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源

avio_open

8.写入头文件信息

avformat_write_header

9.初始化过滤器 并过滤

init_filter av_buffersrc_add_frame_flags av_frame_make_writable

10.将编码后的视频压缩数据写入文件中

①设置帧类型②发送过滤的frame,接收packet③更新编码帧中流序号,并进行时间基转换④压缩数据写入文件中

11.写入文件尾部信息

av_write_trailer

12.释放资源

二、代码片段

1.输入文件的解码以及编码

int FfmpegTest::open_input_file(const char *filename)
{
	int ret;

	// 1. 打开视频文件:读取文件头,将文件格式信息存储在ifmt_ctx中
	if ((ret = avformat_open_input(&ifmt_ctx, filename, NULL, NULL)) < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
		return ret;
	}
	// 2. 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入ifmt_ctx.streams
	//    ifmt_ctx.streams是一个指针数组,数组大小是ifmt_ctx.nb_streams
	if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
		return ret;
	}

	// 每路音频流/视频流一个AVCodecContext
	ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
	if (ret < 0)
	{
		cout << "no find best stream";
		return -1;
	}
	videoindex = ret;

	codec_ctx = avcodec_alloc_context3(codec);
	if (!codec_ctx)
	{
		av_log(NULL, AV_LOG_ERROR, "Failed to allocate the decoder context for stream #%u\n");
		return AVERROR(ENOMEM);
	}

	// 3.3 AVCodecContext初始化:使用codec参数codecpar初始化AVCodecContext相应成员
	ret = avcodec_parameters_to_context(codec_ctx, ifmt_ctx->streams[videoindex]->codecpar);
	if (ret < 0)
	{
		return ret;
	}
	ret = avcodec_open2(codec_ctx, codec, NULL);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "Failed to open decoder for stream #%u\n", ret);
		return ret;
	}
	av_dump_format(ifmt_ctx, 0, filename, 0);

	if (!init_filter(codec_ctx))
		return -1;

	if (open_output_file("uuu.mp4") < 0)
		return -1;

	AVPacket *packet;
	packet = (AVPacket*)av_malloc(sizeof(AVPacket));
	AVPacket *outpacket=nullptr;
	outpacket = (AVPacket*)av_malloc(sizeof(AVPacket));


	AVFrame *frame;
	frame = av_frame_alloc();

	AVFrame *outframe;
	outframe = av_frame_alloc();

	bool write_status = true;

	AVBufferRef *bufferRef;
	while (av_read_frame(ifmt_ctx, packet) >= 0)
	{
		//时间基转换,解码
        av_packet_rescale_ts(packet, ifmt_ctx->streams[videoindex]->time_base, encode_ctx->time_base);
        if (packet->stream_index == videoindex)
		{
			ret = avcodec_send_packet(codec_ctx, packet);
			if (ret < 0)
			{
				cout << "avcodec_send_packet" << endl;
				break;
			}
			while (ret >= 0)
			{
				ret = avcodec_receive_frame(codec_ctx, frame);
				if (ret == AVERROR(EINVAL))
				{
					cout << "codec not opened, or it is an encoder" << endl;
					goto end;
				}
				else if (ret == AVERROR(EAGAIN))
				{
					cout << "user must try to send new input" << endl;
					continue;
				}
				else if (ret == AVERROR_EOF)
				{
					cout << "the decoder has been fully flushed" << endl;
					break;
				}
				else if (ret >= 0)
				{
					frame->pts = frame->best_effort_timestamp;
					//push the decoded frame into the filtergrapth
					if (av_buffersrc_add_frame_flags(bufsrc_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0)
					{
						cout << "Error while feeding the filtergraph" << endl;
						break;
					}
					//pull filtered frames from the filtergraph
					while (1)
					{
						ret = av_buffersink_get_frame(bufsink_ctx, outframe);
						if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
							break;
						if (ret < 0)
							goto end;
						
						if (!outframe || !encode_ctx)
							break;
						if (av_frame_make_writable(outframe) < 0)
						{
							cout << "av_frame_make_writable NO!" << endl;
							break;
						}

						// 设置帧类型
						outframe->pict_type = AV_PICTURE_TYPE_NONE;
						//10.将编码后的视频压缩数据写入文件中
						while (1)
						{
							write_status = true;
							ret = avcodec_send_frame(encode_ctx, outframe);
							if (ret < 0)
							{
								cout << "avcodec_send_frame failed!" << endl;
								goto end;
							}

							ret = avcodec_receive_packet(encode_ctx, outpacket);
							if (ret == AVERROR_EOF || ret == AVERROR(EINVAL))
							{
								break;
							}
							else if(ret == AVERROR(EAGAIN))
							{
								continue;
							}
							else if (ret >= 0)
							{
								// 3.3 更新编码帧中流序号,并进行时间基转换
								//     AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式其AVStream.time_base不同
								//     所以输出文件中,每个packet需要根据输出封装格式重新计算pts和dts
								outpacket->stream_index = videoindex;
								AVRational dst = { 1,25 };
								av_packet_rescale_ts(outpacket, encode_ctx->time_base, ofm_ctx->streams[0]->time_base);
								
								//将编码后的packet写入输出媒体文件
								ret = av_interleaved_write_frame(ofm_ctx, outpacket);
								av_packet_unref(outpacket);
								if (ret < 0)
								{
									av_log(NULL, AV_LOG_ERROR, "write vframe error %d\n", ret);
								}
								else
									cout << "av_interleaved_write_frame success" << endl;
								write_status = false;
								break;
							}

						}					
						av_frame_unref(outframe);
						if (!write_status)
							break;
					}
					
				}
			}
		}
		av_packet_unref(packet);
	}
	//11.写入文件尾部信息
	ret = av_write_trailer(ofm_ctx);
	cout << "success" << endl;
end:

	avfilter_graph_free(&filter_graph);
	av_packet_unref(packet);
	av_packet_unref(outpacket);
	av_frame_unref(outframe);
	av_frame_unref(frame);
	avcodec_close(codec_ctx);
	avformat_close_input(&ifmt_ctx);
	avcodec_free_context(&encode_ctx);
	avformat_free_context(ofm_ctx);

	return -1;

}

2.初始化过滤器

bool FfmpegTest::init_filter(AVCodecContext *codec_ctx)
{
	char args[512] = "";
	int ret = -1;
	AVFilter *bufsink = (AVFilter*)avfilter_get_by_name("buffersink");
	AVFilter *bufsrc = (AVFilter*)avfilter_get_by_name("buffer");
	AVFilterInOut *input = avfilter_inout_alloc();
	AVFilterInOut *output = avfilter_inout_alloc();
	AVPixelFormat fmt1[] = { (AVPixelFormat)codec_ctx->pix_fmt, AV_PIX_FMT_NONE};
	AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;

	filter_graph = avfilter_graph_alloc();
	
	snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->time_base.num,
		codec_ctx->time_base.den, codec_ctx->sample_aspect_ratio.num, codec_ctx->sample_aspect_ratio.den);

	ret = avfilter_graph_create_filter(&bufsrc_ctx, bufsrc, "in", args, NULL, filter_graph);
	if (ret < 0)
	{
		cout << "bufsrc_ctx avfilter_graph_create_filter failed" << endl;
		return false;
	}
	ret = avfilter_graph_create_filter(&bufsink_ctx, bufsink, "out", NULL, NULL, filter_graph);
	if (ret < 0)
	{
		cout << "avfilter_graph_create_filter failed" << endl;
		return false;
	}
	ret = av_opt_set_int_list(bufsink_ctx, "pix_fmts", fmt1, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
	if (ret < 0)
	{
		cout << "Cannot set output pixel format" << endl;
		return false;
	}

	output->name = av_strdup("in");
	output->filter_ctx = bufsrc_ctx;
	output->pad_idx = 0;
	output->next = nullptr;

	input->name = av_strdup("out");
	input->filter_ctx = bufsink_ctx;
	input->pad_idx = 0;
	input->next = nullptr;

	const char *filter_desc = "movie=Mario.png[wm];[in][wm]overlay=0:0[out]";//添加水印到坐标5,5  (这儿一定注意是否超出范围坐标)
	//const char *filter_desc = "scale=78:24,transpose=cclock";

	//const char *filter_desc = "drawtext = fontfile = simhei.ttf: text = 'we are family' : x = 10 : y = 10 : fontsize = 24 : fontcolor = white : shadowy = 2";
	ret = avfilter_graph_parse(filter_graph, filter_desc, input, output, NULL);
	if (ret < 0)
	{
		return false;
	}
	ret = avfilter_graph_config(filter_graph, NULL);
	if (ret < 0)
		return false;
	return true;

}

3.输出文件的初始化

int FfmpegTest::open_output_file(const char *filename)
{
	ret = -1;
	//1.初始化化封装格式上下文  并打开文件
	ofm_ctx = avformat_alloc_context();
	ret = avformat_alloc_output_context2(&ofm_ctx, NULL, NULL, filename);
	if (ret < 0)
	{
		cout << "avformat_alloc_output_context2 failed!" << endl;
		goto end;
	}
	//2.创建输出码流
	a_stream = avformat_new_stream(ofm_ctx, NULL);
	//3.创建编码器(根据输入编码类型)
	encode = avcodec_find_encoder(ifmt_ctx->streams[videoindex]->codecpar->codec_id);
	//4.视频编码器上下文 并设置参数
	encode_ctx = avcodec_alloc_context3(encode);
	encode_ctx->codec_id = codec_ctx->codec_id;
	encode_ctx->codec_type = codec_ctx->codec_type;
	//encode_ctx->frame_size = codec_ctx->frame_size;
	encode_ctx->height = codec_ctx->height;
	encode_ctx->width = codec_ctx->width;
	encode_ctx->sample_aspect_ratio = codec_ctx->sample_aspect_ratio; // 采样宽高比:像素宽/像素高
	encode_ctx->refs = codec_ctx->refs;
	encode_ctx->extradata = codec_ctx->extradata;
	encode_ctx->delay = codec_ctx->delay;
	encode_ctx->framerate = codec_ctx->framerate;
	encode_ctx->time_base.den = 25;
	encode_ctx->time_base = av_inv_q(codec_ctx->framerate);  // 时基:解码器帧率取倒数
	encode_ctx->time_base.num = 1;
	encode_ctx->pix_fmt = codec_ctx->pix_fmt;        // 编码器采用解码器的像素格式
	encode_ctx->bit_rate = codec_ctx->bit_rate;//设置自己的比特率
	encode_ctx->max_b_frames = 0;
	
	//5.打开编码器
	ret = avcodec_open2(encode_ctx, encode, NULL);
	if (ret < 0)
	{
		cout << "avcodec open failed!" << endl;
		goto end;
	}
	//6.把编码器信息写入流中
	ret = avcodec_parameters_from_context(a_stream->codecpar, encode_ctx);
	if (ret < 0)
	{
		cout << "avcodec_parameters_from_context failed!" << endl;
		goto end;
	}	
	av_dump_format(ofm_ctx, 0, filename, 1);
	if (!(ofm_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究AVFMT_NOFILE标志
	{
		// 7. 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
		ret = avio_open(&ofm_ctx->pb, filename, AVIO_FLAG_READ_WRITE);
		if (ret < 0) {
			av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
			goto end;
		}
	}	
	//8.写入头文件信息(前提得初始化一个AVIOContext)
	ret = avformat_write_header(ofm_ctx, NULL);
	if (ret < 0)
	{
		cout << "write header failed!" << endl;
		goto end;
	}

	return 0;
end:
	avcodec_free_context(&encode_ctx);
	avformat_free_context(ofm_ctx);
	return -1;
}

整体上的代码函数就这三个,没有进行细化函数,存在许多优化,本身就是一个demo,能实现功能即可。

三、遇到问题

因之前一开始对编码和解码都是属于混乱状态,一直没理解,出现很多类似的问题,找问题出处,很难下手都是一一尝试,最后才解决的。

1.运行到avformat_write_header()时,ffmpeg_transcode.exe 中)引发的异常: 0xC0000005: 读取位置 0x0000000000000088 时发生访问冲突

write之前并没有执行avio_open(),所以一直报异常

	if (!(ofm_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究AVFMT_NOFILE标志
	{
		// 7. 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
		ret = avio_open(&ofm_ctx->pb, filename, AVIO_FLAG_READ_WRITE);
		if (ret < 0) {
			av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
			goto end;
		}
	}	

2.过滤器初始化过程中错误,导致后面进行avcodec_send_frame或者avcodec_receive_packet也报类似的异常

①原因是格式设置错误,不对应,fmt格式应该于输入格式的pix_fmt对应。

AVPixelFormat fmt1[] = { (AVPixelFormat)codec_ctx->pix_fmt, AV_PIX_FMT_NONE};

②添加水印图片时,坐标起点加上图标大小超出界限

const char *filter_desc = "movie=Mario.png[wm];[in][wm]overlay=0:0[out]";//添加水印到坐标0.0  

3.编码后的视频出现时长不对,可能导致时间缩小(快进几倍的样子),时间变长(扩大播放时间)

原因是没有进行时间基转换av_packet_rescale_ts,得设置对应的时间基,不然就会出现问题。

解码时需要进行时间基转换,编码也需要进行转换,才能对应 (花了长时间才发现的 )

可参考:https://www.cnblogs.com/leisure_chn/p/10584910.html

 

以上问题就是在编写过程中,花了时间去解决的问题,很多都能避免的。学习新东西,就应该脚踏实地的去实践,不要想着直接把别人的demo进行复制,实现即可。还是需要自己理清思路一步一步实现,解决其中问题,才能更好掌握知识。

学习方法:先掌握基础知识和接口函数调用,再去看逻辑思路,最后再看原理和算法(借用我领导给我讲的东西,总结了一下)

时刻提醒自己,一步一步脚印来,不要好高骛远。

参考:http://ffmpeg.org/doxygen/trunk/examples.html

 

上一篇:Django报错UnicodeDecodeError: 'gbk' codec can't decode byte 0xa6 in position 9737: ill.


下一篇:HDA codec相关(1)