摘要:本篇文檔概括性的介紹了
DirectShow的主要組成部分,以及一些Directshow的基本概念。熟悉這些基本的知識對于Directshow的應(yīng)用開發(fā)或者過濾器的開發(fā)者都會有所幫助。
DirectShow是微軟公司提供的一套在Windows平臺上進行流媒體處理的開發(fā)包,與DirectX開發(fā)包一起發(fā)布。那么,DirectShow能夠做些什么呢?且看,DirectShow為多媒體流的捕捉和回放提供了強有力的支持。運用DirectShow,我們可以很方便地從支持WDM驅(qū)動模型的采集卡上捕獲數(shù)據(jù),并且進行相應(yīng)的后期處理乃至存儲到文件中。它廣泛地支持各種媒體格式,包括Asf、Mpeg、Avi、Dv、Mp3、Wave等等,使得多媒體數(shù)據(jù)的回放變得輕而易舉。另外,DirectShow還集成了DirectX其它部分(比如DirectDraw、DirectSound)的技術(shù),直接支持DVD的播放,視頻的
非線性編輯,以及與數(shù)字?jǐn)z像機的數(shù)據(jù)交換。更值得一提的是,DirectShow提供的是一種開放式的開發(fā)環(huán)境,我們可以根據(jù)自己的需要定制自己的組件。
應(yīng)用程序與DirectShow組件以及DirectShow所支持的軟硬件之間的關(guān)系如圖1所示。
 圖1 DirectShow系統(tǒng)框圖 |
1、DirectShow的 Filter
Directshow是基于模塊化,每個功能模塊都采取COM組件方式,稱為Filter。Directshow提供了一系列的標(biāo)準(zhǔn)的模塊可用于應(yīng)用開發(fā),開發(fā)者也可以開發(fā)自己的功能Filter來擴展Directshow的應(yīng)用。下面我們用一個例子來說明如何采取Filter來播放一個AVI的視頻文件。
1) 首先從一個文件中讀取AVI數(shù)據(jù),形成字節(jié)流。(這個工作由源Filter完成)
2) 檢查AVI數(shù)據(jù)流的頭格式,然后通過AVI分割Filter將視頻流和音頻流分開。
3) 解碼視頻流,根據(jù)壓縮格式的不同,選取不同的decoder filters 。
4) 通過Renderer Filter重畫視頻圖像。
5) 音頻流送到聲卡進行播放,一般采用缺省的 DirectSound
Device Filter。流程見下圖。
 圖2 音頻流播放Graph圖 |
從上面的圖表看,每一個filter都一個其他的一個或者兩個filter相連接。兩個Filter相連接的連接點也是com對象,我們稱為Pin。Filter通過pin將數(shù)據(jù)從一個filter傳遞到另一個filter中,從而可以使數(shù)據(jù)在由filter組成的鏈表中流動。圖中的箭頭表示filter鏈表中的數(shù)據(jù)流的方向。在Directshow中,像上面的這樣一個filter 鏈表我們稱為filter Graph。
Filter具有三個狀態(tài),運行,停止,暫停。當(dāng)一個filter運行時,它就處理媒體數(shù)據(jù)流,當(dāng)停止時,filter就不在處理數(shù)據(jù),暫停狀態(tài)常用來給運行狀態(tài)之前cure data。Data Flow in the Filter Graph一章詳細(xì)描述了這些概念,可以參考。
除了一些特別的例外, Filter graph中所有的filter的狀態(tài)的改變都是統(tǒng)一的,也就說,filte graph中的所有的filter 的狀態(tài)改變是一致協(xié)調(diào)的。也就是說,我們也可以用filter graph也可以有運行,停止,暫停三種狀態(tài)。
Filter 一般分為下面幾種類型。
(1)源過濾器(
source filter):源過濾器引入數(shù)據(jù)到過濾器圖表中,數(shù)據(jù)來源可以是文件、網(wǎng)絡(luò)、照相機等。不同的源過濾器處理不同類型的數(shù)據(jù)源。
(2)變換過濾器(transform filter):變換過濾器的工作是獲取輸入流,處理數(shù)據(jù),并生成輸出流。變換過濾器對數(shù)據(jù)的處理包括
編解碼、格式轉(zhuǎn)換、壓縮解壓縮等。
(3)提交過濾器(renderer filter):提交過濾器在過濾器圖表里處于最后一級,它們接收數(shù)據(jù)并把數(shù)據(jù)提交給外設(shè)。
(4)分割過濾器(splitter filter):分割過濾器把輸入流分割成多個輸出。例如,AVI分割過濾器把一個AVI格式的字節(jié)流分割成視頻流和音頻流。
(5)混合過濾器(mux filter):混合過濾器把多個輸入組合成一個單獨的數(shù)據(jù)流。例如,AVI混合過濾器把視頻流和音頻流合成一個AVI格式的字節(jié)流。
過濾器的這些分類并不是絕對的,例如一個ASF讀過濾器(ASF Reader filter)既是一個源過濾器又是一個分割過濾器。
2、關(guān)于Filter Graph Manager
Filter Graph Manager也是一個com對象,用來控制Filter graph中的所有的filter,主要有以下的功能:
1) 用來協(xié)調(diào)filter之間的狀態(tài)改變,從而使graph 中的所有的filter的狀態(tài)的改變應(yīng)該一致。
2) 建立一個參考時鐘。
3) 將filter 的消息通知返回給應(yīng)用程序
4) 提供用來建立 filter graph的方法。
這里只是簡單的描述一下,詳細(xì)地可以參考文檔。
狀態(tài)改變,Graph中的filter的狀態(tài)改變應(yīng)該一致,因此,應(yīng)用程序并將狀態(tài)改變的命令直接發(fā)給filter,而是將相應(yīng)的狀態(tài)改變的命令發(fā)送給Filter graph Manager,由manager將命令分發(fā)給graph中每一個filter。Seeking也是同樣的方式工作,首先由應(yīng)用程序?qū)eek命令發(fā)送到filter graph 管理器,然后由其分發(fā)給每個filter。
參考時鐘,graph中的filter都采用的同一個時鐘,稱為參考時鐘(reference clock),參考時鐘可以確保所有的數(shù)據(jù)流同步,視頻楨或者音頻楨應(yīng)該被提交的時間稱為presentation time.presentation time 是相對于參考時鐘來確定的。Filter graph Manager應(yīng)該選擇一個參考時鐘,可以選擇聲卡上的時鐘,也可以選擇系統(tǒng)時鐘。
Graph事件, Graph 管理器采用事件機制將graph中發(fā)生的事件通知給應(yīng)用程序,這個機制類似于windows的消息循環(huán)機制。
Graph構(gòu)建的方法,graph管理器給應(yīng)用程序提供了將filter添加進graph的方法,連接filter的方法,斷開filter連接的方法。
但是,graph 管理器沒有提供如何將數(shù)據(jù)從一個filter發(fā)送到另一個filter的方法,這個工作是由filter在內(nèi)部通過pin來獨立完成的,
3、媒體類型
因為Directshow是基于com組件的,就需要有一種方式來描述filter graph每一個點的數(shù)據(jù)格式,例如,我們還以播放AVI文件為例,數(shù)據(jù)以RIFF塊的形式進入graph中,然后被分割成視頻和音頻流,視頻流有一系列的壓縮的視頻楨組成,解壓后,視頻流由一系列的無壓縮的位圖組成,音頻流也要走同樣的步驟。
Media Types: How DirectShow Represents Formats |
媒體類型是一種很普遍的,可以擴展的用來描述數(shù)字媒體格式的方法,當(dāng)兩個filter連接的時候,他們會就采用某一種媒體類型達(dá)成一致的協(xié)議。媒體類型定義了處于源頭的filter將要給下游的filter發(fā)送什么樣的數(shù)據(jù),以及數(shù)據(jù)的physical layout。如果兩個filter不能夠支持同一種的媒體類型,那么他們就沒法連接起來。
對于大多數(shù)的應(yīng)用來說,也許你不用考慮媒體類型,但是,有些應(yīng)用程序中,你會直接應(yīng)用到媒體類型的。
媒體類型是通過AM_MEDIA_TYPE結(jié)構(gòu)定義的,看看原始定義吧
typedef struct _MediaType { GUID majortype; GUID subtype; BOOL bFixedSizeSamples; BOOL bTemporalCompression; ULONG lSampleSize; GUID formattype; IUnknown *pUnk; ULONG cbFormat; [size_is(cbFormat)] BYTE *pbFormat; } AM_MEDIA_TYPE; |
Major type:是一個GUID,用來定義數(shù)據(jù)的主類型,包括,音頻,視頻,unparsed字節(jié)流,MIDI數(shù)據(jù),等等,具體可以參考msdn。
Subtype:子類型,也是一個GUID,用來進一步的細(xì)化數(shù)據(jù)格式,例如,在視頻主類型中,還包括RGB-24, RGB-32, UYVY等等一些子類型,在音頻主類型中還包括PCM audio, MPEG-1 payload等類型,子類型提供了比主類型更詳細(xì)的信息,但是并沒有定義所有的格式,例如,視頻的子類型并沒有定義圖像大小,楨率。這些由下面的字段定義。
bFixedSizeSamples當(dāng)這個值為TRUE時,表示sample大小固定。
bTemporalCompression當(dāng)這個值為TRUE時,表示sample采用了臨時壓縮格式,表明不是所有的楨都是關(guān)鍵楨,如果為FALSE,表明所有的都是關(guān)鍵楨。
lSampleSize 表示sample的大小。對于壓縮的數(shù)據(jù),這個值可能為零。
Formattype一個GUID值,用來表明內(nèi)存塊的格式。包括如下:FORMAT_None,F(xiàn)ORMAT_DvInfo,F(xiàn)ORMAT_MPEGVideo,F(xiàn)ORMAT_MPEG2Video,F(xiàn)ORMAT_VideoInfo,F(xiàn)ORMAT_VideoInfo2,F(xiàn)ORMAT_WaveFormatEx,GUID_NULL。
pUnk該參數(shù)沒有用到。
cbFormat內(nèi)存塊的大小。
pbFormat指向內(nèi)存塊的指針。
下面我們看一段代碼,看看filter如何檢測媒體類型的。
HRESULT CheckMediaType(AM_MEDIA_TYPE *pmt) { if (pmt == NULL) return E_POINTER; // Check the major type. We’re looking for video. if (pmt->majortype != MEDIATYPE_Video) { return VFW_E_INVALIDMEDIATYPE; } // Check the subtype. We’re looking for 24-bit RGB. if (pmt->subtype != MEDIASUBTYPE_RGB24) { return VFW_E_INVALIDMEDIATYPE; } // Check the format type and the size of the format block. if ((pmt->formattype == FORMAT_VideoInfo) && (pmt->cbFormat >= sizeof(VIDEOINFOHEADER) && (pmt->pbFormat != NULL)) { // Now it’s safe to coerce the format block pointer to the // correct structure, as defined by the formattype GUID. VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat; // Examine pVIH (not shown). If it looks OK, return S_OK. return S_OK; } return VFW_E_INVALIDMEDIATYPE; } |
下面簡單介紹幾個和 Media Type相關(guān)的函數(shù):
AM_MEDIA_TYPE結(jié)構(gòu)包含一個指向數(shù)據(jù)塊的指針,因此,當(dāng)你使用這個結(jié)構(gòu)的時候,一定要小心內(nèi)存分配,以防內(nèi)存泄漏。
分配函數(shù)
1) AM_MEDIA_TYPE * WINAPI CreateMediaType(AM_MEDIA_TYPE const *pSrc );
這個函數(shù)分配一個新的AM_MEDIA_TYPE結(jié)構(gòu),包含特定格式的數(shù)據(jù)塊。釋放由這個函數(shù)分配的內(nèi)存,可以調(diào)用DeleteMediaType函數(shù)
2) STDAPI CreateAudioMediaType(const WAVEFORMATEX *pwfx,AM_MEDIA_TYPE *pmt,BOOL bSetFormat);
該函數(shù)利用一個給定的WAVEFORMATIEX結(jié)構(gòu)來初始化媒體類型,如果bsetFormat參數(shù)為TRUE,該函數(shù)就分配一塊新的內(nèi)存,如果原來的pmt已經(jīng)包含內(nèi)存,就有可能發(fā)生內(nèi)存泄漏。為了避免內(nèi)存泄漏,在調(diào)用這個函數(shù)前要調(diào)用FreeMediaType(),在這個函數(shù)返回之后,再次調(diào)用FreeMediaType(),釋放format block。
3) HRESULT WINAPI CopyMediaType(AM_MEDIA_TYPE *pmtTarget,const AM_MEDIA_TYPE *pmtSource);
這個函數(shù)復(fù)制了一個結(jié)構(gòu)到另一個結(jié)構(gòu)中去。這個函數(shù)也要重新分配內(nèi)存給目的結(jié)構(gòu),如果pmtTarget,已經(jīng)包含一個內(nèi)存塊,就要內(nèi)存泄漏,因此,在調(diào)用該函數(shù)前后都要調(diào)用FreeMediaType函數(shù)。
釋放函數(shù)
4) void WINAPI DeleteMediaType( AM_MEDIA_TYPE *pmt);
無論是采用CoTaskMemAlloc函數(shù)還是用CreateMediaType函數(shù)分配的內(nèi)存都可以用這個函數(shù)來釋放,如果你沒有連接基類的動態(tài)庫,你可以用下面的代碼
void MyDeleteMediaType(AM_MEDIA_TYPE *pmt) { if (pmt != NULL) { MyFreeMediaType(*pmt); // 見下面的 FreeMediaType 函數(shù) CoTaskMemFree(pmt); } } |
5) void WINAPI FreeMediaType( AM_MEDIA_TYPE& mt);
這個函數(shù)用來釋放數(shù)據(jù)塊的內(nèi)存,如果要刪除AM_MEDIA_TYPE結(jié)構(gòu),可以使用DeleteMediaType函數(shù)。
void MyFreeMediaType(AM_MEDIA_TYPE& mt) { if (mt.cbFormat != 0) { CoTaskMemFree((PVOID)mt.pbFormat); mt.cbFormat = 0; mt.pbFormat = NULL; } if (mt.pUnk != NULL) { // Unecessary because pUnk should not be used, but safest. mt.pUnk->Release(); mt.pUnk = NULL; } } |
4、媒體Samples和Allocators
Filters通過pin的連接來傳遞數(shù)據(jù),數(shù)據(jù)流是從一個filter的輸出pin流向相連的filter的輸入pin。輸出pin常用的傳遞數(shù)據(jù)的方式是調(diào)用輸入pin上的IMemInputPin::Receive方法。
對于filter來說,可以有好幾種方式來分配媒體數(shù)據(jù)使用的內(nèi)存塊,可以在堆上分配,可以在DirectDraw的表面,也可以采用GDI共享內(nèi)存,還有其他的一些方法,在Directshow中用來進行內(nèi)存分配任務(wù)的是內(nèi)存分配器(allocator),也是一個COM對象,暴露了一個IMemAllocator接口。
當(dāng)兩個pin連接的時候,必須有一個pin提供一個allocator,Directshow定義了一系列函數(shù)調(diào)用用來確定由哪個pin提供allocator,以及buffer的數(shù)量和大小。
在數(shù)據(jù)流開始之前,allocator會創(chuàng)建一個內(nèi)存池(pool of buffer),在開始發(fā)送數(shù)據(jù)流以后,源filter就會將數(shù)據(jù)填充到內(nèi)存池中一個空閑的buffer中,然后傳遞給下面的filter。但是,源filter并不是直接將內(nèi)存buffer的指針直接傳遞給下游的filter,而是通過一個media samples的COM對象,這個sample是allocator創(chuàng)建的用來管理內(nèi)存buffer。Media sample暴露了IMediaSample接口,一個sample包含了下面的內(nèi)容:
一個指向沒有發(fā)送的內(nèi)存的指針。
一個時間戳
一些標(biāo)志
媒體類型。
時間戳表明了presentation time,Renderer filter就是根據(jù)這個時間來安排render順序的。標(biāo)志是用來標(biāo)示數(shù)據(jù)是否中斷等等,媒體類型提供了中途改變數(shù)據(jù)格式的一種方法,不過,一般sample沒有媒體類型,表明它們的媒體類型一直沒有改變。
當(dāng)一個filter正在使用buffer,它就會保持一個sample的引用計數(shù),allocator通過sample的引用計數(shù)用來確定是否可以重新使用一個buffer。這樣就防止了buffer的使用沖突,當(dāng)所有的filter都釋放了對sample的引用,sample才返回到allocator的內(nèi)存池,供重新使用。
5、硬件設(shè)備在graph中的作用
下面的這段話借用的是陸其明的一段文檔,特此標(biāo)記2005-1-26我覺得他對硬件的表述比較清楚。
大家知道,為了提高系統(tǒng)的穩(wěn)定性,Windows操作系統(tǒng)對硬件操作進行了隔離;應(yīng)用程序一般不能直接訪問硬件。DirectShow Filter工作在用戶模式(User mode,操作系統(tǒng)特權(quán)級別為Ring 3),而硬件工作在內(nèi)核模式(Kernel mode,操作系統(tǒng)特權(quán)級別為Ring 0),那么它們之間怎么協(xié)同工作呢?
DirectShow解決的方法是,為這些硬件設(shè)計包裝Filter;這種Filter能夠工作在用戶模式下,外觀、控制方法跟普通Filter一樣,而包裝Filter內(nèi)部完成與硬件驅(qū)動程序的交互。這樣的設(shè)計,使得編寫DirectShow應(yīng)用程序的開發(fā)人員,從為支持硬件而需做出的特殊處理中解脫出來。DirectShow已經(jīng)集成的包裝Filter,包括Audio Capture Filter(qcap.dll)、VfW Capture Filter(qcap.dll,F(xiàn)ilter的Class Id為CLSID_VfwCapture)、TV Tuner Filter(KSTVTune.ax,F(xiàn)ilter的Class Id為CLSID_CTVTunerFilter)、Analog Video Crossbar Filter(ksxbar.ax)、TV Audio Filter(Filter的Class Id為CLSID_TVAudioFilter)等;另外,DirectShow為采用WDM驅(qū)動程序的硬件設(shè)計了KsProxy Filter(Ksproxy.ax,)。我們可以看一下結(jié)構(gòu)圖:見圖1
我們可以看出,Ksproxy.ax、Kstune.ax、Ksxbar.ax這些包裝Filter跟其它普通的DirectShow Filter處于同一個級別,可以協(xié)同工作;用戶模式下的Filter通過Stream Class控制硬件的驅(qū)動程序minidriver(由硬件廠商提供的實現(xiàn)對硬件控制功能的DLL);Stream Class和minidriver一起向上層提供系統(tǒng)底層級別的服務(wù)。值得注意的是,這里的Stream Class是一種驅(qū)動模型,它負(fù)責(zé)調(diào)用硬件的minidriver;另外,Stream Class的功能還在于協(xié)調(diào)minidriver之間的工作,使得一些數(shù)據(jù)可以直接在Kernel mode下從一個硬件傳輸?shù)搅硪粋€硬件(或同一個硬件上的不同功能模塊),提高了系統(tǒng)的工作效率。(更多的關(guān)于底層驅(qū)動程序的細(xì)節(jié),請讀者參閱Windows DDK。)
下面,我們分別來看一下幾種常見的硬件。
VfW視頻采集卡。這類硬件在市場上已經(jīng)處于一種淘汰的趨勢;新生產(chǎn)的視頻采集卡一般采用WDM驅(qū)動模型。但是,DirectShow為了保持向后兼容,還是專門提供了一個包裝Filter支持這種硬件。和其他硬件的包裝Filter一樣,這種包裝Filter的創(chuàng)建不是像普通Filter一樣使用CoCreateInstance,而要通過系統(tǒng)枚舉,然后BindToObject。
音頻采集卡(聲卡)。聲卡的采集功能也是通過包裝Filter來實現(xiàn)的;而且現(xiàn)在的聲卡大部分都有混音的功能。這個Filter一般有幾個Input pin,每個pin都代表一個輸入,如Line In、Microphone、CD、MIDI等。值得注意的是,這些pin代表的是聲卡上的物理輸入端子,在Filter Graph中是永遠(yuǎn)不會連接到其他Filter上的。聲卡的輸出功能,可以有兩個Filter供選擇:DirectSound Renderer Filter和Audio Renderer (WaveOut) Filter。注意,這兩個Filter不是上述意義上的包裝Filter,它們能夠同硬件交互,是因為它們使用了API函數(shù):前者使用了DirectSound API,后者使用了waveOut API。這兩個Filter的區(qū)別,還在于后者輸出音頻的同時不支持混音。(順便說明一下,Video Renderer Filter能夠訪問顯卡,也是因為使用了GDI、DirectDraw或Direct3D API。)如果你的機器上有聲卡的話,你可以通過GraphEdit,在Audio Capture Sources目錄下看到這個聲卡的包裝Filter。
WDM驅(qū)動的硬件(包括視頻捕捉卡、硬件解壓卡等)。這類硬件都使用Ksproxy.ax這個包裝Filter。Ksproxy.ax實現(xiàn)了很多功能,所以有“瑞士軍刀”的美譽;它還被稱作為“變色龍Filter”,因為該Filter上定義了統(tǒng)一的接口,而接口的實現(xiàn)因具體的硬件驅(qū)動程序而異。在Filter Graph中,Ksproxy Filter顯示的名字為硬件的Friendly name(一般在驅(qū)動程序的.inf文件中定義)。我們可以通過GraphEdit,在WDM Streaming開頭的目錄中找到本機系統(tǒng)中安裝的WDM硬件。因為KsProxy.ax能夠代表各種WDM的音視頻設(shè)備,所以這個包裝Filter的工作流程有點復(fù)雜。這個Filter不會預(yù)先知道要代表哪種類型的設(shè)備,它必須首先訪問驅(qū)動程序的屬性集,然后動態(tài)配置Filter上應(yīng)該實現(xiàn)的接口。
當(dāng)Ksproxy Filter上的接口方法被應(yīng)用程序或其他Filter調(diào)用時,它會將調(diào)用方法以及參數(shù)傳遞給驅(qū)動程序,由驅(qū)動程序最終完成指定功能。除此以外,WDM硬件還支持內(nèi)核流(Kernel Streaming),即內(nèi)核模式下的數(shù)據(jù)傳輸,而無需經(jīng)過到用戶模式的轉(zhuǎn)換。因為內(nèi)核模式與用戶模式之間的相互轉(zhuǎn)換,需要花費很大的計算量。如果使用內(nèi)核流,不僅可以避免大量的計算,還避免了內(nèi)核數(shù)據(jù)與主機內(nèi)存之間的拷貝過程。在這種情況下,用戶模式的Filter Graph中,即使pin之間是連接的,也不會有實際的數(shù)據(jù)流動。典型的情況,如帶有Video Port Pin的視頻捕捉卡,Preview時顯示的圖像就是在內(nèi)核模式下直接傳送到顯卡的顯存的。所以,你也休想在VP Pin后面截獲數(shù)據(jù)流。
講到這里,我想大家應(yīng)該對DirectShow對硬件的支持問題有了一個總體的認(rèn)識。對于應(yīng)用程序開發(fā)人員來說,這方面的內(nèi)容不用研究得太透,而只需作為背景知識了解一下就好了。其實,大量繁瑣的工作DirectShow已經(jīng)幫我們做好了。