摘要:本篇文档主要讲述了利用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; } 
|