directshow

轉自 http://blog.csdn.net/lxiongh/article/details/5350329
 
 

以文本方式查看主題
- 溫馨小築 (http://www.learnsky.com/bbs/index.asp)
-- 電腦編程 (http://www.learnsky.com/bbs/list.asp?boardid=6)
---- DirectShow (http://www.learnsky.com/bbs/dispbbs.asp?boardid=6&id=868)


-- 作者:admin
-- 發佈時間:2005-11-26 2:46:00
-- DirectShow

最近一段時間,在編寫DirectShow應用程序時常常遇到一些問題,原因是對DirectShow技術沒有較全面地掌握,對各個接口間的關係以及filter與filter之間連接的內部過程等都只是一知半解,除了再仔細地看看DirectShow的基類庫源文件之外,覺得也很有必要從頭到尾看一遍DirectShow的MSDN文檔。在看時順便有選擇地翻譯出來,一來以便以後再看時可以輕鬆點,二來也敦促自己不能不求甚解早早看看了事。在翻譯的過程中也加了一些自己的補充,因爲覺得某些MSDN章節實在是過於簡單還有些模棱兩可。

1. DirectShow介紹
DirectShow是一個 windows平臺上的流媒體框架,提供了高質量的多媒體流採集和回放功能。它支持多種多樣的媒體文件格式,包括ASF、MPEG、AVI、MP3和 WAV文件,同時支持使用WDM驅動或早期的VFW驅動來進行多媒體流的採集。DirectShow整合了其它的DirectX技術,能自動地偵測並使用可利用的音視頻硬件加速,也能支持沒有硬件加速的系統。
DirectShow大大簡化了媒體回放、格式轉換和採集工作。但與此同時,它也爲用戶自定義的解決方案提供了底層流控制框架,從而使用戶可以自行創建支持新的文件格式或其它用途的DirectShow組件。
以下是幾個使用DirectShow編寫的典型應用:DVD播放器、視頻編輯應用、AVI到ASF轉換器、MP3播放器和數字視頻採集應用。
DirectShow 是建立在組件對象模型(COM)上的,因此當你編寫DirectShow應用時,你必須具備COM客戶端程序編寫的知識。對於大部分的應用,你不需要實現自己的COM對象,DirectShow提供了大部分你需要的DirectShow組件,但是假如你需要編寫自己的DirectShow組件,你還需要具備編寫COM組件的知識。
1.1. DirectShow支持的格式
DirectShow是一個開放的框架,因此只要有合適的filter來分析和解碼,它可以支持任何格式。DirectShow默認支持以下的文件類型和壓縮格式:
注:打*號的需要Windows Media Format SDK支持
文件類型:
Windows Media? Audio (WMA)*
Windows Media? Video (WMV)*
Advanced Systems Format (ASF)*
Motion Picture Experts Group (MPEG)
Audio-Video Interleaved (AVI)
QuickTime (version 2 and lower)
WAV
AIFF
AU
SND
MIDI
壓縮格式:
Windows Media Video*
ISO MPEG-4 video version 1.0*
Microsoft MPEG-4 version 3*
Sipro Labs ACELP*
Windows Media Audio*
MPEG Audio Layer-3 (MP3) (decompression only)
Digital Video (DV)
MPEG-1 (decompression only)
MJPEG
Cinepak
微軟自己沒有提供MPEG2解碼器,一些可用的DirectShow MPEG2硬件或軟件解碼器是由第三方提供的。
1.2. 常見問題集(摘錄)
1.2.1. 一般問題

*DirectShow支持哪些操作系統?
DirectShow 支持Windows9X、Windows2000、Windows Me和Windows XP。
*使用 DirectShow需要多少COM知識?
應用程序開發者只需要基本的COM組件知識:實例化COM組件、調用接口、管理接口的引用計數。Filter開發者則需要更多。
*有與DirectShow兼容的硬件列表(HCL)嗎?
沒有。如果硬件兼容DirectShow,DirectShow會使用它們,如果沒有兼容的硬件,DirectShow使用GDI繪製視頻,以及使用 WaveOut系列多媒體API來播放音頻。
*可以使用哪些語言來編寫DirectShow應用?
DirectShow 主要爲C/C++開發設計。Visual Basic只能使用其中的很小一部分。可以通過MS JScript或VB Script來支持基於腳本的DVD和TV應用。也可能用Delphi來編寫,但SDK文檔不提供這方面的內容。
*DirectShow 會通過託管代碼實現嗎?
目前還沒有這個計劃。DirectX SDK提供了有限的使用音視頻回放類的託管回放功能,你可以使用COM interop創建託管代碼的DirectShow客戶端應用,但是因爲性能上的原因,不推薦創建運行在CLR上的filter。

*DirectShow開發需要什麼樣的編譯器
任何能夠產生COM對象的編譯器都可以。
*DirectShow和DirectX的其它組件的關係
DirectShow 和DirectX的其它組件在內部進行聯繫。DirectShow在硬件的支持下使用DirectSound和DirectDraw。Video Renderer和Overlay Mixer使用DirectDraw 3和DirectDraw5表面(surfaces)。Video Mixing Renderer 7(只支持WINXP)使用DirectDraw7表面。Video Mixing Renderer 9使用最新的(目前是Directx9)Direct3D API函數。即便是某個應用程序包含了DirectX其它組件,你也不必使用其它組件的API去編寫它。參考SDK的例子:Texture3D Sample。
*DirectShow與ActiveMovie的關係?
ActiveMovie 是DirectShow原來的名稱,現已不再使用,但是一部分API仍保留了"AM"的前綴,比如AM_MEDIA_TYPE和 IAMVideoAccelerator。
*DirectShow是限於多媒體應用嗎?
DirectShow 默認包含的組件主要是爲音視頻流設計的,但是,DirectShow框架已經成功地用於其它數據流的解決方案中。
*GraphEdit 工具有源碼嗎?GraphEdit.exe是否可再發布?
沒有源碼,不可再發布。
*DMO 可以代替DirectShow filter嗎?
在編寫編碼器、解碼器、效果器應用時,鼓勵用DMO代替 DirectShow filter。在其它的應用中,使用DirectShow filter可能會比較合適。
1.2.2. 程序編寫問題
*如何設置編譯環境,需要哪些頭文件和庫?
參考"設置編譯環境"章節
*GraphEdit列示了很多沒有文檔支持的filter,它們都是些什麼?
GraphEdit 枚舉了所有作爲filter類型註冊在系統中的filter,包括由第三方應用程序安裝的filter,以及其它微軟技術如Windows Media或NetMeeting安裝的,另外,一些DirectShow filter被用來做硬編碼或硬解碼驅動的外殼。Microsoft H.263 Video Codec用於NetMeeting,不再被DirectShow支持。
*如何知道 DirectShow已經被安裝?
調用CoCreateInstance創建一個Filter Graph Manager實例,如果成功,表示DirectShow已經被安裝,下面是一個例子:

IGraphBuilder *pGraph;

HRESULT hr = CoCreateInstance(CLSID_FilterGraph,
NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **) &pGraph);

*如果不通過屬性設置頁來更改filter的設置?
當然是通過filter提供的接口羅。如果沒有提供,就沒有辦法啦
*DirectShow能通知應用程序當前回放位置嗎?
不提供回調來通知位置,需要使用一個計時器定時調用 IMediaSeeking::GetCurrentPosition方法來得到當前回放位置。
*filter 運行在哪個特權級別下?
運行在Ring 3特權級別下,某些流控制驅動(如音視頻採集驅動)運行在Ring 0特權級別下。
*需要一個Kernel調試器嗎?
這依據具體的項目。安裝DirectX調試運行時庫(DirectX debug runtime library)意味着安裝調試驅動(Debug driver)和其它核心組件(kernel mode component),因此如果你的應用程序在其中的某個組件中產生了一個調試斷言(debug assert),你的機器就會自動重啓除非你擁有一個kernal調試器。
*DEFINE_GUID宏是怎麼工作的?
使用DEFINE_GUID宏可以讓你通過包含同一個頭文件來定義GUID值而不必使用extern關鍵詞。比如,你的工程中有三個源文件:src1.cpp,src2.cpp,src3.cpp,它們都使用一個相同的GUID值,而爲了保證一致性,這個GUID只能在你的工程中定義一次,這時,其它的源文件必須定義外部引用來使用它。用了DEFINE_GUID,你可以使用在所有源文件中包含同一個頭文件,在頭文件中這樣定義GUID:

DEFINE_GUID(CLSID_MyObject,
0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

這個例子中GUID爲0,實際編程中請用Guidgen工具來產生一個GUID,在其中的一個源文件中,在你的頭文件前包含initguid.h,如:

  // Src1.cpp

 #include 

 #include "MyGuids.h"

 

 // Src2.cpp

 #include "MyGuids.h" 


// Src3.cpp #include "MyGuids.h"

在沒有包含Initguid.h的地方,DEFINE_GUID宏創建外部引用來使用GUID值,在包含Initguid.h的地方,DEFINE_GUID重定義DEFINE_GUID宏以產生GUID的定義。
如是沒有在任何地方添加 Initguid.h,你會得到一個鏈接錯誤:"unresolved external symbol." ,如果同樣的GUID包含Initguid.h兩次,會得到編譯錯誤"redefinition; multiple initialization."要解決這些問題,請確認Initguid.h只包含一次。同樣的,不要包含Initguid.h到預編譯頭文件中去,因爲預編譯頭文件會被每個源文件包含。

[此貼子已經被作者於 2005-11-26 2:49:14編輯過]


-- 作者:admin
-- 發佈時間:2005-11-26 2:46:00
--
桃花塢裏桃花庵,桃花庵下桃花仙。
桃花仙人種桃樹,又摘桃花換酒錢。
酒醒只在花前坐,酒醉還來花下眠。
半醉半醒日復日,花落花開年復年。
但願老死花酒間,不願鞠躬車馬前。
車塵馬足顯者事,酒盞花枝隱士緣。
若將顯者比隱士,一在平地一在天。
若將花酒比車馬,彼何碌碌我何閒。
別人笑我太瘋癲,我笑他人看不穿。
不見五陵豪傑墓,無花無酒鋤作田。


-- 作者:admin
-- 發佈時間:2005-11-26 2:48:00
-- 開始DirectShow旅程

2. 開始DirectShow旅程
這個章節的內容主要是編寫 DirectShow應用所需的一些基本概念,可以把它當作一個高級介紹,理解這些內容只需具備一般的編程和有關多媒體的知識。
2.1. 設置DirectShow開發的編譯環境
這節內容描述瞭如何來編譯DirectShow應用。你可以使用命令行形式來編譯一個工程,也可以在Microsoft Visual Studio集成環境下(包含VC++)實現。
頭文件:
所有的DirectShow應用都需要Dshow.h這個頭文件,某些DirectShow接口需要附加的頭文件,參考接口的說明視具體情況定。
庫文件:
DirectShow使用以下庫文件:
Strmiids.lib 輸出類標識(CLSID)和接口標識(IID),所有DirectShow應用均需此庫。
Quartz.lib 輸出AMGetErrorText函數,如果不調用此函數,此庫不是必需的。
有了以上這些頭文件和庫文件,你已經可以編寫 DirectShow應用了,但是微軟建議使用DirectShow基類庫來編寫filter,這樣可以大大減少程序編寫的工作量。要使用 DirectShow基類庫,需要先編譯它,基類庫位於SDK的Samples//Multimedia//DirectShow //BaseClasses文件夾下,包含兩個版本的庫:發佈版(retail version)Strmbase.lib和調試版(debug version)Strmbasd.lib。具體參見"創建DirectShow Filter"一節。
2.2. DirectShow應用程序編程簡介
這節介紹DirectShow用到的一些基本術語和概念,看完這節後,你將能夠編寫你的第一個DirectShow應用程序。
Filter和Filter Graph
一個DirectShow應用程序是由一個個稱爲filter 的軟件構件組合而成的,filter執行一些多媒體流的操作,如:讀文件、從視頻採集設備中獲得視頻、將不同的格式的流解碼如MPEG1、將數據送到圖形卡或聲卡中去。
Filter 接收輸入併產生輸出。舉個例子,一個解碼MPEG1視頻流的filter,輸入MPEG1格式的視頻流,輸出一系列未壓縮的視頻幀。
在 DirectShow中,應用程序要實現功能就必須將這些filter鏈接在一起,因而一個filter的輸出就變成了另一個filter的輸入。這一系列串在一起的filter稱爲filter graph。例如,下圖就顯示了一個播放avi文件的 filter graph:

File Source(Async) filter從硬盤中讀取avi文件;AVI Splitter filter分析文件並將其分解成兩個流:一個壓縮的視頻流和一個音頻流;AVI Decompressor filter將視頻幀解碼,Video Renderer filter將解碼後的視頻幀通過DirectDraw或GDI顯示出來;Default DirectSound Device filter使用DirectSound播放音頻流。
應用程序沒有必要對這些數據流進行管理,而是通過一個叫Filter Graph Manager這個上層組件來控制這些filter。應用程序調用上層API如"Run"(通過graph移動數據)或"Stop"(停止移動數據)。如果你需要對數據流作更多的操作,你可以通過COM接口直接進入filter。Filter Graph Manager同樣也輸出事件通知給應用程序。
Filter Graph的另一個用途是將filter連在一起創建一個filter graph。
編寫一個DirectShow應用程序大體需要三個步驟:
1.創建一個Filter Graph Manager的實例
2.使用Filter Graph Manager創建一個filter graph,此時,需要已經具備所有必需的filter。
3.使用Filter Graph Manager控制filter graph和通過這些filter的流,在這個過程中,應用程序會收到Filter Graph Manager發送的事件。
完成這些後,應用程序需發佈這個Filter Graph Manager和所有的filter。
2.3. 播放一個文件
這一章以本節這個有趣的例子來結束,這個例子是一個播放音頻或視頻文件的簡單控制檯程序。程序只有寥寥數行,但卻展示了DirectShow編程的強大能力。
正如上一節所講的創建DirectShow應用程序的三個步驟,第一步,首先,需要調用CoInitialize來作初始化,然後調用CoCreateInstance創建Filter Graph Manager:

HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
return;
}

IGraphBuilder *pGraph;
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL,
CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph);

如上所示,類標識符(CLSID)是CLSID_FilterGraph。Filter Graph Manager由進程內DLL(in-process DLL)提供,因此參數3,dwClsContext的值爲CLSCTX_INPROC_SERVER。由於DirectShow運行自由線程模式 (free-threading model),所以你同樣可以使用COINIT_MULTITHREADED參數來調用CoInitializeEx。
第二步是創建filter graph,調用CoCreateInstance得到的IGraphBuilder接口包含了大部分創建filter graph的方法。在這個例子中還需要另外兩個接口:IMediaControl和IMediaEvent。
IMediaControl 控制數據流,它包含開啓和停止graph的方法;IMediaEvent包含從Filter Graph Manager獲取事件的方法,在這個例子中,這個接口用來得到回放結束事件。
所有這些接口由Filter Graph Manager提供,使用得到的IGraphBuiler接口指針來查詢得到。

IMediaControl *pControl;
IMediaEvent *pEvent;
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

現在你可以創建filter graph了,對於文件回放只需要一個簡單的調用:

hr = pGraph->RenderFile(L"C:////Example.avi", NULL);

IGraphBuilder::RenderFile方法創建了一個能夠播放指定文件的 filter graph,事實上,原本需要做的一些如創建filter實例及將這些filter連接起來的工作,都由這個方法自動完成了,如果是視頻文件,這個 filter graph看起來應該是這個樣子:
[file source]->[如果是縮格式,這裏是個解碼器]->[Video Renderer]
要開始回放,調用 IMediaControl::Run方法:

hr = pControl->Run();

當filter graph運行時,數據經過各個filter最後回放爲視頻或音頻。回放發生在一個單獨的線程中。你可以通過調用 IMediaEvent::WaitForCompletion方法來等待回放的結束:

long evCode = 0;
pEvent->WaitForCompletion(INFINITE, &evCode);

這個方法在播放期間被阻塞,直至播放結束或超時。
當應用程序結束時,需要釋放接口指針並關閉COM庫:

pControl->Release();
pEvent->Release();
pGraph->Release();
CoUninitialize();

下面是這個例子的完整代碼:

#include
void main(void)
{
IGraphBuilder *pGraph = NULL;
IMediaControl *pControl = NULL;
IMediaEvent *pEvent = NULL;

// Initialize the COM library.
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
printf("ERROR - Could not initialize COM library");
return;
}

// Create the filter graph manager and query for interfaces.
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
if (FAILED(hr))
{
printf("ERROR - Could not create the Filter Graph Manager.");
return;
}

hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

// Build the graph. IMPORTANT: Change this string to a file on your system.
hr = pGraph->RenderFile(L"C:////Example.avi", NULL);
if (SUCCEEDED(hr))
{
// Run the graph.
hr = pControl->Run();
if (SUCCEEDED(hr))
{
// Wait for completion.
long evCode;
pEvent->WaitForCompletion(INFINITE, &evCode);

// Note: Do not use INFINITE in a real application, because it
// can block indefinitely.
}
}
pControl->Release();
pEvent->Release();
pGraph->Release();
CoUninitialize();
}


-- 作者:admin
-- 發佈時間:2005-11-26 2:49:00
-- 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將應用程序與衆多複雜的設備隔離開來,通信和控制這些設備均出DirectShow的 filter來完成。DirectShow同樣爲某種文件格式提供與之對應的編解碼器。


-- 作者:admin
-- 發佈時間:2005-11-26 2:50:00
--

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)

如圖所示,每個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)在顯卡上。某些情況,比如採集視頻到一個文件中去,在某些點上需要將數據從內核模式傳入到用戶模式,但是,仍然沒有必要將數據拷貝到內存的一個新位置中去。
應用程序開發者通常只需瞭解一個內核流的背景知識而不需要深究它的細節。


-- 作者:admin
-- 發佈時間:2005-11-26 2:51:00
--

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中尋找


-- 作者:admin
-- 發佈時間:2005-11-26 2:51:00
--

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。

每一個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。

上游的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。

當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::GetPointer,IMediaSample::GetSize和IMediaSample::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 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::Run,IMediaControl::Pause或IMediaControl::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餘下的部分使用標準的推模式。


-- 作者:admin
-- 發佈時間:2005-11-26 2:52:00
--

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))
{
tch(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)
{
tch (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會關閉事件句柄,因此在這之後你就不能再使用事件句柄了。


-- 作者:admin
-- 發佈時間:2005-11-26 2:53:00
--
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與輸入的音頻數據速率進行匹配。


-- 作者:admin
-- 發佈時間:2005-11-26 2:53:00
--

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來執行。
如下圖:

假設我們要將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::Reconnect或IGraphConfig::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。

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

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

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

在 filter鏈狀態變化前,所在在filter鏈分界線上調用的數據處理都必須已完成。這個規則應用於IMemInputPin::Receive、IPin::NewSeqment和IPin::EndOfStream方法。鏈中的filter必須從由鏈外filter實現的這些方法調用中返回;而鏈外的filter也必須從這些由鏈內filter實現的這些方法調用中返回。
舉個例子,在上圖中,filter B必須完成在filter A上的所有數據處理調用,而filter E也必須完成從filter D上的調用。如果pin暴露了IPinFlowControl和IPinConnection接口,那麼如在動態重連那一節中所講的,你可以通過調用IPinFlowControl::Block和IGraphConfig::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接口,或者用它自己定義的接口代替。


-- 作者:admin
-- 發佈時間:2005-11-26 2:54:00
--

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::Run, IDistributorNotify::Pause或IDistributorNotify::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接口,如果不行則返回錯誤。


-- 作者:admin
-- 發佈時間:2005-11-26 2:55:00
-- 奇妙”的Merit(玩死Media Player)

運行GraphEdit,插入Filter,我們可以看到:每個Filter的信息一般包括Displayname、Filename、Merit、各個Pin以及Pin支持的Mediatype,還有Version等。我們今天就來看一看這個Merit(其他的一些Filter信息大家從它的名字上就可以猜到它的意義)。
要說Merit,肯定要先說Filter Graph Manager使用的智能連接(Intelligent Connect)機制。我們在Filter Graph中Render一個Pin,或者Render一個File,然後看到一條自動的“解碼”Filter鏈路就完成了——這就是智能連接機制。執行這個機制的調用方法爲:IGraphBuilder::RenderFile, IGraphBuilder::Render, 和 IGraphBuilder::Connect。下面分別對這三個調用方法進行闡述。
RenderFile :給出一個文件名,首先要找到正確的Source Filter。Filter Graph Manager通過查找註冊表來決定使用什麼Source Filter。在註冊表中,一般會有文件擴展名或者特徵字節與使用的Source Filter的對應信息。找到Source Filter之後,就從該Source Filter的各個Output pin開始,進行剩下的職能連接過程。這是一個“遞歸”過程,直到所有的分支都連到一個Renderer Filter上。步驟大致爲:
1. 如果Output pin支持IStreamBuilder接口,則把剩下的工作交給IStreamBuilder::Render。
2. 使用在內存中緩衝的Filter進行是試連接。
3. 使用在當前Filter Graph中還沒有完全連接的Filter進行試連接。(如果你想智能連接使用特定的你想使用的Filter,一種方法是,在開始智能連接之前先把該 Filter加入到Filter Graph中。)
4. 使用IFilterMapper2::EnumMatchingFilters搜索註冊表。Filter Graph Manager使用Merit值大於MERIT_DO_NOT_USE的所有Filter(Filter所在的目錄Merit值也應該大於 MERIT_DO_NOT_USE)進行試連接。在匹配Mediatype的前提下,Merit值越高,該Filter被使用的概率越高。
Render :這個方法從當前Filter Graph的某個Filter的指定Output pin開始,進行從這個Pin往下的一條支路的智能連接。智能連接的算法與上述RenderFile的類似。
Connect :這個方法調用,以欲連接的一對Input pin和Output pin作爲參數。首先進行這兩個Pin之間的直接連接。如果不能成功連接,則要插入“中介”Filter。這個“中介”Filter的插入過程就是一個智能連接過程,算法與上述的RenderFile類似。

現在我們知道了智能連接是怎麼回事。DirectShow的這個機制,很“聰明”,可以方便地使用第三方(非Microsoft公司)開發的Filter。但是,有一個問題,就是如果系統中存在一些“惡意”的Filter,那麼這個智能連接機制就會受到嚴峻的考驗。因爲這個原因,基於智能連接機制的應用程序(比如Windows Media Player)也會變得不穩定。(筆者並不贊成直接使用DirectShow Editing Services API進行非線性編輯,就是出於這方面的考慮。)
大家可以下載我寫的這個測試Filter源代碼(http://hqtech.nease.net/Document.htm)。其實這是一個 CTransFormFilter的空架子,只是這個Filter的Merit值非常高(0x8800000),而且支持所有的Mediatype。註冊這個Filter後,當有DirectShow應用程序使用智能連接機制時,就會反反覆覆地使用這個Filter進行試連接,沒有休止。如果你使用 Windows Media Player播放媒體文件(AVI、MPEG、WMV等等),應用程序就會阻塞住;即使是RealOne Player,在播放微軟格式的文件時也會出現這種現象。
好了,不玩了,別把Media Player弄得太慘,畢竟對我們也沒什麼好處!:)期望大家已經對這個Merit有了更深的認識。接下去,把這個Filter從你的系統註銷吧:regsvr32 /u yourlocalpath//HQMPKiller.ax。


-- 作者:admin
-- 發佈時間:2005-11-26 2:56:00
-- 玩轉 DVR-MS

發佈日期: 6/7/2005 | 更新日期: 6/7/2005

Stephen Toub
Microsoft Corporation

適用於:
Microsoft Windows XP Media Center Edition 2005
Microsoft DirectShow
DirectX 9.0 SDK

摘要: Stephen Toub 討論了 Windows XP Media Center 2005 生成的 DVR-MS 文件格式,介紹了 DirectShow 並展示瞭如何使用後者處理前者。

下載 DVR-MS 示例 Code.msi

本頁內容


播放 DVR-MS 文件


DirectShow 和 GraphEdit 簡介


DirectShow 接口


將編碼轉換爲 WMV


調試篩選器圖形


非託管資源清理


將 WmvConverter 投入使用: WmvTranscoderPlugin


訪問 DVR-MS 元數據


編輯 DVR-MS 文件


小結


相關書籍


致謝

幾年前我擁有一臺 TiVo。它已經不知藏在公寓壁櫥的哪個角落了,我想現在一定是佈滿灰塵,誠然,就是現在我也可能這樣對待它。佔據電視旁寶貴空位的是一個更漂亮、更復雜的現代化軟件和電子產品 — Microsoft Windows XP Media Center 2005。我的家人爲該設備取了個既得體又人性化的名字 —“米老鼠”,它有許多神奇的功能。然而,當我建議我的“技術嫺熟”的朋友們放棄他們現在使用的任一款數字攝像機 (DVR) 而轉爲使用此平臺時,只要他們讓我說明一個理由,我的回答都很簡單:可以對錄製的電視節目進行文件訪問。

DVR-MS 文件是由 Windows XP Service Pack 1 引入的流緩衝引擎(Stream Buffer Engine,SBE)創建的,Media Center 用它存儲錄製的電視節目。在本文中,我將向您演示如何通過託管代碼使用 DirectShow 來處理和操作 DVR-MS 文件。在此過程中,我將向您介紹我爲處理 DVR-MS 文件而創建的一些有用的實用工具,併爲您提供您在編寫自己的代碼時需要的工具和庫。所以,請打開 Visual Studio .NET,抓一把爆米花,享受這個過程吧。

注 本文假定您的系統中有一個正在工作的 MPEG2 解碼器,並且您使用的是 NTSC 而非 HD 內容(雖然這裏討論的大多數概念適用於 PAL 和 HD,但示例代碼可能無法正確地處理這些格式)。另外,由於內容所有者或廣播公司所設置的策略,一些 DVR-MS 文件受到複製保護。這種保護是在生成文件時通過檢查廣播公司的複製保護標誌 (CGMS-A) 確定的,它會限制您訪問特定 DVR-MS 文件的方式和時間。例如,在收費臺(如 HBO)錄製的電影可能是加密的,因此本文描述的技術就不適用了。最後,與本文相關聯的代碼示例和應用程序是針對 .NET Framework 1.1 編譯的。然而,默認情況下 Windows XP Media Center 2005 並沒有附帶安裝 .NET Framework 1.1,而是安裝 1.0。因此,要在您的 Media Center 中使用這些示例,您必須安裝 .NET Framework 1.1(可通過 Windows Update 獲得)或者重新編譯該示例以適用 .NET Framework 1.0。


-- 作者:admin
-- 發佈時間:2005-11-26 2:56:00
--

播放 DVR-MS 文件

談到視頻文件時,播放或許是可以執行的最重要的操作,所以我將從此入手。在您自己的應用程序中可以有多種播放 DVR-MS 文件的方式,這裏我將演示其中的一些。爲此,我創建了一個簡單的應用程序(如圖 1 所示),您可以在與本文有關的代碼下載中獲得。

圖 1. 播放 DVR-MS 文件的示例應用程序

播放 DVR-MS 文件的第一種方式也是最簡單的方式是,使用 System.Diagnostics.Process 類來執行它。由於Process.Start 包裝了來自 shell32.dll 的 ShellExecuteEx 非託管函數,因此這種方式利用了與從 Windows Explorer 雙擊一個文件相同的功能來播放 DVR-MS 文件:

private void btnProcessStart_Click(object sender, System.EventArgs e){  Process.Start(txtDvrmsPath.Text);}

這也意味着,視頻將在一個獨立的進程中播放,這個進程在 DVR-MS 文件的任何默認處理程序中運行;對於大多數機器和我的機器來說,它就是 Windows Media Player(我使用 Windows Media Player 10,如果您沒有,我建議您從http://www.microsoft.com/windows/windowsmedia/mp10/default.aspx 免費升級到該版本)。當然,Process.Start 有另一個同時接受可執行路徑和參數的重載,可以使用它在任何您想要的播放機中啓動 DVR-MS 文件,不管它是否是 .dvr-ms 擴展名的默認處理程序:

private void btnProcessStart_Click(object sender, System.EventArgs e){    Process.Start(    @"c://Program Files//Windows Media Player//wmplayer.exe",    "//"" + txtDvrmsPath.Text + "//"");}

您應該注意到,當這樣做時,有必要對 DVR-MS 文件的路徑加上引號(正如此處名爲 txtDvrmsPath 的 TextBox 的內容所提供的),因爲要使用的內容是 wmplayer.exe 的一個命令行參數。否則,路徑中的任何空格都會使路徑被分隔並解釋爲多個參數。

Process.Start 返回一個代表啓動進程的 Process 實例,這意味着您可以利用Process 提供的功能來與 Windows Media Player 進一步交互。例如,在您的應用程序中,您可能想先等待視頻停止再讓用戶繼續,可以使用Process.WaitForExit 方法來完成這樣的任務:

private void btnProcessStart_Click(object sender, System.EventArgs e){  using(Process p = Process.Start(txtDvrmsPath.Text))  {    p.WaitForExit();  }}

當然,這只是等待 Media Player 關閉,而不是像初始請求那樣播放您指定的文件,因爲您的應用程序沒有真正的視圖可以查看 Media Player 執行的內容。當打開 Media Player 時,按上述方法編碼也會凍結應用程序的 GUI,這個問題可以通過訂閱ProcessExited 事件加以解決,而不是用 WaitForExit 方法阻止。

總而言之,該解決方案編碼簡單方便,但非常不靈活,而且是在應用程序的外部播放視頻。它可能只在以下情況下才適用,您想允許用戶查看指定的文件,不過是在應用程序不必關心視頻內容而且應用程序根本不與視頻交互的情況下查看。例如,如果您的應用程序是一個下載代理,而且您想允許用戶查看已經複製到本地的視頻文件,則可能適合採用這種方式。


-- 作者:admin
-- 發佈時間:2005-11-26 2:57:00
--

由於我們知道 Windows Media Player 可以播放 DVR-MS 文件,因此對於大多數情況,更好的解決方案是在應用程序中宿主 Windows Media Player ActiveX 控件的一個實例。在 Visual Studio .NET 中,只需右鍵單擊工具箱,選擇添加控件並選擇 Windows Media Player COM 控件。這樣它就會出現在工具箱中,如圖 2 所示。

圖 2. 工具箱中的 Windows Media Player ActiveX 控件

當窗體中有一個 ActiveX 控件的實例時,讓它播放 DVR-MS 文件就只需設置播放器的 URL 屬性:

player.URL = txtDvrmsPath.Text;

在我的示例應用程序中,我選擇讓它更進一步。我創建了一個 System.Windows.Forms.Panel ,它位於想要顯示視頻的窗體中。當用戶請求使用 Media Player 播放選定的視頻時,我就新建一個 Media Player 控件的實例,將它添加到Panel 的子控件集合中,使其保持在最大化,並設置其 URL 屬性。這種方案允許我完全控制 Media Player 的生存期,而且可以輕鬆管理它在窗體中的位置,而不用擔心它的絕對定位值(這種方案也使演示播放視頻的其他方法變得輕鬆,稍後您將看到)。正在使用的這種方案的屏幕快照如圖 3 所示,下面顯示的是我使用的代碼:

private void btnWmp_Click(object sender, System.EventArgs e){  AxWindowsMediaPlayer player = new AxWindowsMediaPlayer();  pnlVideo.Controls.Add(player);  player.Dock = DockStyle.Fill;  player.PlayStateChange +=     new _WMPOCXEvents_PlayStateChangeEventHandler(      player_PlayStateChange);   player.URL = txtDvrmsPath.Text;}private void player_PlayStateChange(  object sender, _WMPOCXEvents_PlayStateChangeEvent e){  AxWindowsMediaPlayer player = (AxWindowsMediaPlayer)sender;  if (e.newState == (int)WMPLib.WMPPlayState.wmppsMediaEnded ||    e.newState == (int)WMPLib.WMPPlayState.wmppsStopped)  {    player.Parent = null; // removes the control from the panel    ThreadPool.QueueUserWorkItem(      new WaitCallback(CleanupVideo), sender);  }} private void CleanupVideo(object video){  ((IDisposable)video).Dispose();}

圖 3. 使用 WMP 控件的嵌入式 DVR-MS 播放

要阻止顯示 Media Player 工具欄,您可以更改控件的 uiMode 屬性:

player.uiMode = "none";

要在用戶右鍵單擊控件時阻止顯示 Media Player 上下文菜單,可以將其 enableContextMenu 屬性設置爲 false:

player.enableContextMenu = false;

您將注意到,在播放 DVR-MS 文件的前一刻,我爲播放器的 PlayStateChange 事件註冊了一個事件處理程序。這可以使我在播放停止時從Panel 刪除播放器。在 PlayStateChange 事件的處理程序中,我檢查播放是否結束,如果結束,就將播放器從其父控件(面板)刪除,並將一個工作項排入 .NETThreadPool 隊列中。這個工作項的作用只是處置播放器控件。我是在後臺線程中進行此次處置的,因爲無法在 PlayStateChange 事件處理程序中直接處置。在此事件處理程序中處置控件會在控件本身中引發異常,因爲事件處理程序是在控件中引發的,控件在執行完我的處理程序之後還需要進行更多的處理。在處理程序中處置播放器控件會導致功能被破壞,所以我讓該操作在事件處理程序完成之後稍微延遲一會,以便留出必要的時間。您將看到,在使用所演示的下一個播放機制時,就需要用到同一技術。

宿主 Windows Media Player ActiveX 控件有許多好處。它使用起來非常方便,而且提供了大量的功能。然而,Windows Media Player 使用 DirectX(特別是 DirectShow)來播放 DVR-MS 文件(本文後面我將更詳細地討論 DirectShow)。您不是依賴 Windows Media Player 與 DirectX 交互,而是在您的應用程序中使用 Managed DirectX,完全跳過 Windows Media Player。

在寫作本文時 Managed DirectX 的最新版本是 DirectX 9.0 SDK Update February 2005 下載 的一部分。(要獲得本文後面介紹的內容,您還需要 February 2005 Extras 下載 。)此 SDK 在您的全局程序集緩存 (GAC) 中安裝了 AudioVideoPlayback.dll 程序集,使其可用於您的應用程序(DirectX 運行庫安裝也安裝了此 DLL 以使您的最終用戶可以訪問它)。AudioVideoPlayback 是一個高級包裝,它含有您在 .NET 應用程序中播放視頻和音頻文件所需要的最少的 DirectShow 功能。

有了 Windows Media Player ActiveX 控件後,使用 AudioVideoPlayback 變得非常簡單。

private void btnManagedDirectX_Click(object sender, System.EventArgs e){  Video v = new Video(txtDvrmsPath.Text);  Size s = pnlVideo.Size;  v.Owner = pnlVideo;  v.Ending += new EventHandler(v_Ending);  v.Play();  pnlVideo.Size = s;}private void v_Ending(object sender, EventArgs e){  ThreadPool.QueueUserWorkItem(    new WaitCallback(CleanupVideo), sender);}private void CleanupVideo(object video){  ((IDisposable)video).Dispose();}

這段代碼首先實例化一個新的 Microsoft.DirectX.AudioVideoPlayback.Video 對象,然後將要播放的 DVR-MS 文件的路徑提供給它。當播放一段Video 時,它會自動將自身的大小(更具體地說是將它的所有者控件)調整爲所播放視頻的合適大小;爲了解決這個問題,我存儲了父面板控件的原始大小,這樣在開始播放後就可以重置其大小。就像處理 ActiveX 控件那樣,我註冊了一個要在播放停止時激發的事件處理程序,然後播放視頻。當播放結束時,我將一個工作項排入要處置Video 對象的 ThreadPool 隊列中,如同使用 ActiveX 控件一樣(原因也相同)。當您不再使用Video 對象時,對其進行處置是非常重要的;否則會浪費大量非託管資源,而且由於此對象有一個非常小的託管佔地,垃圾回收器 (GC) 沒有重大的動因可以及時進行回收,這樣將使這些非託管資源的分配情況不明,除非您手動通過IDisposable 處置。圖 4 中的屏幕快照演示了 AudioVideoPlayback 功能的使用。

圖 4. 採用 AudioVideoPlayback 的嵌入式播放

當然,雖然 AudioVideoPlayback 是一個高級 DirectShow 包裝,但並不意味着您不能創建自己的託管包裝(實際上,在本文後面我們將這樣做)。創建託管包裝的最簡單方式是使用 tlbimp.exe(或者採用類似的做法 — 使用 Visual Studio .NET 的 COM 類型庫導入功能。Visual Studio .NET 和 tlbimp.exe 都依賴於 Framework 中同樣的庫執行導入)。


-- 作者:admin
-- 發佈時間:2005-11-26 2:57:00
--

DirectShow 運行庫的核心庫是 quartz.dll,位於 %windir%//system32//quartz.dll。它包含用於音頻和視頻播放的最重要的 COM 接口和 coclass,本文後面將對此進行更加詳細的討論。在 quartz.dll 上運行 tlbimp.exe 會產生一個 interop 庫 — Interop.QuartzTypeLib.dll(此程序集的描述信息爲“ActiveMovie control type library”,因爲 DirectShow 的前身名爲 ActiveMovie),並公開FilgraphManagerClass (篩選器圖形管理器)和 IVideoWindow 接口。要播放視頻,您只需創建該圖形管理器的一個新實例並使用RenderFile 方法,在 DVR-MS 文件路徑中傳送,以便初始化該對象以進行播放。然後可以使用由 FilgraphManagerClass 實現的IVideoWindow 接口來控制播放選項,例如所有者窗口、視頻在父窗口中的位置,以及視頻窗口的標題。要開始播放,可以使用 Run 方法。WaitForCompletion 方法可以用於等待視頻停止播放(或者,可以指定一個正的毫秒數,作爲要等待的最長時間),Stop 方法可以用於暫停播放。要銷燬該對象並釋放用於播放的所有非託管資源(包括播放窗口本身),System.Runtime.InteropServices.Marshal 類及其ReleaseComObject 方法就會派得上用場了。使用 quartz.dll 的屏幕快照如圖 5 所示。

private void btnQuartz_Click(object sender, System.EventArgs e){  FilgraphManagerClass fm = new FilgraphManagerClass();  fm.RenderFile(txtDvrmsPath.Text);  IVideoWindow vid = (IVideoWindow)fm;  vid.Owner = pnlVideo.Handle.ToInt32();  vid.Caption = string.Empty;  vid.SetWindowPosition(0, 0, pnlVideo.Width, pnlVideo.Height);  ThreadPool.QueueUserWorkItem(new WaitCallback(RunQuartz), fm);}private void RunQuartz(object state){  FilgraphManagerClass fm = (FilgraphManagerClass)state;  fm.Run();  int code;  fm.WaitForCompletion(Timeout.Infinite, out code);  fm.Stop();  while(Marshal.ReleaseComObject(fm) > 0);}

圖 5. 使用 quartz.dll 的嵌入式播放

我剛剛向您介紹了一些在自己的應用程序中播放 DVR-MS 文件的方法。雖然我討論了多個播放 DVR-MS 文件的方法(而且我還沒列舉完),但所有這些方法都要依賴於 DirectShow 纔有播放功能。因此,我們將簡要介紹一下 DirectShow(或者讓那些具有 DirectShow 經驗的人重溫一下)。

返回頁首

DirectShow 和 GraphEdit 簡介

在本質上,使用 DirectShow 處理視頻文件的應用程序是通過一組稱爲篩選器的組件完成的。一個篩選器通常只對多媒體數據流執行一種操作。這樣的篩選器很多,每個篩選器執行不同的任務,例如讀取 DVR-MS 文件、寫出 AVI 文件、對 MPEG-2 壓縮視頻進行解碼、將視頻和音頻呈現到視頻卡和聲卡上,等等。這些篩選器的實例可以連接在一起並組合成一個篩選器圖形,然後由 DirectShow 篩選器圖形管理器組件進行管理(在前面介紹 quartz.dll 時,您已簡要地對其進行了瞭解)。這些圖形是定向的,也是非循環的,這意味着兩個篩選器之間的特定連接只允許數據朝一個方向流動,而且只能流經特定篩選器一次。這種數據流程稱爲流 (stream),而篩選器則用來處理這些流。篩選器是通過它們公開的針 (pin) 連接到其他篩選器的,因此,一個篩選器的輸出針連接到另一個篩選器的輸入針,並按從前者發送到後者的方式發送數據流。

爲了對此進行演示並顯示本文中所使用的圖形,我使用了 DirectX SDK 中一個名爲 GraphEdit 的實用工具。GraphEdit 可以用來使篩選器圖形可視化,當要確定如何構建用於特定目的的圖形以及調試您所構建的圖形時,這個功能就能派上用場。稍後,我將介紹如何使用 GraphEdit 來對在您的應用程序中運行的篩選器圖形進行連接和可視化。

現在,我們運行 GraphEdit。在“File”菜單下,選擇“Render Media File”,然後選擇本地可用的任何有效的 DVR-MS 文件(請注意,您可能需要在“Open File”對話框中將篩選器擴展名更改爲“All Files”,而不是“All Media Files”,因爲最近發佈的 GraphEdit 版本並沒有將 .dvr-ms 擴展名歸類爲媒體文件)。您應該能夠看到一個圖形,它類似於圖 6 所示的圖形。

圖 6. GraphEdit 準備播放 DVR-MS 文件

此時,GraphEdit 已構造了一個篩選器圖形,它能夠播放選定的 DVR-MS 文件。這些藍框中的每一個都是一個篩選器,箭頭顯示每個篩選器上的輸入和輸出針如何互相連接以形成圖形。圖形中的第一個篩選器是 StreamBufferSource 篩選器的實例,它由 Windows XP SP1 及更高版本的 %windir%//system32//sbe.dll 庫公開。選擇這個篩選器是因爲它在註冊表中配置爲 .dvr-ms 擴展名的源篩選器 (HKCL//Media Type//Extensions//.dvr-ms//Source Filter)。它的作用是從磁盤中讀取一個文件,並將該文件的數據以流的形式發送到圖形的其他部分。它從一個 DVR-MS 文件提供三個流。


-- 作者:admin
-- 發佈時間:2005-11-26 2:58:00
--

第一個是音頻流。如果您檢查第一個針的針屬性(DVR Out - 1,可以通過右鍵單擊 GraphEdit 中的針來訪問針屬性),您可以發現該針的主要類型是 Audio,而其子類型是 Encrypted/Tagged,這意味我們在對該數據進行任何操作之前必須先對它進行解密和/或取消標記。這個過程是由 Decrypter/Detagger 篩選器(由 %windir%//system32//encdec.dll 公開)處理的。Decrypter/Detagger 將加密/帶標記的音頻流作爲輸入,然後發出 MPEG-1 音頻流(對於高清晰度的內容則輸出 dolby-AC3 流),這一點您可以通過檢查該篩選器的 In(Enc/Tag) 和 Out 針加以驗證。這裏將音頻發送到 MPEG Audio Decoder 篩選器(由 quartz.dll 公開),通過它將音頻解壓縮爲脈衝編碼調製 (PCM) 音頻流。音頻流的最後一個篩選器 DirectSound Audio Renderer(也由 quartz.dll 公開)接收此 PCM 音頻數據並在計算機的聲卡上播放。

DVR-MS 源篩選器提供的第二個流包含所錄製的電視節目的閉合字幕數據。和音頻流一樣,閉合字幕流也是經過加密/標記的,所以它必須首先通過 Decrypter/Detagger 篩選器。如果查看此篩選器的 Out 針,您會發現其主要類型是 AUXLine21Data,而其子類型是 Line21_BytePair。電視節目中的閉合字幕是作爲電視圖像的一部分發送的,並專門編碼到圖像的 line 21 中。

DVR-MS 源篩選器發出的第三個流是視頻內容 (video feed)。與音頻和閉合字幕數據一樣,這個流也是經過加密/標記的,所以它必須首先通過 Decrypter/Detagger 篩選器。Decrypter/Detagger 篩選器的輸出是 MPEG-2 視頻流,所以它必須先通過 MPEG-2 視頻解碼器才能呈現視頻。Microsoft 沒有在 Windows 中附帶 MPEG-2 解碼器,所以系統中必須有可用的第三方解碼器才能播放。解碼後的視頻流再送到默認的視頻呈現程序(由 quartz.dll 公開)。

單擊圖形上方的綠色播放按鈕就會出現一個標題爲 ActiveMovie Window 的新窗口並在該窗口中播放 DVR-MS 文件。請注意,由於閉合字幕 Decrypt/Tag Out 針沒有連接到任何地方,因此在呈現視頻時沒有用到閉合字幕數據。您可以通過修改圖形對此進行更改。實際做法是,首先刪除默認的視頻呈現程序(單擊該篩選器並按“Delete”鍵),因爲該呈現程序不能處理多路輸入。具體來說,我們需要這樣的呈現程序:它可以顯示視頻流,並能將包含呈現的閉合字幕數據的位圖覆蓋其上。如何從 Decrypter/Detagger 篩選器獲取 line 21 字節對,將其作爲位圖呈現出來呢?Windows 實際上附帶了一個正好可以完成此任務的 DirectShow 篩選器。使用“Graph”菜單下的“Insert Filters...”命令,展開樹視圖中的 DirectShow 篩選器節點並選擇“Video Mixing Renderer 9”篩選器。單擊“insert”按鈕將此篩選器的實例添加到圖形中,然後關閉“insert filters”對話框。現在,Video Mixing Renderer 9 篩選器成爲圖形的一部分了,但沒有連接到任何地方,也就不能使用(實際上,如果您現在單擊“play”按鈕,則只播放音頻,因爲視頻流沒有連接到呈現程序)。單擊 MPEG-2 解碼器上的 Video Output 針,並將它拖到呈現程序的 VMR Input0 針上(請注意,如果您使用的解碼器不是 NVDVD,則視頻輸出針的名稱可能不同,但概念是一樣的)。如果您現在播放圖形,則會看到輸出與使用默認視頻呈現程序播放時基本一致。然而,您將看到,此時呈現程序篩選器公開了多個輸入針(實際上,篩選器可以根據連接到它們的其他篩選器動態更改公開的針)。我們可以將閉合字幕 Decrypter/Detagger 篩選器的 Out 針連接到呈現程序的 VMR Input1 針上,以此利用這一特性。GraphEdit 會自動插入一個 Line 21 Decoder 2 篩選器,將 Decrypter/Detagger 篩選器連接到解碼器篩選器,並將解碼器篩選器連接到呈現程序篩選器。現在,您應該能看到如圖 7 所示的圖形。當您播放此圖形時,您將看到閉合字幕像您期望的那樣,以文本的形式出現在視頻前。

圖 7. 將閉合字幕合併到視頻顯示中

此時,對 DirectShow 不熟悉的讀者可能會產生疑惑:是如何發現 Line 21 Decoder 2 篩選器的?爲什麼一開始只需使用 GraphEdit 的 Render Media File 操作就能構造出整個圖形呢?GraphEdit 依賴IGraphBuilder 接口提供的功能來查找和選擇合適的篩選器,並在需要時將它們互連(IGraphBuilder 是由我們在介紹如何播放 DVR-MS 文件時簡要提到的FilgraphManager 組件實現的,實際上我們使用的 RenderFile 方法就是 IGraphBuilder 接口的一部分)。


-- 作者:admin
-- 發佈時間:2005-11-26 2:58:00
--

這種用於自動構建篩選器圖形的機制稱爲 Intelligent Connect。由於您並不真的需要知道 Intelligent Connect 的具體內容(除非您正在實現自己的篩選器並想讓它們可以自動構建圖形),因此在這裏此主題我不想介紹得太多,而是讓您參考 DirectX SDK 中該主題的詳細文檔。然而,簡單地說,RenderFile 方法是一個簡單的包裝,它包裝了IGraphBuilder 中的另外兩個方法:AddSourceFilterRenderRenderFile 首先調用 AddSourceFilter ,對於本地文件,它只需在註冊表中查找正在播放的文件的擴展名所必需的源篩選器的類型,將適當的篩選器實例添加到篩選器圖形中,並對它進行配置以使其指向指定的源文件。對於此源篩選器的每個輸出針,RenderFile 再調用Render 方法,該方法試圖查找從此針到圖形中的呈現程序的一條路徑。如果該針實現了 IStreamBuilder 接口,則Render 只是委託該實現,將所有細節都交給該篩選器的實現。否則,Render 會試圖查找此針可以連接的篩選器。爲此,它會查找在圖形構建過程前期可能緩存的緩存篩選器,查找已經成爲圖形的一部分且有未連接的輸入針的任何篩選器,並使用IFilterMapper 接口查找註冊表中兼容的篩選器類型。如果找到了一個篩選器,則它會再對這個新的篩選器重複此過程,直到到達呈現篩選器,此時就成功地停止。如果沒有找到篩選器,則 Intelligent Connect 構建圖形未成功。這就是依賴 Intelligent Connect 的一個缺點:它並非始終有效。另外,如果您的機器上安裝了新的篩選器,則 Intelligent Connect 可能會選擇這些新的篩選器,而不是您當前期望在應用程序中使用的篩選器。因此,您在設計時可能要選擇避免這種情況(我後面將要介紹,如果您確切地知道想在圖形中使用哪些篩選器,則顯式構建圖形而不使用 Intelligent Connect 是很容易的)。

既然您對 DirectShow 已有所瞭解,我們將要以編程方式使用它,以便對 DVR-MS 文件進行許多很合適的操作。畢竟,一旦 DVR-MS 源篩選器加載到圖形中,我們就可以像處理其他音頻和視頻數據流那樣處理來自 DVR-MS 的數據,操作它們的方法是無限的。


-- 作者:admin
-- 發佈時間:2005-11-26 2:58:00
--

DirectShow 接口

然而,我們首先需要的是能夠以編程方式處理 DirectShow。對於非託管代碼,這可能是立即可行的,因爲 SDK 包含了通過 C++ 訪問 DirectShow 庫所需要的所有頭文件。對於託管代碼,問題就有些棘手。雖然 Managed DirectX 確實包含前面討論的 AudioVideoPlayback.dll 庫,但該庫級別很高,它提供VideoAudio 級別的抽象,而我們需要的是能夠在篩選器和針級別對篩選器圖形進行操作。雖然我覺得這個問題將來會得到改善,但至少當前版本的 Managed DirectX 對我們愛莫能助。

quartz.dll 是什麼?quartz.dll 的類型庫公開了一些我們需要的功能,這裏列出所公開接口的完整列表

[此貼子已經被作者於 2005-11-26 3:03:44編輯過]


-- 作者:admin
-- 發佈時間:2005-11-26 2:59:00
--

接口
描述

IAMCollection

篩選器圖形對象集合,例如篩選器或針。

IAMStats

允許應用程序從圖形管理器中檢索性能數據。篩選器可以使用此接口記錄性能數據。

IBasicAudio

允許應用程序控制音頻流的音量和平衡。

IBasicVideo

允許應用程序設置視頻屬性,例如目標矩形和源矩形

IBasicVideo2

從 IBasicVideo 接口派生,爲應用程序提供了一個附加方法,通過它可以檢索視頻流的首選縱橫比。

IDeferredCommand

允許應用程序取消或修改該應用程序先前使用 IQueueCommand 接口排入隊列的圖形-控制命令。

IFilterInfo

管理篩選器的信息並提供訪問篩選器和表示篩選器上的針的 IPinInfo 接口。

IMediaControl

提供方法來控制經過篩選器圖形的數據流。它包含運行、暫停和停止圖形的方法。

IMediaEvent

包含用來檢索事件通知和用於重寫篩選器圖形管理器的默認事件處理的方法。

IMediaEventEx

從 IMediaEvent 派生並添加方法來啓用一個應用程序窗口,以便在事件發生時接收消息。

IMediaPosition

包含用於查找流中一個位置的方法。

IMediaTypeInfo

包含用於檢索針連接的媒體類型的方法。

IPinInfo

包含用於檢索針信息和連接針的方法。

IQueueCommand

允許應用程序預先將圖形-控制命令排入隊列。

IRegFilterInfo

提供對 Windows 註冊表中的篩選器的訪問,以及向篩選器圖形中添加已註冊的篩選器。

IVideoWindow

包含用於設置窗口所有者、窗口的位置和尺寸及其他窗口屬性的方法。

[此貼子已經被作者於 2005-11-26 3:12:42編輯過]


-- 作者:admin
-- 發佈時間:2005-11-26 3:13:00
--
這確實是個很好的開頭,但它沒有爲我們提供一些處理圖形和篩選器的最重要的接口。例如,手動構造圖形比較常用的接口之一,IGraphBuilder 接口,並沒有包括在內。表示特定篩選器實例和提供對其針訪問的IBaseFilter 接口也沒有包括在內。下表列出了在本文中要完成圖形需要訪問的主要接口:


-- 作者:admin
-- 發佈時間:2005-11-26 3:15:00
--

接口
描述

IBaseFilter

提供用於控制篩選器的方法。應用程序可以使用此接口枚舉針和查詢篩選器信息。

IConfigAsfWriter2

提供用於獲取和設置 WM ASF Writer 篩選器寫文件要使用的高級流格式(Advanced Streaming Format,ASF)配置文件的方法和用於支持 Windows Media Format 9 Series SDK 中的新功能(例如雙向編碼和對反交錯視頻的支持)的方法。

IFileSinkFilter

在將媒體流寫入文件的篩選器上實現。

IFileSourceFilter

在從文件讀媒體流的篩選器上實現。

IGraphBuilder

提供方法來支持應用程序構建篩選器圖形。

IMediaControl

提供方法來控制數據流經篩選器圖形的流程。它包括用於運行、暫停和停止圖形的方法。

IMediaEvent

包含用於檢索事件通知和重寫篩選器圖形管理器的默認事件處理的方法。

IMediaSeeking

包含用於查詢當前位置和查找流中的特定位置的方法。

IWmProfileManager

用於創建配置文件、加載現有的配置文件和保存配置文件。

另外,我還需要顯式實例化各個 COM 類,下面展示了其中最重要的一些類,以及它們的類 ID 和對每個類的描述:


類 ID
描述

篩選器圖形管理器

E436EBB3-524F-11CE-9F53-0020AF0BA770

構建和控制篩選器圖形。此對象是 DirectShow 中的中心組件。

Decrypter/Detagger 篩選器

C4C4C4F2-0049-4E2B-98FB-9537F6CE516D

有條件地解密由 Encrypter/Tagger 篩選器加密的示例。輸出類型與 Encrypter/Tagger 篩選器接收到的原始輸入類型相匹配。

WM ASF Writer 篩選器

7C23220E-55BB-11D3-8B16-00C04FB6BD3D

接受數量可變的輸入流並創建高級流格式 (ASF) 文件。


-- 作者:admin
-- 發佈時間:2005-11-26 3:16:00
--

正如 Eric Gunnerson 在關於 DirectShow 和 C# 的 his blog entry 中指出的,一種快捷簡便的導入接口的方法是使用 DirectX SDK 附帶的 DirectShow 接口定義語言(Interface Definition Language,IDL)文件。這些文件包含了 COM 接口定義,我對其中的大部分接口都很感興趣。我可以創建自己的 IDL 文件(它的創作是爲了產生一個類型庫),然後通過 Microsoft 接口定義語言 (MIDL) 編譯器 (midl.exe) 運行它。這將產生一個類型庫,然後我再使用 .NET Framework tool Type Library Importer (tlbimp.exe) 將它轉換成託管程序集。

遺憾的是,Eric 也指出,它不是一個完美的解決方案。首先,隨 DirectX SDK 附帶的 IDL 文件並沒有描述我需要的所有接口,例如 IMediaEventIMediaControl 。其次,即使我需要的所有接口都描述了,但通常需要對 interop 簽名的創建進行更多控制,而不只是 tlbimp.exe 所提供的控制。例如,如果在圖形運行完成之前用戶指定的時間到期,則IMediaEvent.WaitForCompletion (本文後面將會介紹)會返回一個 E_ABORT HRESULT;它將轉換成在 .NET 中引發的異常,如果您在輪詢循環中要頻繁調用WaitForCompletion (我就打算這樣做),則這樣做就不合適。另外,IDL 類型和託管類型之間並不是一對一的映射;實際上,存在這樣的情況,類型可能根據使用它的上下文不同而進行不同的封送處理。例如,在 DirectX SDK 的 axcore.idl 文件中,IEnumPins 接口公開了以下方法:

HRESULT Next(  [in] ULONG cPins,            // Retrieve this many pins.  [out, size_is(cPins)] IPin ** ppPins,  // Put them in this array.  [out] ULONG * pcFetched         // How many were returned?);

當它編譯成類型庫並由 tlbimp.exe 進行轉換時,產生的程序集包含以下方法:

void Next(  [In] uint cPins,   [Out, MarshalAs(UnmanagedType.Interface)] out IPin ppPins,   [Out] out uint pcFetched);

雖然非託管的 IEnumPins::Next 可以被任何正整數值的 cPins 調用,但如果調用託管版本用的 cPins 值不是 1,則會產生錯誤,因爲 ppPins 不是IPin 實例數組,而是單個 IPin 實例的引用。


-- 作者:admin
-- 發佈時間:2005-11-26 3:16:00
--

基於所有這些原因,以及 DirectShow 接口相對簡單,我選擇手動用 C# 實現 COM 接口 interop 定義;雖然這需要的工作更多,但它可以讓您最好地控制封送內容、方式和時間(不過,請注意,在創建這些手動編碼的 interop 定義時,採用 tlbimp.exe 生成的 MSIL 是一個很好的起點,或者更好的方式 — 採用這些導入類型庫的反編譯 C# 實現,可以使用 Lutz Roeder 的 .NET 發送程序生成它,這個程序可以從http://www.aisto.com/roeder/dotnet/ 獲得)。在與本文有關的代碼下載中,您會發現我在本文中使用的每個非託管 DirectShow 接口都有手動編碼的 C# 接口。舉個例子,下面是前面討論的IGraphBuilder 接口的 C# 實現:

[ComImport][Guid("56A868A9-0AD4-11CE-B03A-0020AF0BA770")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IGraphBuilder{  void AddFilter([In] IBaseFilter pFilter,     [In, MarshalAs(UnmanagedType.LPWStr)] string pName);  void RemoveFilter([In] IBaseFilter pFilter);  IEnumFilters EnumFilters();  IBaseFilter FindFilterByName(    [In, MarshalAs(UnmanagedType.LPWStr)] string pName);  void ConnectDirect([In] IPin ppinOut, [In] IPin ppinIn,     [In, MarshalAs(UnmanagedType.LPStruct)] AmMediaType pmt);  void Reconnect([In] IPin ppin);  void Disconnect([In] IPin ppin);  void SetDefaultSyncSource();  void Connect([In] IPin ppinOut, [In] IPin ppinIn);  void Render([In] IPin ppinOut);  void RenderFile(    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFile,    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrPlayList);  IBaseFilter AddSourceFilter(    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFileName,    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFilterName);  void SetLogFile(IntPtr hFile);  void Abort();  void ShouldOperationContinue();}

然後就可以通過我的 IGraphBuilder 接口來轉換和使用篩選器圖形管理器組件的實例。那麼,如何獲取篩選器圖形管理器組件的實例呢?我使用瞭如下代碼:

public class ClassId{  public static readonly Guid FilterGraph =     new Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770");  public static readonly Guid WMAsfWriter =     new Guid("7C23220E-55BB-11D3-8B16-00C04FB6BD3D");  public static readonly Guid DecryptTag =     new Guid("C4C4C4F2-0049-4E2B-98FB-9537F6CE516D");  ...  public static object CoCreateInstance(Guid id)  {    return Activator.CreateInstance(Type.GetTypeFromCLSID(id));  }}

在這個包裝就位後,我就可以創建篩選器圖形管理器的實例,配置能夠播放 DVR-MS 文件的篩選器圖形,以及播放文件,總共只需要五行代碼:

object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);((IGraphBuilder)filterGraph).RenderFile(pathToDvrmsFile);((IMediaControl)filterGraph).Run();EventCode status;((IMediaEvent)filterGraph).WaitForCompletion(Timeout.Infinite, out status);

既然我們知道如何通過託管代碼使用 DirectShow,現在我們就來看看如何利用它做一些很酷的事情。


-- 作者:admin
-- 發佈時間:2005-11-26 3:17:00
--

將編碼轉換爲 WMV

如果 Internet 搜索引擎能提供任何線索,則人們對 DVR-MS 文件想做的最流行的一件事情就是將它們轉換成 Windows Media Video 文件。通過目前我們爲處理 DVR-MS 文件和 DirectShow 而創建的框架,這個任務是很容易實現的。簡單地說,我需要做的就是創建一個使用 DVR-MS 源篩選器和 WM ASF Writer 篩選器接收器(它編碼並寫出 WMV 文件)的圖形,並在它們之間建立適當的篩選器和連接。我故意對這些中間篩選器含糊其詞,因爲我可以讓 Intelligent Connect 替我查找和插入它們。作爲說明手動進行此操作的簡單性的例子,我們按照以下簡單步驟在 GraphEdit 中創建適當的轉換圖形:

1.

打開 GraphEdit。

2.

從“Graph”菜單中選擇“Insert Filters”,插入一個 DirectShow WM ASF Writer 篩選器。當提示輸入一個輸出文件名時,請輸入目標文件的名稱,以 .wmv 爲擴展名。

3.

從“File”菜單中選擇“Render Media File”,並在彈出的“Open File”對話框中選擇輸入的 DVR-MS 文件(再次提醒,您很可能需要將篩選器文件擴展名更改爲“All Files”而不是“All Media Files”)。

GraphEdit 將使用該圖形的 RenderFile 方法來爲 DVR-MS 文件添加一個源篩選器,並通過需要的一系列中間篩選器將它連接到適當的呈現程序。由於以上操作發生時 WM ASF Writer 篩選器接收器已經在圖形中,因此使用 Intelligent Connect 的RenderFile 會將流發送到該篩選器接收器上,而不是插入新的默認呈現程序篩選器。您應該能看到如圖 8 所示的圖形。

圖 8. 將 DVR-MS 編碼轉換爲 WMV 的圖形

以編程方式進行這種轉換是非常簡單的,可以通過以下代碼實現:

// Get the filter graphobject filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);DisposalCleanup.Add(filterGraph);IGraphBuilder graph = (IGraphBuilder)filterGraph;// Add the ASF writer and set the output nameIBaseFilter asfWriterFilter = (IBaseFilter)  ClassId.CoCreateInstance(ClassId.WMAsfWriter);DisposalCleanup.Add(asfWriterFilter);graph.AddFilter(asfWriterFilter, null);IFileSinkFilter sinkFilter = (IFileSinkFilter)asfWriterFilter;sinkFilter.SetFileName(OutputFilePath, null);// Render the DVR-MS file and run the graphgraph.RenderFile(InputFilePath, null);RunGraph(graph, asfWriterFilter);

先創建一個篩選器圖形,將 WM ASF Writer 篩選器添加到其中並配置爲指向適當的輸出文件路徑,然後將 DVR-MS 文件添加到該圖形中並使用圖形的 RenderFile 方法來呈現。遺憾的是,這在控制 WMV 文件編碼方式上並沒有提供很多靈活性。爲了做到這一點,我們需要用一個配置文件配置 WM ASF Writer,這可以通過在調用RenderFile 之前插入以下代碼來完成:

// Set the profile to be used for conversionif (_profilePath != null){  // Load the profile XML contents  string profileData;  using(StreamReader reader =     new StreamReader(File.OpenRead(_profilePath)))  {    profileData = reader.ReadToEnd();  }  // Create an appropriate IWMProfile from the data  IWMProfileManager profileManager = ProfileManager.CreateInstance();  DisposalCleanup.Add(profileManager);  IntPtr wmProfile = profileManager.LoadProfileByData(profileData);  DisposalCleanup.Add(wmProfile);  // Set the profile on the writer  IConfigAsfWriter2 configWriter =    (IConfigAsfWriter2)asfWriterFilter;  configWriter.ConfigureFilterUsingProfile(wmProfile); }

這段代碼假定配置文件 PRX 文件的路徑已經存儲在字符串成員變量 _profilePath 中。首先,使用 System.IO.StreamReader 將該配置文件的 XML 內容讀到一個字符串中。然後創建 Windows Media Profile Manager(通過IWMProfileManager 接口訪問),並使用該管理器的 LoadProfileByData 方法將配置文件加載到其中。這爲我們提供了一個指向所加載的配置文件的接口指針,可以用它來配置 WM ASF Writer 篩選器。WM ASF Writer 篩選器實現了IConfigAsfWriter2 接口,它提供了 ConfigureFilterUsingProfile 方法,這個方法可以根據接口指針指定的配置文件配置編寫器。

創建和配置好圖形之後,剩下的工作就是運行它,我是使用特意指定的 RunGraph 方法實現的。該方法首先獲取指定圖形的 IMediaControlIMediaEvent 接口。它還試圖獲取可用於跟蹤源 DVR-MS 文件處理進度的IMediaSeeking 接口。然後使用 IMediaControl 接口來運行圖形,從此時開始,方法中的剩餘代碼僅僅是用來跟蹤轉換的處理進度。在圖形結束運行前,代碼會不斷輪詢IMediaEvent.WaitForCompletion 方法,如果等待時間已到但圖形還沒完成運行,則該方法將返回狀態代碼 EventCode.None (0x0)。如果發生這種情況,則會使用 IMediaSeeking 接口來查詢已經處理多少 DVR-MS 文件以及該文件的持續時間,由此我可以計算文件處理的百分比。

當圖形最終完成運行時,IMediaEvent.WaitForCompletion 會返回 EventCode.Complete (0x1),並使用IMediaControl.Stop 來停止圖形。


-- 作者:admin
-- 發佈時間:2005-11-26 3:17:00
--

protected void RunGraph( IGraphBuilder graphBuilder, IBaseFilter seekableFilter){ IMediaControl mediaControl = (IMediaControl)graphBuilder; IMediaEvent mediaEvent = (IMediaEvent)graphBuilder; IMediaSeeking mediaSeeking = seekableFilter as IMediaSeeking; if (!CanGetPositionAndDuration(mediaSeeking))  {  mediaSeeking = graphBuilder as IMediaSeeking;  if (!CanGetPositionAndDuration(mediaSeeking)) mediaSeeking = null; } using(new GraphPublisher(graphBuilder,  Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")) {  mediaControl.Run();  try  {   OnProgressChanged(0);   bool done = false;   while(!CancellationPending && !done)   {    EventCode statusCode = EventCode.None;    int hr = mediaEvent.WaitForCompletion(     PollFrequency, out statusCode);    switch(statusCode)    {     case EventCode.Complete:      done = true;      break;     case EventCode.None:       if (mediaSeeking != null)      {       ulong curPos = mediaSeeking.GetCurrentPosition();       ulong length = mediaSeeking.GetDuration();       double progress = curPos * 100.0 / (double)length;       if (progress > 0) OnProgressChanged(progress);      }      break;     default:      throw new DirectShowException(hr, null);    }   }   OnProgressChanged(100);  }  finally { mediaControl.Stop(); } }}

簡單吧?DirectShow 是一項令人驚訝的技術。這段代碼允許您將非 DRM/'d、NTSC、存儲在 DVR-MS 文件中的 SD 內容轉換成 WMV 文件。如果您檢查本文代碼下載中的文件,正如您將看到的,我已將此函數編碼到一個名爲Converter 的抽象基類中。一個派生類(在本例中爲 WmvConverter )構建合適的圖形,然後調用基類的RunGraph 方法。另外,Converter 還公開了可用於配置、監視和暫停圖形流程的屬性和事件,正如您在以下部分將看到的,Converter 公開了使調試圖形變得更加簡單的功能。


-- 作者:admin
-- 發佈時間:2005-11-26 3:19:00
--

調試篩選器圖形

您將在 RunGraph 方法中看到,圖形是在如下所示的 using 塊內部運行的:

using(new GraphPublisher(graphBuilder,  Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")){  ... // run the graph}

我這裏使用的 GraphPublisher 類是一個自定義類,它是我爲幫助調試圖形而編寫的。它有兩個用途。第一,如果在 GraphPublisher 的構造函數的第二個參數中指定了一個文件路徑,則它會將 graphBuilder 對象所表示的圖形保存到該文件中(該文件應該使用 .grf 擴展名)。隨後 GraphEdit 可以打開此文件,從而讓您查看整個圖形,如同它在發佈時出現的樣子。這個功能可以通過篩選器圖形管理器的IPersistStream 接口實現來使用:

private const ulong STGM_CREATE = 0x00001000L;private const ulong STGM_TRANSACTED = 0x00010000L;private const ulong STGM_WRITE = 0x00000001L;private const ulong STGM_READWRITE = 0x00000002L;private const ulong STGM_SHARE_EXCLUSIVE = 0x00000010L;[DllImport("ole32.dll", PreserveSig=false)]private static extern IStorage StgCreateDocfile(  [MarshalAs(UnmanagedType.LPWStr)]string pwcsName,   [In] uint grfMode, [In] uint reserved);private static void SaveGraphToFile(IGraphBuilder graph, string path){  using(DisposalCleanup dc = new DisposalCleanup())  {    string streamName = "ActiveMovieGraph";    IPersistStream ps = (IPersistStream)graph;    IStorage graphStorage = StgCreateDocfile(path,      (uint)(STGM_CREATE | STGM_TRANSACTED |       STGM_READWRITE | STGM_SHARE_EXCLUSIVE), 0);    dc.Add(graphStorage);    UCOMIStream stream = graphStorage.CreateStream(      streamName, (uint)(STGM_WRITE | STGM_CREATE |       STGM_SHARE_EXCLUSIVE), 0, 0);    dc.Add(stream);    ps.Save(stream, true);    graphStorage.Commit(0);  }}

然而,GraphPublisher 的主要目的和它在 using 塊中使用的原因是將實時圖形發佈到 GraphEdit。GraphEdit 允許您連接到另一個流程所公開的遠程圖形,只要該圖形已經發布到運行中對象表 (ROT) — 一個用作跟蹤運行對象的全局可訪問的查找表。GraphEdit 不僅可以讓您在另一個流程中查看和檢查一個實時篩選器圖形,它還常常允許您對其加以控制。

該圖形發佈到 ROT 是使用以下代碼完成的:

private class RunningObjectTableCookie : IDisposable{  private int _value;  private bool _valid;  internal RunningObjectTableCookie(int value)  {    _value = value;    _valid = true;  }  ~RunningObjectTableCookie() { Dispose(false); }  public void Dispose()  {    GC.SuppressFinalize(this);    Dispose(true);  }  private void Dispose(bool disposing)  {    if (_valid)    {      RemoveGraphFromRot(this);      _valid = false;      _value = -1;    }  }  internal bool IsValid   {     get { return _valid; } set { _valid = value; }   }}private static RunningObjectTableCookie AddGraphToRot(  IGraphBuilder graph){  if (graph == null) throw new ArgumentNullException("graph");  UCOMIRunningObjectTable rot = null;  UCOMIMoniker moniker = null;  try   {    // Get the ROT    rot = GetRunningObjectTable(0);    // Create a moniker for the graph    int pid;    using(Process p = Process.GetCurrentProcess()) pid = p.Id;    IntPtr unkPtr = Marshal.GetIUnknownForObject(graph);    string item = string.Format("FilterGraph {0} pid {1}",       ((int)unkPtr).ToString("x8"), pid.ToString("x8"));    Marshal.Release(unkPtr);    moniker = CreateItemMoniker("!", item);        // Registers the graph in the running object table    int cookieValue;    rot.Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, graph,       moniker, out cookieValue);    return new RunningObjectTableCookie(cookieValue);  }  finally  {    // Releases the COM objects    if (moniker != null)       while(Marshal.ReleaseComObject(moniker)>0);     if (rot != null) while(Marshal.ReleaseComObject(rot)>0);   }}private static void RemoveGraphFromRot(RunningObjectTableCookie cookie){  if (!cookie.IsValid) throw new ArgumentException("cookie");  UCOMIRunningObjectTable rot = null;  try   {    // Get the running object table and revoke the cookie    rot = GetRunningObjectTable(0);    rot.Revoke(cookie.Value);    cookie.IsValid = false;  }  finally  {    if (rot != null) while(Marshal.ReleaseComObject(rot)>0);   }}private const int ROTFLAGS_REGISTRATIONKEEPSALIVE  = 1;[DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]private static extern UCOMIRunningObjectTable GetRunningObjectTable(  int reserved);[DllImport("ole32.dll", CharSet=CharSet.Unicode,   ExactSpelling=true, PreserveSig=false)]private static extern UCOMIMoniker CreateItemMoniker(  [In] string lpszDelim, [In] string lpszItem);

在其構造函數中,GraphPublisher 使用 AddGraphToRot 將圖形添加到 ROT 中,並存儲產生的 cookie。在其IDisposable.Dispose 方法中,GraphPublisher 通過將存儲的 cookie 傳遞到RemoveGraphFromRot 來將圖形從 ROT 中刪除。


-- 作者:admin
-- 發佈時間:2005-11-26 3:21:00
--

非託管資源清理

當資源使用完畢後,儘早將它們釋放是非常重要的。當使用處理大量音頻和視頻資源的 DirectShow COM 對象時,這一點尤其重要。可以使用 Marshal.ReleaseComObject 方法來強制處置 COM 對象,此方法會減少所提供的運行時可調用包裝的引用計數。當引用數到達零時,運行庫會釋放它在非託管 COM 對象上的所有引用。(有關Marshal.ReleaseComObject 的更多信息,請參見該方法的 MSDN 文檔 。)對於使用的每個 COM 對象,我不是將我的代碼隨便放在 try/finally 塊中,而是創建一個名爲 DisposalCleanup 的助手類,它可以簡化 COM 對象的生存期管理:

public class DisposalCleanup : IDisposable{  private ArrayList _toDispose = new ArrayList();  public void Add(params object [] toDispose)  {    if (_toDispose == null)       throw new ObjectDisposedException(GetType().Name);    if (toDispose != null)    {      foreach(object obj in toDispose)      {        if (obj != null && (obj is IDisposable ||           obj.GetType().IsCOMObject || obj is IntPtr))        {          _toDispose.Add(obj);        }      }    }  }  void IDisposable.Dispose()  {    if (_toDispose != null)    {      foreach(object obj in _toDispose) EnsureCleanup(obj);      _toDispose = null;    }  }  private void EnsureCleanup(object toDispose)  {    if (toDispose is IDisposable)     {      ((IDisposable)toDispose).Dispose();    }    else if (toDispose is IntPtr) // IntPtrs must be interface ptrs    {      Marshal.Release((IntPtr)toDispose);    }    else if (toDispose.GetType().IsCOMObject)     {      while (Marshal.ReleaseComObject(toDispose) > 0);    }  }}

這裏一個重要的方法是 EnsureCleanup ,它是通過 DisposalCleanupIDisposable.Dispose 方法調用的。通過使用其 Add 方法來調用添加到 DisposalCleanup 中的每個對象,EnsureCleanup 調用了一個 IDisposable 對象上的Dispose 、一個 COM 對象上的 Marshal.ReleaseComObject 和一個接口指針上的Marshal.Release 。通過這些,我的代碼只需將使用許多 COM 對象的代碼塊放在一個創建了新的 DisposalCleanup 的 using 塊中,將任何 COM 對象或接口添加到DisposalCleanup 實例中,並在 using 塊結束時調用 DisposalCleanupIDisposable.Dispose 方法來釋放所有使用過的資源。我的 Converter 基類實現了此方案,並通過一個受保護的DisposalCleanup 屬性公開了構造的 DisposalCleanup

public object Convert() {   _cancellationPending = false;  try  {    object result;    using(_dc = new DisposalCleanup())    {      // Do the actual work      result = DoWork.();    }    OnConversionComplete(null, result);    return result;  }  catch(DirectShowException exc)  {    OnConversionComplete(exc, null);    throw;  }  catch(Exception exc)  {    exc = new DirectShowException(exc);    OnConversionComplete(exc, null);    throw exc;  }  catch  {    OnConversionComplete(new DirectShowException(), null);    throw;  }}private DisposalCleanup _dc;protected DisposalCleanup DisposalCleanup { get { return _dc; } }

DoWork. 方法是抽象方法,如果是 WmvConverter 類,它可以構建篩選器圖形並調用RunGraph 方法。通過這種方式,派生類可以實現 DoWork. 並簡單地向基類的 DisposalCleanup 中添加可處置的對象;當派生類的工作執行完畢後,即使它引發異常,基類也會自動處置這些資源。


-- 作者:admin
-- 發佈時間:2005-11-26 3:21:00
--

將 WmvConverter 投入使用: WmvTranscoderPlugin

顯而易見,通過前面討論的代碼,您可以編寫功能豐富的應用程序來處理 DVR-MS 文件並將其轉換成 WMV 文件。但據我所見,此功能最常見的請求是作爲 Media Center-集成解決方案的一部分。由此創建了許多非常有用的解決方案,其中最著名的有 Dan Giambalvo 創建的 dCut (可通過http://www.inseattle.org/~dan/Dcut.htm 下載)以及 Alex Seigler、José Peña、James Edelen 和 Jeff Griffin 創建的 DVR 2 WMV(可通過http://www.thegreenbutton.com/downloads.aspx 下載)。這兩個應用程序都依賴於 Alex Siegler 編寫的 dvr2wmv DLL(使用的技術與本文所介紹的非常類似,不過採用的是非託管代碼)。這些應用程序不懈努力地嘗試集成到 Media Center 中,更具體地說是模仿 Media Center 外殼的外觀,但遺憾的是,目前的 Media Center SDK 只允許做到這麼多。幸運的是,SDK 有另一個相對未開發的區域,它使這種功能可以輕鬆地集成到 Media Center UI 中,但仍然保留 Media Center 團隊已編寫的所有烙印:ListMaker 外接程序。

ListMaker 外接程序是由第三方提供的託管組件,它運行在 Media Center 進程內,使用 Microsoft.MediaCenter.dll 程序集公開的 API 元素(您可以在 Media Center 系統的 %windir%//ehome 目錄下找到此 DLL)。ListMaker 外接程序的工作非常簡單:它的目的是獲取 Media Center 提供給它的文件列表,並對該列表進行一些操作(進行什麼操作取決於該外接程序)。Media Center 已將它構建到 UI 中以處理列表生成和隨外接程序處理列表時的報告而顯示的進程更新。很酷的一點是 Media Center 並不在意該外接程序對媒體列表進行了什麼操作。因此,您可以編寫這樣一個外接程序,它將用戶選定的每個 DVR-MS 文件轉換成 WMV,並將它們寫到硬盤的一個文件夾中。更明確地說,我擁有這樣的外接程序(圖 9),下面我將向您介紹如何實現。

圖 9. WMV Transcoder 外接程序

首先,ListMaker 外接程序必須從 System.MarshalByRefObject 派生,如同所有用於 Media Center 的外接程序那樣(遺憾的是,SDK 文檔目前沒有提到這一點,但是這一點非常重要)。Media Center 將所有外接程序加載到一個獨立的應用程序域中,這意味着它使用 .NET Remoting 基礎結構跨應用程序域邊界訪問該外接程序。MarshalByRefObject 類能實現這一目的,它允許跨應用程序域邊界訪問對象,因此外接程序必須以它爲基類。如果您忘記從MarshalByRefObject 派生,則您的外接程序將無法正確加載或運行。

除了從 MarshalByRefObject 派生外,ListMaker 外接程序還實現了兩個來自 Microsoft.MediaCenter.dll 程序集的主要接口:Microsoft.MediaCenter.AddIn.IAddInModuleMicrosoft.MediaCenter.AddIn.ListMaker.ListMaker

public class WmvTranscoderPlugin : MarshalByRefObject,   IAddInModule, ListMakerApp, IBrandInfo{  ...}

所有 Media Center 外接程序都實現了 IAddInModuleIAddInModule 通過實現IAddInModule.InitializeIAddInModule.Uninitialize 方法來初始化和處置要運行的代碼。在許多情況下,初始化階段需要做的事情非常少;對於我的外接程序,我只需查看一下註冊表,找到用戶首選項,例如經過編碼轉換的文件應該寫到哪個磁盤(註冊表中 HKLM//Software//Toub//WmvTranscoderPlugin 項的 PreferredDrive 值)以及應該使用哪個 Windows Media 配置文件來將代碼轉換爲 WMV(註冊表中的 HKLM//Software//Toub//WmvTranscoderPlugin 項的 ProfilePath 值)。如果沒有指定驅動器(或者指定的驅動器無效),則我將默認值設置爲從System.IO.Directory.GetLogicalDrives 返回的第一個有效的驅動器,其中,有效的驅動器定義爲 Win32GetDriveType 函數聲明的固定驅動器中的任何一個驅動器。

ListMakerApp 是列表的主要接口,用於處理和服務雙重目的:允許用戶選擇要處理的媒體文件集(圖 10)並啓動外接程序的處理,在這之後它允許 Media Center UI 報告進度(圖 11)。

圖 10. 選擇要進行編碼轉換的節目

圖 11. Media Center 外殼中的進度更新

前者涉及的成員並不令人非常滿意,所以我不想花太多時間介紹它們。從根本上說,Media Center 通過此接口調用外接程序以獲取如選擇多少 DVR-MS 文件、還能添加多少文件之類的信息,並在每次用戶更改要處理的列表項時調用它。它的核心部分是由三個方法處理的:

public void ItemAdded(ListMakerItem item){  _itemsUsed++;  _bytesUsed += item.ByteSize;  _timeUsed += item.Duration;}public void ItemRemoved(ListMakerItem item){  _itemsUsed--;  _bytesUsed -= item.ByteSize;  _timeUsed -= item.Duration;}public void RemoveAllItems(){  _itemsUsed = 0;  _bytesUsed = 0;  _timeUsed = TimeSpan.FromSeconds(0);}

然後通過其他屬性和方法(如下所示)公開捕獲的信息:

public TimeSpan TimeUsed { get { return _timeUsed; } }public int ItemUsed { get { return _itemsUsed; } }public long ByteUsed { get { return _bytesUsed; } }public TimeSpan TimeCapacity { get { return TimeSpan.MaxValue; } } public int ItemCapacity { get { return int.MaxValue; } } public long ByteCapacity {   get { return (long)GetFreeSpace(_selectedDrive); } }

Used 方法只是返回上述方法所維護的計數值。TimeCapacity ItemCapacity 屬性同時返回其類型各自的 MaxValue 值,因爲計算實際用時和實際可用的項數遠遠超出了本文的討論範圍。ByteCapacity 使用我的私有GetFreeSpace 方法(再次說明,它只是 Win32 GetDiskFreeSpaceEx 函數的一個 p/invoke 包裝)來返回磁盤中的可用空間;當然,在與ByteUsed 配合時這個值也沒有什麼用處,因爲 ByteUsed 表示的是 DVR-MS 文件的大小,而ByteCapacity 則用於確定磁盤中是否有空間來存放這些文件,但輸出文件卻是壓縮過的 WMV 文件。不過這個實現細節您應該能夠自如地進行更改。

我還將介紹三個更加重要但實現很簡單的屬性:

public MediaType SupportedMediaTypes {   get { return MediaType.RecordedTV; } } public bool OrderIsImportant { get { return true; } }public IBrandInfo BrandInfo { get { return this; } }

SupportedMediaTypes 返回一個加標記的枚舉,列出此外接程序支持的媒體類型:可能的類型包括圖片、視頻、音樂和錄製的電視等,Media Center 通常支持所有這些媒體類型。然而,由於此外接程序的主要作用是將 DVR-MS 文件轉換成 WMV 文件,因此我將其實現爲只從SupportedMediaTypes 返回 MediaType.RecordedTV

Media Center 使用 OrderIsImportant 來確定是否應該允許用戶對要處理的錄製節目列表重排序。雖然順序對此外接程序來說並不是真的很重要(因爲它只是將文件寫到硬盤中),但我想讓用戶安排某些特定節目在其他節目之前轉換(圖 12),所以我從這個屬性返回 true 而不是 false。

圖 12. 對選定的節目重排序

BrandInfo 屬性允許外接程序的作者修改 Media Center 顯示的 UI 以便包含特定於產品的信息。該屬性返回一個實現IBrandInfo 接口的對象。爲簡單起見,我只在我的外接程序中實現該接口並返回對該外接程序對象自身的引用:

public class WmvTranscoderPlugin : MarshalByRefObject,   IAddInModule, ListMakerApp, IBrandInfo{  ...  public IBrandInfo BrandInfo { get { return this; } }  ...  public string ViewListPageTitle { get { return "Files to transcode"; } }  public string SaveListButtonTitle { get { return "Transcode"; } }  public string PageTitle { get { return "Transcode to WMV"; } }  public string CreatePageTitle { get { return "Specify target folder"; } }  public string ViewListButtonTitle { get { return "View List"; } }  public string ViewListIcon { get { return null; } }  public string MainIcon { get { return null; } }  public string StatusBarIcon { get { return null; } }  ...}

IBrandInfo 的八個屬性被分成兩類:呈現在 UI 中的文本字符串和指定磁盤中圖形位置的路徑字符串。如果一個屬性返回 null,則使用默認值。這樣,由於我現在的圖形藝術水平還有些欠缺,因此對所有圖標屬性我都返回 null。這些屬性在 UI 中出現的位置如下表所示:


-- 作者:admin
-- 發佈時間:2005-11-26 3:22:00
--

屬性
描述

PageTitle

當外接程序使用時顯示在右上角的文本。

CreatePageTitle

列表創建頁面的標題文本。

SaveListButtonTitle

用於在列表創建之後啓動處理操作的按鈕上的文本。

ViewListButtonTitle

用於查看要複製以進行處理的媒體項的按鈕上的文本。

ViewListPageTitle

列表查看頁面的標題文本。

MainIcon

包含要作爲列表生成頁面上主圖標(水印)使用的圖標的文件路徑。

StatusBarIcon

包含 Media Center 放在生成頁面左下角的圖標的文件路徑。

ViewListIcon

Media Center 放在列表查看頁面頂部的圖標文件的路徑。


-- 作者:admin
-- 發佈時間:2005-11-26 3:23:00
--

ListMakerApp 上最有趣的方法是 LaunchCancel 。一旦用戶創建了要處理的文件列表並單擊按鈕開始處理,Media Center 就會調用Launch 方法,它提供三個參數:用戶選擇的錄製節目列表、可被調用以通知 Media Center 狀態更新的進程更新委託和應該調用以通知 Media Center 處理完成(成功或因某種異常情況)的完成委託。Launch 方法的作用是立即返回並在後臺線程中執行實際的工作。當用戶選擇取消處理時就會調用Cancel 方法,然後由外接程序停止和終止其操作。

WmvTranscoderPlugin 的實現遵循這種模式:將 Launch 的參數存儲到成員變量中,然後將執行實際轉換工作的ConvertToWmv 方法排入 ThreadPool 隊列中:

public void Launch(ListMakerList lml, ProgressChangedEventHandler pce,   CompletionEventHandler ce){  _listMakerList = lml;  _progressChangedHandler = pce;  _completedHandler = ce;  _cancellationPending = false;  ThreadPool.QueueUserWorkItem(new WaitCallback(ConvertToWmv), null);}private void ConvertToWmv(object ignored){  ThreadPriority oldThreadPriority = Thread.CurrentThread.Priority;  Thread.CurrentThread.Priority = ThreadPriority.Lowest;  try  {    DirectoryInfo outDir = Directory.CreateDirectory(      _selectedDrive + ":////" + _listMakerList.ListTitle);    _currentConvertingIndex = 0;    foreach(ListMakerItem item in _listMakerList.Items)    {      if (_cancellationPending) break;      string dvrMsName = item.Filename;      string wmvName = outDir.FullName + "////" +         item.Name + ".wmv";      _currentConverter = new WmvConverter(        dvrMsName, wmvName, _profilePath);      _priorCompletedPercentage = _currentConvertingIndex /         (float)_listMakerList.Count;      _currentConverter.PollFrequency = 2000;      _currentConverter.ProgressChanged +=         new ProgressChangedEventHandler(ReportChange);      _currentConverter.Convert();      _currentConverter = null;      _currentConvertingIndex++;    }    _completedHandler(this, new CompletionEventArgs());  }   catch(Exception exc)  {    _completedHandler(this, new CompletionEventArgs(exc));  }  finally  {    Thread.CurrentThread.Priority = oldThreadPriority;  }}

ConvertToWmv 在選定的驅動器上創建一個目錄,使用用戶指定的目標文件夾的名稱(參見圖 13)。然後該方法循環訪問所提供的ListMakerList 中的所有 ListMakerItem 對象,獲取 DVR-MS 文件的路徑並使用前面構建的WmvConverter 來將目標目錄中的每個 DVR-MS 文件轉換成 WMV 文件。ConverterProgressChanged 事件關聯到外接程序中的一個私有方法 — ReportChange 上,然後由該方法調用 Media Center 的進程更新委託。另外,當前轉換程序存儲在一個成員變量中,因而可以使用Cancel 方法來停止其進程


-- 作者:admin
-- 發佈時間:2005-11-26 3:23:00
--

Cancel 方法也非常簡單。它設置了一個成員變量,用於警告在另一個線程中運行的 ConvertToWmv 方法,通知它用戶已經請求取消。然而,正如您在ConvertToWmv 方法中看到的,只有當該方法準備轉換下一個 DVR-MS 文件時纔會對此進行檢查,所以 Cancel 方法還使用存儲在一個成員變量中的 WmvConverter 對象,使用該 ConverterCancelAsync 方法取消當前執行的轉換。正如我們前面所看到的,這將導致 Converter.RunGraph 方法從WaitForCompletion 方法返回後即刻停止。

public void Cancel(){  // Cancel any pending conversions  _cancellationPending = true;  // Cancel the current conversion  WmvConverter converter = _currentConverter;  if (converter != null) converter.CancelAsync();}

我在本文的下載中包含了此外接程序的一個完整的工作實現,包括一個安裝程序。該安裝程序同時將 WmvTranscoderPlugin 的程序集和WmvConverter 的程序集安裝到全局程序集緩存 (GAC) 中,然後使用 RegisterMceApp.exe 工具來將此外接程序通知 Media Center。註冊應用程序依賴於一個 XML 配置文件,如下所示:

        

您應該能夠運行安裝程序並直接通過一個我們都不必編寫的非常時髦的 UI 來將 DVR-MS 立即轉換成 WMV。(感謝你,Media Center 團隊!)

圖 14. 成功的編碼轉換

返回頁首

訪問 DVR-MS 元數據

DVR-MS 文件格式既包含音頻、視頻和閉合字幕數據,也包含描述文件及其內容的元數據。一旦電視節目錄制下來,節目的標題、描述、演員表和原始播放日期等信息就存儲在這個位置。很酷的一點是,您的應用程序可以通過 DirectShowStreamBufferRecordingAttribute 對象實現的 IStreamBufferRecordingAttribute 接口輕鬆地訪問此數據。這個對象可以使用它的 CLSID 來創建,正如我本文中創建其他 DirectShow 對象那樣。

要使用 IStreamBufferRecordingAttribute ,首先必須爲它提供一個託管接口(您會在本文的代碼下載中發現這段代碼,它嵌套在DvrmsMetadataEditor 類中):


-- 作者:admin
-- 發佈時間:2005-11-26 3:24:00
--

[ComImport][Guid("16CA4E03-FE69-4705-BD41-5B7DFC0C95F3")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]private interface IStreamBufferRecordingAttribute{  void SetAttribute(    [In] uint ulReserved,     [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,    [In] MetadataItemType StreamBufferAttributeType,    [In, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,    [In] ushort cbAttributeLength);  ushort GetAttributeCount([In] uint ulReserved);  void GetAttributeByName(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,    [In] ref uint pulReserved,    [Out] out MetadataItemType pStreamBufferAttributeType,    [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbAttribute,    [In, Out] ref ushort pcbLength);  void GetAttributeByIndex (    [In] ushort wIndex,    [In, Out] ref uint pulReserved,    [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszAttributeName,    [In, Out] ref ushort pcchNameLength,    [Out] out MetadataItemType pStreamBufferAttributeType,    [Out, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,    [In, Out] ref ushort pcbLength);  [return: MarshalAs(UnmanagedType.Interface)]  object EnumAttributes();}

爲了訪問 DVR-MS 文件的元數據,我構造了一個 StreamBufferRecordingAttribute 對象並獲取它的IFileSourceFilter 接口(您在本文前面也看到了相應的 IFileSinkFilter 接口;它們幾乎完全相同)。IFileSourceFilterLoad 方法可用於打開我對其元數據感興趣的 DVR-MS 文件,此時可以獲取它的 IStreamBufferRecordingAttribute 接口並將該接口用於檢索和編輯元數據:

public class DvrmsMetadataEditor : MetadataEditor{  IStreamBufferRecordingAttribute _editor;  public DvrmsMetadataEditor(string filepath)  {    IFileSourceFilter sourceFilter = (IFileSourceFilter)      ClassId.CoCreateInstance(ClassId.RecordingAttributes);    sourceFilter.Load(filepath, null);    _editor = (IStreamBufferRecordingAttribute)sourceFilter;  }  ...}

對元數據的讀訪問是通過 DvrmsMetadataEditor.GetAttributes 方法提供的,該方法提供了 IStreamBufferRecordingAttributeGetAttributeCount GetAttributeByIndex 方法的簡單抽象。

public override System.Collections.IDictionary GetAttributes(){  if (_editor == null)     throw new ObjectDisposedException(GetType().Name);  Hashtable propsRetrieved = new Hashtable();  ushort attributeCount = _editor.GetAttributeCount(0);  for(ushort i = 0; i < attributeCount; i++)  {    MetadataItemType attributeType;    StringBuilder attributeName = null;    byte[] attributeValue = null;    ushort attributeNameLength = 0;    ushort attributeValueLength = 0;    uint reserved = 0;    _editor.GetAttributeByIndex(i, ref reserved, attributeName,       ref attributeNameLength, out attributeType,       attributeValue, ref attributeValueLength);    attributeName = new StringBuilder(attributeNameLength);    attributeValue = new byte[attributeValueLength];    _editor.GetAttributeByIndex(i, ref reserved, attributeName,       ref attributeNameLength, out attributeType,       attributeValue, ref attributeValueLength);    if (attributeName != null && attributeName.Length > 0)    {      object val = ParseAttributeValue(        attributeType, attributeValue);      string key = attributeName.ToString().TrimEnd(/'//0/');      propsRetrieved[key] = new MetadataItem(        key, val, attributeType);    }  }  return propsRetrieved;}

首先,使用 GetAttributeCount 方法來查明要檢索的元數據項有多少。然後,對於每個屬性,使用 GetAttributeByIndex 方法檢索屬性名的長度和值的長度(以字節爲單位)(通過將 name 和 value 參數指定爲空值)。當獲得長度之後,我就可以創建大小適當的緩衝區來存儲數據,並且可以再次調用GetAttributeByIndex 來檢索屬性的真實名稱和字節數組值。如果檢索成功,則會根據屬性的類型將存儲該值的字節數組解析爲適當的託管對象。我的ParseAttributeValue 方法返回 GUID、無符號整型、無符號長整型、無符號短整型、字符串、布爾值或者原始數組(如果值是簡單的二進制),這對大多數複雜的元數據屬性都是通用的。然後使用該屬性的名稱及其類型和值構造一個新的MetadataItem 實例,這個實例將添加到該文件的所有屬性的 Hashtable 中。當所有屬性都檢索完畢時,此集合將返回給用戶。

SetAttributes 方法的工作方式則相反。它是隨 MetadataItem 對象集合提供的,其中每個對象都根據其類型格式化爲適當的字節數組,然後與SetAttribute 方法一起使用,以便設置文件的元數據屬性:

public override void SetAttributes(IDictionary propsToSet){  if (_editor == null)     throw new ObjectDisposedException(GetType().Name);  if (propsToSet == null)     throw new ArgumentNullException("propsToSet");  byte [] attributeValueBytes;  foreach(DictionaryEntry entry in propsToSet)  {    MetadataItem item = (MetadataItem)entry.Value;    if (TranslateAttributeToByteArray(      item, out attributeValueBytes))    {      try      {        _editor.SetAttribute(0, item.Name,           item.Type, attributeValueBytes,           (ushort)attributeValueBytes.Length);      }      catch(ArgumentException){}      catch(COMException){}    }  }}

MetadataItem 是一個屬性的名稱、值和類型的簡單包裝。MetadataItemType 是有效類型(GUID、字符串、無符號整型等)的枚舉。


-- 作者:admin
-- 發佈時間:2005-11-26 3:24:00
--

您可能注意到 DvrmsMetadataEditor 類是從 MetadataEditor 基類派生的。我這樣做是爲了提供另一個類 —AsfMetadataEditor ,它也是從 MetadataEditor 派生的。AsfMetadataEditor 基於包含在 Windows Media Format SDK(從此處下載 SDK )中的示例代碼。它使用 Windows Media IWMMetadataEditorIWMHeaderInfo3 接口來獲取 WMA 和 WMV 文件(這兩者都基於 ASF 文件格式)的相關元數據信息。您可能會發現,當前這些 Windows Media Format SDK 接口除了能用於處理 WMA 和 WMV 文件外,還可以處理 DVR-MS 文件,不過將來可能不再這樣,而且 Microsoft 強烈建議使用IStreamBufferRecordingAttribute 接口來處理 DVR-MS 文件。IWMHeaderInfo3 接口的相關部分與IStreamBufferRecordingAttribute 接口幾乎相同,因此 AsfMetadataEditor 類和DvrmsMetadataEditor 類也極其相似。

在這些類就位後,將元數據從一個媒體文件複製到另一個(例如從 DVR-MS 文件複製到經過代碼轉換的 WMV 文件)就變得極爲簡單,從而讓您保持與經過編碼轉換的 TV 錄製相關聯的元數據的保真度:

using(MetadataEditor sourceEditor = new DvrmsMetadataEditor(srcPath)){  using(MetadataEditor destEditor = new AsfMetadataEditor(dstPath))  {    destEditor.SetAttributes(sourceEditor.GetAttributes());  }}

實際上,正是出於從一個媒體文件向另一個媒體文件複製元數據的目的,我在 MetadataEditor 類中創建了一個靜態的 MigrateMetdata 方法,這個方法不僅能按上述方式遷移元數據,而且對它加以擴大,這樣在 Media Player 中查看 DVR-MS 文件和在 Media Center 中播放 WMV 文件時,就可以顯示更多的可用信息。

返回頁首

編輯 DVR-MS 文件

除了轉換爲 WMV 之外,編輯和拼接 DVR-MS 文件可能是我在網上新聞組中看到的第二個最常請求的功能。許多人沒有意識到的是,DirectShow RecComp 對象及其 IStreamBufferRecComp 接口提供了現成的拼接功能。IStreamBufferRecComp 接口用於從現有的錄製片段創建新的錄製,以及將來自一個或多個 DVR-MS 文件的片段連接在一起。

IStreamBufferRecComp 接口非常簡單,它的一個 C# 導入如下所示:

[ComImport][Guid("9E259A9B-8815-42ae-B09F-221970B154FD")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IStreamBufferRecComp{  void Initialize(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszTargetFilename,     [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecProfileRef);  void Append(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording);  void AppendEx(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording,    [In] ulong rtStart, [In] ulong rtStop);  uint GetCurrentLength();  void Close();  void Cancel();}

要拼接 DVR-MS 文件,首先要創建 RecComp 對象的實例。這可以通過本文前面介紹的 ClassId.CoCreateInstance 方法來完成,代碼如下:

IStreamBufferRecComp recCom =   (IStreamBufferRecComp)ClassId.CoCreateInstance(ClassId.RecComp)and with ClassId.RecComp defined aspublic static readonly Guid RecComp =   new Guid("D682C4BA-A90A-42FE-B9E1-03109849C423");

有了 IStreamBufferRecComp 之後,就可以使用它的 Initialize 方法來爲新的錄製指定輸出文件名。另外,Initialize 的第二個參數應該是要拼接的其中一個 DVR-MS 輸入文件的文件路徑。IStreamBufferRecComp 支持連接來自一個或多個文件的片段,但所有這些文件必須使用相同的配置文件錄製,這意味着它們必須使用 Media Center 中的相同配置和設置進行錄製。RecComp 需要知道輸出文件使用什麼配置文件,因此您必須指定一個輸入文件作爲第二個參數,以便它可以檢查其配置文件信息並將該信息作爲輸出文件的基礎。

一旦初始化了 IStreamBufferRecComp ,您就可以開始構建新文件。調用 Append 方法,指定一個 DVR-MS 輸入文件的完整路徑,則整個文件就會追加到輸出文件中。AppendEx 方法允許您指定附加的開始和停止時間,以便只使用輸入文件的一部分並將這部分追加到輸出文件中。在非託管接口中,這些時間被定義爲REFERENCE_TIME — 一個代表以 100 毫微秒爲單位的數值的 64 位長整數值,所以在託管代碼中,您可以使用如下所示的函數來將秒轉換成傳遞給AppendEx REFERENCE_TIME 值:

internal static ulong SecondsToHundredNanoseconds(double seconds){  return (ulong)(seconds * 10000000);}

當您完成追加到輸出文件時,Close 方法就會關閉輸出文件。在您連接到該文件時,可以使用一個單獨線程的 GetCurrentLength 方法來確定輸出文件的當前長度。然後您可以使用此信息和您對輸入文件/片段長度的瞭解來計算完成拼接的百分比。請注意,這個過程非常快,因爲將片段從一個 DVR-MS 文件追加到另一個文件並不需要編碼和解碼。


-- 作者:admin
-- 發佈時間:2005-11-26 3:25:00
--

爲了演示此接口,我構建了 DVR-MS 編輯器應用程序(如圖 15 所示),並將它作爲與本文有關的代碼下載的一部分。

圖 15. DVR-MS 編輯器

這個應用程序其實非常簡單,用了一個多小時就實現了。它使用 Windows Media Player ActiveX 控件來顯示輸入的視頻文件。爲了加載視頻文件,它將AxWindowsMediaPlayer.URL 屬性設置爲 DVR-MS 文件的路徑,這樣可以使 Media Player 加載該視頻(如果AxWindowsMediaPlayer.settings.autoStart 屬性爲真,它還會開始播放)。

一旦加載了視頻,用戶就可以使用“Media Player”工具欄對它進行控制,這個工具欄可以使用戶完全控制視頻的播放和搜索。當到達用戶想要開始或停止一段視頻的位置時,就會查詢AxWindowsMediaPlayer.Ctlcontrols.currentPosition 屬性。然後,剛纔描述的 IStreamBufferRecComp 接口可以使用這些時間來創建輸出文件。

另外,Media Player 對視頻的當前位置提供了細粒度的編程控制。您可以使用如下所示的代碼來逐幀移動視頻:

((WMPLib.IWMPControls2)player.Ctlcontrols).step(1);

或者,還可以通過設置剛纔討論的 AxWindowsMediaPlayer.Ctlcontrols.currentPosition 來跳轉到視頻中的特定位置。

DVR-MS 編輯器應用程序還利用了本文前面描述的一些其他技術,例如將元數據從源視頻文件複製到輸出視頻文件。

返回頁首

小結

這是令人驚訝的技術,不是嗎?DirectShow 和 Windows XP Media Center Edition 團隊爲開發人員提供了許多處理 DVR-MS 文件的工具(包括非託管代碼的和託管代碼的)。通過使用這些工具,可以創建新的應用程序來提供大多數人沒有意識到他們能夠使用的真正強大的功能。本文所討論的主題只涉及到您可以用來處理 DVR-MS 文件的各種技術的一部分,而在人們編寫的使用這些庫和工具的解決方案中,它們所佔的比例則甚至更小。我期待着獲悉您使用這種功能來開發解決方案。

現在,我要回去看會電視了。

返回頁首

相關書籍

Programming Microsoft DirectShow for Digital Video and Television (Microsoft Press, 2003)

Fundamentals of Audio and Video Programming for Games (Microsoft Press, 2003)

返回頁首

致謝

我衷心感謝 Matthijs Gates、Aaron DeYonker、Ryan D/'Aurelio、Ethan Zoller、Eric Gunnerson 和 Alex Seigler 提供他們的研究領域的專家見解,感謝 ABC 允許我使用來自他們電視節目的示例和屏幕快照,也要感謝我的好朋友 John Keefe 和 Eden Riegel,感謝他們允許我在本文中使用他們的肖像。

關於作者

Stephen Toub 是 MSDN Magazine 的技術編輯,他還爲該雜誌撰寫 .NET Matters 專欄。


< type="text/javascript"> //鼠標雙擊自動滾屏 var currentpos,timer; function initialize() {timer=setInterval("scrollwindow()",10);} function sc(){clearInterval(timer); } function scrollwindow() {currentpos=document.body.scrollTop; window.scroll(0,++currentpos); if (currentpos != document.body.scrollTop) sc();} document.onmousedown=sc document.ondblclick=initialize function New(para_URL) {var URL =new String(para_URL) window.open(URL,'','resizable,scrollbars')}

Copyright ©2002 - 2005 learnsky
執行時間:3,921.87500 毫秒。查詢數據庫3次。

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