015 如何開發視頻播放Filter

Post date: 2015/4/9 上午 01:29:27

摘要:這篇文章討論了一個播放Filter需要處理的一些消息通知。 只要正確的處理這些消息通知, 才能夠正確地設置Directshow播放視頻的畫面。 1 開發一個可選擇的視頻播放filter Directshow提供了一個基於窗口的視頻播放Filter, 它也提供了一個全屏幕實時播放的filter。 你可以利用Directshow的基類開發自己的可選擇的視頻播放filter。 你可以利用CBaseRenderer and CBaseVideoRenderer類根據下面的一些經驗指南就可以開發一個可選擇的視頻播放filter。 在Directshow中主要有三種消息通知。 1數據流的通知, 這是數據流在graph圖中傳遞的過程中, 從一個filter到另一個filter的過程中發生的事件通知。 例如, begin-flushing, end-flushing or end-of-stream事件通知, 這些事件通知都是通過上遊fitlter調用的下遊filter的輸入pin來通知下遊filter的。 比如IPin::BeginFlush。 2 filter圖表管理器發送的消息通知, 這些都是filter給圖表管理器發送的事件通知, 比如EC_COMPLETE, 這種消息一般都是通過圖表管理器的IMediaEventSink::Notify發送或接收的。 3 應用程序發送的消息通知 應用程序通過調用圖表管理器上的IMediaEvent::GetEvent方法就可以獲得這些事件消息。 一般來說, 圖表管理經常講得到的消息傳遞給應用程序處理。 2對End-of-stream and Flushing消息的處理 當源filter發現沒有數據傳送的時候, 它就會向下遊發送一個end-of-stream通知, 這個通知會沿著Graph圖表中的filter一個一個的往下傳遞, 最後到達Render Filter。 這就導致Graph圖表產生一個EC_COMPLETE消息。 當Renderer的輸入pin上的IPin::EndOfStream被上遊的filter調用的時候, Render Filter就會接收到一個end-of-stream消息通知。 Render Filter應該標記下這個消息通知, 然後將已經接收到的數據處理完畢。 當所有剩餘的數據接收完畢, Render Filter就會給Graph圖表管理器發送一個EC_COMPLETE消息。 當Render Filter在處理完畢所有的數據時, 你應該給graph圖表管理器發送一次EC_COMPLETE消息。 只有當Render Filter處於運行狀態時才能給圖表管理器發送EC_COMPLETE消息。 如果一個Render 正處於paused狀態時接收到源filter 發送的end-of-stream通知, 只有當Filter Graph結束的時候Render 才能給圖表管理器發送EC_COMPLETE消息。 end-of-stream通知發出以後, 如果上遊filter再次調用Render Filter上的輸入pin上的IMemInputPin::Receive or IMemInputPin::ReceiveMultiple方法時, Render就要拒絕它, 此時就返回一個E_UNEXPECTED錯誤消息。 當Filter 圖表管理器停止的時候, Render應該將捕捉到的任何end-of-stream通知都應該被清除, 當圖表管理器再次啟動的時候, Render不應該再給管理器發送任何通知。 因為圖表管理器在啟動以前會Paused所有的Filter, 這就會導致發生flushing。 例如, 如果Filter graph處於Pause狀態收到一個end-of-stream通知, 然後Filter Graph就停止了, 當filter Graph再次運行的時候, Render 就不應該給管理器發送EC_COMPLETE消息了。 如果沒有發生seek, 源filter會自動地在發生pause時給下遊的filter發送一個end-of-stream通知, 如果在filter Graph圖表stop的時候發生seek, 此時源filter也許正有數據要發送, 所以它不會發送end-of-stream通知。 Render filter經常依靠end-of-stream通知來發送EC_COMPLETE通知。 例如, 如果一個數據流已經結束發送(也就是end-of-stream消息已經發送出來), 另一個窗口已經覆蓋到視頻窗口上, 也產生了一系列的WM_PAINT消息。 但是, 當end-of-stream消息發出以後, Render就處於等待狀態, 但是Render也明白它不會再接收任何數據了, 視頻播放窗口就會出現黑屏幕。 Flushing是Render應該處理的另外一種複雜的事件。 Flushing消息是通過IPin上的兩個方法BeginFlush and EndFlush.觸發的。 源filter在沒有調用EndFlush,而僅僅調用了BeginFlush方法是不合法的, 所以此時Flushing的狀態是短暫和不連續的。 但是在flushing狀態下, Render要處理好數據以及接收的消息。 在BeginFlush方法被調用之後所有接收到的數據都要立即被rejected, 並且返回一個S_FASLE。 並且捕捉到的end-of-stream也要立即清除。 當Render接收到seek消息後, 就立即處於flush狀態。 Flush確保在重新發送數據之前從filter Graph中清除所有的舊的數據。 3如何處理狀態的改變Handling State Changes and Pause Completion 當一個Renderer filter的狀態改變的時候, 它的行為和其他的filter是一樣的, 但是也有以下的區別, 當filter的狀態變為pause的時候, Render filter 的數據就排成隊列, 等待下次播放, 當一個video 播放filter 停止的時候, 它也會保留這些隊列中的數據, 這就是一個例外, 因為dshow規定, 當一個graph停止的時候, 它不應該保留任何資源。 造成這種例外的原因是如果render filter保持資源, 這樣, 當這個filter接收到一個WM_PAINT 消息時可以通過這個資源來重繪窗口。 同樣保持這個資源也可以滿足一些方法的調用, 比如CBaseControlVideo::GetStaticImage,, 這個方法用來返回當前圖像的一份拷貝。 保持資源的另一個原因是, 在資源保持的過程中, 它所佔用的內存塊不會被回收, 這樣, 再次開始數據傳輸時速度會比較快一點, 因為不用分配內存了。 在graph運行的期間, sample中的內容會被隨時地提交sample內存也隨時地被釋放, 但是, 在停止運行的狀態中, sample只能被提交, 不能被釋放, 例如, 在窗口繪製一幅靜態的圖畫。 音頻流在停止的狀態沒法被提交, 但是他們可以進行其他的動作, 比如準備wave設備。 Sample被提交的時間由sample的stream time和IMediaControl::Run方法調用時傳遞的參考時間綜合以後得到的。 當開始時間小於等於結束時間時, sample就應該被丟棄。 當應用程序調用IMediaControl::Pause方法準備停止一個graph圖時, 只有當提交過濾器中有一個數據隊列時才能夠返回。 為了確保此點, 當一個render filter 沒有等待提交的數據時, 該方法就返回S_FALSE,如果有數據等待提交, 返回S_OK。 未來確保Render filter 有一個等待提交的數據, Filter圖表管理器在停止一個graph時會檢查方法的返回值, 如果一個或者幾個filter 還沒有準備好, filter 圖表管理器就會調用GetState方法來polls filter。 GetState方法帶有一個超時的參數, 當GetState函數的等待的時間到期返回之前, 如果filter還在等待數據的到來時, 那麼該函數返回VFW_S_STATE_INTERMEDIATE, 如果filter已經有等到數據的時候, GetState返回s_ok。 當一個filter在等待數據的時候, 源filter會發送一個end of stream的通知, 此時狀態的轉變完成。 當一個graph中的所有的filter都有了等待提交的數據, 那麼整個graph就成為了pause狀態。 4如何處理終止態(Handling Termination) 視頻提交過濾器必須能夠正確處理來自用戶的終止數據流的事件。 這就意味著要正確地隱藏窗口, 並且知道當窗口重新顯示的時候該怎麼做。 同時, 當窗口銷毀的時候, 提交過濾器也要能夠通知Filter 圖表管理器來正確的釋放資源。 當用戶關閉了視頻窗口時, 或者(用戶按ALT+F4), 一般的做法是將視頻窗口隱藏同時給filter 圖表管理器發送一個EC_USERABORT通知, 這個通知最終會被發送到應用程序, 然後應用程序就會停止播放視頻。 當發出EC_USERABORT通知後, 所有發送給提交filter的數據都會被拒絕。 當一個視頻正在在播放的時候, 如果用戶此時按下ATL +F4, 視頻窗口就會暫時的隱藏起來, 然後所有送往窗口的數據都會被拒絕。 當窗口重新顯示的時候, 不會產生EC_REPAINT通知。 當一個視頻提交filter終止的時候, 它要給filter圖表管理器發送一個EC_WINDOW_DESTROYED消息通知。 事實上, 最好的處理這個消息的時機是在IBaseFilter::JoinFilterGraph方法調用時, 而不是等到實際的窗口銷毀時。 Sending this notification enables the plug-in distributor in the Filter Graph Manager to pass on resources that depend on window focus to other filters (such as audio devices). 5如何處理數據格式的動態改變 視頻提交filter一般只接受那些容易處理的數據格式, 例如, 一般只接受RGB格式的數據, 因為這種數據格式和顯示器格式相匹配。 通常的話, 上遊的filter都是調用下遊filter上的輸入pin上的IPin::QueryAccept方法來查詢, 下遊的filter是否接受新的數據格式, 從而來動態的改變數據格式。 一個render filter應該支持動態的修改數據格式, 至少它應該允許上遊的filter能夠改變調色板。 當上遊filter改變媒體類型, 它會在採用新格式的第一個smpale上貼上新的媒體類型。 如果一個render filter還有一些老格式的數據沒有提交完, 它會等到這些老格式的數據提交完畢才改變媒體類型。 解碼器也會動態的改變數據格式, 這樣就要求render filter能夠相應的跟著改變, 例如, 如果我們想要解碼器提供一種和DirectDraw兼容的數據格式, 當render paused的時候, 它就開始通過QueryAccept向上遊的filter 詢問, 解碼器都支持什麼數據格式, 解碼器一般不會將它支持的所有數據格式都列舉出來, 因此, render filter 就要提供一些解碼器接口沒有說明的數據格式。 如果解碼器可以接收要求的數據格式, 它就會通過QueryAccept方法返回一個s_ok, 於是Render filter就將新的媒體類型 attach to 上遊內存分配器分配的下一個sample上。 因此, render filter 就要提供一個內存分配器, 這個提供一個私有的方法來將媒體類型貼到新的下一個samples上, 在這個私有的方法中, 調用IMediaSample::SetMediaType來設置媒體類型。 Render filter 的輸入pin應該在IMemInputPin::GetAllocator方法中返回render filter 的內存分配器, 重載一下IMemInputPin::NotifyAllocator方法, 如果上遊的filter不使用render filter 提供的內存分配器, 那麼這個方法就會返回false。 在一些解碼器中, 如果將biHeight設置為一個YUV類型的正數, 那麼就解碼器就會產生上下顛倒的畫面, 這是不正確的, 應該視為解碼器的一個bug。 當render filter 發現graph中的數據格式發生改變的時候, 它都會發送一個EC_DISPLAY_CHANGED消息通知的。 大多數的render filter 在連接的時候都會選擇一個GDI支持的數據格式, 如果用戶改變了當前的顯示模式而沒有重新啟動機器, 那麼render filter 發現自己正使用一種很糟糕的數據格式進行連接, 那麼它就會發送上面的消息通知。 第一個參數就是需要重新連接的pin, 管理器就會讓graph圖停止運行, 重新連接pin。 在隨後的重新連接過程中, render filter 就會選擇接受合適的數據格式。 當render fitler發現調色板發生改變的時候, 它要發送EC_PALETTE_CHANGED的消息通知給filter 圖表管理器。 最後, 視頻render filter 發現視頻的尺寸發生改變, 它會給圖表管理器發送一個EC_VIDEO_SIZE_CHANGED消息通知。 6如何處理永久性屬性(Persistent Properties) 所有通過IBasicVideo and IVideoWindow接口設置的屬性都意味著在整個連接過程中是永久不不變的。 因此, 斷開連接, 重新連接都對窗口的大小, 位置, 樣式等屬性沒有影響。 但是, 如果視頻的尺寸大小發生改變的時候, render filter應該重新設置源或者目的的矩形大小。 源或者目的的位置是通過IBasicVideo 設置的。 IBasicVideo and IVideoWindow 提供了足夠的接口方法可以讓應用程序來一種永久的格式來保存或者存儲經過接口的數據。 7如何處理EC_REPAINT通知 當一個render filter 暫停或者停止的時候, 它發送一個EC_REPAINT消息。 這個消息告訴filter 圖表管理器render filter 需要數據。 如果一個filter 管理器準備停止的時候接收到這個消息, 它會首先暫停 filter graph, 等到所有的filter 都接收到了數據(通過調用GetState), 然後它再重新停止graph。 當處於停止狀態, 一個render filter 應該保存一副圖畫, 這樣可以在處理WM_PAINT消息的時候來顯示這幅圖畫。 當一個render filter 在停止或者暫停的時候收到WM_PAINT 消息時, 如果它沒有任何的數據用來顯示, 它也會給 filter圖表管理器發送一個EC_REPAINT消息。 當處於暫停狀態的filtr 圖表管理器接收到一個EC_REPAINT消息時, 圖表管理器就會調用IMediaPosition::put_CurrentPosition方法, 以當前的位置為參數, 這個方法的調用就會導致源filter flush 圖表管理器, 然後通過圖表管理器發送新的數據給render filter。 Render filter一般只發送一次這樣的消息通知, 也就是說, 如果一個render filter發送了一個ec_repaint消息, 在數據到來之前它不應該再發送ec_repaint消息了, 因此, 通常的做法設置一個標誌用來標示已經發送了一個ec_repaint消息, 當render 接收到數據或者輸入pin 被flushed以後, 這個標誌應該被重置。 當然, 如果輸入pin接收到end-of-stream通知的時候, 這個標誌不重置。 如果一個render filter 不控制它的EC_REPAINT消息的時候, 那麼整個filter圖表管理器都會被EC_REPAINT消息淹沒的。 例如, 如果一個render 沒有圖像顯示的時候, 如果一個窗口從render 窗口上拖動, 那麼render 窗口就會接收到大量的wm_paint消息的, 只有第一個消息可以產生EC_REPAINT事件。 Render filter 應該將它的輸入pin作為 EC_REPAINT 消息的第一個參數, by doing this, rendfilter 首先向附加的輸出pin(不知道這麼說是否正確, 原文the attached output pin will be queried for IMediaEventSink) 請求IMediaEventSink,接口, 如果輸出pin支持這個接口, 那麼Ec_REPAINT消息就首先通過這裏發送出去。 這樣就使輸出pin在graph收到消息前能夠處理 repain事件。 如果graph沒有處於停止的狀態, 輸出pin不會處理這個消息, 因為沒有空閑的內存數據塊。 如果輸出pin不能夠處理這個請求, 或者graph正在運行, 那麼EC_REPAINT消息就會被丟棄。 輸出pin上的IMediaEventSink::Notify方法調用, 返回S_OK表明輸出pin可以正確的處理repaint消息。 在graph的工作線程中會調用輸出pin的, 這樣就避免了render直接調用輸出pin, 防止死鎖。 如果graph處於停止狀態或者暫停狀態, 或者是輸出pin不能處理這個請求, 然後就會有缺省的處理過程來處理這個消息。 8如何處理全屏幕顯示 IVideoWindow插件管理graph的全屏回放。 它可以控制一個render filter 的窗口伸展成一個全屏來顯示圖像, 或者直接用一個全屏的filter來直接回放。 當一個filter從普通狀態到全屏顯示轉化的過程都要發送EC_ACTIVATE消息, 無論activated or deactivated.。 也就是說, render filter 在接收到一個WM_ACTIVATEAPP一定要發送EC_ACTIVATE消息。 當一個filter 處於全屏模式的時候, 這些消息用來管理是進入還是退出全屏模式。 當graph 接收到一個EC_ACTIVATE消息通知準備退出全屏模式, 那麼graph就發送一個EC_FULLSCREEN_LOST給應用程序, 應用程序也許會利用這個消息來保存全屏按鈕的狀態。 9消息通知小結Notifications 1 EC_ACTIVATE 說明;render filter 在接收到一個WM_ACTIVATEAPP一定要發送EC_ACTIVATE消息。 2 EC_COMPLETE 當所有的數據都提交完畢的時候, 發送此消息 3 EC_DISPLAY_CHANGED 當顯示的格式發生變化的時候, 發送此消息 4 EC_PALETTE_CHANGED 當調色板發生變化的時候, 發送此消息 5 EC_REPAINT 重畫的時候, 只發送一次 6 EC_USERABORT 當用戶關閉的時候 7 EC_VIDEO_SIZE_CHANGED 當視頻的尺寸發生變化的時候 8 EC_WINDOW_DESTROYED 當render filter 銷毀的時候, 發送此消息 10Render中的源和目標矩形 在VIDEOINFO, VIDEOINFOHEADER, and VIDEOINFOHEADER2三種媒體結構中有三個尺寸。 這篇文檔就是想解釋一下這三個尺寸有何不同, 以及他們是用來做什麼的。 首先, 在這些結構中有一個bmiHeader數據成員, 這個成員是BITMAPINFOHEADER結構, 這個結構的定義如下 typedef struct tagBITMAPINFOHEADER { DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER; 這個結構有兩個結構成員, biWidth和biHeight。 第二, 在這些結構中有一個rcSource數據成員, 同時也有一個rcTarget成員, 假如你有兩個filter, A和B, 假如這兩個filter以某一種媒體數據類型 相連, A在左邊, 上遊的filter, B在右邊, 下遊的filter。 在這兩個filter間傳遞的buffer具有一定的尺寸, 可以用bmiHeader.biWidth, bmiHeader.biHeight來標示。 Filter A的輸入視頻流由rcSource 的大小控制, Filter應該將輸入視頻的一部分擴展填充到buffer中rcTarget區域中, 填充的一部分的大小, 是根據rcSource和數據媒體類型的大小比較結果而定的。 也就是rcSource和(biWidth, biHeight) 比較。 如果rcSource為空, Filter A就會將全部的輸入pin拷貝到rcTarget, 如果rcTarget為空, 那麼Filter A就會將視頻填充到整個的輸出buffer中。 舉例如下: 假定Filter A接收的視頻圖像為160*120單位為象素, 假定A和B連接的時候採用如下的數據類型 (biWidth, biHeight):320, 240 RcSource:(0, 0, 0, 0) RcTarget(0, 0, 0, 0) 這就意味著Filter A就會將它接收到的視頻數據x方向和y方向乘於2以後填充到320*240的輸出bufer中。 又假如Filter A接收的視頻圖像為160*120單位為象素, 假定A和B連接的時候採用如下的數據類型 (biWidth, biHeight):320, 240 RcSource:(0, 0, 160, 240) RcTarget(0, 0, 0, 0) 兩個filter連接的buffer是320*240, 因為指定的rcSource指定了buffer的左半部分, Filter A就將輸入視頻的左半部分或者(0, 0, 80, 120)部分, 然後將視頻擴展到320*240(x方向*4, y方向*2)然後填充到320*240的輸出buffer中。 現在我們假定Filter A調用CBaseAllocator::GetBuffer,方法, 這個方法的返回的sample會附著一個媒體類型, 用來標示Filter B期望Filter A能夠提供一個和前面的視頻流具有不同size或者格式的數據。 假定 新的媒體類型如下 (biWidth, biHeight): 640, 480 rcSource: (0, 0, 160, 120) rcTarget: (0, 0, 80, 60) 這就意味著sample具有一個640*480的buffer, 原來的rcSource適用於原來的(320, 240)的媒體類型不適用於新的媒體格式, 因此, rcSource指定輸入只使用左上角的四分之一, 這一部分被放置到rcTarget的輸出buffer的左上角(80, 60), 因為Filte A接收160*120的視頻, 輸入視頻的左上角正好是(80, 60), 和輸出的位圖一樣大, 不用擴展 Filter A不會在輸出buffer的其它部分放置數據, The rcSource member is bounded by the biWidth and biHeight of the original connected media type between filters A and B, and rcTarget is bounded by the new biWidth and biHeight of the media sample.上面的例子中, rcSource就在 (0, 0, 320, 240)範圍內, rcTarget就在(0, 0, 640, 480)範圍內; 作者簡介:李強, 目前暫時供職於山大聯潤信息科技有限公司, 從事網絡視頻會議軟件的開發, 目前的感興趣的方向, 移動設備上多媒體的開發。 aooang@hotmail.com 歡迎轉載本文檔 。 (王朝網路 wangchao.net.cn)