2009年9月4日

DirectShow實踐經驗雜談

DirectShow實踐經驗雜談
流媒體的處理,以其複雜性和技術性,一向廣受工業界的關注。


特別伴隨著英特網的普及,流媒體在網路上的廣泛套用,怎樣使流媒體的處理變得簡單而富有成效逐漸成為了焦點問題。選項一種合適的套用方案,事半功倍。此時,微軟的DirectShow,給了我們一個不錯的選項。

DirectShow是微軟公司提供的一套在Windows平台上進行流媒體處理的開發包,與DirectX開發包一起發佈。目前,DirectX最新版本為8.1。


那麼,DirectShow能夠做些什麼呢?


且看,DirectShow為多媒體流的捕捉和回放提供了強有力的支持。運用DirectShow,我們可以很方便地從支持WDM驅動模型的採集卡上捕獲資料,並且進行相應的後期處理乃至存儲到文件中。


它廣泛地支持各種媒體格式,包括Asf、Mpeg、Avi、Dv、Mp3、Wave等等,使得多媒體資料的回放變得輕而易舉。


另外,DirectShow還集成了DirectX其它部分(譬如DirectDraw、DirectSound)的技術,直接支持DVD的播放,視瀕的非線性編輯,以及與數字攝像機的資料交換。


更值得一提的是,DirectShow提供的是一種開放式的開發環境,我們可以根據自己的需要定製自己的元件。


接下去,我們需要對DirectShow系統有個整體的印象。參見以下DirectShow的系統示意圖:

圖中央最大的一塊即是DirectShow系統。


DirectShow使用一種叫Filter Graph的模型來管理整個資料流的處理程序;參與資料處理的各個功能模組叫做Filter;各個Filter在Filter Graph中按一定的順序連接成一條「流水線」協同工作。


大家可以看到,按照功能來分,Filter大致分為三類:Source Filters、Transform Filters和Rendering Filters。


Source Filters主要負責取得資料,資料來源可以是文件、英特網、或者電腦裡的採集卡、數字攝像機等,然後將資料往下傳輸;Transform Fitlers主要負責資料的格式轉換、傳輸;Rendering Filtes主要負責資料的最終去向,我們可以將資料送給音效卡、顯示卡進行多媒體的演示,也可以輸出到文件進行存儲。值得注意的是,三個部分並不是都只 有一個Filter去完成功能。恰恰相反,每個部分往往是有幾個Fitler協同工作的。


譬如,Transform Filters可能包含了一個Mpeg的解碼Filter、以及視瀕色彩空間的轉換Filter、音瀕採樣頻率轉換Filter等等。


除了系統提供的大量Filter外,我們可以定製自己的Filter,以完成我們需要的功能。下圖是一條典型的Avi文件回放Filter Graph鏈路:

在DirectShow系統之上,我們看到的,即是我們的應用程式(Application)。


應用程式要按照一定的意圖建立起相應的Filter Graph,然後通過Filter Graph Manager來控制整個的資料處理程序。


DirectShow能在Filter Graph執行的時候接收到各種事件,並通過消息的方式傳送到我們的應用程式。這樣,就實現了應用程式與DirectShow系統之間的交互。下圖給出了DirectShow應用程式開發的一般程序:


以上簡單介紹了DirectShow的系統結構,希望大家對這個鑽勁的套用框架已經有了大概的認識。如果你有興趣,可以詳細研究DirectX的說明 我的文件。


DirectShow是一個強大的開發包;另外,它是關於COM的,因此要求程序員具有COM編程的一些基本知識。關於如何深入學習DirectShow 套用結構以及開發自己的Filter,請參閱筆者的後續文章。筆者將從編程的角度,詳細講述來源於實際工作中的經驗之談。


在上一講中,筆者介紹了DirectShow的總體系統框架。從這一講開始,我們要從程序員的角度,進一步深入探討一下DirectShow的套用以及Filter的開發。
在這之前,筆者首先要特別提一下微軟提供的一個Filter測試工具——GraphEdit,它的路徑在DXSDK\bin\DXUtils\GraphEdit.exe。

(如果您還沒有安裝DirectX SDK,請到微軟的網站上去下載。)通過這個工具,我們可以很直觀地看到Filter Graph的執行及處理流程,方便我們進行程序偵錯。


(如果您手邊就有電腦,還等什麼,馬上體驗一下吧:
執行GraphEdit,執行File->Render Media File…選項一個媒體文

件;當Filter Graph構建成功後,按下工作列的執行按鈕;您就能看到剛才選項的媒體文件被回放出來了!看到了吧,寫一個媒體播放器也就這麼回事!)
接下去,我們開講Filter的開發。

學習DirectShow Filter的開發,不外乎以下幾種方法:看說明 我的文件、看示例程式碼和看SDK基類來源碼。


看說明 我的文件,應著重於總體概念上的理解;看示例程式碼應與基類來源碼的研究同步進行,因為自己寫Filter,關鍵的第一步是選項一個合適的Filter基 類和Pin的基類。對於Filter的把握,一般認為要掌握以下三方面的內容:Filter之間Pin的連接、Filter之間的資料傳輸以及流媒體的隨 機訪問(或者說流的定位)。下面就開始分別進行闡述。



所謂的Filter Pin之間的連接,實際上是Pin之間Media Type(媒體類型)的一個協商程序。連接總是從輸出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之間連接完成,也就是說,連接雙方通過協商取得了一種大家都支持的媒體類型之後,即開始為資料傳輸做準備。這些準備工作中,最重要 的是Pin上的記憶體分配器的協商,一般也是由輸出Pin發起。在DirectShow Filter之間,資料是通過一個一個資料包傳送的,這個資料包叫做Sample。


Sample本身是一個COM對象,擁有一段記憶體用以裝載資料,Sample就由記憶體分配器(Allocator)來統一管理。


已成功連接的一對輸出、輸入Pin使用同一個記憶體分配器,所以資料從輸出Pin傳送到輸入Pin上是無需記憶體拷貝的。而典型的資料拷貝,一般發生在 Filter內部,從Filter的輸入Pin上讀取資料後,進行一定意圖的處理,然後在Filter的輸出Pin上填充資料,然後繼續往下傳輸。下面, 我們就直接闡述一下Filter之間的資料傳送。


首先,大家要區分一下Filter的兩種主要的資料傳輸模式:推模式(Push Model)和拉模式(Pull Model)。參考圖如下:



所謂推模式,即源Filter(Source Filter)自己能夠產生資料,並且一般在它的輸出Pin上有獨立的子線程負責將資料傳送出去,一般的情況如代表WDM模型的採集卡的Live Source Filter;而所謂拉模式,即源Filter不具有把自己的資料送出去的能力,這種情況下,一般源Filter後緊跟著接一個Parser Filter或Splitter Filter,這種Filter一般在輸入Pin上有個獨立的子線程,負責不斷地從源Filter索取資料,然後經過處理後將資料傳送下去,一般的情況如 文件源。推模式下,源Filter是主動的;拉模式下,源Filter是被動的。而事實上,如果將上圖拉模式中的源Filter和Splitter Filter看成另一個虛擬的源Filter,則後面的Filter之間的資料傳輸也與推模式完全相同。


那麼,資料到底是怎麼通過連接著的Pin傳輸的呢?首先來看推模式。


在源Filter後面的Filter輸入Pin上,一定實現了一個IMemInputPin接頭,資料正是通過上一級Filter使用這個接頭的 Receive方法進行傳輸的。值得注意的是(上面已經提到過),資料從輸出Pin通過Receive方法使用傳輸到輸入Pin上,並沒有進行記憶體拷 貝,它只是一個相當於資料到達的「通知」。


再看一下拉模式。拉模式下的源Filter的輸出Pin上,一定實現了一個IAsyncReader接頭;其後面的Splitter Filter,就是通過使用這個接頭的Request方法或者SyncRead方法來獲得資料。


Splitter Filter然後像推模式一樣,使用下一級Filter輸入Pin上的IMemInputPin接頭Receive方法實現資料的往下傳送。深入瞭解這部分內容,請認真研讀SDK的基類來源碼(位於
DXSDK\samples\Multimedia\DirectShow\BaseClasses\source.cpp和pullpin.cpp)。


下面,我們來講一下流的定位(Media Seeking)。在GraphEdit中,當我們成功構建了一個Filter Graph之後,我們就可以播放它。在播放中,我們可以看到進度條也在相應地前進。當然,我們也可以通過移到進度條,實現隨機訪問。


要做到這一點,在應用程式級別應該可以知道Filter Graph總共要播放多長時間,當前播放到什麼位置等等。那麼,在Filter級別,這一點是怎麼實現的呢?

我們知道,若干個Filter通過Pin的相互連接組成了Filter Graph。


而這個Filter Graph是由另一個COM對像Filter Graph Manager來管理的。通過Filter Graph Manager,我們就可以得到一個IMediaSeeking的接頭來實現對流媒體的定位。



在Filter級別,我們可以看到,Filter Graph Manager首先從最後一個Filter(Renderer Filter)開始,詢問上一級Filter的輸出Pin是否支持IMediaSeeking接頭。如果支持,則返回這個接頭;如果不支持,則繼續往上一 級Filter詢問,直到源Filter。


一般在源Filter的輸出Pin上實現IMediaSeeking接頭,它告訴使用者總共有多長時間的媒體內容,當前播放位置等信息。



(如果是文件源,一般在Parser Filter或Splitter Filter實現這個接頭。)對於Filter開發者來說,如果我們寫的是源Filter,我們就要在Filter的輸出Pin上實現 IMediaSeeking這個接頭;如果寫的是中間的傳輸Filter,只需要在輸出Pin上將用戶的獲得接頭請求往上傳遞給上一級Filter的輸出 Pin;如果寫的是Renderer Filter,需要在Filter上將用戶的獲得接頭請求往上傳遞給上一級Filter的輸出Pin。


進一步的瞭解,請認真研讀SDK的基類來源碼(位於DXSDK\samples\Multimedia\DirectShow\BaseClasses \transfrm.cpp的類方法CTransformOutputPin::NonDelegatingQueryInterface實現和 ctlutil.cpp中類CPosPassThru的實現)。



以上我們介紹了一下如何學習DirectShow Filter開發,以及一些開始寫自己的Filter之前的預備知識。下一講,筆者將根據自己開發Filter的經驗,一步一步教你如何寫自己的Filter。


在上兩講中,筆者介紹了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中。


DirectX媒體對像(DirectX Media Objects,簡稱DMOs),是微軟提供的另一種流資料處理COM元件。與DirectShow filter相比,DMO有很多相似之處。對filter原理的熟悉,將會大大說明 你對DMO的學習。另外,DMO也因其結構簡單、易於新增和使用而倍受微軟推崇。

下面,我們來看一看DMO與filter的對比。

1. DMO比filter實現的功能要少很多,這使得DMO「體積」很小;

2. DMO使用起來比filter更有靈活性。DMO的使用不需要filter graph,應用程式可以直接與DMO交互。而DMO也可以通過一個DMO wrapper filter工作於DirectShow環境;

3. DMO總是同步處理資料,不像filter有獨立的資料傳送線程,需要考慮多線程編程問題;

4. 與傳統的編解碼管理器ACM、VCM相比,用DMO開發的編解碼器是關於COM的,更易於擴展。並且DMO支持多個輸入和多個輸出;

5. DMO不需要像filter一樣分配資料傳送的記憶體,而有DMO的使用者負責;


6. DMO是一個獨立功能模組,不需要像filter一樣連接成一條鏈路;


7. DMO不需要像filter一樣將資料「推」下去,資料的輸入輸出都是由DMO的使用者完成的;

所有這些優點,使得DMO成為微軟對於Encoder和Decoder開發的重點推薦模式。


DirectX 9.0 SDK中,微軟更是把DMO從DirectShow中分離出來,而對於一些transform filter,微軟也推薦用DMO的方式來替換。


關於DMO的使用方式,目前大概有兩種:

一種是應用程式直接使用DMO,另一種就是在DirectShow filter中的套用。


後者比較簡單,只是使用了一個DMO wrapper filter。在DirectShow應用程式中,DMO是對用戶透明的,所有使用DMO的工作均由DMO wrapper filter來完成。參見下面的程式碼。


// Create the DMO Wrapper filter.
IBaseFilter *pFilter;
HRESULT hr = CoCreateInstance(CLSID_DMOWrapperFilter, NULL,
CLSCTX_INPROC_SERVER, IID_IBaseFilter,
reinterpret_cast(&pFilter));

if (SUCCEEDED(hr))
{
// Query for IDMOWrapperFilter.
IDMOWrapperFilter *pDmoWrapper;
hr = pFilter->QueryInterface(IID_IDMOWrapperFilter,
reinterpret_cast(&pDmoWrapper));

if (SUCCEEDED(hr))
{
// Initialize the filter.
hr = pDmoWrapper->Init(CLSID_MyDMO, DMOCATEGORY_VIDEO_EFFECT);
pDmoWrapper->Release();

if (SUCCEEDED(hr))
{
// Add the filter to the graph.
hr = pGraph->AddFilter(pFilter, L"My DMO");
}
}
pFilter->Release();
}
而對於DMO的直接使用,以下幾點是要特別注意的。

1. 在處理資料之前,必須為每條輸入輸出stream設定media type(Optional stream除外);

2. 從DMO從獲取的media type未必包含format塊,但是在給DMO設定media type時,務必帶上這部分信息(MIDI除外);

3. 應用程式必須自己負責分配資料緩衝。


緩衝的大小可以通過使用DMO的IMediaObject::GetInputSizeInfo或 IMediaObject::GetOutputSizeInfo得到。DMO使用的資料緩衝也是一個COM對象,支持ImediaBuffer接頭,與 DirectShow filter的Media Sample類似。


4. 一般的DMO依次使用IMediaObject::ProcessInput和IMediaObject::ProcessOutput處理資料,In-Place的DMO使用IMediaObjectInPlace::Process處理資料。


兩套方法不能混用。

5. 在使用ProcessOutput時,如果返回的標記是DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE,說明資料的資料還沒有完全取出,需要再次使用ProcessOutput。


6. 所有輸入資料都已輸入完成,應該使用DMO的IMediaObject:Discontinuity方法。


7. 如果你想中斷資料處理流程,使用DMO的IMediaObject::Flush。
區別兩種不同的可丟棄stream,標記分別為DMO_OUTPUT_STREAMF_OPTIONAL和DMO_OUTPUT_STREAMF_DISCARDABLE。注意,後者是要設定media type的。

沒有留言: