学习DirectShow有一段时间了,把这段学习过程中翻译出来的SDK与大家分享,同时也希望专家们指出我理解上的错误,万分感谢。
1. DirectShow介绍
DirectShow是一个windows平台上的流媒体框架,提供了高质量的多媒体流采集和回放功能。它支持多种多样的媒体文件格式,包括ASF、 MPEG、AVI、MP3和WAV文件,同时支持使用WDM驱动或早期的VFW驱动来进行多媒体流的采集。DirectShow整合了其它的 DirectX技术,能自动地侦测并使用可利用的音视频硬件加速,也能支持没有硬件加速的系统。
DirectShow大大简化了媒体回放、格式转换和采集工作。但与此同时,它也为用户自定义的解决方案提供了底层流控制框架,从而使用户可以自行创建支持新的文件格式或其它用途的DirectShow组件。
以下是几个使用DirectShow编写的典型应用:DVD播放器、视频编辑应用、AVI到ASF转换器、MP3播放器和数字视频采集应用。
DirectShow是建立在组件对象模型(COM)上的,因此当你编写DirectShow应用时,你必须具备COM客户端程序编写的知识。对于大部分 的应用,你不需要实现自己的COM对象,DirectShow提供了大部分你需要的DirectShow组件,但是假如你需要编写自己的 DirectShow组件,你还需要具备编写COM组件的知识。
1.1. DirectShow支持的格式
DirectShow是一个开放的框架,因此只要有合适的filter来分析和解码,它可以支持任何格式。DirectShow默认支持以下的文件类型和压缩格式:
注:打*号的需要Windows Media Format SDK支持
文件类型:
Windows Media? Audio (WMA)*
Windows Media? Video (WMV)*
Advanced Systems Format (ASF)*
Motion Picture Experts Group (MPEG)
Audio-Video Interleaved (AVI)
QuickTime (version 2 and lower)
WAV
AIFF
AU
SND
MIDI
压缩格式:
Windows Media Video*
ISO MPEG-4 video version 1.0*
Microsoft MPEG-4 version 3*
Sipro Labs ACELP*
Windows Media Audio*
MPEG Audio Layer-3 (MP3) (decompression only)
Digital Video (DV)
MPEG-1 (decompression only)
MJPEG
Cinepak
微软自己没有提供MPEG2解码器,一些可用的DirectShow MPEG2硬件或软件解码器是由第三方提供的。
1.2. 常见问题集(摘录)
1.2.1. 一般问题
*DirectShow支持哪些操作系统?
DirectShow支持Windows9X、Windows2000、Windows Me和Windows XP。
*使用DirectShow需要多少COM知识?
应用程序开发者只需要基本的COM组件知识:实例化COM组件、调用接口、管理接口的引用计数。Filter开发者则需要更多。
*有与DirectShow兼容的硬件列表(HCL)吗?
没有。如果硬件兼容DirectShow,DirectShow会使用它们,如果没有兼容的硬件,DirectShow使用GDI绘制视频,以及使用WaveOut系列多媒体API来播放音频。
*可以使用哪些语言来编写DirectShow应用?
DirectShow主要为C/C++开发设计。Visual Basic只能使用其中的很小一部分。可以通过MS JScript或VB Script来支持基于脚本的DVD和TV应用。也可能用Delphi来编写,但SDK文档不提供这方面的内容。
*DirectShow会通过托管代码实现吗?
目前还没有这个计划。DirectX SDK提供了有限的使用音视频回放类的托管回放功能,你可以使用COM interop创建托管代码的DirectShow客户端应用,但是因为性能上的原因,不推荐创建运行在CLR上的filter。
*DirectShow开发需要什么样的编译器?
任何能够产生COM对象的编译器都可以。
*DirectShow和DirectX的其它组件的关系
DirectShow和DirectX的其它组件在内部进行联系。DirectShow在硬件的支持下使用DirectSound和 DirectDraw。Video Renderer和Overlay Mixer使用DirectDraw 3和DirectDraw5表面(surfaces)。Video Mixing Renderer 7(只支持WINXP)使用DirectDraw7表面。Video Mixing Renderer 9使用最新的(目前是Directx9)Direct3D API函数。即便是某个应用程序包含了DirectX其它组件,你也不必使用其它组件的API去编写它。参考SDK的例子:Texture3D Sample。
*DirectShow与ActiveMovie的关系?
ActiveMovie是DirectShow原来的名称,现已不再使用,但是一部分API仍保留了"AM"的前缀,比如AM_MEDIA_TYPE和IAMVideoAccelerator。
*DirectShow是限于多媒体应用吗?
DirectShow默认包含的组件主要是为音视频流设计的,但是,DirectShow框架已经成功地用于其它数据流的解决方案中。
*GraphEdit工具有源码吗?GraphEdit.exe是否可再发布?
没有源码,不可再发布。
*DMO可以代替DirectShow filter吗?
在编写编码器、解码器、效果器应用时,鼓励用DMO代替DirectShow filter。在其它的应用中,使用DirectShow filter可能会比较合适。
1.2.2. 程序编写问题
*如何设置编译环境,需要哪些头文件和库?
参考"设置编译环境"章节
*GraphEdit列示了很多没有文档支持的filter,它们都是些什么?
GraphEdit枚举了所有作为filter类型注册在系统中的filter,包括由第三方应用程序安装的filter,以及其它微软技术如 Windows Media或NetMeeting安装的,另外,一些DirectShow filter被用来做硬编码或硬解码驱动的外壳。Microsoft H.263 Video Codec用于NetMeeting,不再被DirectShow支持。
*如何知道DirectShow已经被安装?
调用CoCreateInstance创建一个Filter Graph Manager实例,如果成功,表示DirectShow已经被安装,下面是一个例子:
IGraphBuilder *pGraph; HRESULT hr = CoCreateInstance(CLSID_FilterGraph, |
*如果不通过属性设置页来更改filter的设置?
当然是通过filter提供的接口罗。如果没有提供,就没有办法啦
*DirectShow能通知应用程序当前回放位置吗?
不提供回调来通知位置,需要使用一个计时器定时调用IMediaSeeking::GetCurrentPosition方法来得到当前回放位置。
*filter运行在哪个特权级别下?
运行在Ring 3特权级别下,某些流控制驱动(如音视频采集驱动)运行在Ring 0特权级别下。
*需要一个Kernel调试器吗?
这依据具体的项目。安装DirectX调试运行时库(DirectX debug runtime library)意味着安装调试驱动(Debug driver)和其它核心组件(kernel mode component),因此如果你的应用程序在其中的某个组件中产生了一个调试断言(debug assert),你的机器就会自动重启除非你拥有一个kernal调试器。
*DEFINE_GUID宏是怎么工作的?
使用DEFINE_GUID宏可以让你通过包含同一个头文件来定义GUID值而不必使用extern关键词。比如,你的工程中有三个源文 件:src1.cpp,src2.cpp,src3.cpp,它们都使用一个相同的GUID值,而为了保证一致性,这个GUID只能在你的工程中定义一 次,这时,其它的源文件必须定义外部引用来使用它。用了DEFINE_GUID,你可以使用在所有源文件中包含同一个头文件,在头文件中这样定义 GUID:
DEFINE_GUID(CLSID_MyObject, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); |
这个例子中GUID为0,实际编程中请用Guidgen工具来产生一个GUID,在其中的一个源文件中,在你的头文件前包含initguid.h,如:
// Src1.cpp #include #include "MyGuids.h" // Src2.cpp #include "MyGuids.h"
// Src3.cpp #include "MyGuids.h" |
2. 开始DirectShow旅程
这个章节的内容主要是编写DirectShow应用所需的一些基本概念,可以把它当作一个高级介绍,理解这些内容只需具备一般的编程和有关多媒体的知识。
2.1. 设置DirectShow开发的编译环境
这节内容描述了如何来编译DirectShow应用。你可以使用命令行形式来编译一个工程,也可以在Microsoft Visual Studio集成环境下(包含VC++)实现。
头文件:
所有的DirectShow应用都需要Dshow.h这个头文件,某些DirectShow接口需要附加的头文件,参考接口的说明视具体情况定。
库文件:
DirectShow使用以下库文件:
Strmiids.lib 输出类标识(CLSID)和接口标识(IID),所有DirectShow应用均需此库。
Quartz.lib 输出AMGetErrorText函数,如果不调用此函数,此库不是必需的。
有了以上这些头文件和库文件,你已经可以编写DirectShow应用了,但是微软建议使用DirectShow基类库来编写filter,这样可以大大 减少程序编写的工作量。要使用DirectShow基类库,需要先编译它,基类库位于SDK的Samples\Multimedia \DirectShow\BaseClasses文件夹下,包含两个版本的库:发布版(retail version)Strmbase.lib和调试版(debug version)Strmbasd.lib。具体参见"创建DirectShow Filter"一节。
2.2. DirectShow应用程序编程简介
这节介绍DirectShow用到的一些基本术语和概念,看完这节后,你将能够编写你的第一个DirectShow应用程序。
Filter和Filter Graph
一个DirectShow应用程序是由一个个称为filter的软件构件组合而成的,filter执行一些多媒体流的操作,如:读文件、从视频采集设备中获得视频、将不同的格式的流解码如MPEG1、将数据送到图形卡或声卡中去。
Filter接收输入并产生输出。举个例子,一个解码MPEG1视频流的filter,输入MPEG1格式的视频流,输出一系列未压缩的视频帧。
在DirectShow中,应用程序要实现功能就必须将这些filter链接在一起,因而一个filter的输出就变成了另一个filter的输入。这一系列串在一起的filter称为filter graph。例如,下图就显示了一个播放avi文件的filter graph:
File Source(Async) filter从硬盘中读取avi文件;AVI Splitter filter分析文件并将其分解成两个流:一个压缩的视频流和一个音频流;AVI Decompressor filter将视频帧解码,Video Renderer filter将解码后的视频帧通过DirectDraw或GDI显示出来;Default DirectSound Device filter使用DirectSound播放音频流。
应用程序没有必要对这些数据流进行管理,而是通过一个叫Filter Graph Manager这个上层组件来控制这些filter。应用程序调用上层API如"Run"(通过graph移动数据)或"Stop"(停止移动数据)。如 果你需要对数据流作更多的操作,你可以通过COM接口直接进入filter。Filter Graph Manager同样也输出事件通知给应用程序。
Filter Graph的另一个用途是将filter连在一起创建一个filter graph。
编写一个DirectShow应用程序大体需要三个步骤:
1.创建一个Filter Graph Manager的实例
2.使用Filter Graph Manager创建一个filter graph,此时,需要已经具备所有必需的filter。
3.使用Filter Graph Manager控制filter graph和通过这些filter的流,在这个过程中,应用程序会收到Filter Graph Manager发送的事件。
完成这些后,应用程序需发布这个Filter Graph Manager和所有的filter。
2.3. 播放一个文件
这一章以本节这个有趣的例子来结束,这个例子是一个播放音频或视频文件的简单控制台程序。程序只有寥寥数行,但却展示了DirectShow编程的强大能力。
正如上一节所讲的创建DirectShow应用程序的三个步骤,第一步,首先,需要调用CoInitialize来作初始化,然后调用CoCreateInstance创建Filter Graph Manager:
HRESULT hr = CoInitialize(NULL); |
如上所示,类标识符(CLSID)是CLSID_FilterGraph。Filter Graph Manager由进程内DLL(in-process DLL)提供,因此参数3,dwClsContext的值为CLSCTX_INPROC_SERVER。由于DirectShow运行自由线程模式 (free-threading model),所以你同样可以使用COINIT_MULTITHREADED参数来调用CoInitializeEx。
第二步是创建filter graph,调用CoCreateInstance得到的IGraphBuilder接口包含了大部分创建filter graph的方法。在这个例子中还需要另外两个接口:IMediaControl和IMediaEvent。
IMediaControl控制数据流,它包含开启和停止graph的方法;IMediaEvent包含从Filter Graph Manager获取事件的方法,在这个例子中,这个接口用来得到回放结束事件。
所有这些接口由Filter Graph Manager提供,使用得到的IGraphBuiler接口指针来查询得到。
IMediaControl *pControl; IMediaEvent *pEvent; hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl); hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent); |
现在你可以创建filter graph了,对于文件回放只需要一个简单的调用:
hr = pGraph->RenderFile(L"C:\\Example.avi", NULL); |
IGraphBuilder::RenderFile方法创建了一个能够播放指定文件的filter graph,事实上,原本需要做的一些如创建filter实例及将这些filter连接起来的工作,都由这个方法自动完成了,如果是视频文件,这个 filter graph看起来应该是这个样子:
[file source]->[如果是缩格式,这里是个解码器]->[Video Renderer]
要开始回放,调用IMediaControl::Run方法:
hr = pControl->Run(); |
当filter graph运行时,数据经过各个filter最后回放为视频或音频。回放发生在一个单独的线程中。你可以通过调用IMediaEvent::WaitForCompletion方法来等待回放的结束:
long evCode = 0; pEvent->WaitForCompletion(INFINITE, &evCode); |
这个方法在播放期间被阻塞,直至播放结束或超时。
当应用程序结束时,需要释放接口指针并关闭COM库:
pControl->Release(); pEvent->Release(); pGraph->Release(); CoUninitialize(); |
#include void main(void) { IGraphBuilder *pGraph = NULL; IMediaControl *pControl = NULL; IMediaEvent *pEvent = NULL; // Initialize the COM library. // Create the filter graph manager and query for interfaces. hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl); // Build the graph. IMPORTANT: Change this string to a file on your system. // Note: Do not use INFINITE in a real application, because it |
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; // 检查主类型,我们需要的是视频 // 检查子类型,我们需要的是24-bit RGB. // 检查format type和格式块的大小. return VFW_E_INVALIDMEDIATYPE; |
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); |
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::GetPointer,IMediaSample::GetSize和IMediaSample::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::Run,IMediaControl::Pause或IMediaControl::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; |
要重置filter graph manager默认的事件处理过程,调用IMediaEvent::CancelDefaultHandling方法,用事件号做
http://blog.sina.com.cn/s/blog_5e63d75d0102vqwi.html