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