如何寫Directshow Render Filter並實現視頻渲染、疊加字幕和位圖功能

    在播放器上疊加字幕或位圖(Logo)是一個很常見的需求,現在很多播放器都支持該功能。播放器開發目前可基於框架的有很多,比如MPlayer,gstreamer,Directshow,而這篇教程就是講解怎麼在Directshow播放器上疊加字幕和Logo的,如果你不是從事Directshow開發的程序員或根本不熟悉Directshow,那可以繞路了。

疊加字幕或圖標一般分兩種應用:一種是在顯示視頻的界面上顯示字幕或圖標;另外一種是在採集設備採集出來的圖像或從視頻文件解碼出來的圖像中進行字幕疊加,疊加後對視頻再編碼保存。兩者應用場景區別是:前者是在視頻顯示的時候(即渲染階段)疊加,沒有修改原來的視頻圖像數據,字幕可以動態添加或移除,而後者的應用場景中,原視頻圖像和OSD圖像經過疊加處理,進行保存,OSD已經被寫到視頻畫面上了。對於字幕疊加(和LOGO的處理流程基本一樣),我們有多種實現方法,現在介紹兩種最常見的方法。假設現在輸入一張圖片,我們要在上面疊加文字,我們可以分兩步操作,第一:渲染這張圖片,讓它填充整個窗口;第二,在窗口某個位置上畫字幕(DrawText)。這樣,字幕就在原圖像圖層之上顯示,這種方式我們叫渲染時疊加。而另外一種方法,我們可以在渲染前修改原圖像素,在字幕要打上去的位置將字符點陣“拷貝”到目標區域,OSD區域分文字前景和背景,我們需要將OSD前景的像素保留,而背景的像素用原圖像的像素代替,通過這種規則將兩種像素(OSD像素和原圖像像素)混合,最終生成另外一張圖片。第一種方法(渲染時疊加)沒有修改原圖片,這種方法一般是在視頻渲染到顯卡到後備緩存時,將文字或位圖作爲一個圖層或Texture表面與原圖像進行圖層混合,也就是使用顯卡的硬件加速來進行圖層混合 ,一般效率比較高,並且可以實現更加豐富的效果,比如圖像旋轉、變形、改變Alpha等。但是這種方法缺點是不適合作後期處理和保存,如果混合後還需要把疊加後圖像保存到磁盤,則用第一種方法比較合適。

前面講了兩種不同字幕疊加方式的原理,這裏只給大家講一下第一種方法即渲染時疊加的方法的技術實現,而第二種方法(修改圖像像素)就不作詳細介紹了,我的另外一篇博文怎麼在視頻上疊加字幕和Logo--技術實現2有詳細介紹。另外有必要說明一下,這篇博文采用的方法跟怎麼在視頻上疊加字幕和Logo--技術實現1所用方法是一樣的,只是加多了Directshow Filter層的封裝。

Directshow框架爲我們提供了幾個功能強大的渲染視頻的插件:VMR7,VMR9。這兩個插件(統稱爲VMR)提供了統一的接口對視頻顯示進行一些高級的控制,其中就包括疊加位圖(疊加文字也可以用疊加位圖的方法,因爲疊加文字其實是通過創建一個位圖,將文字BitBlt到位圖上,熟悉GDI的朋友應該對這種方法不會陌生)。VMR提供的疊加位圖的接口是:IVMRMixerBitmap,這個接口有一個方法:SetAlphaBitmap,這個方法傳入一個位圖結構對象,將疊加的位圖信息告訴顯卡,這樣顯卡就能將圖層正確的顯示到視頻上。具體大家可以參考Directshow文檔,標題:Displaying an Application-Supplied Bitmap on the Composited Image。用VMR7和VMR9在使用這個接口上有一點點區別:因爲疊加字幕VMR需要啓用Mixer模式,VMR7默認不工作在Mixer模式下,除非你顯示調用Renderer的 IVMRFilterConfig::SetNumberOfStreams方法,而VMR9默認工作在Mixer模式下,不需要調用 IVMRFilterConfig::SetNumberOfStreams方法。雖然通過SDK的接口能夠簡易地實現疊加位圖功能,但是,但是這個接口有個明顯的弊端:它只能設置疊加一個位圖到視頻上,如果有兩個Logo或有兩段在不同位置顯示的文字需要疊加,那這種方法就無能爲力了(不要想着將多個OSD合併到一個位圖上和原視頻疊加,這種方法的效率太低,因爲圖層混合是有開銷的,位圖越大,資源消耗越大)。

既然框架提供的接口沒有實現我們想要的功能,我們有什麼其他辦法呢?其實,在渲染階段疊加方式,最終用到的都是GDI,DirectDraw,Direct3D等API,所以,我們也可以用這些API將OSD畫到視頻上。其中,用GDI繪製的方法效率比較低,一般不建議用,更常見的方法是用DirectDraw或D3D技術。因爲DirectDraw的API更簡單,更易於使用,所以我提供的這個例子也用到DirectDraw技術來畫字幕和位圖。這個例子實現了一個自定義的渲染器,Filter類名是CFilterNetSender,繼承於CBaseRenderer,具有Directshow渲染器的基本屬性和功能。這個類封裝了渲染器的常見接口,並且提供回調將輸入的Media Sample數據(可理解爲視頻圖像)回調給應用層處理。

假設現在我們要播放一個文件,首先,用Directshow我們要創建一個FilterGraph,將一些需要用到的Filter加進去並連接起來,這些Filter包括SourceFilter,Splitter Filter,Decoder Filter,Renderer Filter。下面的函數創建了一個播放的FilterGraph鏈路:

BOOL CFilePlayGraph::RenderFile()
{
	if (mGraph == NULL)
	{
		return FALSE;
	}

	if(!FileExist(m_szMeidaFile))
	{
		TRACE("文件路徑不存在!\n");
		return FALSE;
	}

	TRACE("Begin to call RenderFile: %s \n", m_szMeidaFile);

#if 0
	DWORD dwTick1 = GetTickCount();

	WCHAR    szFilePath[MAX_PATH];
	MultiByteToWideChar(CP_ACP, 0, m_szMeidaFile, -1, szFilePath, MAX_PATH);
	if (FAILED(mGraph->RenderFile(szFilePath, NULL))) //說明:RenderFile自動添加Filter和自動連接Pin,但是這種方法可能會耗時比較長,所以最好用手動添加的方法
	{
		TRACE("RenderFile() 失敗!\n");
		m_bRenderSuccess = FALSE;
		//return FALSE;
	}
	else
	{
		m_bRenderSuccess = TRUE;
	}

	TRACE("RenderFile() used time: %ld\n", GetTickCount() - dwTick1);
#endif

	HRESULT   hr;
	CComPtr<IBaseFilter>	mSplitter;
	CComPtr<IBaseFilter>    mVideoDecoder;
	CComPtr<IBaseFilter>    mAudioDecoder;

	hr = CoCreateInstance(CLSID_LAVSource, NULL, CLSCTX_INPROC, IID_IBaseFilter, (void**)&mSplitter);
	if(SUCCEEDED(hr))
	{
		hr = mGraph->AddFilter(mSplitter, L"LAV Source Filter");
	}

	if (FAILED(hr))
	{
		OutputDebugString(_T("Add LAV splitter/Source Filter Failed\n"));
		return FALSE;
	}



	CComPtr<IFileSourceFilter> pFileSource;
	mSplitter->QueryInterface(IID_IFileSourceFilter, (void**)&pFileSource);
	if (pFileSource)
	{
		USES_CONVERSION;
		hr = pFileSource->Load(A2W(m_szMeidaFile), NULL);

		if (FAILED(hr))
		{
			return hr;
		}
	}


	hr = CoCreateInstance(CLSID_LAVVideoDecoder, NULL, CLSCTX_INPROC, IID_IBaseFilter, (void**)&mVideoDecoder);
	if(SUCCEEDED(hr))
	{
		hr = mGraph->AddFilter(mVideoDecoder, L"LAV Video Decoder");
		OutputDebugString(_T("Add LAV Video Decoder \n"));
	}

	if (FAILED(hr))
	{
		OutputDebugString(_T("Add LAV Video Decoder Failed\n"));
		return FALSE;
	}

	hr = CoCreateInstance(CLSID_LAVAudioDecoder, NULL, CLSCTX_INPROC, IID_IBaseFilter, (void**)&mAudioDecoder);
	if(SUCCEEDED(hr))
	{
		hr = mGraph->AddFilter(mAudioDecoder, L"LAV Audio Decoder");
	}
	else
	{
		OutputDebugString("Add LAV Audio Decoder Failed\n");
	}

	hr = RenderFilter(mSplitter);
	if(FAILED(hr))
	{
		OutputDebugString("RenderFilter Failed \n");
		return FALSE;
	}
	m_bRenderSuccess = TRUE;

	if(m_bPreviewMode) //預覽模式下用VMR Filter(默認渲染器)進行視頻渲染
	{
		return m_bRenderSuccess;
	}

    //Do other things
}

這個構建Filter Graph的函數我們加入了幾個指定的Filter:LAV Source Filter,LAV Video Decoder Filter,LAV Audio Decoder。這幾個Filter都是著名的LAV Filters開源Directshow插件中的成員。加入了這幾個Filter並調用RenderFilter進行自動連接後,一個播放文件的鏈路就構建好了,其中視頻解碼器後面連接了VMR Renderer Filter,而音頻解碼器後面連接了Audio Renderer Filter。但是,我們要做的事情是將自己自定義的Renderer Filter連上來,並且將傳遞給Render Filter的Media Sample回調到應用層,由應用層去顯示或處理。所以,上面的RenderFille函數還需要修改一下,下面是後續的處理代碼:

//下面將渲染器進行替換,改成自定義的CFilterNetSender,其中視頻的Sender Filter會回調視頻幀數據,由應用層進行圖像繪製


	IBaseFilter *pVideoFilter = NULL;
	IBaseFilter *pAudioFilter = NULL;
	IPin *pInputPin = NULL, *pOutPin = NULL;
	IPin * pVideoOutPin = NULL;

	hr = FindVideoRenderer(mGraph,  &pVideoFilter);
	if(SUCCEEDED(hr) && pVideoFilter)
	{
		TRACE("FindVideoRenderer success!\n");

		pInputPin = GetInPin(pVideoFilter, 0);
		if(pInputPin == NULL)
		{
			return FALSE;
		}
		hr = pInputPin->ConnectedTo(&pVideoOutPin);
		if(FAILED(hr))
		{
			return FALSE;
		}

		PIN_INFO pinInfo;

		pVideoOutPin->QueryPinInfo(&pinInfo);
		if(pinInfo.pFilter != NULL)
		{
			char szName[256]= {0};

			FILTER_INFO  FilterInfo;
			pinInfo.pFilter->QueryFilterInfo(&FilterInfo);

			WideCharToMultiByte(CP_ACP, 0, FilterInfo.achName, -1, szName, 256, 0, 0);

			TRACE("Video Decode Filter: %s \n", szName); //解碼器的名稱

			FilterInfo.pGraph->Release();

			pinInfo.pFilter->Release();
		}

		if(pVideoFilter)
		{
			mGraph->RemoveFilter(pVideoFilter);
		}

		// Create the Sample Grabber.
		IBaseFilter *pF = NULL;

		hr = S_OK;

		m_pGrabberFilter[0] = new CFilterNetSender(NULL, &hr);

		hr =  m_pGrabberFilter[0]->QueryInterface(IID_IBaseFilter, (void**)&pF);

		hr = mGraph->AddFilter(pF, L"video Sample Grabber");
		if(FAILED(hr))
			return FALSE;

		m_pGrabberFilter[0]->SetDataCallback(m_CaptureCB, 0);

#ifdef FASTEST_PLAY_MODE
		m_pGrabberFilter[0]->SetFastMode(TRUE); //去時間戳以最大速度播放
#else
		m_pGrabberFilter[0]->SetFastMode(FALSE); //以正常速度播放
#endif

		struct SUPPORT_RAW_VIDEOTYPE
		{
			GUID videotype;
		} vformat[3] = {
			MEDIASUBTYPE_YV12,
			MEDIASUBTYPE_YUY2,
			MEDIASUBTYPE_RGB24
		};

		for(int i=0; i<3; i++)
		{
			// Set the media type.
			AM_MEDIA_TYPE mt;
			ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
			mt.majortype = MEDIATYPE_Video;
			mt.subtype = vformat[i].videotype;

			m_pGrabberFilter[0]->SetPreferMediaType(&mt); //輸出格式優先用YV12

			pInputPin = GetInPin(pF, 0);
			hr = mGraph->Connect(pVideoOutPin, pInputPin);
			if(SUCCEEDED(hr))//成功連接Pin,表示下游Filter支持該格式,跳出循環
			{
				break;
			}
		}

		if(FAILED(hr))
		{
			m_pGrabberFilter[0]->Release();
			m_pGrabberFilter[0] = NULL;

			return FALSE;
		}

	}//videoRenderer

	/////////////////////////////////////////////////////////////////
	//Find  AudioRenderer
	hr = FindAudioRenderer(mGraph,  &pAudioFilter);
	if(SUCCEEDED(hr) && pAudioFilter)
	{
		TRACE("FindAudioRenderer success.\n");

		IPin * pAudioOutPin = NULL;

		pInputPin = GetInPin(pAudioFilter, 0);
		if(pInputPin == NULL)
		{
			return FALSE;
		}
		hr = pInputPin->ConnectedTo(&pAudioOutPin);
		if(FAILED(hr))
		{
			return FALSE;
		}

		if(pAudioFilter)
		{
			mGraph->RemoveFilter(pAudioFilter);
		}

		// Create the Sample Grabber.
		IBaseFilter *pF = NULL;

		hr = S_OK;

		m_pGrabberFilter[1] = new CFilterNetSender(NULL, &hr);

		hr =  m_pGrabberFilter[1]->QueryInterface(IID_IBaseFilter, (void**)&pF);

		hr = mGraph->AddFilter(pF, L"Audio Sample Grabber ");
		if(FAILED(hr))
			return FALSE;

		m_pGrabberFilter[1]->SetDataCallback(m_CaptureCB, 1);

#ifdef FASTEST_PLAY_MODE
		m_pGrabberFilter[1]->SetFastMode(TRUE); //去掉時間戳,以最大速度播放
#else
		m_pGrabberFilter[1]->SetFastMode(FALSE); //正常速度播放
#endif

		// Set the media type.
		AM_MEDIA_TYPE mt;
		ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
		mt.majortype = MEDIATYPE_Audio;
		mt.subtype = MEDIASUBTYPE_PCM;

		m_pGrabberFilter[1]->SetPreferMediaType(&mt);

		pInputPin = GetInPin(pF, 0);
		hr = mGraph->Connect(pAudioOutPin, pInputPin);
		if(FAILED(hr))
		{
			m_pGrabberFilter[1]->Release();
			m_pGrabberFilter[1] = NULL;
			return FALSE;
		}
	}//Audio Renderer

	if(pVideoFilter == NULL && pAudioFilter == NULL)
	{
		TRACE("AudioRenderer and VideoRender is Null.\n");
		return FALSE;
	}

	m_bRenderSuccess = TRUE;

上面的代碼中先在Filter Graph中找到Video Renderer Filter,將其移除掉,然後加入自定義的Renderer Filter---CFilterNetSender,將CFilterNetSender和Video Decoder連上。同理,音頻的分支鏈路也這樣處理。這樣,所有的Sample最終都會送給自定義的Renderer Filter,由我們自己去處理視頻幀或音頻幀。在連接CFilterNetSender Filter和上游Filter(這裏值指Video Decoder Filter)的Pin時有個媒體類型協商的過程,上游的Filter需要和CFilterNetSender協商一些信息,比如兩者傳遞Media Sample的緩衝池大小和緩衝區個數,MediaType信息,其中MediaType就包括視頻圖像格式的信息,目前這個CFilterNetSender Filter的輸入Pin支持YV12,YUY2,RGB24這三種圖像格式,如果Video Decoder Filter能輸出這三種格式的任意一種,就能和CFilterNetSender連接上。除此之外,這上述三種圖像格式是有分優先級的,在協商Pin連接時優先用YV12,YUY2次之,如果前面兩者類型都不行,則最後用RGB24連接。這樣分優先級是因爲渲染器Filter輸出的圖像幀最後要拷到DirectDraw表面,而DirectDraw支持的格式是YUV(YV12和YUY2這兩種格式廣泛受支持),而RGB格式可能有些顯卡不支持。

上面說了,我們還需要設置回調函數,代碼中的這一句: m_pGrabberFilter[0]->SetDataCallback(m_CaptureCB, 0); 是設置用戶回調函數的。我們還需要在應用層實現一個回調函數,並在裏面實現渲染視頻和繪製OSD的操作。下面是回調函數的實現:

void CALLBACK OnReceiveAVData(int devNum, PBYTE pData, DWORD dwSize, INT64 & lTimeStamp)
{
	gpMainFrame->OnMediaFrame(pData, dwSize, lTimeStamp, devNum);
}

其中,OnMediaFrame函數的代碼實現爲:

void CMainFrame::OnMediaFrame( PBYTE pData, DWORD nBytes, INT64 & lTimeStamp, int type)
{
	if(type == 0) // video
	{

		ShowFPS();

		//TRACE("OnMediaFrame timediff: %lld \n", (lTimeStamp - llLastTimeStamp)/10000);

		llLastTimeStamp = lTimeStamp;

		if(m_bCapture)	
		{
			g_Painter.Play( pData, nBytes, lTimeStamp, g_Param.nPixelFormat); //繪製圖像
		}

	}
	else
	{
		
	}

}

g_Painter是CDXDrawPainter類的對象,CDXDrawPainter負責渲染視頻和疊加字幕,圖標。回調函數中調用了g_Painter對象的一個接口Play來顯示視頻和OSD。

CDXDrawPainter  g_Painter;

先看看CDXDrawPainter類的聲明,它繼承與CVideoPlayer類。

class CDXDrawPainter : public CVideoPlayer
{
private:
	VIDEOINFOHEADER *	mVideoInfo;
	HWND			mVideoWindow;
	HDC				mWindowDC;
	RECT			mTargetRect;
	RECT			mSourceRect;
	BOOL			mNeedStretch;
  
public:
	CDXDrawPainter();
	~CDXDrawPainter();

	void SetVideoWindow(HWND inWindow);
	BOOL SetInputFormat(BYTE * inFormat, long inLength);

	BOOL Open(void);
	BOOL Stop(void);
	BOOL Play(BYTE * inData, DWORD inLength, ULONG inSampleTime, int inputFormat);

	void  SetSourceSize(CSize size);
	BOOL GetSourceSize(CSize & size);

};

它的方法Play將一個視頻圖像傳入,並帶上時間戳,圖像格式等信息。而疊加OSD的操作是在Play函數內處理。Play是在視頻播放啓動運行之後調用的方法,如果沒有設置OSD,顯示的只是視頻,如果設置了OSD屬性,則會將OSD疊加顯示到圖像上面。設置OSD顯示屬性(比如OSD的文字,顯示座標,顯示顏色等)需要調用其他幾個方法,而這些方法都放到了CVideoPlayer這個基類裏面。讓我們看看CVideoPlayer類裏面有哪些重要的方法:

void    SetRenderSurfaceType(RenderSurfaceType type) { m_SurfaceType = type; }

// Initialization
int     Init( HWND hWnd , BOOL bUseYUV, int width, int height); 
void    Uninit();

void    SetPlayRect(RECT r);

// Rendering
BOOL	RenderFrame(BYTE* frame,int w,int h, int inputFmt);

//設置OSD接口
BOOL    SetOsdText(int nIndex, CString strText, COLORREF TextColor, RECT & OsdRect);
BOOL    SetOsdBitmap(int nIndex, HBITMAP hBitmap, RECT & OsdRect);
void    DisableOsd(int nIndex);

下面是這些方法的說明:

1.  SetRenderSurfaceType:設置Directdraw表面創建的類型,目前支持YUY2,YV12.

2. Init:  創建和初始化DirectDraw表面,傳入視頻窗口的句柄和圖像大小,並且設置是否用Overlay模式,bUserYUV參數爲TRUE表示使用Overlay模式,但是Overlay模式經常使用不了,並且有很多限制。建議該參數賦值爲FALSE。

3. Unit:  銷燬DirectDraw表面。

4. SetPlayRect:設置視頻在窗口中的顯示區域。

5. RenderFrame: 顯示圖像和疊加OSD。其中inputFmt是傳入的圖像格式,取值爲:0--YV12, 1--YUY2, 2--RGB24, 3--RGB32。爲什麼DirectDraw表面只支持YUV格式,而傳入的圖像格式可以是RGB呢?這樣是爲了增加兼容性,支持更多的圖像格式,因爲某些視頻解碼出來就是RGB格式的。但是,RGB格式在拷貝到DirectDraw表面前是需要經過轉換的,默認轉爲YV12格式。舉個例子,如果傳入的圖像是RGB24的,則在設置表面類型(調用SetRenderSurfaceType)必須爲YV12.

6.  SetOsdText: 設置OSD文字的相關屬性,包括Index,字符內容,文字顏色,和顯示座標。

7.  SetOsdBitmap: 設置疊加的OSD位圖屬性,包括Index,傳入位圖句柄,顯示位置。

8.  DisableOsd:關閉OSD。

OSD的信息用一個結構體類型--OSDPARAM表示,OSDPARAM定義如下:

typedef struct _osdparam
{
	BOOL     bEnable;
	int      nIndex;
	char     szText[128];
	LOGFONT  mLogFont;
	COLORREF clrColor;
	RECT     rcPosition;
	HBITMAP  hWatermark;
}OSDPARAM;

我們定義了一個OSD數組: OSDPARAM       m_OsdInfo[MAX_OSD_NUM];

所有OSD信息都存到這個數組裏,Index是OSD的一個索引,每個OSD是數組的一個成員,可自定義最大的疊加的OSD個數。

接着,簡單描述一下CVideoPlayer類內部的處理流程:在Init函數調用的時候會創建顯示視頻的後備緩衝區(Back Buffer)和前緩衝區(Front Buffer),DirectDraw Suface其實就是後備緩衝區。視頻圖像和OSD,位圖等數據在RenderFrame調用時都會先拷貝到後備緩衝區,然後再送到前緩衝區(通過Blt操作)顯示。對於YUY2和YV12的圖像會直接拷貝到後備緩衝區(前提是和表面的格式對應)。但因爲傳入的視頻圖像也可能是RGB24格式,這時候就需要將圖像進行轉換,轉成YV12,然後再拷貝到後備緩衝區。

至於怎麼創建Directdraw表面,怎麼將視頻數據從外部緩存拷貝到DrawDraw表面,怎麼顯示字幕到和位圖到後備緩存,實現也比較簡單,讀者自己詳細看一下CDXDrawPainter類的實現。

最後,我們要運行Filter Graph,可以調用IMediaControl接口的Run方法。

播放的效果如下:

OSD疊加播放界面

代碼資源下載地址:https://download.csdn.net/download/zhoubotong2012/11855579

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