如何写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	mSplitter;
	CComPtr    mVideoDecoder;
	CComPtr    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 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方法。

播放的效果如下:

如何写Directshow Render Filter并实现视频渲染、叠加字幕和位图功能_第1张图片

代码资源下载地址:https://download.csdn.net/download/zhoubotong2012/11855579

你可能感兴趣的:(directshow,Directshow,叠加字幕,叠加位图)