DirectShow是微软公司提供的一套在Windows平台上进行流媒体处理的开发包,与DirectX开发包一起发布。那么,DirectShow能够做些什么呢?且看,DirectShow为多媒体流的捕捉和回放提供了强有力的支持。运用DirectShow,我们可以很方便地从支持WDM驱动模型的采集卡上捕获数据,并且进行相应的后期处理乃至存储到文件中。它广泛地支持各种媒体格式,包括Asf、Mpeg、Avi、Dv、Mp3、Wave等等,使得多媒体数据的回放变得轻而易举。另外,DirectShow还集成了DirectX其它部分(比如DirectDraw、DirectSound)的技术,直接支持DVD的播放,视频的非线性编辑,以及与数字摄像机的数据交换。更值得一提的是,DirectShow提供的是一种开放式的开发环境,我们可以根据自己的需要定制自己的组件。
应用程序与DirectShow组件以及DirectShow所支持的软硬件之间的关系如图1所示。
![]() 图1 DirectShow系统框图 |
1、DirectShow的 Filter
Directshow是基于模块化,每个功能模块都采取COM组件方式,称为Filter。Directshow提供了一系列的标准的模块可用于应用开发,开发者也可以开发自己的功能Filter来扩展Directshow的应用。下面我们用一个例子来说明如何采取Filter来播放一个AVI的视频文件。
1) 首先从一个文件中读取AVI数据,形成字节流。(这个工作由源Filter完成)
2) 检查AVI数据流的头格式,然后通过AVI分割Filter将视频流和音频流分开。
3) 解码视频流,根据压缩格式的不同,选取不同的decoder filters 。
4) 通过Renderer Filter重画视频图像。
5) 音频流送到声卡进行播放,一般采用缺省的 DirectSound DeviceFilter。流程见下图。
![]() 图2 音频流播放Graph图 |
从上面的图表看,每一个filter都一个其他的一个或者两个filter相连接。两个Filter相连接的连接点也是com对象,我们称为Pin。Filter通过pin将数据从一个filter传递到另一个filter中,从而可以使数据在由filter组成的链表中流动。图中的箭头表示filter链表中的数据流的方向。在Directshow中,像上面的这样一个filter 链表我们称为filter Graph。
Filter具有三个状态,运行,停止,暂停。当一个filter运行时,它就处理媒体数据流,当停止时,filter就不在处理数据,暂停状态常用来给运行状态之前cure data。Data Flow in the Filter Graph一章详细描述了这些概念,可以参考。
除了一些特别的例外, Filter graph中所有的filter的状态的改变都是统一的,也就说,filte graph中的所有的filter 的状态改变是一致协调的。也就是说,我们也可以用filter graph也可以有运行,停止,暂停三种状态。
Filter 一般分为下面几种类型。
(1)源过滤器(sourcefilter):源过滤器引入数据到过滤器图表中,数据来源可以是文件、网络、照相机等。不同的源过滤器处理不同类型的数据源。
(2)变换过滤器(transform filter):变换过滤器的工作是获取输入流,处理数据,并生成输出流。变换过滤器对数据的处理包括编解码、格式转换、压缩解压缩等。
(3)提交过滤器(renderer filter):提交过滤器在过滤器图表里处于最后一级,它们接收数据并把数据提交给外设。
(4)分割过滤器(splitter filter):分割过滤器把输入流分割成多个输出。例如,AVI分割过滤器把一个AVI格式的字节流分割成视频流和音频流。
(5)混合过滤器(mux filter):混合过滤器把多个输入组合成一个单独的数据流。例如,AVI混合过滤器把视频流和音频流合成一个AVI格式的字节流。
过滤器的这些分类并不是绝对的,例如一个ASF读过滤器(ASF Reader filter)既是一个源过滤器又是一个分割过滤器。
2、关于Filter Graph Manager
Filter Graph Manager也是一个com对象,用来控制Filter graph中的所有的filter,主要有以下的功能:
1) 用来协调filter之间的状态改变,从而使graph 中的所有的filter的状态的改变应该一致。
2) 建立一个参考时钟。
3) 将filter 的消息通知返回给应用程序
4) 提供用来建立 filter graph的方法。
这里只是简单的描述一下,详细地可以参考文档。
状态改变,Graph中的filter的状态改变应该一致,因此,应用程序并将状态改变的命令直接发给filter,而是将相应的状态改变的命令发送给Filter graph Manager,由manager将命令分发给graph中每一个filter。Seeking也是同样的方式工作,首先由应用程序将seek命令发送到filter graph 管理器,然后由其分发给每个filter。
参考时钟,graph中的filter都采用的同一个时钟,称为参考时钟(reference clock),参考时钟可以确保所有的数据流同步,视频桢或者音频桢应该被提交的时间称为presentation time.presentation time 是相对于参考时钟来确定的。Filter graph Manager应该选择一个参考时钟,可以选择声卡上的时钟,也可以选择系统时钟。
Graph事件, Graph 管理器采用事件机制将graph中发生的事件通知给应用程序,这个机制类似于windows的消息循环机制。
Graph构建的方法,graph管理器给应用程序提供了将filter添加进graph的方法,连接filter的方法,断开filter连接的方法。
但是,graph 管理器没有提供如何将数据从一个filter发送到另一个filter的方法,这个工作是由filter在内部通过pin来独立完成的,
3、媒体类型
因为Directshow是基于com组件的,就需要有一种方式来描述filter graph每一个点的数据格式,例如,我们还以播放AVI文件为例,数据以RIFF块的形式进入graph中,然后被分割成视频和音频流,视频流有一系列的压缩的视频桢组成,解压后,视频流由一系列的无压缩的位图组成,音频流也要走同样的步骤。 Media Types: How DirectShow Represents Formats
媒体类型是一种很普遍的,可以扩展的用来描述数字媒体格式的方法,当两个filter连接的时候,他们会就采用某一种媒体类型达成一致的协议。媒体类型定义了处于源头的filter将要给下游的filter发送什么样的数据,以及数据的physical layout。如果两个filter不能够支持同一种的媒体类型,那么他们就没法连接起来。
对于大多数的应用来说,也许你不用考虑媒体类型,但是,有些应用程序中,你会直接应用到媒体类型的。
媒体类型是通过AM_MEDIA_TYPE结构定义的,看看原始定义吧 typedef struct _MediaType {
GUID majortype;
GUID subtype;
BOOL bFixedSizeSamples;
BOOL bTemporalCompression;
ULONG lSampleSize;
GUID formattype;
IUnknown *pUnk;
ULONG cbFormat;
[size_is(cbFormat)] BYTE *pbFormat;
} AM_MEDIA_TYPE;
Major type:是一个GUID,用来定义数据的主类型,包括,音频,视频,unparsed字节流,MIDI数据,等等,具体可以参考msdn。
Subtype:子类型,也是一个GUID,用来进一步的细化数据格式,例如,在视频主类型中,还包括RGB-24, RGB-32, UYVY等等一些子类型,在音频主类型中还包括PCM audio, MPEG-1 payload等类型,子类型提供了比主类型更详细的信息,但是并没有定义所有的格式,例如,视频的子类型并没有定义图像大小,桢率。这些由下面的字段定义。
bFixedSizeSamples当这个值为TRUE时,表示sample大小固定。
bTemporalCompression当这个值为TRUE时,表示sample采用了临时压缩格式,表明不是所有的桢都是关键桢,如果为FALSE,表明所有的都是关键桢。
lSampleSize 表示sample的大小。对于压缩的数据,这个值可能为零。
Formattype一个GUID值,用来表明内存块的格式。包括如下:FORMAT_None,FORMAT_DvInfo,FORMAT_MPEGVideo,FORMAT_MPEG2Video,FORMAT_VideoInfo,FORMAT_VideoInfo2,FORMAT_WaveFormatEx,GUID_NULL。
pUnk该参数没有用到。
cbFormat内存块的大小。
pbFormat指向内存块的指针。
下面我们看一段代码,看看filter如何检测媒体类型的。HRESULT CheckMediaType(AM_MEDIA_TYPE *pmt)
{
if (pmt == NULL) return E_POINTER;
// Check the major type. We’re looking for video.
if (pmt->majortype != MEDIATYPE_Video)
{
return VFW_E_INVALIDMEDIATYPE;
}
// Check the subtype. We’re looking for 24-bit RGB.
if (pmt->subtype != MEDIASUBTYPE_RGB24)
{
return VFW_E_INVALIDMEDIATYPE;
}
// Check the format type and the size of the format block.
if ((pmt->formattype == FORMAT_VideoInfo) && (pmt->cbFormat >= sizeof(VIDEOINFOHEADER) &&
(pmt->pbFormat != NULL))
{
// Now it’s safe to coerce the format block pointer to the
// correct structure, as defined by the formattype GUID.
VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;
// Examine pVIH (not shown). If it looks OK, return S_OK.
return S_OK;
}
return VFW_E_INVALIDMEDIATYPE;
}
下面简单介绍几个和 Media Type相关的函数:
AM_MEDIA_TYPE结构包含一个指向数据块的指针,因此,当你使用这个结构的时候,一定要小心内存分配,以防内存泄漏。
分配函数
1) AM_MEDIA_TYPE * WINAPI CreateMediaType(AM_MEDIA_TYPE const *pSrc );
这个函数分配一个新的AM_MEDIA_TYPE结构,包含特定格式的数据块。释放由这个函数分配的内存,可以调用DeleteMediaType函数
2) STDAPI CreateAudioMediaType(const WAVEFORMATEX *pwfx,AM_MEDIA_TYPE *pmt,BOOL bSetFormat);
该函数利用一个给定的WAVEFORMATIEX结构来初始化媒体类型,如果bsetFormat参数为TRUE,该函数就分配一块新的内存,如果原来的pmt已经包含内存,就有可能发生内存泄漏。为了避免内存泄漏,在调用这个函数前要调用FreeMediaType(),在这个函数返回之后,再次调用FreeMediaType(),释放format block。
3) HRESULT WINAPI CopyMediaType(AM_MEDIA_TYPE *pmtTarget,const AM_MEDIA_TYPE *pmtSource);
这个函数复制了一个结构到另一个结构中去。这个函数也要重新分配内存给目的结构,如果pmtTarget,已经包含一个内存块,就要内存泄漏,因此,在调用该函数前后都要调用FreeMediaType函数。
释放函数
4) void WINAPI DeleteMediaType( AM_MEDIA_TYPE *pmt);
无论是采用CoTaskMemAlloc函数还是用CreateMediaType函数分配的内存都可以用这个函数来释放,如果你没有连接基类的动态库,你可以用下面的代码void MyDeleteMediaType(AM_MEDIA_TYPE *pmt)
{
if (pmt != NULL)
{
MyFreeMediaType(*pmt); // 见下面的 FreeMediaType 函数
CoTaskMemFree(pmt);
}
}
5) void WINAPI FreeMediaType( AM_MEDIA_TYPE& mt);
这个函数用来释放数据块的内存,如果要删除AM_MEDIA_TYPE结构,可以使用DeleteMediaType函数。void MyFreeMediaType(AM_MEDIA_TYPE& mt)
{
if (mt.cbFormat != 0)
{
CoTaskMemFree((PVOID)mt.pbFormat);
mt.cbFormat = 0;
mt.pbFormat = NULL;
}
if (mt.pUnk != NULL)
{
// Unecessary because pUnk should not be used, but safest.
mt.pUnk->Release();
mt.pUnk = NULL;
}
}
4、媒体Samples和Allocators
Filters通过pin的连接来传递数据,数据流是从一个filter的输出pin流向相连的filter的输入pin。输出pin常用的传递数据的方式是调用输入pin上的IMemInputPin::Receive方法。
对于filter来说,可以有好几种方式来分配媒体数据使用的内存块,可以在堆上分配,可以在DirectDraw的表面,也可以采用GDI共享内存,还有其他的一些方法,在Directshow中用来进行内存分配任务的是内存分配器(allocator),也是一个COM对象,暴露了一个IMemAllocator接口。
当两个pin连接的时候,必须有一个pin提供一个allocator,Directshow定义了一系列函数调用用来确定由哪个pin提供allocator,以及buffer的数量和大小。
在数据流开始之前,allocator会创建一个内存池(pool of buffer),在开始发送数据流以后,源filter就会将数据填充到内存池中一个空闲的buffer中,然后传递给下面的filter。但是,源filter并不是直接将内存buffer的指针直接传递给下游的filter,而是通过一个media samples的COM对象,这个sample是allocator创建的用来管理内存buffer。Media sample暴露了IMediaSample接口,一个sample包含了下面的内容:
一个指向没有发送的内存的指针。
一个时间戳
一些标志
媒体类型。
时间戳表明了presentation time,Renderer filter就是根据这个时间来安排render顺序的。标志是用来标示数据是否中断等等,媒体类型提供了中途改变数据格式的一种方法,不过,一般sample没有媒体类型,表明它们的媒体类型一直没有改变。
当一个filter正在使用buffer,它就会保持一个sample的引用计数,allocator通过sample的引用计数用来确定是否可以重新使用一个buffer。这样就防止了buffer的使用冲突,当所有的filter都释放了对sample的引用,sample才返回到allocator的内存池,供重新使用。
5、硬件设备在graph中的作用
下面的这段话借用的是陆其明的一段文档,特此标记2005-1-26我觉得他对硬件的表述比较清楚。
大家知道,为了提高系统的稳定性,Windows操作系统对硬件操作进行了隔离;应用程序一般不能直接访问硬件。DirectShow Filter工作在用户模式(User mode,操作系统特权级别为Ring 3),而硬件工作在内核模式(Kernel mode,操作系统特权级别为Ring 0),那么它们之间怎么协同工作呢?
DirectShow解决的方法是,为这些硬件设计包装Filter;这种Filter能够工作在用户模式下,外观、控制方法跟普通Filter一样,而包装Filter内部完成与硬件驱动程序的交互。这样的设计,使得编写DirectShow应用程序的开发人员,从为支持硬件而需做出的特殊处理中解脱出来。DirectShow已经集成的包装Filter,包括Audio Capture Filter(qcap.dll)、VfW Capture Filter(qcap.dll,Filter的Class Id为CLSID_VfwCapture)、TV Tuner Filter(KSTVTune.ax,Filter的Class Id为CLSID_CTVTunerFilter)、Analog Video Crossbar Filter(ksxbar.ax)、TV Audio Filter(Filter的Class Id为CLSID_TVAudioFilter)等;另外,DirectShow为采用WDM驱动程序的硬件设计了KsProxy Filter(Ksproxy.ax,)。我们可以看一下结构图:见图1
我们可以看出,Ksproxy.ax、Kstune.ax、Ksxbar.ax这些包装Filter跟其它普通的DirectShow Filter处于同一个级别,可以协同工作;用户模式下的Filter通过Stream Class控制硬件的驱动程序minidriver(由硬件厂商提供的实现对硬件控制功能的DLL);Stream Class和minidriver一起向上层提供系统底层级别的服务。值得注意的是,这里的Stream Class是一种驱动模型,它负责调用硬件的minidriver;另外,Stream Class的功能还在于协调minidriver之间的工作,使得一些数据可以直接在Kernel mode下从一个硬件传输到另一个硬件(或同一个硬件上的不同功能模块),提高了系统的工作效率。(更多的关于底层驱动程序的细节,请读者参阅Windows DDK。)
下面,我们分别来看一下几种常见的硬件。
VfW视频采集卡。这类硬件在市场上已经处于一种淘汰的趋势;新生产的视频采集卡一般采用WDM驱动模型。但是,DirectShow为了保持向后兼容,还是专门提供了一个包装Filter支持这种硬件。和其他硬件的包装Filter一样,这种包装Filter的创建不是像普通Filter一样使用CoCreateInstance,而要通过系统枚举,然后BindToObject。
音频采集卡(声卡)。声卡的采集功能也是通过包装Filter来实现的;而且现在的声卡大部分都有混音的功能。这个Filter一般有几个Input pin,每个pin都代表一个输入,如Line In、Microphone、CD、MIDI等。值得注意的是,这些pin代表的是声卡上的物理输入端子,在Filter Graph中是永远不会连接到其他Filter上的。声卡的输出功能,可以有两个Filter供选择:DirectSound Renderer Filter和Audio Renderer (WaveOut) Filter。注意,这两个Filter不是上述意义上的包装Filter,它们能够同硬件交互,是因为它们使用了API函数:前者使用了DirectSound API,后者使用了waveOut API。这两个Filter的区别,还在于后者输出音频的同时不支持混音。(顺便说明一下,Video Renderer Filter能够访问显卡,也是因为使用了GDI、DirectDraw或Direct3D API。)如果你的机器上有声卡的话,你可以通过GraphEdit,在Audio Capture Sources目录下看到这个声卡的包装Filter。
WDM驱动的硬件(包括视频捕捉卡、硬件解压卡等)。这类硬件都使用Ksproxy.ax这个包装Filter。Ksproxy.ax实现了很多功能,所以有“瑞士军刀”的美誉;它还被称作为“变色龙Filter”,因为该Filter上定义了统一的接口,而接口的实现因具体的硬件驱动程序而异。在Filter Graph中,Ksproxy Filter显示的名字为硬件的Friendly name(一般在驱动程序的.inf文件中定义)。我们可以通过GraphEdit,在WDM Streaming开头的目录中找到本机系统中安装的WDM硬件。因为KsProxy.ax能够代表各种WDM的音视频设备,所以这个包装Filter的工作流程有点复杂。这个Filter不会预先知道要代表哪种类型的设备,它必须首先访问驱动程序的属性集,然后动态配置Filter上应该实现的接口。
当Ksproxy Filter上的接口方法被应用程序或其他Filter调用时,它会将调用方法以及参数传递给驱动程序,由驱动程序最终完成指定功能。除此以外,WDM硬件还支持内核流(Kernel Streaming),即内核模式下的数据传输,而无需经过到用户模式的转换。因为内核模式与用户模式之间的相互转换,需要花费很大的计算量。如果使用内核流,不仅可以避免大量的计算,还避免了内核数据与主机内存之间的拷贝过程。在这种情况下,用户模式的Filter Graph中,即使pin之间是连接的,也不会有实际的数据流动。典型的情况,如带有Video Port Pin的视频捕捉卡,Preview时显示的图像就是在内核模式下直接传送到显卡的显存的。所以,你也休想在VP Pin后面截获数据流。
讲到这里,我想大家应该对DirectShow对硬件的支持问题有了一个总体的认识。对于应用程序开发人员来说,这方面的内容不用研究得太透,而只需作为背景知识了解一下就好了。其实,大量繁琐的工作DirectShow已经帮我们做好了。