利用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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章