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