DirectShow編程1

3. 關於DirectShow 3.1. DirectShow體系概述     多媒體的難題     處理多媒體有幾個主要的難題:     *多媒體流包含了巨大的數據量,而這些數據都必須非常快地被處理     *音頻和視頻必須同步,因此它們必須在同一時間開始或停止,並以同一速率播放     *數據可能來自很多的源,如本地文件、網絡、電視廣播和視頻攝像機     *數據有各種各樣的格式,如AVI、ASF、MPEG和DV     *程序員無法預知最終用戶使用什麼樣的硬件設備     DirectShow的解決方案     DirectShow被設計成用來解決所有這些難題,它主要的設計目的就是通過將複雜的數據轉輸、硬件的多樣性和同步問題從應用程序中獨立出來,從而簡化在windows平臺上數字媒體應用程序的開發任務。     要實現數據高效地被處理,需要流化音視頻數據,而DirectShow會儘可能地使用DirectDraw和DirectSound,從而高效地將數據送到用戶的聲音和圖形設備中進行播放。同步則是通過在媒體數據中加入時間戳來實現。而DirectShow模塊化的架構,使其可以輕鬆操縱變化多端的源、格式和硬件設備,在這樣的架構裏,應用程序只需組合和匹配多個filter來實現功能。     DirectShow提供的filter支持基於WDM的採集和調諧設備,也支持早先的VFW採集卡和爲ACM和VCM接口編寫的編碼器。     下圖顯示了應用程序、DirectShow組件和DirectShow支持的硬件和軟件組件之間的關係:         directshow overview

    如圖,DirectShow將應用程序與衆多複雜的設備隔離開來,通信和控制這些設備均出DirectShow的filter來完成。DirectShow同樣爲某種文件格式提供與之對應的編解碼器。

3.2. Filter Graph和它的組件     這一節描述了DirectShow的主要組件,爲DirectShow應用程序和DirectShow Filter開發者提供一個介紹。應用程序開發者可以忽略掉很多底層部分,但是,瞭解底層對於理解DirectShow架構還是很有幫助的。 3.2.1. 關於DirectShow Filter     DirectShow使用一個模塊化的架構,每個處理過程都由一個叫做filter的COM對象來實現。DirectShow爲應用程序提供了一系列標準的filter,開發者也可以編寫自己的filter來擴展DirectShow的功能。下面是播放一個AVI文件的各個步驟:     *從文件中讀取數據並轉成字節流(File Source filter)     *檢查AVI頭,分析字節流並將它們分離成視頻和音頻(AVI Aplitter filter)     *將視頻解碼(不同的解碼filter,取決於不同的壓縮格式)     *將視頻顯示出來(Video Renderer filter)     *將音頻送入聲卡(Default DirectSound Device filter)

avi play filter graph

    如圖所示,每個filter與一個或多個其它的filter相連,其中的連接點也是一個COM對象,稱作Pin,filter使用Pin將數據從一個filter轉移到另一個,圖中的箭頭指示了數據流動的方向。在DirectShow中,這一系列連接在一起的filter稱作filter graph。     Filter可能處於有三種不同的狀態:運行、停止和暫停狀態。filter在運行狀態時處理數據,停止狀態時停止處理數據,暫停狀態則是表示就緒,可以開始進入運行狀態。除了極個別的情況,一個filter Graph中的所有filter通常都處理同一個狀態下,因此,filter graph也可以稱其處於運行、停止、暫停狀態。     Filter可以被分成幾個大的種類:     *source filter - filter graph的數據源,這些數據可以來自文件、網絡、攝像頭或任何其它東西。每一個source filter操縱不同類型的數據源。     *transform filter - 接收數據,處理數據並將它送入下一個filter。編碼filter和解碼filter都屬於這個種類。     *Renderer filter - 處於filter鏈的未端,接受數據並將其展現給用戶。比如,一個視頻renderer在顯示器上繪製視頻圖像;一個音頻renderer將音頻數據送入聲卡;一個寫文件filter(file-writer filter)將數據存盤。     *splitter filter - 分析輸入的數據流並將其分解成兩路或多路,比如,AVI splitter分析字節流並將其分解成視頻流和音頻流。     *mux filter - 將多路輸入流合併成一路。比如,AVI Mux正好與AVI splitter做相反的工作,它將視頻和音頻流合成爲一個AVI格式的字節流。     以上的分類並不是絕對的,比如,ASF Reader Filter同時充當了source filter和splitter filter的角色。     所有的DirectShow filter都提供IBaseFilter接口,所有的Pin也都提供IPin接口。DirectShow也定義了許多其它的接口以實現特定的功能。

3.2.2. 關於Filter Graph Manager     Filter Graph Manager是一個用以控制filter graph中的filter的COM對象。它提供了許多功能,包括:     *協調filter之間的狀態變化     *建立參考時鐘(reference clock)     *將事件返回給應用程序     *提供應用程序建立filter graph的方法     這裏先簡單地描述一個這些功能。     狀態變化:filter們的狀態變化必須遵照一個特定的次序,因此,應用程序不能將狀態變化的命令直接發給filter,而是將一個簡單的命令發給filter graph manager,由它來將命令分發給各個filter。定位命令同樣使用這種方式,應用程序發送一個定位命令給filter graph manager,由它來分發。     參考時鐘:在filter graph中的所有filter都使用一個相同的時鐘,稱爲參考時鐘(reference clock)。參考時鐘保證了所有流的同步。一個視頻幀或一段音頻樣本被播放的時間鈔稱作呈現時間(presentation time)。呈現時間精確地相對於參考時鐘。Filter Graph Manager通常選擇的參考時鐘是聲卡參考時鐘或系統時鐘。     Graph事件:filter graph manager使用一個消息隊列來通知應用程序發生在filter graph中的事件。     Graph-buliding 方法:filter graph manager提供給應用程序將filter加入到filter graph中的方法,以及將filter與filter連接或斷開連接的方法。     Filter graph manager不提供操縱在filter之間流動數據的功能,這個功能由filter通過pin連接在一個單獨的線程中自行完成。 3.2.3. 關於媒體類型(Media Type)     因爲DirectShow是模塊化的,因此需要有一個在filter graph各個點之間描述格式的方法。比如說,AVI回放,數據輸入時是一個RIFF塊的流,然後被分解成視頻和音頻流。視頻流由一個個可能被壓縮的視頻幀組成,解壓後,視頻流又變成了一系列未壓縮的位圖。音頻與視頻類似。     Media Type:DirectShow怎樣來描述格式     Media Type是描述數字媒體格式的常用方式。當兩個filter連接時,它們需要協商決定同一個Media Type。Media Type標識了從上一個filter遞交到下一個filter或物理層的數據流格式。如果兩個filter對Media Type不能協商一致,則不能連接。     對於某些應用程序,你不必去關心Media type,比如文件回放,DirectShow做了所有有關它的事情。     Media type使用AM_MEDIA_TYPE結構體來定義,這個結構體包含了以下內容:     *Major type:主類型,是一個GUID,定義了數據的整體類型,包括了:視頻、音頻、未分析的字節流、MIDI等。     *Subtype:子類型,另一個GUID,進一步定義了數據格式。比如,如果主類型是視頻,則子類型可以是RGB-24、RGB-32、UYVY等格式,如果主類型是音頻,則可能是PCM或MPEG-1 payload等。子類型提供了比主類型更多的內容,但仍未提供完整的格式定義,比如,子類型沒有定義圖像尺寸和幀率,這些都將在Format block中被定義。     *Format block:格式塊,定義了具體的格式。格式塊是AM_MEDIA_TYPE結構體中一個單獨被分配的內存空間,pbFormat成員指向這塊內存空間。因爲不同的格式會有不同的格式描述,所以pbFormat成員的類型是void*。比如,PCM音頻使用WAVEFORMATEX結構體,視頻使用不同的結構體包括:VIDEOINFOHEADER和VIDEOINFOHEADER2。formattype成員是一個GUID,指定了格式塊包含了哪種結構體,每一種格式的結構體都被分配了GUID。cbFormat成員定義了格式式塊的長度。     當格式塊被定義時,主類型和子類型包含的信息就顯得有點多餘了。其實,主類型和子類型爲識別格式提供了一個便利的方法,比方說,你可以指定一個普通的24位RGB格式(MEDIASUBTYPE_RGB24),而不需去關心VIDEOINFOHEADER結構體中諸如圖像尺寸和幀率這些信息。     下面是一個filter檢查媒體類型的例子:

HRESULT CheckMediaType(AM_MEDIA_TYPE *pmt) {     if (pmt == NULL) return E_POINTER; 

    // 檢查主類型,我們需要的是視頻     if (pmt->majortype != MEDIATYPE_Video)     {         return VFW_E_INVALIDMEDIATYPE;     }

    // 檢查子類型,我們需要的是24-bit RGB.     if (pmt->subtype != MEDIASUBTYPE_RGB24)     {         return VFW_E_INVALIDMEDIATYPE;     }

    // 檢查format type和格式塊的大小.     if ((pmt->formattype == FORMAT_VideoInfo) &&          (pmt->cbFormat >= sizeof(VIDEOINFOHEADER) &&          (pmt->pbFormat != NULL))     {         // 現在可以安全地將格式塊指針指向正確的結構體。         VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;              // 檢查pVIH (未展示). 如果正確,返回S_OK.         return S_OK;     }

    return VFW_E_INVALIDMEDIATYPE; }

    AM_MEDIA_TYPE結構體還包含了一些任選項,用來提供附加的信息,filter不需要這些信息:     *ISampleSize,如果這個字段非零,表示這是每個sample的尺寸,如果是零,則表示sample的尺寸會改變。     *bFixdSizeSamples,如果這個布爾類型的標記是TRUE,表示ISampleSize有效,否則,你可以忽略ISampleSize。     *bTemporalCompression,如果這個布爾類型的標記是FALSE,表示所有幀都是關鍵幀。

3.2.4. 關於媒體樣本(Media Sample)和分配器(Allocator)     Filter通過Pin與Pin之間的連接來遞交數據,數據從一個filter的輸出Pin轉移到另一個filter的輸入Pin,除了個別情況,實現這種功能通常的方法是調用輸入Pin上的IMemInputPin::Receive方法。     依靠filter,媒體數據的內存空間可以通過多個途徑來分配:在堆上、在DirectDraw表面(surface)、在共享GDI內存或使用其它的分配機制。這個負責分配內存空間的對象稱爲分配器(Allocator)是一個暴露IMemAllocator接口的COM對象。     當兩個Pin相連時,其中的一個Pin必須提供一個分配器。DirectShow定義了一個方法調用序列來決定到底由哪個Pin來提供分配器。Pin還負責協商分配器創建的緩衝數和每個緩衝的尺寸。     在數據流開始之前,分配器創建了一個緩衝池。在數據流動過程中,上游filter在緩衝中填入數據並遞送給下游filter,但是,上游filter遞送給下游filter的並不是原始的緩衝區指針,而是一個稱爲媒體樣本(Media Sample)的COM對象,它由分配器創建並用來管理緩衝區,暴露IMediaSample接口。一個媒體樣本包含:     *指向下層緩衝區的指針     *時間戳     *各種標記     *可選的媒體類型     時間戳定義了呈現時間(presentation time),用以讓renderer filter確定播放的合適時機。各種標記可以用來指示很多事情,比如,數據在上一個sample後是否被打段過(如重新定位、掉幀)等。媒體類型爲流中間改變數據格式提供了途徑,通常,沒有媒體類型的sample,被認爲從上一個sample以來數據格式沒有被改變過。     當filter使用一個緩衝時,它保存了sample上的參考計數。分配器使用參考計數來決定什麼時候可以重用這個緩衝,這防止了一個filter在寫一個緩衝時另一個filter還在使用這個緩衝,除非所有的filter都釋放了這個緩衝,否則sample不會將其返回給分配器的緩衝池。      3.2.5. 硬件如何參與Filter Graph     這一節描述了DirectShow如何與音頻和視頻硬件交互。           外殼filter(Wrapper Filter)     所有的DirectShow filter都是用戶模式的軟件組件。爲了使象視頻採集卡這樣的內核模式的硬件驅動加入到filter graph中,必須使其象用戶模式的filter那樣。DirectShow提供外殼filter來完成這個功能,這類filter包括:Audio Capture filter、VFW Capture filter、TV Tuner filter、TV Audio filter和Analog Video Crossbar filter。DirectShow也提供一個叫KsProxy的filter,它可以實現任何類型的WDM流驅動。硬件商通過提供一個Ksproxy plug-in來擴展KsProxy,以使其支持自己的功能,ksproxy plug-in是一個被KsProxy聚合的COM對象。     外殼filter通過暴露COM接口來實現設備的功能。應用程序使用這些接口將信息傳遞給filter,filter再把這些COM調用轉化爲設備驅動調用,將信息傳遞到內核模式下的設備中去,然後返回結果給應用程序。TV Tuner、TV Audio、Analog Video Crossbar和KsProxy filter都通過IKsPropertySet接口來支持驅動的自定義屬性,VFW Capture filter和Audio Capture filter不支持這種方式。     外殼filter使應用程序可以象控制其它directshow filter一樣來控制設備,filter已經封裝了與內核驅動通信的細節。          Video for Windows Devices     VFW Capture filter支持早期的VFW採集卡,當一個設備加入到目標系統中支後,它可以被directshow使用系統設備枚舉器(System Device Enumerator)發現並加入到filter graph中去。     音頻採集(Audio Capture)和混音設備(聲卡)(Mixing Device/Sound Card)     較新的聲卡都有麥克風等設備的插口,而且大多數這類聲卡都有板級的混頻能力,可單獨控制每一個連接設備的音量及高低音。在directshow中,聲卡的輸入和混頻設備被Audio Capture filter封裝。每個聲卡都能被系統設備枚舉器發現。要查看你的系統中的所有聲卡,只需打開GraphEdit,從Audio Capture Sources一類中選擇即可,每個在這個類裏的filter都是一個單獨的Audio Capture filter。

    WDM流設備     較新的硬解碼設備和採集卡都遵照WDM規範。這些設備和比VFW設備更強大的功能,以及可以應用於多種系統(winxp,winNT,win2000,win98/me)。WDM視頻採集卡支持許多VFW所沒有的功能,包括枚舉採集的格式、編程控制視頻參數(如對比度、亮度)、編程選擇輸入端和電視調諧支持。     爲了支持WDM流設備,directshow提供了KsProxy filter(ksproxy.ax)。KsProxy被稱爲“瑞士軍刀",因爲它可以做很多不同的事情。filter上pin的數量,以及COM接口的數量,取決於底層驅動的能力。KsProxy不以"KsProxy"這個名字顯示在filter graph中,而是使用一個已在註冊表中登記的設備名稱。要查看你係統中的WDM設備,可以運行GraphEdit然後從WDM Streaming這個類別中選擇。即使你的系統中只有一塊WDM卡,這塊卡也可能包含多個設備,而每一個設備都表現爲一個filter,每個filter是實際意義上的KsProxy。     應用程序使用系統設備枚舉器在系統中尋找WDM設備moniker,然後調用moniker的BindToObject來實例化。因爲KsProxy能夠表現所有類型的WDM設備,因此它必須通過詢問驅動來決定哪些屬性是驅動所支持的。屬性集是一組數據結構的集合,被WDM設備使用,也被諸如MPEG2軟解碼filter這樣的用戶模式filter使用。KsProxy通過暴露COM接口來配置自己,硬件商則通過提供插件來擴展KsProxy,插件暴露硬件商自定義的一些接口,用以實現特殊的功能。所有這些細節對於應用程序來說都是不可見的,應用程序通過KsProxy控制設備就象控制其它的DirectShow filter一樣。          內核流     WDM設備支持內核流,在內核流中數據在內核模式下被徹底流化而永遠不需要切換到用戶模式下去,從而避免了在內核模式和用戶模式之間切換的巨大開銷,內核流允許高的比特率而不消耗CPU的時間。基於WDM的filter能夠使用內核流將多媒體數據一個硬件設備送入到另一箇中去,既可以是在同一塊卡中也可以在不同的卡中,而不需要將數據拷入系統主存。     從應用程序的視點來看,數據好象是從一個用戶模式的filter傳到另一箇中去,但是實際上,數據根本就沒有傳到用戶模式下過,而是可能支接從內核模式的設備中傳到下一個中去直至被呈現(render)在顯卡上。某些情況,比如採集視頻到一個文件中去,在某些點上需要將數據從內核模式傳入到用戶模式,但是,仍然沒有必要將數據拷貝到內存的一個新位置中去。     應用程序開發者通常只需瞭解一個內核流的背景知識而不需要深究它的細節。

3.3. 構建Filter Graph 3.3.1. 用於構建Graph的組件     DirectShow提供了一系列用於構建filter graph的組件,包括:     *Filter Graph Manager。這個對象用於控制filter graph,支持IGraphBuilder、IMediaControl和IMediaEventEx等許多接口。所有的directshow應用程序都需要在某些地方用到這個對象,雖然在有些情況下,是其它的對象爲應用程序創建了filter graph manager。     *Capture Graph Builder。這個對象爲構建filter graph提供附加的方法。它最初是爲構建提供視頻採集的graph而設計的(這正是它的名字由來),但是對於構建許多另外類型的filter graph也是很有用的。它支持ICaptureGraphBuilder2接口。     *Filter Mapper和System Device Enumerator。這些對象用於查找在系統中註冊的或代表硬件驅動的filter。     *DVD Graph Builder。這個對象構建用以回放和導航DVD的filter graph。它支持IDvdGraphBuilder接口。基於腳本的應用程序能夠使用MSWebDVD ActiveX控件來控制DVD回放。     *Video Control。WinXP提供這個ActiveX控件,用於操縱directshow中的數據和模擬電視。          智能連接(Intelligent Connect)     智能連接這個術語覆蓋了一系列Filter Graph Manager用於構建所有或部份filter graph的算法。任何時候,當Filter Graph Manager需要添加filter來完成graph時,它大致做以下幾件事情:     1.如果有一個filter存在於graph中,而且這個filter有至少一個沒有連接的input pin,Filter Graph Manager試着去試用這個filter。     2.否則,Filter Graph Manager在已註冊的filter中尋找連接時可以接受合適的媒體類型的filter。每一個filter都註冊有一個Merit值,這個值用以標記哪個filter最容易被Filter Graph Manager選中來完成graph。Filter Graph Manager按Merit值的順序來選擇filter,Merit值越大,被選中的機會越大。對於每種流類型(如音頻、視頻、MIDI),默認的renderer具有一個很高的Merit值,解碼器同樣是,專用filter具有低Merit值。     如果Filter Graph Manager因選擇的filter不合適而被困,它會返回來嘗試另外的filter組合。

3.3.2 Grap構建概述     創建一個filter graph,從創建一個Filter Graph Manager實例開始:

     IGraphBuilder* pIGB;      HRESULT hr = CoCreateInstance(CLSID_FilterGraph,      NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder,(void **)&pIGB);
     Filter Graph Manager支持下列Graph構建方法:     *IFilterGraph::ConnectDirect,在兩個pin之間進行直接連接,如果連接失敗,則返回失敗     *IFilterGraph::Connect,連接兩個Pin,如果可能的話,直接連接它們,否則,在中間加入其它的filter來完成連接。     *IGraphBuilder::Render,從某個輸出Pin處開始完成餘下的graph構建。該方法會自動在輸出pin後面添加必須的filter,直到renderer filter爲止。     *IGraphBuilder::RenderFile,構建一個完整的文件回放graph。     *IGraphBuilder::AddFilter,將一個filter添加到graph中。它不連接filter,並且在調用此方法前,filter必須已經被創建。創建filter可以是用CoCreateInstance方法或使用Filter Mapper或系統設備枚舉器(System Device Enumerator)。     這些方法提供了三種構建graph的途徑:     1.filter graph manager構建整個graph     2.filter graph manager構建部分graph     3.應用程序構建整個graph          Filter Graph Manager構建整個graph     如果你僅僅是想回放一個已知格式的文件,如AVI、MPEG、WAV或MP3,使用RenderFile方法。     RenderFile方法首先尋找註冊在系統中能分析源文件的filter,它使用協議名(如http://),文件擴展名或文件的頭幾個字節來決定選擇哪一個源filter。     Filter Graph Manager使用一個迭代過程來完成餘下的graph構建。在這個迭代過程中,它逐個列出filter的輸出pin上支持的媒體類型,並搜索哪個已註冊的filter的輸入Pin接受該媒體類型。它使用一系列的規則來縮小filter的範圍並排定優先順序:     *filter類別(category)標識的filter的一般功能     *媒體類型描述filter能在接受或能輸出哪種數據類型     *merit值決定filter被嘗試的次序。如果兩個filter具有相同的filter類別並且同時支持相同的輸入類型,Filter Graph Manager選擇merit值大的那一個。一些filter故意給出一個小merit值是因爲它是爲特殊用途設計的,僅能由應用程序來將其添加到graph。     Filter Graph Manager使用Filter Mapper對象來搜索已註冊的filter。     每個filter被添加時,filter graph manager試着將其與前一個filter的輸出pin連接。它們協商決定他們是否能連接,如果能,哪一種媒體類型被用來連接。如果新filter不能連接,filter graph manager丟棄它並嘗試別一個,這個過程一直繼續到每個流都被render爲止。          Filter Graph Manager構建部分graph     如果不僅僅是播放一個文件,那麼你的應用程序就必須做一些graph的構建工作。比如,一個視頻採集應用程序必須先選擇一個source filter並將其添加到graph中去。如果你需要將數據寫入到一個AVI文件中,你必須添加一個AVI Mux和File Write filter。不過,也經常有可能讓filter graph manager來完成整個graph,比如,你可以通過Render方法來render一個pin進行預覽。          應用程序構建整個graph     在某些場合,你的應用程序需要添加和連接每個filter來構建graph。在這種情況下,你很可能明確地知道哪些filter需要加到graph中去。使用這種方式,應用程序通過調用AddFilter方法添加每個filter,然後枚舉filter上的pin,調用Connect或ConnectDirect來連接它們。

3.3.3. 智能連接     智能連接是filter graph manager用以構建filter graph的機制。它包含了一系列相關的用以選擇filter和將它們添加到graph中去的算法。作爲應用程序開發者,你並不需要很具體地瞭解智能連接的細節。如果你在構建某個filter graph時遇到問題並希望能解決它,或者你正在編寫你自己的filter並希望它能自動地被graph構建,請閱讀這一節。     智能連接涉及以下IGraphBuilder方法:     *IGraphBuilder::Render     *IGraphBuilder::AddSourceFilter     *IGraphBuilder::RenderFile     *IGraphBuilder::Connect     Render方法構建一部分graph,它從一個尚未連接的輸出pin開始順着數據流的方向往下,添加必要的filter,起始的那個filter必須已被添加到了graph中。Render方法每一步都搜索一個能夠連接到前一個filter的filter,如果新連接上的filter有多個輸出pin,數據流能自動分流,搜索直到每個流都被renderer爲止。如果Render方法搜索到的filter無法使用,它會返回去嘗試另一個filter。     要連接每一個輸出pin,Render方法做以下工作:     1.如果pin支持IStreamBuilder接口,Filter Graph Manager讓pin的IStreamBuilder::Render方法來完成整過程。通過暴露這個接口,pin承擔了構建graph剩餘部分的全部工作。但是,只有很少數的filter支持此接口。     2.Filter Graph Manager嘗試使用任何在緩存中的filter。在智能連接的整個過程中,filter graph manager可以在早期將filter緩存起來。     3.如果filter graph包含了任何有未連接的輸入pin的filter,filter graph manager會將其當作下一個filter來嘗試連接。你可以通過在調用Render之前添加特定的filter來強制讓Render方法來嘗試這個filter。     4.最後,filter graph manager使用IFilterMapper2::EnumMatchingFilters方法在所有註冊的filter中尋找,依據已註冊的媒體類型列表來逐個試着匹配輸出pin的各個媒體類型(按優先級高低排列)。     每個已註冊的filter都有一個merit值,這是一個用來表示filter優先級的數字,最大優先級越高,EnumMatchingFilters方法返回的filter集依據merit值來排列,直至最小的merit值MERIT_DO_NOT_USE+1,它忽略merit爲MERIT_DO_NOT_USR或更小的filter。filter也通過GUID來歸類,類別本身也有merit值,EnumMatchingFilters方法忽略任何merit值爲MERIT_DO_NOT_USE或更小的類別,即使在那個類別中的filter有較高的merit值。     總結一下,Render方法以下列步驟嘗試filter     1.使用IStreamBuilder     2.嘗試被緩存的filter     3.嘗試已添加在graph中的filter     4.在已註冊的filter中尋找     AddSourceFilter方法添加一個能render特定文件的source filter。首先,它依據協議名(如Http://)、文件擴展名、或文件頭在已註冊的filter中尋找匹配的那個。如果此方法定位到了一個合適的source filter,它便立刻創建一個這個filter的實例,並將其添加到graph中,然後調用filter的IFileSourceFilter::Load方法。     RenderFile方法依據一個文件名來構建一個默認的回放graph,在其內部,RenderFile方法調用AddSourceFilter來定位source filter,並且用Render來構建Graph的餘下部分。     Connect方法將輸出pin連接到輸入pin上去,這個方法自動添加必要的中間filter到graph中去,使用在Render方法中描述的那一系列算法:     1.使用IStreamBuilder     2.嘗試被緩存的filter     3.嘗試已添加在graph中的filter     4.在已註冊的filter中尋找

3.4. Filter Graph中的數據流     這一節主要描述媒體數據是如何在filter graph中流動的。如果你只是爲了編寫DirectShow應用程序,你不需要知道這些細節,當然,知道這些細節對於編寫directshow應用程序仍然是有幫助的。但是如果你要編寫directshow filter,那麼你就必須掌握這部分知識了。 3.4.1. DirectShow數據流概述    在這一部分先粗略地描述一下DirectShow中數據流是如何工作的。     數據首先是被保存在緩衝區裏的,在緩衝區裏,它們僅僅是一個字節數組。每一個緩衝區被一個稱作媒體樣本(media sample)的COM對象所包容,media sample提供IMediaSample接口。media sample由另一個稱作分配器(allocator)的COM對象創建,allocator提供IMemAllocator接口。每一個pin連接都指定有一個allocator,當然,兩個或多個pin連接也可以共享幾個allocator。

Buffers, samples, and allocators

    每一個allocator都創建一個media sample池,併爲每個sample分配緩衝區。一旦一個filter需要一個緩衝區來填充數據,它就調用IMemAllocator::GetBuffer方法來請求一個sample。只要allocator有一個sample還沒有被任何filter使用,GetBuffer方法就立即返回一個sample的指針。如果allocator所有的sample已經被用完,這個方法就阻塞在那裏,直到有一個sample變成可用的了。GetBuffer返回一個sample後,filter就將數據寫入到sample的緩衝區中去,並在sample上設置適當的標記(如時間戳),然後將它遞交到下一個filter去。     當一個renderer filter接收到了一個sample時,renderer filter檢查時間戳,並將sample先保存起來,直到filter graph的參考時鐘指示這個sample的數據可以被render了。當filter將數據render後,它就將sample釋放掉,此時sample並不立即回到allocator的sample池中去,除非這個sample上的參考計數已經變爲0,表示所有的filter都已釋放這個sample。     

Decoder waiting for a free media sample

    上游的filter可能在renderer之前運行,這就意味着,上游的filter填充緩衝的速度可能快於renderer銷燬它們。但是儘管如此,samples也並無必要更早地被render,因爲renderer將一直保存它們直到適當的時機去render,並且,上游filter也不會意外地將這些samples的緩衝覆蓋掉,因爲GetSample方法只會返回那些沒有被使用的sample。上游filter可以提前使用的sample的數量取決於allocator分配池中的sample的數量。     前面的圖表只顯示了一個allocator,但是通常的情況下,每個流中都會有多個allocator。因此,當renderer釋放了一個sample時,它會產生一個級聯效應。如下圖所示,一個decoder保存了一個視頻壓縮幀,它正在等待renderer釋放一個sample,而parser filter也正在decoder去釋放一個sample。     

Two filters waiting for samples

    當renderer釋放了一個sample後,decoder完成尚未完成的GetBuffer調用。然後decoder便可以對壓縮的視頻幀進行解碼並釋放它保存的sample,從而使parser完成它的GetBuffer調用。     3.4.2. 傳輸協議(Transports)    爲了使媒體數據能在filter graph中流動,Directshow filter必須能支持多個協議中的一個,這些協議被稱作傳輸協議(transports)。當兩個filter連接後,它們必須支持同一個傳輸協議,否則,它們將不能交換數據。通常,一個傳輸協議要求某個pin支持一個特定的接口,當兩個filter連接時,另一個pin來調用這個pin的這個接口。     大多數的directshow filter在主存中保存媒體數據,並且通過pin連接向另一個filter遞交數據,這種類型的傳輸協議被稱作本地內存傳輸協議(local memory transport)。儘管這類傳輸協議在directshow中應用最普遍,但並非所有的filter都使用它。例如,某些filter通過硬件途徑來傳遞數據,使用pin僅僅是爲了傳遞控制信息,如IOverlay接口。     DirectShow爲本地內存傳輸協議定義了兩種機制,推(push)模式拉(pull)模式。在推模式中,source filter產生數據,並將其遞交給下游的filter,下游的filter被動地接收數據並處理它們,再將數據傳遞給它的下游filter。在拉模式中,source filter與一個parser filter連接,parser filter向source filter請求數據,source filter迴應請求並傳遞數據。推模式使用IMemInputPin接口,而拉模式使用IAsyncReader接口。     推模式比拉模式應用更廣泛。      3.4.3. 媒體樣本(sample)和分配器(allocator)     當一個pin向另一個pin傳遞媒體數據時,它並不是直接傳遞一個內存緩衝區的指針,而是傳遞一個COM對象的指針,這個COM對象管理着內存緩衝,被稱爲媒體樣本(media sample),暴露IMediaSample接口。接收方pin通過調用IMediaSample接口的方法來訪問內存緩衝,如IMediaSample::GetPointerIMediaSample::GetSizeIMediaSample::GetActualDataLength。     sample總是從輸出pin到輸入pin向下傳輸。在推模式中,輸出pin通過在輸入pin上調用IMemInputPin::Receive方法來傳遞一個sample。輸入pin或者在Receive方法內部同步地處理數據,或者另開一個工作線程進行異步處理。如果輸入pin需要等待資源,允許在Receive中阻塞。     另一個用來管理媒體樣本的COM對象,被稱作分配器(allocator),它暴露IMemAllocator接口。一旦一個filter需要一個空閒的媒體樣本,它調用IMemAllocator::GetBuffer方法來獲得sample的指針。每一個pin連接都共享一個allocator,當兩個pin連接時,它們協商決定哪個filter來提供allocator。pin可以設置allocator的屬性,比如緩衝的數量和每個緩衝的大小。     下圖顯示了allocator、media sample和filter的關係:          

Media Samples and Allocators

    媒體樣本參考計數(Media Sample Reference Counts)     一個allocator創建的是一個擁有有限個sample的sample池。在某一時刻,有些sample正在被使用,有些則可被GetBuffer方法使用。allocator使用參考計數來跟蹤sample,GetBuffer方法返回的sample參考計數爲1,如果參考計數變爲0,sample就可以返回到allocator的sample池中去了,這樣它就可以再次被GetBuffer方法使用。在參考計數大於0期間,sample是不能被GetBuffer使用的。如果每個從屬於allocator的sample都在被使用,則GetBuffer方法會被阻塞直至有sample可以被使用。     舉個例子,假設一個輸入pin接收到一個sample。如果它同步地在Receive方法內部處理它,sample的參考計數不增加,當Receive返回時,輸出pin釋放這個sample,參考計數歸0,sample就返回到sample池中去了。另一種情況,如果輸入pin異步地處理sample,它就在Receive方法返回前將sample的參考計數加1,此時參考計數變爲2。當輸出pin釋放這個sample時,參考計數變爲1,sample不能返回到sample池中去,直到異步處理的工作線程完成工作,調用Release釋放這個sample,參考計數變爲0時,它纔可以返回到sample池中去。     當一個pin接收到一個sample,它可以將數據拷貝到另一個sample中去,或者修改原始的sample並將其傳遞到下一個filter中去。一個sample可能在整個graph長度內被傳遞,每個filter都依次調用AddRef和Release。因而,輸出pin在調用Receive後一定不能重複使用同一個sample,因爲下游的filter可能正在使用這個sample。輸出pin只能調用GetBuffer來獲得新的sample。     這個機制減少了總的內存分配過程,因爲filter可以重複使用同樣的緩衝。它同樣防止了數據在被處理前意外地被覆蓋寫入。     當filter處理數據後數據量會變大(如解碼數據),一個filter可以爲輸入pin和輸出pin分配不同的allocator。如果輸出數據並不比輸入數據量要大,filter可以用替換的方式來處理數據而不用將其拷貝到新的sample中去,在這種情況下,兩個或多個pin連接共享一個allocator。          提交(Commit)和反提交(Decommit)分配器     當一個filter首次創建一個allocator時,allocator並不爲其分配內存緩衝,此時如果調用GetBuffer方法的話會失敗。當流開始流動時,輸出pin調用IMemAllocator::Commit來提交allocator,從而爲其分配內存。此時pin可以調用GetBuffer了。     當流停止時,pin調用IMemAllocator::Decommit來反提交allocator,在allocator被再次提交前所有後來的GetBuffer調用都將失敗,同樣,如果有阻塞的正在等待sample的GetBuffer調用,也將立即返回失敗信息。Decommit方法是否釋放內存取決於實現方式,如CMemAllocator類直至析構時才釋放內存。      3.4.4. filter狀態     filter有三種可能的狀態:停止(stopped)就緒(paused)運行(running)。就緒狀態的目的是爲了讓graph提前做準備以便在run命令下達時可以立即響應。Filter Graph Manager控制所有的狀態轉換。當一個應用程序調用IMediaControl::RunIMediaControl::PauseIMediaControl::Stop時,Filter Graph Manager在所有filter上調用相應的IMediaFilter方法。在停止狀態和運行狀態之間轉換時總是要經過就緒狀態,即如果應用程序在一個處於停止狀態的graph上調用Run時,Filter Graph Manager在運行它之前先將其轉爲pause狀態。     對於大多數filter來說,運行狀態和就緒狀態是等同的。看下面的這個graph:     Source > Transform > Renderer     假設這個source filter不是一個實時採集源,當source filter就緒時,它創建一個線程來儘可能快地產生新數據並寫入到media sample中去。線程通過在transform filter的輸入pin上調用IMemInputPin方法將sample“推”到下游filter。transform filter在source filter的線程中接收數據,它可能也使用一個工作線程赤將sample傳遞給renderer,但是在通常情況下,它在同一個線程中傳遞它們。如renderer處理就緒狀態下,它等待接收sample,當它接收到一個時,它或阻塞或保存那個sample,如果這是一個Video renderer,則它將sample顯示爲一個靜態的圖片,只在必要的時候刷新它。     此時,流已經準備充分去被render,如果graph仍然處理就緒狀態下,sample會在每一個sample後堆積,直至每個filter都被阻塞在Receive或GetBuffer下。沒有數據會被丟失。一旦source線程的阻塞被解除時,它只是簡單地從阻塞點那裏進行恢復。     source filter和transform filter忽略從就緒狀態轉到運行狀態——它們僅僅是儘可能快地繼續處理數據。但是當renderer運行時,它就要開始render sample了。首先,它render在就緒狀態下保存的那個sample,接着,每接收到一個新的sample,它計算這個sample的呈現時間,renderer保存每個sample直至到了它們的呈現時間再render它們。在等待合適的呈現時間時,它或者阻塞在Receive方法上,或者在一個工作線程中接收數據並將其放入隊列中去。renderer的上一個filter不關心這些問題。     實時源(live source),如採集設備,是通常情況中的一個例外。在實時源中,不適合提前準備數據。應用程序可能將graph置於就緒狀態下,然後等很長時間纔再運行它。graph不應該再render就緒期間的sample,因此,一個實時源在就緒狀態時不產生新的sample。要將這種情況通知給filter graph manager,source filter的IMediaFilter::GetState方法返回VFW_S_CANT_CUE。這個返回值表示filter已切換到就緒狀態下,即使renderer還沒有收到任何數據。     當一個filter停止時,它不再接收任何傳遞給它的數據。source filter關閉它們的流線程,別的filter關閉所有它們創建的工作線程。pin反提交(decommit)它們的allocator。          狀態轉換    filter graph manager按從下游filter到上游filter的次序來完成所有的狀態轉換,從renderer開始逐個向上直至source filter,這個次序是必要的,可以防止數據丟失或graph死鎖。最重要狀態轉換是就緒狀態和停止狀態間的轉換:     *停止狀態到就緒狀態:當每一個filter被置爲就緒態時,它便準備好從上一個filter接收sample。source filter是最後一個被置爲就緒態的filter,它創建數據流線程並開始傳遞sample。因爲所有下游filter都處於就緒狀態,所以沒有一個filter會拒絕接收sample。當graph中所有的renderer都接收到一個sample後,filter graph manager才徹底完成狀態轉換工作(實時源除外)。     *就緒狀態到停止狀態:當一個filter停止時,它釋放了所有它保存的sample,就將解除所有上游filter調用GetBuffer時的阻塞。如果filter正在Receive方法中等待數據,則它停止等待並從Receive中返回,從而解除阻塞。因而,此時當filter graph manager再去將上游filter轉換爲停止狀態時,它已經不再阻塞於GetBuffer和Receive,從而可以響應停止命令。上游filter在得到停止命令前可能會傳遞下一些過時的sample,但下游filter不再接收它們,因爲此時下游filter已處於停止狀態了。     3.4.5. 拉模式     在IMemInputPin接口中,上游filter決定哪些數據要被髮送,然後將數據推到下游filter中去。但是在某些情況下,拉模式會更加合適。在拉模式中,只有當下遊filter從上游filter中請求數據時,數據才被傳遞下去,數據流動由下游filter發起。這種類型的連接使用IAsyncReader接口。     典型的拉模式應用是文件回放。比如,在一個AVI回放graph中,Async File Source filter完成一般的文件讀操作並將數據作爲字節流傳遞下去,沒有什麼格式信息。AVI Splitter filter讀取AVI頭並將數據流分解成視頻和音頻sample。AVI Splitter比Async File Source filter更能決定它們需要哪些數據,因此需用IAsyncReader接口來代替IMemInputPin接口。     要從輸出pin請求數據,輸入pin調用下面方法中的一個:     *IAsyncReader::Request     *IAsyncReader::SyncRead     *IAsyncReader::SyncReadAligned     第一個方法是異步的,支持多重讀操作。其餘的是同步的。     理論上,任一個filter都能支持IAsyncReader,但是實際上,它僅僅在連接有一個parser filter的source filter上使用。分析器(parser)非常象一個推模式的source filter,當它就緒時,它創建一個數據流線程,從IAsyncReader連接中拉數據並將其推到下一遊filter中去。它的輸出pin使用IMemInputPin,graph餘下的部分使用標準的推模式。

3.5 DirectShow中的事件通告     這一節主要描述在directshow filter graph中事件是怎樣發生的,以及應用程序如何接收事件通告並響應它們。 3.5.1 概述     一個filter通過發送一個事件通來通知filter graph manager某個事件已經發生。這些事件可以是一些預知的事件比如流結束事件,也可以是一些異常如render流時失敗。一部分事件由filter graph manager自己處理,另一部分則由應用程序來處理。如果filter graph manager不處理某個事件,那麼這個事件會被放入到隊列中去。filter graph也可以通過隊列將自己的事件發送給應用程序。     應用程序從隊列中接收事件並根據其類型來響應它們。DirectShow中的事件通告類似於windows的消息隊列機制。應用程序可以讓filter graph manager取消對指定的事件類型的默認操作,而是將它們放入事件隊列由應用程序來處理它們。     由於這樣的機制,使我們能做到:      *filter graph manager與應用程序的對話      *filter可以即和應用程序也和filter graph manager對話      *由應用程序來決定處理事件的複雜度。

3.5.2 從隊列中取事件     Filter Graph Manager暴露3個支持事件通知的接口:      *IMediaEventSink 包含filter發送事件的方法      *IMediaEvent 包含應用程序取事件的方法      *IMediaEventEx 繼承擴展IMediaEvent接口     filter通過在filter graph manager上調用IMediaEventSink::Notify方法來發送事件通告,一個事件通知由一個表示事件類型的事件號,和兩個DWORD類型用以放置附加信息的參數組成。按事件號的不同,這兩個參數可以是指針、返回值、參考時間或者其它信息。完整的事件號和參數列表,參見Event Notification codes(http://msdn.microsoft.com/library/en-us/directshow/htm/eventnotificationcodes.asp)。     要從事件隊列中取事件,應用程序需要在filter graph manager上調用IMediaEvent::GetEvent事件。這個方法一直阻塞到取到事件或超時。一旦隊列中有了事件,這個方法就返回事件號和兩個事件參數。在調用GetEvent後,應用程序應該總是調用IMediaEvent::FreeEventParams方法來釋放與事件參數相關的所有資源。比如,一個參數可能是由filter graph分配的BSTR值。     下面的代碼是一個如何從隊列中取事件的框架:

  long evCode, param1, param2;   HRESULT hr;   while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr))   {       switch(evCode)        {            // Call application-defined functions for each            // type of event that you want to handle.       }        hr = pEvent->FreeEventParams(evCode, param1, param2);   }

  要重置filter graph manager默認的事件處理過程,調用IMediaEvent::CancelDefaultHandling方法,用事件號做參數。你可以通過調用IMediaEvent::RestoreDefaultHandling方法來恢復某個事件的處理過程。如果filter graph對某個事件號沒有默認處理過程,則調用上面兩個方法不產生任何影響。

3.5.3 當事件發生時     要處理DirectShow事件,應用程序需要一個方法來知道事件何時正等待在隊列中。Filter Graph Manager提供兩種方法:     *窗口通告:一旦有事件發生,Filter Graph Manager就發送一個用戶自定義窗口消息來通知應用程序窗口     *事件信號:如果有DirectShow事件在隊列中,filter graph manager就觸發一個windows事件,如果隊列爲空,則reset這個事件。     應用程序可以使用任何一種方法,但通常窗口通告方法相對比較簡單。          窗口通告:     要設置窗口通告,調用IMediaEventEx::SetNotifyWindow方法並指定一個私有消息,私有消息可以是從WM_APP到0xBFFF的任一個。一旦filter graph manager把一個新的事件通告放入隊列中,它便發送這個消息給指定的窗口。應用程序從窗口的消息循環中來響應這個消息。     下面是如何設置通知窗口的例子:

   #define WM_GRAPHNOTIFY WM_APP + 1   // Private message.   pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0); 

  消息是一個普通的windows消息,並且獨立於DirectShow消息通告隊列被髮送。使用這種方法的好處是大部分應用程序擁有一個消息循環,因此,要知道DirectShow事件何時發生便無需做額外的工作了。   下面是一段如何響應通告消息的框架代碼:

   LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)   {       switch (msg)       {           case WM_GRAPHNOTIFY:               HandleEvent();  // Application-defined function.               break;           // Handle other Windows messages here too.       }       return (DefWindowProc(hwnd, msg, wParam, lParam));   }

      因爲事件通告與消息循環均爲異步進行的,因此在應用程序響應事件時隊列中可以會有多個事件。而當事件變爲非法時,它們會從隊列中被清除掉。所以在你的事件處理代碼中,調用GetEvent直至返回一個表示隊列已空的失敗代號。     在釋放IMediaEventEx指針前,請以NULL作參數調用SetNotifyWindow方法來取消事件通告。並且在你的事件處理代碼中,在調用GetEvent前檢查IMediaEventEx指針是否合法。這些步驟可以防止在釋放IMediaEventEx指針後應用程序繼續接收事件通告的錯誤。          事件信號:     Filter Graph Manager建立一個反映事件隊列狀態的手工重設事件(manual-reset event)如果隊列中包含有未處理的事件通告,Filter Graph Manager就會發信號給手工重設事件。如果隊列是空的,則調用IMediaEvent::GetEvent方法會重設(reset)事件。應用程序可以通過這個事件來確定隊列的狀態。          注意:此處的術語可能被混淆。手工重設事件是由windows的CreateEvent函數創建的一種事件類型,它與由DirectShow定義的事件無關。          調用IMediaEvent::GetEventHandle方法得到手工重設事件的句柄,調用一個函數如WaitForMultipleObjects來等待發送給手工重設事件的信號。一旦收到信號,就可以調用IMediaEvent::GetEvent來接收DirectShow事件了。     下面的代碼舉例說明了這種方法。在取得事件句柄後,在100毫秒時間間隔內等待發送給手工重設事件的信號,如果有信號發來,它調用GetEvent然後在windows控制檯上打印出事件號和事件參數,循環在EC_COMPLETE事件發生後結束,這標誌着回放結束。

  HANDLE  hEvent;    long    evCode, param1, param2;   BOOLEAN bDone = FALSE;   HRESULT hr = S_OK;   hr = pEvent->GetEventHandle((OAEVENT*)&hEvent);   if (FAILED(hr)   {       /* Insert failure-handling code here. */   }   while(!bDone)    {       if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100))       {            while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr))            {               printf("Event code: %#04x/n Params: %d, %d/n", evCode, param1, param2);               pEvent->FreeEventParams(evCode, param1, param2);               bDone = (EC_COMPLETE == evCode);           }       }   } 

         因爲Filter Graph會在適當的時候自動重設事件,因此你的應用程序應當不去作重設工作。同時,當你釋放filter graph時,filter graph會關閉事件句柄,因此在這之後你就不能再使用事件句柄了。

 

3.6. DirectShow中的時間和時鐘     這一節主要概述DirectShow體系中時間和時鐘。 3.6.1. 參考時鐘    Filter Graph Manager的一個功能,能夠以同一個時鐘來同步所有在graph中的filter,稱作參考時鐘(reference clock)     任何暴露了IReferenceClock接口的對象都能夠作爲一個參考時鐘來使用。參考時鐘可以由一個DirectShow filter來提供,例如可以直接使用硬件時鐘的audio renderer。另外,Filter Graph Manager也能使用系統時間來作參考時鐘。     名義上,一個參考時鐘以千萬分之一秒的精度來度量時間,但是實際上的精度不會這麼高。要取得參考時鐘的當前時間,調用IReferenceClock::GetTime方法。由於時鐘的基準時間,即時鐘開始時的時間計數,是依賴於具體的實現的,因此GetTime的返回值不反映絕對時間,只反映相對於graph開始時的相對時間。     雖然參考時鐘的精度是變化的,但是GetTime的返回值卻保證是單調遞增的,換句話說,也就是參考時鐘的時間是不會回退的。如果參考時鐘的時間是由硬件源產生的,而硬件時鐘回退了(比如,有一個調節器調節了時鐘),GetTime依然返回最晚的那個時間只到硬件時鐘追上它。要知道更多的內容可以參考CBaseReferenceClock類(http://msdn.microsoft.com/library/en-us/directshow/htm/cbasereferenceclockclass.asp)。          默認參考時鐘     當Graph運行時,Filter Graph Manager會自動選擇參考時鐘,選擇參考時鐘的規則如下:     *如果應用程序指定了一個時鐘,則使用這個時鐘;     *如果Graph包含了一個支持IReferenceClock的活動源filter(即推模式源filter),則使用這個filter;     *如果Graph未包含任何支持IReferenceClock的推模式源filter,使用任何一個支持IReferenceClock接口的filter,選擇的次序是從Renderer filter開始依次向上。已連接的filter優先於未連接的filter被選。(如果這個graph會render一個音頻流,則這個規則通常就會選擇audio renderer filter來作爲參考時鐘)     *如果沒有filter支持合適的時鐘,則使用系統參考時鐘。          設置參考時鐘     應用程序可以在Filter Graph Manager上調用IMediaFilter::SetSyncSource方法來選擇時鐘,只有在由於你有一個特殊原因想要選擇自己的時鐘時才需要這麼做。     想要讓Filter Graph Manager不使用任何參考時鐘,可以調用SetSyncSource,參數爲NULL。比如,你需要儘可能快地來處理sample時,就可以這麼做。要恢復黑認的參考時鐘,在Filter Graph Manager上調用IFilterGraph::SetDefaultSyncSource方法。     當參考時鐘發生變化時,Filter Graph Manager會通知每一個filter調用它的IMediaFilter::SetSyncSource方法,應用程序無需調用filter的這個方法。      3.6.2. 時鐘時間    DirectShow定義了兩種相關時間:參考時間(reference time)流時間(stream time)     *參考時間是一個絕對時間,由參考時鐘返回     *流時間是一個相對於graph最後開始時的相對時間      ·當graph處於運行態時,流時間等於參考時間減去起始時間      ·當graph處於暫停態時,流時間停留在暫停的那一刻      ·在重新定位後,流時間被重設爲0      ·當graph處於停止態時,流時間無意義     如果一個媒體樣本有一個時間戳t,表示這個在流時間爲t時被render,正因爲這個原因,因此流時間也被叫做呈現時間(presentation time)。     當應用程序調用IMediaControl::Run運行graph時,Filter Graph Manager調用每個filter的IMediaFilter::Run。爲了補償消耗在運行每個filter的時間總和,Filter Graph Manager會略微晚一點來定義起始時間。      3.6.3. 時間戳     時間戳定義了媒體樣本的起始和結束時間。時間戳有時被稱作呈現時間(presentation time)。在閱讀餘下的文章時,一個必須記住的要點是並非所有的媒體格式都以相同的方式來使用時間戳。舉個例子,並不是所有MPEG樣本都被打上了時間戳,在MPEG Filter Graph中,時間戳在被解碼前並非應用在每個幀上。     當一個renderer filter接收到一個樣本時,它以時間戳爲基準來確定render時間。如果樣本來晚了,或者這個樣本沒有時間戳,那個filter就立刻render它,否則,filter就等在那直到合適的時機。(通過IReferenceClock::AdviseTime方法來等待樣本的render時間)     源filter和語法解析filte使用下列原則,在它們處理的樣本上設置合適的時間戳:     *文件回放:第一個樣本被打上起始時間戳,爲0,後面的時間戳由樣本長度和回放速率來決定,這些都由文件格式來決定。分析文件的filter負責計算出合適的時間戳。例子見(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directshow/htm/avisplitterfilter.asp)     *音視頻採集:每個樣本都被打上一個起始時間戳,這個時間戳與當它被捕獲時的Stream time相同。應注意以下幾點:      ·從預覽pin(Preview Pin)出來的樣本沒有時間戳。因爲圖像處理的延時,一個打上採集時間的視頻幀總是會遲一點到達視頻renderer。這會導致在進行質量控制時,renderer會丟棄部分視頻幀。關於質量控制,參見(http://msdn.microsoft.com/library/en-us/directshow/htm/qualitycontrolmanagement.asp)      ·音頻採集:音頻採集filter使用它自己的緩衝集,而並非使用音頻驅動程序的。音頻驅動以固定的時間間隔來填充採集filter的緩衝。這個時間間隔由驅動決定,通常不超過10毫秒。在音頻樣本上的時間戳反映的是驅動填充採集filter時的時間,因此會有偏差,尤其是當應用程序使用一個很小的緩衝區時。不過,媒體時間可以精確地反映緩衝區中音頻樣本的數量。     *混合filter(Mux filter):依賴於輸出格式,一個mux filter可能需要由它來產生時間戳,也可能不需要。舉個例子,AVI文件格式使用固定的幀率而沒有時間戳,因此AVI Mux filter假設那些樣本在近似正確的時間內到達。如果樣本沒有在合適的時間間隔內到達,AVI Mux filter會插入一個長度爲0的空樣本,來表示一個丟失的幀。在文件回放時,新的時間戳在運行時如前面所述地那樣產生。     要在一個樣本上設置一個時間戳,調用IMediaSample::SetTime方法。     此外,filter還可以爲樣本指定一個媒體時間(media time)。在視頻流中,media time表示視頻幀的數量。在音頻流中,media time表示包中的樣本數量,比如,如果每個包包含以44.1KHz的採樣率採集的一秒鐘的音頻,那麼第一個包具有一個爲0的媒體起始時間以及爲44100的媒體終止時間。在一個可以定位的流中,媒體時間總是相對於流的起始時間,比如,假設你在一個15幀/秒的視頻流上定位到2秒這個位置,那麼定位後的每一個媒體樣本的時間媒爲0,但是它的媒體時間爲30.     Renderer和Mux filter能使用媒體時間通過檢查是否有缺口來確定幀或樣本是否被丟棄了。但是,filter不是一定要設定媒體時間。要設置媒體時間,調用IMediaSample::SetMediaTime方法。     3.6.4 實時源(Live Source)     實時源,也被叫做推模式源(push source),實時地接收數據,比如視頻採集和網絡廣播。通常情況下,一個實時源不能控制數據到達的速率。     一個filter被認爲是實時源需要具有以下幾點:     * 調用IAMFilterMiscFlags::GetMiscFlags方法時返回AM_FILTER_MISC_FLAGS_IS_SOURCE標記,並且至少有一個輸出pin暴露IAMPushSource接口。     * filter暴露IKsPropertySet接口,並具有一個capture pin(PIN_CATEGORY_CAPTURE)。          延時(latency)     一個filter的延時是這個filter處理一個樣本所需的時間總和。在實時源中,延時取決於保存樣本的緩衝區大小。舉個例子,假設graph有一個具有33ms延時的視頻源和一個具有500ms延時的音頻源,那麼每個到達視頻renderer的視頻幀要比與之匹配的音頻樣本到達音頻renderer早470ms,除非graph對這個差別進行補償,否則音視頻將會不同步。     實時源可以通過IAMPushSource接口來進行同步。Filter Graph Manager並做同步工作除非應用程序通過調用IAMGraphStreams::SyncUsingStreamOffset方法來激活它。如果同步被激活,Filter Graph Manager通過IAMPushSource來查詢每一個source filter,如果filter支持IAMPushSource,那麼Filter Graph Manager調用IAMLatency::GetLatency來得到filter預期的延時(IAMPushSource繼承自IAMLatency)。通過組合的延時值,filter graph manager決定graph中最大的預期延時,然後調用IAMPushSource::SetStreamOffset來給每一個source filter一個流偏移,以後filter會在產生時間戳時加上這個偏移。     這個方法主要是爲了實現實時預覽,但是,注意實時採集設備(比如攝像頭)的preview pin上是沒有時間戳的,因此,要在一個實時採集設備上使用這種方法,你必須在capture pin上進行視頻預覽。     通常,IAMPushSource接口被VFW Capture filter和音頻採集filter(Audio capture filter)支持。          速率匹配(Rate Matching)     如果renderer filter和source filter使用不同的參考時鐘,那麼就會有問題,renderer可能比source要快,這就導致了數據的缺口,或則renderer比source慢,就會導致數據擁堵而樣本丟棄。通常一個實時源無法控制速率,因此要求renderer來與source進行速率匹配。     通常,只有audio renderer實現速率匹配,因爲聲音回放的頻率比視頻更重要。要實現速率匹配,audio renderer必須排除以下幾點:     *如果graph沒有使用一個參考時鐘,那麼audio renderer不會去進行速率匹配(如果graph沒有參考時鐘,那麼樣本總是在到達時就被立刻render)。     *另外,如果graph中有一個參考時鐘,audio renderer檢測是否有一個實時源在上游,如果沒有,audio renderer不進行速率匹配。     *如果有一個實時源在上游,並且這個實時源在它的輸出Pin上暴露IAMPushSource接口,audio renderer調用IAMPushSource::GetPushSourceFlags,並尋找以下標記:      ·AM_PUSHSOURCECAPS_INTERNAL_RM,這個標記表示這個實時源擁有自己的速率匹配機制,因此audio renderer不進行速率匹配。      ·AM_PUSHSOURCECAPS_NOT_LIVE,這個標記表示source filter並不是一個真正的實時源,即使它暴露了IAMPushSource接口,因此,audio renderer不進行速率匹配。      ·AM_PUSHSOURCECAPS_PRIVATE_CLOCK,這個標記表示source filter使用一個私有的時鐘來產生時間戳。在這種情況下,audio renderer速率匹配與時間戳會有衝突。(如果樣本沒有時間戳,那麼renderer忽略這個標記。     *如果GetPushSourceFlags返回沒有標記(0),audio renderer的行爲依賴於graph時鐘和樣本是否擁有時間戳:      ·如果audio renderer不是graph參考時鐘,並且樣本擁有時間戳,那麼audio renderer速率匹配與時間戳會有衝突      ·如果樣本沒有時間戳,audio renderer嘗試與輸入的音頻數據的速率進行匹配。      ·如果audio renderer是graph參考時鐘,它與輸入的數據速率進行匹配。     最後一種情況的原因如下:如果audio renderer是參考時鐘,並且source filter使用同樣的時鐘來產生時間戳,那麼audio renderer不會與這個時間戳進行速率匹配,因爲如果它這樣做了,導致的結果是,它等於在嘗試與自己進行速率匹配,這將導致時鐘偏差。因此,在這種情況下,renderer與輸入的音頻數據速率進行匹配。 

 

3.7. Graph動態重建(Dynamic Graph Building)     如果你需要修改一個已經存在的filter graph,你可以停止,修改後再重新啓動它。這通常是一種最佳的解決方法。但是,在某此情況下,你可能需要在一個graph處於運行狀態時來修改它,比如:     *應用程序在進行視頻回放時需要插入一個(視頻濾鏡filter)Video effect filter;     *source filter在播放的過程中改變了媒體格式,此時可能需要接入新的解碼filter;     *應用程序在graph中加入一個新的視頻流。     上面的這些都是graph動態重建的例子。所有在graph繼續處於運行狀態而做的graph修改都被叫做graph動態重建。動態重建可以由應用程序發起,也可以由一個在graph中的filter發起。動態重建有三種可能:     *媒體格式動態變化:一個filter可以在運行的中途改變媒體格式,而不需要重新被替換爲另一個;     *動態重連:在graph中添加或刪除filter     *Filter Chain操作:添加,刪除,控制filter chain,(Filter Chain是相互連接着的一條Filter鏈路,並且鏈路中的每個Filter至多有一個Input pin,至多有一個Output pin)     3.7.1. 動態重連     在絕大多數的directshow filter中,當graph處於運行狀態時pin是不能被重新連接的,應用程序必須在重連前停止graph。但是,某些filter卻支持動態重連,這既可以由應用程序來執行,也可以由graph中的一個filter來執行。     如下圖:      

Dynamic graph-building diagram

      假設我們要將filter 2從graph中移除掉,替換成另一個filter,而此時graph還處於運行狀態,那麼必須具備以下幾個條件:     *filter 3的輸入pin(pin D)必須支持IPinConnection接口,這個接口可以重新連接pin而不需要停止它。     *filter 1的輸出pin(pin A)必須能夠在重連時阻塞媒體數據,數據不再在pin A和pin D之間傳遞。也就是說,輸出Pin必須支持IPinFlowControl接口。但是,如果filter 1是發起重連的那個filter,那麼它有可能已經在其內部實現了阻塞;     動態重連包括下列步驟:     1. 從Pin A那裏阻塞數據流     2. 重新連接Pin A和Pin D,或者在中間加入新的filter     3. 取消Pin A上的阻塞

    步驟1. 阻塞數據流     通過調用Pin A上的IPinFlowControl::Block方法來阻塞數據流。這個方法既可以被同步調用,也可以被異步調用。要異步調用這個方法,需要創建一個win32事件對象,並將事件句柄傳給Block,方法會立即返回,然後使用WaitForSingleObject或其它函數來等待事件的觸發。當阻塞工作完成時,pin會觸發這個事件。如:

// Create an event HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEvent != NULL) {     // Block the data flow.     hr = pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK, hEvent);      if (SUCCEEDED(hr))     {         // Wait for the pin to finish.         DWORD dwRes = WaitForSingleObject(hEvent, dwMilliseconds);     } }

    如果是同步調用Block,那麼只需將傳入的hEvent參數設爲NULL,此時這個方法會一直阻塞到阻塞工作完成爲止。如果pin還沒有準備好deliver一個新的sample,那麼就會一直阻塞。而如果filter處於就緒狀態,這可能會花費任意長的時間,因此,不要在應用程序的主線程中使用同步調用,以免發生死鎖,開一個工作線程來使用同步調用,或者乾脆就使用異步調用。

    步驟2. 重連pin     要重新連接pin,查詢graph的IGraphConfig接口並調用IGraphConfig::ReconnectIGraphConfig::Reconfigure。Reconnect方法使用比較簡單:     *停止中間filter(比如filter 2),並移除它     *如果需要的話,加入新的中間filter     *連接所有的pin     *pause或run所有新的filter,使它的狀態與graph相同     Reconnect方法有參數可以用來指定pin連接的媒體類型和中間filter。如:

pGraph->AddFilter(pNewFilter, L"New Filter for the Graph"); pConfig->Reconnect(     pPinA,      // Reconnect this output pin...     pPinD,      // ... to this input pin.     pMediaType, // Use this media type.     pNewFilter, // Connect them through this filter.     NULL,      0); 

    如果Reconnect還不夠用來應付我們的要求,那麼你可以使用Reconfigure方法,它調用一個由應用程序定義的回調函數來重連這些pin。要調用這個方法,需要在你的應用程序中實現IGraphConfigCallback接口。     在調用Reconfigure之前,如前面所述地那樣阻塞輸出pin的數據流。然後如下所示,將處於待處理狀態的數據push下去:     1. 在重連鏈路中處於下游的最遠的那個輸入pin(例子中爲Pin D)上調用IPinConnection::NotifyEndOfStream方法,方法的參數是一個Win32事件句柄;     2. 在與要阻塞數據的那個輸出pin直接相連的那個輸入pin上調用IPin::EndOfStream方法。(在例子中,要阻塞的那個輸出pin是pin A,那麼直接與之相連的那個輸入pin爲Pin B);     3. 等待事件觸發。輸入pin(pin D)在它接收到end-of-stream事件通告時觸發事件。這表示再沒有數據需要傳輸,此時就可以安全地進行重連了。     注意:IGraphConfig::Reconnect方法會自動處理上述步驟,你僅在調用Reconfigure方法時才需要自己來處理。     當數據完成push後,調用Reconfigure,傳入IGraphConfigCallback回調函數的指針。Filter Graph Manager會調用IGraphConfigCallback::Reconfigure方法。

    步驟3. 取消數據流的阻塞     當你完成重連後,通過調用IPinFlowControl::Block,第一個參數爲0來取消阻塞。

    注意:如果動態重連是由一個filter來執行的,那麼你需要知道一點線程方面的問題。如果filter graph manager嘗試去停止filter,它可能會死鎖,因爲graph等待filter停止,而與此同時,filter有可能在等待數據在graph中完成push。要防止這個可能存在的死鎖問題,如前所述可以用事件機制來處理。

3.7.2. filter鏈(filter chains)     一個filter chain是一系列具備下述條件的相互連接的filter:     *每一個在鏈中的filter最多隻有一個已連接的輸入pin和一個已連接的輸出pin;     *Filter鏈路中的數據流不依賴於鏈路外的其他Filter     舉個例子,在下圖中,filter A-B,C-D和F-G-H是一個filter chains。每個F-G-H中的子鏈(F-G和G-H)也是一個filter chain。一個filter chain同樣可以是由單個filter組成的,因此A、B、C、D、F、G和H同樣也是filter chain。filter E由於有兩個輸入連接,所以任何含有E的一系列filter都不是filter chain。  

Filter chain (Example 1)

      IFilterChain接口提供下述方法來控制filter chain:

    IFilterChain::StartChain  開啓一個鏈     IFilterChain::StopChain   停止一個鏈     IFilterChain::PauseChain  暫停一個鏈     IFilterChain::RemoveChain  從graph中移除一個鏈

    沒有特殊的方法來添加一個鏈,要添加鏈,通過調用IFilterGraph::AddFilter方法來插入新的filter,然後調用IGraphBuilder::ConnectIGraphBuilder::Render或類似的方法來連接它們。     當graph運行時,一個filter chain可以在運行和停止狀態間切換。當graph處理就緒狀態時,它可以在就緒和停止狀態間切換。這是兩種僅有的filter chain狀態切換可能。     Filter鏈指南     當你使用IFilterChain方法時,確認在graph中的filter是否能支持filter鏈操作是十分必要的,否則,可能會發生死鎖或graph錯誤。filter連接到鏈上必須發生在鏈狀態改變後。     使用IFilterChain的最佳情況是與一系統爲鏈而設計的filter一起使用。使用下面的指南來確保你的filter是鏈操作安全的。參考下圖:    

Filter chain (Example 2)

        在filter鏈狀態變化前,所在在filter鏈分界線上調用的數據處理都必須已完成。這個規則應用於IMemInputPin::ReceiveIPin::NewSeqmentIPin::EndOfStream方法。鏈中的filter必須從由鏈外filter實現的這些方法調用中返回;而鏈外的filter也必須從這些由鏈內filter實現的這些方法調用中返回。     舉個例子,在上圖中,filter B必須完成在filter A上的所有數據處理調用,而filter E也必須完成從filter D上的調用。如果pin暴露了IPinFlowControlIPinConnection接口,那麼如在動態重連那一節中所講的,你可以通過調用IPinFlowControl::BlockIGraphConfig::PushThroughData方法來推數據。filter也可能通過自己的方法來推數據。     上游filter必須與鏈的狀態一起發生變化。比如,在上圖中,假如鏈已停止,但filter A調用IMemInputPin::Receive方法,那麼調用將失敗,作爲迴應,filter A停止流。當應用程序重新開啓鏈時,不會產生什麼影響,因爲filter A不再向使數據流動了。     下游filter必須同樣與鏈的狀態一起發生變化,否則,下游filter在等待取得sample時會發生死鎖,因爲sample不會再到來了。比如,多路複用(MUX)filter總是在它所有的input pin上需要數據,如果掛起其中的一個input pin,在其它input pin上的流處理也會被阻塞。這會導致graph死鎖     每個與鏈內部filter相連的外部filter的pin必須擁有自己的分配器(allocator),它不能被其它pin連接共享。當鏈的狀態發生變化或從graph移除掉時,分配器便不可用了,此時如果還有其它的連接使用這個分配器的話,它們將不能再處理sample了。     除非與鏈相連的filter支持動態斷開,否則不要移除鏈。典型的,已連接的filter會支持IPinConnection或IPinFlowControl接口,或者用它自己定義的接口代替。

3.8. 插件發佈者(Plug-in Distributors)

    Plug-in Distributors(PIDs)是擴展filter graph manager的一種方法。一個PID是filter graph manager在運行時聚合的一個COM對象。應用程序通過filter graph manager來進入PID。     當filter graph manager被要求查詢一個它不支持的接口時,它會搜索註冊表項:         HKEY_CLASSES_ROOT/Interface/IID/Distributor    IID是接口的GUID,如果註冊項存在,那麼鍵值便是支持該接口的PID類標識(CLSID)。filter graph manager聚合了PID並返回接口指針,應用程序調用這個指針時實際上就是在調用PID,但是這對於應用程序來說是透明的,對於應用程序來說,它就象是在filter graph manager上調用這個接口一樣。    PID爲應用程序提供了一種簡單的控制filter的方法,如通過調用IFilterGraph::EnumFilters方法,PID可以枚舉graph中的所有filter並調用這些filter上的方法。    當filter graph manager聚合了一個PID時,它查詢PID的IDistributorNotify接口,如果PID支持這個接口,filter graph manager用它來通知PID有關graph的狀態變化:  * 當filter graph在run、pause和stop狀態之間切換時,它調用IDistributorNotify::RunIDistributorNotify::PauseIDistributorNotify::Stop。  * 如果調置了參考時鐘,filter graph manager調用IDistributorNotify::SetSyncSource。  * 當有filter添加或移除,或pin連接有變化時,filter graph manager調用IDistributorNotify::NotifyGraphChange。      當自己定製PID時,自己所創建的COM對象必須支持聚合,並且它所支持的接口是filter graph manager本身所沒有的。IDistributorNotify接口是可選的。    如果PID從filter graph manager上獲得一個接口,那它必須立即release這個接口,否則會在COM對象上出現循環引用的參考計數,使得filter graph manager無法被銷燬。在filter graph manager上保持一個參考計數是多餘的,因爲PID的生命期是由filter graph manager控制的。    因爲PID是明確指定是被filter graph manager聚合使用的,因此你應該在PID的構造函數中強行檢查IUnknown指針是否爲NULL,如果爲NULL,則返回錯誤碼VFW_E_NEED_OWNER。同時,爲了防止其它對象聚合PID,你可以在IUnknown上查詢IGraphBuilder接口,如果不行則返回錯誤。

發佈了0 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章