1. Filter概述
Filter是一個COM組件,由一個或多個Pin組成。Pin也是一個COM組件。Filter文件的擴展名為.ax,但也可以是.dll。Filter根據其包含Input pin或Output pin的情況(或在Filter Graph的位置),大致可分為三類:Source Filter(僅有Output pin)、Transform Filter(同時具有Input pin和Output pin)和Renderer Filter(僅有Input pin)。

一般情況下,創建Filter使用一個普通的Win32 DLL項目。而且,一般Filter項目不使用MFC。這時,應用程序通過CoCreateInstance函數Filter實例;Filter與應用程序在二進制級別的協作。另外一種方法,也可以在MFC的應用程序項目中創建Filter。這種情況下,Filter不需註冊為COM組件,Filter與應用程序之間的協作是源代碼級別的;創建Filter實例,不再使用CoCreateInstance函數,而是直接new出一個Filter對象,如下:
m_pFilterObject = new CFilterClass();
// make the initial refcount 1 to match COM creation
m_pFilterObject ->AddRef();
因為Filter的基類實現了對象的引用計數,所以即使在第二種情況下,對創建後的Filter對象的操作也完全可以遵循COM標準。
Filter是一個獨立功能模塊,最好不要將Filter依賴於其他第三方的DLL。因為Filter具有COM的位置透明性特點,Filter文件可以放在硬盤的任何位置,只要位置移動後重新註冊。但此時,如果Filter依賴其他DLL,則Filter對該DLL的定位就會出現問題。

Filter不能脫離Filter Graph單獨使用。所以,如果你想繞過Filter Graph直接使用Filter實現的模塊功能,請將你的Filter移植成DMO(DirectX Media Object)。對於DirectShow應用程序開發者來說,還有一點,請不要忘記使用OleInitialize進行初始化。

2. Filter的註冊
Filter是COM組件,所以在使用前一定要註冊。Filter的註冊程序為regsvr32.exe。如果帶上命令行參數/u,表示註銷;如果帶上是/s,表示不彈出任何註冊/註銷成功與否的提示對話框。如果你想在Build Filter項目的時候進行自動註冊,請在VC的Project settings的Custom Build頁如下設置:
Description: Register filter
Commands: regsvr32 /s /c $(TargetPath)
echo regsvr32 exe.time > $(TargetDir)\$(TargetName).trg
Outputs: $(TargetDir)\$(TargetName).trg

Filter的註冊信息包括兩部分:基本的COM信息和Filter信息。註冊信息都存放在註冊表中。前者的位置為:HKEY_CLASSES_ROOT\CLSID\Filter Clsid\,後者的位置為:HKEY_CLASSES_ROOT\CLSID\Category\Instance\ Filter Clsid\。COM信息標示了Filter是一個標準的可以通過CoCreateInstance函數創建的COM組件,Filter信息標示了我們通過Graphedit看到的描述這個Filter的信息。如果你不想讓Graphedit看到(或者讓Filter枚舉器找到)你寫的Filter,你完全可以不註冊Filter信息。而且不用擔心,你這麼做也完全不會影響Filter的功能。
屏蔽註冊Filter信息的方法也很簡單。因為CBaseFilter實現了IAMovieSetup接口的兩個函數:Register和Unregister。我們只需重載這兩個函數,直接return S_OK就行了。

Filter的Merit值。這個值是微軟的「智能連接」函數使用的。在Graphedit中,當我們加入一個Source Filter後,在它的pin上執行「Render」,會自動連上一些Filter。Merit的值參考如下:
MERIT_PREFERRED = 0x800000,
MERIT_NORMAL = 0x600000,
MERIT_UNLIKELY = 0x400000,
MERIT_DO_NOT_USE = 0x200000,
MERIT_SW_COMPRESSOR = 0x100000,
MERIT_HW_COMPRESSOR = 0x100050
Merit值只有大於MERIT_DO_NOT_USE的時候才有可能被「智能連接」使用;Merit的值越大,這個Filter的機會就越大。

3. Filter之間Pin的連接過程
Filter只有加入到Filter Graph中並且和其它Filter連接成完整的鏈路後,才會發揮作用。Filter之間的連接(也就是Pin之間的連接),實際上是連接雙方的一個Media type的協商過程。連接的方向總是從Output pin指向Input pin。連接的大致過程為:如果調用連接函數時已經指定了完整的Media type,則用這個Media type進行連接,成功與否都結束連接過程;如果沒有指定或不完全指定了Media type,則進入下面的枚舉過程。枚舉欲連接的Input pin上所有的Media type,逐一用這些Media type與Output pin進行連接(如果連接函數提供了不完全Media type,則要先將每個枚舉出來的Media type與它進行匹配檢查),如果Output pin也接受這種Media type,則Pin之間的連接宣告成功;如果所有Input pin上枚舉的Media type,Output pin都不支持,則枚舉Output pin上的所有Media type,並逐一用這些Media type與Input pin進行連接。如果Input pin接受其中的一種Media type,則Pin之間的連接到此也宣告成功;如果Output pin上的所有Media type,Input pin都不支持,則這兩個Pin之間的連接過程宣告失敗。

每個Pin都可以實現GetMediaType函數來提供該Pin上支持的所有Preferred Media type(但一般只在Output pin上實現,Input pin主要實現CheckMediaType看是否支持當前提供的Media type就行了)。連接過程中,Pin上枚舉得到的所有Media type就是這裡提供的。

在CBasePin類中有一個protected的成員變量m_bTryMyTypesFirst,默認值為false。在我們定制Filter的Output pin中改變這個變量的值為true,可以定制我們自己的連接過程(先枚舉Output pin上的Media type)。

當Pin之間的連接成功後,各自的pin上都會調用CompleteConnect函數。我們可以在這裡取得一些連接上的Media type的信息,以及進行一些計算等。在Output pin的CompleteConnect實現中,還有一個重要的任務,就是協商Filter Graph運行起來後Sample傳輸使用的內存配置情況。這同樣是一個交互過程:首先要詢問一下Input pin上的配置要求,如果Input pin提供內存管理器(Allocator),則優先使用Input pin上的內存管理器;否則,使用Output pin自己生成的內存管理器。我們一般都要實現DecideBufferSize來決定存放Sample的內存大小。注意:這個過程協商完成之後,實際的內存並沒有分配,而要等到Output pin上的Active函數調用。

4. Filter Media type概述
Media type一般可以有兩種表示:AM_MEDIA_TYPE和CMediaType。前者是一個Struct,後者是從這個Struct繼承過來的類。
每個Media type有三部分組成:Major type、Subtype和Format type。這三個部分都使用GUID來唯一標示。Major type主要定性描述一種Media type,比如指定這是一個Video,或Audio或Stream等;Subtype進一步細化Media type,如果Video的話可以進一步指定是UYVY或YUY2或RGB24或RGB32等;Format type用一個Struct更進一步細化Media type。
如果Media type的三個部分都是指定了某個具體的GUID值,則稱這個Media type是完全指定的;如果Media type的三個部分中有任何一個值是GUID_NULL,則稱這個Media type 是不完全指定的。GUID_NULL具有通配符的作用。

常用的Major type:
MEDIATYPE_Video;
MEDIATYPE_Audio;
MEDIATYPE_AnalogVideo; // Analog capture
MEDIATYPE_AnalogAudio;
MEDIATYPE_Text;
MEDIATYPE_Midi;
MEDIATYPE_Stream;
MEDIATYPE_Interleaved; // DV camcorder
MEDIATYPE_MPEG1SystemStream;
MEDIATYPE_MPEG2_PACK;
MEDIATYPE_MPEG2_PES;
MEDIATYPE_DVD_ENCRYPTED_PACK;
MEDIATYPE_DVD_NAVIGATION;

常用的Subtype:
MEDIASUBTYPE_YUY2;
MEDIASUBTYPE_YVYU;
MEDIASUBTYPE_YUYV;
MEDIASUBTYPE_UYVY;
MEDIASUBTYPE_YVU9;
MEDIASUBTYPE_Y411;
MEDIASUBTYPE_RGB4;
MEDIASUBTYPE_RGB8;
MEDIASUBTYPE_RGB565;
MEDIASUBTYPE_RGB555;
MEDIASUBTYPE_RGB24;
MEDIASUBTYPE_RGB32;
MEDIASUBTYPE_ARGB32; // Contains alpha value
MEDIASUBTYPE_Overlay;
MEDIASUBTYPE_MPEG1Packet;
MEDIASUBTYPE_MPEG1Payload; // Video payload
MEDIASUBTYPE_MPEG1AudioPayload; // Audio payload
MEDIASUBTYPE_MPEG1System; // A/V payload
MEDIASUBTYPE_MPEG1VideoCD;
MEDIASUBTYPE_MPEG1Video;
MEDIASUBTYPE_MPEG1Audio;
MEDIASUBTYPE_Avi;
MEDIASUBTYPE_Asf;
MEDIASUBTYPE_QTMovie;
MEDIASUBTYPE_PCM;
MEDIASUBTYPE_WAVE;
MEDIASUBTYPE_dvsd; // DV
MEDIASUBTYPE_dvhd;
MEDIASUBTYPE_dvsl;
MEDIASUBTYPE_MPEG2_VIDEO;
MEDIASUBTYPE_MPEG2_PROGRAM;
MEDIASUBTYPE_MPEG2_TRANSPORT;
MEDIASUBTYPE_MPEG2_AUDIO;
MEDIASUBTYPE_DOLBY_AC3;
MEDIASUBTYPE_DVD_SUBPICTURE;
MEDIASUBTYPE_DVD_LPCM_AUDIO;
MEDIASUBTYPE_DVD_NAVIGATION_PCI;
MEDIASUBTYPE_DVD_NAVIGATION_DSI;
MEDIASUBTYPE_DVD_NAVIGATION_PROVIDER;

常用的Format type:
FORMAT_None
FORMAT_DvInfo DVINFO
FORMAT_MPEGVideo MPEG1VIDEOINFO
FORMAT_MPEG2Video MPEG2VIDEOINFO
FORMAT_VideoInfo VIDEOINFOHEADER
FORMAT_VideoInfo2 VIDEOINFOHEADER2
FORMAT_WaveFormatEx WAVEFORMATEX

5. Filter之間的數據傳送
Filter之間的數據是通過Sample來傳送的。Sample是一個COM組件,擁有自己的一段數據緩衝。Sample由Allocator統一管理。如下圖所示:


Filter之間數據傳送的方式有兩種:Push模式和Pull模式。

所謂Push模式,即Source filter自己能夠產生數據,並且一般在它的Output pin上有獨立的子線程負責將數據發送出去,常見的情況如WDM模型的採集卡的Live Source Filter;而所謂Pull模式,即Source filter不具有把自己的數據送出去的能力,這種情況下,一般Source filter後緊跟著接一個Parser Filter或Splitter Filter,這種Filter一般在Input pin上有個獨立的子線程,負責不斷地從Source filter索取數據,然後經過處理後將數據傳送下去,常見的情況如File source。Push模式下,Source filter是主動的;Pull模式下,Source filter是被動的。而事實上,如果將上圖Pull模式中的Source filter和Splitter Filter看成另一個虛擬的Source filter,則後面的Filter之間的數據傳送也與Push模式完全相同。

那麼,數據到底是怎麼通過連接著的Pin傳送的呢?首先來看Push模式。在Source filter後面Filter的 Input pin上,一定實現了一個IMemInputPin接口,數據正是通過上一級Filter調用這個接口的Receive方法進行傳送的。值得注意的是,數據從Output pin通過Receive方法調用傳送到Input pin上,並沒有進行內存拷貝,它只是一個相當於數據到達的「通知」。再看一下Pull模式。Pull模式下的Source filter的 Output pin上,一定實現了一個IAsyncReader接口;其後面的Splitter Filter,就是通過調用這個接口的Request方法或者SyncRead方法來獲得數據。Splitter Filter然後像Push模式一樣,調用下一級Filter的Input pin上的IMemInputPin接口Receive方法實現數據的往下傳送。

一個DirectShow的應用程序,至少會有兩條線程:主線程和Filter用於數據傳送的子線程。既然是多線程,就不可避免會出現線程同步問題。Filter的狀態改變都在主線程中完成,Filter的數據相關操作都在數據線程中調用。各線程一些主要函數調用參考如下:
Streaming thread(s): IMemInputPin::Receive, IMemInputPin::ReceiveMultiple, IPin::EndOfStream, IMemAllocator::GetBuffer.
Application thread: IMediaFilter::Pause, IMediaFilter::Run, IMediaFilter::Stop, IMediaSeeking::SetPositions, IPin::BeginFlush, IPin::EndFlush.
Either: IPin::NewSegment.
這些函數切忌混合調用,否則會引起線程的死鎖。另外值得注意的是,BeginFlush和EndFlush屬於主線程調用,而不是數據線程調用。

6. Transform filter和Trans-in-place filter的區別
首先,這兩種Filter是有共同點的,因為Trans-in-place filter本身就是從Transform filter中繼承過來的。其次,我們要明白的是,Trans-in-place filter「盡力」使自己的Input pin和Output pin使用相同的Allocator,以免去一次Sample數據的memcpy。我們說「盡力」,就是說Trans-in-place filter也未必能夠實現它的初衷。(如果Trans-in-place filter使用的Allocator是ReadOnly的,而Trans-in-place filter又要修改Sample的數據,則Trans-in-place filter的Input pin和Output pin將不得不使用不同的Allocator。)
Trans-in-place filter有一個protected的成員變量m_bModifiesData,默認值為true。如果你確信定制Trans-in-place filter不需要修改Sample數據,則將m_bModifiesData賦值為false,這樣可以保證Input pin和Output pin使用相同的Allocator。

Trans-in-place filter的實現主要體現在以下三個函數:CTransInPlaceFilter::CompleteConnect、CTransInPlaceInputPin::GetAllocator和CTransInPlaceInputPin::NotifyAllocator。CompleteConnect中進行必要的重連(Reconnect),保證Trans-in-place filter的Input pin和Output pin使用相同的Media type。GetAllocator能夠取得Trans-in-place filter下一級Filter的Input pin上的Allocator。NotifyAllocator「盡力」使Trans-in-place filter的Input pin和Output pin使用同一個Allocator。

7. IMediaSeeking的實現
IMediaSeeking的實現在Filter上,但應用程序應該從Filter Graph Manager上得到這個接口。在Filter級別,Filter Graph Manager首先從Renderer filter開始詢問上一級Filter的Output pin是否支持IMediaSeeking接口。如果支持,則返回這個接口;如果不支持,則繼續往上一級Filter詢問,直到Source filter。一般在Source filter的Output pin上實現IMediaSeeking接口。(如果是File source,一般在Parser Filter或Splitter Filter實現這個接口。)對於Filter開發者來說,如果我們寫的是Source filter,就要在Filter的Output pin上實現IMediaSeeking接口;如果寫的是Transform filter,只需要在Output pin上將用戶的接口請求往上傳遞給上一級Filter的Output pin;如果寫的是Renderer Filter,需要在Filter上將用戶的接口請求往上傳遞給上一級Filter的Output pin。

注意:為了保證Seek操作後Stream的同步性,如果實際實現IMediaSeeking接口的Filter有多個Output pin,一般僅有一個pin支持Seek操作。對於你定制的Transform filter,如果有多個Input pin,你需要自己決定當Output pin接收到IMediaSeeking接口請求時選擇哪一條路徑往上繼續請求。

應用程序能夠在任何時候(running, paused or stopped)對Filter graph執行Seek操作。但當Filter graph正在running的時候,Filter graph manager會先pause住,執行完Seek操作後,再重新run起來。

IMediaSeeking可以有如下幾種Seek的時間格式:
TIME_FORMAT_FRAME Video frames.
TIME_FORMAT_SAMPLE Samples in the stream.
TIME_FORMAT_FIELD Interlaced video fields.
TIME_FORMAT_BYTE Byte offset within the stream.
TIME_FORMAT_MEDIA_TIME Reference time (100-nanosecond units).
但實現這個接口的Filter未必支持所有的這些格式。一般Filter都會支持TIME_FORMAT_MEDIA_ TIME,當使用其它的格式時,最好調用IMediaSeeking::IsFormatSupported進行一下確認。

對於Filter,不贊成使用IMediaPosition接口。IMediaPosition是用以支持Automation的(比如VB裡面使用DirectShow),IMediaSeeking不支持Automation。

8. Filter的狀態轉換
Filter有三種狀態:stopped, paused, running。paused是一種中間狀態,stopped狀態到running狀態必定經過paused狀態。paused可以理解為數據就緒狀態,是為了快速切換到running狀態而設計的。在paused狀態下,數據線程是啟動的,但被Renderer filter阻塞了。

paused與running兩者間的狀態轉換,對於Source filter和Transform filter可以忽略不計,而對於Renderer filter(特別是Video renderer / Audio renderer)情形稍有不同。Renderer首先處理那個paused狀態下Hold的Sample,當接收到新的Sample時,判斷Sample上的時間戳。如果時間未到,Renderer會Hold住這個Sample進行等待。

Filter graph manager以從下到上的順序對Filter進行狀態轉換,即從Renderer filter一直回溯到Source filter。這個順序能夠有效地避免Sample的丟失以及Filter graph的死鎖。
Stopped to paused:首先從Renderer開始進行paused狀態的轉換。這時,Filter調用自己所有Pin的Active函數進行初始化(一般Pin在Active中進行Sample內存的分配,如果是Source filter還將啟動數據線程),使Filter處於一種就緒狀態。Source filter是最後一個完成到就緒狀態轉換的Filter。然後,Source filter啟動數據線程,往下發送Sample。當Renderer接收到第一個Sample後就阻塞住。當所有的Renderer實現了狀態轉換,Filter graph manager才認為狀態轉換完成。
Paused to stopped:當Filter進入stopped狀態時,調用自己所有Pin的Inactive函數(一般Pin在Inactive中進行Sample內存的釋放,如果是Source filter還將終止數據線程)。釋放所有Hold的Sample,以使上一級Filter的GetBuffer脫離阻塞;終止所有在Receive中的等待,以使上一級Filter的Receive函數調用返回。Filter在stopped狀態下拒絕接受任何Sample。這樣從Renderer filter往上一級一級脫離阻塞,當到達Source filter的時候,可以確保數據線程終止。

9. EndOfStream問題
當Source filter的所有數據都已經發送出去,則會調用下一級Filter的Input pin上的IPin::EndOfStream,直到Renderer filter。當這個Renderer filter的所有Input pin都被調用了EndOfStream,則向Filter graph manager發送一個EC_COMPLETE事件。僅當Filter graph中的所有Stream都發送了EC_COMPLETE事件,Filter graph manager才會將這個事件發送給應用程序。

在我們定制的Filter中,如果接收到了上一級Filter傳過來的EndOfStream,則說明上面的數據已經全部傳送完畢,Receive方法不須再接收數據。如果我們對數據進行了緩衝,則應確認緩衝中的所有數據都被處理完並往下發送了,然後再往下調用EndOfStream。Pull模式下,一般是Splitter filter或Parser filter發送EndOfStream,而且方向是往下的,Source filter上不會收到這樣的通知。

10. BeginFlush、EndFlush、NewSegment問題
典型的情況,當進行MediaSeeking之後,會調用BeginFlush、EndFlush。一般在Input pin上實現這兩個函數。
對於Filter開發者來說,Filter在被調用BeginFlush時需要做以下工作:
· 調用下一級Filter的BeginFlush,使其不再接收新的Sample;
· 拒絕接收上一級Filter的數據,包括Receive調用和EndOfStream調用;
· 如果上一級Filter正在阻塞等待空的Sample,此時需要讓它脫離阻塞(通過析構Allocator);
· 確保數據流線程脫離阻塞狀態。
Filter在被調用EndFlush時需要做以下工作:
· 確保所有等待緩存的Sample被丟棄;
· 確保Filter上已經緩存的數據被丟棄;
· 清除沒有發出去的EC_COMPLETE事件(如果這是一個Rendered input pin);
· 調用下一級Filter的EndFlush。
還有一點:如果你必須在定制的Filter中為每個Sample打Time stamp,那麼記住在MediaSeeking之後出去的Sample的Time stamp應該從0開始重打。

Segment是一段時間內具有相同的Playback rate的一組Sample,以NewSegment函數調用來表示這個Segment的開始。NewSegment一般在開始新的Stream的時候,或者用戶進行了MediaSeeking之後,由Source filter(Push模式下)或Parser/Splitter filter(Pull模式下)發起,並往下層層調用,一直到Renderer filter。
在我們定制的Filter中可以利用NewSegment傳遞下來的信息,特別是對於Decoder。對於Audio renderer也是一個典型例子,它根據Playback rate和Audio實際的採樣頻率來對聲卡產生輸出。

11. Quality Control問題
Filter之間的數據傳送,有時候過快,有時候過慢。DirectShow使用Quality Control來解決這個問題,即IQualityControl接口的兩個函數(SetSink和Notify)。一般,Renderer filter在Filter上實現這個接口,而其他Filter在Output pin上實現這個接口。

上圖為一般的Quality Control的處理過程。而能夠調整發送速度的IQualityControl接口一般在Source filter(pull模式下為parser/splitter filter)上實現,Transform filter只是將Quality Message往上一級Filter傳遞。
應用程序可以實現自己的Quality Control Manager,然後通過調用SetSink方法設置給Filter。上述的處理過程就改變了,Quality Message直接發送給自定義的Manager。但一般並不這麼做。值得注意的是,具體的Quality Control實現取決於實際的Filter,可能是調整發送速度,也可能會丟失部分數據。所以,請慎重使用Quality Message。

12. 對運行過程中Media type改變的支持
我們可以從CBaseInputPin::Receive中可以看到,Input pin每次在接收Sample的之前,一般都會進行CheckStreaming(如果當前Filter已經Stop或正在Flush或發生了RuntimeError,則拒絕接收Sample),然後將當前的Sample屬性保存到protected的m_SampleProps成員變量中。描述Sample屬性的是一個AM_SAMPLE2_PROPERTIES的結構,它有一個標記來表明當前Sample的Media type是否已經改變。如果Media type改變了,則進行CheckMedaiType看我們的Filter是否仍然支持它。如果不支持,則發出一個RuntimeError,並發送EndOfStream。
一個健全的Filter應該能夠對運行時Media type的改變做出處理。在我們的Receive方法實現中,我們可以通過if (pProps->dwSampleFlags & AM_SAMPLE_TYPECHANGED)來判斷Meida type是否已經改變;如果改變,我們需要根據新的Media type進行必要的初始化。

一個典型的案例:當Camcorder輸入時,Audio的Media type可能改變。比如,Filter連接時Media type使用了MEDIATYPE_PCM,而在運行時又換成了MEDIATYPE_WAVE;或者連接時Audio的採樣頻率時44.1K,而在運行時卻變成了48K;或者Camcorder的帶子上本身保存了混合的44.1K和48K的Audio。

arrow
arrow
    全站熱搜

    lblmine 發表在 痞客邦 留言(0) 人氣()