DirectShow開發快速入門之慨述(轉載)

 摘要:本篇文檔概括性的介紹了DirectShow的主要組成部分,以及一些Directshow的基本概念。熟悉這些基本的知識對於Directshow的應用開發或者過濾器的開發者都會有所幫助。 

DirectShow是微軟公司提供的一套在Windows平臺上進行流媒體處理的開發包,與DirectX開發包一起發佈。那麼,DirectShow能夠做些什麼呢?且看,DirectShow爲多媒體流的捕捉和回放提供了強有力的支持。運用DirectShow,我們可以很方便地從支持WDM驅動模型的採集卡上捕獲數據,並且進行相應的後期處理乃至存儲到文件中。它廣泛地支持各種媒體格式,包括Asf、Mpeg、Avi、Dv、Mp3、Wave等等,使得多媒體數據的回放變得輕而易舉。另外,DirectShow還集成了DirectX其它部分(比如DirectDraw、DirectSound)的技術,直接支持DVD的播放,視頻的非線性編輯,以及與數字攝像機的數據交換。更值得一提的是,DirectShow提供的是一種開放式的開發環境,我們可以根據自己的需要定製自己的組件。 

應用程序與DirectShow組件以及DirectShow所支持的軟硬件之間的關係如圖1所示。


圖1 DirectShow系統框圖

1、DirectShow的 Filter

Directshow是基於模塊化,每個功能模塊都採取COM組件方式,稱爲Filter。Directshow提供了一系列的標準的模塊可用於應用開發,開發者也可以開發自己的功能Filter來擴展Directshow的應用。下面我們用一個例子來說明如何採取Filter來播放一個AVI的視頻文件。

1) 首先從一個文件中讀取AVI數據,形成字節流。(這個工作由源Filter完成)

2) 檢查AVI數據流的頭格式,然後通過AVI分割Filter將視頻流和音頻流分開。

3) 解碼視頻流,根據壓縮格式的不同,選取不同的decoder filters 。

4) 通過Renderer Filter重畫視頻圖像

5) 音頻流送到聲卡進行播放,一般採用缺省的 DirectSound DeviceFilter。流程見下圖。


圖2 音頻流播放Graph圖

從上面的圖表看,每一個filter都一個其他的一個或者兩個filter相連接。兩個Filter相連接的連接點也是com對象,我們稱爲Pin。Filter通過pin將數據從一個filter傳遞到另一個filter中,從而可以使數據在由filter組成的鏈表中流動。圖中的箭頭表示filter鏈表中的數據流的方向。在Directshow中,像上面的這樣一個filter 鏈表我們稱爲filter Graph。

Filter具有三個狀態,運行,停止,暫停。當一個filter運行時,它就處理媒體數據流,當停止時,filter就不在處理數據,暫停狀態常用來給運行狀態之前cure data。Data Flow in the Filter Graph一章詳細描述了這些概念,可以參考。

除了一些特別的例外, Filter graph中所有的filter的狀態的改變都是統一的,也就說,filte graph中的所有的filter 的狀態改變是一致協調的。也就是說,我們也可以用filter graph也可以有運行,停止,暫停三種狀態。

Filter 一般分爲下面幾種類型。

(1)源過濾器(sourcefilter):源過濾器引入數據到過濾器圖表中,數據來源可以是文件、網絡、照相機等。不同的源過濾器處理不同類型的數據源。

(2)變換過濾器(transform filter):變換過濾器的工作是獲取輸入流,處理數據,並生成輸出流。變換過濾器對數據的處理包括編解碼、格式轉換、壓縮解壓縮等。 

(3)提交過濾器(renderer filter):提交過濾器在過濾器圖表裏處於最後一級,它們接收數據並把數據提交給外設。

(4)分割過濾器(splitter filter):分割過濾器把輸入流分割成多個輸出。例如,AVI分割過濾器把一個AVI格式的字節流分割成視頻流和音頻流。

(5)混合過濾器(mux filter):混合過濾器把多個輸入組合成一個單獨的數據流。例如,AVI混合過濾器把視頻流和音頻流合成一個AVI格式的字節流。

過濾器的這些分類並不是絕對的,例如一個ASF讀過濾器(ASF Reader filter)既是一個源過濾器又是一個分割過濾器。

2、關於Filter Graph Manager

Filter Graph Manager也是一個com對象,用來控制Filter graph中的所有的filter,主要有以下的功能: 

1) 用來協調filter之間的狀態改變,從而使graph 中的所有的filter的狀態的改變應該一致。

2) 建立一個參考時鐘。

3) 將filter 的消息通知返回給應用程序

4) 提供用來建立 filter graph的方法。

這裏只是簡單的描述一下,詳細地可以參考文檔。

狀態改變,Graph中的filter的狀態改變應該一致,因此,應用程序並將狀態改變的命令直接發給filter,而是將相應的狀態改變的命令發送給Filter graph Manager,由manager將命令分發給graph中每一個filter。Seeking也是同樣的方式工作,首先由應用程序將seek命令發送到filter graph 管理器,然後由其分發給每個filter。

參考時鐘,graph中的filter都採用的同一個時鐘,稱爲參考時鐘(reference clock),參考時鐘可以確保所有的數據流同步,視頻楨或者音頻楨應該被提交的時間稱爲presentation time.presentation time 是相對於參考時鐘來確定的。Filter graph Manager應該選擇一個參考時鐘,可以選擇聲卡上的時鐘,也可以選擇系統時鐘。

Graph事件, Graph 管理器採用事件機制將graph中發生的事件通知給應用程序,這個機制類似於windows的消息循環機制。

Graph構建的方法,graph管理器給應用程序提供了將filter添加進graph的方法,連接filter的方法,斷開filter連接的方法。

但是,graph 管理器沒有提供如何將數據從一個filter發送到另一個filter的方法,這個工作是由filter在內部通過pin來獨立完成的,3、媒體類型

因爲Directshow是基於com組件的,就需要有一種方式來描述filter graph每一個點的數據格式,例如,我們還以播放AVI文件爲例,數據以RIFF塊的形式進入graph中,然後被分割成視頻和音頻流,視頻流有一系列的壓縮的視頻楨組成,解壓後,視頻流由一系列的無壓縮的位圖組成,音頻流也要走同樣的步驟。 

Media Types: How DirectShow Represents Formats
媒體類型是一種很普遍的,可以擴展的用來描述數字媒體格式的方法,當兩個filter連接的時候,他們會就採用某一種媒體類型達成一致的協議。媒體類型定義了處於源頭的filter將要給下游的filter發送什麼樣的數據,以及數據的physical layout。如果兩個filter不能夠支持同一種的媒體類型,那麼他們就沒法連接起來。

對於大多數的應用來說,也許你不用考慮媒體類型,但是,有些應用程序中,你會直接應用到媒體類型的。

媒體類型是通過AM_MEDIA_TYPE結構定義的,看看原始定義吧 

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,用來定義數據的主類型,包括,音頻,視頻,unparsed字節流,MIDI數據,等等,具體可以參考msdn。

Subtype:子類型,也是一個GUID,用來進一步的細化數據格式,例如,在視頻主類型中,還包括RGB-24, RGB-32, UYVY等等一些子類型,在音頻主類型中還包括PCM audio, MPEG-1 payload等類型,子類型提供了比主類型更詳細的信息,但是並沒有定義所有的格式,例如,視頻的子類型並沒有定義圖像大小,楨率。這些由下面的字段定義。

bFixedSizeSamples當這個值爲TRUE時,表示sample大小固定。

bTemporalCompression當這個值爲TRUE時,表示sample採用了臨時壓縮格式,表明不是所有的楨都是關鍵楨,如果爲FALSE,表明所有的都是關鍵楨。

lSampleSize 表示sample的大小。對於壓縮的數據,這個值可能爲零。
 
Formattype一個GUID值,用來表明內存塊的格式。包括如下:FORMAT_None,FORMAT_DvInfo,FORMAT_MPEGVideo,FORMAT_MPEG2Video,FORMAT_VideoInfo,FORMAT_VideoInfo2,FORMAT_WaveFormatEx,GUID_NULL。

pUnk該參數沒有用到。

cbFormat內存塊的大小。

pbFormat指向內存塊的指針。

下面我們看一段代碼,看看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相關的函數:

AM_MEDIA_TYPE結構包含一個指向數據塊的指針,因此,當你使用這個結構的時候,一定要小心內存分配,以防內存泄漏。

分配函數

1) AM_MEDIA_TYPE * WINAPI CreateMediaType(AM_MEDIA_TYPE const *pSrc );

這個函數分配一個新的AM_MEDIA_TYPE結構,包含特定格式的數據塊。釋放由這個函數分配的內存,可以調用DeleteMediaType函數

2) STDAPI CreateAudioMediaType(const WAVEFORMATEX *pwfx,AM_MEDIA_TYPE *pmt,BOOL bSetFormat);

該函數利用一個給定的WAVEFORMATIEX結構來初始化媒體類型,如果bsetFormat參數爲TRUE,該函數就分配一塊新的內存,如果原來的pmt已經包含內存,就有可能發生內存泄漏。爲了避免內存泄漏,在調用這個函數前要調用FreeMediaType(),在這個函數返回之後,再次調用FreeMediaType(),釋放format block。

3) HRESULT WINAPI CopyMediaType(AM_MEDIA_TYPE *pmtTarget,const AM_MEDIA_TYPE *pmtSource);

這個函數複製了一個結構到另一個結構中去。這個函數也要重新分配內存給目的結構,如果pmtTarget,已經包含一個內存塊,就要內存泄漏,因此,在調用該函數前後都要調用FreeMediaType函數。

釋放函數

4) void WINAPI DeleteMediaType( AM_MEDIA_TYPE *pmt);

無論是採用CoTaskMemAlloc函數還是用CreateMediaType函數分配的內存都可以用這個函數來釋放,如果你沒有連接基類的動態庫,你可以用下面的代碼

void MyDeleteMediaType(AM_MEDIA_TYPE *pmt)
{
if (pmt != NULL)
{
MyFreeMediaType(*pmt); // 見下面的 FreeMediaType 函數
CoTaskMemFree(pmt);
}
}

5) void WINAPI FreeMediaType( AM_MEDIA_TYPE& mt);

這個函數用來釋放數據塊的內存,如果要刪除AM_MEDIA_TYPE結構,可以使用DeleteMediaType函數。

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的連接來傳遞數據,數據流是從一個filter的輸出pin流向相連的filter的輸入pin。輸出pin常用的傳遞數據的方式是調用輸入pin上的IMemInputPin::Receive方法。

對於filter來說,可以有好幾種方式來分配媒體數據使用的內存塊,可以在堆上分配,可以在DirectDraw的表面,也可以採用GDI共享內存,還有其他的一些方法,在Directshow中用來進行內存分配任務的是內存分配器(allocator),也是一個COM對象,暴露了一個IMemAllocator接口。

當兩個pin連接的時候,必須有一個pin提供一個allocator,Directshow定義了一系列函數調用用來確定由哪個pin提供allocator,以及buffer的數量和大小。

在數據流開始之前,allocator會創建一個內存池(pool of buffer),在開始發送數據流以後,源filter就會將數據填充到內存池中一個空閒的buffer中,然後傳遞給下面的filter。但是,源filter並不是直接將內存buffer的指針直接傳遞給下游的filter,而是通過一個media samples的COM對象,這個sample是allocator創建的用來管理內存buffer。Media sample暴露了IMediaSample接口,一個sample包含了下面的內容:

一個指向沒有發送的內存的指針。

一個時間戳

一些標誌

媒體類型。

時間戳表明了presentation time,Renderer filter就是根據這個時間來安排render順序的。標誌是用來標示數據是否中斷等等,媒體類型提供了中途改變數據格式的一種方法,不過,一般sample沒有媒體類型,表明它們的媒體類型一直沒有改變。

當一個filter正在使用buffer,它就會保持一個sample的引用計數,allocator通過sample的引用計數用來確定是否可以重新使用一個buffer。這樣就防止了buffer的使用衝突,當所有的filter都釋放了對sample的引用,sample才返回到allocator的內存池,供重新使用。

5、硬件設備在graph中的作用

下面的這段話借用的是陸其明的一段文檔,特此標記2005-1-26我覺得他對硬件的表述比較清楚。

大家知道,爲了提高系統的穩定性,Windows操作系統對硬件操作進行了隔離;應用程序一般不能直接訪問硬件。DirectShow Filter工作在用戶模式(User mode,操作系統特權級別爲Ring 3),而硬件工作在內核模式(Kernel mode,操作系統特權級別爲Ring 0),那麼它們之間怎麼協同工作呢?

DirectShow解決的方法是,爲這些硬件設計包裝Filter;這種Filter能夠工作在用戶模式下,外觀、控制方法跟普通Filter一樣,而包裝Filter內部完成與硬件驅動程序的交互。這樣的設計,使得編寫DirectShow應用程序的開發人員,從爲支持硬件而需做出的特殊處理中解脫出來。DirectShow已經集成的包裝Filter,包括Audio Capture Filter(qcap.dll)、VfW Capture Filter(qcap.dll,Filter的Class Id爲CLSID_VfwCapture)、TV Tuner Filter(KSTVTune.ax,Filter的Class Id爲CLSID_CTVTunerFilter)、Analog Video Crossbar Filter(ksxbar.ax)、TV Audio Filter(Filter的Class Id爲CLSID_TVAudioFilter)等;另外,DirectShow爲採用WDM驅動程序的硬件設計了KsProxy Filter(Ksproxy.ax,)。我們可以看一下結構圖:見圖1

我們可以看出,Ksproxy.ax、Kstune.ax、Ksxbar.ax這些包裝Filter跟其它普通的DirectShow Filter處於同一個級別,可以協同工作;用戶模式下的Filter通過Stream Class控制硬件的驅動程序minidriver(由硬件廠商提供的實現對硬件控制功能的DLL);Stream Class和minidriver一起向上層提供系統底層級別的服務。值得注意的是,這裏的Stream Class是一種驅動模型,它負責調用硬件的minidriver;另外,Stream Class的功能還在於協調minidriver之間的工作,使得一些數據可以直接在Kernel mode下從一個硬件傳輸到另一個硬件(或同一個硬件上的不同功能模塊),提高了系統的工作效率。(更多的關於底層驅動程序的細節,請讀者參閱Windows DDK。)

下面,我們分別來看一下幾種常見的硬件。

VfW視頻採集卡。這類硬件在市場上已經處於一種淘汰的趨勢;新生產的視頻採集卡一般採用WDM驅動模型。但是,DirectShow爲了保持向後兼容,還是專門提供了一個包裝Filter支持這種硬件。和其他硬件的包裝Filter一樣,這種包裝Filter的創建不是像普通Filter一樣使用CoCreateInstance,而要通過系統枚舉,然後BindToObject。

音頻採集卡(聲卡)。聲卡的採集功能也是通過包裝Filter來實現的;而且現在的聲卡大部分都有混音的功能。這個Filter一般有幾個Input pin,每個pin都代表一個輸入,如Line In、Microphone、CD、MIDI等。值得注意的是,這些pin代表的是聲卡上的物理輸入端子,在Filter Graph中是永遠不會連接到其他Filter上的。聲卡的輸出功能,可以有兩個Filter供選擇:DirectSound Renderer Filter和Audio Renderer (WaveOut) Filter。注意,這兩個Filter不是上述意義上的包裝Filter,它們能夠同硬件交互,是因爲它們使用了API函數:前者使用了DirectSound API,後者使用了waveOut API。這兩個Filter的區別,還在於後者輸出音頻的同時不支持混音。(順便說明一下,Video Renderer Filter能夠訪問顯卡,也是因爲使用了GDI、DirectDraw或Direct3D API。)如果你的機器上有聲卡的話,你可以通過GraphEdit,在Audio Capture Sources目錄下看到這個聲卡的包裝Filter。

WDM驅動的硬件(包括視頻捕捉卡、硬件解壓卡等)。這類硬件都使用Ksproxy.ax這個包裝Filter。Ksproxy.ax實現了很多功能,所以有“瑞士軍刀”的美譽;它還被稱作爲“變色龍Filter”,因爲該Filter上定義了統一的接口,而接口的實現因具體的硬件驅動程序而異。在Filter Graph中,Ksproxy Filter顯示的名字爲硬件的Friendly name(一般在驅動程序的.inf文件中定義)。我們可以通過GraphEdit,在WDM Streaming開頭的目錄中找到本機系統中安裝的WDM硬件。因爲KsProxy.ax能夠代表各種WDM的音視頻設備,所以這個包裝Filter的工作流程有點複雜。這個Filter不會預先知道要代表哪種類型的設備,它必須首先訪問驅動程序的屬性集,然後動態配置Filter上應該實現的接口。

當Ksproxy Filter上的接口方法被應用程序或其他Filter調用時,它會將調用方法以及參數傳遞給驅動程序,由驅動程序最終完成指定功能。除此以外,WDM硬件還支持內核流(Kernel Streaming),即內核模式下的數據傳輸,而無需經過到用戶模式的轉換。因爲內核模式與用戶模式之間的相互轉換,需要花費很大的計算量。如果使用內核流,不僅可以避免大量的計算,還避免了內核數據與主機內存之間的拷貝過程。在這種情況下,用戶模式的Filter Graph中,即使pin之間是連接的,也不會有實際的數據流動。典型的情況,如帶有Video Port Pin的視頻捕捉卡,Preview時顯示的圖像就是在內核模式下直接傳送到顯卡的顯存的。所以,你也休想在VP Pin後面截獲數據流。

講到這裏,我想大家應該對DirectShow對硬件的支持問題有了一個總體的認識。對於應用程序開發人員來說,這方面的內容不用研究得太透,而只需作爲背景知識瞭解一下就好了。其實,大量繁瑣的工作DirectShow已經幫我們做好了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章