2.5 How Hardware Devices Participate in the Filter Graph
本节描述DirectShow是如何与音频、视频设备进行交互。
2.5.1 Wrapper Filters
所有的DirectShow Filters都是用户组件模式的软件组件。为了把内核模式的硬件设备,比如视频捕捉卡等加入倒Filter Graph,设备必须被表示为用户模式Filter。这项功能是通过DirectShow提供的包装Filters来实现的。这些Filters包括Audio Capture Filters, VFW Capture Filters, TV Tuner Filter, TV Audio Filter和Analog Video Crossbar Filter. DirectShow也提供了称为KsProxy的Filter用来描述任何类型的WDM流设备。硬件供应商可以提供聚集了KsProxy的COM对象KsProxy Plug-in, 以此扩展KsProxy来支持自定义功能,
这些包装Filters暴露了描述设备功能的接口。应用程序使用这些接口与Filter交互数据。Filter把COM函数调用翻译为设备驱动调用,把信息传递到内核的驱动,然后再把结果转化给应用程序。TV Tuner, TV Audio, Analog Video Crossbar和KsProxy通过IKsPropertySet接口支持自定义驱动属性。VFW Capture Filter和Audio Capture Filter并没有这种扩展。
对应用程序开发人员,包装Filter允许应用程序控制设备像控制其他DirectShow Filter一样。我们不需要特殊的编程,这些与内核模式的通信细节都在Filter中封装起来。
2.5.2 Video for Windows Devices
VFW Capture Filter支持早期的VFW捕捉卡。当目标系统上存在VFW卡时,它可以被发现,然后通过DirectShow的System Device Enumerator添加到Filter Graph。细节可参考Enumerating Devices and Filters.
2.5.3 Audio Capture and Mixing Devices (Sound Cards)
新一代的声卡都有麦克风和其他类型设备的输入娱乐工具。通常这些声卡还有控制每个输入音量、立体音和重音的实时混合功能。在DirectShow中,声卡的输入和混合器都被Audio Capture Filter包装。每个声卡都能被System Device Enumerator发现。查看系统的声卡,运行GraphEdit并选择Audio Capture Source种类。属于此类的每个Filter都是Audio Capture Filter的一个单独实例。(参考Using GraphEdit).
2.5.4 WDM Streaming Devices
新一代硬件解码器和捕捉卡都与WDM规范一致。这些设备的功能比VFW设备强,可以在Windows NT/2K和Windows 98/ME移植。WDM视频捕捉卡可以支持VFW不能支持的特征,包括枚举捕捉格式,编程控制视频参数,比如色调和亮度,编程选择输入端和TV Tuner支持。
为了支持WDM流设备,DirectShow提供了KsProxy Filter(ksproxy.ax)。KsProxy也称为“Swiss Army Knife Filter”因为它的实现的功能很多。此Filter的PINS数量、暴露的COM接口数量,都取决于内在驱动的性能。KsProxy在Filter Graph中不以名称KsProxy出现。而总是采用设备的Friendly Name, 可以通过注册表读取。查看系统的WDM设备,运行GraphEdit选择WDM Streaming种类。即使系统只有一个WDM卡,它也可能包括多个设备。每个设备是以不同的Filter表示。每个Filter实际上都是KsProxy.
应用程序使用System Device Enumerator查找系统的WDM设备Monikers。KsProxy被函数BindToObject实例化。因为KsProxy可以表示所有的WDM设备。它必须查询驱动来决定驱动支持那种属性集。属性集是WDM驱动使用的数据结构集合,也被一些用户模式的Filter使用。KsProxy把COM调用转译为属性集并发送给驱动。硬件供应商可以提供Plug-in扩展KsProxy。Plug-in是硬件供应商指定暴露、实现特殊设备功能的接口。所有这些都从应用程序隐藏。应用程序通过KsProxy控制设备像控制其他DirectShow Filter一样。
2.5.5 Kernel Streaming
WDM设备支持内核流,这当中所有的数据都是在内核模式流动。永远也不需要转换到用户模式。在内核模式和用户模式切换的计算代价很高。内核模式允许高位率的流而不加重CPU负担。基于WDM的Filter可使用内核流把多媒体数据从一个设备传递到另一个设备,可以是相同卡也可是不同的设备卡,不需要把数据拷贝到系统内存。
从应用程序角度看,数据好像是从一个用户模式Filter移动到下一个。而实际上,数据可能根本没有进入用户模式,而是从一个内核设备直接流向另一个设备,直到被提交到图形卡。对于某些情况,比如捕捉到文件,某些时候要求数据从内核模式传递到用户模式。但是这样的交换并不必要求数据被拷贝到内存的新位置。应用程序开发人员一般不需要关心内核流的细节,除了作为背景信息。参考Microsoft DDK的关于WDM, 内核流和KsProxy的相关主题。
3. Building the Filter Graph
The Filter Graph and Its Component一节描述了建立DirectShow Filter Graph的基本组件。本节检查这些组件是如何创建、连接在一起开始处理数据的。
3.1 Graph-Building Components
DirectShow提供了组件用来建立Filter Graph。包括:
·Filter Graph Manager. 它控制Filter Graph. 支持IGraphBuiler, IMediaControl, IMediaEventEx及其他接口。所有的DirectShow应用程序在某些时候都使用此对象,尽管在某些情况下Filter Graph Manager是由其他对象创建的。
·Capture Graph Builder. 提供建立Filter Graphs的额外方法。它最开始是设计来创建执行视频捕捉的Graph(从名称看也是),但是对一些其他类型的自定义Filter Graph也有用。它支持接口ICaptureGraphBuilder2.
·Filter Mapper和System Device Enumerator. 它们定位注册在用户系统上的Filters. 或者表示硬件设备。
·DVD Graph Builder. 建立DVD回放和导航的Filter Graph. 支持IDvdGraphBuilder接口。基于脚本的应用程序可使用MSWebDVD ActiveX控制进行DVD回放。
·视频控制。此ActiveX控制在Windows XP上可用。它在DirectShow中处理数字和模拟电视。更多信息可参考Using the Video Control.
3.1.1 Intelligent Connect
术语“Intelligent Connect”覆盖了Filter Graph Manager用来建立全部、部分Filter Graph的一个算法集。在Filter Graph Manager需要其他Filters来完成Graph的任何时候,粗略按照如下步骤进行:
(1)、如果Filter在Graph中,并且至少有一个未连接的PIN,Filter Graph Manager就尝试使用这个Filter。
(2)、否则Filter Graph Manager在注册表查询能接收媒体类型连接的Filter。每个Filter在注册表有一个Merit值。它能粗略说明在完成Graph中Filter可能有多大的用处。Filter Graph Manager根据Merit值的顺序进行尝试。每种流类型(比如音频、视频或者MIDI),默认的Renderer都有一个较高的Merit值。解码器的Merit值也比较高。特殊目的Filter的值比较低。
如果Filter Graph Manager遇到困难,就返回尝试不同Filters的组合。我们可从Intelligent Gonnect主题找到具体细节。
3.2 Overview of Graph Building
创建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直接在两个PINS上尝试连接。若PIN不能连接,函数失败。
·IFilterGraph::Connect连接两个PINS,如果可能先进行直接连接。否则使用中介Filter完成连接。
·IGraphBuilder::Render从某输出PIN开,建立Graph剩余部分。此函数在下一级添加必要的Filter直到到达Renderer Filter.
·IFilterGraph::AddFilter把Filter添加到Graph. 并不连接Filter.必须在调用函数前创建Filter。
这些方法提供三种基本的方法建立Graph:
(1)、Filter Graph Manager建立整个Graph
(2)、Filter Graph Manager建立部分Graph
(3)、应用程序建立整个Graph
3.2.1 The Filter Graph Manager builds the entire graph
如果我们想简单播放的已经格式创建的文件,比如AVI, MPEG, WAV或MP3, 使用RenderFile函数。How To Play a File一节说明了具体如何操作。
RendererFile开始在注册表搜索能解析文件的Source Filter。它使用协议(比如文件名称中的http://), 文件扩展名,或者文件中的预定义字节模式来决定使用哪个Source Filter。详细信息可参考Registering a Custom File Type.
在建立Graph剩余部分时,Filter Graph Manager使用一种交互过程。交互时用Filter输出PIN支持的媒体类型,然后在注册表查找可以用此类型做输入的Filter。它使用几个标准来加快搜索和决定Filters的使用次序:
·Filter Category标示了Filter的基本功能
·媒体类型描述了Filter可接受的输入或者发送的输出格式
·Merit值决定了Filter被尝试使用的顺序。如果两个Filter属于相同的Category,支持相同的输入类型,Filter Graph Manager就选择Merit值高的一个。某些Filter的Merit设置得比较低,因为它们通常用于特殊用途,应该由应用程序把它们加入到Graph。
Filter Graph Manager使用Filter Mapper来查找注册表。
只要添加了每个Filter后,Filter Graph Manager开始把它与前一个Filter的输出PIN连接。Filter进行协商来决定它们是否连接。如果是,再决定连接采用的媒体类型。如果新Filter不能连接。Filter Graph Manager丢弃它并尝试其他Filter。这个过程会一直持续下去直到每个流都被提交。
3.2.2 The Filter Graph Manager builds part of the graph
当我们不止想简单播放文件时,应用程序必须至少执行一些Graph的建立工作。比如视频捕捉应用程序必须选择捕捉Source Filter并加入Graph。如果把数据写入到AVI文件,必须添加AVI Mux和File Writer Filter到Graph中。但是,让Filter Graph Manager来完成Graph也是可能的。比如我们可以调用Render函数提交PIN进行预览。
3.2.3 The Application builds the entire graph
在某些时候,我们的应用程序需要通过添加或者连接每个Filter来建立Graph。这时,我们应该知道哪个Filter应该被加入到Graph。使用这种方法,应用程序调用AddFilter来添加Filter。枚举Filter的PIN,调用Connect或ConnectDirect来连接它们。
3.3 Intelligent Connect
智能连接是Filter Graph Manager建立Graph的机制。它包含几个选择Filter、把Filter添加到Filter Graph的算法。对于应用程序编程,我们很少需要知道智能连接的细节。如果你建立某些Filter有困难,并且想解决问题时,或者是编写自己的Filter并且允许支持自动Graph建立,可阅读此节。
智能连接主要涉及到如下几个IGraphBuilder的函数:
·IGraphBuilder::Render
·IGraphBuilder::AddSourceFiler
·IGraphBuilder::RenderFile
·IGraphBuilder::Connect
Render函数建立部分Graph.它从一个未连接的输出PIN开始,并向下工作,必要时添加Filter.开始的Filter必须已经在Graph中。每一步,Render函数都查找可以连接上一级Filter的Filter。数据流可以分支,如果连接Filter有多个输出PIN。直到每个流都有提交查找才停止。如果Render遇到困难,它可能返回用不同的Filter进行再次的尝试。
为了连接每个输出PIN,Render函数执行如下工作:
(1)、如果PIN支持ISteamBuilder接口,Filter Graph Manager就委派给IStreamBuilder的Render函数。通过暴露此接口,PIN就被假定承担建立Graph剩余部分,一直到Renderer. 但是很少有PIN支持这个接口。
(2)、Filter Graph Manager尝试使用缓存在内存的Filter。如果有,贯穿整个智能连接过程,Filter Graph Manager可能缓存这个Filter开始处理的每一步的Filter.(参考Dynamic Graph Building)。
(3)、如果Filter Graph中某个Filter有未连接输出PIN,Filter Graph Manager下一步就尝试它们。我们可以在调用Render之前,把某个特殊Filter添加到Graph,以此来强制Render函数尝试使用它。
(4)、最后,Filter Graph Manager调用IFilterMapper2::EnumMatchingFitlers查找注册表。它尝试用输出PIN的首先媒体类型来匹配注册表中列举的媒体类型。
每个Filter都注册了一个Merit数值来区别与其他Filter的优先级。EnumMatchingFitlers函数返回以Merit排序的Filters, 最小的Merit是MERIT_DO_NOT_USE+1. 它会忽略比这个最小值还小的Filter. Filter还会以GUID值的种类排序。种类本身也有Merit。EnumMatchingFitlers也忽略Merit比最小值小的种类。即使种类中存在Merit比较高的Filter。
总结起来,Render函数按下面的顺序尝试Filter:
·使用IStramBuilder
·尝试缓存Filter
·尝试Graph中的Filter
·查询注册表的Filter
AddSourceFilter函数添加一个可以提交指定文件的Source Filter。首先查询注册表匹配协议(比如http://),文件扩展名,预定义的检测字节集(通常是匹配一定模式的在文件中特殊偏移位置的字节)。细节可参考Registering a Custom File Type. 如果函数找到合适的Source Filter, 就创建Filter的实例并添加到Graph. 然后用文件名调用Filter的IFileSourceFilter::Load函数。
RenderFile函数从文件名建立一个默认的回放Graph。内部它使用AddSourceFilter来查找正确的Source Filter,然后用Render建立Graph的余下部分。
Connect函数连接输入、输出PIN。此函数会根据Render中描述的各种算法添加中介Filter,如果需要:
(1)、尝试不用中介Filter直接连接两个Filter
(2)、尝试缓存Filter
(3)、尝试Graph中的Filter
(4)。查询注册表的Filter.
4. Data Flow in the Filter Graph
本节描述媒体数据如何在Filter Graph中移动。通常,编写DirectShow应用程序不需要知道这些细节,尽管有时候会感觉到有帮助。如果是编写DirectShow Filter,就需要理解这些知识。
4.1 Transports
为了在Filter Graph中传送媒体数据,DirectShow Filter需要支持一些协议,称之为传输协议(transport)。相连的Filter必须支持同样的传输协议,否则不能交换媒体数据。通常,一个传输协议要求PIN支持一个特殊的接口。当Filter连接时,一个PIN就向另一个查询此接口。
大多数的DirectShow Filter把媒体数据保存在主存储器中,并通过Pin连接把数据递送给其它的Filter,这种传输称为本地内存传输。虽然本地内存传输在DirectShow中最常用,但并不是所有的Filter都使用它。例如,有些Filter通过硬件传送媒体数据,Pin只是用来提交控制信息,如IOverlay接口。
DirectShow为本地内存器传输定义了两种机制:推模式和拉模式。在推模式中,Source Filter生成数据并递送给下一级Filter。下一级Filter被动接收数据,完成处理后再传送给再下一级Filter。在拉模式中,Source Filter与一个Parse Filter相连。Parse Filter向Source Filter请求数据后,Source Filter才递送数据以响应请求。推模式用IMemInputPin接口,拉模式用IAsyncReader接口。
推模式比拉模式要更常用。因为后续的文章都假定使用推模式。本节的最后一部分,Pull Mode说明了IAsyncReader接口与IMemInputPin接口的区别。
4.2 Samples and Allocators
当一个Pin向另一个Pin传递数据的时候,它并不是直接将内存块的指针传递下一个Pin,实际上传递的是一个管理内存的COM对象指针。这个COM对象称Media Sample。暴露了IMediaSample接口。接收Pin通过调用IMediaSample的方法来对内存进行操作,比如GetSize、GetActualDataLength以及GetPointer方法。
Sample总是从上到下递送的,从输出Pin到输入Pin,输出Pin通过调用输入Pin上IMemInputPin的Receive方法传递Sample,输入Pin可以在Receive函数同步处理数据,或者另外采用一个工作线程进行异步处理。输入PIN也允许阻塞在Receive方法中,如果需要等待资源。
另外一个COM对象,叫做Allocator,用来创建和管理Sample的。暴露了IMemAllocator接口。当一个Filter需要一个空的Buffer的时候,就可以调用IMemAllocator::GetBuffer,该方法返回一个指向Sample的指针。每一个Pin连接都共享一个Allocator,当两个Pin连接的时候,他们会决定由哪个Filter来提供Allocator,通过Pin还可以设置Allocator的属性,例如,Buffer的数量和大小。具体细节可参考How Filters Connect, Negotiating Allocators.
下面的图表显示了Allocator,Sample和Filter之间的关系。
4.2.1 Media Sample Reference Counts
Allocator创建了一个有限的Sample池。在任何时候,某些Sample可能在使用,而其他的可以响应GetBuffer调用。Allocator用引用计数保持跟踪Samples。GetBuffer返回的Sample的引用计数是1。当Sample的引用计数为0时,Sample就返回Allocator池,成为空闲Sample,可以再次响应GetBuffer的调用。如果所有的Sample都处于繁忙状态,GetBuffer就会阻塞,直到有一个Sample空闲。
例如,假设一个输入Pin接到一个Sample,如果它在Receive方法里同步的处理这个Sample,没有增加该Sample的引用计数,在Receive返回后,输出Pin释放这个Sample,引用计数为0,Sample就返回到Allocator池。另一方面,如果输入Pin在工作线程中处理该Sample,引用计数增加1,成为2,输出Pin返回,释放Sample,计数成1。在工作线程结束处理后,调用Release释放Sample. 现在Sample返回到池中。
当一个Pin接收到一个Sample时,它可以将数据复制到另一个Sample中,也可以将这个Sample传递到下一个Filter。一个Sample可以流遍整个Filter graph。每个Filter依次调用AddRef和Release(译注:相当于使引用计数要保持大于0)。因此,输出Pin调用Receive后就决不能再使用这个Sample。因为也许下游还有Filter正在使用该Sample。输出Pin必须调用GetBuffer获取新的Sample。
这种机制减少了内存分配的,因为Buffer可以重用。也防止了Filter在没有处理的Sample重新写入,因为Allocator维护了一个可用Sample列表。
Filter可以给输入、输出使用分别的Allocator. Filter可以在扩展输入数据时这么做(比如解压数据)。如果输出并不比输入大,Filter可以就地处理数据,而不需拷贝到新Sample。那时,两个或者更多的PIN连接就可以共享Allocator.
4.2.2 Committing and Decommitting Allocators
当Filter创建一个Allocator的时候,Allocator还没有保留任何的内存,如果这个时候有人GetBuffer,就会失败。只有当数据流开始的时候,输出Pin调用IMemAllocator::Commit,提交Allocator,现在才能分配内存。然后才可以调用GetBuffer.
当数据流停止时,Pin就调用IMemAllocator::Decommit,来销毁Allocator。在Allocator再次Commit之前,所有的GetBuffer调用都会失败。同样,即使是GetBuffer正在阻塞等待Sample,也会立即返回一个错误码。Decommit能不能释放内存取决于它的实现。比如CMemAllocator类会等到它的析构函数来释放内存。