DirectShow编程

3. 关于DirectShow 3.1. DirectShow体系概述     多媒体的难题     处理多媒体有几个主要的难题:     *多媒体流包含了巨大的数据量,而这些数据都必须非常快地被处理     *音频和视频必须同步,因此它们必须在同一时间开始或停止,并以同一速率播放     *数据可能来自很多的源,如本地文件、网络、电视广播和视频摄像机     *数据有各种各样的格式,如AVI、ASF、MPEG和DV     *程序员无法预知最终用户使用什么样的硬件设备     DirectShow的解决方案     DirectShow被设计成用来解决所有这些难题,它主要的设计目的就是通过将复杂的数据转输、硬件的多样性和同步问题从应用程序中独立出来,从而简化在windows平台上数字媒体应用程序的开发任务。     要实现数据高效地被处理,需要流化音视频数据,而DirectShow会尽可能地使用DirectDraw和DirectSound,从而高效地将数据送到用户的声音和图形设备中进行播放。同步则是通过在媒体数据中加入时间戳来实现。而DirectShow模块化的架构,使其可以轻松操纵变化多端的源、格式和硬件设备,在这样的架构里,应用程序只需组合和匹配多个filter来实现功能。     DirectShow提供的filter支持基于WDM的采集和调谐设备,也支持早先的VFW采集卡和为ACM和VCM接口编写的编码器。     下图显示了应用程序、DirectShow组件和DirectShow支持的硬件和软件组件之间的关系:         

    如图,DirectShow将应用程序与众多复杂的设备隔离开来,通信和控制这些设备均出DirectShow的filter来完成。DirectShow同样为某种文件格式提供与之对应的编解码器。

3.2. Filter Graph和它的组件     这一节描述了DirectShow的主要组件,为DirectShow应用程序和DirectShow Filter开发者提供一个介绍。应用程序开发者可以忽略掉很多底层部分,但是,了解底层对于理解DirectShow架构还是很有帮助的。 3.2.1. 关于DirectShow Filter     DirectShow使用一个模块化的架构,每个处理过程都由一个叫做filter的COM对象来实现。DirectShow为应用程序提供了一系列标准的filter,开发者也可以编写自己的filter来扩展DirectShow的功能。下面是播放一个AVI文件的各个步骤:     *从文件中读取数据并转成字节流(File Source filter)     *检查AVI头,分析字节流并将它们分离成视频和音频(AVI Aplitter filter)     *将视频解码(不同的解码filter,取决于不同的压缩格式)     *将视频显示出来(Video Renderer filter)     *将音频送入声卡(Default DirectSound Device filter)

    如图所示,每个filter与一个或多个其它的filter相连,其中的连接点也是一个COM对象,称作Pin,filter使用Pin将数据从一个filter转移到另一个,图中的箭头指示了数据流动的方向。在DirectShow中,这一系列连接在一起的filter称作filter graph。     Filter可能处于有三种不同的状态:运行、停止和暂停状态。filter在运行状态时处理数据,停止状态时停止处理数据,暂停状态则是表示就绪,可以开始进入运行状态。除了极个别的情况,一个filter Graph中的所有filter通常都处理同一个状态下,因此,filter graph也可以称其处于运行、停止、暂停状态。     Filter可以被分成几个大的种类:     *source filter - filter graph的数据源,这些数据可以来自文件、网络、摄像头或任何其它东西。每一个source filter操纵不同类型的数据源。     *transform filter - 接收数据,处理数据并将它送入下一个filter。编码filter和解码filter都属于这个种类。     *Renderer filter - 处于filter链的未端,接受数据并将其展现给用户。比如,一个视频renderer在显示器上绘制视频图像;一个音频renderer将音频数据送入声卡;一个写文件filter(file-writer filter)将数据存盘。     *splitter filter - 分析输入的数据流并将其分解成两路或多路,比如,AVI splitter分析字节流并将其分解成视频流和音频流。     *mux filter - 将多路输入流合并成一路。比如,AVI Mux正好与AVI splitter做相反的工作,它将视频和音频流合成为一个AVI格式的字节流。     以上的分类并不是绝对的,比如,ASF Reader Filter同时充当了source filter和splitter filter的角色。     所有的DirectShow filter都提供IBaseFilter接口,所有的Pin也都提供IPin接口。DirectShow也定义了许多其它的接口以实现特定的功能。

3.2.2. 关于Filter Graph Manager     Filter Graph Manager是一个用以控制filter graph中的filter的COM对象。它提供了许多功能,包括:     *协调filter之间的状态变化     *建立参考时钟(reference clock)     *将事件返回给应用程序     *提供应用程序建立filter graph的方法     这里先简单地描述一个这些功能。     状态变化:filter们的状态变化必须遵照一个特定的次序,因此,应用程序不能将状态变化的命令直接发给filter,而是将一个简单的命令发给filter graph manager,由它来将命令分发给各个filter。定位命令同样使用这种方式,应用程序发送一个定位命令给filter graph manager,由它来分发。     参考时钟:在filter graph中的所有filter都使用一个相同的时钟,称为参考时钟(reference clock)。参考时钟保证了所有流的同步。一个视频帧或一段音频样本被播放的时间钞称作呈现时间(presentation time)。呈现时间精确地相对于参考时钟。Filter Graph Manager通常选择的参考时钟是声卡参考时钟或系统时钟。     Graph事件:filter graph manager使用一个消息队列来通知应用程序发生在filter graph中的事件。     Graph-buliding 方法:filter graph manager提供给应用程序将filter加入到filter graph中的方法,以及将filter与filter连接或断开连接的方法。     Filter graph manager不提供操纵在filter之间流动数据的功能,这个功能由filter通过pin连接在一个单独的线程中自行完成。 3.2.3. 关于媒体类型(Media Type)     因为DirectShow是模块化的,因此需要有一个在filter graph各个点之间描述格式的方法。比如说,AVI回放,数据输入时是一个RIFF块的流,然后被分解成视频和音频流。视频流由一个个可能被压缩的视频帧组成,解压后,视频流又变成了一系列未压缩的位图。音频与视频类似。     Media Type:DirectShow怎样来描述格式     Media Type是描述数字媒体格式的常用方式。当两个filter连接时,它们需要协商决定同一个Media Type。Media Type标识了从上一个filter递交到下一个filter或物理层的数据流格式。如果两个filter对Media Type不能协商一致,则不能连接。     对于某些应用程序,你不必去关心Media type,比如文件回放,DirectShow做了所有有关它的事情。     Media type使用AM_MEDIA_TYPE结构体来定义,这个结构体包含了以下内容:     *Major type:主类型,是一个GUID,定义了数据的整体类型,包括了:视频、音频、未分析的字节流、MIDI等。     *Subtype:子类型,另一个GUID,进一步定义了数据格式。比如,如果主类型是视频,则子类型可以是RGB-24、RGB-32、UYVY等格式,如果主类型是音频,则可能是PCM或MPEG-1 payload等。子类型提供了比主类型更多的内容,但仍未提供完整的格式定义,比如,子类型没有定义图像尺寸和帧率,这些都将在Format block中被定义。     *Format block:格式块,定义了具体的格式。格式块是AM_MEDIA_TYPE结构体中一个单独被分配的内存空间,pbFormat成员指向这块内存空间。因为不同的格式会有不同的格式描述,所以pbFormat成员的类型是void*。比如,PCM音频使用WAVEFORMATEX结构体,视频使用不同的结构体包括:VIDEOINFOHEADER和VIDEOINFOHEADER2。formattype成员是一个GUID,指定了格式块包含了哪种结构体,每一种格式的结构体都被分配了GUID。cbFormat成员定义了格式式块的长度。     当格式块被定义时,主类型和子类型包含的信息就显得有点多余了。其实,主类型和子类型为识别格式提供了一个便利的方法,比方说,你可以指定一个普通的24位RGB格式(MEDIASUBTYPE_RGB24),而不需去关心VIDEOINFOHEADER结构体中诸如图像尺寸和帧率这些信息。     下面是一个filter检查媒体类型的例子:

HRESULT CheckMediaType(AM_MEDIA_TYPE *pmt) {     if (pmt == NULL) return E_POINTER; 

    // 检查主类型,我们需要的是视频     if (pmt->majortype != MEDIATYPE_Video)     {         return VFW_E_INVALIDMEDIATYPE;     }

    // 检查子类型,我们需要的是24-bit RGB.     if (pmt->subtype != MEDIASUBTYPE_RGB24)     {         return VFW_E_INVALIDMEDIATYPE;     }

    // 检查format type和格式块的大小.     if ((pmt->formattype == FORMAT_VideoInfo) &&          (pmt->cbFormat >= sizeof(VIDEOINFOHEADER) &&          (pmt->pbFormat != NULL))     {         // 现在可以安全地将格式块指针指向正确的结构体。         VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;              // 检查pVIH (未展示). 如果正确,返回S_OK.         return S_OK;     }

    return VFW_E_INVALIDMEDIATYPE; }

    AM_MEDIA_TYPE结构体还包含了一些任选项,用来提供附加的信息,filter不需要这些信息:      *ISampleSize,如果这个字段非零,表示这是每个sample的尺寸,如果是零,则表示sample的尺寸会改变。      *bFixdSizeSamples,如果这个布尔类型的标记是TRUE,表示ISampleSize有效,否则,你可以忽略ISampleSize。      *bTemporalCompression,如果这个布尔类型的标记是FALSE,表示所有帧都是关键帧。

3.2.4. 关于媒体样本(Media Sample)和分配器(Allocator)     Filter通过Pin与Pin之间的连接来递交数据,数据从一个filter的输出Pin转移到另一个filter的输入Pin,除了个别情况,实现这种功能通常的方法是调用输入Pin上的IMemInputPin::Receive方法。     依靠filter,媒体数据的内存空间可以通过多个途径来分配:在堆上、在DirectDraw表面(surface)、在共享GDI内存或使用其它的分配机制。这个负责分配内存空间的对象称为分配器(Allocator)是一个暴露IMemAllocator接口的COM对象。     当两个Pin相连时,其中的一个Pin必须提供一个分配器。DirectShow定义了一个方法调用序列来决定到底由哪个Pin来提供分配器。Pin还负责协商分配器创建的缓冲数和每个缓冲的尺寸。     在数据流开始之前,分配器创建了一个缓冲池。在数据流动过程中,上游filter在缓冲中填入数据并递送给下游filter,但是,上游filter递送给下游filter的并不是原始的缓冲区指针,而是一个称为媒体样本(Media Sample)的COM对象,它由分配器创建并用来管理缓冲区,暴露IMediaSample接口。一个媒体样本包含:     *指向下层缓冲区的指针     *时间戳     *各种标记     *可选的媒体类型     时间戳定义了呈现时间(presentation time),用以让renderer filter确定播放的合适时机。各种标记可以用来指示很多事情,比如,数据在上一个sample后是否被打段过(如重新定位、掉帧)等。媒体类型为流中间改变数据格式提供了途径,通常,没有媒体类型的sample,被认为从上一个sample以来数据格式没有被改变过。     当filter使用一个缓冲时,它保存了sample上的参考计数。分配器使用参考计数来决定什么时候可以重用这个缓冲,这防止了一个filter在写一个缓冲时另一个filter还在使用这个缓冲,除非所有的filter都释放了这个缓冲,否则sample不会将其返回给分配器的缓冲池。      3.2.5. 硬件如何参与Filter Graph     这一节描述了DirectShow如何与音频和视频硬件交互。           外壳filter(Wrapper Filter)     所有的DirectShow filter都是用户模式的软件组件。为了使象视频采集卡这样的内核模式的硬件驱动加入到filter graph中,必须使其象用户模式的filter那样。DirectShow提供外壳filter来完成这个功能,这类filter包括:Audio Capture filter、VFW Capture filter、TV Tuner filter、TV Audio filter和Analog Video Crossbar filter。DirectShow也提供一个叫KsProxy的filter,它可以实现任何类型的WDM流驱动。硬件商通过提供一个Ksproxy plug-in来扩展KsProxy,以使其支持自己的功能,ksproxy plug-in是一个被KsProxy聚合的COM对象。     外壳filter通过暴露COM接口来实现设备的功能。应用程序使用这些接口将信息传递给filter,filter再把这些COM调用转化为设备驱动调用,将信息传递到内核模式下的设备中去,然后返回结果给应用程序。TV Tuner、TV Audio、Analog Video Crossbar和KsProxy filter都通过IKsPropertySet接口来支持驱动的自定义属性,VFW Capture filter和Audio Capture filter不支持这种方式。     外壳filter使应用程序可以象控制其它directshow filter一样来控制设备,filter已经封装了与内核驱动通信的细节。          Video for Windows Devices     VFW Capture filter支持早期的VFW采集卡,当一个设备加入到目标系统中支后,它可以被directshow使用系统设备枚举器(System Device Enumerator)发现并加入到filter graph中去。     音频采集(Audio Capture)和混音设备(声卡)(Mixing Device/Sound Card)     较新的声卡都有麦克风等设备的插口,而且大多数这类声卡都有板级的混频能力,可单独控制每一个连接设备的音量及高低音。在directshow中,声卡的输入和混频设备被Audio Capture filter封装。每个声卡都能被系统设备枚举器发现。要查看你的系统中的所有声卡,只需打开GraphEdit,从Audio Capture Sources一类中选择即可,每个在这个类里的filter都是一个单独的Audio Capture filter。

    WDM流设备     较新的硬解码设备和采集卡都遵照WDM规范。这些设备和比VFW设备更强大的功能,以及可以应用于多种系统(winxp,winNT,win2000,win98/me)。WDM视频采集卡支持许多VFW所没有的功能,包括枚举采集的格式、编程控制视频参数(如对比度、亮度)、编程选择输入端和电视调谐支持。     为了支持WDM流设备,directshow提供了KsProxy filter(ksproxy.ax)。KsProxy被称为“瑞士军刀",因为它可以做很多不同的事情。filter上pin的数量,以及COM接口的数量,取决于底层驱动的能力。KsProxy不以"KsProxy"这个名字显示在filter graph中,而是使用一个已在注册表中登记的设备名称。要查看你系统中的WDM设备,可以运行GraphEdit然后从WDM Streaming这个类别中选择。即使你的系统中只有一块WDM卡,这块卡也可能包含多个设备,而每一个设备都表现为一个filter,每个filter是实际意义上的KsProxy。     应用程序使用系统设备枚举器在系统中寻找WDM设备moniker,然后调用moniker的BindToObject来实例化。因为KsProxy能够表现所有类型的WDM设备,因此它必须通过询问驱动来决定哪些属性是驱动所支持的。属性集是一组数据结构的集合,被WDM设备使用,也被诸如MPEG2软解码filter这样的用户模式filter使用。KsProxy通过暴露COM接口来配置自己,硬件商则通过提供插件来扩展KsProxy,插件暴露硬件商自定义的一些接口,用以实现特殊的功能。所有这些细节对于应用程序来说都是不可见的,应用程序通过KsProxy控制设备就象控制其它的DirectShow filter一样。          内核流     WDM设备支持内核流,在内核流中数据在内核模式下被彻底流化而永远不需要切换到用户模式下去,从而避免了在内核模式和用户模式之间切换的巨大开销,内核流允许高的比特率而不消耗CPU的时间。基于WDM的filter能够使用内核流将多媒体数据一个硬件设备送入到另一个中去,既可以是在同一块卡中也可以在不同的卡中,而不需要将数据拷入系统主存。     从应用程序的视点来看,数据好象是从一个用户模式的filter传到另一个中去,但是实际上,数据根本就没有传到用户模式下过,而是可能支接从内核模式的设备中传到下一个中去直至被呈现(render)在显卡上。某些情况,比如采集视频到一个文件中去,在某些点上需要将数据从内核模式传入到用户模式,但是,仍然没有必要将数据拷贝到内存的一个新位置中去。     应用程序开发者通常只需了解一个内核流的背景知识而不需要深究它的细节。

3.3. 构建Filter Graph 3.3.1. 用于构建Graph的组件     DirectShow提供了一系列用于构建filter graph的组件,包括:     *Filter Graph Manager。这个对象用于控制filter graph,支持IGraphBuilder、IMediaControl和IMediaEventEx等许多接口。所有的directshow应用程序都需要在某些地方用到这个对象,虽然在有些情况下,是其它的对象为应用程序创建了filter graph manager。     *Capture Graph Builder。这个对象为构建filter graph提供附加的方法。它最初是为构建提供视频采集的graph而设计的(这正是它的名字由来),但是对于构建许多另外类型的filter graph也是很有用的。它支持ICaptureGraphBuilder2接口。     *Filter Mapper和System Device Enumerator。这些对象用于查找在系统中注册的或代表硬件驱动的filter。     *DVD Graph Builder。这个对象构建用以回放和导航DVD的filter graph。它支持IDvdGraphBuilder接口。基于脚本的应用程序能够使用MSWebDVD ActiveX控件来控制DVD回放。     *Video Control。WinXP提供这个ActiveX控件,用于操纵directshow中的数据和模拟电视。          智能连接(Intelligent Connect)     智能连接这个术语覆盖了一系列Filter Graph Manager用于构建所有或部份filter graph的算法。任何时候,当Filter Graph Manager需要添加filter来完成graph时,它大致做以下几件事情:     1.如果有一个filter存在于graph中,而且这个filter有至少一个没有连接的input pin,Filter Graph Manager试着去试用这个filter。     2.否则,Filter Graph Manager在已注册的filter中寻找连接时可以接受合适的媒体类型的filter。每一个filter都注册有一个Merit值,这个值用以标记哪个filter最容易被Filter Graph Manager选中来完成graph。Filter Graph Manager按Merit值的顺序来选择filter,Merit值越大,被选中的机会越大。对于每种流类型(如音频、视频、MIDI),默认的renderer具有一个很高的Merit值,解码器同样是,专用filter具有低Merit值。     如果Filter Graph Manager因选择的filter不合适而被困,它会返回来尝试另外的filter组合。

3.3.2 Grap构建概述     创建一个filter graph,从创建一个Filter Graph Manager实例开始:

     IGraphBuilder* pIGB;      HRESULT hr = CoCreateInstance(CLSID_FilterGraph,      NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder,(void **)&pIGB);
      Filter Graph Manager支持下列Graph构建方法:      *IFilterGraph::ConnectDirect,在两个pin之间进行直接连接,如果连接失败,则返回失败      *IFilterGraph::Connect,连接两个Pin,如果可能的话,直接连接它们,否则,在中间加入其它的filter来完成连接。      *IGraphBuilder::Render,从某个输出Pin处开始完成余下的graph构建。该方法会自动在输出pin后面添加必须的filter,直到renderer filter为止。      *IGraphBuilder::RenderFile,构建一个完整的文件回放graph。      *IGraphBuilder::AddFilter,将一个filter添加到graph中。它不连接filter,并且在调用此方法前,filter必须已经被创建。创建filter可以是用CoCreateInstance方法或使用Filter Mapper或系统设备枚举器(System Device Enumerator)。     这些方法提供了三种构建graph的途径:     1.filter graph manager构建整个graph     2.filter graph manager构建部分graph     3.应用程序构建整个graph           Filter Graph Manager构建整个graph     如果你仅仅是想回放一个已知格式的文件,如AVI、MPEG、WAV或MP3,使用RenderFile方法。     RenderFile方法首先寻找注册在系统中能分析源文件的filter,它使用协议名(如http://),文件扩展名或文件的头几个字节来决定选择哪一个源filter。     Filter Graph Manager使用一个迭代过程来完成余下的graph构建。在这个迭代过程中,它逐个列出filter的输出pin上支持的媒体类型,并搜索哪个已注册的filter的输入Pin接受该媒体类型。它使用一系列的规则来缩小filter的范围并排定优先顺序:     *filter类别(category)标识的filter的一般功能     *媒体类型描述filter能在接受或能输出哪种数据类型     *merit值决定filter被尝试的次序。如果两个filter具有相同的filter类别并且同时支持相同的输入类型,Filter Graph Manager选择merit值大的那一个。一些filter故意给出一个小merit值是因为它是为特殊用途设计的,仅能由应用程序来将其添加到graph。     Filter Graph Manager使用Filter Mapper对象来搜索已注册的filter。     每个filter被添加时,filter graph manager试着将其与前一个filter的输出pin连接。它们协商决定他们是否能连接,如果能,哪一种媒体类型被用来连接。如果新filter不能连接,filter graph manager丢弃它并尝试别一个,这个过程一直继续到每个流都被render为止。           Filter Graph Manager构建部分graph     如果不仅仅是播放一个文件,那么你的应用程序就必须做一些graph的构建工作。比如,一个视频采集应用程序必须先选择一个source filter并将其添加到graph中去。如果你需要将数据写入到一个AVI文件中,你必须添加一个AVI Mux和File Write filter。不过,也经常有可能让filter graph manager来完成整个graph,比如,你可以通过Render方法来render一个pin进行预览。           应用程序构建整个graph     在某些场合,你的应用程序需要添加和连接每个filter来构建graph。在这种情况下,你很可能明确地知道哪些filter需要加到graph中去。使用这种方式,应用程序通过调用AddFilter方法添加每个filter,然后枚举filter上的pin,调用Connect或ConnectDirect来连接它们。

3.3.3. 智能连接     智能连接是filter graph manager用以构建filter graph的机制。它包含了一系列相关的用以选择filter和将它们添加到graph中去的算法。作为应用程序开发者,你并不需要很具体地了解智能连接的细节。如果你在构建某个filter graph时遇到问题并希望能解决它,或者你正在编写你自己的filter并希望它能自动地被graph构建,请阅读这一节。     智能连接涉及以下IGraphBuilder方法:     *IGraphBuilder::Render     *IGraphBuilder::AddSourceFilter     *IGraphBuilder::RenderFile     *IGraphBuilder::Connect     Render方法构建一部分graph,它从一个尚未连接的输出pin开始顺着数据流的方向往下,添加必要的filter,起始的那个filter必须已被添加到了graph中。Render方法每一步都搜索一个能够连接到前一个filter的filter,如果新连接上的filter有多个输出pin,数据流能自动分流,搜索直到每个流都被renderer为止。如果Render方法搜索到的filter无法使用,它会返回去尝试另一个filter。     要连接每一个输出pin,Render方法做以下工作:     1.如果pin支持IStreamBuilder接口,Filter Graph Manager让pin的IStreamBuilder::Render方法来完成整过程。通过暴露这个接口,pin承担了构建graph剩余部分的全部工作。但是,只有很少数的filter支持此接口。     2.Filter Graph Manager尝试使用任何在缓存中的filter。在智能连接的整个过程中,filter graph manager可以在早期将filter缓存起来。     3.如果filter graph包含了任何有未连接的输入pin的filter,filter graph manager会将其当作下一个filter来尝试连接。你可以通过在调用Render之前添加特定的filter来强制让Render方法来尝试这个filter。     4.最后,filter graph manager使用IFilterMapper2::EnumMatchingFilters方法在所有注册的filter中寻找,依据已注册的媒体类型列表来逐个试着匹配输出pin的各个媒体类型(按优先级高低排列)。     每个已注册的filter都有一个merit值,这是一个用来表示filter优先级的数字,最大优先级越高,EnumMatchingFilters方法返回的filter集依据merit值来排列,直至最小的merit值MERIT_DO_NOT_USE+1,它忽略merit为MERIT_DO_NOT_USR或更小的filter。filter也通过GUID来归类,类别本身也有merit值,EnumMatchingFilters方法忽略任何merit值为MERIT_DO_NOT_USE或更小的类别,即使在那个类别中的filter有较高的merit值。     总结一下,Render方法以下列步骤尝试filter     1.使用IStreamBuilder     2.尝试被缓存的filter     3.尝试已添加在graph中的filter     4.在已注册的filter中寻找     AddSourceFilter方法添加一个能render特定文件的source filter。首先,它依据协议名(如Http://)、文件扩展名、或文件头在已注册的filter中寻找匹配的那个。如果此方法定位到了一个合适的source filter,它便立刻创建一个这个filter的实例,并将其添加到graph中,然后调用filter的IFileSourceFilter::Load方法。     RenderFile方法依据一个文件名来构建一个默认的回放graph,在其内部,RenderFile方法调用AddSourceFilter来定位source filter,并且用Render来构建Graph的余下部分。     Connect方法将输出pin连接到输入pin上去,这个方法自动添加必要的中间filter到graph中去,使用在Render方法中描述的那一系列算法:     1.使用IStreamBuilder     2.尝试被缓存的filter     3.尝试已添加在graph中的filter     4.在已注册的filter中寻找

3.4. Filter Graph中的数据流     这一节主要描述媒体数据是如何在filter graph中流动的。如果你只是为了编写DirectShow应用程序,你不需要知道这些细节,当然,知道这些细节对于编写directshow应用程序仍然是有帮助的。但是如果你要编写directshow filter,那么你就必须掌握这部分知识了。 3.4.1. DirectShow数据流概述     在这一部分先粗略地描述一下DirectShow中数据流是如何工作的。     数据首先是被保存在缓冲区里的,在缓冲区里,它们仅仅是一个字节数组。每一个缓冲区被一个称作媒体样本(media sample)的COM对象所包容,media sample提供IMediaSample接口。media sample由另一个称作分配器(allocator)的COM对象创建,allocator提供IMemAllocator接口。每一个pin连接都指定有一个allocator,当然,两个或多个pin连接也可以共享几个allocator。

    每一个allocator都创建一个media sample池,并为每个sample分配缓冲区。一旦一个filter需要一个缓冲区来填充数据,它就调用IMemAllocator::GetBuffer方法来请求一个sample。只要allocator有一个sample还没有被任何filter使用,GetBuffer方法就立即返回一个sample的指针。如果allocator所有的sample已经被用完,这个方法就阻塞在那里,直到有一个sample变成可用的了。GetBuffer返回一个sample后,filter就将数据写入到sample的缓冲区中去,并在sample上设置适当的标记(如时间戳),然后将它递交到下一个filter去。     当一个renderer filter接收到了一个sample时,renderer filter检查时间戳,并将sample先保存起来,直到filter graph的参考时钟指示这个sample的数据可以被render了。当filter将数据render后,它就将sample释放掉,此时sample并不立即回到allocator的sample池中去,除非这个sample上的参考计数已经变为0,表示所有的filter都已释放这个sample。     

    上游的filter可能在renderer之前运行,这就意味着,上游的filter填充缓冲的速度可能快于renderer销毁它们。但是尽管如此,samples也并无必要更早地被render,因为renderer将一直保存它们直到适当的时机去render,并且,上游filter也不会意外地将这些samples的缓冲覆盖掉,因为GetSample方法只会返回那些没有被使用的sample。上游filter可以提前使用的sample的数量取决于allocator分配池中的sample的数量。     前面的图表只显示了一个allocator,但是通常的情况下,每个流中都会有多个allocator。因此,当renderer释放了一个sample时,它会产生一个级联效应。如下图所示,一个decoder保存了一个视频压缩帧,它正在等待renderer释放一个sample,而parser filter也正在decoder去释放一个sample。     

    当renderer释放了一个sample后,decoder完成尚未完成的GetBuffer调用。然后decoder便可以对压缩的视频帧进行解码并释放它保存的sample,从而使parser完成它的GetBuffer调用。     3.4.2. 传输协议(Transports)     为了使媒体数据能在filter graph中流动,Directshow filter必须能支持多个协议中的一个,这些协议被称作传输协议(transports)。当两个filter连接后,它们必须支持同一个传输协议,否则,它们将不能交换数据。通常,一个传输协议要求某个pin支持一个特定的接口,当两个filter连接时,另一个pin来调用这个pin的这个接口。     大多数的directshow filter在主存中保存媒体数据,并且通过pin连接向另一个filter递交数据,这种类型的传输协议被称作本地内存传输协议(local memory transport)。尽管这类传输协议在directshow中应用最普遍,但并非所有的filter都使用它。例如,某些filter通过硬件途径来传递数据,使用pin仅仅是为了传递控制信息,如IOverlay接口。     DirectShow为本地内存传输协议定义了两种机制,推(push)模式拉(pull)模式。在推模式中,source filter产生数据,并将其递交给下游的filter,下游的filter被动地接收数据并处理它们,再将数据传递给它的下游filter。在拉模式中,source filter与一个parser filter连接,parser filter向source filter请求数据,source filter回应请求并传递数据。推模式使用IMemInputPin接口,而拉模式使用IAsyncReader接口。     推模式比拉模式应用更广泛。      3.4.3. 媒体样本(sample)和分配器(allocator)     当一个pin向另一个pin传递媒体数据时,它并不是直接传递一个内存缓冲区的指针,而是传递一个COM对象的指针,这个COM对象管理着内存缓冲,被称为媒体样本(media sample),暴露IMediaSample接口。接收方pin通过调用IMediaSample接口的方法来访问内存缓冲,如IMediaSample::GetPointerIMediaSample::GetSizeIMediaSample::GetActualDataLength。     sample总是从输出pin到输入pin向下传输。在推模式中,输出pin通过在输入pin上调用IMemInputPin::Receive方法来传递一个sample。输入pin或者在Receive方法内部同步地处理数据,或者另开一个工作线程进行异步处理。如果输入pin需要等待资源,允许在Receive中阻塞。     另一个用来管理媒体样本的COM对象,被称作分配器(allocator),它暴露IMemAllocator接口。一旦一个filter需要一个空闲的媒体样本,它调用IMemAllocator::GetBuffer方法来获得sample的指针。每一个pin连接都共享一个allocator,当两个pin连接时,它们协商决定哪个filter来提供allocator。pin可以设置allocator的属性,比如缓冲的数量和每个缓冲的大小。     下图显示了allocator、media sample和filter的关系:          

    媒体样本参考计数(Media Sample Reference Counts)     一个allocator创建的是一个拥有有限个sample的sample池。在某一时刻,有些sample正在被使用,有些则可被GetBuffer方法使用。allocator使用参考计数来跟踪sample,GetBuffer方法返回的sample参考计数为1,如果参考计数变为0,sample就可以返回到allocator的sample池中去了,这样它就可以再次被GetBuffer方法使用。在参考计数大于0期间,sample是不能被GetBuffer使用的。如果每个从属于allocator的sample都在被使用,则GetBuffer方法会被阻塞直至有sample可以被使用。     举个例子,假设一个输入pin接收到一个sample。如果它同步地在Receive方法内部处理它,sample的参考计数不增加,当Receive返回时,输出pin释放这个sample,参考计数归0,sample就返回到sample池中去了。另一种情况,如果输入pin异步地处理sample,它就在Receive方法返回前将sample的参考计数加1,此时参考计数变为2。当输出pin释放这个sample时,参考计数变为1,sample不能返回到sample池中去,直到异步处理的工作线程完成工作,调用Release释放这个sample,参考计数变为0时,它才可以返回到sample池中去。     当一个pin接收到一个sample,它可以将数据拷贝到另一个sample中去,或者修改原始的sample并将其传递到下一个filter中去。一个sample可能在整个graph长度内被传递,每个filter都依次调用AddRef和Release。因而,输出pin在调用Receive后一定不能重复使用同一个sample,因为下游的filter可能正在使用这个sample。输出pin只能调用GetBuffer来获得新的sample。     这个机制减少了总的内存分配过程,因为filter可以重复使用同样的缓冲。它同样防止了数据在被处理前意外地被覆盖写入。     当filter处理数据后数据量会变大(如解码数据),一个filter可以为输入pin和输出pin分配不同的allocator。如果输出数据并不比输入数据量要大,filter可以用替换的方式来处理数据而不用将其拷贝到新的sample中去,在这种情况下,两个或多个pin连接共享一个allocator。          提交(Commit)和反提交(Decommit)分配器     当一个filter首次创建一个allocator时,allocator并不为其分配内存缓冲,此时如果调用GetBuffer方法的话会失败。当流开始流动时,输出pin调用IMemAllocator::Commit来提交allocator,从而为其分配内存。此时pin可以调用GetBuffer了。     当流停止时,pin调用IMemAllocator::Decommit来反提交allocator,在allocator被再次提交前所有后来的GetBuffer调用都将失败,同样,如果有阻塞的正在等待sample的GetBuffer调用,也将立即返回失败信息。Decommit方法是否释放内存取决于实现方式,如CMemAllocator类直至析构时才释放内存。      3.4.4. filter状态     filter有三种可能的状态:停止(stopped)就绪(paused)运行(running)。就绪状态的目的是为了让graph提前做准备以便在run命令下达时可以立即响应。Filter Graph Manager控制所有的状态转换。当一个应用程序调用IMediaControl::RunIMediaControl::PauseIMediaControl::Stop时,Filter Graph Manager在所有filter上调用相应的IMediaFilter方法。在停止状态和运行状态之间转换时总是要经过就绪状态,即如果应用程序在一个处于停止状态的graph上调用Run时,Filter Graph Manager在运行它之前先将其转为pause状态。     对于大多数filter来说,运行状态和就绪状态是等同的。看下面的这个graph:     Source > Transform > Renderer     假设这个source filter不是一个实时采集源,当source filter就绪时,它创建一个线程来尽可能快地产生新数据并写入到media sample中去。线程通过在transform filter的输入pin上调用IMemInputPin方法将sample“推”到下游filter。transform filter在source filter的线程中接收数据,它可能也使用一个工作线程赤将sample传递给renderer,但是在通常情况下,它在同一个线程中传递它们。如renderer处理就绪状态下,它等待接收sample,当它接收到一个时,它或阻塞或保存那个sample,如果这是一个Video renderer,则它将sample显示为一个静态的图片,只在必要的时候刷新它。     此时,流已经准备充分去被render,如果graph仍然处理就绪状态下,sample会在每一个sample后堆积,直至每个filter都被阻塞在Receive或GetBuffer下。没有数据会被丢失。一旦source线程的阻塞被解除时,它只是简单地从阻塞点那里进行恢复。     source filter和transform filter忽略从就绪状态转到运行状态——它们仅仅是尽可能快地继续处理数据。但是当renderer运行时,它就要开始render sample了。首先,它render在就绪状态下保存的那个sample,接着,每接收到一个新的sample,它计算这个sample的呈现时间,renderer保存每个sample直至到了它们的呈现时间再render它们。在等待合适的呈现时间时,它或者阻塞在Receive方法上,或者在一个工作线程中接收数据并将其放入队列中去。renderer的上一个filter不关心这些问题。     实时源(live source),如采集设备,是通常情况中的一个例外。在实时源中,不适合提前准备数据。应用程序可能将graph置于就绪状态下,然后等很长时间才再运行它。graph不应该再render就绪期间的sample,因此,一个实时源在就绪状态时不产生新的sample。要将这种情况通知给filter graph manager,source filter的IMediaFilter::GetState方法返回VFW_S_CANT_CUE。这个返回值表示filter已切换到就绪状态下,即使renderer还没有收到任何数据。     当一个filter停止时,它不再接收任何传递给它的数据。source filter关闭它们的流线程,别的filter关闭所有它们创建的工作线程。pin反提交(decommit)它们的allocator。          状态转换     filter graph manager按从下游filter到上游filter的次序来完成所有的状态转换,从renderer开始逐个向上直至source filter,这个次序是必要的,可以防止数据丢失或graph死锁。最重要状态转换是就绪状态和停止状态间的转换:     *停止状态到就绪状态:当每一个filter被置为就绪态时,它便准备好从上一个filter接收sample。source filter是最后一个被置为就绪态的filter,它创建数据流线程并开始传递sample。因为所有下游filter都处于就绪状态,所以没有一个filter会拒绝接收sample。当graph中所有的renderer都接收到一个sample后,filter graph manager才彻底完成状态转换工作(实时源除外)。     *就绪状态到停止状态:当一个filter停止时,它释放了所有它保存的sample,就将解除所有上游filter调用GetBuffer时的阻塞。如果filter正在Receive方法中等待数据,则它停止等待并从Receive中返回,从而解除阻塞。因而,此时当filter graph manager再去将上游filter转换为停止状态时,它已经不再阻塞于GetBuffer和Receive,从而可以响应停止命令。上游filter在得到停止命令前可能会传递下一些过时的sample,但下游filter不再接收它们,因为此时下游filter已处于停止状态了。      3.4.5. 拉模式     在IMemInputPin接口中,上游filter决定哪些数据要被发送,然后将数据推到下游filter中去。但是在某些情况下,拉模式会更加合适。在拉模式中,只有当下游filter从上游filter中请求数据时,数据才被传递下去,数据流动由下游filter发起。这种类型的连接使用IAsyncReader接口。     典型的拉模式应用是文件回放。比如,在一个AVI回放graph中,Async File Source filter完成一般的文件读操作并将数据作为字节流传递下去,没有什么格式信息。AVI Splitter filter读取AVI头并将数据流分解成视频和音频sample。AVI Splitter比Async File Source filter更能决定它们需要哪些数据,因此需用IAsyncReader接口来代替IMemInputPin接口。     要从输出pin请求数据,输入pin调用下面方法中的一个:     *IAsyncReader::Request     *IAsyncReader::SyncRead     *IAsyncReader::SyncReadAligned     第一个方法是异步的,支持多重读操作。其余的是同步的。     理论上,任一个filter都能支持IAsyncReader,但是实际上,它仅仅在连接有一个parser filter的source filter上使用。分析器(parser)非常象一个推模式的source filter,当它就绪时,它创建一个数据流线程,从IAsyncReader连接中拉数据并将其推到下一游filter中去。它的输出pin使用IMemInputPin,graph余下的部分使用标准的推模式。

3.5 DirectShow中的事件通告     这一节主要描述在directshow filter graph中事件是怎样发生的,以及应用程序如何接收事件通告并响应它们。 3.5.1 概述     一个filter通过发送一个事件通来通知filter graph manager某个事件已经发生。这些事件可以是一些预知的事件比如流结束事件,也可以是一些异常如render流时失败。一部分事件由filter graph manager自己处理,另一部分则由应用程序来处理。如果filter graph manager不处理某个事件,那么这个事件会被放入到队列中去。filter graph也可以通过队列将自己的事件发送给应用程序。     应用程序从队列中接收事件并根据其类型来响应它们。DirectShow中的事件通告类似于windows的消息队列机制。应用程序可以让filter graph manager取消对指定的事件类型的默认操作,而是将它们放入事件队列由应用程序来处理它们。     由于这样的机制,使我们能做到:      *filter graph manager与应用程序的对话      *filter可以即和应用程序也和filter graph manager对话      *由应用程序来决定处理事件的复杂度。

3.5.2 从队列中取事件     Filter Graph Manager暴露3个支持事件通知的接口:      *IMediaEventSink 包含filter发送事件的方法      *IMediaEvent 包含应用程序取事件的方法      *IMediaEventEx 继承扩展IMediaEvent接口     filter通过在filter graph manager上调用IMediaEventSink::Notify方法来发送事件通告,一个事件通知由一个表示事件类型的事件号,和两个DWORD类型用以放置附加信息的参数组成。按事件号的不同,这两个参数可以是指针、返回值、参考时间或者其它信息。完整的事件号和参数列表,参见Event Notification codes(http://msdn.microsoft.com/library/en-us/directshow/htm/eventnotificationcodes.asp)。     要从事件队列中取事件,应用程序需要在filter graph manager上调用IMediaEvent::GetEvent事件。这个方法一直阻塞到取到事件或超时。一旦队列中有了事件,这个方法就返回事件号和两个事件参数。在调用GetEvent后,应用程序应该总是调用IMediaEvent::FreeEventParams方法来释放与事件参数相关的所有资源。比如,一个参数可能是由filter graph分配的BSTR值。     下面的代码是一个如何从队列中取事件的框架:

  long evCode, param1, param2;   HRESULT hr;   while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))   {       switch(evCode)        {            // Call application-defined functions for each            // type of event that you want to handle.       }        hr = pEvent->FreeEventParams(evCode, param1, param2);   }

  要重置filter graph manager默认的事件处理过程,调用IMediaEvent::CancelDefaultHandling方法,用事件号做参数。你可以通过调用IMediaEvent::RestoreDefaultHandling方法来恢复某个事件的处理过程。如果filter graph对某个事件号没有默认处理过程,则调用上面两个方法不产生任何影响。

3.5.3 当事件发生时     要处理DirectShow事件,应用程序需要一个方法来知道事件何时正等待在队列中。Filter Graph Manager提供两种方法:     *窗口通告:一旦有事件发生,Filter Graph Manager就发送一个用户自定义窗口消息来通知应用程序窗口     *事件信号:如果有DirectShow事件在队列中,filter graph manager就触发一个windows事件,如果队列为空,则reset这个事件。     应用程序可以使用任何一种方法,但通常窗口通告方法相对比较简单。          窗口通告:     要设置窗口通告,调用IMediaEventEx::SetNotifyWindow方法并指定一个私有消息,私有消息可以是从WM_APP到0xBFFF的任一个。一旦filter graph manager把一个新的事件通告放入队列中,它便发送这个消息给指定的窗口。应用程序从窗口的消息循环中来响应这个消息。     下面是如何设置通知窗口的例子:

   #define WM_GRAPHNOTIFY WM_APP + 1   // Private message.   pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0); 

  消息是一个普通的windows消息,并且独立于DirectShow消息通告队列被发送。使用这种方法的好处是大部分应用程序拥有一个消息循环,因此,要知道DirectShow事件何时发生便无需做额外的工作了。   下面是一段如何响应通告消息的框架代码:

   LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)   {       switch (msg)       {           case WM_GRAPHNOTIFY:               HandleEvent();  // Application-defined function.               break;           // Handle other Windows messages here too.       }       return (DefWindowProc(hwnd, msg, wParam, lParam));   }

      因为事件通告与消息循环均为异步进行的,因此在应用程序响应事件时队列中可以会有多个事件。而当事件变为非法时,它们会从队列中被清除掉。所以在你的事件处理代码中,调用GetEvent直至返回一个表示队列已空的失败代号。     在释放IMediaEventEx指针前,请以NULL作参数调用SetNotifyWindow方法来取消事件通告。并且在你的事件处理代码中,在调用GetEvent前检查IMediaEventEx指针是否合法。这些步骤可以防止在释放IMediaEventEx指针后应用程序继续接收事件通告的错误。          事件信号:     Filter Graph Manager建立一个反映事件队列状态的手工重设事件(manual-reset event)如果队列中包含有未处理的事件通告,Filter Graph Manager就会发信号给手工重设事件。如果队列是空的,则调用IMediaEvent::GetEvent方法会重设(reset)事件。应用程序可以通过这个事件来确定队列的状态。          注意:此处的术语可能被混淆。手工重设事件是由windows的CreateEvent函数创建的一种事件类型,它与由DirectShow定义的事件无关。          调用IMediaEvent::GetEventHandle方法得到手工重设事件的句柄,调用一个函数如WaitForMultipleObjects来等待发送给手工重设事件的信号。一旦收到信号,就可以调用IMediaEvent::GetEvent来接收DirectShow事件了。     下面的代码举例说明了这种方法。在取得事件句柄后,在100毫秒时间间隔内等待发送给手工重设事件的信号,如果有信号发来,它调用GetEvent然后在windows控制台上打印出事件号和事件参数,循环在EC_COMPLETE事件发生后结束,这标志着回放结束。

  HANDLE  hEvent;    long    evCode, param1, param2;   BOOLEAN bDone = FALSE;   HRESULT hr = S_OK;   hr = pEvent->GetEventHandle((OAEVENT*)&hEvent);   if (FAILED(hr)   {       /* Insert failure-handling code here. */   }   while(!bDone)    {       if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100))       {            while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))            {               printf("Event code: %#04x/n Params: %d, %d/n", evCode, param1, param2);               pEvent->FreeEventParams(evCode, param1, param2);               bDone = (EC_COMPLETE == evCode);           }       }   } 

         因为Filter Graph会在适当的时候自动重设事件,因此你的应用程序应当不去作重设工作。同时,当你释放filter graph时,filter graph会关闭事件句柄,因此在这之后你就不能再使用事件句柄了。

 

3.6. DirectShow中的时间和时钟     这一节主要概述DirectShow体系中时间和时钟。 3.6.1. 参考时钟     Filter Graph Manager的一个功能,能够以同一个时钟来同步所有在graph中的filter,称作 参考时钟(reference clock)     任何暴露了 IReferenceClock接口的对象都能够作为一个参考时钟来使用。参考时钟可以由一个DirectShow filter来提供,例如可以直接使用硬件时钟的audio renderer。另外,Filter Graph Manager也能使用系统时间来作参考时钟。     名义上,一个参考时钟以千万分之一秒的精度来度量时间,但是实际上的精度不会这么高。要取得参考时钟的当前时间,调用IReferenceClock::GetTime方法。由于时钟的基准时间,即时钟开始时的时间计数,是依赖于具体的实现的,因此GetTime的返回值不反映绝对时间,只反映相对于graph开始时的相对时间。     虽然参考时钟的精度是变化的,但是GetTime的返回值却保证是单调递增的,换句话说,也就是参考时钟的时间是不会回退的。如果参考时钟的时间是由硬件源产生的,而硬件时钟回退了(比如,有一个调节器调节了时钟),GetTime依然返回最晚的那个时间只到硬件时钟追上它。要知道更多的内容可以参考CBaseReferenceClock类( http://msdn.microsoft.com/library/en-us/directshow/htm/cbasereferenceclockclass.asp)。           默认参考时钟     当Graph运行时,Filter Graph Manager会自动选择参考时钟,选择参考时钟的规则如下:     *如果应用程序指定了一个时钟,则使用这个时钟;     *如果Graph包含了一个支持IReferenceClock的活动源filter(即推模式源filter),则使用这个filter;     *如果Graph未包含任何支持IReferenceClock的推模式源filter,使用任何一个支持IReferenceClock接口的filter,选择的次序是从Renderer filter开始依次向上。已连接的filter优先于未连接的filter被选。(如果这个graph会render一个音频流,则这个规则通常就会选择audio renderer filter来作为参考时钟)     *如果没有filter支持合适的时钟,则使用系统参考时钟。           设置参考时钟     应用程序可以在Filter Graph Manager上调用IMediaFilter::SetSyncSource方法来选择时钟,只有在由于你有一个特殊原因想要选择自己的时钟时才需要这么做。     想要让Filter Graph Manager不使用任何参考时钟,可以调用SetSyncSource,参数为NULL。比如,你需要尽可能快地来处理sample时,就可以这么做。要恢复黑认的参考时钟,在Filter Graph Manager上调用IFilterGraph::SetDefaultSyncSource方法。     当参考时钟发生变化时,Filter Graph Manager会通知每一个filter调用它的IMediaFilter::SetSyncSource方法,应用程序无需调用filter的这个方法。      3.6.2. 时钟时间     DirectShow定义了两种相关时间: 参考时间(reference time)流时间(stream time)     *参考时间是一个绝对时间,由参考时钟返回     *流时间是一个相对于graph最后开始时的相对时间      ·当graph处于运行态时,流时间等于参考时间减去起始时间      ·当graph处于暂停态时,流时间停留在暂停的那一刻      ·在重新定位后,流时间被重设为0      ·当graph处于停止态时,流时间无意义     如果一个媒体样本有一个时间戳t,表示这个在流时间为t时被render,正因为这个原因,因此流时间也被叫做 呈现时间(presentation time)。     当应用程序调用IMediaControl::Run运行graph时,Filter Graph Manager调用每个filter的IMediaFilter::Run。为了补偿消耗在运行每个filter的时间总和,Filter Graph Manager会略微晚一点来定义起始时间。      3.6.3. 时间戳     时间戳定义了媒体样本的起始和结束时间。时间戳有时被称作呈现时间(presentation time)。在阅读余下的文章时,一个必须记住的要点是并非所有的媒体格式都以相同的方式来使用时间戳。举个例子,并不是所有MPEG样本都被打上了时间戳,在MPEG Filter Graph中,时间戳在被解码前并非应用在每个帧上。     当一个renderer filter接收到一个样本时,它以时间戳为基准来确定render时间。如果样本来晚了,或者这个样本没有时间戳,那个filter就立刻render它,否则,filter就等在那直到合适的时机。(通过IReferenceClock::AdviseTime方法来等待样本的render时间)     源filter和语法解析filte使用下列原则,在它们处理的样本上设置合适的时间戳:     *文件回放:第一个样本被打上起始时间戳,为0,后面的时间戳由样本长度和回放速率来决定,这些都由文件格式来决定。分析文件的filter负责计算出合适的时间戳。例子见( http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directshow/htm/avisplitterfilter.asp)     *音视频采集:每个样本都被打上一个起始时间戳,这个时间戳与当它被捕获时的Stream time相同。应注意以下几点:      ·从 预览pin(Preview Pin)出来的样本没有时间戳。因为图像处理的延时,一个打上采集时间的视频帧总是会迟一点到达视频renderer。这会导致在进行质量控制时,renderer会丢弃部分视频帧。关于质量控制,参见( http://msdn.microsoft.com/library/en-us/directshow/htm/qualitycontrolmanagement.asp)      ·音频采集:音频采集filter使用它自己的缓冲集,而并非使用音频驱动程序的。音频驱动以固定的时间间隔来填充采集filter的缓冲。这个时间间隔由驱动决定,通常不超过10毫秒。在音频样本上的时间戳反映的是驱动填充采集filter时的时间,因此会有偏差,尤其是当应用程序使用一个很小的缓冲区时。不过,媒体时间可以精确地反映缓冲区中音频样本的数量。     *混合filter(Mux filter):依赖于输出格式,一个mux filter可能需要由它来产生时间戳,也可能不需要。举个例子,AVI文件格式使用固定的帧率而没有时间戳,因此AVI Mux filter假设那些样本在近似正确的时间内到达。如果样本没有在合适的时间间隔内到达,AVI Mux filter会插入一个长度为0的空样本,来表示一个丢失的帧。在文件回放时,新的时间戳在运行时如前面所述地那样产生。     要在一个样本上设置一个时间戳,调用IMediaSample::SetTime方法。     此外,filter还可以为样本指定一个 媒体时间(media time)。在视频流中,media time表示视频帧的数量。在音频流中,media time表示包中的样本数量,比如,如果每个包包含以44.1KHz的采样率采集的一秒钟的音频,那么第一个包具有一个为0的媒体起始时间以及为44100的媒体终止时间。在一个可以定位的流中,媒体时间总是相对于流的起始时间,比如,假设你在一个15帧/秒的视频流上定位到2秒这个位置,那么定位后的每一个媒体样本的时间媒为0,但是它的媒体时间为30.     Renderer和Mux filter能使用媒体时间通过检查是否有缺口来确定帧或样本是否被丢弃了。但是,filter不是一定要设定媒体时间。要设置媒体时间,调用IMediaSample::SetMediaTime方法。      3.6.4 实时源(Live Source)     实时源,也被叫做 推模式源(push source),实时地接收数据,比如视频采集和网络广播。通常情况下,一个实时源不能控制数据到达的速率。     一个filter被认为是实时源需要具有以下几点:     * 调用IAMFilterMiscFlags::GetMiscFlags方法时返回AM_FILTER_MISC_FLAGS_IS_SOURCE标记,并且至少有一个输出pin暴露IAMPushSource接口。     * filter暴露IKsPropertySet接口,并具有一个capture pin(PIN_CATEGORY_CAPTURE)。           延时(latency)     一个filter的延时是这个filter处理一个样本所需的时间总和。在实时源中,延时取决于保存样本的缓冲区大小。举个例子,假设graph有一个具有33ms延时的视频源和一个具有500ms延时的音频源,那么每个到达视频renderer的视频帧要比与之匹配的音频样本到达音频renderer早470ms,除非graph对这个差别进行补偿,否则音视频将会不同步。     实时源可以通过IAMPushSource接口来进行同步。Filter Graph Manager并做同步工作除非应用程序通过调用IAMGraphStreams::SyncUsingStreamOffset方法来激活它。如果同步被激活,Filter Graph Manager通过IAMPushSource来查询每一个source filter,如果filter支持IAMPushSource,那么Filter Graph Manager调用IAMLatency::GetLatency来得到filter预期的延时(IAMPushSource继承自IAMLatency)。通过组合的延时值,filter graph manager决定graph中最大的预期延时,然后调用IAMPushSource::SetStreamOffset来给每一个source filter一个流偏移,以后filter会在产生时间戳时加上这个偏移。     这个方法主要是为了实现实时预览,但是,注意实时采集设备(比如摄像头)的preview pin上是没有时间戳的,因此,要在一个实时采集设备上使用这种方法,你必须在capture pin上进行视频预览。     通常,IAMPushSource接口被VFW Capture filter和音频采集filter(Audio capture filter)支持。           速率匹配(Rate Matching)     如果renderer filter和source filter使用不同的参考时钟,那么就会有问题,renderer可能比source要快,这就导致了数据的缺口,或则renderer比source慢,就会导致数据拥堵而样本丢弃。通常一个实时源无法控制速率,因此要求renderer来与source进行速率匹配。     通常,只有audio renderer实现速率匹配,因为声音回放的频率比视频更重要。要实现速率匹配,audio renderer必须排除以下几点:     *如果graph没有使用一个参考时钟,那么audio renderer不会去进行速率匹配(如果graph没有参考时钟,那么样本总是在到达时就被立刻render)。     *另外,如果graph中有一个参考时钟,audio renderer检测是否有一个实时源在上游,如果没有,audio renderer不进行速率匹配。     *如果有一个实时源在上游,并且这个实时源在它的输出Pin上暴露IAMPushSource接口,audio renderer调用IAMPushSource::GetPushSourceFlags,并寻找以下标记:      ·AM_PUSHSOURCECAPS_INTERNAL_RM,这个标记表示这个实时源拥有自己的速率匹配机制,因此audio renderer不进行速率匹配。      ·AM_PUSHSOURCECAPS_NOT_LIVE,这个标记表示source filter并不是一个真正的实时源,即使它暴露了IAMPushSource接口,因此,audio renderer不进行速率匹配。      ·AM_PUSHSOURCECAPS_PRIVATE_CLOCK,这个标记表示source filter使用一个私有的时钟来产生时间戳。在这种情况下,audio renderer速率匹配与时间戳会有冲突。(如果样本没有时间戳,那么renderer忽略这个标记。     *如果GetPushSourceFlags返回没有标记(0),audio renderer的行为依赖于graph时钟和样本是否拥有时间戳:      ·如果audio renderer不是graph参考时钟,并且样本拥有时间戳,那么audio renderer速率匹配与时间戳会有冲突      ·如果样本没有时间戳,audio renderer尝试与输入的音频数据的速率进行匹配。      ·如果audio renderer是graph参考时钟,它与输入的数据速率进行匹配。     最后一种情况的原因如下:如果audio renderer是参考时钟,并且source filter使用同样的时钟来产生时间戳,那么audio renderer不会与这个时间戳进行速率匹配,因为如果它这样做了,导致的结果是,它等于在尝试与自己进行速率匹配,这将导致时钟偏差。因此,在这种情况下,renderer与输入的音频数据速率进行匹配。 

 

3.7. Graph动态重建(Dynamic Graph Building)     如果你需要修改一个已经存在的filter graph,你可以停止,修改后再重新启动它。这通常是一种最佳的解决方法。但是,在某此情况下,你可能需要在一个graph处于运行状态时来修改它,比如:     *应用程序在进行视频回放时需要插入一个(视频滤镜filter)Video effect filter;     *source filter在播放的过程中改变了媒体格式,此时可能需要接入新的解码filter;     *应用程序在graph中加入一个新的视频流。     上面的这些都是graph动态重建的例子。所有在graph继续处于运行状态而做的graph修改都被叫做graph动态重建。动态重建可以由应用程序发起,也可以由一个在graph中的filter发起。动态重建有三种可能:     *媒体格式动态变化:一个filter可以在运行的中途改变媒体格式,而不需要重新被替换为另一个;     *动态重连:在graph中添加或删除filter     *Filter Chain操作:添加,删除,控制filter chain,(Filter Chain是相互连接着的一条Filter链路,并且链路中的每个Filter至多有一个Input pin,至多有一个Output pin)      3.7.1. 动态重连     在绝大多数的directshow filter中,当graph处于运行状态时pin是不能被重新连接的,应用程序必须在重连前停止graph。但是,某些filter却支持动态重连,这既可以由应用程序来执行,也可以由graph中的一个filter来执行。     如下图:      

      假设我们要将filter 2从graph中移除掉,替换成另一个filter,而此时graph还处于运行状态,那么必须具备以下几个条件:     *filter 3的输入pin(pin D)必须支持IPinConnection接口,这个接口可以重新连接pin而不需要停止它。     *filter 1的输出pin(pin A)必须能够在重连时阻塞媒体数据,数据不再在pin A和pin D之间传递。也就是说,输出Pin必须支持IPinFlowControl接口。但是,如果filter 1是发起重连的那个filter,那么它有可能已经在其内部实现了阻塞;     动态重连包括下列步骤:     1. 从Pin A那里阻塞数据流     2. 重新连接Pin A和Pin D,或者在中间加入新的filter     3. 取消Pin A上的阻塞

    步骤1. 阻塞数据流     通过调用Pin A上的IPinFlowControl::Block方法来阻塞数据流。这个方法既可以被同步调用,也可以被异步调用。要异步调用这个方法,需要创建一个win32事件对象,并将事件句柄传给Block,方法会立即返回,然后使用WaitForSingleObject或其它函数来等待事件的触发。当阻塞工作完成时,pin会触发这个事件。如:

// Create an event HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEvent != NULL) {     // Block the data flow.     hr = pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK, hEvent);      if (SUCCEEDED(hr))     {         // Wait for the pin to finish.         DWORD dwRes = WaitForSingleObject(hEvent, dwMilliseconds);     } }

    如果是同步调用Block,那么只需将传入的hEvent参数设为NULL,此时这个方法会一直阻塞到阻塞工作完成为止。如果pin还没有准备好deliver一个新的sample,那么就会一直阻塞。而如果filter处于就绪状态,这可能会花费任意长的时间,因此,不要在应用程序的主线程中使用同步调用,以免发生死锁,开一个工作线程来使用同步调用,或者干脆就使用异步调用。

    步骤2. 重连pin     要重新连接pin,查询graph的IGraphConfig接口并调用IGraphConfig::ReconnectIGraphConfig::Reconfigure。Reconnect方法使用比较简单:     *停止中间filter(比如filter 2),并移除它     *如果需要的话,加入新的中间filter     *连接所有的pin     *pause或run所有新的filter,使它的状态与graph相同     Reconnect方法有参数可以用来指定pin连接的媒体类型和中间filter。如:

pGraph->AddFilter(pNewFilter, L"New Filter for the Graph"); pConfig->Reconnect(     pPinA,      // Reconnect this output pin...     pPinD,      // ... to this input pin.     pMediaType, // Use this media type.     pNewFilter, // Connect them through this filter.     NULL,      0); 

    如果Reconnect还不够用来应付我们的要求,那么你可以使用Reconfigure方法,它调用一个由应用程序定义的回调函数来重连这些pin。要调用这个方法,需要在你的应用程序中实现IGraphConfigCallback接口。     在调用Reconfigure之前,如前面所述地那样阻塞输出pin的数据流。然后如下所示,将处于待处理状态的数据push下去:     1. 在重连链路中处于下游的最远的那个输入pin(例子中为Pin D)上调用IPinConnection::NotifyEndOfStream方法,方法的参数是一个Win32事件句柄;     2. 在与要阻塞数据的那个输出pin直接相连的那个输入pin上调用IPin::EndOfStream方法。(在例子中,要阻塞的那个输出pin是pin A,那么直接与之相连的那个输入pin为Pin B);     3. 等待事件触发。输入pin(pin D)在它接收到end-of-stream事件通告时触发事件。这表示再没有数据需要传输,此时就可以安全地进行重连了。     注意:IGraphConfig::Reconnect方法会自动处理上述步骤,你仅在调用Reconfigure方法时才需要自己来处理。     当数据完成push后,调用Reconfigure,传入IGraphConfigCallback回调函数的指针。Filter Graph Manager会调用IGraphConfigCallback::Reconfigure方法。

    步骤3. 取消数据流的阻塞     当你完成重连后,通过调用IPinFlowControl::Block,第一个参数为0来取消阻塞。

    注意:如果动态重连是由一个filter来执行的,那么你需要知道一点线程方面的问题。如果filter graph manager尝试去停止filter,它可能会死锁,因为graph等待filter停止,而与此同时,filter有可能在等待数据在graph中完成push。要防止这个可能存在的死锁问题,如前所述可以用事件机制来处理。

3.7.2. filter链(filter chains)     一个filter chain是一系列具备下述条件的相互连接的filter:     *每一个在链中的filter最多只有一个已连接的输入pin和一个已连接的输出pin;     *Filter链路中的数据流不依赖于链路外的其他Filter     举个例子,在下图中,filter A-B,C-D和F-G-H是一个filter chains。每个F-G-H中的子链(F-G和G-H)也是一个filter chain。一个filter chain同样可以是由单个filter组成的,因此A、B、C、D、F、G和H同样也是filter chain。filter E由于有两个输入连接,所以任何含有E的一系列filter都不是filter chain。  

      IFilterChain接口提供下述方法来控制filter chain:

    IFilterChain::StartChain  开启一个链     IFilterChain::StopChain   停止一个链     IFilterChain::PauseChain  暂停一个链     IFilterChain::RemoveChain  从graph中移除一个链

    没有特殊的方法来添加一个链,要添加链,通过调用IFilterGraph::AddFilter方法来插入新的filter,然后调用IGraphBuilder::ConnectIGraphBuilder::Render或类似的方法来连接它们。     当graph运行时,一个filter chain可以在运行和停止状态间切换。当graph处理就绪状态时,它可以在就绪和停止状态间切换。这是两种仅有的filter chain状态切换可能。     Filter链指南     当你使用IFilterChain方法时,确认在graph中的filter是否能支持filter链操作是十分必要的,否则,可能会发生死锁或graph错误。filter连接到链上必须发生在链状态改变后。     使用IFilterChain的最佳情况是与一系统为链而设计的filter一起使用。使用下面的指南来确保你的filter是链操作安全的。参考下图:    

        在filter链状态变化前,所在在filter链分界线上调用的数据处理都必须已完成。这个规则应用于IMemInputPin::ReceiveIPin::NewSeqmentIPin::EndOfStream方法。链中的filter必须从由链外filter实现的这些方法调用中返回;而链外的filter也必须从这些由链内filter实现的这些方法调用中返回。     举个例子,在上图中,filter B必须完成在filter A上的所有数据处理调用,而filter E也必须完成从filter D上的调用。如果pin暴露了IPinFlowControlIPinConnection接口,那么如在动态重连那一节中所讲的,你可以通过调用IPinFlowControl::BlockIGraphConfig::PushThroughData方法来推数据。filter也可能通过自己的方法来推数据。     上游filter必须与链的状态一起发生变化。比如,在上图中,假如链已停止,但filter A调用IMemInputPin::Receive方法,那么调用将失败,作为回应,filter A停止流。当应用程序重新开启链时,不会产生什么影响,因为filter A不再向使数据流动了。     下游filter必须同样与链的状态一起发生变化,否则,下游filter在等待取得sample时会发生死锁,因为sample不会再到来了。比如,多路复用(MUX)filter总是在它所有的input pin上需要数据,如果挂起其中的一个input pin,在其它input pin上的流处理也会被阻塞。这会导致graph死锁     每个与链内部filter相连的外部filter的pin必须拥有自己的分配器(allocator),它不能被其它pin连接共享。当链的状态发生变化或从graph移除掉时,分配器便不可用了,此时如果还有其它的连接使用这个分配器的话,它们将不能再处理sample了。     除非与链相连的filter支持动态断开,否则不要移除链。典型的,已连接的filter会支持IPinConnection或IPinFlowControl接口,或者用它自己定义的接口代替。

3.8. 插件发布者(Plug-in Distributors)

    Plug-in Distributors(PIDs)是扩展filter graph manager的一种方法。一个PID是filter graph manager在运行时聚合的一个COM对象。应用程序通过filter graph manager来进入PID。     当filter graph manager被要求查询一个它不支持的接口时,它会搜索注册表项:         HKEY_CLASSES_ROOT/Interface/IID/Distributor    IID是接口的GUID,如果注册项存在,那么键值便是支持该接口的PID类标识(CLSID)。filter graph manager聚合了PID并返回接口指针,应用程序调用这个指针时实际上就是在调用PID,但是这对于应用程序来说是透明的,对于应用程序来说,它就象是在filter graph manager上调用这个接口一样。    PID为应用程序提供了一种简单的控制filter的方法,如通过调用IFilterGraph::EnumFilters方法,PID可以枚举graph中的所有filter并调用这些filter上的方法。    当filter graph manager聚合了一个PID时,它查询PID的IDistributorNotify接口,如果PID支持这个接口,filter graph manager用它来通知PID有关graph的状态变化:  * 当filter graph在run、pause和stop状态之间切换时,它调用IDistributorNotify::RunIDistributorNotify::PauseIDistributorNotify::Stop。  * 如果调置了参考时钟,filter graph manager调用IDistributorNotify::SetSyncSource。  * 当有filter添加或移除,或pin连接有变化时,filter graph manager调用IDistributorNotify::NotifyGraphChange。      当自己定制PID时,自己所创建的COM对象必须支持聚合,并且它所支持的接口是filter graph manager本身所没有的。IDistributorNotify接口是可选的。    如果PID从filter graph manager上获得一个接口,那它必须立即release这个接口,否则会在COM对象上出现循环引用的参考计数,使得filter graph manager无法被销毁。在filter graph manager上保持一个参考计数是多余的,因为PID的生命期是由filter graph manager控制的。    因为PID是明确指定是被filter graph manager聚合使用的,因此你应该在PID的构造函数中强行检查IUnknown指针是否为NULL,如果为NULL,则返回错误码VFW_E_NEED_OWNER。同时,为了防止其它对象聚合PID,你可以在IUnknown上查询IGraphBuilder接口,如果不行则返回错误。

你可能感兴趣的:(DirectShow)