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