天天看点

利用DirectShow开发自己的Filter

学习directshow已经有几天了,下面将自己的学习心得写下来,希望对其他的人有帮助。 filter实质是个com组件,所以学习开发filter之前你应该对com的知识有点了解。com组件的实质是一个实现了纯虚指针接口的c++对象。关于com的东西,这里不多讲。

  <b>一 给vc配置directshow的开发环境</b>

  无论开发filter还是开发dshow的应用程序都要配置一下开发环境的,其实就是包含一下dshow用到的头文件和动态库。 选择tools菜单下面的options。在弹出的option对话框配置如下

利用DirectShow开发自己的Filter

图1 添加头文件

  选择动态库文件添加到工程中

利用DirectShow开发自己的Filter

图2 添加动态库

  <b>二 创建工程以及filter的入口函数</b>

  创建工程

  一般情况下,创建filter使用一个普通的win32 dll项目。而且,一般filter项目不使用mfc。这时,应用程序通过cocreateinstance函数filter实例; filter与应用程序在二进制级别的协作。另外一种方法,也可以在mfc的应用程序项目中创建filter。

  在vc里新建一个工程,选择win32动态库,如下图

利用DirectShow开发自己的Filter

图3

利用DirectShow开发自己的Filter

                  图4

       这样生成了一个简单的dll,只有一个dllmain入口函数。

  下面我要给这个filter添加入口函数了。

  filter是个基于dll的com组件,所以一般的filter都要实现下面几个入口函数

              dllmain

             dllgetclassobject 

             dllcanunloadnow

             dllregisterserver 

             dllunregisterserver

dllmain

dllgetclassobject

dllcanunloadnow

dllregisterserver

dllunregisterserver

  首先定义导出函数

  要导出这些函数有两种方法,一是在定义函数时使用导出关键字_declspec(dllexport),另外一种方法是在创建dll文件时使用模块定义文件.def。使用导出函数关键字_declspec(dllexport)创建mydll.dll就是在 .h文件中定义定义函数如下:

extern "c" _declspec(dllexport)bool dllregisterserver;

       extern "c" _declspec(dllexport)bool dllregisterserver;

  为了用.def文件创建dll,往该工程中加入一个文本文件,命名为mydll.def,再在该文件中加入如下代码:

        rary myfilter.ax 

        exports

        dllmain private

        dllgetclassobject private

        dllcanunloadnow private

        dllregisterserver private

        dllunregisterserver private

library myfilter.ax

exports

dllmain private

dllgetclassobject private

dllcanunloadnow private

dllregisterserver private

dllunregisterserver private

  其中library语句说明该def文件是属于相应dll的,exports语句下列出要导出的函数名称。我们可以在.def文件中的导出函数后加@n,如max@1,min@2,表示要导出的函数顺序号,在进行显式连时可以用到它。该dll编译成功后,打开工程中的debug目录,同样也会看到mydll.dll和mydll.lib文件。

  然后要定义这些函数的实现了,其实这些工作dshow的基类里都已经替我们做好了,我们所要做的就拿来用就是了,最重要的三个函数的实现一般如下

 

stdapi dllregisterserver()

{

 return amoviedllregisterserver2(true);

}

stdapi dllunregisterserver()

 return amoviedllregisterserver2(false);

extern "c" bool winapi dllentrypoint(hinstance, ulong, lpvoid);

bool apientry dllmain(handle hmodule, dword dwreason, lpvoid lpreserved)

 return dllentrypoint((hinstance)(hmodule), dwreason, lpreserved);

  其中dllentrypoint 是在c:\dx90sdk\samples\c++\directshow\baseclasses\dllentry.cpp定义的,如果感兴趣我们可以去看看它的定义。 amoviedllregisterserver2函数是在下面 c:\dx90sdk\samples\c++\directshow\baseclasses\dllsetup.cpp这个文件定义的,具体实现可以自己看看。

  到了这里你恐怕要做点工作,还是要设置一下你的项目环境,否则恐怕你编译是通不过的,因为你用到了基类的一些东西,所以你要将你的dshow基类的定义和库文件包含进来。首先包含

#include streams.h

  其次在project –setting菜单下配置自己的filter输出的名字和连接的lib文件

利用DirectShow开发自己的Filter

图5

  其中library modules里的包含的动态库如下

c:\dx90sdk\samples\c++\directshow\baseclasses\debug\strmbasd.lib msvcrtd.lib quartz.lib vfw32.lib winmm.lib kernel32.lib advapi32.lib version.lib largeint.lib user32.lib gdi32.lib comctl32.lib ole32.lib olepro32.lib oleaut32.lib uuid.lib

此时你编译一下,好像还是通不过,它提示有一个全局的用于实现com接口的变量没有定义,不着急,下面我们就开始实现filter的com接口。

三 如何实现filter 的类厂对象

  我们知道一个filter是一个com组件,所以它com特性的实现其实在其基类中实现的,比如iunknown接口,我们直接从基类派生出我们的filter后,它就支持com接口了,它就是一个com组件了。

  所有的com组件为了实现二进制的封装,所以连创建的接口都封装了,因此每个com对象都有个类对象(也叫类厂对象,本身也是com对象,用来创建com组件)来创建com组件。

  下面温习一下com组件的创建过程,其中涉及到几个函数

  1、当客户端要创建一个com组件时,它通过底层的com api函数 cogetclassobject()使用scm的服务,这个函数请scm把一个指针绑定到客户端请求的com组件的类对象上,其实在cogetclassobject()里它装载了该dll的库,通过该dll的导出函数dllgetclassobject();dllgetclassobject根据客户端提供的com组件classid,返回该com组件类对象的指针。下面com组件的创建和scm无关了。

  2、客户端利用组件的类对象(类厂对象)的iclassfactory::createinstance方法创建com组件。

  filter在这里使用了一个类厂模板类来当作filter的类厂对象。下面看看类厂在dshow是怎么工作的。

  类厂对象也是一个com组件。本来dllgetclassobject是应该由我们自己完成一个函数,在directshow基类里已经完成了,我们不用管它了。它的功能就是来寻找这个dll中的类厂对象,看是否有符合客户端请求的类厂对象。

  dll里声明了一个全局的类厂模板数组,当dllgetclassobject请求类厂对象的时候,它就搜索这个数组,看是否有和clsid匹配的类厂对象。当它找到一个匹配的clsid,它就创建一个类厂对象,然后讲类厂指针返回给cogetclassobject,然后客户端可以根据返回去的类厂指针,调用 iclassfactory::createinstance方法创建组件,类厂就根据数组里定义的方法创建com组件。

  factory template包含下列变量:

const wchar * m_name; // name

const clsid * m_clsid; // clsid

lpfnnewcomobject m_lpfnnew; // function to create an instance of the component

lpfninitroutine m_lpfninit; // initialization function (optional)

const amoviesetup_filter * m_pamoviesetup_filter; // set-up information (for filters)

      const wchar * m_name; // name

      const clsid * m_clsid; // clsid

      lpfnnewcomobject m_lpfnnew; // function to create an instance of the component

      lpfninitroutine m_lpfninit; // initialization function (optional)

      const amoviesetup_filter * m_pamoviesetup_filter; // set-up information (for filters)

  其中的两个函数指针m_lpfnnew and m_lpfninit使用下面的定义

typedef cunknown *(callback *lpfnnewcomobject)(lpunknown punkouter, hresult *phr);

typedef void (callback *lpfninitroutine)(bool bloading, const clsid *rclsid);

        typedef cunknown *(callback *lpfnnewcomobject)(lpunknown punkouter, hresult *phr);

  你可以参照如下的方式定义你的类厂对象

cunknown * winapi cmyfilter::createinstance(lpunknown punk, hresult *phr)

 cmyfilter *pfilter = new cmyfilter(name("my filter"), punk, phr);

 if (pfilter== null)

 {

  *phr = e_outofmemory;

 }

 return pfilter;

  你可以声明自己的类厂数组如下:

cfactorytemplate g_templates[1] =

  l"my filter", // name

  &amp;clsid_myfilter, // clsid

  cmyfilter::createinstance, // method to create an instance of mycomponent

  null, // initialization function

  &amp;sudinftee // set-up information (for filters)

};

int g_ctemplates = sizeof(g_templates) / sizeof(g_templates[0]);

  如果在这个com组件中你要支持多个filter,你可以在这个数组中继续添加就是了。

四 如何实现自己的filter

  在这里就要讲如何创建自己的filter了,下面我们以写一个ctransformfilter为例

  1、选择一个基类,声明自己的类

  创建filter很简单,你只要根据自己的需要选择不同的基类filter派生出自己的filter,它就已经支持com特性了。

  从逻辑上考虑,在写filter之前,选择一个合适的filter基类是至关重要的。为此,你必须对几个filter的基类有相当的了解。在实际应用中,filter的基类并不总是选择cbasefilter的。相反,因为我们绝大部分写的都是中间的传输filter(transform filter),所以基类选择ctransformfilter和ctransinplacefilter的居多。如果我们写的是源filter,我们可以选择csource作为基类;如果是renderer filter,可以选择cbaserenderer或cbasevideorenderer等。

  总之,选择好filter的基类是很重要的。当然,选择filter的基类也是很灵活的,没有绝对的标准。能够通过ctransformfilter实现的filter当然也能从cbasefilter一步一步实现。

  下面,笔者就从本人的实际经验出发,对filter基类的选择提出几点建议供大家参考。

  首先,你必须明确这个filter要完成什么样的功能,即要对filter项目进行需求分析。请尽量保持filter实现的功能的单一性。如果必要的话,你可以将需求分解,由两个(或者更多的)功能单一的filter去实现总的功能需求。

  其次,你应该明确这个filter大致在整个filter graph的位置,这个filter的输入是什么数据,输出是什么数据,有几个输入pin、几个输出pin等等。你可以画出这个filter的草图。弄清这一点十分重要,这将直接决定你使用哪种“模型”的filter。比如,如果filter仅有一个输入pin和一个输出pin,而且一进一处的媒体类型相同,则一般采用ctransinplacefilter作为filter的基类;如果媒体类型不一样,则一 般选择ctransformfilter作为基类。

  再者,考虑一些数据传输、处理的特殊性要求。比如filter的输入和输出的sample并不是一一对应的,这就一般要在输入pin上进行数据的缓存,而在输出pin上使用专门的线程进行数据处理。这种情况下,filter的基类选择csource为宜(虽然这个filter并不是源filter)。当filter的基类选定了之后,pin的基类也就相应选定了。接下去,就是filter和pin上的代码实现了。有一点需要注意的是,从软件设计的角度上来说,应该将你的逻辑类代码同filter的代码分开。下面,我们一起来看一下输入pin的实现。你需要实现基类所有的纯虚函数,比如checkmediatype等。在checkmediatype内,你可以对媒体类型进行检验,看是否是你期望的那种。因为大部分filter采用的是推模式传输数据,所以在输入pin上一般都实现了receive方法。有的基类里面已经实现了receive,而在filter类上留一个纯虚函数供用户重载进行数据处理。这种情况下一般是无需重载receive方法的,除非基类的实现不符合你的实际要求。而如果你重载了receive方法,一般会同时重载以下三个函数endofstream、beginflush和endflush。我们再来看一下输出pin的实现。一般情况下,你要实现基类所有的纯虚函数,除了checkmediatype进行媒体类型检查外,一般还有decidebuffersize以决定sample使用内存的大小,getmediatype提供支持的媒体类型。

最后,我们看一下filter类的实现。首先当然也要实现基类的所有纯虚函数。除此之外,filter还要实现createinstance以提供com的入口,实现nondelegatingqueryinterface以暴露支持的接口。如果我们创建了自定义的输入、输出pin,一般我们还要重载getpincount和getpin两个函数。

  这里我主要为了举例,所以简单写的filter没有pin接口,但在我的demo里的filter,却是有个out pin和一个input pin。

我的filter类的定义如下:

class cmyfilter : public ccritsec, public cbasefilter

 public:

  cmyfilter(tchar *pname,lpunknown punk,hresult *hr);

  virtual ~cmyfilter();

  static cunknown * winapi createinstance(lpunknown punk, hresult *phr);

  cbasepin *getpin(int n);

  int getpincount();

  注:因为基类是一个纯虚的基类,所以在你的filter一定要派生一个其中的纯虚函数,否则编译器会提示你的派生类也是一个纯虚类,

你在创建这个com组件对象的时候,纯虚类是没法创建对象的。

  2、给自己的filter生成一个clsid

// {1915c5c7-02aa-415f-890f-76d94c85aaf1}

define_guid(clsid_myfilter,

0x1915c5c7, 0x2aa, 0x415f, 0x89, 0xf, 0x76, 0xd9, 0x4c, 0x85, 0xaa, 0xf1);

  你可以用guidgen or uuidgen给自己的filter生成一个128位的id号,然后利用define_guid宏在filter的头文件声明该filter的clsid;

[myfilter.h]

  这个clsid_myfilter在类厂数组用到,在注册filter时也要用到。

  3 cmyfilter类的简单实现

  这个类纯粹为了演示用,所以特别简单,你可以参考我的demo,那个filter写的功能比较全。

cmyfilter::cmyfilter(tchar *pname,lpunknown punk,hresult *hr)

:cbasefilter(name("my filter"), punk, this, clsid_myfilter)

{ }

cmyfilter::~cmyfilter()

{}

// public method that returns a new instance.

cbasepin * cmyfilter::getpin(int n)

 return null;

int cmyfilter::getpincount()

 return 0;

  这样基本上就实现了一个filter,但是这个filter没有与之相联系的pin,但是实现filter的基本过程就时这样了,至于逻辑上的东西,比如filter和pin如何连接,数据流是如何流动的,你都要去看看sdk了,按照上面的步骤你就可以写一个filter的框架出来。

  下面我们总结一下写一个filter至少需要那些东西。

  1、filter的实现类

  在这里就是cmyfilter类,在这个类里你可以实现自己的逻辑上的功能,包括定义你的filter的特性,给你的filter配备pin接口等。

  2、com组件的引出函数

  五个全局函数:

  dllmain //dll的入口函数

  dllgetclassobject //获得com组件的类厂对象

  dllcanunloadnow //com组件是否可以卸载

  dllregisterserver //注册com组件

  dllunregisterserver //卸载com组件

  其中dllgetclassobject 已经由基类完成你自己只要完成三个函数即可dllmain,dllregisterserver,dllunregisterserver。

  3、com组件的类厂对象

  类厂对象是用来生成filter对象的,用的模板类定义了一个全局的模板类对象数组,一般格式如下

  4、关于你自己定义的filter以及pin的信息

  这些是一个全局的结构变量,用于描述你的filter和你定义的pin,在注册filter的时候会用到,如下

  amoviesetup_filter 描述一个filter

  amoviesetup_pin 描述pin

  amoviesetup_mediatype 描述数据类型

  下面的代码描述了一个filter带有一个output pin

static const wchar g_wszname[] = l"some filter";

amoviesetup_mediatype sudmediatypes[] = {

 { &amp;mediatype_video, &amp;mediasubtype_rgb24 },

 { &amp;mediatype_video, &amp;mediasubtype_rgb32 },

amoviesetup_pin sudoutputpin = {

 l"", // obsolete, not used.

 false, // is this pin rendered?

 true, // is it an output pin?

 false, // can the filter create zero instances?

 false, // does the filter create multiple instances?

 &amp;guid_null, // obsolete.

 null, // obsolete.

 2, // number of media types.

 sudmediatypes // pointer to media types.

amoviesetup_filter sudfilterreg = {

 &amp;clsid_somefilter, // filter clsid.

 g_wszname, // filter name.

 merit_normal, // merit.

 1, // number of pin types.

 &amp;sudoutputpin // pointer to pin information.

  最后如果你还是调试通不过,看看你是否包含了下面的头文件

#include initguid.h

#include tchar.h

#include stdio.h