当Filter的Pin之间连接完成,也就是说,连接双方通过协商取得了一种大家都支持的媒体类型之后,即开始为数据传输做准备。这些准备工作中,最重要的是Pin上的内存分配器的协商,一般也是由输出Pin发起。在DirectShow Filter之间,数据是通过一个一个数据包传送的,这个数据包叫做Sample。Sample本身是一个COM对象,拥有一段内存用以装载数据,Sample就由内存分配器(Allocator)来统一管理。已成功连接的一对输出、输入Pin使用同一个内存分配器,所以数据从输出Pin传送到输入Pin上是无需内存拷贝的。而典型的数据拷贝,一般发生在Filter内部,从Filter的输入Pin上读取数据后,进行一定意图的处理,然后在Filter的输出Pin上填充数据,然后继续往下传输。下面,我们就具体阐述一下Filter之间的数据传送。
首先,大家要区分一下Filter的两种主要的数据传输模式:推模式(Push Model)和拉模式(Pull Model)。
所谓推模式,即源Filter(Source Filter)自己能够产生数据,并且一般在它的输出Pin上有独立的子线程负责将数据发送出去,常见的情况如代表WDM模型的采集卡的Live Source Filter;而所谓拉模式,即源Filter不具有把自己的数据送出去的能力,这种情况下,一般源Filter后紧跟着接一个Parser Filter或Splitter Filter,这种Filter一般在输入Pin上有个独立的子线程,负责不断地从源Filter索取数据,然后经过处理后将数据传送下去,常见的情况如文件源。推模式下,源Filter是主动的;拉模式下,源Filter是被动的。而事实上,如果将上图拉模式中的源Filter和Splitter Filter看成另一个虚拟的源Filter,则后面的Filter之间的数据传输也与推模式完全相同。
那么,数据到底是怎么通过连接着的Pin传输的呢?首先来看推模式。在源Filter后面的Filter输入Pin上,一定实现了一个IMemInputPin接口,数据正是通过上一级Filter调用这个接口的Receive方法进行传输的。值得注意的是(上面已经提到过),数据从输出Pin通过Receive方法调用传输到输入Pin上,并没有进行内存拷贝,它只是一个相当于数据到达的“通知”。再看一下拉模式。拉模式下的源Filter的输出Pin上,一定实现了一个IAsyncReader接口;其后面的Splitter Filter,就是通过调用这个接口的Request方法或者SyncRead方法来获得数据。Splitter Filter然后像推模式一样,调用下一级Filter输入Pin上的IMemInputPin接口Receive方法实现数据的往下传送。深入了解这部分内容,请认真研读SDK的基类源代码(位于DXSDK\samples\Multimedia\DirectShow\BaseClasses\source.cpp和pullpin.cpp)。
下面,我们来讲一下流的定位(Media Seeking)。在GraphEdit中,当我们成功构建了一个Filter Graph之后,我们就可以播放它。在播放中,我们可以看到进度条也在相应地前进。当然,我们也可以通过拖动进度条,实现随机访问。要做到这一点,在应用程序级别应该可以知道Filter Graph总共要播放多长时间,当前播放到什么位置等等。那么,在Filter级别,这一点是怎么实现的呢?
我们知道,若干个Filter通过Pin的相互连接组成了Filter Graph。而这个Filter Graph是由另一个COM对象Filter Graph Manager来管理的。通过Filter Graph Manager,我们就可以得到一个IMediaSeeking的接口来实现对流媒体的定位。在Filter级别,我们可以看到,Filter Graph Manager首先从最后一个Filter(Renderer Filter)开始,询问上一级Filter的输出Pin是否支持IMediaSeeking接口。如果支持,则返回这个接口;如果不支持,则继续往上一级Filter询问,直到源Filter。一般在源Filter的输出Pin上实现IMediaSeeking接口,它告诉调用者总共有多长时间的媒体内容,当前播放位置等信息。(如果是文件源,一般在Parser Filter或Splitter Filter实现这个接口。)对于Filter开发者来说,如果我们写的是源Filter,我们就要在Filter的输出Pin上实现IMediaSeeking这个接口;如果写的是中间的传输Filter,只需要在输出Pin上将用户的获得接口请求往上传递给上一级Filter的输出Pin;如果写的是Renderer Filter,需要在Filter上将用户的获得接口请求往上传递给上一级Filter的输出Pin。进一步的了解,请认真研读SDK的基类源代码(位于DXSDK\samples\Multimedia\DirectShow\BaseClasses\transfrm.cpp的类方法CTransformOutputPin::NonDelegatingQueryInterface实现和ctlutil.cpp中类CPosPassThru的实现)。
以上我们介绍了一下如何学习DirectShow Filter开发,以及一些开始写自己的Filter之前的预备知识。下一讲,笔者将根据自己开发Filter的经验,手把手教你如何写自己的Filter。
如何写自己的Filter
首先,从VC++的项目开始(请确认你已经给VC++配置好了DirectX的开发环境)。写自己的Filter,第一步是使用VC++建立一个Filter的项目。由于DirectX SDK提供了很多Filter的例子项目(位于DXSDK\samples\Multimedia\DirectShow\ Filters目录下),最简单的方法就是拷贝一个,然后再在此基础上修改。但如果你是Filter开发的初学者,笔者并不赞成这么做。
自己新建一个Filter项目也很简单。使用VC++的向导,建立一个空的”Win32 Dynamic-link Library”项目。注意,几个文件是必须有的:.def文件,定义四个导出函数;定义Filter类的.cpp文件和.h文件,并在.cpp文件中定义Filter的注册信息以及两个Filter的注册函数:DllRegisterServer和DllUnregisterServer。(注:Filter的注册信息是Filter在注册时写到注册表里的内容,格式可以参考SDK的示例代码,Filter相关的GUID务必使用GuidGen.exe产生。)接下去进行项目的设置(Project->Settings…)。此时,你可以打开一个SDK的例子项目进行对比,有些宏定义完全可以照抄,最后注意将输出文件的扩展名改为.ax。
上一讲曾经提到过,在写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框架的实现大致就是这样。你或许还想知道怎样在Filter上实现一个自定义的接口,以及怎么实现Filter的属性页等等。限于篇幅,笔者就不展开阐述了。其实,这些问题都能在SDK的示例项目中找到答案。其他的,关于在实际编程中应该注意的一些问题,笔者整理了一下,供大家参考。
1. 锁(Lock)问题
DirectShow应用程序至少包含有两条线程:一条主线程和一条数据传输线程。既然是多线程,肯定会碰到线程同步的问题。Filter有两种锁:Filter对象锁和数据流锁。Filter对象锁用于Filter级别的如Filter状态转换、BeginFlush、EndFlush等;数据流锁用于数据处理线程内,比如Receive、EndOfStream等。如果这两种锁没有搞清楚,很容易产生程序的死锁,这一点特别需要提醒。
2. EndOfStream问题
当Filter接收到这个“消息”,意味着上一级Filter的数据都已经发送完毕。在这之后,如果Receive再有数据接收,也不应该去理睬它。如果Filter对输入Pin上的数据进行了缓存,在接收到EndOfStream后应确保所有缓存的数据都已经处理过了才能返回。
3. Media Seeking问题
一般情况下,你只需要在Filter的输出Pin上实现NonDelegatingQueryInterface方法,当用户申请得到IID_ImediaPosition接口或IID_IMediaSeeking接口时将请求往上一级Filter的输出Pin上传递。当Filter Graph进行Mediaseeking的时候,一般会调用Filter上的BeginFlush、EndFlush和NewSegment。如果你的Filter对数据进行了缓存,你就要重载它们,并做出相应的处理。如果你的Filter负责给发送出去的Sample打时间戳,那么,在Mediaseeking之后应该重新从零开始打起。
4. 关于使用专门的线程
如果你使用了专门的线程进行数据的处理和发送,你需要特别小心,不要让线程进行死循环,并且要让线程处理函数能够去时时检查线程命令。应该确保在Filter结束工作的时候,线程也能正常地结束。有时候,你把GraphEdit程序关掉,但GraphEdit进程仍在内存中,往往就是因为数据线程没有安全关闭这个原因。
5. 如何从媒体类型中获取信息
比如,你想在输入Pin连接的媒体类型中,获取视频图像的宽、高等信息,你应该在输入Pin的CompleteConnect方法中实现,而不要在SetMediaType中。
DirectX媒体对象(DirectX Media Objects,简称DMOs),是微软提供的另一种流数据处理COM组件。与DirectShow filter相比,DMO有很多相似之处。对filter原理的熟悉,将会大大帮助你对DMO的学习。另外,DMO也因其结构简单、易于创建和使用而倍受微软推崇。
DMO与filter的对比
1. DMO比filter实现的功能要少很多,这使得DMO“体积”很小;
2. DMO使用起来比filter更有灵活性。DMO的使用不需要filter graph,应用程序可以直接与DMO交互。而DMO也可以通过一个DMO wrapper filter工作于DirectShow环境;
3. DMO总是同步处理数据,不像filter有独立的数据传送线程,需要考虑多线程编程问题;
4. 与传统的编解码管理器ACM、VCM相比,用DMO开发的编解码器是基于COM的,更易于扩展。并且DMO支持多个输入和多个输出;
5. DMO不需要像filter一样分配数据传送的内存,而有DMO的使用者负责;
6. DMO是一个独立功能模块,不需要像filter一样连接成一条链路;
7. DMO不需要像filter一样将数据“推”下去,数据的输入输出都是由DMO的使用者完成的;
所有这些优点,使得DMO成为微软对于Encoder和Decoder开发的重点推荐模式。DirectX 9.0 SDK中,微软更是把DMO从DirectShow中分离出来,而对于一些transform. filter,微软也推荐用DMO的方式来替换。
关于DMO的使用方式,目前大概有两种:一种是应用程序直接使用DMO,另一种就是在DirectShow filter中的应用。后者比较简单,只是使用了一个DMO wrapper filter。在DirectShow应用程序中,DMO是对用户透明的,所有使用DMO的工作均由DMO wrapper filter来完成。参见下面的代码。
// Create the DMO Wrapper filter. if (SUCCEEDED(hr)) if (SUCCEEDED(hr)) if (SUCCEEDED(hr)) |
而对于DMO的直接使用,以下几点是要特别注意的。
1. 在处理数据之前,必须为每条输入输出stream设置media type(Optional stream除外);
2. 从DMO从获取的media type未必包含format块,但是在给DMO设置media type时,务必带上这部分信息(MIDI除外);
3. 应用程序必须自己负责分配数据缓存。缓存的大小可以通过调用DMO的IMediaObject::GetInputSizeInfo或IMediaObject::GetOutputSizeInfo得到。DMO使用的数据缓存也是一个COM对象,支持ImediaBuffer接口,与DirectShow filter的Media Sample类似。
4. 一般的DMO依次调用IMediaObject::ProcessInput和IMediaObject::ProcessOutput处理数据,In-Place的DMO调用IMediaObjectInPlace::Process处理数据。两套方法不能混用。
5. 在调用ProcessOutput时,如果返回的标记是DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE,说明数据的数据还没有完全取出,需要再次调用ProcessOutput。
6. 所有输入数据都已输入完成,应该调用DMO的IMediaObject::Discontinuity方法。
7. 如果你想中断数据处理流程,调用DMO的IMediaObject::Flush。
8. 区别两种不同的可丢弃stream,标记分别为DMO_OUTPUT_STREAMF_OPTIONAL和DMO_OUTPUT_STREAMF_DISCARDABLE。注意,后者是要设置media type的。