011 如何開發傳輸過濾器(Transform filter)

Post date: 2015/4/9 上午 01:26:01

摘要:本篇文檔主要講述了利用Directshow開發傳輸filter 時應該注意的一些事情。 在開發自己的filter之前, 看看DMO(DirectX Media Object)是否滿足你的要求, 因為DMO可以做許多和filter相同的工作, 但是開發DMO比開發filter要簡單多了。 開發transform filter主要有下面的幾個步驟, 努力的遵循吧 第一步選擇一個基類 下面的基類適合開發transform filter。 CTransformFilter就是為了transform filter而設計的基類, 這個類中有分開的輸入和輸出buffers, 這種類型的filter有時也稱作copy-transform filter, 當一個copy-transform filter接收到一個輸入samples的時候, 它就將sample寫入到一塊新的輸出buffer中, 然後將這個新的buffer傳遞給下一個filter。 CTransInPlaceFilter, 這個類型的filter在原來的buffer裏修改data, 也叫trans-in-place filters. 當這種類型的filter接收到一個sample, 它改變這個sample中的數據, 然後將sample傳遞下去, 這種類型的輸入pin和輸出pin總是按照某個媒體類型連接起來。 CVideoTransformFilter這個類型的filter僅僅是為了視頻解碼器設計的。 從CTransFormFilter派生而來, 但是這個filter可以根據下遊的render自動的丟棄data。 CBaseFilter是個總基類, 所有的filter都是從這個類派生出去的。 如果上面的filter都不適合你, 那麼你只有自己從這個基類中派生了。 第二步聲明自己的Filter 類 首先聲明一個從基類派生的c++類 class CRleFilter : public CTransformFilter { /* Declarations will go here. */ }; 每個filter類都需要連接的pin類。 根據你的需要, 你要派生和你的filter連接的pin類。 你還要給你的filter設置一個不能重複的CLSID, 你可以利用Guidgen or Uuidgen來產生一個128位CLSID, 切忌不要拷貝其它的filter的。 有很多種方法來聲明CLSID, 下面的例子使用了DEFINE_GUID宏。 [RleFilt.h] // {1915C5C7-02AA-415f-890F-76D94C85AAF1} DEFINE_GUID(CLSID_RLEFilter, 0x1915c5c7, 0x2aa, 0x415f, 0x89, 0xf, 0x76, 0xd9, 0x4c, 0x85, 0xaa, 0xf1); [RleFilt.cpp] #include <initguid.h> #include "RleFilt.h" 然後, 給你的filter寫一個構造函數 CRleFilter::CRleFilter() : CTransformFilter(NAME("My RLE Encoder"), 0, CLSID_RLEFilter) { /* Initialize any private variables here. */ } 注意, 構造函數中有個參數就是我前面定義的CLSID。 第三步 支持媒體類戲協議 當兩個pin連接的時候, 他們必須就某種媒體類型達成一致協議, 否則連接失敗, 數據媒體類型描述了數據的格式, 如果沒有媒體類型, 一個filter可能傳遞一種類型的數據, 然後其它的filte卻不能識別這種數據。 Pin連接的時候達成協議的機制主要通過IPin::ReceiveConnection方法來實現的。 輸出pin用某種媒體類型作參數調用輸入pin上的這個方法, 輸入pin要麼接受, 要麼拒絕。 如果輸入pin拒絕連接, 那麼輸出pin更改一下媒體類型繼續連接, 直至所有的媒體類型都連接一遍, 如果沒有找到合適的媒體的類型, 那麼連接失敗。 在輸入pin也可以通過IPin::EnumMediaTypes方法來任意的枚舉它所支持的媒體類型list。 輸出pin可以通過這個list也可以檢查是否支持某種媒體類型。 CTransformFilter實現一個通用的框架。 如下 1 輸入pin沒有首選的媒體類型, 這個主要看上遊的filter提議的媒體類型。 對於視頻數據, 媒體類型包括圖片的大小, 和楨率, 這個信息必須由上遊的源filter或者parser filter提供。 對於音頻數據, 設置的數據格式就小了許多, 因此, 要重載輸入pin的CBasePin::GetMediaType 2 當上遊的filter提議一個媒體類型進行連接的時候, 輸入pin就調用 CTransformFilter::CheckInputType方法, 這個方法拒絕和接受媒體類型。 3 只有輸入pin連接以後, 輸出pin才能夠連接, 這個是屬於transform filter的一個特性。 大多數情況下, filter在設置輸出pin的type之前一定要設置好輸入pin的類型 4當輸出pin沒有連接的時候, 它向下遊filter連接的時候, 要枚舉本filter支持的媒體類型, 形成一個list, 他通過調用CTransformFilter::GetMediaType方法來產生這個list, 輸出pin會就下遊filter所支持的所有的媒體類型進行連接 5 為了檢測輸入pin是否支持某個特定的輸出媒體類型, 輸出pin通過調用CTransformFilter::CheckTransform方法。 上面列出的三個CTransformFilter方法都是純虛函數, 因此你的filter必須實現這三個函數 當上遊的filter連接的時候提議一個媒體類型, 那麼輸入pin就會調用函數 virtual HRESULT CheckInputType(const CMediaType* mtIn) pure; 這個函數包含了一個CMediaType類型的對象指針, 這個類型封裝了一個AM_MEDIA_TYPE結構。 在這個函數中, 你要檢查AM_MEDIA_TYPE結構的中相關的field, 如果該結構中有任何fied不合法, 就返回VFW_E_TYPE_NOT_ACCEPTED, 如果所有的媒體類型都是正確的, 返還S_OK , 例如, 在RLE編碼filter, 輸入類型必須是8位或者4位的沒有壓縮的RGB視頻。 沒有必要支持其它的輸入格式, 例如16, 24位, 因為那樣, filter還得進行轉換。 下面的例子假定filter只支持8位的視頻, 不支持4位的視頻 HRESULT CRleFilter::CheckInputType(const CMediaType *mtIn) { if ((mtIn->majortype != MEDIATYPE_Video) || (mtIn->subtype != MEDIASUBTYPE_RGB8) || (mtIn->formattype != FORMAT_VideoInfo) || (mtIn->cbFormat < sizeof(VIDEOINFOHEADER))) { return VFW_E_TYPE_NOT_ACCEPTED; } VIDEOINFOHEADER *pVih = reinterpret_cast<VIDEOINFOHEADER*>(mtIn->pbFormat); if ((pVih->bmiHeader.biBitCount != 8) || (pVih->bmiHeader.biCompression != BI_RGB)) { return VFW_E_TYPE_NOT_ACCEPTED; } // Check the palette table. if (pVih->bmiHeader.biClrUsed > PALETTE_ENTRIES(pVih)) { return VFW_E_TYPE_NOT_ACCEPTED; } DWORD cbPalette = pVih->bmiHeader.biClrUsed * sizeof(RGBQUAD); if (mtIn->cbFormat < sizeof(VIDEOINFOHEADER) + cbPalette) { return VFW_E_TYPE_NOT_ACCEPTED; } // Everything is good. return S_OK; } 在這個例子中, 函數首先檢查major type and subtype, 然後檢查格式類型, 為了確保block格式是一個VIDEOINFOHEADER結構, 這個filter也要支持VIDEOINFOHEADER2, 如果格式類型是正確的, 這個sample還得檢查VIDEOINFOHEADER結構的biBitCount and biCompression members, 2 virtual HRESULT GetMediaType(int iPosition, CMediaType *pMediaType) PURE; CTransformFilter::GetMediaType根據序號iPositiong返回一個fiter支持的輸出類型。 只有輸入pin被連接上以後, 這個方法才會被調用, 因此, 你可以利用上遊filter支持的媒體類型來決定下遊輸出的媒體類型 下面的例子返回一個輸出媒體類型, 這個輸出是根據輸入類型修改的 HRESULT CRleFilter::GetMediaType(int iPosition, CMediaType *pMediaType) { ASSERT(m_pInput->IsConnected()); if (iPosition < 0) { return E_INVALIDARG; } if (iPosition == 0) { HRESULT hr = m_pInput->ConnectionMediaType(pMediaType); if (FAILED(hr)) { return hr; } FOURCCMap fccMap = FCC('MRLE'); pMediaType->subtype = static_cast<GUID>(fccMap); pMediaType->SetVariableSize(); pMediaType->SetTemporalCompression(FALSE); ASSERT(pMediaType->formattype == FORMAT_VideoInfo); VIDEOINFOHEADER *pVih = reinterpret_cast<VIDEOINFOHEADER*>(pMediaType->pbFormat); pVih->bmiHeader.biCompression = BI_RLE8; pVih->bmiHeader.biSizeImage = DIBSIZE(pVih->bmiHeader); return S_OK; } // else return VFW_S_NO_MORE_ITEMS; } 這個例子函數中, 調用了IPin::ConnectionMediaType從輸入pin上得到輸入的媒體類型。 然後改變了媒體類型結構的幾個filed, 表示是壓縮格式 1 It assigns a new subtype GUID, which is constructed from the FOURCC code 'MRLE', using the FOURCCMap class. 2 It calls the CMediaType::SetVariableSize method, which sets the bFixedSizeSamples flag to FALSE and the lSampleSize member to zero, indicating variable-sized samples. 3 It calls the CMediaType::SetTemporalCompression method with the value FALSE, indicating that every frame is a key frame. (This field is informational only, so you could safely ignore it.) 4 It sets the biCompression field to BI_RLE8. 5 It sets the biSizeImage field to the image size. 3 virtual HRESULT CheckTransform(const CMediaType* mtIn, const CMediaType* mtOut) PURE; CTransformFilter::CheckTransform檢查輸出的媒體類型和輸入的媒體類型是否匹配。 當輸入pin在輸出pin連接之後才開始連接的時候, 輸出pin會調用這個函數來檢查輸出媒體類型是否和輸入媒體類型是否匹配。 下面的例子演示了查詢數據的格式是否為RLE8視頻, 圖像的大小是否和輸入的匹配, 調色板的入口是否一致, 如果圖像大小不一致就要拒絕 HRESULT CRleFilter::CheckTransform( const CMediaType *mtIn, const CMediaType *mtOut) { // Check the major type. if (mtOut->majortype != MEDIATYPE_Video) { return VFW_E_TYPE_NOT_ACCEPTED; } // Check the subtype and format type. FOURCCMap fccMap = FCC('MRLE'); if (mtOut->subtype != static_cast<GUID>(fccMap)) { return VFW_E_TYPE_NOT_ACCEPTED; } if ((mtOut->formattype != FORMAT_VideoInfo) || (mtOut->cbFormat < sizeof(VIDEOINFOHEADER))) { return VFW_E_TYPE_NOT_ACCEPTED; } // Compare the bitmap information against the input type. ASSERT(mtIn->formattype == FORMAT_VideoInfo); BITMAPINFOHEADER *pBmiOut = HEADER(mtOut->pbFormat); BITMAPINFOHEADER *pBmiIn = HEADER(mtIn->pbFormat); if ((pBmiOut->biPlanes != 1) || (pBmiOut->biBitCount != 8) || (pBmiOut->biCompression != BI_RLE8) || (pBmiOut->biWidth != pBmiIn->biWidth) || (pBmiOut->biHeight != pBmiIn->biHeight)) { return VFW_E_TYPE_NOT_ACCEPTED; } // Compare source and target rectangles. RECT rcImg; SetRect(&rcImg, 0, 0, pBmiIn->biWidth, pBmiIn->biHeight); RECT *prcSrc = &((VIDEOINFOHEADER*)(mtIn->pbFormat))->rcSource; RECT *prcTarget = &((VIDEOINFOHEADER*)(mtOut->pbFormat))->rcTarget; if (!IsRectEmpty(prcSrc) && !EqualRect(prcSrc, &rcImg)) { return VFW_E_INVALIDMEDIATYPE; } if (!IsRectEmpty(prcTarget) && !EqualRect(prcTarget, &rcImg)) { return VFW_E_INVALIDMEDIATYPE; } // Check the palette table. if (pBmiOut->biClrUsed != pBmiIn->biClrUsed) { return VFW_E_TYPE_NOT_ACCEPTED; } DWORD cbPalette = pBmiOut->biClrUsed * sizeof(RGBQUAD); if (mtOut->cbFormat < sizeof(VIDEOINFOHEADER) + cbPalette) { return VFW_E_TYPE_NOT_ACCEPTED; } if (0 != memcmp(pBmiOut + 1, pBmiIn + 1, cbPalette)) { return VFW_E_TYPE_NOT_ACCEPTED; } // Everything is good. return S_OK; } 第四步 設置Allocator屬性 當連個pin就某個媒體類型達成一致協議的時候, 他們就選擇一個allocator, 就allocator的屬性進行設置, 比如buffer大小, buffer的數量。 在CTransformFilter 類中, 有兩個allocator, 一個用於上遊的pin的連接, 一個用於下遊的pin的連接, 上遊的filter選擇upstream allocator設置屬性, 無論上遊的filter怎麼設置這個upstream allocator, 輸入pin都會接受, 如果你想改變這個中狀況, 你可以繼承CBaseInputPin::NotifyAllocator函數 Transform filter的輸出pin選擇下遊的allocator, 步驟如下 1 如果下遊的filter可以提供一個allocator, 那麼輸出pin就使用這個allocator, 否則, 輸出pin就創建一個新的allocator。 2 輸出pin通過下遊filter的輸入pin上的IMemInputPin::GetAllocatorRequirements.方法來確定下遊filter的allocator的要求。 3 輸出pin調用transform filter上的CTransformFilter::DecideBufferSize函數, 這個函數也是一個純虛的函數, virtual HRESULT DecideBufferSize( IMemAllocator * pAllocator, ALLOCATOR_PROPERTIES *pprop) PURE; 這個函數有一個指向allocator的指針, 和一個指向ALLOCATOR_PROPERTIES結構的指針, 這個指針包含了對allocator的屬性的設置, 如果下遊的filter對allocator沒有設置屬性, 那麼這個結構就是NULL。 4在DecideBufferSize方法中, 派生類的函數通過調用IMemAllocator::SetProperties.函數來設置allocator的屬性。 通常, 派生類會根據輸出的格式, 下遊filter得要求, 自身得要求來設置allocator的屬性, allocator屬性的設置要符合下遊filter的要求, 否則的話, 連接就可能被拒絕。 下面的例子中, HRESULT CRleFilter::DecideBufferSize( IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pProp) { AM_MEDIA_TYPE mt; HRESULT hr = m_pOutput->ConnectionMediaType(&mt); if (FAILED(hr)) { return hr; } ASSERT(mt.formattype == FORMAT_VideoInfo); BITMAPINFOHEADER *pbmi = HEADER(mt.pbFormat); pProp->cbBuffer = DIBSIZE(*pbmi) * 2; if (pProp->cbAlign == 0) { pProp->cbAlign = 1; } if (pProp->cBuffers == 0) { pProp->cBuffers = 1; } // Release the format block. FreeMediaType(mt); // Set allocator properties. ALLOCATOR_PROPERTIES Actual; hr = pAlloc->SetProperties(pProp, &Actual); if (FAILED(hr)) { return hr; } // Even when it succeeds, check the actual result. if (pProp->cbBuffer > Actual.cbBuffer) { return E_FAIL; } return S_OK; } 即使SetProperties函數成功, 你也要檢查結果, 以確保滿足你的需要 缺省的情況下, 所有的filter都採用CMemAllocator類類分配內存, 這個類從客戶進程的虛擬地址中分配內存, 如果你的filter需要其它的內存, 比如, DirectDraw表面, 你可以派生一個通用的allocator, 你可以從CBaseAllocator類派生一個新的類, 根據不同的pin使用你的派生的新的allocator類, 你需要繼承不同的函數, Input pin: CBaseInputPin::GetAllocator and CBaseInputPin::NotifyAllocator. Output pin: CBaseOutputPin::DecideAllocator. 如果其它的filter拒絕使用你的custom allocator, 你的filter和其它filter連接的時候就會失敗, 第五步 傳遞媒體數據 上遊filter通過調用filter上輸入pin上的IMemInputPin::Receive方法, 將sample傳遞到filter, filter調用CTransformFilter::Transform方法來處理數據, 注意, 這個方法也是一個純虛的函數, 你要是想用, 你必須提供函數實現。 CTransformFilter::Transform有兩個指針, 一個指向輸入sample, 一直只想輸出smaple, 再調用這個方法之前, 要將sample從輸入sample拷貝到輸出sample。 如果transform返回S_ok, filter就將sample傳遞到下遊的filter。 下面的代碼演示了RLE encoder如何實現這個函數的, 你可以參考一下, 當然你的函數和這個是不一樣的。 要注意 HRESULT CRleFilter::Transform(IMediaSample *pSource, IMediaSample *pDest) { // Get pointers to the underlying buffers. BYTE *pBufferIn, *pBufferOut; hr = pSource->GetPointer(&pBufferIn); if (FAILED(hr)) { return hr; } hr = pDest->GetPointer(&pBufferOut); if (FAILED(hr)) { return hr; } // Process the data. DWORD cbDest = EncodeFrame(pBufferIn, pBufferOut); KASSERT((long)cbDest <= pDest->GetSize()); pDest->SetActualDataLength(cbDest); pDest->SetSyncPoint(TRUE); return S_OK; } 需要注意的幾個問題 1 時間戳, CTransformFilter在調用Transform方法之前就給輸出sample打上了時間戳, 它僅僅是從輸入的stample上講時間戳拷貝過來, 不做任何的改動, 如果你的filter需要改動時間戳, 你可以調用輸出sample上的IMediaSample::SetTime方法 2 數據格式的改變 上遊的filter可以 動態的改變數據的格式, 在改動數據的格式之前, 它要調用你的輸入pin上的IPin::QueryAccept方法, 在filter上, 這個方法的調用會引起CheckInputType, 和CheckTransform的方法的調用。 下遊的filter也可以改變數據格式, 機理和這個一樣。 在你的filter中, 需要做兩件事情 1 )要確保QueryAccept返回正確 2 )如果你的filter不接受數據格式的改變, 那麼就在你的filter的Transform方法中調用IMediaSample::GetMediaType.方法, 如果這個方法返回s_ok, 那麼你的filter就要適用數據的改變。 3線程, 在CTransformFilter中, filter在Receive方法同步的發送輸出sample。 Filter沒有創建任何的線程來處理數據。 第六步支持COM特性 最後一步是支持com屬性 添加com支持的步驟和前面一樣, 並且在前面也講述的很清楚了, 下面列出必須的幾個要素 1 引用計數Reference Counting, 接口查詢QueryInterface 很簡單, 從基類派生即可 CMyFilter : public CBaseFilter, public IMyCustomInterface { public: DECLARE_IUNKNOWN STDMETHODIMP NonDelegatingQueryInterface(REFIID iid, void **ppv); }; STDMETHODIMP CMyFilter::NonDelegatingQueryInterface(REFIID iid, void **ppv) { if (riid == IID_IMyCustomInterface) { return GetInterface(static_cast<IMyCustomInterface*>(this), ppv); } return CBaseFilter::NonDelegatingQueryInterface(riid,ppv); } 2對象的創建Object Creation CUnknown * WINAPI CRleFilter::CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr) { CRleFilter *pFilter = new CRleFilter(); if (pFilter== NULL) { *pHr = E_OUTOFMEMORY; } return pFilter; } 模板數組 static WCHAR g_wszName[] = L"My RLE Encoder"; CFactoryTemplate g_Templates[] = { { g_wszName, &CLSID_RLEFilter, CRleFilter::CreateInstance, NULL, NULL } }; int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]); 3組件的註冊 // Declare media type information. FOURCCMap fccMap = FCC('MRLE'); REGPINTYPES sudInputTypes = { &MEDIATYPE_Video, &GUID_NULL }; REGPINTYPES sudOutputTypes = { &MEDIATYPE_Video, (GUID*)&fccMap }; // Declare pin information. REGFILTERPINS sudPinReg[] = { // Input pin. { 0, FALSE, // Rendered? FALSE, // Output? FALSE, // Zero? FALSE, // Many? 0, 0, 1, &sudInputTypes // Media types. }, // Output pin. { 0, FALSE, // Rendered? TRUE, // Output? FALSE, // Zero? FALSE, // Many? 0, 0, 1, &sudOutputTypes // Media types. } }; // Declare filter information. REGFILTER2 rf2FilterReg = { 1, // Version number. MERIT_DO_NOT_USE, // Merit. 2, // Number of pins. sudPinReg // Pointer to pin information. }; STDAPI DllRegisterServer(void) { HRESULT hr = AMovieDllRegisterServer2(TRUE); if (FAILED(hr)) { return hr; } IFilterMapper2 *pFM2 = NULL; hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER, IID_IFilterMapper2, (void **)&pFM2); if (SUCCEEDED(hr)) { hr = pFM2->RegisterFilter( CLSID_RLEFilter, // Filter CLSID. g_wszName, // Filter name. NULL, // Device moniker. &CLSID_VideoCompressorCategory, // Video compressor category. g_wszName, // Instance data. &rf2FilterReg // Filter information. ); pFM2->Release(); } return hr; } STDAPI DllUnregisterServer() { HRESULT hr = AMovieDllRegisterServer2(FALSE); if (FAILED(hr)) { return hr; } IFilterMapper2 *pFM2 = NULL; hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER, IID_IFilterMapper2, (void **)&pFM2); if (SUCCEEDED(hr)) { hr = pFM2->UnregisterFilter(&CLSID_VideoCompressorCategory, g_wszName, CLSID_RLEFilter); pFM2->Release(); } return hr; } 有時候, 你的filter並不總是通過DLL提供的, 有時, 你可能給一個特定的程序寫了一個特定的filter, 那麼你就可以直接用你的filter, 你可以直接用new方法, 如下 #include "MyFilter.h" // Header file that declares the filter class. // Compile and link MyFilter.cpp. int main() { IBaseFilter *pFilter = 0; { // Scope to hide pF. CMyFilter* pF = new MyFilter(); if (!pF) { printf("Could not create MyFilter.\n"); return 1; } pF->QueryInterface(IID_IBaseFilter, reinterpret_cast<void**>(&pFilter)); } /* Now use pFilter as normal. */ pFilter->Release(); // Deletes the filter. return 0; }