在上一講中,筆者介紹了DirectShow的總體系統(tǒng)框架。從這一講開始,我們要從程序員的角度,進一步深入探討一下DirectShow的應用以及Filter的開發(fā)。
在這之前,筆者首先要特別提一下微軟提供的一個Filter測試工具——GraphEdit,它的路徑在DXSDK\bin\DXUtils\GraphEdit.exe。(如果您還沒有安裝DirectX SDK,請到微軟的網(wǎng)站上去下載。)通過這個工具,我們可以很直觀地看到Filter Graph的運行及處理流程,方便我們進行程序調(diào)試。(如果您手邊就有電腦,還等什么,馬上體驗一下吧:運行GraphEdit,執(zhí)行File->Render Media File…選擇一個媒體文件;當Filter Graph構建成功后,按下工具欄的運行按鈕;您就能看到剛才選擇的媒體文件被回放出來了!看到了吧,寫一個媒體播放器也就這么回事!)
接下去,我們開講Filter的開發(fā)。
學習DirectShow Filter的開發(fā),不外乎以下幾種方法:看幫助文檔、看示例代碼和看SDK基類源代碼。看幫助文檔,應著重于總體概念上的理解;看示例代碼應與基類源代碼的研究同步進行,因為自己寫Filter,關鍵的第一步是選擇一個合適的Filter基類和Pin的基類。對于Filter的把握,一般認為要掌握以下三方面的內(nèi)容:Filter之間Pin的連接、Filter之間的數(shù)據(jù)傳輸以及流媒體的隨機訪問(或者說流的定位)。下面就開始分別進行闡述。
所謂的Filter Pin之間的連接,實際上是Pin之間Media Type(媒體類型)的一個協(xié)商過程。連接總是從輸出Pin指向輸入Pin的。要想深入了解具體的連接過程,就必須認真研讀SDK的基類源代碼(位于DXSDK\samples\Multimedia\DirectShow\BaseClasses\amfilter.cpp,類CBasePin的Connect方法)。連接的大致過程為,枚舉欲連接的輸入Pin上所有的媒體類型,逐一用這些媒體類型與輸出Pin進行連接,如果輸出Pin也接受這種媒體類型,則Pin之間的連接宣告成功;如果所有輸入Pin上枚舉的媒體類型輸出Pin都不支持,則枚舉輸出Pin上的所有媒體類型,并逐一用這些媒體類型與輸入Pin進行連接。如果輸入Pin接受其中的一種媒體類型,則Pin之間的連接到此也宣告成功;如果輸出Pin上的所有媒體類型,輸入Pin都不支持,則這兩個Pin之間的連接過程宣告失敗。
有一點需要注意的是,上述的輸入Pin與輸出Pin一般不屬于同一個Filter,典型的是上一級Filter(也叫Upstream Filter)的輸出Pin連向下一級Filter(也叫Downstream Filter)的輸入Pin。如下圖所示:

當Filter的Pin之間連接完成,也就是說,連接雙方通過協(xié)商取得了一種大家都支持的媒體類型之后,即開始為數(shù)據(jù)傳輸做準備。這些準備工作中,最重要的是Pin上的內(nèi)存分配器的協(xié)商,一般也是由輸出Pin發(fā)起。在DirectShow Filter之間,數(shù)據(jù)是通過一個一個數(shù)據(jù)包傳送的,這個數(shù)據(jù)包叫做Sample。Sample本身是一個COM對象,擁有一段內(nèi)存用以裝載數(shù)據(jù),Sample就由內(nèi)存分配器(Allocator)來統(tǒng)一管理。已成功連接的一對輸出、輸入Pin使用同一個內(nèi)存分配器,所以數(shù)據(jù)從輸出Pin傳送到輸入Pin上是無需內(nèi)存拷貝的。而典型的數(shù)據(jù)拷貝,一般發(fā)生在Filter內(nèi)部,從Filter的輸入Pin上讀取數(shù)據(jù)后,進行一定意圖的處理,然后在Filter的輸出Pin上填充數(shù)據(jù),然后繼續(xù)往下傳輸。下面,我們就具體闡述一下Filter之間的數(shù)據(jù)傳送。
首先,大家要區(qū)分一下Filter的兩種主要的數(shù)據(jù)傳輸模式:推模式(Push Model)和拉模式(Pull Model)。參考圖如下:


所謂推模式,即源Filter(Source Filter)自己能夠產(chǎn)生數(shù)據(jù),并且一般在它的輸出Pin上有獨立的子線程負責將數(shù)據(jù)發(fā)送出去,常見的情況如代表WDM模型的采集卡的Live Source Filter;而所謂拉模式,即源Filter不具有把自己的數(shù)據(jù)送出去的能力,這種情況下,一般源Filter后緊跟著接一個Parser Filter或Splitter Filter,這種Filter一般在輸入Pin上有個獨立的子線程,負責不斷地從源Filter索取數(shù)據(jù),然后經(jīng)過處理后將數(shù)據(jù)傳送下去,常見的情況如文件源。推模式下,源Filter是主動的;拉模式下,源Filter是被動的。而事實上,如果將上圖拉模式中的源Filter和Splitter Filter看成另一個虛擬的源Filter,則后面的Filter之間的數(shù)據(jù)傳輸也與推模式完全相同。
那么,數(shù)據(jù)到底是怎么通過連接著的Pin傳輸?shù)哪兀渴紫葋砜赐颇J健T谠碏ilter后面的Filter輸入Pin上,一定實現(xiàn)了一個IMemInputPin接口,數(shù)據(jù)正是通過上一級Filter調(diào)用這個接口的Receive方法進行傳輸?shù)摹V档米⒁獾氖牵ㄉ厦嬉呀?jīng)提到過),數(shù)據(jù)從輸出Pin通過Receive方法調(diào)用傳輸?shù)捷斎隤in上,并沒有進行內(nèi)存拷貝,它只是一個相當于數(shù)據(jù)到達的“通知”。再看一下拉模式。拉模式下的源Filter的輸出Pin上,一定實現(xiàn)了一個IAsyncReader接口;其后面的Splitter Filter,就是通過調(diào)用這個接口的Request方法或者SyncRead方法來獲得數(shù)據(jù)。Splitter Filter然后像推模式一樣,調(diào)用下一級Filter輸入Pin上的IMemInputPin接口Receive方法實現(xiàn)數(shù)據(jù)的往下傳送。深入了解這部分內(nèi)容,請認真研讀SDK的基類源代碼(位于DXSDK\samples\Multimedia\DirectShow\BaseClasses\source.cpp和pullpin.cpp)。
下面,我們來講一下流的定位(Media Seeking)。在GraphEdit中,當我們成功構建了一個Filter Graph之后,我們就可以播放它。在播放中,我們可以看到進度條也在相應地前進。當然,我們也可以通過拖動進度條,實現(xiàn)隨機訪問。要做到這一點,在應用程序級別應該可以知道Filter Graph總共要播放多長時間,當前播放到什么位置等等。那么,在Filter級別,這一點是怎么實現(xiàn)的呢?
我們知道,若干個Filter通過Pin的相互連接組成了Filter Graph。而這個Filter Graph是由另一個COM對象Filter Graph Manager來管理的。通過Filter Graph Manager,我們就可以得到一個IMediaSeeking的接口來實現(xiàn)對流媒體的定位。在Filter級別,我們可以看到,F(xiàn)ilter Graph Manager首先從最后一個Filter(Renderer Filter)開始,詢問上一級Filter的輸出Pin是否支持IMediaSeeking接口。如果支持,則返回這個接口;如果不支持,則繼續(xù)往上一級Filter詢問,直到源Filter。一般在源Filter的輸出Pin上實現(xiàn)IMediaSeeking接口,它告訴調(diào)用者總共有多長時間的媒體內(nèi)容,當前播放位置等信息。(如果是文件源,一般在Parser Filter或Splitter Filter實現(xiàn)這個接口。)對于Filter開發(fā)者來說,如果我們寫的是源Filter,我們就要在Filter的輸出Pin上實現(xiàn)IMediaSeeking這個接口;如果寫的是中間的傳輸Filter,只需要在輸出Pin上將用戶的獲得接口請求往上傳遞給上一級Filter的輸出Pin;如果寫的是Renderer Filter,需要在Filter上將用戶的獲得接口請求往上傳遞給上一級Filter的輸出Pin。進一步的了解,請認真研讀SDK的基類源代碼(位于DXSDK\samples\Multimedia\DirectShow\BaseClasses\transfrm.cpp的類方法CTransformOutputPin::NonDelegatingQueryInterface實現(xiàn)和ctlutil.cpp中類CPosPassThru的實現(xiàn))。
以上我們介紹了一下如何學習DirectShow Filter開發(fā),以及一些開始寫自己的Filter之前的預備知識。下一講,筆者將根據(jù)自己開發(fā)Filter的經(jīng)驗,手把手教你如何寫自己的Filter。