利用FFmpeg API进行字符叠加和加水印

前面两篇文章详细讲解了怎么叠加字幕和Logo,但是这两篇的例子主要是针对Windows平台的,用到大量Windows API,一些非Windows程序员想要移植到其他平台(如Linux、Android)可能还要费一番功夫。要在其他平台进行叠加字幕和Logo有什么比较通用的方案呢?其实FFmpeg已经集成了一个加水印滤镜功能,用跨平台的FFmpeg能够帮助我们轻松实现该功能。

废话少说,先看看加水印滤镜怎么用。

首先要调用avfilter_register_all() 注册所有AVFilter。

接着,定义几个跟加水印滤镜相关的变量:

AVFilterContext * buffersink_ctx = NULL;
AVFilterContext * buffersrc_ctx = NULL;
AVFilterGraph * filter_graph = NULL;
BOOL      g_bInitFilterOK = FALSE;
	
CCritSec     g_FilterLock;

FFmpeg初始化加水印滤镜的例子代码如下:


static int init_filters(const char *filters_descr, AVCodecContext *pCodecCtx)
{
	CAutoLock lock(&g_FilterLock);

	if(filter_graph != NULL)
		return 1;

	if(pCodecCtx->pix_fmt != PIX_FMT_YUV420P) //检查输入图像像素格式
		return 2;

    char args[512];
    int ret;
    AVFilter *buffersrc  = avfilter_get_by_name("buffer");
    AVFilter *buffersink = avfilter_get_by_name("ffbuffersink");
    AVFilterInOut *outputs = avfilter_inout_alloc();
    AVFilterInOut *inputs  = avfilter_inout_alloc();
    enum PixelFormat pix_fmts[] = { PIX_FMT_YUV420P, PIX_FMT_NONE };
    AVBufferSinkParams *buffersink_params;

    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",
            pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
            pCodecCtx->time_base.num, pCodecCtx->time_base.den,
            pCodecCtx->sample_aspect_ratio.num, pCodecCtx->sample_aspect_ratio.den);

    ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                                       args, NULL, filter_graph);
    if (ret < 0) {
        TRACE("Cannot create buffer source\n");
        return ret;
    }

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

    /* Endpoints for the filter graph. */
    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;

    if ((ret = avfilter_graph_parse_ptr(filter_graph, filters_descr,
                                    &inputs, &outputs, NULL)) < 0)
        return ret;

    if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
        return ret;

	g_bInitFilterOK = TRUE;
    return 0;
}

上面代码主要用到的API如下:

avfilter_graph_alloc():为FilterGraph分配内存。
avfilter_graph_create_filter():创建并向FilterGraph中添加一个Filter。
avfilter_graph_parse_ptr():将一串通过字符串描述的Graph添加到FilterGraph中。
avfilter_graph_config():检查FilterGraph的配置。
av_buffersrc_add_frame():向FilterGraph中加入一个AVFrame。
av_buffersink_get_frame():从FilterGraph中取出一个AVFrame。

init_filters函数需要传入一个字符串,这个字符串是描述要加水印的属性的,包括:水印图片的路径,水印显示坐标等。水印支持PNG图片文件作为输入,即支持背景透明。这个字符串的格式如下:

movie=logo.png,scale=60:30[watermask];[in] [watermask] overlay=30:10 [out]

参数说明:

logo.png: 添加的水印图片;

scale:水印大小,水印长度*水印的高度;

overlay:水印的位置,距离原视频左侧的距离:距离原视频上侧的距离;mainW主视频宽度, mainH主视频高度,overlayW水印宽度,overlayH水印高度

  左上角overlay参数为 overlay=0:0

  右上角为 overlay= main_w-overlay_w:0

  右下角为 overlay= main_w-overlay_w:main_h-overlay_h

  左下角为 overlay=0: main_h-overlay_h

FFmpeg水印滤镜支持的参数见下面表格:

参数

参数

 

 

 

overlay

main_w

视频单帧图像宽度

main_h

视频单帧图像高度

overlay_w

水印图片的宽度

overlay_h

水印图片的高度

-vf

设置video过滤器,视频旋转,缩放,水印等处理

af

设置audio过滤器

关于更多的参数可以参考ffmpeg官网filter的描述:https://ffmpeg.org/ffmpeg-filters.html 

下面是在程序中调用init_filters的代码:

	CString strFilterLogoDesc;
	
	if(m_bIsOSDText)
		strFilterLogoDesc = "movie=logo_text.png[watermark];[in][watermark]overlay=10:10[out]"; //movie=后面的参数是Logo图标的文件名,overlay=后面的是坐标,OSD坐标位置暂时固定为(10, 10)
	else
		strFilterLogoDesc = "movie=logo.png[watermark];[in][watermark]overlay=10:10[out]";

	int nRet = init_filters(strFilterLogoDesc, input_st->codec);

这里要说明一下,Logo文件的路径默认是跟执行文件同一个目录的,如果要用绝对路径指定Logo的路径,则可能会失败。我在Windows平台上测试过,Logo文件用绝对路径的话,init_filters将会返回-2(不知在Linux上是否也有这个问题)。后来找到一个解决办法,Logo.png的路径不用绝对路径,但要设置当前目录的路径:调用系统API设置SetCurrentDirectory(path),将目标目录路径指定为Logo文件所在的目录。

从上面调用代码可以看到,对于叠加字幕和叠加图标都需要传入一个水印PNG文件,对于文字怎么生成一个图片文件呢?在Windows平台,我们可以用Windows GDI 函数在内存中生成一个位图,然后打印上字符,并保存为PNG图片(额!还是得用Windows API,不是说跨平台吗?这个只是作者本人的技术倾向,对Windows API比较熟悉,其实不直接用系统API也可以,一些跨平台库如SDL、QT也有类似在的内存中打印文字和输出图形的功能)。下面是从一段文字转为一个带Alpha通道的位图的代码:

//创建一个显示OSD文字的位图,位图带透明背景
static BOOL CreateOsdTextBitmap(const char * szText, LOGFONT * lplf, CImage & image)
{
	BOOL pass = FALSE;

	CSize csTextSize;

	HDC memDC = CreateCompatibleDC(NULL);

	HFONT hFont = ::CreateFontIndirect(lplf);
	ASSERT(hFont != NULL);
	::SelectObject(memDC, hFont);
	GetTextExtentPoint32(memDC, szText, lstrlen(szText), &csTextSize);

	int cx = csTextSize.cx + 12;
	int cy = csTextSize.cy + 8;

	
	HBITMAP membmp = CreateCompatibleBitmap(memDC, cx, cy);
	HBITMAP oldbmp = (HBITMAP) SelectObject(memDC, membmp);

	SetBkMode(memDC, TRANSPARENT);
	::SetBkColor(memDC, RGB(0, 0, 0)); //背景色
	SetTextColor(memDC, RGB(0xFF, 0, 0));

	CRect OsdRect(6, 4, cx-6, cy-4);
	DrawText(memDC, szText, lstrlen(szText), &OsdRect, DT_CENTER);

#if 1
	if(!image.Create(cx, cy, 32, 0x01)) //创建带Alpha通道的32位位图
#else
	if(!image.Create(cx, cy, 32)) 
#endif
	{
		pass = FALSE;
		goto end_osd_bitmap_func;

	}

	pass = TRUE;
	

	HDC hImgDC = image.GetDC();

	BitBlt(hImgDC, 0, 0, cx, cy, memDC, 0, 0, SRCCOPY);

	image.ReleaseDC();

#if 1
	if(image.GetBPP() == 32)
	{
		//将OSD背景的部分设置为透明

		int image_cx = image.GetWidth();
		int image_cy = image.GetHeight();

		long lPitch = image.GetPitch();

		BYTE * image_data;
		if(lPitch < 0)
		{
			image_data = (BYTE *)image.GetBits()+(image.GetPitch()*(image.GetHeight()-1));
		}
		else
		{
			image_data = (BYTE *)image.GetBits();
		}

		BYTE * pImage = NULL;
		for(int y = 0; y < image_cy; y++)
		{
			pImage = image_data + abs(lPitch) * y;
			for(int x = 0; x < image_cx; x++)
			{
				if(pImage[0] == 0 && pImage[1] == 0 && pImage[2] == 0) //RGB等于背景色
				{
					pImage[3] = 0; //透明
				}
				else
				{
					pImage[3] = 0xff;
				}
				pImage += 4;
			}
		}
	}
#endif

end_osd_bitmap_func:
	::DeleteObject(hFont);

	SelectObject(memDC, oldbmp);
	DeleteObject(membmp);
	DeleteDC(memDC);

	return pass;
}

对于字幕,我们保存的图片名是logo_text.png;而对于Logo,我们加载Logo图片(logo.png)。

初始化完滤镜后,我们在视频图像解码出来之后就可以往上叠上水印了。解码出来的图像帧通过回调函数传递到应用层,这个回调函数的实现如下:

//视频图像回调
LRESULT CALLBACK VideoCaptureCallback(AVStream * input_st, enum PixelFormat pix_fmt, AVFrame *pframe, INT64 lTimeStamp)
{
	
	if(gpMainFrame->IsOverlayOSD())
	{
		CAutoLock lock(&g_FilterLock);

		if(filter_graph == NULL)
		{
			CString strFilterLogoDesc;

			//注意:Logo图片需要跟执行文件同一个目录,并且要调用SetCurrentDirectory设置当前目录,否则初始化Filter将会失败,返回-2
			
			if(gpMainFrame->m_bIsOSDText)
				strFilterLogoDesc = "movie=logo_text.png[watermark];[in][watermark]overlay=10:10[out]"; //movie=后面的参数是Logo图标的文件名,overlay=后面的是坐标,OSD坐标位置暂时固定为(10, 10)
			else
				strFilterLogoDesc = "movie=logo.png[watermark];[in][watermark]overlay=10:10[out]";

			::SetCurrentDirectory(GetAppDir());

			int nRet = init_filters(strFilterLogoDesc, input_st->codec);
			if(nRet != 0)
			{
				TRACE("Error: init_filters failed!! nRet = %d\n", nRet);
				goto end_filter;
			}
		}

		if(!g_bInitFilterOK)
			goto end_filter;

		AVFilterBufferRef *picref;

		 pframe->pts = av_frame_get_best_effort_timestamp(pframe);


		if (av_buffersrc_add_frame(buffersrc_ctx, pframe) < 0) 
		{
			TRACE( "Error while feeding the filtergraph\n");
			goto end_filter;
		}

		int ret;

		while (1) 
		{
			ret = av_buffersink_get_buffer_ref(buffersink_ctx, &picref, 0);
			if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
				break;
			if (ret < 0)
				break;

			if (picref) 
			{

				int y_size=picref->video->w*picref->video->h;

				AVFrame outframe;
				outframe.data[0] = picref->data[0];
				outframe.data[1] = picref->data[1];
				outframe.data[2] = picref->data[2];
				outframe.data[3] = 0;
				outframe.linesize[0] = picref->linesize[0];
				outframe.linesize[1] = picref->linesize[1];
				outframe.linesize[2] = picref->linesize[2];
				outframe.linesize[3] = 0;

				gpMainFrame->m_Painter.PlayAVFrame(input_st, &outframe);

				avfilter_unref_bufferp(&picref);
			}
		}// while

		return 0;
	}
	else
	{
#if 0
		if(filter_graph != NULL)
		{
			avfilter_graph_free(&filter_graph);
			filter_graph = NULL;
		}
#endif
	}

end_filter:
	//if(gpMainFrame->IsPreview())
	{
		gpMainFrame->m_Painter.PlayAVFrame(input_st, pframe);
	}

	return 0;
}

上述函数中,   成员变量  m_bOverlayOSD是表示当前是否正在叠加字幕或图标;变量   m_bIsOSDText 表示叠加的是字幕;根据这两个变量在界面上更新叠加对象类型,显示不同的叠加效果(目前只支持叠加一个OSD,不同类型的OSD需要切换,读者可在基础上进行扩展)。

最后说说这个水印滤镜的缺点:就是它不支持从内存冲传入水印图片,而需要读取一个磁盘上的图片文件,如果想实现动态更新的OSD效果,比如显示一个不停更新时间的OSD,则需要频繁的构建图片-》保存图片-》加载,效率肯定不好,不知道FFmpeg对这种情况有没有更好的实现方式?欢迎大家留言评论。

例子下载地址:https://download.csdn.net/download/zhoubotong2012/11855623

 

你可能感兴趣的:(ffmpeg,音视频开发)