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