建立过滤器表
标准表的建立
为了建立过滤器表, 你必须首先建立一个过滤器表管理器的实例,取得一个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类需要调用它的析构函数来释放内存。)
(待续...)