014 Directshow中的視頻捕捉

Post date: 2015/4/9 上午 01:28:23

本篇文檔主要描述關於用Directshow進行視頻開發的一些技術 主要包括下面內容 1關於視頻捕捉(About Video Capture in Dshow) 2選擇一個視頻捕捉設備(Select capture device) 3預覽視頻(Previewing Video) 4如何捕捉視頻流並保存到文件(Capture video to File) 5將設備從系統中移走時的事件通知(Device remove Notify) 6如何控制Capture Graph(Controlling Capture Graph) 7如何配置一個視頻捕捉設備 8從靜止圖像pin中捕捉圖片 1關於視頻捕捉(About Video Capture in Dshow) 1視頻捕捉Graph的構建 一個能夠捕捉音頻或者視頻的graph圖都稱之為捕捉graph圖。 捕捉graph圖比一般的文件回放graph圖要複雜許多, dshow提供了一個Capture Graph Builder COM組件使得捕捉graph圖的生成更加簡單。 Capture Graph Builder提供了一個ICaptureGraphBuilder2接口, 這個接口提供了一些方法用來構建和控制捕捉graph。 首先創建一個Capture Graph Builder對像和一個graph manger對象, 然後用filter graph manager 作參數, 調用ICaptureGraphBuilder2::SetFiltergraph來初始化Capture Graph Builder。 看下面的代碼把 HRESULT InitCaptureGraphBuilder( IGraphBuilder **ppGraph, // Receives the pointer. ICaptureGraphBuilder2 **ppBuild // Receives the pointer. ) { if (!ppGraph || !ppBuild) { return E_POINTER; } IGraphBuilder *pGraph = NULL; ICaptureGraphBuilder2 *pBuild = NULL; // Create the Capture Graph Builder. HRESULT hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)&pGraph); if (SUCCEEDED(hr)) { // Create the Filter Graph Manager. hr = CoCreateInstance(CLSID_FilterGraph, 0, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)&pGraph); if (SUCCEEDED(hr)) { // Initialize the Capture Graph Builder. pBuild->SetFiltergraph(pGraph); // Return both interface pointers to the caller. *ppBuild = pBuild; *ppGraph = pGraph; // The caller must release both interfaces. return S_OK; } else { pBuild->Release(); } } return hr; // Failed } 2視頻捕捉的設備 現在許多新的視頻捕捉設備都採用的是WDM驅動方法, 在WDM機制中, 微軟提供了一個獨立於硬件設備的驅動, 稱為類驅動程序。 驅動程序的供應商提供的驅動程序稱為minidrivers。 Minidrivers提供了直接和硬件打交道的函數, 在這些函數中調用了類驅動。 在directshow的filter圖表中, 任何一個WDM捕捉設備都是做為一個WDM Video Capture 過濾器(Filter)出現。 WDM Video Capture過濾器根據驅動程序的特徵構建自己的filter 下面是陸其明的一篇有關於dshow和硬件的文章, 可以拿來參考一下 //陸文章開始 大家知道, 為了提高系統的穩定性, Windows操作系統對硬件操作進行了隔離;應用程序一般不能直接訪問硬件。 DirectShow Filter工作在用戶模式(User mode, 操作系統特權級別為Ring 3), 而硬件工作在內核模式(Kernel mode, 操作系統特權級別為Ring 0), 那麼它們之間怎麼協同工作呢? DirectShow解決的方法是, 為這些硬件設計包裝Filter;這種Filter能夠工作在用戶模式下, 外觀、控制方法跟普通Filter一樣, 而包裝Filter內部完成與硬件驅動程序的交互。 這樣的設計, 使得編寫DirectShow應用程序的開發人員, 從為支持硬件而需做出的特殊處理中解脫出來。 DirectShow已經集成的包裝Filter, 包括Audio Capture Filter(qcap.dll)、 VfW Capture Filter(qcap.dll, Filter的Class Id為CLSID_VfwCapture)、TV Tuner Filter(KSTVTune.ax, Filter的Class Id為CLSID_CTVTunerFilter)、 Analog Video Crossbar Filter(ksxbar.ax)、TV Audio Filter(Filter的Class Id為CLSID_TVAudioFilter)等;另外, DirectShow為採用WDM驅動程序的硬件設計了KsProxy Filter(Ksproxy.ax,)。 我們來看一下結構圖: Directshow中的視頻捕捉 圖1 從上圖中, 我們可以看出, Ksproxy.ax、Kstune.ax、Ksxbar.ax這些包裝Filter跟其它普通的DirectShow Filter處於同一個級別, 可以協同工作;用戶模式下的Filter通過Stream Class控制硬件的驅動程序minidriver(由硬件廠商提供的實現對硬件控制功能的DLL);Stream Class和minidriver一起向上層提供系統底層級別的服務。 值得注意的是, 這裏的Stream Class是一種驅動模型, 它負責調用硬件的minidriver;另外, Stream Class的功能還在於協調minidriver之間的工作, 使得一些數據可以直接在Kernel mode下從一個硬件傳輸到另一個硬件(或同一個硬件上的不同功能模塊), 提高了系統的工作效率。 (更多的關於底層驅動程序的細節, 請讀者參閱Windows DDK。 ) 下面, 我們分別來看一下幾種常見的硬件。 VfW視頻採集卡。 這類硬件在市場上已經處於一種淘汰的趨勢;新生產的視頻採集卡一般採用WDM驅動模型。 但是, DirectShow為了保持向後兼容, 還是專門提供了一個包裝Filter支持這種硬件。 和其他硬件的包裝Filter一樣, 這種包裝Filter的創建不是像普通Filter一樣使用CoCreateInstance, 而要通過系統枚舉, 然後BindToObject。 音頻採集卡(聲卡)。 聲卡的採集功能也是通過包裝Filter來實現的;而且現在的聲卡大部分都有混音的功能。 這個Filter一般有幾個Input pin, 每個pin都代表一個輸入, 如Line In、Microphone、CD、MIDI等。 值得注意的是, 這些pin代表的是聲卡上的物理輸入端子, 在Filter Graph中是永遠不會連接到其他Filter上的。 聲卡的輸出功能, 可以有兩個Filter供選擇:DirectSound Renderer Filter和Audio Renderer (WaveOut) Filter。 注意, 這兩個Filter不是上述意義上的包裝Filter, 它們能夠同硬件交互, 是因為它們使用了API函數:前者使用了DirectSound API, 後者使用了waveOut API。 這兩個Filter的區別, 還在於後者輸出音頻的同時不支持混音。 (順便說明一下, Video Renderer Filter能夠訪問顯卡, 也是因為使用了GDI、DirectDraw或Direct3D API。 ) 如果你的機器上有聲卡的話, 你可以通過GraphEdit, 在Audio Capture Sources目錄下看到這個聲卡的包裝Filter。 WDM驅動的硬件(包括視頻捕捉卡、硬件解壓卡等)。 這類硬件都使用Ksproxy.ax這個包裝Filter。 Ksproxy.ax實現了很多功能, 所以有「瑞士軍刀」的美譽;它還被稱作為「變色龍Filter」, 因為該Filter上定義了統一的接口, 而接口的實現因具體的硬件驅動程序而異。 在Filter Graph中, Ksproxy Filter顯示的名字為硬件的Friendly name(一般在驅動程序的.inf文件中定義)。 我們可以通過GraphEdit, 在WDM Streaming開頭的目錄中找到本機系統中安裝的WDM硬件。 因為KsProxy.ax能夠代表各種WDM的音視頻設備, 所以這個包裝Filter的工作流程有點複雜。 這個Filter不會預先知道要代表哪種類型的設備, 它必須首先訪問驅動程序的屬性集, 然後動態配置Filter上應該實現的接口。 當Ksproxy Filter上的接口方法被應用程序或其他Filter調用時, 它會將調用方法以及參數傳遞給驅動程序, 由驅動程序最終完成指定功能。 除此以外, WDM硬件還支持內核流(Kernel Streaming), 即內核模式下的數據傳輸, 而無需經過到用戶模式的轉換。 因為內核模式與用戶模式之間的相互轉換, 需要花費很大的計算量。 如果使用內核流, 不僅可以避免大量的計算, 還避免了內核數據與主機內存之間的拷貝過程。 在這種情況下, 用戶模式的Filter Graph中, 即使pin之間是連接的, 也不會有實際的數據流動。 典型的情況, 如帶有Video Port Pin的視頻捕捉卡, Preview時顯示的圖像就是在內核模式下直接傳送到顯卡的顯存的。 所以, 你也休想在VP Pin後面截獲數據流。 講到這裏, 我想大家應該對DirectShow對硬件的支持問題有了一個總體的認識。 對於應用程序開發人員來說, 這方面的內容不用研究得太透, 而只需作為背景知識瞭解一下就好了。 其實, 大量繁瑣的工作DirectShow已經幫我們做好了。 //陸其明文章結束 Direcshow中視頻捕捉的Filter Pin的種類 捕捉Filter一般都有兩個或多個輸出pin, 他們輸出的媒體類型都一樣, 比如預覽pin和捕捉pin, 因此根據媒體類型就不能很好的區別這些pin。 此時就要根據pin的功能來區別每個pin了, 每個pin都有一個GUID, 稱為pin的種類。 如果想仔細的瞭解pin的種類, 請看後面的相關內容Working with Pin Categories。 對於大多數的應用來說, ICaptureGraphBuilder2提供了一些函數可以自動確定pin的種類。 預覽pin和捕捉pin 視頻捕捉Filter都提供了預覽和捕捉的輸出pin, 預覽pin用來將視頻流在屏幕上顯示, 捕捉pin用來將視頻流寫入文件。 預覽pin和輸出pin有下面的區別: 1 為了保證捕捉pin對視頻楨流量, 預覽pin必要的時候可以停止。 2 經過捕捉pin的視頻楨都有時間戳, 但是預覽pin的視頻流沒有時間戳。 預覽pin的視頻流之所以沒有時間戳的原因在於filter圖表管理器在視頻流裏加一個很小的latency, 如果捕捉時間被認為就是render時間的話, 視頻renderFilter就認為視頻流有一個小小的延遲, 如果此時render filter試圖連續播放的時候, 就會丟楨。 去掉時間戳就保證了視頻楨來了就可以播放, 不用等待, 也不丟楨。 預覽pin的種類GUID為PIN_CATEGORY_PREVIEW 捕捉pin的種類GUID為PIN_CATEGORY_CAPTURE Video Port pin Video Port是一個介於視頻設備(TV)和視頻卡之間的硬件設備。 同過Video Port, 視頻數據可以直接發送到圖像卡上, 通過硬件的覆蓋, 視頻可以直接在屏幕顯示出來。 Video Port就是連接兩個設備的。 使用Video Port的最大好處是, 不用CPU的任何工作, 視頻流直接寫入內存中。 當然它也有下面的缺點drawbacks: 略 如果捕捉設備使用了Video Port, 捕捉Filter就用一個video port pin代替預覽pin。 video port pin的種類GUID為PIN_CATEGORY_VIDEOPORT 一個捕捉filter至少有一個Capture pin, 另外, 它可能有一個預覽pin 和一個video port pin , 或者兩者都沒有, 也許filter有很多的capture pin, 和預覽pin, 每一個pin都代表一種媒體類型, 因此一個filter可以有一個視頻capture pin, 視頻預覽pin, 音頻捕捉pin, 音頻預覽pin。 Upstream WDM Filters 在捕捉Filter之上, WDM設備可能需要額外的filters, 下面就是這些filter TV Tuner Filter TV Audio Filter. Analog Video Crossbar Filter 儘管這些都是一些獨立的filter, 但是他們可能代表的是同一個硬件設備, 每個filter都控制設備的不同函數, 這些filter通過pin連接起來, 但是在pin中沒有數據流動。 因此, 這些pin 的連接和媒體類型無關。 他們使用一個GUID值來定義一個給定設備的minidriver, 例如:TV tuner Filter 和video capture filter都支持同一種medium。 在實際應用中, 如果你使用ICaptureGraphBuilder2來創建你的capture graphs, 這些filters就會自動被添加到你的graph中。 更多的詳細資料, 可以參考WDM Class Driver Filters 2選擇一個視頻捕捉設備(Select capture device) 如何選擇一個視頻捕捉設備, 可以採用系統設備枚舉, 詳細資料參見Using the System Device Enumerator 。 enumerator可以根據filter的種類返回一個設備的monikers。 Moniker是一個com對象, 可以參見IMoniker的SDK。 對於捕捉設備, 下面兩種類是相關的。 CLSID_AudioInputDeviceCategory 音頻設備 CLSID_VideoInputDeviceCategory 視頻設備 下面的代碼演示了如何枚舉一個視頻捕捉設備 ICreateDevEnum *pDevEnum = NULL; IEnumMoniker *pEnum = NULL; // Create the System Device Enumerator. HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, reinterpret_cast<void**>(&pDevEnum)); if (SUCCEEDED(hr)) { //創建一個枚舉器, 枚舉視頻設備 hr = pDevEnum->CreateClassEnumerator( CLSID_VideoInputDeviceCategory, &pEnum, 0); } IEnumMoniker接口pEnum返回一個IMoniker接口的列表, 代表一系列的moniker, 你可以顯示所有的設備, 然後讓用戶選擇一個。 採用IMoniker::BindToStorage方法, 返回一個IPropertyBag接口指針。 然後調用IPropertyBag::Read讀取moniker的屬性。 下面看看都包含什麼屬性 1 FriendlyName 是設備的名字 2 Description 屬性僅僅適用於DV和D-VHS/MPEG攝像機, 如果這個屬性可用, 這個屬性更詳細的描述了設備的資料 3DevicePath 這個屬性是不可讀的, 但是每個設備都有一個獨一無二的。 你可以用這個屬性來區別同一個設備的不同實例 下面的代碼演示了如何顯示遍曆設備的名稱 , 接上面的代碼 HWND hList; // Handle to the list box. IMoniker *pMoniker = NULL; while (pEnum->Next(1, &pMoniker, NULL) == S_OK) { IPropertyBag *pPropBag; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)(&pPropBag)); if (FAILED(hr)) { pMoniker->Release(); continue; // Skip this one, maybe the next one will work. } // Find the description or friendly name. VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"Description", &varName, 0); if (FAILED(hr)) { hr = pPropBag->Read(L"FriendlyName", &varName, 0); } if (SUCCEEDED(hr)) { // Add it to the application's list box. USES_CONVERSION; (long)SendMessage(hList, LB_ADDSTRING, 0, (LPARAM)OLE2T(varName.bstrVal)); VariantClear(&varName); } pPropBag->Release(); pMoniker->Release(); } 如果用戶選中了一個設備調用IMoniker::BindToObject為設備生成filter, 然後將filter加入到graph中。 IBaseFilter *pCap = NULL; hr = pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pCap); if (SUCCEEDED(hr)) { hr = m_pGraph->AddFilter(pCap, L"Capture Filter"); } 3預覽視頻(Previewing Video) 為了創建可以預覽視頻的graph, 可以調用下面的代碼 ICaptureGraphBuilder2 *pBuild; // Capture Graph Builder // Initialize pBuild (not shown). IBaseFilter *pCap; // Video capture filter. /* Initialize pCap and add it to the filter graph (not shown). */ hr = pBuild->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap, NULL, NULL); 4如何捕捉視頻流並保存到文件(Capture video to File) 1 將視頻流保存到AVI文件 下面的圖表顯示了graph圖 Directshow中的視頻捕捉 圖2 AVI Mux filter接收從capture pin過來的視頻流, 然後將其打包成AVI流。 音頻流也可以連接到AVI Mux Filter上, 這樣mux filter就將視頻流和視頻流合成AVI流。 File writer將AVI流寫入到文件中。 可以像下面這樣構建graph圖 IBaseFilter *pMux; hr = pBuild->SetOutputFileName( &MEDIASUBTYPE_Avi, // Specifies AVI for the target file. L"C:\\Example.avi", // File name. &pMux, // Receives a pointer to the mux. NULL); // (Optional) Receives a pointer to the file sink. 第一個參數表明文件的類型, 這裏表明是AVI, 第二個參數是制定文件的名稱。 對於AVI文件, SetOutputFileName函數會創建一個AVI mux Filter 和一個 File writer Filter , 並且將兩個filter添加到graph圖中, 在這個函數中, 通過File Writer Filter 請求IFileSinkFilter接口, 然後調用IFileSinkFilter::SetFileName方法, 設置文件的名稱。 然後將兩個filter連接起來。 第三個參數返回一個指向 AVI Mux的指針, 同時, 它也通過第四個參數返回一個IFileSinkFilter參數, 如果你不需要這個參數, 你可以將這個參數設置成NULL。 然後, 你應該調用下面的函數將capture filter 和AVI Mux連接起來。 hr = pBuild->RenderStream( &PIN_CATEGORY_CAPTURE, // Pin category. &MEDIATYPE_Video, // Media type. pCap, // Capture filter. NULL, // Intermediate filter (optional). pMux); // Mux or file sink filter. // Release the mux filter. pMux->Release(); 第5個參數就是使用的上面函數返回的pMux指針。 當捕捉音頻的時候, 媒體類型要設置為MEDIATYPE_Audio, 如果你從兩個不同的設備捕捉視頻和音頻, 你最好將音頻設置成主流, 這樣可以防止兩個數據流間drift, 因為avi mux filter為同步音頻, 會調整視頻的播放速度的。 為了設置master 流, 調用IConfigAviMux::SetMasterStream方法, 可以採用如下的代碼: IConfigAviMux *pConfigMux = NULL; hr = pMux->QueryInterface(IID_IConfigAviMux, (void**)&pConfigMux); if (SUCCEEDED(hr)) { pConfigMux->SetMasterStream(1); pConfigMux->Release(); } SetMasterStream的參數指的是數據流的數目, 這個是由調用RenderStream的次序決定的。 例如, 如果你調用RenderStream首先用於視頻流, 然後是音頻, 那麼視頻流就是0, 音頻流就是1。 添加編碼filter IBaseFilter *pEncoder; /* Create the encoder filter (not shown). */ // Add it to the filter graph. pGraph->AddFilter(pEncoder, L"Encoder); /* Call SetOutputFileName as shown previously. */ // Render the stream. hr = pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, pEncoder, pMux); pEncoder->Release(); 2將視頻流保存成wmv格式的文件 為了將視頻流保存成並編碼成windows media video (WMV)格式的文件, 將capture pin連到WM ASF Writer filter。 Directshow中的視頻捕捉 圖3 構建graph圖最簡單的方法就是將在ICaptureGraphBuilder2::SetOutputFileName方法中指定MEDIASUBTYPE_Asf的filter。 如下 IBaseFilter* pASFWriter = 0; hr = pBuild->SetOutputFileName( &MEDIASUBTYPE_Asf, // Create a Windows Media file. L"C:\\VidCap.wmv", // File name. &pASFWriter, // Receives a pointer to the filter. NULL); // Receives an IFileSinkFilter interface pointer (optional). 參數MEDIASUBTYPE_Asf 告訴graph builder, 要使用wm asf writer作為文件接收器, 於是, pbuild 就創建這個filter, 將其添加到graph圖中, 然後調用IFileSinkFilter::SetFileName來設置輸出文件的名字。 第三個參數用來返回一個ASF writer指針, 第四個參數用來返回文件的指針。 在將任何pin連接到WM ASF Writer之前, 一定要對WM ASF Writer進行一下設置, 你可以同過WM ASF Writer的IConfigAsfWriter接口指針來進行設置。 IConfigAsfWriter *pConfig = 0; hr = pASFWriter->QueryInterface(IID_IConfigAsfWriter, (void**)&pConfig); if (SUCCEEDED(hr)) { // Configure the ASF Writer filter. pConfig->Release(); } 然後調用ICaptureGraphBuilder2::RenderStream將capture Filter 和 ASF writer連接起來。 hr = pBuild->RenderStream( &PIN_CATEGORY_CAPTURE, // Capture pin. &MEDIATYPE_Video, // Video. Use MEDIATYPE_Audio for audio. pCap, // Pointer to the capture filter. 0, pASFWriter); // Pointer to the sink filter (ASF Writer). 3保存成自定義的文件格式 如果你想將文件保存成自己的格式, 你必須有自己的 file writer。 看下面的代碼 IBaseFilter *pMux = 0; IFileSinkFilter *pSink = 0; hr = pBuild->SetOutputFileName( &CLSID_MyCustomMuxFilter, //自己開發的Filter L"C:\\VidCap.avi", &pMux, &pSink); 4如何將視頻流保存進多個文件 當你將視頻流保存進一個文件後, 如果你想開始保存第二個文件, 這時, 你應該首先將graph停止, 然後通過IFileSinkFilter::SetFileName改變 File Writer 的文件名稱。 注意, IFileSinkFilter指針你可以在SetOutputFileName時通過第四個參數返回的。 看看保存多個文件的代碼吧 IBaseFilter *pMux; IFileSinkFilter *pSink hr = pBuild->SetOutputFileName(&MEDIASUBTYPE_Avi, L"C:\\YourFileName.avi", &pMux, &pSink); if (SUCCEEDED(hr)) { hr = pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, NULL, pMux); if (SUCCEEDED(hr)) { pControl->Run(); /* Wait awhile, then stop the graph. */ pControl->Stop(); // Change the file name and run the graph again. pSink->SetFileName(L"YourFileName02.avi", 0); pControl->Run(); } pMux->Release(); pSink->Release(); } 5組合視頻的捕捉和預覽 如果想組建一個既可以預覽視頻, 又可以將視頻保存成文件的graph, 只需要兩次調用ICaptureGraphBuilder2::RenderStream即可。 代碼如下: // Render the preview stream to the video renderer. hr = pBuild->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap, NULL, NULL); // Render the capture stream to the mux. hr = pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, NULL, pMux); 在上面的代碼中, graph builder 其實隱藏了下面的細節。 1 如果capture Filter既有preview pin 也有captrue pin, 那麼RenderStream僅僅將兩個pin和render filter接起來。 如下圖 Directshow中的視頻捕捉 圖4 2如果caprture Filter只有一個capture pin, 那麼Capture Graph Builder就採用一個Smart Tee Filter將視頻流分流, graph圖如下 Directshow中的視頻捕捉 圖5 5如何控制Capture Graph(Controlling Capture Graph) Filter圖表管理器可以通過IMediaControl接口控制整個graph的運行, 停止和暫停。 但是當一個graph有捕捉和預覽兩個數據流的時候, 如果我們想單獨的控制其中的一個數據流話, 我們可以通過ICaptureGraphBuilder2::ControlStream 。 下面講一下如何來單獨控制捕捉和預覽數據流。 1 控制捕捉視頻流 下面的代碼, 讓捕捉數據流在graph開始運行1秒後開始, 允運行4秒後結束。 // Control the video capture stream. REFERENCE_TIME rtStart = 1000 0000, rtStop = 5000 0000; const WORD wStartCookie = 1, wStopCookie = 2; // Arbitrary values. hr = pBuild->ControlStream( &PIN_CATEGORY_CAPTURE, // Pin category. &MEDIATYPE_Video, // Media type. pCap, // Capture filter. &rtStart, &rtStop, // Start and stop times. wStartCookie, wStopCookie // Values for the start and stop events. ); pControl->Run(); 第一個參數表明需要控制的數據流, 一般採用的是pin種類GUID, 第二個參數表明了媒體類型。 第三個參數指明了捕捉的filter。 如果想要控制graph圖中的所有捕捉filter, 第二個和第三個參數都要設置成NULL。 第四和第五個參數表明了流開始和結束的時間, 這是一個相對於graph開始的時間。 只有你調用IMediaControl::Run以後, 這個函數才有作用。 如果graph正在運行, 這個設置立即生效。 最後的兩個參數用來設置當數據流停止, 開始能夠得到的事件通知。 對於任何一個運用此方法的數據流, graph當流開始的時候, 會發送EC_STREAM_CONTROL_STARTED通知, 在流結束的時候, 要發送EC_STREAM_CONTROL_STOPPED通知。 wStartCookie和wStopCookie是作為第二個參數的。 看看事件通知處理過程吧 while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr)) { switch (evCode) { case EC_STREAM_CONTROL_STARTED: // param2 == wStartCookie break; case EC_STREAM_CONTROL_STOPPED: // param2 == wStopCookie break; } pEvent->FreeEventParams(evCode, param1, param2); } ControlStream還定義了一些特定的值來表示開始和停止的時間。 MAXLONGLONG 從不開始, 只有在graph停止的時候才停止 NULL, 立即開始和停止 例如, 下面的代碼立即停止捕捉流。 pBuild->ControlStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, 0, 0, // Start and stop times. wStartCookie, wStopCookie); 2控制預覽視頻流 只要給ControlStream第一個參數設置成PIN_CATEGORY_PREVIEW就可以控制預覽pin, 整個函數的使用和控制捕捉流一樣, 但是唯一區別是在這裏你沒法設置開始和結束時間了, 因為預覽的視頻流沒有時間戳, 因此你必須使用NULL或者MAXLONGLONG。 例子 Use NULL to start the preview stream: pBuild->ControlStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap, NULL, // Start now. 0, // (Don't care.) wStartCookie, wStopCookie); Use MAXLONGLONG to stop the preview stream: pBuild->ControlStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap, 0, // (Don't care.) MAXLONGLONG, // Stop now. wStartCookie, wStopCookie); 3關於數據流的控制 Pin的缺省的行為是傳遞sample, 例如, 如果你對PIN_CATEGORY_CAPTURE使用了ControlStream, 但是對於PIN_CATEGORY_PREVIEW沒有使用該函數, 因此, 當你run graph的時候, preview 流會立即運行起來, 而capture 流則要等到你設置的時間運行。 6如何配置一個視頻捕捉設備 1顯示VFW驅動的視頻設備對話框 如果視頻捕捉設備採用的仍然是VFW方式的驅動程序, 則必須支持下面三個對話框, 用來設置視頻設備。 1 Video Source 用來選擇視頻輸入設備並且調整設備的設置, 比如亮度和對比度。 2Video Format 用來設置楨的大小和位 3Video Display 用來設置視頻的顯示參數 為了顯示上面的三個對話框, 你可以do the following 1 停止graph。 2向捕捉filter請求IAMVfwCaptureDialogs接口, 如果成功, 表明設備支持VFW驅動。 3調用IAMVfwCaptureDialogs::HasDialog來檢查驅動程序是否支持你請求的對話框, 如果支持, 返回S_OK,否則返回S_FALSE。 注意不要用SUCCEDED宏。 4如果驅動支持該對話框, 調用IAMVfwCaptureDialogs::ShowDialog顯示該對話框。 5 重新運行graph 代碼如下 pControl->Stop(); // Stop the graph. // Query the capture filter for the IAMVfwCaptureDialogs interface. IAMVfwCaptureDialogs *pVfw = 0; hr = pCap->QueryInterface(IID_IAMVfwCaptureDialogs, (void**)&pVfw); if (SUCCEEDED(hr)) { // Check if the device supports this dialog box. if (S_OK == pVfw->HasDialog(VfwCaptureDialog_Source)) { // Show the dialog box. hr = pVfw->ShowDialog(VfwCaptureDialog_Source, hwndParent); } } pControl->Run(); 2 調整視頻的質量 WDM驅動的設備支持一些屬性可以用來調整視頻的質量, 比如亮度, 對比度, 飽和度, 等要向調整視頻的質量, do the following 1 從捕捉filter上請求IAMVideoProcAmp接口 2 對於你想調整的任何一個屬性, 調用IAMVideoProcAmp::GetRange可以返回這個屬性賦值的範圍, 缺省值, 最小的增量值。 IAMVideoProcAmp::Get返回當前正在使用的值。 VideoProcAmpProperty枚舉每個屬性定義的標誌。 3調用IAMVideoProcAmp::Set來設置這個屬性值。 設置屬性的時候, 不用停止graph。 看看下面的代碼是如何調整視頻的質量的 HWND hTrackbar; // Handle to the trackbar control. // Initialize hTrackbar (not shown). // Query the capture filter for the IAMVideoProcAmp interface. IAMVideoProcAmp *pProcAmp = 0; hr = pCap->QueryInterface(IID_IAMVideoProcAmp, (void**)&pProcAmp); if (FAILED(hr)) { // The device does not support IAMVideoProcAmp, so disable the control. EnableWindow(hTrackbar, FALSE); } else { long Min, Max, Step, Default, Flags, Val; // Get the range and default value. hr = m_pProcAmp->GetRange(VideoProcAmp_Brightness, &Min, &Max, &Step, &Default, &Flags); if (SUCCEEDED(hr)) { // Get the current value. hr = m_pProcAmp->Get(VideoProcAmp_Brightness, &Val, &Flags); } if (SUCCEEDED(hr)) { // Set the trackbar range and position. SendMessage(hTrackbar, TBM_SETRANGE, TRUE, MAKELONG(Min, Max)); SendMessage(hTrackbar, TBM_SETPOS, TRUE, Val); EnableWindow(hTrackbar, TRUE); } else { // This property is not supported, so disable the control. EnableWindow(hTrackbar, FALSE); } } 3調整視頻輸出格式 我們知道視頻流可以有多種輸出格式, 一個設備可以支持16-bit RGB, 32-bit RGB, and YUYV, 在每一種格式下, 設備還可以調整視頻楨的大小。 在WDM驅動設備上, IAMStreamConfig 接口用來報告設備輸出視頻的格式的, VFW設備, 可以採用對話框的方式來設置, 參見前面的內容。 捕捉Filter的捕捉pin和預覽pin都支持IAMStreamConfig 接口, 可以通過ICaptureGraphBuilder2::FindInterface獲得IAMStreamConfig接口。 IAMStreamConfig *pConfig = NULL; hr = pBuild->FindInterface( &PIN_CATEGORY_PREVIEW, // Preview pin. 0, // Any media type. pCap, // Pointer to the capture filter. IID_IAMStreamConfig, (void**)&pConfig); 設備還支持一系列的媒體類型, 對於每一個媒體類型, 設備都要支持一系列的屬性, 比如, 楨的大小, 圖像如何縮放, 楨率的範圍等。 通過IAMStreamConfig::GetNumberOfCapabilities獲得設備所支持的媒體類型的數量。 這個方法返回兩個值, 一個是媒體類型的數量, 二是屬性所需結構的大小。 這個結構的大小很重要, 因為這個方法是用於視頻和音頻的, 視頻採用的是VIDEO_STREAM_CONFIG_CAPS結構, 音頻用AUDIO_STREAM_CONFIG_CAPS結構。 通過函數IAMStreamConfig::GetStreamCaps來枚舉媒體類型, 要給這個函數傳遞一個序號作為參數, 這個函數返回媒體類型和相應的屬性結構體。 看代碼把 int iCount = 0, iSize = 0; hr = pConfig->GetNumberOfCapabilities(&iCount, &iSize); // Check the size to make sure we pass in the correct structure. if (iSize == sizeof(VIDEO_STREAM_CONFIG_CAPS) { // Use the video capabilities structure. for (int iFormat = 0; iFormat < iCount; iFormat++) { VIDEO_STREAM_CONFIG_CAPS scc; AM_MEDIA_TYPE *pmtConfig; hr = pConfig->GetStreamCaps(iFormat, &pmtConfig, (BYTE*)&scc); if (SUCCEEDED(hr)) { /* Examine the format, and possibly use it. */ // Delete the media type when you are done. hr = pConfig->SetFormat(pmtConfig);//重新設置視頻格式 DeleteMediaType(pmtConfig); } } 你可以調用IAMStreamConfig::SetFormat設置新的媒體類型 hr = pConfig->SetFormat(pmtConfig); 如果pin沒有連接, 當連接的時候就試圖用新的格式, 如果pin已經在連接了, 它就會用的新的媒體格式重新連接。 在任何一種情況下, 下遊的filter都有可能拒絕新的媒體格式。 在SetFormat前你可以修改VIDEO_STREAM_CONFIG_CAPS結構來重新設置媒體類型。 例如: 如果GetStreamCaps返回的是24-bit RGB format, 楨的大小是320 x 240 像素, 你可以通過檢查媒體類型的major type, subtpye, 和format等值 if ((pmtConfig.majortype == MEDIATYPE_Video) && (pmtConfig.subtype == MEDIASUBTYPE_RGB24) && (pmtConfig.formattype == FORMAT_VideoInfo) && (pmtConfig.cbFormat >= sizeof (VIDEOINFOHEADER)) && (pmtConfig.pbFormat != NULL)) { VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)pmtConfig.pbFormat; // pVih contains the detailed format information. LONG lWidth = pVih->bmiHeader.biWidth; LONG lHeight = pVih->bmiHeader.biHeight; } VIDEO_STREAM_CONFIG_CAPS結構裏包含了該媒體類型的視頻長度和寬度的最大值和最小值, 還有遞增的幅度值, 就是每次調整視頻size的幅度, 例如, 設備可能返回如下的值 ? MinOutputSize: 160 x 120 ? MaxOutputSize: 320 x 240 ? OutputGranularityX: 8 pixels (horizontal step size) ? OutputGranularityY: 8 pixels (vertical step size) 這樣你可以在(160, 168, 176, ... 304, 312, 320) 範圍內設置寬度, 在 (120, 128, 136, ... 104, 112, 120).設置高度值, Directshow中的視頻捕捉 圖6 如果想設置新的值, 直接修改在GetStreamCaps函數中返回的值即可, pVih->bmiHeader.biWidth = 160; pVih->bmiHeader.biHeight = 120; pVih->bmiHeader.biSizeImage = DIBSIZE(pVih->bmiHeader); 然後將媒體類型傳遞給SetFormat函數, 就可修改視頻格式了。 7將設備從系統中移走時的事件通知(Device remove Notify) 如果用戶將一個graph正在使用的即插即用型的設備從系統中去掉, filter圖表管理器就會發送一個EC_DEVICE_LOST事件通知, 如果該設備又可以使用了, filter圖表管理器就發送另外的一個EC_DEVICE_LOST通知, 但是先前組建的捕捉filter graph圖就沒法用了, 用戶必須重新組建graph圖。 當系統中有新的設備添加時, dshow是不會發送任何通知的, 所以, 應用程序如果想要知道系統中何時添加新的設備, 應用程序可以監控WM_DEVICECHANGE消息。 8從靜止圖像pin中捕捉圖片 有些照相機, 攝像頭除了可以捕獲視頻流以外還可以捕獲單張的, 靜止的圖片。 通常, 靜止的圖片的質量要比流的質量要高。 攝像頭一般都一個按鈕來觸發, 或者是支持軟件觸發。 支持輸出靜態圖片的攝像頭一般都要提供一個靜態圖片pin, 這個pin的種類是PIN_CATEGORY_STILL。 從設備中獲取靜態圖片, 我們一般推薦使用windows Image Acquisition (WIA) APIs。 當然, 你也可以用dshow來獲取圖片。 在graph運行的時候利用IAMVideoControl::SetMode來觸發靜態的pin。 代碼如下 pControl->Run(); // Run the graph. IAMVideoControl *pAMVidControl = NULL; hr = pCap->QueryInterface(IID_IAMVideoControl, (void**)&pAMVidControl); if (SUCCEEDED(hr)) { // Find the still pin. IPin *pPin = 0; hr = pBuild->FindPin(pCap, PINDIR_OUTPUT, &PIN_CATEGORY_STILL, 0, FALSE, 0, &pPin); if (SUCCEEDED(hr)) { hr = pAMVidControl->SetMode(pPin, VideoControlFlag_Trigger); pPin->Release(); } pAMVidControl->Release(); } 首先向capture Filter 請求IAMVideoContol, 如果支持該接口, 就調用ICaptureGraphBuilder2::FindPin請求指向靜止pin 的指針, 然後調用pin的put_Mode方法。 根據不同的攝像頭, 你可能靜態pin連接前要render 該pin。 捕捉靜態圖片常用的filter是Sample Grabber filter, Sample Grabber使用了一個用戶定義的回調汗水來處理圖片。 關於這個filter的詳細用法, 參見Using the Sample Grabber.。 下面的例子假設靜態pin傳遞的是沒有壓縮的RGB圖片。 首先定義一個類, 從ISampleGrabberCB繼承。 // Class to hold the callback function for the Sample Grabber filter. class SampleGrabberCallback : public ISampleGrabberCB { // Implementation is described later. } // Global instance of the class. SampleGrabberCallback g_StillCapCB; 然後將捕捉filter的靜態pin連接到Sample Grabber, 將Sample Grabber連接到Null Renderer filter。 Null Renderer僅僅是將她接收到的sample丟棄掉。 實際的工作都是在回調函數裏進行, 連接Null Renderer 僅僅是為了給Sample Grabber's 輸出pin上連接點東西。 具體見下面的代碼 // Add the Sample Grabber filter to the graph. IBaseFilter *pSG_Filter; hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pSG_Filter); hr = pGraph->AddFilter(pSG_Filter, L"SampleGrab"); // Add the Null Renderer filter to the graph. IBaseFilter *pNull; hr = CoCreateInstance(CLSID_NullRendere, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pNull); hr = pGraph->AddFilter(pSG_Filter, L"NullRender"); 然後通過RenderStream將still pin , sample grabber , null Renderer連接起來 hr = pBuild->RenderStream( &PIN_CATEGORY_STILL, // Connect this pin ... &MEDIATYPE_Video, // with this media type ... pCap, // on this filter ... pSG_Filter, // to the Sample Grabber ... pNull); // ... and finally to the Null Renderer. 然後調用ISampleGrabber指針, 來通過這個指針可以分配內存。 // Configure the Sample Grabber. ISampleGrabber *pSG; hr = pSG_Filter->QueryInterface(IID_ISampleGrabber, (void**)&pSG); pSG->SetOneShot(FALSE); pSG->SetBufferSamples(TRUE); 設置你的回調對像 pSG->SetCallback(&g_StillCapCB, 0); // 0 = Use the SampleCB callback method 獲取靜態pin和sample grabber之間連接所用的媒體類型 // Store the media type for later use. AM_MEDIA_TYPE g_StillMediaType; hr = pSG->GetConnectedMediaType(&g_StillMediaType); pSG->Release(); 媒體類型包含一個BITMAPINFOHEADER結構來定義圖片的格式, 在程序退出前一定要釋放媒體類型 // On exit, remember to release the media type. FreeMediaType(g_StillMediaType); 看看下面的回調類吧。 這個類從ISampleGrabber接口派生, 但是它沒有保持引用計數, 因為應用程序在堆上創建這個對象, 在整個graph的生存週期它都存在。 所有的工作都在BufferCB函數裏完成, 當有一個新的sample到來的時候, 這個函數就會被sample Grabber調用到。 在下面的例子裏, bitmap被寫入到一個文件中 class SampleGrabberCallback : public ISampleGrabberCB { public: // Fake referance counting. STDMETHODIMP_(ULONG) AddRef() { return 1; } STDMETHODIMP_(ULONG) Release() { return 2; } STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject) { if (NULL == ppvObject) return E_POINTER; if (riid == __uuidof(IUnknown)) { *ppvObject = static_cast<IUnknown*>(this); return S_OK; } if (riid == __uuidof(ISampleGrabberCB)) { *ppvObject = static_cast<ISampleGrabberCB*>(this); return S_OK; } return E_NOTIMPL; } STDMETHODIMP SampleCB(double Time, IMediaSample *pSample) { return E_NOTIMPL; } STDMETHODIMP BufferCB(double Time, BYTE *pBuffer, long BufferLen) { if ((g_StillMediaType.majortype != MEDIATYPE_Video) || (g_StillMediaType.formattype != FORMAT_VideoInfo) || (g_StillMediaType.cbFormat < sizeof(VIDEOINFOHEADER)) || (g_StillMediaType.pbFormat == NULL)) { return VFW_E_INVALIDMEDIATYPE; } HANDLE hf = CreateFile("C:\\Example.bmp", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL); if (hf == INVALID_HANDLE_VALUE) { return E_FAIL; } long cbBitmapInfoSize = g_StillMediaType.cbFormat - SIZE_PREHEADER; VIDEOINFOHEADER *pVideoHeader = (VIDEOINFOHEADER*)g_StillMediaType.pbFormat; BITMAPFILEHEADER bfh; ZeroMemory(&bfh, sizeof(bfh)); bfh.bfType = 'MB'; // Little-endian for "MB". bfh.bfSize = sizeof( bfh ) + BufferLen + cbBitmapInfoSize; bfh.bfOffBits = sizeof( BITMAPFILEHEADER ) + cbBitmapInfoSize; // Write the file header. DWORD dwWritten = 0; WriteFile( hf, &bfh, sizeof( bfh ), &dwWritten, NULL ); WriteFile(hf, HEADER(pVideoHeader), cbBitmapInfoSize, &dwWritten, NULL); WriteFile( hf, pBuffer, BufferLen, &dwWritten, NULL ); CloseHandle( hf ); return S_OK; } };(王朝網路 wangchao.net.cn)