DirectShow技术描述与应用
―――释雪
DirectShow是我最早接触一项微软技术,比COM技术还早,真不知道当时我是怎么学的。DirectShow是一个基于COM组件技术的多媒体控制组件。它能够进行媒体的捕捉、格式转换以及播放等等功能。实际上,MediaPlayer就是在DirectShow基础上搭建起来的。功能可以说非常的强大。此次我将DirectX 8.1中的DirectShow的使用说明部分翻译出来,与大家分享。出于个人英语水平,必有错译及误漏之处,还请不吝指正,于此万谢。
DirectShow系统概述
多媒体面临的挑战:
现今多媒体技术主要面临的技术有如下几点:
1.多媒体数据流一般包含了很大的数据信息,如何才能更好处理这些数据。
2.由于音频与视频必须同步播放,那么就需要解决它们如何同时开始和停止,并且拥有同样播放速率(rate)。
3.数据流可以来自很多地方,如本地文件,网络,电话广播和数码相机等。如何解决它们,使能够同样地播放和处理。
4.数据流可以是不同的格式,如AVI,ASF还可以是MPEG等。如何解决它们,使能够同样地播放和处理。
5.在实施程序设计时,并不了解使用者的硬件配置与性能。如何使得应用程序具有机器无关性。
DirectShow解决方案
DirectShow设计初衷就是要解决以上所涉及到种种技术问题。它主要设计目的是简化建立基于Windows®平台的数字媒体应用程序的设计任务。并使它无需涉及数据传输,硬件通用性,媒体同步等等诸多复杂问题。
为了完成所需的音视频流的处理,DirectShow应用了DirectDraw®和DirectSound®技术。这些技术可以有效率将数据图像和音频还原(render)到用户的显卡和声卡上去。DirectShow通过压缩媒体流上的时间标记(time-stamped)来实现媒体同步重放。为了能够处理不同可能出现的数据源、数据格式以及硬件系统,DirectShow采用了一种标准化结构体系。在其中,应用程序可以混合和匹配不同被称为过滤器(filter,也有叫滤镜,我记得在Photoshop里见过。)的组件。
DirectShow提供的过滤器支持捕获和配置基于Windows驱动模型(Windows Driver Model)的设备。同样地,过滤器也支持视频捕捉卡和通过ACM和VCM接口进行编解码。
在DirectShow标准结构体系中,DirectShow过滤器与控制(control)、多样化的设备、本地文件系统、TV调频和视频捕获卡、VFW编码器、视频显示卡(通过DirectDraw或GDI、GDI+)和显卡进行交互(communicate,通信)。因而,DirectShow可以将应用程序与前面所列出的种种复杂性素进行隔离。DirectShow也为几种文件格式提供本地压缩和解压过滤器。
过滤器表(filters graph)及其组件
过滤器
过滤器是DirectShow最基本组成部分。一个应用程序可以通过将不同过滤器进行连接来实现附合特殊的要求媒体功能。
比如:
Async File Source可以读取本地文件。
TV Tuner过滤器可以改变TV卡上的电视频道。
MPEG-2 Splitter过滤器从MPEG-2数据流分解出音视频数据。
虽然,每一个过滤器都有它自己特殊的功能,但应用程序可以用同样的语法来访问它们,所有的过滤器都支持IBaseFilter接口。
大多数的过滤器都可以被以下几大类:
源过滤器在处理时提供未加工的数据(raw data,源数据?)。它可以从本地文件、DVD光盘、电话卡、捕获卡、数码照机或者其它数据源中取得数据。
转换(transform)过滤器接受从其它过滤器传来的数据,处理它并传给下一个过滤器(第三方过滤器,a third filter)。转换过滤器一般有这几种:解析,将未加工的字节流并转换成有意义的(meaningful)媒体数据;编码与解码,在压缩数据与解压缩数据之间进行转换;分解(splitters),将一个数据流分成两个或多个数据流;混合,将几个数据流合并成一个数据流。
还原(renderer)过滤器从其它过滤器传来的数据,并播放它,或者是传递给其它硬件设备。这类过滤器还包括了那些负责将数据储存在储存器上的过滤器。视频还原使用DirectDraw来显示图像,默认音频还原使用DirectSound来播放音频。
DirectShow提供的播放、转换、捕获各种不同媒体格式的过滤器,可以在任何一种的Windows上工作。开发者也可以编写自定义的过滤器来完成自己所需求的功能。
过滤器表和过滤器表管理器
为了在DirectShow中完成各种任务,应用程序应当建立一个或者更多的过滤器并将它们连接在一起。所以数据从一个过滤器传递到下一个,从源过滤器一至到还原过滤器。工作在一起的一组过滤器叫做过滤器表。
过滤器表,我觉得将其认为是一组过滤器之间连接逻辑更为容易理解。
过滤器表本身并不是一个实在的(distinct)软件组件。然而,它是可以通过一个被称为过滤器表管理器的COM组件来管理的。在一些任务中,过滤器表管理器控制着过滤器表的状态改变,为所有的过滤器建立一个参考时钟,为过滤器和应用程序进行通信。过滤器表管理器可以自动为文件的播放建立一个合适的过滤器表,也可以建立表的某一个部分,从而减少应用程序的工作负担。
Note Filters always reside in the same process as the Filter Graph Manager. They are always loaded from in-process servers. Therefore, method calls are not marshalled between filters, or between filters and the Filter Graph Manager.
用术语来讲,过滤器表是一个直线的,非循环的,非连接的(non-connected)表:
直线的:数据必须按照预先设定好的唯一在过滤器移动。
非循环的:在过滤器表中不能有过滤器连接成环形。
非连接的:过滤器表是将过滤器串成链而不是连接。也就是说过滤器无法得知与它相连的是什么过滤器。每个过滤器都是各自封闭的。
针(Pins)
将一个过滤器连接到其它过滤器的点叫做针。每个针都是支持IPin接口的COM组件对象。针虽然使用COM引用计数,但是过滤器自己的针控制针的生存周期。过滤器可以实时(runtime)自动建立或销毁针对象。
每一个针都只有一个传递方向,即输入或输出。它必须与它相反的传递方向的针进行连接。(即只能输出针与输入针进行连接)。媒体数据通过针连接,总是从输出针到输入针。我们叫输入针的方向叫做下游,输出针的方向叫做上游。(一些的控制信息可以向上游方向移动)
当两个针连接时,必须提供连接的详细资料,如媒体类型、缓内大小和传输机制(被称为传输器)。如果其中有任意一个针拒绝连接,那么两个过滤器就无法交换数据。
媒体样本(sample)和媒体类型
当两个过滤器连接时,它们必须使用同一种数据类型。然后,上游过滤器将附合这种数据类型的数据通过过滤器的输出针输出,下游过滤器的输入针接受数据,进入处理。这样,确保下游过滤器可以处理它所接受的数据。在DirectShow中,数据类型是使用AM_MEDIA_TYPE结构进行描述,被称为媒体类型。每个针的连接必须确定一个媒体类型。一些针可以接受并行(a wide range?)的媒体类型,其它有限的几种类型。
一旦过滤器的连接被建立起来,上游的过滤器可以通过针的连接来发送数据给下游的过滤器,上游过滤器将数据打包成一个被称为媒体样本的COM对象。媒体样本支持IMediaSample或IMediaSample2接口。可以向里面添加实际媒体数据的媒体样本包含一个时间标识,用来说明样本的当前时间。
比较典型的,一个视频媒体样本包含一个图像帧,一个音频媒体样本可以包含几个音频样本。
过滤器自动地分配内存来建立一个包含媒体数据的媒体样本。
时钟
在播放媒体时,维持一个正确的播放速率是非常重要的。为了这个目标,每个媒体样本都携带一个时间标识,来说明所携带的媒体数据的时间。过滤器表管理器在整个表中维持一个时钏。这个时钟称为参考时钟,它所提供的时间称为参考时间。参考时间以千万分之一秒为单位(100 nanaoseconds。我算术算对了吧)
在一些过滤器表中,过滤器可以提供参考时钟。例如,音频还原过滤器通常可以用于基于声卡的精确时钟。如果没有过滤器可以提供时钟上,则过滤器表使用Windows系统时钟。
建立过滤器表
标准表的建立
为了建立过滤器表,你必须首先建立一个过滤器表管理器的实例,取得一个IGraphBuilder接口指针。
IGraphBuilder* pIGB;
HRESULT hr;
hr = CoCreateInstance(CLSID_FilterGraph,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGraphBuilder,
(void **)&pIGB);
IGraphBuilder接口,顾名思义(as its name suggests),其中包含了建立过滤器表的方法。这些方法提供三个最基本的方式来建立表。
1.应用程序可以让过滤器表管理自动来建立一个完整的表。
2.应用程序和过滤器表可以共用参与表的建立工作。
3.应用程序手动连接每个过滤器来完成整个表的建立
Approach #1: The Filter Graph Manager builds the entire graph
方式1:过滤器表管理器自主完成整个表的建立
方式1是假定你只想完成播放几种通常格式的媒体文件而设计的,这些格式可以是AVI,MPEG,WAV,MP3等等。在DIrectShow中,术语还原(render)经常被引用,它的意思是在视频在显示器上显示,音频在音箱中播放的播放方式。为了能够使过滤器表管理器可以自动为还原指定的媒体文件建立过滤器表,应用程序应当使用IGraphBuilder::RenderFile方法。下面将描述一个过滤器表建立表的几个基本步骤。
查找相应的过滤器(Finding the Filters)
建立过滤器表的第一步就是查找并建立所需的过滤器实例。过滤器选择操作首先是要确定媒体文件格式。这是因为如果两个过滤器不使用同一种公共媒体格式,那么它们就不可能被连接起来。过滤器表管理器可以通过对文件名的后缀的检查来确定媒体格式(如.wma,.avi,.wav等等)。如果通过后缀名不能确定媒体格式,那么过滤器表管理器就过在文件中查找标识字节(check bytes)来确定媒体格式,这是通过使用不同源过滤器进行读取文件,直到有匹配的源过滤器来实现。而且,这是通过使用IGraphBuilder::AddSourceFilter来完现这一个任务的。
所有DirectShow过滤器在注册表里都有唯一的GUID,还记录着包括过滤器的类别,支持的格式,过滤器的merit等其它信息。过滤器表管理器调查所有这些信息来确定过滤器,并建立其实例。过滤器类别描述过滤器的主要使用用途。媒体格式信息描述过滤器可以接受什么媒体格式,又可以向它下游的过滤器传递何种媒体格式。merit值记录着过滤器在表自动建立中被考虑的优先级。如果系统中有两个媒体格式信息相近的过滤器,那么过滤器表管理器会选择merit级别高的过滤器。(有些过滤器表有意地定义较低的优先级值,因为它们原意是为自定义过滤表而设计,使得应用必须手动向过滤器表进行添加)
为了搜索注册表,过滤器表管理器建立一个DirectShow Filter Mapper的对象,并调用其IFilterMapper2::EnumMatchingFilters方法来完成搜索。这个方法可以通过种类或媒体格式来枚举过滤器。它会返回一个标准的IEnumMoniker对象,其包含所有被搜索到的过滤器的名字对象(monilders)。
向表中添加过滤器
如果找到可以读取相应文件格式的源过滤器的名字对象,过滤器表管理器使用CoCreateInstance(通过GUID)来建立相应过滤器的实例。这个源过滤器将是用做文件源过滤器。过滤器表管理器调用IFilterGraph::AddFilter方法来向过滤器表添加这个过滤器。当过滤器被通知要加入表后,过滤器就会建立相应的媒体格式的输出针。当建立针后,过滤器表管理器会从注册表里找到可以接受其输出针所输出的媒体格式的过滤器,当找到匹配的,则向表中添加它的一个实例。
连接过滤器
当下游过滤器被成功地添辑后,它就设置它的针,来准备与其上游的过滤器进行连接。过滤器表管理器询问每一个表中的过滤器,确定其适当的针,并调用IGraphBuilder::Connect方法来连接它们。上游过滤器的输出针和下游过滤器的输入针必须协商确定使用何种数据媒体格式、确定由哪个针来提供为媒体样本对象分配内存和存放媒体样本对象内存的管理对象的工作。
完成表的建立
当针们(pins)建立好连接后,过滤器表管理器检查媒体格式和搜索支持新加入的过滤器的输出针的媒体格式的过滤器,并再次加入表当中去。直到所需的过滤器都被添加到表里去了。在文件回放表中,当读取一个未加工数据流时,第二个过滤器通常是解析或者分解过滤器。如:AVI,MPEG,分解它成音频流和视频流。并为每一个流建立一个输出针。如果数据流中有任何一个是被压缩的,那么下一个过滤器必须是解压装置,之后才能添加还原过滤器。
当表建立好之后,应用程序只需从过滤器表管理器QueryInterface出IMediaControl接口,调用其Run方法就可以开始播放文件。
方式2:应用程序和过滤器表管理器共同参与表的建立工作
当你的过滤器表需要比简单的播放文件更复杂的功能时,你的程序至少要完成表的建立工作中的一部分。诸如,如果你要建立一个将AVI转换到MPEG的表,那么你仍可以通过IGraphBuilder::RenderFile方法来建立一个AVI回放表。但你必须通知表将输出设置为MPEG文件而不是显示器和音箱。这包括断开和移除音频还原过滤器和视频还原过滤器(这可以使用IFilterGraph::Disconnect和IFilterGraph::RemoveFilter)和添加MPEG视频压缩过滤器、MPEG音频压缩过滤器、MPEG混合过滤器和一个写文件过滤器(file writer filter)。
它也许需要添加一个中间(intermediary)过滤器来转换MPEG压缩所支持的媒体格式。应用程序可以直接添加中间过滤器,也可以使用IGraphBuilder::Connect方法来添加。当这个方法被调用,过滤器表管理器首先尝试直接连接过滤器,如果它们不支持相同的媒体格式,那么过滤器表管理器会搜索并添加相应中间过滤器。如果防止过滤器表管理器尝试添加中间过滤器的话,则应该使用IFilterGraph::ConnectDirect方法。
在上面的一部分,讲解了应用程序参与了建立表的输出部分。如果,应用程序只参与表的输入部分(即源部分)。则可以使用IGraphBuilder::Render方法来让过滤器表管理器自动完成表的输出部分的建立。
方式3:应用程序自己建立整个表。
在一些情况中,你的应用程序需要完成整个表的建立。当然,这里包括过滤器的添加以及每个过滤器的连接。这一种状况中,你应该知道有哪些过滤器需要被添加入表中,所以这里是不需要使用Filter Mapper对象来搜索过滤器的。你可以使用IGraphBuilder::AddFilter方法来向表添加过滤器,并使用Connect或者ConnectDirect来连接过滤器。
在过滤器表中的数据流动
这部分将进步地描述媒体数据是如何在过滤器表中移动的。通常,你在编写DirectShow应用程序时并用不着这些。不过,在某些情况中,这些资料可能是会对你有所帮助的。如果你正在写一个DirectShow过滤器的话,那么你就需要理解这部分资料。
传输协议(Transports)
为了使媒体数据可以在过滤器表中流动,DirectShow过滤器必须支持几种内部协议的其中的一个。这些协议被称为传输协议。当两个过滤器连接时,它必须支持一种相同的传输协议;否则它们就无法交换媒体数据。通常,传输器需要一个支持特殊接口的针。当过滤器连接时,针将查询得到(QueryInterface)其它针的这个接口。
大多数过滤器在主存中处理媒体数据,通过针的连接来将媒体数据传递给其它的过滤器。这个传输的方式叫做本地存储传输协议(local memory transport)。这种本地存储传输协议在DirectShow中很普遍,但并不是说所有的过滤器都使用这种协议。如:有些过滤器是通过硬件途径来传送媒体数据的,它们只使用针来传送控制信息。
DirectShow为本地存储传输协议定义了两种机制,分别是push模式和pull模式。在push模式中,由源过滤器来产生媒体数据并传递给它的下游过滤器。push模式中,在下游的过滤器总是被动地接收数据,处理它,传递其下游的过滤器。在pull模式中,源过滤器连接解析过滤器。解析过滤器向源过滤器发送数据请求,源过滤器则发送数据来回应其请求。push模式使用ImemInputPin接口,pull模式使用IasyncReader接口。
在DirectShow中,push模式的应用要比pull模式更为普遍。因此,本文将只考虑push模式的应用。我们将本部分的最后一节, pull模式,来讲述IAsyncReader接口与IMemInputPin接口有什么不同。
样本与分配器(Allocators)
当针传递数据给其它针时,它并不直接传递其数据内存的指针。而是,传递一个管理内存数据的COM对象指针。这个对象就是媒体样本,其向外暴露ImediaSample接口。接收针通调用ImediaSample接口的方法来对内存进行访问。比较经典的方法有IMediaSample::GetPointer,IMediaSample::GetSize和IMediaSample::GetActualDataLength方法。
媒体样本总是通过输出针到输入针的途径向下游传播。在push模式中,输出针调用IMemInputPin::Receive来向输入针传递媒体样本,输入针同步地处理数据(实际上,这些动作都是在Receive方法中完成)。或者在工作线程(worker thread)中异步处理。如果输入针在处理数据时需要等待资源,则可以阻塞(block)Receive方法。
另一个名为分配器的COM对象用于建立和管理媒体样本。分配器向外暴露IMemAllocator接口.当过滤器需要媒体样本时,它可以调用IMemAllocator::GetBuffer方法,来返回这个样本的指针。每两个针的连接中都共享一个分配器。当两个针连接时,它们会决定由哪个过滤器来提出供分配符,它们并设置诸如内存区的数量和其它内存的大小。
样本的引用计数
分配符只可以建立有穷数量的媒体样本。在当其它人调用GetBuffer时,一些样本可能在被使用。为了避免这种情况,分配器使用引用记数来保留样本的使用记录。GetBuffer方法会返回一个引用记数为1的媒体样本。当如果引用记数变成0时,样本就被收入到分配符的回收箱(pool)中,它就可以在下一次的GetBuffer调用时被使用。当样本的引用记数大于0时,样本就不会被GetBuffer所返回。当所有属于分配器的样本都在被使用(即引用记数大于0),则GetBuffer方法被阻塞,直到有一个样本变成可用。(即样子被放弃使用,引用记数等于0)
例如,如果输入针接受到一个媒体样子。如果它是在Receive方法中同步地处理媒体样本,它是不会增加引用记数的。当Receive返回后,输出针放弃(Release)媒体样本,引用记数降为0,媒体样本进行分配器的回收箱。如果输入针是在工作线程中处理媒体样本,则它就是给在Receive方法返回前,增加引用记数。那么现在的引用记数就是2。当输出针放弃了样本后,引用记数变成了1,但样本仍然没有回到回收箱中。在工作线程完成处理后,线程调用Release来释放样本。
当一个针接收到样本后,它可以复制其数据来其它的样本当中去,也可以修改这个样本并传给它下一个过滤器。因此,一个媒体样本是可以在整个表中进行流动的。每个过滤器在改变其数据时,都会调用其AddRef和Release方法改变其引用记数。因此,输出针在其调用完Receive方法后,不得现次使用媒体样本,因为其下游过滤器也许也在使用这个样本。输出针可以调用GetBuffer方法来获得新的样本。
这种机制很好地避免了内存的使用数量,因为过滤器在重复使用同一个内存区域。它也可以避免过滤器在处理时,出现内存访问出界的情况。因为分配器维护着一个可使用样本的列表。
A filter can use separate allocators for input and output. It might do this if it expands the input data (for example, by decompressing it). If the output is no larger than the input, a filter might process the data in place, without copying it to a new sample. In that case, two or more pin connections can share one allocator.
过滤器可以为输入和输出建立不同的分配器。当输出比输入的数据量大时,通常使用这种方法(比如,解压缩)。如果输出数据量没有输入的大,过滤器可以直接修改样本上的数据,传到下一个过滤器。在这种情况中,两个或更多针的连接可以共享于一个分配器。
提交与解除提交分配器(Committing and Decommitting Allocators)
当过滤器第一次建立分配器时,分配器并没有内存区域可供管理。基于这一点,这时任何的GetBuffer调用操作都会失败。当表开始运行时,输出针会调用IMemAllocator::Commit方法,来提交分配器,使用其开始进行内存的分配。之后,针们就可以调用GetBuffer方法了。
当表停止运作时,针则会调用IMemAllocator::Decommit方法,来解除提交分配器。之后,任何的GetBuffer调用操作就会遭受失败,真到分配器再一次被提交。同样地,那些被阻塞的GetBuffer,以期得到可用的样本的调用,也会被立即返回。(执行Decomit方法并不能实际释放内存。例如,CMemeAllocator类需要调用它的析构函数来释放内存。)
DirectShow中的事件通知
这一部分将描述在Microsoft® DirectShow®过滤器表中,事件是如何实现的;一个应用程序如何才能接受到事件通知并且响应它们。
事件通知概述
过滤器通过投递事件通知来向过滤器表管理器通报一个事件。事件可以是包含任何信息,如流的结束,也可以是一个错误,如还原流的失败。过滤器表管理器本身处理一些过滤器事件,其它事件则留给应用程序来进行处理。如果过滤器表管理器遇到一个不能处理的事件,它就将事件放入到一个队列中去。同样的,过滤器表管理器也会将它自己的事件通知放入队列中去,以期应用程序来进行处理。
应用程序可以从队列中接收到事件并对它们做出响应。因此DirectShow的事件通知与Microsoft® Windows®消息队列模式非常相似。应用程序可以取消过滤器表管理器本身可以处理事件的这种行为,过滤器表管理则直接地将这些事件放入队列中去,由应用程序来进行处理。这种机制允许:
过滤器表管理器与应用程序进行通信。
过滤器可以与应用程序和过滤器表管理进行通信。
由应用程序确定它自己都处理哪些事件。
接收事件
过滤器表管理器向外部暴露三个接口,来支持事件通知.
IMediaEventSink包含过滤器投递事件的方法
IMediaEvent包含应用程序接收事件的方法
IMediaEventEx inherits from and extends the IMediaEvent interface.
IMediaEventEx继承于并扩展了IMdeiaEvent接口
过滤器通过调用过滤器表管理器的IMediaEventSink::Notify方法来投递事件通知。一个事件通知由一个事件代码和两个DWORD型参数组成。根据不同的事件代码,参数值可能会是指针、返回码、参考时间或者其它信息。
为能从队列中接收事件,应用程序应调用IMediaEvent::GetEvent方法来接收数据。这个方法会被阻断直到从一个队列中得到一个事件或者超过时限。当调用完GetEvent后,应用程序应该总是调用IMediaEvent::FreeEventParams方法来释放在事件参数中的资源。例如参数有可能是由过滤器表提供的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);
}
为了停止某个事件的过滤器表管理器的默认处理,可以调用IMediaEvent::CancelDefaultHandling方法。你可以通过调用IMediaEvent::RestoreDefaultHandling方法来恢复事件的默认处理。对于那些没有过滤器表管理器默认处理的事件,调用这些方法将不会产生任何影响。
事件的什么时候发生
为了DirectShow事件,应用程序需要一种在队列中有事件在等待时,能够被得知的方法。过滤表管理器提供两种途径来实现:
窗口通知:过滤器表管理器在产生一个新事件时,向应用程序窗口发送一个自定义的消息。
事件信号:如果在队列有DirectShow事件时,则过滤器表管理器发送一个窗口事件。并在队列为空时重置事件。
An application can use either technique. Window notification is usually simpler.
应用程序可以使用其它手法来实现。但窗口消息通常会更为简单。
窗口通知
可以调用IMediaEventEx::SetNotifyWindow方法来指定一个私有消息,来建立起窗口通知。这个私有消息可以使用从WM_APP到WM_APP+0xBfff之间的数。只要当过滤器表管理向队列里放入新的事件,它就会向指定的应用程序窗口发送信息。应用程序对来自Windows消息循环的消息作出响应。
下面代码说明了如何设置通知窗口。
#define WM_GRAPHNOTIFY WM_APP + 1 // Private message.
pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
下面代码说明了如何对窗口消息做出响应
LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)
{
switch (msg)
{
case WM_GRAPHNOTIFY:
HandleEvent(); //应用程序定义的函数.
break;
//也可以在这里编写消息处理。
}
return (DefWindowProc(hwnd, msg, wParam, lParam));
}
因为事件通知和消息循环都是异步的,所以在你的应用程序对消息作出响应时,队列中有可能包含多个事件消息。也可能有时候事件会变得无效,那么它就会被清除出队列。因此,在你的事件处理代码中,应不停地的调用GetEvent方法,直至返回失败码,即消息队列已为空。
注意,在你释放IMediaEventEx指针时,先要通过向SetNotifyWindow传入一个空指针来取消事件通知。在你的事件处理代码中,一个定要在调用GetEvent方法前检查IMeiaEventEx指针的有效性。这样可以预防在释放了IMediaEventEx指针后,应用程序再次执行事件处理代码所可能出现的错误。
事件的发出(signal)
过滤器表管理器维护一个手动重置(manual-reset)的事件,用来反应事件队列的状态。如果事件队列包含有未解决的事件通知,那么过滤器表管理会发出一个手动重置事件。如果事件为空,调用IMediaEvent::GetEvent方法会重置事件。应用程序可以使用此事件来确定队列的状态。
注意:这个语术可能会被误解。手动重置事件是一个通过Windows CreateEvent函数建立的事件类型,它在DirectShow中并不做任何事情。
调用IMediaEvent::GetEventHandle方法可以得到手动重置事件的句柄。可以通过WaitForMultipleObject函数来等待这个事件的发生。一旦此事件发生,调用IMediaEvent::GetEvent方法就可以得到DirectShow事件。
下面的代码示例了这样的功能。它取得事件的句柄,每100毫秒的间隔等待事件的发生。当事件发生后,通过调用GetEvent方法来取得事件,并将事件代码和事件参数显示出来。当EC_COMPLETE事件(其表示回放已经完成)出现则循环结束。
HANDLE hEvent;
long evCode, param1, param2;
BOOLEAN bDone = FALSE;
HRESULT hr = S_OK;
hr = pEvent->GetEventHandle((OAEVENT*)&hEv);
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);
hr = pEvent->FreeEventParams(evCode, param1, param2);
bDone = (EC_COMPLETE == evCode);
}
}
}
因为过滤器表会适当地自动设置或重置事件,你的应用程序可以免于对这些事情的处理。同样的,当你释放了过滤器表后,过滤器表就会释放掉事件句柄。因此在你释放过滤器表后,就不要再去使用事件句柄。
过滤器的状态(Filter States)
过滤器有三种状态:已停止,已暂停和正在运行。这个已暂停的状态可以立即对运行命令做出响应。在DirectShow中是由过滤器表管理器来控制所有状态的改变的。当应用程序调用IMediaControl::Run,IMediaControl::Pause和IMediaControl::Stop方法时,过滤器表管理器则用所有过滤器的IMediaFilter接口中的相应的方法,来改变所有的过滤器的状态。已停止状态和正在运行状态之间的转换,总会经过已暂停状态。所以,如果表处于已停止状态,当应用程序启动表时,过滤器表管理器首先会将表处于已暂停状态,然后再从已暂停状态转入正在运行状态。
大多数过滤器中,正在运行状态和已暂停状态是一样的。请考虑下面这么一个过滤器表:
Source > Transform > Renderer
假定现在的源过滤器不是一个实时捕获源。当源过滤器处于暂停状态,它建立一个线程来生成新的数据并将其快速地写入媒体样本。这个线程通过ImemInputPin::Receive方法将媒体样本push给其下游过滤器。转换过滤器接收源过滤器线程传来的媒体样本。它也可以使用工作线程来处理媒样本并传递给还原过滤器,但通常是不另外建立线程来进行处理数据的。当还原过滤器处于暂停状态,它就等待接受一个媒体样本。当它接受到一个媒体样本后,它就不一定是阻塞它还是保存它了。如果它是视频还原器,那么它就将数据还原为图像,因为只接受一个媒体样本,那么这个图象就是静止的,像是一张大海报。如果表处于暂停状态,那么媒体样本将在表的第一个媒体样本之后被搁置,每个过滤器都将在Receive或GetBuffer被阻塞。虽然不会有数据被丢失,一旦源线程被取消阻塞,它将从被阻塞的地方重新开始。
源过滤器和转换过滤器可以忽略暂停状态到运行状态的转换过程,它们会立即而且非常迅速地恢复处理数据的工作。但当还原器运作,开始还原媒体样本时。首先它还原它在暂停时所保存的媒体样本。每当还原器接收到一个新的媒体样本时,它会计算这个媒体样本的表达(presentation)时间。还原器保存在当前还原的媒体样本的表达时间以前的每一个媒体样本。当它等待所表达时间时,它要么在Receive方法中阻断,要么在工作线程中的队列里接受一个新的媒体样本。还原器上游的过滤器的不参与时序按排。
实时源,如捕获设备,是一个普通的结构体系的例外。对一个实时源来说,它不适合于预先提供任何数据。应用程序可以暂停过滤器表,而再一次启动它时则需要等待很长一段时间。表不应处理”旧的”媒体样本。因此,在暂停时实时源不产生样本,而只在运行时产生。为了向过滤器表管理器发送这个行为信息。源过滤器的IMediaFilter::GetState方法就会返VFW_S_CANT_CUE。这个常量指明尽管还原器没有接收到任何数据,但过滤器已经处于暂停状态,
当过滤器停止时,它将拒绝任何传递给它。随后,源过滤器关闭它的流处理线程,其它的过滤器关闭它所建立的工作线程。针们解除提交分配器。
状态的改变
过滤器表管理器以下游而上的顺序完成所以的状态改变,其开始于还原器,结束于源过滤器。这种顺序可以防止样本的丢失和表的死锁。在状态的改变中,暂停状态与停止状态之间的转换是至关重要的。
停止到暂停:在每个过滤器进入暂停后,它们都进入准备接受媒体样本的状态。源过滤器依然是最后一个进行暂停状态的。它建立一个流处理线程和开始传递媒体样本。因为所有的下游过滤器都处于暂停状态,所以没有过滤器拒绝样本。当每一个表里的还原器都接收到一个样本时,过滤器表管理器的状态改变便完成。(实时源过滤器是一个例外,先前已经讨论过。)
暂停到停止:当过滤器进入停止后,它释放(Release)所持有(holds)的媒体样本,这样可以解开上游过滤器在GetBuffer的等待。如果进入停止的当时,过滤器正在Receive方法中等待资源,那么它将停止等待并从Receive返回,因为其下游过滤器已经处于暂停状态,下游过滤器无条件地结束Receive方法。因此,当过滤器表管理器停止下一个上游过滤器时,那个过滤器已经从GetBuffer和Receive解锁出来,可以响应停止命令。上游过滤器也许会在它得到停止命令前递送一些媒体样本,但其下游过滤器会直接拒绝它,因为下游过滤器已经处于停止状态了。
Pull模式
在ImemInputPin接口中,是由上游过滤器来确定到底要发送什么数据,并由它将数据Push给下游过滤器。不过,在某些过滤器中,使用pull模式而为适合。在这种模式中,下游过滤器向上游过滤器请求数据。媒体样本仍然是从输出针到输入针到达下游过滤器,不同的是,是由下游过滤器激起数据的流动。这种连接应使用IasyncReader接口。
The typical use for the pull model is in file playback. For example, in an AVI playback graph, the Async File Source filter performs generic file reading operations and delivers the data as a byte stream, with no format information. The AVI Splitter filter reads the AVI headers and parses the stream into video and audio samples. The AVI Splitter can determine what data it needs better than the Async File Source filter, and therefore it uses IAsyncReader instead of IMemInputPin.
文件播放是一个比较典型的pull模式。例如,在一个AVI播放过滤器表中,Async File源过滤器完成文件的读操作,并产生一个没有任何格式信息的字节数据流。AVI分解过滤器读取AVI数据头并将流解析为音频流和视频流。AVI分解过滤器可以决定Async File源过滤器读取它所需的数据,因为它使用了IAsyncReader接口,代替了IMemeInputPin接口。
为了能够向输出针请求数据,输入针使用下面几种方法:
IAsyncReader::Request
IAsyncReader::SyncRead
IAsyncReader::SyncReadAligned.
第一种方法是异步,用来支持多重交迭读取。其它方法都是同步的。
理论上,有些过滤器可以支持IAsyncReader,但在实际中,它为那些与分解过滤器连接的源过滤器为设计。分解器的行为与在Push模式中的源过滤器非常相似。当它暂停时,它建立一个从IAsyncReader连接中pull数据的流处理线程,并向下流过滤器push数据。其输出针使用IMemInputPin接口,而且表的其它部分是标准的Push模式。
DirectShow中的事件通知
这一部分将描述在Microsoft® DirectShow®过滤器表中,事件是如何实现的;一个应用程序如何才能接受到事件通知并且响应它们。
事件通知概述
过滤器通过投递事件通知来向过滤器表管理器通报一个事件。事件可以是包含任何信息,如流的结束,也可以是一个错误,如还原流的失败。过滤器表管理器本身处理一些过滤器事件,其它事件则留给应用程序来进行处理。如果过滤器表管理器遇到一个不能处理的事件,它就将事件放入到一个队列中去。同样的,过滤器表管理器也会将它自己的事件通知放入队列中去,以期应用程序来进行处理。
应用程序可以从队列中接收到事件并对它们做出响应。因此DirectShow的事件通知与Microsoft® Windows®消息队列模式非常相似。应用程序可以取消过滤器表管理器本身可以处理事件的这种行为,过滤器表管理则直接地将这些事件放入队列中去,由应用程序来进行处理。这种机制允许:
过滤器表管理器与应用程序进行通信。
过滤器可以与应用程序和过滤器表管理进行通信。
由应用程序确定它自己都处理哪些事件。
接收事件
过滤器表管理器向外部暴露三个接口,来支持事件通知.
IMediaEventSink包含过滤器投递事件的方法
IMediaEvent包含应用程序接收事件的方法
IMediaEventEx inherits from and extends the IMediaEvent interface.
IMediaEventEx继承于并扩展了IMdeiaEvent接口
过滤器通过调用过滤器表管理器的IMediaEventSink::Notify方法来投递事件通知。一个事件通知由一个事件代码和两个DWORD型参数组成。根据不同的事件代码,参数值可能会是指针、返回码、参考时间或者其它信息。
为能从队列中接收事件,应用程序应调用IMediaEvent::GetEvent方法来接收数据。这个方法会被阻断直到从一个队列中得到一个事件或者超过时限。当调用完GetEvent后,应用程序应该总是调用IMediaEvent::FreeEventParams方法来释放在事件参数中的资源。例如参数有可能是由过滤器表提供的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);
}
为了停止某个事件的过滤器表管理器的默认处理,可以调用IMediaEvent::CancelDefaultHandling方法。你可以通过调用IMediaEvent::RestoreDefaultHandling方法来恢复事件的默认处理。对于那些没有过滤器表管理器默认处理的事件,调用这些方法将不会产生任何影响。
事件的什么时候发生
为了DirectShow事件,应用程序需要一种在队列中有事件在等待时,能够被得知的方法。过滤表管理器提供两种途径来实现:
窗口通知:过滤器表管理器在产生一个新事件时,向应用程序窗口发送一个自定义的消息。
事件信号:如果在队列有DirectShow事件时,则过滤器表管理器发送一个窗口事件。并在队列为空时重置事件。
An application can use either technique. Window notification is usually simpler.
应用程序可以使用其它手法来实现。但窗口消息通常会更为简单。
窗口通知
可以调用IMediaEventEx::SetNotifyWindow方法来指定一个私有消息,来建立起窗口通知。这个私有消息可以使用从WM_APP到WM_APP+0xBfff之间的数。只要当过滤器表管理向队列里放入新的事件,它就会向指定的应用程序窗口发送信息。应用程序对来自Windows消息循环的消息作出响应。
下面代码说明了如何设置通知窗口。
#define WM_GRAPHNOTIFY WM_APP + 1 // Private message.
pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
下面代码说明了如何对窗口消息做出响应
LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)
{
switch (msg)
{
case WM_GRAPHNOTIFY:
HandleEvent(); //应用程序定义的函数.
break;
//也可以在这里编写消息处理。
}
return (DefWindowProc(hwnd, msg, wParam, lParam));
}
因为事件通知和消息循环都是异步的,所以在你的应用程序对消息作出响应时,队列中有可能包含多个事件消息。也可能有时候事件会变得无效,那么它就会被清除出队列。因此,在你的事件处理代码中,应不停地的调用GetEvent方法,直至返回失败码,即消息队列已为空。
注意,在你释放IMediaEventEx指针时,先要通过向SetNotifyWindow传入一个空指针来取消事件通知。在你的事件处理代码中,一个定要在调用GetEvent方法前检查IMeiaEventEx指针的有效性。这样可以预防在释放了IMediaEventEx指针后,应用程序再次执行事件处理代码所可能出现的错误。
事件的发出(signal)
过滤器表管理器维护一个手动重置(manual-reset)的事件,用来反应事件队列的状态。如果事件队列包含有未解决的事件通知,那么过滤器表管理会发出一个手动重置事件。如果事件为空,调用IMediaEvent::GetEvent方法会重置事件。应用程序可以使用此事件来确定队列的状态。
注意:这个语术可能会被误解。手动重置事件是一个通过Windows CreateEvent函数建立的事件类型,它在DirectShow中并不做任何事情。
调用IMediaEvent::GetEventHandle方法可以得到手动重置事件的句柄。可以通过WaitForMultipleObject函数来等待这个事件的发生。一旦此事件发生,调用IMediaEvent::GetEvent方法就可以得到DirectShow事件。
下面的代码示例了这样的功能。它取得事件的句柄,每100毫秒的间隔等待事件的发生。当事件发生后,通过调用GetEvent方法来取得事件,并将事件代码和事件参数显示出来。当EC_COMPLETE事件(其表示回放已经完成)出现则循环结束。
HANDLE hEvent;
long evCode, param1, param2;
BOOLEAN bDone = FALSE;
HRESULT hr = S_OK;
hr = pEvent->GetEventHandle((OAEVENT*)&hEv);
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);
hr = pEvent->FreeEventParams(evCode, param1, param2);
bDone = (EC_COMPLETE == evCode);
}
}
}
因为过滤器表会适当地自动设置或重置事件,你的应用程序可以免于对这些事情的处理。同样的,当你释放了过滤器表后,过滤器表就会释放掉事件句柄。因此在你释放过滤器表后,就不要再去使用事件句柄。
阅读(1077) | 评论(0) | 转发(0) |