Windows 視頻Directshow開發介紹


在Windows平台上實現一個文件播放器有什么好的開發庫和方案呢?方案有很多,比如基於FFmpeg,VLC的插件,mplayer,Directshow。用FFmpeg來實現文件格式解析、分離視頻音頻流、解碼是很方便的,但是要實現一個播放器,還要實現視音頻的顯示和回放、視音頻同步的處理,要做很多額外的開發工作,比較麻煩。而用VLC的插件不方便調試,擴充功能要改VLC的源代碼,不靈活。而用Directshow來做播放器在Windows平台上是比較成熟和簡單的方案,所以特別給大家介紹一下。

微軟的Directshow是一個實現多媒體播放,視音頻處理等功能的多媒體庫,並且提供了Filter的機制使得多媒體任務的各個處理單元(讀取數據、分離、解碼、編碼、回放)達到模塊化,提高了復用性。開發者可以用別人提供的Filter,也可以實現自己的Filter來處理特定的任務。每個Filter至少有一個Pin,Pin分輸入Pin和輸出Pin,按照Filter的類型輸入Pin和輸出Pin的數目不等。每個Pin都有自己接受的媒體格式,比如視頻渲染器Filter的輸入Pin只接受RGB或YUV的格式。兩個Filter的Pin連接前需要對Pin進行媒體類型協商(確定兩方的Pin的媒體類型是否一致),如果協商成功,兩個Pin就可以連接起來。一連串的Filter通過這種機制連接形成一個Graph,從而完成復雜的多媒體任務,比如播放文件,錄制麥克風的音頻,采集攝像頭圖像。下面演示了一個播放AVI文件的Graph的Filter連接圖:

在上面的Graph圖中,包括幾種Filter:Source Filter,Splitter,Video Decoder,Audio Decoder,Video Renderer,Audio Renderer。而這些Filter大部分系統已經提供了,還有少數功能需要第三方的Filter來完成。因為多媒體文件的格式眾多,所以播放文件的Graph需要加入支持不同格式的Filter,其中最主要的Filter是:分離器,解碼器。解碼器Filter目前有很多了,免費的而又出名的解碼器Filter有FFDshow。FFDshow底層用到了FFmpeg,基於Directshow框架來實現,解碼效率很高,但是比較龐大和臃腫,現在已基本不維護了。並且FFDshow只提供了編碼器和解碼器,還沒有實現分離器。以前微軟平台上的Directshow開發員會了支持盡量多的媒體格式,想盡辦法從各處收集到各種分離器Filter,結果程序打包之后體積很大,並且兼容各種格式要插入不同的的Filter使程序變得復雜。很多第三方Filter不開源,加上測試不夠,穩定性不好,容易產生很多問題。幸好,現在有個老外開發了一套Directshow插件,叫LAV Filters。不錯,是一套,包括分離器,視頻解碼器和音頻解碼器。只要安裝這套插件,播放大多數的多媒體格式基本上沒問題了,省了開發員很多功夫。還有,這個LAV Filters套件還是開源的,作者對工程的更新速度很快,已經修復了很多Bug,現在變得很穩定了,連大名鼎鼎的開源播放器MPC也使用了LAV Filters,將它列為優先選用的插件。

說了那么多,大家一定很想使用LAV Filters來做開發吧?不用急,我下面就介紹如何在Directshow Graph中使用它。
首先,我們要知道這幾個Filter的CLSID,下面是這幾個Filter的CLSID定義:
  DEFINE_GUID(CLSID_LAVSplitter,

                0x171252A0, 0x8820, 0x4AFE, 0x9D, 0xF8, 0x5C, 0x92, 0xB2, 0xD6, 0x6B, 0x04);

  DEFINE_GUID(CLSID_LAVVideoDecoder, 
                0xEE30215D, 0x164F, 0x4A92, 0xA4, 0xEB, 0x9D, 0x4C, 0x13, 0x39, 0x0F, 0x9F);

  DEFINE_GUID(CLSID_LAVAudioDecoder,

                0xE8E73B6B, 0x4CB3, 0x44A4, 0xBE, 0x99, 0x4F, 0x7B, 0xCB, 0x96, 0xE4, 0x91);

  另外還有一個LAV Source Filter,CLSID是:

  DEFINE_GUID(CLSID_LAVSource, 

                0xB98D13E7, 0x55DB, 0x4385, 0xA3, 0x3D, 0x09, 0xFD, 0x1B, 0xA2, 0x63, 0x38);

 這個LAV Source Filter的功能跟LAVSplitter差不多,都是分離器,不同的是它沒有輸入Pin,它集成了了Async Source Filter + LAVSplitter的功能。
下面我們就准備在Directshow的Graph中加入這些Filter。首先,我們需要實現一個類來封裝播放文件的所有接口,下面是類的聲明:

class CDXGraph
{
private:
	IGraphBuilder *     mGraph;  
	IMediaControl *	    mMediaControl;
	IMediaEventEx *	    mEvent;
	IBasicVideo *	    mBasicVideo;
	IBasicAudio *	    mBasicAudio;
	IVideoWindow  *	    mVideoWindow;
	IMediaSeeking *	    mSeeking;
	IBaseFilter *       m_pSourceFilter;
	IBaseFilter *       m_pDumpFilter;
 
	DWORD		    mObjectTableEntry; 
	UINT                m_GraphMsg;
 
public:
	bool           m_bEnableSound;
	int            m_nJPEGQuality;
	SIZE           m_PictureSize;
	UINT           m_nFramerate;
 
public:
	CDXGraph();
	virtual ~CDXGraph();
 
public:
	virtual bool Create(void);
	virtual void Release(void);
	virtual bool Attach(IGraphBuilder * inGraphBuilder);
 
	IGraphBuilder * GetGraph(void); // Not outstanding reference count
	IMediaEventEx * GetEventHandle(void);
 
        UINT   GetGraphState();
 
	bool   SetClipSourceRect(RECT rcSource);
	bool   SetDisplayWindow(HWND inWindow, LPRECT  rcTarget);
	bool   SetNotifyWindow(HWND inWindow, long lMsg);
	bool   ResizeVideoWindow(long inLeft, long inTop, long inWidth, long inHeight);
 
	void   HandleEvent(LONG * eventCode);
 
	bool   Run(void);        // Control filter graph
	bool   Stop(void);
	bool   Pause(void);
 
	bool   IsRunning(void);  // Filter graph status
	bool   IsStopped(void);
	bool   IsPaused(void);
 
	//bool SetFullScreen(BOOL inEnabled);
	//bool GetFullScreen(void);
 
	// IMediaSeeking
	bool    GetCurrentPosition(LONGLONG * outPosition); //獲得當前播放時間(單位為100納秒, 等於10^(-4) ms )
	bool    GetStopPosition(LONGLONG * outPosition);
	bool    SetCurrentPosition(LONGLONG inPosition); //設置當前播放時間(單位為100納秒, 等於10^(-4) ms )
	bool    SetStartStopPosition(LONGLONG inStart, LONGLONG inStop);
	bool    GetDuration(LONGLONG * outDuration); //獲得文件時間長度(單位為100納秒, 等於10^(-4) ms )
	bool    SetPlaybackRate(double inRate); //設置播放速度
	bool    GetPlaybackRate(double * outRate);
 
	// Attention: range from -10000 to 0, and 0 is FULL_VOLUME.
	bool    SetAudioVolume(long inVolume);//調節音量
	long    GetAudioVolume(void);
	// Attention: range from -10000(left) to 10000(right), and 0 is both.
	bool    SetAudioBalance(long inBalance);
	long    GetAudioBalance(void);
 
	bool    RenderFile(const TCHAR * inFile, DWORD & dwError);
	bool    SnapshotBitmap(const TCHAR * outFile);
	bool    GetTotalFrames(LONGLONG * outNum);
 
private:
	void    AddToObjectTable(void) ;
	void    RemoveFromObjectTable(void);
	
	bool    QueryInterfaces(void);
 
	HRESULT ConnectFilters(IPin * inOutputPin, IPin * inInputPin, const AM_MEDIA_TYPE * inMediaType = 0);
	void    DisconnectFilters(IPin * inOutputPin);
 
	HRESULT  RenderFilter(IBaseFilter * pFilter);
	HRESULT  GetVideoProps(IPin * pVideoOutputPin);
};

  

 接着列出最核心的一個函數CDXGraph::RenderFile,它負責創建FilterGraph,將各個Filter添加到Graph和連接起來。下面是函數的實現:

bool CDXGraph::RenderFile(const TCHAR * inFile, DWORD & dwError)
{
	if (mGraph == NULL)
	{
	   dwError = ERROR_INVALID_POINTER;
           return false;
	}
 
	dwError = 0;
 
	HRESULT hr;
	
	bool bVideoPinConnected = false;
	bool bAudioPinConnected = false;
	bool bPrivateStreamPinConnected = false;
 
#ifndef UNICODE
	WCHAR   wszFilePath[MAX_PATH] = {0};
	MultiByteToWideChar(CP_ACP, 0, inFile, -1, wszFilePath, MAX_PATH);
#else
	TCHAR   wszFilePath[MAX_PATH] = {0};
    lstrcpy(wszFilePath, inFile);
#endif
     
	CComPtr<IFileSourceFilter> pFileSource;
	CComPtr<IBaseFilter> pSplitter;
	CComPtr<IBaseFilter> pVideoDecoder;
	CComPtr<IBaseFilter> pVideoRenderer;
 
#if 0
	hr = AddFilterByCLSID(mGraph, CLSID_MpegSourceFilter, L"Mpeg Splitter ", &pSplitter );
	if(FAILED(hr))
	{
		OutputDebugString("Add Mpeg Splitter Filter Failed \n");
		return FALSE;
	}
#else
	hr = AddFilterByCLSID(mGraph, CLSID_LAVSource, L"LAV Source Splitter ", &pSplitter );
	if(FAILED(hr))
	{
		OutputDebugString("Add LAV Splitter Filter Failed \n");
		return FALSE;
	}
#endif
	hr  = pSplitter->QueryInterface(IID_IFileSourceFilter, (void**)&pFileSource);
	if(FAILED(hr))
	{
		dwError = ERROR_GET_INTERFACE_FAIL;
		return false;
	}
 
	hr = pFileSource->Load(wszFilePath, NULL);
	if(FAILED(hr))
	{
		dwError = ERROR_LOADFILE_FAIL;
		return false;
	}
 
	hr = AddFilterByCLSID(mGraph, CLSID_LAVVideoDecoder, L"LAV Video Decoder", &pVideoDecoder);
	if(FAILED(hr))
	{
		OutputDebugString("Add LAV Video Filter Failed \n");
		//return FALSE;
	}
		  
	hr = AddFilterByCLSID(mGraph, CLSID_VideoMixingRenderer9, L"VMR9 Renderer ", &pVideoRenderer);
	if(FAILED(hr))
	{
		OutputDebugString("Add VMR9 Renderer Filter Failed \n");
		return FALSE;
	}
 
	hr = RenderFilter(pSplitter);
	if(SUCCEEDED(hr))
		bVideoPinConnected = true;
	else
		bVideoPinConnected = false;
 
	if(!bAudioPinConnected && !bVideoPinConnected)
	{
	    OutputDebugString("RenderFilter Failed!\n");
 
	    OutputDebugString("第二次嘗試用RenderFile自動連接\n");
 
		if (FAILED(mGraph->RenderFile(wszFilePath, NULL)))
		{
			OutputDebugString("RenderFile Failed!\n\n");
 
			dwError = ERROR_AUTO_RENDERFILE_FAIL;
			return false;
		}
		
		IBaseFilter * pVideoRenderer = NULL;
		hr = FindVideoRenderer(mGraph, &pVideoRenderer);
		if(SUCCEEDED(hr))
		{
		  CComPtr<IPin> pVideoPin = GetInPin(pVideoRenderer, 0);
		  GetVideoProps(pVideoPin);
		}
	}
 
	if(!m_bEnableSound)
	{
	   SetAudioVolume(-10000);
	}
 
	OutputDebugString("RenderFile Succeeded!\n");
 
   return true;
}

CDXGraph::RenderFile函數顯示加入了這幾個Filter:LAV Source,LAV Video Decoder,VMR9。其中,VMR9是視頻渲染器,負責渲染圖像和根據時間戳控制視頻幀何時顯示;LAVSource負責讀取文件和從文件容器里分離出視頻流和音頻流,當我們調用AddFilterByCLSID函數(實際上調用了COM接口CoCreateInstance函數)創建這個Filter實例時,它是沒有加載文件的,我們必須查詢它的IFileSourceFilter接口指針,通過這個接口指針設置文件路徑,把文件加載進去,如果文件加載成功,LAV Source會根據文件容器里媒體流的數目生成對應的OutputPin。接着,加入LAV Video Decoder,然后調用RenderFilter函數把LAV Source的每個OutputPin自動與下游的Filter進行連接,因為連接Filter的時候Graph Manager會優先選用已經添加到Graph中的Filter,那么LAV Source的Video Output Pin就會嘗試與Video Decoder進行連接,而Audio Output Pin也一樣,但是由於我們沒有顯示加入任何的Audio Decoder,Graph Manager會從系統安裝的Filter中找到一個合適的解碼器Filter插入進去,然后自動連接兩個Filter的OutputPin與InputPin。如果所有Filter連接成功,那么播放文件Graph的Filter鏈路圖就像下面這樣子:

上面所講的內容是Directshow開發很基本的操作,相信熟悉Directshow的讀者看了覺得很熟悉和簡單。

Mitov組件商使用的就是Directshow和Windows Api模式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM