在上兩講中,筆者介紹了DirectShow的應(yīng)用原理以及開發(fā)Filter之前的一些預備知識。這一講,筆者就要手把手教你如何寫自己的Filter啦。
首先,從VC++的項目開始(請確認你已經(jīng)給VC++配置好了DirectX的開發(fā)環(huán)境)。寫自己的Filter,第一步是使用VC++建立一個Filter的項目。由于DirectX SDK提供了很多Filter的例子項目(位于DXSDK\samples\Multimedia\DirectShow\ Filters目錄下),最簡單的方法就是拷貝一個,然后再在此基礎(chǔ)上修改。但如果你是Filter開發(fā)的初學者,筆者并不贊成這么做。
自己新建一個Filter項目也很簡單。使用VC++的向?qū)?,建立一個空的”Win32 Dynamic-link Library”項目。注意,幾個文件是必須有的:.def文件,定義四個導出函數(shù);定義Filter類的.cpp文件和.h文件,并在.cpp文件中定義Filter的注冊信息以及兩個Filter的注冊函數(shù):DllRegisterServer和DllUnregisterServer。(注:Filter的注冊信息是Filter在注冊時寫到注冊表里的內(nèi)容,格式可以參考SDK的示例代碼,F(xiàn)ilter相關(guān)的GUID務(wù)必使用GuidGen.exe產(chǎn)生。)接下去進行項目的設(shè)置(Project->Settings…)。此時,你可以打開一個SDK的例子項目進行對比,有些宏定義完全可以照抄,最后注意將輸出文件的擴展名改為.ax。
上一講曾經(jīng)提到過,在寫Filter之前,選擇一個合適的Filter基類是至關(guān)重要的。為此,你必須對幾個Filter的基類有相當?shù)牧私狻T趯嶋H應(yīng)用中,F(xiàn)ilter的基類并不總是選擇CBaseFilter的。相反,因為我們絕大部分寫的都是中間的傳輸Filter(Transform Filter),所以基類選擇CTransformFilter和CTransInPlaceFilter的居多。如果我們寫的是源Filter,我們可以選擇CSource作為基類;如果是Renderer Filter,可以選擇CBaseRenderer或CBaseVideoRenderer等。
總之,選擇好Filter的基類是很重要的。當然,選擇Filter的基類也是很靈活的,沒有絕對的標準。能夠通過CTransformFilter實現(xiàn)的Filter當然也能從CBaseFilter一步一步實現(xiàn)。下面,筆者就從本人的實際經(jīng)驗出發(fā),對Filter基類的選擇提出幾點建議供大家參考。
首先,你必須明確這個Filter要完成什么樣的功能,即要對Filter項目進行需求分析。請盡量保持Filter實現(xiàn)的功能的單一性。如果必要的話,你可以將需求分解,由兩個(或者更多的)功能單一的Filter去實現(xiàn)總的功能需求。
其次,你應(yīng)該明確這個Filter大致在整個Filter Graph的位置,這個Filter的輸入是什么數(shù)據(jù),輸出是什么數(shù)據(jù),有幾個輸入Pin、幾個輸出Pin等等。你可以畫出這個Filter的草圖。弄清這一點十分重要,這將直接決定你使用哪種“模型”的Filter。比如,如果Filter僅有一個輸入Pin和一個輸出Pin,而且一進一處的媒體類型相同,則一般采用CTransInPlaceFilter作為Filter的基類;如果媒體類型不一樣,則一般選擇CTransformFilter作為基類。
再者,考慮一些數(shù)據(jù)傳輸、處理的特殊性要求。比如Filter的輸入和輸出的Sample并不是一一對應(yīng)的,這就一般要在輸入Pin上進行數(shù)據(jù)的緩存,而在輸出Pin上使用專門的線程進行數(shù)據(jù)處理。這種情況下,F(xiàn)ilter的基類選擇CSource為宜(雖然這個Filter并不是源Filter)。
當Filter的基類選定了之后,Pin的基類也就相應(yīng)選定了。接下去,就是Filter和Pin上的代碼實現(xiàn)了。有一點需要注意的是,從軟件設(shè)計的角度上來說,應(yīng)該將你的邏輯類代碼同F(xiàn)ilter的代碼分開。下面,我們一起來看一下輸入Pin的實現(xiàn)。你需要實現(xiàn)基類所有的純虛函數(shù),比如CheckMediaType等。在CheckMediaType內(nèi),你可以對媒體類型進行檢驗,看是否是你期望的那種。因為大部分Filter采用的是推模式傳輸數(shù)據(jù),所以在輸入Pin上一般都實現(xiàn)了Receive方法。有的基類里面已經(jīng)實現(xiàn)了Receive,而在Filter類上留一個純虛函數(shù)供用戶重載進行數(shù)據(jù)處理。這種情況下一般是無需重載Receive方法的,除非基類的實現(xiàn)不符合你的實際要求。而如果你重載了Receive方法,一般會同時重載以下三個函數(shù)EndOfStream、BeginFlush和EndFlush。我們再來看一下輸出Pin的實現(xiàn)。一般情況下,你要實現(xiàn)基類所有的純虛函數(shù),除了CheckMediaType進行媒體類型檢查外,一般還有DecideBufferSize以決定Sample使用內(nèi)存的大小,GetMediaType提供支持的媒體類型。最后,我們看一下Filter類的實現(xiàn)。首先當然也要實現(xiàn)基類的所有純虛函數(shù)。除此之外,F(xiàn)ilter還要實現(xiàn)CreateInstance以提供COM的入口,實現(xiàn)NonDelegatingQueryInterface以暴露支持的接口。如果我們創(chuàng)建了自定義的輸入、輸出Pin,一般我們還要重載GetPinCount和GetPin兩個函數(shù)。
Filter框架的實現(xiàn)大致就是這樣。你或許還想知道怎樣在Filter上實現(xiàn)一個自定義的接口,以及怎么實現(xiàn)Filter的屬性頁等等。限于篇幅,筆者就不展開闡述了。其實,這些問題都能在SDK的示例項目中找到答案。其他的,關(guān)于在實際編程中應(yīng)該注意的一些問題,筆者整理了一下,供大家參考。
1.    鎖(Lock)問題
DirectShow應(yīng)用程序至少包含有兩條線程:一條主線程和一條數(shù)據(jù)傳輸線程。既然是多線程,肯定會碰到線程同步的問題。Filter有兩種鎖:Filter對象鎖和數(shù)據(jù)流鎖。Filter對象鎖用于Filter級別的如Filter狀態(tài)轉(zhuǎn)換、BeginFlush、EndFlush等;數(shù)據(jù)流鎖用于數(shù)據(jù)處理線程內(nèi),比如Receive、EndOfStream等。如果這兩種鎖沒有搞清楚,很容易產(chǎn)生程序的死鎖,這一點特別需要提醒。
2.    EndOfStream問題
當Filter接收到這個“消息”,意味著上一級Filter的數(shù)據(jù)都已經(jīng)發(fā)送完畢。在這之后,如果Receive再有數(shù)據(jù)接收,也不應(yīng)該去理睬它。如果Filter對輸入Pin上的數(shù)據(jù)進行了緩存,在接收到EndOfStream后應(yīng)確保所有緩存的數(shù)據(jù)都已經(jīng)處理過了才能返回。
3.    Media Seeking問題
一般情況下,你只需要在Filter的輸出Pin上實現(xiàn)NonDelegatingQueryInterface方法,當用戶申請得到IID_ImediaPosition接口或IID_IMediaSeeking接口時將請求往上一級Filter的輸出Pin上傳遞。當Filter Graph進行Mediaseeking的時候,一般會調(diào)用Filter上的BeginFlush、EndFlush和NewSegment。如果你的Filter對數(shù)據(jù)進行了緩存,你就要重載它們,并做出相應(yīng)的處理。如果你的Filter負責給發(fā)送出去的Sample打時間戳,那么,在Mediaseeking之后應(yīng)該重新從零開始打起。
4.    關(guān)于使用專門的線程
如果你使用了專門的線程進行數(shù)據(jù)的處理和發(fā)送,你需要特別小心,不要讓線程進行死循環(huán),并且要讓線程處理函數(shù)能夠去時時檢查線程命令。應(yīng)該確保在Filter結(jié)束工作的時候,線程也能正常地結(jié)束。有時候,你把GraphEdit程序關(guān)掉,但GraphEdit進程仍在內(nèi)存中,往往就是因為數(shù)據(jù)線程沒有安全關(guān)閉這個原因。
5.    如何從媒體類型中獲取信息
比如,你想在輸入Pin連接的媒體類型中,獲取視頻圖像的寬、高等信息,你應(yīng)該在輸入Pin的CompleteConnect方法中實現(xiàn),而不要在SetMediaType中。