034 簡介DirectShow Filter

Post date: 2015/5/5 上午 06:27:34

所 謂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。