在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模式。