最近一段时间,在编写DirectShow应用程序时常常遇到一些问题,原因是对DirectShow技术没有较全面地掌握,对各个接口间的关系以及filter与filter之间连接的内部过程等都只是一知半解,除了再仔细地看看DirectShow的基类库源文件之外,觉得也很有必要从头到尾看一遍DirectShow的MSDN文档。在看时顺便有选择地翻译出来,一来以便以后再看时可以轻松点,二来也敦促自己不能不求甚解早早看看了事。在翻译的过程中也加了一些自己的补充,因为觉得某些MSDN章节实在是过于简单还有些模棱两可。
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,
NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **) &pGraph);
*如果不通过属性设置页来更改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"
在没有包含Initguid.h的地方,DEFINE_GUID宏创建外部引用来使用GUID值,在包含Initguid.h的地方,DEFINE_GUID重定义DEFINE_GUID宏以产生GUID的定义。
如是没有在任何地方添加 Initguid.h,你会得到一个链接错误:"unresolved external symbol." ,如果同样的GUID包含Initguid.h两次,会得到编译错误"redefinition; multiple initialization."要解决这些问题,请确认Initguid.h只包含一次。同样的,不要包含Initguid.h到预编译头文件中去,因为预编译头文件会被每个源文件包含。
[此贴子已经被作者于 2005-11-26 2:49:14编辑过]
-- 作者:admin
-- 发布时间:2005-11-26 2:46:00
--
桃花坞里桃花庵,桃花庵下桃花仙。
桃花仙人种桃树,又摘桃花换酒钱。
酒醒只在花前坐,酒醉还来花下眠。
半醉半醒日复日,花落花开年复年。
但愿老死花酒间,不愿鞠躬车马前。
车尘马足显者事,酒盏花枝隐士缘。
若将显者比隐士,一在平地一在天。
若将花酒比车马,彼何碌碌我何闲。
别人笑我太疯癫,我笑他人看不穿。
不见五陵豪杰墓,无花无酒锄作田。
-- 作者:admin
-- 发布时间:2005-11-26 2:48:00
-- 开始DirectShow旅程
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);
if (FAILED(hr))
{
return;
}
IGraphBuilder *pGraph;
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL,
CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph);
如上所示,类标识符(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.
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
printf("ERROR - Could not initialize COM library");
return;
}
// Create the filter graph manager and query for interfaces.
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
if (FAILED(hr))
{
printf("ERROR - Could not create the Filter Graph Manager.");
return;
}
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);
// Build the graph. IMPORTANT: Change this string to a file on your system.
hr = pGraph->RenderFile(L"C:////Example.avi", NULL);
if (SUCCEEDED(hr))
{
// Run the graph.
hr = pControl->Run();
if (SUCCEEDED(hr))
{
// Wait for completion.
long evCode;
pEvent->WaitForCompletion(INFINITE, &evCode);
// Note: Do not use INFINITE in a real application, because it
// can block indefinitely.
}
}
pControl->Release();
pEvent->Release();
pGraph->Release();
CoUninitialize();
}
-- 作者:admin
-- 发布时间:2005-11-26 2:49:00
-- 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同样为某种文件格式提供与之对应的编解码器。
-- 作者:admin
-- 发布时间:2005-11-26 2:50:00
--
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;
// 检查主类型,我们需要的是视频
if (pmt->majortype != MEDIATYPE_Video)
{
return VFW_E_INVALIDMEDIATYPE;
}
// 检查子类型,我们需要的是24-bit RGB.
if (pmt->subtype != MEDIASUBTYPE_RGB24)
{
return VFW_E_INVALIDMEDIATYPE;
}
// 检查format type和格式块的大小.
if ((pmt->formattype == FORMAT_VideoInfo) &&
(pmt->cbFormat >= sizeof(VIDEOINFOHEADER) &&
(pmt->pbFormat != NULL))
{
// 现在可以安全地将格式块指针指向正确的结构体。
VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;
// 检查pVIH (未展示). 如果正确,返回S_OK.
return S_OK;
}
return VFW_E_INVALIDMEDIATYPE;
}
AM_MEDIA_TYPE结构体还包含了一些任选项,用来提供附加的信息,filter不需要这些信息:
*ISampleSize, 如果这个字段非零,表示这是每个sample的尺寸,如果是零,则表示sample的尺寸会改变。
*bFixdSizeSamples ,如果这个布尔类型的标记是TRUE,表示ISampleSize有效,否则,你可以忽略ISampleSize。
*bTemporalCompression ,如果这个布尔类型的标记是FALSE,表示所有帧都是关键帧。
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)在显卡上。某些情况,比如采集视频到一个文件中去,在某些点上需要将数据从内核模式传入到用户模式,但是,仍然没有必要将数据拷贝到内存的一个新位置中去。
应用程序开发者通常只需了解一个内核流的背景知识而不需要深究它的细节。
-- 作者:admin
-- 发布时间:2005-11-26 2:51:00
--
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);
Filter Graph Manager支持下列Graph构建方法:
*IFilterGraph::ConnectDirect ,在两个pin之间进行直接连接,如果连接失败,则返回失败
*IFilterGraph::Connect ,连接两个Pin,如果可能的话,直接连接它们,否则,在中间加入其它的filter来完成连接。
*IGraphBuilder::Render ,从某个输出Pin处开始完成余下的graph构建。该方法会自动在输出pin后面添加必须的filter,直到renderer filter为止。
*IGraphBuilder::RenderFile ,构建一个完整的文件回放graph。
*IGraphBuilder::AddFilter ,将一个 filter添加到graph中。它不连接filter,并且在调用此方法前,filter必须已经被创建。创建filter可以是用 CoCreateInstance方法或使用Filter Mapper或系统设备枚举器(System Device Enumerator)。
这些方法提供了三种构建graph的途径:
1.filter graph manager构建整个graph
2.filter graph manager构建部分graph
3.应用程序构建整个graph
Filter Graph Manager构建整个graph
如果你仅仅是想回放一个已知格式的文件,如AVI、MPEG、WAV或MP3,使用RenderFile方法。
RenderFile方法首先寻找注册在系统中能分析源文件的filter,它使用协议名(如http://),文件扩展名或文件的头几个字节来决定选择哪一个源filter。
Filter Graph Manager使用一个迭代过程来完成余下的graph构建。在这个迭代过程中,它逐个列出filter的输出pin上支持的媒体类型,并搜索哪个已注册的filter的输入Pin接受该媒体类型。它使用一系列的规则来缩小filter的范围并排定优先顺序:
*filter类别 (category)标识的filter的一般功能
*媒体类型描述filter能在接受或能输出哪种数据类型
*merit 值决定filter被尝试的次序。如果两个filter具有相同的filter类别并且同时支持相同的输入类型,Filter Graph Manager选择merit值大的那一个。一些filter故意给出一个小merit值是因为它是为特殊用途设计的,仅能由应用程序来将其添加到 graph。
Filter Graph Manager使用Filter Mapper对象来搜索已注册的filter。
每个filter被添加时,filter graph manager试着将其与前一个filter的输出pin连接。它们协商决定他们是否能连接,如果能,哪一种媒体类型被用来连接。如果新filter不能连接,filter graph manager丢弃它并尝试别一个,这个过程一直继续到每个流都被render为止。
Filter Graph Manager构建部分graph
如果不仅仅是播放一个文件,那么你的应用程序就必须做一些graph的构建工作。比如,一个视频采集应用程序必须先选择一个source filter并将其添加到graph中去。如果你需要将数据写入到一个AVI文件中,你必须添加一个AVI Mux和File Write filter。不过,也经常有可能让filter graph manager来完成整个graph,比如,你可以通过Render方法来render一个pin进行预览。
应用程序构建整个graph
在某些场合,你的应用程序需要添加和连接每个filter来构建graph。在这种情况下,你很可能明确地知道哪些filter需要加到graph中去。使用这种方式,应用程序通过调用 AddFilter方法添加每个filter,然后枚举filter上的pin,调用Connect或ConnectDirect来连接它们。
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中寻找
-- 作者:admin
-- 发布时间:2005-11-26 2:51:00
--
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余下的部分使用标准的推模式。
-- 作者:admin
-- 发布时间:2005-11-26 2:52:00
--
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;
HRESULT hr;
while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))
{
tch(evCode)
{
// Call application-defined functions for each
// type of event that you want to handle.
}
hr = pEvent->FreeEventParams(evCode, param1, param2);
}
要重置filter graph manager默认的事件处理过程,调用IMediaEvent::CancelDefaultHandling方法,用事件号做参数。你可以通过调用 IMediaEvent::RestoreDefaultHandling方法来恢复某个事件的处理过程。如果filter graph对某个事件号没有默认处理过程,则调用上面两个方法不产生任何影响。
3.5.3 当事件发生时
要处理DirectShow事件,应用程序需要一个方法来知道事件何时正等待在队列中。Filter Graph Manager提供两种方法:
*窗口通告:一旦有事件发生,Filter Graph Manager就发送一个用户自定义窗口消息来通知应用程序窗口
*事件信号:如果有DirectShow 事件在队列中,filter graph manager就触发一个windows事件,如果队列为空,则reset这个事件。
应用程序可以使用任何一种方法,但通常窗口通告方法相对比较简单。
窗口通告:
要设置窗口通告,调用IMediaEventEx::SetNotifyWindow方法并指定一个私有消息,私有消息可以是从WM_APP到0xBFFF的任一个。一旦filter graph manager把一个新的事件通告放入队列中,它便发送这个消息给指定的窗口。应用程序从窗口的消息循环中来响应这个消息。
下面是如何设置通知窗口的例子:
#define WM_GRAPHNOTIFY WM_APP + 1 // Private message.
pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
消息是一个普通的windows消息,并且独立于DirectShow消息通告队列被发送。使用这种方法的好处是大部分应用程序拥有一个消息循环,因此,要知道DirectShow事件何时发生便无需做额外的工作了。
下面是一段如何响应通告消息的框架代码:
LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)
{
tch (msg)
{
case WM_GRAPHNOTIFY:
HandleEvent(); // Application-defined function.
break;
// Handle other Windows messages here too.
}
return (DefWindowProc(hwnd, msg, wParam, lParam));
}
因为事件通告与消息循环均为异步进行的,因此在应用程序响应事件时队列中可以会有多个事件。而当事件变为非法时,它们会从队列中被清除掉。所以在你的事件处理代码中,调用GetEvent直至返回一个表示队列已空的失败代号。
在释放 IMediaEventEx指针前,请以NULL作参数调用SetNotifyWindow方法来取消事件通告。并且在你的事件处理代码中,在调用 GetEvent前检查IMediaEventEx指针是否合法。这些步骤可以防止在释放IMediaEventEx指针后应用程序继续接收事件通告的错误。
事件信号:
Filter Graph Manager建立一个反映事件队列状态的手工重设事件(manual-reset event)。如果队列中包含有未处理的事件通告,Filter Graph Manager就会发信号给手工重设事件。如果队列是空的,则调用IMediaEvent::GetEvent方法会重设(reset)事件。应用程序可以通过这个事件来确定队列的状态。
注意:此处的术语可能被混淆。手工重设事件是由windows的 CreateEvent函数创建的一种事件类型,它与由DirectShow定义的事件无关。
调用 IMediaEvent::GetEventHandle方法得到手工重设事件的句柄,调用一个函数如WaitForMultipleObjects来等待发送给手工重设事件的信号。一旦收到信号,就可以调用IMediaEvent::GetEvent来接收DirectShow事件了。
下面的代码举例说明了这种方法。在取得事件句柄后,在100毫秒时间间隔内等待发送给手工重设事件的信号,如果有信号发来,它调用GetEvent然后在 windows控制台上打印出事件号和事件参数,循环在EC_COMPLETE事件发生后结束,这标志着回放结束。
HANDLE hEvent;
long evCode, param1, param2;
BOOLEAN bDone = FALSE;
HRESULT hr = S_OK;
hr = pEvent->GetEventHandle((OAEVENT*)&hEvent);
if (FAILED(hr)
{
/* Insert failure-handling code here. */
}
while(!bDone)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100))
{
while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))
{
printf("Event code: %#04x//n Params: %d, %d//n", evCode, param1, param2);
pEvent->FreeEventParams(evCode, param1, param2);
bDone = (EC_COMPLETE == evCode);
}
}
}
因为Filter Graph会在适当的时候自动重设事件,因此你的应用程序应当不去作重设工作。同时,当你释放filter graph时,filter graph会关闭事件句柄,因此在这之后你就不能再使用事件句柄了。
-- 作者:admin
-- 发布时间:2005-11-26 2:53:00
--
3.6. DirectShow中的时间和时钟
这一节主要概述DirectShow体系中时间和时钟。
3.6.1. 参考时钟
Filter Graph Manager的一个功能,能够以同一个时钟来同步所有在graph中的filter,称作参考时钟(reference clock)。
任何暴露了IReferenceClock接口的对象都能够作为一个参考时钟来使用。参考时钟可以由一个 DirectShow filter来提供,例如可以直接使用硬件时钟的audio renderer。另外,Filter Graph Manager也能使用系统时间来作参考时钟。
名义上,一个参考时钟以千万分之一秒的精度来度量时间,但是实际上的精度不会这么高。要取得参考时钟的当前时间,调用IReferenceClock::GetTime方法。由于时钟的基准时间,即时钟开始时的时间计数,是依赖于具体的实现的,因此GetTime的返回值不反映绝对时间,只反映相对于graph开始时的相对时间。
虽然参考时钟的精度是变化的,但是 GetTime的返回值却保证是单调递增的,换句话说,也就是参考时钟的时间是不会回退的。如果参考时钟的时间是由硬件源产生的,而硬件时钟回退了(比如,有一个调节器调节了时钟),GetTime依然返回最晚的那个时间只到硬件时钟追上它。要知道更多的内容可以参考 CBaseReferenceClock类(http://msdn.microsoft.com/library/en-us/directshow/htm/cbasereferenceclockclass.asp )。
默认参考时钟
当Graph运行时,Filter Graph Manager会自动选择参考时钟,选择参考时钟的规则如下:
*如果应用程序指定了一个时钟,则使用这个时钟;
*如果 Graph包含了一个支持IReferenceClock的活动源filter(即推模式源filter),则使用这个filter;
*如果Graph未包含任何支持IReferenceClock的推模式源filter,使用任何一个支持IReferenceClock接口的 filter,选择的次序是从Renderer filter开始依次向上。已连接的filter优先于未连接的filter被选。(如果这个graph会render一个音频流,则这个规则通常就会选择audio renderer filter来作为参考时钟)
*如果没有filter支持合适的时钟,则使用系统参考时钟。
设置参考时钟
应用程序可以在Filter Graph Manager上调用IMediaFilter::SetSyncSource方法来选择时钟,只有在由于你有一个特殊原因想要选择自己的时钟时才需要这么做。
想要让Filter Graph Manager不使用任何参考时钟,可以调用SetSyncSource,参数为NULL。比如,你需要尽可能快地来处理sample时,就可以这么做。要恢复黑认的参考时钟,在Filter Graph Manager上调用IFilterGraph::SetDefaultSyncSource方法。
当参考时钟发生变化时,Filter Graph Manager会通知每一个filter调用它的IMediaFilter::SetSyncSource方法,应用程序无需调用filter的这个方法。
3.6.2. 时钟时间
DirectShow定义了两种相关时间:参考时间(reference time)和流时间 (stream time)
*参考时间是一个绝对时间,由参考时钟返回
*流时间是一个相对于graph最后开始时的相对时间
·当graph处于运行态时,流时间等于参考时间减去起始时间
·当graph处于暂停态时,流时间停留在暂停的那一刻
·在重新定位后,流时间被重设为0
·当graph处于停止态时,流时间无意义
如果一个媒体样本有一个时间戳t,表示这个在流时间为t时被render,正因为这个原因,因此流时间也被叫做呈现时间(presentation time)。
当应用程序调用IMediaControl::Run运行graph时,Filter Graph Manager调用每个filter的IMediaFilter::Run。为了补偿消耗在运行每个filter的时间总和,Filter Graph Manager会略微晚一点来定义起始时间。
3.6.3. 时间戳
时间戳定义了媒体样本的起始和结束时间。时间戳有时被称作呈现时间(presentation time)。在阅读余下的文章时,一个必须记住的要点是并非所有的媒体格式都以相同的方式来使用时间戳。举个例子,并不是所有MPEG样本都被打上了时间戳,在MPEG Filter Graph中,时间戳在被解码前并非应用在每个帧上。
当一个renderer filter接收到一个样本时,它以时间戳为基准来确定render时间。如果样本来晚了,或者这个样本没有时间戳,那个filter就立刻render 它,否则,filter就等在那直到合适的时机。(通过IReferenceClock::AdviseTime方法来等待样本的render时间)
源 filter和语法解析filte使用下列原则,在它们处理的样本上设置合适的时间戳:
*文件回放:第一个样本被打上起始时间戳,为0,后面的时间戳由样本长度和回放速率来决定,这些都由文件格式来决定。分析文件的filter负责计算出合适的时间戳。例子见(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directshow/htm/avisplitterfilter.asp )
*音视频采集:每个样本都被打上一个起始时间戳,这个时间戳与当它被捕获时的Stream time相同。应注意以下几点:
·从预览pin(Preview Pin)出来的样本没有时间戳。因为图像处理的延时,一个打上采集时间的视频帧总是会迟一点到达视频renderer。这会导致在进行质量控制时,renderer会丢弃部分视频帧。关于质量控制,参见(http://msdn.microsoft.com/library/en-us/directshow/htm/qualitycontrolmanagement.asp )
·音频采集:音频采集filter使用它自己的缓冲集,而并非使用音频驱动程序的。音频驱动以固定的时间间隔来填充采集filter的缓冲。这个时间间隔由驱动决定,通常不超过10毫秒。在音频样本上的时间戳反映的是驱动填充采集filter时的时间,因此会有偏差,尤其是当应用程序使用一个很小的缓冲区时。不过,媒体时间可以精确地反映缓冲区中音频样本的数量。
*混合filter(Mux filter):依赖于输出格式,一个mux filter可能需要由它来产生时间戳,也可能不需要。举个例子,AVI文件格式使用固定的帧率而没有时间戳,因此AVI Mux filter假设那些样本在近似正确的时间内到达。如果样本没有在合适的时间间隔内到达,AVI Mux filter会插入一个长度为0的空样本,来表示一个丢失的帧。在文件回放时,新的时间戳在运行时如前面所述地那样产生。
要在一个样本上设置一个时间戳,调用IMediaSample::SetTime方法。
此外,filter还可以为样本指定一个媒体时间(media time)。在视频流中,media time表示视频帧的数量。在音频流中,media time表示包中的样本数量,比如,如果每个包包含以44.1KHz的采样率采集的一秒钟的音频,那么第一个包具有一个为0的媒体起始时间以及为 44100的媒体终止时间。在一个可以定位的流中,媒体时间总是相对于流的起始时间,比如,假设你在一个15帧/秒的视频流上定位到2秒这个位置,那么定位后的每一个媒体样本的时间媒为0,但是它的媒体时间为30.
Renderer和Mux filter能使用媒体时间通过检查是否有缺口来确定帧或样本是否被丢弃了。但是,filter不是一定要设定媒体时间。要设置媒体时间,调用 IMediaSample::SetMediaTime方法。
3.6.4 实时源(Live Source)
实时源,也被叫做推模式源(push source),实时地接收数据,比如视频采集和网络广播。通常情况下,一个实时源不能控制数据到达的速率。
一个filter被认为是实时源需要具有以下几点:
* 调用IAMFilterMiscFlags::GetMiscFlags方法时返回AM_FILTER_MISC_FLAGS_IS_SOURCE标记,并且至少有一个输出pin暴露IAMPushSource接口。
* filter暴露IKsPropertySet接口,并具有一个capture pin(PIN_CATEGORY_CAPTURE)。
延时(latency)
一个filter的延时是这个filter处理一个样本所需的时间总和。在实时源中,延时取决于保存样本的缓冲区大小。举个例子,假设graph有一个具有33ms延时的视频源和一个具有500ms延时的音频源,那么每个到达视频renderer的视频帧要比与之匹配的音频样本到达音频renderer早470ms,除非 graph对这个差别进行补偿,否则音视频将会不同步。
实时源可以通过IAMPushSource接口来进行同步。Filter Graph Manager并做同步工作除非应用程序通过调用IAMGraphStreams::SyncUsingStreamOffset方法来激活它。如果同步被激活,Filter Graph Manager通过IAMPushSource来查询每一个source filter,如果filter支持IAMPushSource,那么Filter Graph Manager调用IAMLatency::GetLatency来得到filter预期的延时(IAMPushSource继承自 IAMLatency)。通过组合的延时值,filter graph manager决定graph中最大的预期延时,然后调用IAMPushSource::SetStreamOffset来给每一个source filter一个流偏移,以后filter会在产生时间戳时加上这个偏移。
这个方法主要是为了实现实时预览,但是,注意实时采集设备(比如摄像头)的preview pin上是没有时间戳的,因此,要在一个实时采集设备上使用这种方法,你必须在capture pin上进行视频预览。
通常,IAMPushSource接口被VFW Capture filter和音频采集filter(Audio capture filter)支持。
速率匹配(Rate Matching)
如果 renderer filter和source filter使用不同的参考时钟,那么就会有问题,renderer可能比source要快,这就导致了数据的缺口,或则renderer比source 慢,就会导致数据拥堵而样本丢弃。通常一个实时源无法控制速率,因此要求renderer来与source进行速率匹配。
通常,只有 audio renderer实现速率匹配,因为声音回放的频率比视频更重要。要实现速率匹配,audio renderer必须排除以下几点:
*如果graph没有使用一个参考时钟,那么audio renderer不会去进行速率匹配(如果graph没有参考时钟,那么样本总是在到达时就被立刻render)。
*另外,如果 graph中有一个参考时钟,audio renderer检测是否有一个实时源在上游,如果没有,audio renderer不进行速率匹配。
*如果有一个实时源在上游,并且这个实时源在它的输出Pin上暴露IAMPushSource接口,audio renderer调用IAMPushSource::GetPushSourceFlags,并寻找以下标记:
·AM_PUSHSOURCECAPS_INTERNAL_RM,这个标记表示这个实时源拥有自己的速率匹配机制,因此audio renderer不进行速率匹配。
·AM_PUSHSOURCECAPS_NOT_LIVE,这个标记表示source filter并不是一个真正的实时源,即使它暴露了IAMPushSource接口,因此,audio renderer不进行速率匹配。
·AM_PUSHSOURCECAPS_PRIVATE_CLOCK,这个标记表示source filter使用一个私有的时钟来产生时间戳。在这种情况下,audio renderer速率匹配与时间戳会有冲突。(如果样本没有时间戳,那么renderer忽略这个标记。
*如果 GetPushSourceFlags返回没有标记(0),audio renderer的行为依赖于graph时钟和样本是否拥有时间戳:
·如果audio renderer不是graph参考时钟,并且样本拥有时间戳,那么audio renderer速率匹配与时间戳会有冲突
·如果样本没有时间戳,audio renderer尝试与输入的音频数据的速率进行匹配。
·如果audio renderer是graph参考时钟,它与输入的数据速率进行匹配。
最后一种情况的原因如下:如果audio renderer是参考时钟,并且source filter使用同样的时钟来产生时间戳,那么audio renderer不会与这个时间戳进行速率匹配,因为如果它这样做了,导致的结果是,它等于在尝试与自己进行速率匹配,这将导致时钟偏差。因此,在这种情况下,renderer与输入的音频数据速率进行匹配。
-- 作者:admin
-- 发布时间:2005-11-26 2:53:00
--
3.7. Graph动态重建(Dynamic Graph Building)
如果你需要修改一个已经存在的filter graph,你可以停止,修改后再重新启动它。这通常是一种最佳的解决方法。但是,在某此情况下,你可能需要在一个graph处于运行状态时来修改它,比如:
*应用程序在进行视频回放时需要插入一个(视频滤镜filter)Video effect filter;
*source filter在播放的过程中改变了媒体格式,此时可能需要接入新的解码filter;
*应用程序在graph中加入一个新的视频流。
上面的这些都是graph动态重建的例子。所有在graph继续处于运行状态而做的graph修改都被叫做graph动态重建。动态重建可以由应用程序发起,也可以由一个在graph中的filter发起。动态重建有三种可能:
*媒体格式动态变化:一个filter可以在运行的中途改变媒体格式,而不需要重新被替换为另一个;
*动态重连:在graph中添加或删除filter
*Filter Chain操作:添加,删除,控制filter chain,(Filter Chain是相互连接着的一条Filter链路,并且链路中的每个Filter至多有一个Input pin,至多有一个Output pin)
3.7.1. 动态重连
在绝大多数的directshow filter中,当graph处于运行状态时pin是不能被重新连接的,应用程序必须在重连前停止graph。但是,某些filter却支持动态重连,这既可以由应用程序来执行,也可以由graph中的一个filter来执行。
如下图:
假设我们要将filter 2从graph中移除掉,替换成另一个filter,而此时graph还处于运行状态,那么必须具备以下几个条件:
*filter 3的输入pin(pin D)必须支持IPinConnection接口,这个接口可以重新连接pin而不需要停止它。
*filter 1的输出pin(pin A)必须能够在重连时阻塞媒体数据,数据不再在pin A和pin D之间传递。也就是说,输出Pin必须支持IPinFlowControl接口。但是,如果filter 1是发起重连的那个filter,那么它有可能已经在其内部实现了阻塞;
动态重连包括下列步骤:
1. 从Pin A那里阻塞数据流
2. 重新连接Pin A和Pin D,或者在中间加入新的filter
3. 取消Pin A上的阻塞
步骤1. 阻塞数据流 通过调用Pin A上的IPinFlowControl::Block方法来阻塞数据流。这个方法既可以被同步调用,也可以被异步调用。要异步调用这个方法,需要创建一个win32事件对象,并将事件句柄传给Block,方法会立即返回,然后使用 WaitForSingleObject或其它函数来等待事件的触发。当阻塞工作完成时,pin会触发这个事件。如:
// Create an event
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (hEvent != NULL)
{
// Block the data flow.
hr = pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK, hEvent);
if (SUCCEEDED(hr))
{
// Wait for the pin to finish.
DWORD dwRes = WaitForSingleObject(hEvent, dwMilliseconds);
}
}
如果是同步调用Block,那么只需将传入的hEvent参数设为NULL,此时这个方法会一直阻塞到阻塞工作完成为止。如果pin还没有准备好deliver一个新的sample,那么就会一直阻塞。而如果filter处于就绪状态,这可能会花费任意长的时间,因此,不要在应用程序的主线程中使用同步调用,以免发生死锁,开一个工作线程来使用同步调用,或者干脆就使用异步调用。
步骤2. 重连pin
要重新连接pin,查询graph的IGraphConfig接口并调用IGraphConfig::Reconnect或IGraphConfig::Reconfigure。 Reconnect方法使用比较简单:
*停止中间filter(比如filter 2),并移除它
*如果需要的话,加入新的中间filter
*连接所有的pin
*pause或run所有新的filter,使它的状态与graph相同
Reconnect 方法有参数可以用来指定pin连接的媒体类型和中间filter。如:
pGraph->AddFilter(pNewFilter, L"New Filter for the Graph");
pConfig->Reconnect(
pPinA, // Reconnect this output pin...
pPinD, // ... to this input pin.
pMediaType, // Use this media type.
pNewFilter, // Connect them through this filter.
NULL,
0);
如果Reconnect还不够用来应付我们的要求,那么你可以使用Reconfigure方法,它调用一个由应用程序定义的回调函数来重连这些 pin。要调用这个方法,需要在你的应用程序中实现IGraphConfigCallback接口。
在调用Reconfigure之前,如前面所述地那样阻塞输出pin的数据流。然后如下所示,将处于待处理状态的数据push下去:
1. 在重连链路中处于下游的最远的那个输入pin(例子中为Pin D)上调用IPinConnection::NotifyEndOfStream方法,方法的参数是一个Win32事件句柄;
2. 在与要阻塞数据的那个输出pin直接相连的那个输入pin上调用IPin::EndOfStream方法。(在例子中,要阻塞的那个输出pin是pin A,那么直接与之相连的那个输入pin为Pin B);
3. 等待事件触发。输入pin(pin D)在它接收到end-of-stream事件通告时触发事件。这表示再没有数据需要传输,此时就可以安全地进行重连了。
注意:IGraphConfig::Reconnect方法会自动处理上述步骤,你仅在调用Reconfigure方法时才需要自己来处理。
当数据完成push后,调用Reconfigure,传入IGraphConfigCallback回调函数的指针。Filter Graph Manager会调用IGraphConfigCallback::Reconfigure方法。
步骤3. 取消数据流的阻塞
当你完成重连后,通过调用IPinFlowControl::Block,第一个参数为0来取消阻塞。
注意:如果动态重连是由一个filter来执行的,那么你需要知道一点线程方面的问题。如果filter graph manager尝试去停止filter,它可能会死锁,因为graph等待filter停止,而与此同时,filter有可能在等待数据在graph中完成push。要防止这个可能存在的死锁问题,如前所述可以用事件机制来处理。
3.7.2. filter链(filter chains)
一个 filter chain是一系列具备下述条件的相互连接的filter:
*每一个在链中的filter最多只有一个已连接的输入pin 和一个已连接的输出pin;
*Filter链路中的数据流不依赖于链路外的其他Filter
举个例子,在下图中,filter A-B,C-D和F-G-H是一个filter chains。每个F-G-H中的子链(F-G和G-H)也是一个filter chain。一个filter chain同样可以是由单个filter组成的,因此A、B、C、D、F、G和H同样也是filter chain。filter E由于有两个输入连接,所以任何含有E的一系列filter都不是filter chain。
IFilterChain接口提供下述方法来控制filter chain:
IFilterChain::StartChain 开启一个链
IFilterChain::StopChain 停止一个链
IFilterChain::PauseChain 暂停一个链
IFilterChain::RemoveChain 从graph中移除一个链
没有特殊的方法来添加一个链,要添加链,通过调用IFilterGraph::AddFilter方法来插入新的filter,然后调用IGraphBuilder::Connect,IGraphBuilder::Render或类似的方法来连接它们。
当graph运行时,一个filter chain可以在运行和停止状态间切换。当graph处理就绪状态时,它可以在就绪和停止状态间切换。这是两种仅有的filter chain状态切换可能。
Filter链指南
当你使用IFilterChain方法时,确认在graph中的filter是否能支持filter链操作是十分必要的,否则,可能会发生死锁或graph 错误。filter连接到链上必须发生在链状态改变后。
使用IFilterChain的最佳情况是与一系统为链而设计的filter一起使用。使用下面的指南来确保你的filter是链操作安全的。参考下图:
* 在 filter链状态变化前,所在在filter链分界线上调用的数据处理都必须已完成。这个规则应用于IMemInputPin::Receive、IPin::NewSeqment和IPin::EndOfStream方法。链中的filter必须从由链外filter实现的这些方法调用中返回;而链外的filter也必须从这些由链内filter实现的这些方法调用中返回。
举个例子,在上图中,filter B必须完成在filter A上的所有数据处理调用,而filter E也必须完成从filter D上的调用。如果pin暴露了IPinFlowControl和IPinConnection接口,那么如在动态重连那一节中所讲的,你可以通过调用IPinFlowControl::Block和IGraphConfig::PushThroughData方法来推数据。filter也可能通过自己的方法来推数据。
* 上游filter必须与链的状态一起发生变化。比如,在上图中,假如链已停止,但filter A调用IMemInputPin::Receive方法,那么调用将失败,作为回应,filter A停止流。当应用程序重新开启链时,不会产生什么影响,因为filter A不再向使数据流动了。
* 下游filter必须同样与链的状态一起发生变化,否则,下游filter在等待取得sample时会发生死锁,因为sample不会再到来了。比如,多路复用(MUX)filter总是在它所有的input pin上需要数据,如果挂起其中的一个input pin,在其它input pin上的流处理也会被阻塞。这会导致graph死锁
* 每个与链内部filter相连的外部 filter的pin必须拥有自己的分配器(allocator),它不能被其它pin连接共享。当链的状态发生变化或从graph移除掉时,分配器便不可用了,此时如果还有其它的连接使用这个分配器的话,它们将不能再处理sample了。
* 除非与链相连的filter支持动态断开,否则不要移除链。典型的,已连接的filter会支持IPinConnection或IPinFlowControl接口,或者用它自己定义的接口代替。
-- 作者:admin
-- 发布时间:2005-11-26 2:54:00
--
3.8. 插件发布者(Plug-in Distributors)
Plug-in Distributors(PIDs)是扩展filter graph manager的一种方法。一个PID是filter graph manager在运行时聚合的一个COM对象。应用程序通过filter graph manager来进入PID。
当filter graph manager被要求查询一个它不支持的接口时,它会搜索注册表项:
HKEY_CLASSES_ROOT//Interface//IID//Distributor
IID是接口的GUID,如果注册项存在,那么键值便是支持该接口的PID类标识(CLSID)。filter graph manager聚合了PID并返回接口指针,应用程序调用这个指针时实际上就是在调用PID,但是这对于应用程序来说是透明的,对于应用程序来说,它就象是在filter graph manager上调用这个接口一样。
PID为应用程序提供了一种简单的控制filter的方法,如通过调用IFilterGraph::EnumFilters方法,PID可以枚举graph中的所有 filter并调用这些filter上的方法。
当filter graph manager聚合了一个PID时,它查询PID的IDistributorNotify接口,如果PID支持这个接口,filter graph manager用它来通知PID有关graph的状态变化:
* 当filter graph在run、pause和stop状态之间切换时,它调用IDistributorNotify::Run, IDistributorNotify::Pause或IDistributorNotify::Stop。
* 如果调置了参考时钟,filter graph manager调用IDistributorNotify::SetSyncSource。
* 当有filter添加或移除,或pin连接有变化时,filter graph manager调用IDistributorNotify::NotifyGraphChange。
当自己定制PID时,自己所创建的COM对象必须支持聚合,并且它所支持的接口是filter graph manager本身所没有的。IDistributorNotify接口是可选的。
如果PID从filter graph manager上获得一个接口,那它必须立即release这个接口,否则会在COM对象上出现循环引用的参考计数,使得filter graph manager无法被销毁。在filter graph manager上保持一个参考计数是多余的,因为PID的生命期是由filter graph manager控制的。
因为PID是明确指定是被filter graph manager聚合使用的,因此你应该在PID的构造函数中强行检查IUnknown指针是否为NULL,如果为NULL,则返回错误码 VFW_E_NEED_OWNER。同时,为了防止其它对象聚合PID,你可以在IUnknown上查询IGraphBuilder接口,如果不行则返回错误。
-- 作者:admin
-- 发布时间:2005-11-26 2:55:00
-- 奇妙”的Merit(玩死Media Player)
运行GraphEdit,插入Filter,我们可以看到:每个Filter的信息一般包括Displayname、Filename、Merit、各个Pin以及Pin支持的Mediatype,还有Version等。我们今天就来看一看这个Merit(其他的一些Filter信息大家从它的名字上就可以猜到它的意义)。
要说Merit,肯定要先说Filter Graph Manager使用的智能连接(Intelligent Connect)机制。我们在Filter Graph中Render一个Pin,或者Render一个File,然后看到一条自动的“解码”Filter链路就完成了——这就是智能连接机制。执行这个机制的调用方法为:IGraphBuilder::RenderFile, IGraphBuilder::Render, 和 IGraphBuilder::Connect。下面分别对这三个调用方法进行阐述。
RenderFile :给出一个文件名,首先要找到正确的Source Filter。Filter Graph Manager通过查找注册表来决定使用什么Source Filter。在注册表中,一般会有文件扩展名或者特征字节与使用的Source Filter的对应信息。找到Source Filter之后,就从该Source Filter的各个Output pin开始,进行剩下的职能连接过程。这是一个“递归”过程,直到所有的分支都连到一个Renderer Filter上。步骤大致为:
1. 如果Output pin支持IStreamBuilder接口,则把剩下的工作交给IStreamBuilder::Render。
2. 使用在内存中缓冲的Filter进行是试连接。
3. 使用在当前Filter Graph中还没有完全连接的Filter进行试连接。(如果你想智能连接使用特定的你想使用的Filter,一种方法是,在开始智能连接之前先把该 Filter加入到Filter Graph中。)
4. 使用IFilterMapper2::EnumMatchingFilters搜索注册表。Filter Graph Manager使用Merit值大于MERIT_DO_NOT_USE的所有Filter(Filter所在的目录Merit值也应该大于 MERIT_DO_NOT_USE)进行试连接。在匹配Mediatype的前提下,Merit值越高,该Filter被使用的概率越高。
Render :这个方法从当前Filter Graph的某个Filter的指定Output pin开始,进行从这个Pin往下的一条支路的智能连接。智能连接的算法与上述RenderFile的类似。
Connect :这个方法调用,以欲连接的一对Input pin和Output pin作为参数。首先进行这两个Pin之间的直接连接。如果不能成功连接,则要插入“中介”Filter。这个“中介”Filter的插入过程就是一个智能连接过程,算法与上述的RenderFile类似。
现在我们知道了智能连接是怎么回事。DirectShow的这个机制,很“聪明”,可以方便地使用第三方(非Microsoft公司)开发的Filter。但是,有一个问题,就是如果系统中存在一些“恶意”的Filter,那么这个智能连接机制就会受到严峻的考验。因为这个原因,基于智能连接机制的应用程序(比如Windows Media Player)也会变得不稳定。(笔者并不赞成直接使用DirectShow Editing Services API进行非线性编辑,就是出于这方面的考虑。)
大家可以下载我写的这个测试Filter源代码(http://hqtech.nease.net/Document.htm)。其实这是一个 CTransFormFilter的空架子,只是这个Filter的Merit值非常高(0x8800000),而且支持所有的Mediatype。注册这个Filter后,当有DirectShow应用程序使用智能连接机制时,就会反反复复地使用这个Filter进行试连接,没有休止。如果你使用 Windows Media Player播放媒体文件(AVI、MPEG、WMV等等),应用程序就会阻塞住;即使是RealOne Player,在播放微软格式的文件时也会出现这种现象。
好了,不玩了,别把Media Player弄得太惨,毕竟对我们也没什么好处!:)期望大家已经对这个Merit有了更深的认识。接下去,把这个Filter从你的系统注销吧:regsvr32 /u yourlocalpath//HQMPKiller.ax。
-- 作者:admin
-- 发布时间:2005-11-26 2:56:00
-- 玩转 DVR-MS
发布日期: 6/7/2005 | 更新日期: 6/7/2005
Stephen Toub
Microsoft Corporation
适用于:
Microsoft Windows XP Media Center Edition 2005
Microsoft DirectShow
DirectX 9.0 SDK
摘要: Stephen Toub 讨论了 Windows XP Media Center 2005 生成的 DVR-MS 文件格式,介绍了 DirectShow 并展示了如何使用后者处理前者。
下载 DVR-MS 示例 Code.msi 。
播放 DVR-MS 文件
DirectShow 和 GraphEdit 简介
DirectShow 接口
将编码转换为 WMV
调试筛选器图形
非托管资源清理
将 WmvConverter 投入使用: WmvTranscoderPlugin
访问 DVR-MS 元数据
编辑 DVR-MS 文件
小结
相关书籍
致谢
几年前我拥有一台 TiVo。它已经不知藏在公寓壁橱的哪个角落了,我想现在一定是布满灰尘,诚然,就是现在我也可能这样对待它。占据电视旁宝贵空位的是一个更漂亮、更复杂的现代化软件和电子产品 — Microsoft Windows XP Media Center 2005。我的家人为该设备取了个既得体又人性化的名字 —“米老鼠”,它有许多神奇的功能。然而,当我建议我的“技术娴熟”的朋友们放弃他们现在使用的任一款数字摄像机 (DVR) 而转为使用此平台时,只要他们让我说明一个理由,我的回答都很简单:可以对录制的电视节目进行文件访问。
DVR-MS 文件是由 Windows XP Service Pack 1 引入的流缓冲引擎(Stream Buffer Engine,SBE)创建的,Media Center 用它存储录制的电视节目。在本文中,我将向您演示如何通过托管代码使用 DirectShow 来处理和操作 DVR-MS 文件。在此过程中,我将向您介绍我为处理 DVR-MS 文件而创建的一些有用的实用工具,并为您提供您在编写自己的代码时需要的工具和库。所以,请打开 Visual Studio .NET,抓一把爆米花,享受这个过程吧。
注 本文假定您的系统中有一个正在工作的 MPEG2 解码器,并且您使用的是 NTSC 而非 HD 内容(虽然这里讨论的大多数概念适用于 PAL 和 HD,但示例代码可能无法正确地处理这些格式)。另外,由于内容所有者或广播公司所设置的策略,一些 DVR-MS 文件受到复制保护。这种保护是在生成文件时通过检查广播公司的复制保护标志 (CGMS-A) 确定的,它会限制您访问特定 DVR-MS 文件的方式和时间。例如,在收费台(如 HBO)录制的电影可能是加密的,因此本文描述的技术就不适用了。最后,与本文相关联的代码示例和应用程序是针对 .NET Framework 1.1 编译的。然而,默认情况下 Windows XP Media Center 2005 并没有附带安装 .NET Framework 1.1,而是安装 1.0。因此,要在您的 Media Center 中使用这些示例,您必须安装 .NET Framework 1.1(可通过 Windows Update 获得)或者重新编译该示例以适用 .NET Framework 1.0。
-- 作者:admin
-- 发布时间:2005-11-26 2:56:00
--
谈到视频文件时,播放或许是可以执行的最重要的操作,所以我将从此入手。在您自己的应用程序中可以有多种播放 DVR-MS 文件的方式,这里我将演示其中的一些。为此,我创建了一个简单的应用程序(如图 1 所示),您可以在与本文有关的代码下载中获得。
图 1. 播放 DVR-MS 文件的示例应用程序
播放 DVR-MS 文件的第一种方式也是最简单的方式是,使用 System.Diagnostics.Process 类来执行它。由于Process.Start 包装了来自 shell32.dll 的 ShellExecuteEx 非托管函数,因此这种方式利用了与从 Windows Explorer 双击一个文件相同的功能来播放 DVR-MS 文件:
private void btnProcessStart_Click(object sender, System.EventArgs e){ Process.Start(txtDvrmsPath.Text);}
这也意味着,视频将在一个独立的进程中播放,这个进程在 DVR-MS 文件的任何默认处理程序中运行;对于大多数机器和我的机器来说,它就是 Windows Media Player(我使用 Windows Media Player 10,如果您没有,我建议您从 http://www.microsoft.com/windows/windowsmedia/mp10/default.aspx 免费升级到该版本)。当然,Process.Start 有另一个同时接受可执行路径和参数的重载,可以使用它在任何您想要的播放机中启动 DVR-MS 文件,不管它是否是 .dvr-ms 扩展名的默认处理程序:
private void btnProcessStart_Click(object sender, System.EventArgs e){ Process.Start( @"c://Program Files//Windows Media Player//wmplayer.exe", "//"" + txtDvrmsPath.Text + "//"");}
您应该注意到,当这样做时,有必要对 DVR-MS 文件的路径加上引号(正如此处名为 txtDvrmsPath 的 TextBox的内容所提供的),因为要使用的内容是 wmplayer.exe 的一个命令行参数。否则,路径中的任何空格都会使路径被分隔并解释为多个参数。
Process.Start 返回一个代表启动进程的 Process 实例,这意味着您可以利用 Process 提供的功能来与 Windows Media Player 进一步交互。例如,在您的应用程序中,您可能想先等待视频停止再让用户继续,可以使用 Process.WaitForExit 方法来完成这样的任务:
private void btnProcessStart_Click(object sender, System.EventArgs e){ using(Process p = Process.Start(txtDvrmsPath.Text)) { p.WaitForExit(); }}
当然,这只是等待 Media Player 关闭,而不是像初始请求那样播放您指定的文件,因为您的应用程序没有真正的视图可以查看 Media Player 执行的内容。当打开 Media Player 时,按上述方法编码也会冻结应用程序的 GUI,这个问题可以通过订阅 Process 的 Exited 事件加以解决,而不是用 WaitForExit 方法阻止。
总而言之,该解决方案编码简单方便,但非常不灵活,而且是在应用程序的外部播放视频。它可能只在以下情况下才适用,您想允许用户查看指定的文件,不过是在应用程序不必关心视频内容而且应用程序根本不与视频交互的情况下查看。例如,如果您的应用程序是一个下载代理,而且您想允许用户查看已经复制到本地的视频文件,则可能适合采用这种方式。
-- 作者:admin
-- 发布时间:2005-11-26 2:57:00
--
由于我们知道 Windows Media Player 可以播放 DVR-MS 文件,因此对于大多数情况,更好的解决方案是在应用程序中宿主 Windows Media Player ActiveX 控件的一个实例。在 Visual Studio .NET 中,只需右键单击工具箱,选择添加控件并选择 Windows Media Player COM 控件。这样它就会出现在工具箱中,如图 2 所示。
图 2. 工具箱中的 Windows Media Player ActiveX 控件
当窗体中有一个 ActiveX 控件的实例时,让它播放 DVR-MS 文件就只需设置播放器的 URL 属性:
player.URL = txtDvrmsPath.Text;
在我的示例应用程序中,我选择让它更进一步。我创建了一个 System.Windows.Forms.Panel ,它位于想要显示视频的窗体中。当用户请求使用 Media Player 播放选定的视频时,我就新建一个 Media Player 控件的实例,将它添加到 Panel 的子控件集合中,使其保持在最大化,并设置其 URL 属性。这种方案允许我完全控制 Media Player 的生存期,而且可以轻松管理它在窗体中的位置,而不用担心它的绝对定位值(这种方案也使演示播放视频的其他方法变得轻松,稍后您将看到)。正在使用的这种方案的屏幕快照如图 3 所示,下面显示的是我使用的代码:
private void btnWmp_Click(object sender, System.EventArgs e){ AxWindowsMediaPlayer player = new AxWindowsMediaPlayer(); pnlVideo.Controls.Add(player); player.Dock = DockStyle.Fill; player.PlayStateChange += new _WMPOCXEvents_PlayStateChangeEventHandler( player_PlayStateChange); player.URL = txtDvrmsPath.Text;}private void player_PlayStateChange( object sender, _WMPOCXEvents_PlayStateChangeEvent e){ AxWindowsMediaPlayer player = (AxWindowsMediaPlayer)sender; if (e.newState == (int)WMPLib.WMPPlayState.wmppsMediaEnded || e.newState == (int)WMPLib.WMPPlayState.wmppsStopped) { player.Parent = null; // removes the control from the panel ThreadPool.QueueUserWorkItem( new WaitCallback(CleanupVideo), sender); }} private void CleanupVideo(object video){ ((IDisposable)video).Dispose();}
图 3. 使用 WMP 控件的嵌入式 DVR-MS 播放
要阻止显示 Media Player 工具栏,您可以更改控件的 uiMode 属性:
player.uiMode = "none";
要在用户右键单击控件时阻止显示 Media Player 上下文菜单,可以将其 enableContextMenu 属性设置为 false:
player.enableContextMenu = false;
您将注意到,在播放 DVR-MS 文件的前一刻,我为播放器的 PlayStateChange 事件注册了一个事件处理程序。这可以使我在播放停止时从 Panel 删除播放器。在 PlayStateChange 事件的处理程序中,我检查播放是否结束,如果结束,就将播放器从其父控件(面板)删除,并将一个工作项排入 .NET ThreadPool 队列中。这个工作项的作用只是处置播放器控件。我是在后台线程中进行此次处置的,因为无法在 PlayStateChange 事件处理程序中直接处置。在此事件处理程序中处置控件会在控件本身中引发异常,因为事件处理程序是在控件中引发的,控件在执行完我的处理程序之后还需要进行更多的处理。在处理程序中处置播放器控件会导致功能被破坏,所以我让该操作在事件处理程序完成之后稍微延迟一会,以便留出必要的时间。您将看到,在使用所演示的下一个播放机制时,就需要用到同一技术。
宿主 Windows Media Player ActiveX 控件有许多好处。它使用起来非常方便,而且提供了大量的功能。然而,Windows Media Player 使用 DirectX(特别是 DirectShow)来播放 DVR-MS 文件(本文后面我将更详细地讨论 DirectShow)。您不是依赖 Windows Media Player 与 DirectX 交互,而是在您的应用程序中使用 Managed DirectX,完全跳过 Windows Media Player。
在写作本文时 Managed DirectX 的最新版本是 DirectX 9.0 SDK Update February 2005 下载 的一部分。(要获得本文后面介绍的内容,您还需要 February 2005 Extras 下载 。)此 SDK 在您的全局程序集缓存 (GAC) 中安装了 AudioVideoPlayback.dll 程序集,使其可用于您的应用程序(DirectX 运行库安装也安装了此 DLL 以使您的最终用户可以访问它)。AudioVideoPlayback 是一个高级包装,它含有您在 .NET 应用程序中播放视频和音频文件所需要的最少的 DirectShow 功能。
有了 Windows Media Player ActiveX 控件后,使用 AudioVideoPlayback 变得非常简单。
private void btnManagedDirectX_Click(object sender, System.EventArgs e){ Video v = new Video(txtDvrmsPath.Text); Size s = pnlVideo.Size; v.Owner = pnlVideo; v.Ending += new EventHandler(v_Ending); v.Play(); pnlVideo.Size = s;}private void v_Ending(object sender, EventArgs e){ ThreadPool.QueueUserWorkItem( new WaitCallback(CleanupVideo), sender);}private void CleanupVideo(object video){ ((IDisposable)video).Dispose();}
这段代码首先实例化一个新的 Microsoft.DirectX.AudioVideoPlayback.Video 对象,然后将要播放的 DVR-MS 文件的路径提供给它。当播放一段 Video 时,它会自动将自身的大小(更具体地说是将它的所有者控件)调整为所播放视频的合适大小;为了解决这个问题,我存储了父面板控件的原始大小,这样在开始播放后就可以重置其大小。就像处理 ActiveX 控件那样,我注册了一个要在播放停止时激发的事件处理程序,然后播放视频。当播放结束时,我将一个工作项排入要处置 Video 对象的 ThreadPool 队列中,如同使用 ActiveX 控件一样(原因也相同)。当您不再使用 Video 对象时,对其进行处置是非常重要的;否则会浪费大量非托管资源,而且由于此对象有一个非常小的托管占地,垃圾回收器 (GC) 没有重大的动因可以及时进行回收,这样将使这些非托管资源的分配情况不明,除非您手动通过 IDisposable 处置。图 4 中的屏幕快照演示了 AudioVideoPlayback 功能的使用。
图 4. 采用 AudioVideoPlayback 的嵌入式播放
当然,虽然 AudioVideoPlayback 是一个高级 DirectShow 包装,但并不意味着您不能创建自己的托管包装(实际上,在本文后面我们将这样做)。创建托管包装的最简单方式是使用 tlbimp.exe(或者采用类似的做法 — 使用 Visual Studio .NET 的 COM 类型库导入功能。Visual Studio .NET 和 tlbimp.exe 都依赖于 Framework 中同样的库执行导入)。
-- 作者:admin
-- 发布时间:2005-11-26 2:57:00
--
DirectShow 运行库的核心库是 quartz.dll,位于 %windir%//system32//quartz.dll。它包含用于音频和视频播放的最重要的 COM 接口和 coclass,本文后面将对此进行更加详细的讨论。在 quartz.dll 上运行 tlbimp.exe 会产生一个 interop 库 — Interop.QuartzTypeLib.dll(此程序集的描述信息为“ActiveMovie control type library”,因为 DirectShow 的前身名为 ActiveMovie),并公开 FilgraphManagerClass (筛选器图形管理器)和IVideoWindow 接口。要播放视频,您只需创建该图形管理器的一个新实例并使用 RenderFile 方法,在 DVR-MS 文件路径中传送,以便初始化该对象以进行播放。然后可以使用由 FilgraphManagerClass 实现的IVideoWindow 接口来控制播放选项,例如所有者窗口、视频在父窗口中的位置,以及视频窗口的标题。要开始播放,可以使用 Run 方法。WaitForCompletion 方法可以用于等待视频停止播放(或者,可以指定一个正的毫秒数,作为要等待的最长时间),Stop 方法可以用于暂停播放。要销毁该对象并释放用于播放的所有非托管资源(包括播放窗口本身),System.Runtime.InteropServices.Marshal 类及其 ReleaseComObject 方法就会派得上用场了。使用 quartz.dll 的屏幕快照如图 5 所示。
private void btnQuartz_Click(object sender, System.EventArgs e){ FilgraphManagerClass fm = new FilgraphManagerClass(); fm.RenderFile(txtDvrmsPath.Text); IVideoWindow vid = (IVideoWindow)fm; vid.Owner = pnlVideo.Handle.ToInt32(); vid.Caption = string.Empty; vid.SetWindowPosition(0, 0, pnlVideo.Width, pnlVideo.Height); ThreadPool.QueueUserWorkItem(new WaitCallback(RunQuartz), fm);}private void RunQuartz(object state){ FilgraphManagerClass fm = (FilgraphManagerClass)state; fm.Run(); int code; fm.WaitForCompletion(Timeout.Infinite, out code); fm.Stop(); while(Marshal.ReleaseComObject(fm) > 0);}
图 5. 使用 quartz.dll 的嵌入式播放
我刚刚向您介绍了一些在自己的应用程序中播放 DVR-MS 文件的方法。虽然我讨论了多个播放 DVR-MS 文件的方法(而且我还没列举完),但所有这些方法都要依赖于 DirectShow 才有播放功能。因此,我们将简要介绍一下 DirectShow(或者让那些具有 DirectShow 经验的人重温一下)。
返回页首
在本质上,使用 DirectShow 处理视频文件的应用程序是通过一组称为筛选器的组件完成的。一个筛选器通常只对多媒体数据流执行一种操作。这样的筛选器很多,每个筛选器执行不同的任务,例如读取 DVR-MS 文件、写出 AVI 文件、对 MPEG-2 压缩视频进行解码、将视频和音频呈现到视频卡和声卡上,等等。这些筛选器的实例可以连接在一起并组合成一个筛选器图形,然后由 DirectShow 筛选器图形管理器组件进行管理(在前面介绍 quartz.dll 时,您已简要地对其进行了了解)。这些图形是定向的,也是非循环的,这意味着两个筛选器之间的特定连接只允许数据朝一个方向流动,而且只能流经特定筛选器一次。这种数据流程称为流 (stream),而筛选器则用来处理这些流。筛选器是通过它们公开的针 (pin) 连接到其他筛选器的,因此,一个筛选器的输出针连接到另一个筛选器的输入针,并按从前者发送到后者的方式发送数据流。
为了对此进行演示并显示本文中所使用的图形,我使用了 DirectX SDK 中一个名为 GraphEdit 的实用工具。GraphEdit 可以用来使筛选器图形可视化,当要确定如何构建用于特定目的的图形以及调试您所构建的图形时,这个功能就能派上用场。稍后,我将介绍如何使用 GraphEdit 来对在您的应用程序中运行的筛选器图形进行连接和可视化。
现在,我们运行 GraphEdit。在“File”菜单下,选择“Render Media File”,然后选择本地可用的任何有效的 DVR-MS 文件(请注意,您可能需要在“Open File”对话框中将筛选器扩展名更改为“All Files”,而不是“All Media Files”,因为最近发布的 GraphEdit 版本并没有将 .dvr-ms 扩展名归类为媒体文件)。您应该能够看到一个图形,它类似于图 6 所示的图形。
图 6. GraphEdit 准备播放 DVR-MS 文件
此时,GraphEdit 已构造了一个筛选器图形,它能够播放选定的 DVR-MS 文件。这些蓝框中的每一个都是一个筛选器,箭头显示每个筛选器上的输入和输出针如何互相连接以形成图形。图形中的第一个筛选器是 StreamBufferSource 筛选器的实例,它由 Windows XP SP1 及更高版本的 %windir%//system32//sbe.dll 库公开。选择这个筛选器是因为它在注册表中配置为 .dvr-ms 扩展名的源筛选器 (HKCL//Media Type//Extensions//.dvr-ms//Source Filter)。它的作用是从磁盘中读取一个文件,并将该文件的数据以流的形式发送到图形的其他部分。它从一个 DVR-MS 文件提供三个流。
-- 作者:admin
-- 发布时间:2005-11-26 2:58:00
--
第一个是音频流。如果您检查第一个针的针属性(DVR Out - 1,可以通过右键单击 GraphEdit 中的针来访问针属性),您可以发现该针的主要类型是 Audio,而其子类型是 Encrypted/Tagged,这意味我们在对该数据进行任何操作之前必须先对它进行解密和/或取消标记。这个过程是由 Decrypter/Detagger 筛选器(由 %windir%//system32//encdec.dll 公开)处理的。Decrypter/Detagger 将加密/带标记的音频流作为输入,然后发出 MPEG-1 音频流(对于高清晰度的内容则输出 dolby-AC3 流),这一点您可以通过检查该筛选器的 In(Enc/Tag) 和 Out 针加以验证。这里将音频发送到 MPEG Audio Decoder 筛选器(由 quartz.dll 公开),通过它将音频解压缩为脉冲编码调制 (PCM) 音频流。音频流的最后一个筛选器 DirectSound Audio Renderer(也由 quartz.dll 公开)接收此 PCM 音频数据并在计算机的声卡上播放。
DVR-MS 源筛选器提供的第二个流包含所录制的电视节目的闭合字幕数据。和音频流一样,闭合字幕流也是经过加密/标记的,所以它必须首先通过 Decrypter/Detagger 筛选器。如果查看此筛选器的 Out 针,您会发现其主要类型是 AUXLine21Data,而其子类型是 Line21_BytePair。电视节目中的闭合字幕是作为电视图像的一部分发送的,并专门编码到图像的 line 21 中。
DVR-MS 源筛选器发出的第三个流是视频内容 (video feed)。与音频和闭合字幕数据一样,这个流也是经过加密/标记的,所以它必须首先通过 Decrypter/Detagger 筛选器。Decrypter/Detagger 筛选器的输出是 MPEG-2 视频流,所以它必须先通过 MPEG-2 视频解码器才能呈现视频。Microsoft 没有在 Windows 中附带 MPEG-2 解码器,所以系统中必须有可用的第三方解码器才能播放。解码后的视频流再送到默认的视频呈现程序(由 quartz.dll 公开)。
单击图形上方的绿色播放按钮就会出现一个标题为 ActiveMovie Window 的新窗口并在该窗口中播放 DVR-MS 文件。请注意,由于闭合字幕 Decrypt/Tag Out 针没有连接到任何地方,因此在呈现视频时没有用到闭合字幕数据。您可以通过修改图形对此进行更改。实际做法是,首先删除默认的视频呈现程序(单击该筛选器并按“Delete”键),因为该呈现程序不能处理多路输入。具体来说,我们需要这样的呈现程序:它可以显示视频流,并能将包含呈现的闭合字幕数据的位图覆盖其上。如何从 Decrypter/Detagger 筛选器获取 line 21 字节对,将其作为位图呈现出来呢?Windows 实际上附带了一个正好可以完成此任务的 DirectShow 筛选器。使用“Graph”菜单下的“Insert Filters...”命令,展开树视图中的 DirectShow 筛选器节点并选择“Video Mixing Renderer 9”筛选器。单击“insert”按钮将此筛选器的实例添加到图形中,然后关闭“insert filters”对话框。现在,Video Mixing Renderer 9 筛选器成为图形的一部分了,但没有连接到任何地方,也就不能使用(实际上,如果您现在单击“play”按钮,则只播放音频,因为视频流没有连接到呈现程序)。单击 MPEG-2 解码器上的 Video Output 针,并将它拖到呈现程序的 VMR Input0 针上(请注意,如果您使用的解码器不是 NVDVD,则视频输出针的名称可能不同,但概念是一样的)。如果您现在播放图形,则会看到输出与使用默认视频呈现程序播放时基本一致。然而,您将看到,此时呈现程序筛选器公开了多个输入针(实际上,筛选器可以根据连接到它们的其他筛选器动态更改公开的针)。我们可以将闭合字幕 Decrypter/Detagger 筛选器的 Out 针连接到呈现程序的 VMR Input1 针上,以此利用这一特性。GraphEdit 会自动插入一个 Line 21 Decoder 2 筛选器,将 Decrypter/Detagger 筛选器连接到解码器筛选器,并将解码器筛选器连接到呈现程序筛选器。现在,您应该能看到如图 7 所示的图形。当您播放此图形时,您将看到闭合字幕像您期望的那样,以文本的形式出现在视频前。
图 7. 将闭合字幕合并到视频显示中
此时,对 DirectShow 不熟悉的读者可能会产生疑惑:是如何发现 Line 21 Decoder 2 筛选器的?为什么一开始只需使用 GraphEdit 的 Render Media File 操作就能构造出整个图形呢?GraphEdit 依赖 IGraphBuilder 接口提供的功能来查找和选择合适的筛选器,并在需要时将它们互连(IGraphBuilder 是由我们在介绍如何播放 DVR-MS 文件时简要提到的 FilgraphManager 组件实现的,实际上我们使用的 RenderFile 方法就是 IGraphBuilder接口的一部分)。
-- 作者:admin
-- 发布时间:2005-11-26 2:58:00
--
这种用于自动构建筛选器图形的机制称为 Intelligent Connect。由于您并不真的需要知道 Intelligent Connect 的具体内容(除非您正在实现自己的筛选器并想让它们可以自动构建图形),因此在这里此主题我不想介绍得太多,而是让您参考 DirectX SDK 中该主题的详细文档。然而,简单地说,RenderFile 方法是一个简单的包装,它包装了 IGraphBuilder 中的另外两个方法:AddSourceFilter 和 Render 。RenderFile 首先调用AddSourceFilter ,对于本地文件,它只需在注册表中查找正在播放的文件的扩展名所必需的源筛选器的类型,将适当的筛选器实例添加到筛选器图形中,并对它进行配置以使其指向指定的源文件。对于此源筛选器的每个输出针,RenderFile 再调用 Render 方法,该方法试图查找从此针到图形中的呈现程序的一条路径。如果该针实现了 IStreamBuilder 接口,则 Render 只是委托该实现,将所有细节都交给该筛选器的实现。否则,Render会试图查找此针可以连接的筛选器。为此,它会查找在图形构建过程前期可能缓存的缓存筛选器,查找已经成为图形的一部分且有未连接的输入针的任何筛选器,并使用 IFilterMapper 接口查找注册表中兼容的筛选器类型。如果找到了一个筛选器,则它会再对这个新的筛选器重复此过程,直到到达呈现筛选器,此时就成功地停止。如果没有找到筛选器,则 Intelligent Connect 构建图形未成功。这就是依赖 Intelligent Connect 的一个缺点:它并非始终有效。另外,如果您的机器上安装了新的筛选器,则 Intelligent Connect 可能会选择这些新的筛选器,而不是您当前期望在应用程序中使用的筛选器。因此,您在设计时可能要选择避免这种情况(我后面将要介绍,如果您确切地知道想在图形中使用哪些筛选器,则显式构建图形而不使用 Intelligent Connect 是很容易的)。
既然您对 DirectShow 已有所了解,我们将要以编程方式使用它,以便对 DVR-MS 文件进行许多很合适的操作。毕竟,一旦 DVR-MS 源筛选器加载到图形中,我们就可以像处理其他音频和视频数据流那样处理来自 DVR-MS 的数据,操作它们的方法是无限的。
-- 作者:admin
-- 发布时间:2005-11-26 2:58:00
--
然而,我们首先需要的是能够以编程方式处理 DirectShow。对于非托管代码,这可能是立即可行的,因为 SDK 包含了通过 C++ 访问 DirectShow 库所需要的所有头文件。对于托管代码,问题就有些棘手。虽然 Managed DirectX 确实包含前面讨论的 AudioVideoPlayback.dll 库,但该库级别很高,它提供 Video 和 Audio 级别的抽象,而我们需要的是能够在筛选器和针级别对筛选器图形进行操作。虽然我觉得这个问题将来会得到改善,但至少当前版本的 Managed DirectX 对我们爱莫能助。
quartz.dll 是什么?quartz.dll 的类型库公开了一些我们需要的功能,这里列出所公开接口的完整列表
[此贴子已经被作者于 2005-11-26 3:03:44编辑过]
-- 作者:admin
-- 发布时间:2005-11-26 2:59:00
--
接口
描述
IAMCollection
筛选器图形对象集合,例如筛选器或针。
IAMStats
允许应用程序从图形管理器中检索性能数据。筛选器可以使用此接口记录性能数据。
IBasicAudio
允许应用程序控制音频流的音量和平衡。
IBasicVideo
允许应用程序设置视频属性,例如目标矩形和源矩形
IBasicVideo2
从 IBasicVideo 接口派生,为应用程序提供了一个附加方法,通过它可以检索视频流的首选纵横比。
IDeferredCommand
允许应用程序取消或修改该应用程序先前使用 IQueueCommand 接口排入队列的图形-控制命令。
IFilterInfo
管理筛选器的信息并提供访问筛选器和表示筛选器上的针的 IPinInfo 接口。
IMediaControl
提供方法来控制经过筛选器图形的数据流。它包含运行、暂停和停止图形的方法。
IMediaEvent
包含用来检索事件通知和用于重写筛选器图形管理器的默认事件处理的方法。
IMediaEventEx
从 IMediaEvent 派生并添加方法来启用一个应用程序窗口,以便在事件发生时接收消息。
IMediaPosition
包含用于查找流中一个位置的方法。
IMediaTypeInfo
包含用于检索针连接的媒体类型的方法。
IPinInfo
包含用于检索针信息和连接针的方法。
IQueueCommand
允许应用程序预先将图形-控制命令排入队列。
IRegFilterInfo
提供对 Windows 注册表中的筛选器的访问,以及向筛选器图形中添加已注册的筛选器。
IVideoWindow
包含用于设置窗口所有者、窗口的位置和尺寸及其他窗口属性的方法。
[此贴子已经被作者于 2005-11-26 3:12:42编辑过]
-- 作者:admin
-- 发布时间:2005-11-26 3:13:00
--
这确实是个很好的开头,但它没有为我们提供一些处理图形和筛选器的最重要的接口。例如,手动构造图形比较常用的接口之一,IGraphBuilder 接口,并没有包括在内。表示特定筛选器实例和提供对其针访问的IBaseFilter 接口也没有包括在内。下表列出了在本文中要完成图形需要访问的主要接口:
-- 作者:admin
-- 发布时间:2005-11-26 3:15:00
--
接口
描述
IBaseFilter
提供用于控制筛选器的方法。应用程序可以使用此接口枚举针和查询筛选器信息。
IConfigAsfWriter2
提供用于获取和设置 WM ASF Writer 筛选器写文件要使用的高级流格式(Advanced Streaming Format,ASF)配置文件的方法和用于支持 Windows Media Format 9 Series SDK 中的新功能(例如双向编码和对反交错视频的支持)的方法。
IFileSinkFilter
在将媒体流写入文件的筛选器上实现。
IFileSourceFilter
在从文件读媒体流的筛选器上实现。
IGraphBuilder
提供方法来支持应用程序构建筛选器图形。
IMediaControl
提供方法来控制数据流经筛选器图形的流程。它包括用于运行、暂停和停止图形的方法。
IMediaEvent
包含用于检索事件通知和重写筛选器图形管理器的默认事件处理的方法。
IMediaSeeking
包含用于查询当前位置和查找流中的特定位置的方法。
IWmProfileManager
用于创建配置文件、加载现有的配置文件和保存配置文件。
另外,我还需要显式实例化各个 COM 类,下面展示了其中最重要的一些类,以及它们的类 ID 和对每个类的描述:
类
类 ID
描述
筛选器图形管理器
E436EBB3-524F-11CE-9F53-0020AF0BA770
构建和控制筛选器图形。此对象是 DirectShow 中的中心组件。
Decrypter/Detagger 筛选器
C4C4C4F2-0049-4E2B-98FB-9537F6CE516D
有条件地解密由 Encrypter/Tagger 筛选器加密的示例。输出类型与 Encrypter/Tagger 筛选器接收到的原始输入类型相匹配。
WM ASF Writer 筛选器
7C23220E-55BB-11D3-8B16-00C04FB6BD3D
接受数量可变的输入流并创建高级流格式 (ASF) 文件。
-- 作者:admin
-- 发布时间:2005-11-26 3:16:00
--
正如 Eric Gunnerson 在关于 DirectShow 和 C# 的 his blog entry 中指出的,一种快捷简便的导入接口的方法是使用 DirectX SDK 附带的 DirectShow 接口定义语言(Interface Definition Language,IDL)文件。这些文件包含了 COM 接口定义,我对其中的大部分接口都很感兴趣。我可以创建自己的 IDL 文件(它的创作是为了产生一个类型库),然后通过 Microsoft 接口定义语言 (MIDL) 编译器 (midl.exe) 运行它。这将产生一个类型库,然后我再使用 .NET Framework tool Type Library Importer (tlbimp.exe) 将它转换成托管程序集。
遗憾的是,Eric 也指出,它不是一个完美的解决方案。首先,随 DirectX SDK 附带的 IDL 文件并没有描述我需要的所有接口,例如 IMediaEvent 和 IMediaControl 。其次,即使我需要的所有接口都描述了,但通常需要对 interop 签名的创建进行更多控制,而不只是 tlbimp.exe 所提供的控制。例如,如果在图形运行完成之前用户指定的时间到期,则 IMediaEvent.WaitForCompletion (本文后面将会介绍)会返回一个 E_ABORT HRESULT;它将转换成在 .NET 中引发的异常,如果您在轮询循环中要频繁调用 WaitForCompletion (我就打算这样做),则这样做就不合适。另外,IDL 类型和托管类型之间并不是一对一的映射;实际上,存在这样的情况,类型可能根据使用它的上下文不同而进行不同的封送处理。例如,在 DirectX SDK 的 axcore.idl 文件中,IEnumPins 接口公开了以下方法:
HRESULT Next( [in] ULONG cPins, // Retrieve this many pins. [out, size_is(cPins)] IPin ** ppPins, // Put them in this array. [out] ULONG * pcFetched // How many were returned?);
当它编译成类型库并由 tlbimp.exe 进行转换时,产生的程序集包含以下方法:
void Next( [In] uint cPins, [Out, MarshalAs(UnmanagedType.Interface)] out IPin ppPins, [Out] out uint pcFetched);
虽然非托管的 IEnumPins::Next 可以被任何正整数值的 cPins 调用,但如果调用托管版本用的 cPins 值不是 1,则会产生错误,因为 ppPins 不是 IPin 实例数组,而是单个 IPin 实例的引用。
-- 作者:admin
-- 发布时间:2005-11-26 3:16:00
--
基于所有这些原因,以及 DirectShow 接口相对简单,我选择手动用 C# 实现 COM 接口 interop 定义;虽然这需要的工作更多,但它可以让您最好地控制封送内容、方式和时间(不过,请注意,在创建这些手动编码的 interop 定义时,采用 tlbimp.exe 生成的 MSIL 是一个很好的起点,或者更好的方式 — 采用这些导入类型库的反编译 C# 实现,可以使用 Lutz Roeder 的 .NET 发送程序生成它,这个程序可以从 http://www.aisto.com/roeder/dotnet/ 获得)。在与本文有关的代码下载中,您会发现我在本文中使用的每个非托管 DirectShow 接口都有手动编码的 C# 接口。举个例子,下面是前面讨论的 IGraphBuilder 接口的 C# 实现:
[ComImport][Guid("56A868A9-0AD4-11CE-B03A-0020AF0BA770")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IGraphBuilder{ void AddFilter([In] IBaseFilter pFilter, [In, MarshalAs(UnmanagedType.LPWStr)] string pName); void RemoveFilter([In] IBaseFilter pFilter); IEnumFilters EnumFilters(); IBaseFilter FindFilterByName( [In, MarshalAs(UnmanagedType.LPWStr)] string pName); void ConnectDirect([In] IPin ppinOut, [In] IPin ppinIn, [In, MarshalAs(UnmanagedType.LPStruct)] AmMediaType pmt); void Reconnect([In] IPin ppin); void Disconnect([In] IPin ppin); void SetDefaultSyncSource(); void Connect([In] IPin ppinOut, [In] IPin ppinIn); void Render([In] IPin ppinOut); void RenderFile( [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFile, [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrPlayList); IBaseFilter AddSourceFilter( [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFileName, [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFilterName); void SetLogFile(IntPtr hFile); void Abort(); void ShouldOperationContinue();}
然后就可以通过我的 IGraphBuilder 接口来转换和使用筛选器图形管理器组件的实例。那么,如何获取筛选器图形管理器组件的实例呢?我使用了如下代码:
public class ClassId{ public static readonly Guid FilterGraph = new Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770"); public static readonly Guid WMAsfWriter = new Guid("7C23220E-55BB-11D3-8B16-00C04FB6BD3D"); public static readonly Guid DecryptTag = new Guid("C4C4C4F2-0049-4E2B-98FB-9537F6CE516D"); ... public static object CoCreateInstance(Guid id) { return Activator.CreateInstance(Type.GetTypeFromCLSID(id)); }}
在这个包装就位后,我就可以创建筛选器图形管理器的实例,配置能够播放 DVR-MS 文件的筛选器图形,以及播放文件,总共只需要五行代码:
object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);((IGraphBuilder)filterGraph).RenderFile(pathToDvrmsFile);((IMediaControl)filterGraph).Run();EventCode status;((IMediaEvent)filterGraph).WaitForCompletion(Timeout.Infinite, out status);
既然我们知道如何通过托管代码使用 DirectShow,现在我们就来看看如何利用它做一些很酷的事情。
-- 作者:admin
-- 发布时间:2005-11-26 3:17:00
--
如果 Internet 搜索引擎能提供任何线索,则人们对 DVR-MS 文件想做的最流行的一件事情就是将它们转换成 Windows Media Video 文件。通过目前我们为处理 DVR-MS 文件和 DirectShow 而创建的框架,这个任务是很容易实现的。简单地说,我需要做的就是创建一个使用 DVR-MS 源筛选器和 WM ASF Writer 筛选器接收器(它编码并写出 WMV 文件)的图形,并在它们之间建立适当的筛选器和连接。我故意对这些中间筛选器含糊其词,因为我可以让 Intelligent Connect 替我查找和插入它们。作为说明手动进行此操作的简单性的例子,我们按照以下简单步骤在 GraphEdit 中创建适当的转换图形:
1.
打开 GraphEdit。
2.
从“Graph”菜单中选择“Insert Filters”,插入一个 DirectShow WM ASF Writer 筛选器。当提示输入一个输出文件名时,请输入目标文件的名称,以 .wmv 为扩展名。
3.
从“File”菜单中选择“Render Media File”,并在弹出的“Open File”对话框中选择输入的 DVR-MS 文件(再次提醒,您很可能需要将筛选器文件扩展名更改为“All Files”而不是“All Media Files”)。
GraphEdit 将使用该图形的 RenderFile 方法来为 DVR-MS 文件添加一个源筛选器,并通过需要的一系列中间筛选器将它连接到适当的呈现程序。由于以上操作发生时 WM ASF Writer 筛选器接收器已经在图形中,因此使用 Intelligent Connect 的 RenderFile 会将流发送到该筛选器接收器上,而不是插入新的默认呈现程序筛选器。您应该能看到如图 8 所示的图形。
图 8. 将 DVR-MS 编码转换为 WMV 的图形
以编程方式进行这种转换是非常简单的,可以通过以下代码实现:
// Get the filter graphobject filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);DisposalCleanup.Add(filterGraph);IGraphBuilder graph = (IGraphBuilder)filterGraph;// Add the ASF writer and set the output nameIBaseFilter asfWriterFilter = (IBaseFilter) ClassId.CoCreateInstance(ClassId.WMAsfWriter);DisposalCleanup.Add(asfWriterFilter);graph.AddFilter(asfWriterFilter, null);IFileSinkFilter sinkFilter = (IFileSinkFilter)asfWriterFilter;sinkFilter.SetFileName(OutputFilePath, null);// Render the DVR-MS file and run the graphgraph.RenderFile(InputFilePath, null);RunGraph(graph, asfWriterFilter);
先创建一个筛选器图形,将 WM ASF Writer 筛选器添加到其中并配置为指向适当的输出文件路径,然后将 DVR-MS 文件添加到该图形中并使用图形的 RenderFile 方法来呈现。遗憾的是,这在控制 WMV 文件编码方式上并没有提供很多灵活性。为了做到这一点,我们需要用一个配置文件配置 WM ASF Writer,这可以通过在调用RenderFile 之前插入以下代码来完成:
// Set the profile to be used for conversionif (_profilePath != null){ // Load the profile XML contents string profileData; using(StreamReader reader = new StreamReader(File.OpenRead(_profilePath))) { profileData = reader.ReadToEnd(); } // Create an appropriate IWMProfile from the data IWMProfileManager profileManager = ProfileManager.CreateInstance(); DisposalCleanup.Add(profileManager); IntPtr wmProfile = profileManager.LoadProfileByData(profileData); DisposalCleanup.Add(wmProfile); // Set the profile on the writer IConfigAsfWriter2 configWriter = (IConfigAsfWriter2)asfWriterFilter; configWriter.ConfigureFilterUsingProfile(wmProfile); }
这段代码假定配置文件 PRX 文件的路径已经存储在字符串成员变量 _profilePath 中。首先,使用System.IO.StreamReader 将该配置文件的 XML 内容读到一个字符串中。然后创建 Windows Media Profile Manager(通过 IWMProfileManager 接口访问),并使用该管理器的 LoadProfileByData 方法将配置文件加载到其中。这为我们提供了一个指向所加载的配置文件的接口指针,可以用它来配置 WM ASF Writer 筛选器。WM ASF Writer 筛选器实现了 IConfigAsfWriter2 接口,它提供了 ConfigureFilterUsingProfile 方法,这个方法可以根据接口指针指定的配置文件配置编写器。
创建和配置好图形之后,剩下的工作就是运行它,我是使用特意指定的 RunGraph 方法实现的。该方法首先获取指定图形的 IMediaControl 和 IMediaEvent 接口。它还试图获取可用于跟踪源 DVR-MS 文件处理进度的IMediaSeeking 接口。然后使用 IMediaControl 接口来运行图形,从此时开始,方法中的剩余代码仅仅是用来跟踪转换的处理进度。在图形结束运行前,代码会不断轮询 IMediaEvent.WaitForCompletion 方法,如果等待时间已到但图形还没完成运行,则该方法将返回状态代码 EventCode.None (0x0)。如果发生这种情况,则会使用 IMediaSeeking 接口来查询已经处理多少 DVR-MS 文件以及该文件的持续时间,由此我可以计算文件处理的百分比。
当图形最终完成运行时,IMediaEvent.WaitForCompletion 会返回 EventCode.Complete (0x1),并使用IMediaControl.Stop 来停止图形。
-- 作者:admin
-- 发布时间:2005-11-26 3:17:00
--
protected void RunGraph( IGraphBuilder graphBuilder, IBaseFilter seekableFilter){ IMediaControl mediaControl = (IMediaControl)graphBuilder; IMediaEvent mediaEvent = (IMediaEvent)graphBuilder; IMediaSeeking mediaSeeking = seekableFilter as IMediaSeeking; if (!CanGetPositionAndDuration(mediaSeeking)) { mediaSeeking = graphBuilder as IMediaSeeking; if (!CanGetPositionAndDuration(mediaSeeking)) mediaSeeking = null; } using(new GraphPublisher(graphBuilder, Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")) { mediaControl.Run(); try { OnProgressChanged(0); bool done = false; while(!CancellationPending && !done) { EventCode statusCode = EventCode.None; int hr = mediaEvent.WaitForCompletion( PollFrequency, out statusCode); switch(statusCode) { case EventCode.Complete: done = true; break; case EventCode.None: if (mediaSeeking != null) { ulong curPos = mediaSeeking.GetCurrentPosition(); ulong length = mediaSeeking.GetDuration(); double progress = curPos * 100.0 / (double)length; if (progress > 0) OnProgressChanged(progress); } break; default: throw new DirectShowException(hr, null); } } OnProgressChanged(100); } finally { mediaControl.Stop(); } }}
简单吧?DirectShow 是一项令人惊讶的技术。这段代码允许您将非 DRM/'d、NTSC、存储在 DVR-MS 文件中的 SD 内容转换成 WMV 文件。如果您检查本文代码下载中的文件,正如您将看到的,我已将此函数编码到一个名为Converter 的抽象基类中。一个派生类(在本例中为 WmvConverter )构建合适的图形,然后调用基类的RunGraph 方法。另外,Converter 还公开了可用于配置、监视和暂停图形流程的属性和事件,正如您在以下部分将看到的,Converter 公开了使调试图形变得更加简单的功能。
-- 作者:admin
-- 发布时间:2005-11-26 3:19:00
--
您将在 RunGraph 方法中看到,图形是在如下所示的 using 块内部运行的:
using(new GraphPublisher(graphBuilder, Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")){ ... // run the graph}
我这里使用的 GraphPublisher 类是一个自定义类,它是我为帮助调试图形而编写的。它有两个用途。第一,如果在 GraphPublisher 的构造函数的第二个参数中指定了一个文件路径,则它会将 graphBuilder 对象所表示的图形保存到该文件中(该文件应该使用 .grf 扩展名)。随后 GraphEdit 可以打开此文件,从而让您查看整个图形,如同它在发布时出现的样子。这个功能可以通过筛选器图形管理器的 IPersistStream 接口实现来使用:
private const ulong STGM_CREATE = 0x00001000L;private const ulong STGM_TRANSACTED = 0x00010000L;private const ulong STGM_WRITE = 0x00000001L;private const ulong STGM_READWRITE = 0x00000002L;private const ulong STGM_SHARE_EXCLUSIVE = 0x00000010L;[DllImport("ole32.dll", PreserveSig=false)]private static extern IStorage StgCreateDocfile( [MarshalAs(UnmanagedType.LPWStr)]string pwcsName, [In] uint grfMode, [In] uint reserved);private static void SaveGraphToFile(IGraphBuilder graph, string path){ using(DisposalCleanup dc = new DisposalCleanup()) { string streamName = "ActiveMovieGraph"; IPersistStream ps = (IPersistStream)graph; IStorage graphStorage = StgCreateDocfile(path, (uint)(STGM_CREATE | STGM_TRANSACTED | STGM_READWRITE | STGM_SHARE_EXCLUSIVE), 0); dc.Add(graphStorage); UCOMIStream stream = graphStorage.CreateStream( streamName, (uint)(STGM_WRITE | STGM_CREATE | STGM_SHARE_EXCLUSIVE), 0, 0); dc.Add(stream); ps.Save(stream, true); graphStorage.Commit(0); }}
然而,GraphPublisher 的主要目的和它在 using 块中使用的原因是将实时图形发布到 GraphEdit。GraphEdit 允许您连接到另一个流程所公开的远程图形,只要该图形已经发布到运行中对象表 (ROT) — 一个用作跟踪运行对象的全局可访问的查找表。GraphEdit 不仅可以让您在另一个流程中查看和检查一个实时筛选器图形,它还常常允许您对其加以控制。
该图形发布到 ROT 是使用以下代码完成的:
private class RunningObjectTableCookie : IDisposable{ private int _value; private bool _valid; internal RunningObjectTableCookie(int value) { _value = value; _valid = true; } ~RunningObjectTableCookie() { Dispose(false); } public void Dispose() { GC.SuppressFinalize(this); Dispose(true); } private void Dispose(bool disposing) { if (_valid) { RemoveGraphFromRot(this); _valid = false; _value = -1; } } internal bool IsValid { get { return _valid; } set { _valid = value; } }}private static RunningObjectTableCookie AddGraphToRot( IGraphBuilder graph){ if (graph == null) throw new ArgumentNullException("graph"); UCOMIRunningObjectTable rot = null; UCOMIMoniker moniker = null; try { // Get the ROT rot = GetRunningObjectTable(0); // Create a moniker for the graph int pid; using(Process p = Process.GetCurrentProcess()) pid = p.Id; IntPtr unkPtr = Marshal.GetIUnknownForObject(graph); string item = string.Format("FilterGraph {0} pid {1}", ((int)unkPtr).ToString("x8"), pid.ToString("x8")); Marshal.Release(unkPtr); moniker = CreateItemMoniker("!", item); // Registers the graph in the running object table int cookieValue; rot.Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, graph, moniker, out cookieValue); return new RunningObjectTableCookie(cookieValue); } finally { // Releases the COM objects if (moniker != null) while(Marshal.ReleaseComObject(moniker)>0); if (rot != null) while(Marshal.ReleaseComObject(rot)>0); }}private static void RemoveGraphFromRot(RunningObjectTableCookie cookie){ if (!cookie.IsValid) throw new ArgumentException("cookie"); UCOMIRunningObjectTable rot = null; try { // Get the running object table and revoke the cookie rot = GetRunningObjectTable(0); rot.Revoke(cookie.Value); cookie.IsValid = false; } finally { if (rot != null) while(Marshal.ReleaseComObject(rot)>0); }}private const int ROTFLAGS_REGISTRATIONKEEPSALIVE = 1;[DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]private static extern UCOMIRunningObjectTable GetRunningObjectTable( int reserved);[DllImport("ole32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, PreserveSig=false)]private static extern UCOMIMoniker CreateItemMoniker( [In] string lpszDelim, [In] string lpszItem);
在其构造函数中,GraphPublisher 使用 AddGraphToRot 将图形添加到 ROT 中,并存储产生的 cookie。在其IDisposable.Dispose 方法中,GraphPublisher 通过将存储的 cookie 传递到 RemoveGraphFromRot 来将图形从 ROT 中删除。
-- 作者:admin
-- 发布时间:2005-11-26 3:21:00
--
当资源使用完毕后,尽早将它们释放是非常重要的。当使用处理大量音频和视频资源的 DirectShow COM 对象时,这一点尤其重要。可以使用 Marshal.ReleaseComObject 方法来强制处置 COM 对象,此方法会减少所提供的运行时可调用包装的引用计数。当引用数到达零时,运行库会释放它在非托管 COM 对象上的所有引用。(有关 Marshal.ReleaseComObject 的更多信息,请参见该方法的 MSDN 文档 。)对于使用的每个 COM 对象,我不是将我的代码随便放在 try/finally 块中,而是创建一个名为 DisposalCleanup 的助手类,它可以简化 COM 对象的生存期管理:
public class DisposalCleanup : IDisposable{ private ArrayList _toDispose = new ArrayList(); public void Add(params object [] toDispose) { if (_toDispose == null) throw new ObjectDisposedException(GetType().Name); if (toDispose != null) { foreach(object obj in toDispose) { if (obj != null && (obj is IDisposable || obj.GetType().IsCOMObject || obj is IntPtr)) { _toDispose.Add(obj); } } } } void IDisposable.Dispose() { if (_toDispose != null) { foreach(object obj in _toDispose) EnsureCleanup(obj); _toDispose = null; } } private void EnsureCleanup(object toDispose) { if (toDispose is IDisposable) { ((IDisposable)toDispose).Dispose(); } else if (toDispose is IntPtr) // IntPtrs must be interface ptrs { Marshal.Release((IntPtr)toDispose); } else if (toDispose.GetType().IsCOMObject) { while (Marshal.ReleaseComObject(toDispose) > 0); } }}
这里一个重要的方法是 EnsureCleanup ,它是通过 DisposalCleanup 的 IDisposable.Dispose 方法调用的。通过使用其 Add 方法来调用添加到 DisposalCleanup 中的每个对象,EnsureCleanup 调用了一个IDisposable 对象上的 Dispose 、一个 COM 对象上的 Marshal.ReleaseComObject 和一个接口指针上的Marshal.Release 。通过这些,我的代码只需将使用许多 COM 对象的代码块放在一个创建了新的DisposalCleanup 的 using 块中,将任何 COM 对象或接口添加到 DisposalCleanup 实例中,并在 using 块结束时调用 DisposalCleanup 的 IDisposable.Dispose 方法来释放所有使用过的资源。我的 Converter 基类实现了此方案,并通过一个受保护的 DisposalCleanup 属性公开了构造的 DisposalCleanup 。
public object Convert() { _cancellationPending = false; try { object result; using(_dc = new DisposalCleanup()) { // Do the actual work result = DoWork.(); } OnConversionComplete(null, result); return result; } catch(DirectShowException exc) { OnConversionComplete(exc, null); throw; } catch(Exception exc) { exc = new DirectShowException(exc); OnConversionComplete(exc, null); throw exc; } catch { OnConversionComplete(new DirectShowException(), null); throw; }}private DisposalCleanup _dc;protected DisposalCleanup DisposalCleanup { get { return _dc; } }
DoWork. 方法是抽象方法,如果是 WmvConverter 类,它可以构建筛选器图形并调用 RunGraph 方法。通过这种方式,派生类可以实现 DoWork. 并简单地向基类的 DisposalCleanup 中添加可处置的对象;当派生类的工作执行完毕后,即使它引发异常,基类也会自动处置这些资源。
-- 作者:admin
-- 发布时间:2005-11-26 3:21:00
--
显而易见,通过前面讨论的代码,您可以编写功能丰富的应用程序来处理 DVR-MS 文件并将其转换成 WMV 文件。但据我所见,此功能最常见的请求是作为 Media Center-集成解决方案的一部分。由此创建了许多非常有用的解决方案,其中最著名的有 Dan Giambalvo 创建的 dCut (可通过 http://www.inseattle.org/~dan/Dcut.htm 下载)以及 Alex Seigler、José Peña、James Edelen 和 Jeff Griffin 创建的 DVR 2 WMV(可通过http://www.thegreenbutton.com/downloads.aspx 下载)。这两个应用程序都依赖于 Alex Siegler 编写的 dvr2wmv DLL(使用的技术与本文所介绍的非常类似,不过采用的是非托管代码)。这些应用程序不懈努力地尝试集成到 Media Center 中,更具体地说是模仿 Media Center 外壳的外观,但遗憾的是,目前的 Media Center SDK 只允许做到这么多。幸运的是,SDK 有另一个相对未开发的区域,它使这种功能可以轻松地集成到 Media Center UI 中,但仍然保留 Media Center 团队已编写的所有烙印:ListMaker 外接程序。
ListMaker 外接程序是由第三方提供的托管组件,它运行在 Media Center 进程内,使用 Microsoft.MediaCenter.dll 程序集公开的 API 元素(您可以在 Media Center 系统的 %windir%//ehome 目录下找到此 DLL)。ListMaker 外接程序的工作非常简单:它的目的是获取 Media Center 提供给它的文件列表,并对该列表进行一些操作(进行什么操作取决于该外接程序)。Media Center 已将它构建到 UI 中以处理列表生成和随外接程序处理列表时的报告而显示的进程更新。很酷的一点是 Media Center 并不在意该外接程序对媒体列表进行了什么操作。因此,您可以编写这样一个外接程序,它将用户选定的每个 DVR-MS 文件转换成 WMV,并将它们写到硬盘的一个文件夹中。更明确地说,我拥有这样的外接程序(图 9),下面我将向您介绍如何实现。
图 9. WMV Transcoder 外接程序
首先,ListMaker 外接程序必须从 System.MarshalByRefObject 派生,如同所有用于 Media Center 的外接程序那样(遗憾的是,SDK 文档目前没有提到这一点,但是这一点非常重要)。Media Center 将所有外接程序加载到一个独立的应用程序域中,这意味着它使用 .NET Remoting 基础结构跨应用程序域边界访问该外接程序。MarshalByRefObject 类能实现这一目的,它允许跨应用程序域边界访问对象,因此外接程序必须以它为基类。如果您忘记从 MarshalByRefObject 派生,则您的外接程序将无法正确加载或运行。
除了从 MarshalByRefObject 派生外,ListMaker 外接程序还实现了两个来自 Microsoft.MediaCenter.dll 程序集的主要接口:Microsoft.MediaCenter.AddIn.IAddInModule 和Microsoft.MediaCenter.AddIn.ListMaker.ListMaker :
public class WmvTranscoderPlugin : MarshalByRefObject, IAddInModule, ListMakerApp, IBrandInfo{ ...}
所有 Media Center 外接程序都实现了 IAddInModule ,IAddInModule 通过实现 IAddInModule.Initialize 和IAddInModule.Uninitialize 方法来初始化和处置要运行的代码。在许多情况下,初始化阶段需要做的事情非常少;对于我的外接程序,我只需查看一下注册表,找到用户首选项,例如经过编码转换的文件应该写到哪个磁盘(注册表中 HKLM//Software//Toub//WmvTranscoderPlugin 项的 PreferredDrive 值)以及应该使用哪个 Windows Media 配置文件来将代码转换为 WMV(注册表中的 HKLM//Software//Toub//WmvTranscoderPlugin 项的 ProfilePath 值)。如果没有指定驱动器(或者指定的驱动器无效),则我将默认值设置为从System.IO.Directory.GetLogicalDrives 返回的第一个有效的驱动器,其中,有效的驱动器定义为 Win32GetDriveType 函数声明的固定驱动器中的任何一个驱动器。
ListMakerApp 是列表的主要接口,用于处理和服务双重目的:允许用户选择要处理的媒体文件集(图 10)并启动外接程序的处理,在这之后它允许 Media Center UI 报告进度(图 11)。
图 10. 选择要进行编码转换的节目
图 11. Media Center 外壳中的进度更新
前者涉及的成员并不令人非常满意,所以我不想花太多时间介绍它们。从根本上说,Media Center 通过此接口调用外接程序以获取如选择多少 DVR-MS 文件、还能添加多少文件之类的信息,并在每次用户更改要处理的列表项时调用它。它的核心部分是由三个方法处理的:
public void ItemAdded(ListMakerItem item){ _itemsUsed++; _bytesUsed += item.ByteSize; _timeUsed += item.Duration;}public void ItemRemoved(ListMakerItem item){ _itemsUsed--; _bytesUsed -= item.ByteSize; _timeUsed -= item.Duration;}public void RemoveAllItems(){ _itemsUsed = 0; _bytesUsed = 0; _timeUsed = TimeSpan.FromSeconds(0);}
然后通过其他属性和方法(如下所示)公开捕获的信息:
public TimeSpan TimeUsed { get { return _timeUsed; } }public int ItemUsed { get { return _itemsUsed; } }public long ByteUsed { get { return _bytesUsed; } }public TimeSpan TimeCapacity { get { return TimeSpan.MaxValue; } } public int ItemCapacity { get { return int.MaxValue; } } public long ByteCapacity { get { return (long)GetFreeSpace(_selectedDrive); } }
Used 方法只是返回上述方法所维护的计数值。TimeCapacity 和 ItemCapacity 属性同时返回其类型各自的MaxValue 值,因为计算实际用时和实际可用的项数远远超出了本文的讨论范围。ByteCapacity 使用我的私有GetFreeSpace 方法(再次说明,它只是 Win32 GetDiskFreeSpaceEx 函数的一个 p/invoke 包装)来返回磁盘中的可用空间;当然,在与 ByteUsed 配合时这个值也没有什么用处,因为 ByteUsed 表示的是 DVR-MS 文件的大小,而 ByteCapacity 则用于确定磁盘中是否有空间来存放这些文件,但输出文件却是压缩过的 WMV 文件。不过这个实现细节您应该能够自如地进行更改。
我还将介绍三个更加重要但实现很简单的属性:
public MediaType SupportedMediaTypes { get { return MediaType.RecordedTV; } } public bool OrderIsImportant { get { return true; } }public IBrandInfo BrandInfo { get { return this; } }
SupportedMediaTypes 返回一个加标记的枚举,列出此外接程序支持的媒体类型:可能的类型包括图片、视频、音乐和录制的电视等,Media Center 通常支持所有这些媒体类型。然而,由于此外接程序的主要作用是将 DVR-MS 文件转换成 WMV 文件,因此我将其实现为只从 SupportedMediaTypes 返回MediaType.RecordedTV 。
Media Center 使用 OrderIsImportant 来确定是否应该允许用户对要处理的录制节目列表重排序。虽然顺序对此外接程序来说并不是真的很重要(因为它只是将文件写到硬盘中),但我想让用户安排某些特定节目在其他节目之前转换(图 12),所以我从这个属性返回 true 而不是 false。
图 12. 对选定的节目重排序
BrandInfo 属性允许外接程序的作者修改 Media Center 显示的 UI 以便包含特定于产品的信息。该属性返回一个实现 IBrandInfo 接口的对象。为简单起见,我只在我的外接程序中实现该接口并返回对该外接程序对象自身的引用:
public class WmvTranscoderPlugin : MarshalByRefObject, IAddInModule, ListMakerApp, IBrandInfo{ ... public IBrandInfo BrandInfo { get { return this; } } ... public string ViewListPageTitle { get { return "Files to transcode"; } } public string SaveListButtonTitle { get { return "Transcode"; } } public string PageTitle { get { return "Transcode to WMV"; } } public string CreatePageTitle { get { return "Specify target folder"; } } public string ViewListButtonTitle { get { return "View List"; } } public string ViewListIcon { get { return null; } } public string MainIcon { get { return null; } } public string StatusBarIcon { get { return null; } } ...}
IBrandInfo 的八个属性被分成两类:呈现在 UI 中的文本字符串和指定磁盘中图形位置的路径字符串。如果一个属性返回 null,则使用默认值。这样,由于我现在的图形艺术水平还有些欠缺,因此对所有图标属性我都返回 null。这些属性在 UI 中出现的位置如下表所示:
-- 作者:admin
-- 发布时间:2005-11-26 3:22:00
--
属性
描述
PageTitle
当外接程序使用时显示在右上角的文本。
CreatePageTitle
列表创建页面的标题文本。
SaveListButtonTitle
用于在列表创建之后启动处理操作的按钮上的文本。
ViewListButtonTitle
用于查看要复制以进行处理的媒体项的按钮上的文本。
ViewListPageTitle
列表查看页面的标题文本。
MainIcon
包含要作为列表生成页面上主图标(水印)使用的图标的文件路径。
StatusBarIcon
包含 Media Center 放在生成页面左下角的图标的文件路径。
ViewListIcon
Media Center 放在列表查看页面顶部的图标文件的路径。
-- 作者:admin
-- 发布时间:2005-11-26 3:23:00
--
ListMakerApp 上最有趣的方法是 Launch 和 Cancel 。一旦用户创建了要处理的文件列表并单击按钮开始处理,Media Center 就会调用 Launch 方法,它提供三个参数:用户选择的录制节目列表、可被调用以通知 Media Center 状态更新的进程更新委托和应该调用以通知 Media Center 处理完成(成功或因某种异常情况)的完成委托。Launch 方法的作用是立即返回并在后台线程中执行实际的工作。当用户选择取消处理时就会调用 Cancel方法,然后由外接程序停止和终止其操作。
WmvTranscoderPlugin 的实现遵循这种模式:将 Launch 的参数存储到成员变量中,然后将执行实际转换工作的 ConvertToWmv 方法排入 ThreadPool 队列中:
public void Launch(ListMakerList lml, ProgressChangedEventHandler pce, CompletionEventHandler ce){ _listMakerList = lml; _progressChangedHandler = pce; _completedHandler = ce; _cancellationPending = false; ThreadPool.QueueUserWorkItem(new WaitCallback(ConvertToWmv), null);}private void ConvertToWmv(object ignored){ ThreadPriority oldThreadPriority = Thread.CurrentThread.Priority; Thread.CurrentThread.Priority = ThreadPriority.Lowest; try { DirectoryInfo outDir = Directory.CreateDirectory( _selectedDrive + ":////" + _listMakerList.ListTitle); _currentConvertingIndex = 0; foreach(ListMakerItem item in _listMakerList.Items) { if (_cancellationPending) break; string dvrMsName = item.Filename; string wmvName = outDir.FullName + "////" + item.Name + ".wmv"; _currentConverter = new WmvConverter( dvrMsName, wmvName, _profilePath); _priorCompletedPercentage = _currentConvertingIndex / (float)_listMakerList.Count; _currentConverter.PollFrequency = 2000; _currentConverter.ProgressChanged += new ProgressChangedEventHandler(ReportChange); _currentConverter.Convert(); _currentConverter = null; _currentConvertingIndex++; } _completedHandler(this, new CompletionEventArgs()); } catch(Exception exc) { _completedHandler(this, new CompletionEventArgs(exc)); } finally { Thread.CurrentThread.Priority = oldThreadPriority; }}
ConvertToWmv 在选定的驱动器上创建一个目录,使用用户指定的目标文件夹的名称(参见图 13)。然后该方法循环访问所提供的 ListMakerList 中的所有 ListMakerItem 对象,获取 DVR-MS 文件的路径并使用前面构建的 WmvConverter 来将目标目录中的每个 DVR-MS 文件转换成 WMV 文件。Converter 的ProgressChanged 事件关联到外接程序中的一个私有方法 — ReportChange 上,然后由该方法调用 Media Center 的进程更新委托。另外,当前转换程序存储在一个成员变量中,因而可以使用 Cancel 方法来停止其进程
-- 作者:admin
-- 发布时间:2005-11-26 3:23:00
--
Cancel 方法也非常简单。它设置了一个成员变量,用于警告在另一个线程中运行的 ConvertToWmv 方法,通知它用户已经请求取消。然而,正如您在 ConvertToWmv 方法中看到的,只有当该方法准备转换下一个 DVR-MS 文件时才会对此进行检查,所以 Cancel 方法还使用存储在一个成员变量中的 WmvConverter 对象,使用该 Converter 的 CancelAsync 方法取消当前执行的转换。正如我们前面所看到的,这将导致Converter.RunGraph 方法从 WaitForCompletion 方法返回后即刻停止。
public void Cancel(){ // Cancel any pending conversions _cancellationPending = true; // Cancel the current conversion WmvConverter converter = _currentConverter; if (converter != null) converter.CancelAsync();}
我在本文的下载中包含了此外接程序的一个完整的工作实现,包括一个安装程序。该安装程序同时将WmvTranscoderPlugin 的程序集和 WmvConverter 的程序集安装到全局程序集缓存 (GAC) 中,然后使用 RegisterMceApp.exe 工具来将此外接程序通知 Media Center。注册应用程序依赖于一个 XML 配置文件,如下所示:
您应该能够运行安装程序并直接通过一个我们都不必编写的非常时髦的 UI 来将 DVR-MS 立即转换成 WMV。(感谢你,Media Center 团队!)
图 14. 成功的编码转换
返回页首
DVR-MS 文件格式既包含音频、视频和闭合字幕数据,也包含描述文件及其内容的元数据。一旦电视节目录制下来,节目的标题、描述、演员表和原始播放日期等信息就存储在这个位置。很酷的一点是,您的应用程序可以通过 DirectShow StreamBufferRecordingAttribute 对象实现的 IStreamBufferRecordingAttribute 接口轻松地访问此数据。这个对象可以使用它的 CLSID 来创建,正如我本文中创建其他 DirectShow 对象那样。
要使用 IStreamBufferRecordingAttribute ,首先必须为它提供一个托管接口(您会在本文的代码下载中发现这段代码,它嵌套在 DvrmsMetadataEditor 类中):
-- 作者:admin
-- 发布时间:2005-11-26 3:24:00
--
[ComImport][Guid("16CA4E03-FE69-4705-BD41-5B7DFC0C95F3")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]private interface IStreamBufferRecordingAttribute{ void SetAttribute( [In] uint ulReserved, [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName, [In] MetadataItemType StreamBufferAttributeType, [In, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute, [In] ushort cbAttributeLength); ushort GetAttributeCount([In] uint ulReserved); void GetAttributeByName( [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName, [In] ref uint pulReserved, [Out] out MetadataItemType pStreamBufferAttributeType, [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbAttribute, [In, Out] ref ushort pcbLength); void GetAttributeByIndex ( [In] ushort wIndex, [In, Out] ref uint pulReserved, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszAttributeName, [In, Out] ref ushort pcchNameLength, [Out] out MetadataItemType pStreamBufferAttributeType, [Out, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute, [In, Out] ref ushort pcbLength); [return: MarshalAs(UnmanagedType.Interface)] object EnumAttributes();}
为了访问 DVR-MS 文件的元数据,我构造了一个 StreamBufferRecordingAttribute 对象并获取它的IFileSourceFilter 接口(您在本文前面也看到了相应的 IFileSinkFilter 接口;它们几乎完全相同)。IFileSourceFilter 的 Load 方法可用于打开我对其元数据感兴趣的 DVR-MS 文件,此时可以获取它的IStreamBufferRecordingAttribute 接口并将该接口用于检索和编辑元数据:
public class DvrmsMetadataEditor : MetadataEditor{ IStreamBufferRecordingAttribute _editor; public DvrmsMetadataEditor(string filepath) { IFileSourceFilter sourceFilter = (IFileSourceFilter) ClassId.CoCreateInstance(ClassId.RecordingAttributes); sourceFilter.Load(filepath, null); _editor = (IStreamBufferRecordingAttribute)sourceFilter; } ...}
对元数据的读访问是通过 DvrmsMetadataEditor.GetAttributes 方法提供的,该方法提供了IStreamBufferRecordingAttribute 的 GetAttributeCount 和 GetAttributeByIndex 方法的简单抽象。
public override System.Collections.IDictionary GetAttributes(){ if (_editor == null) throw new ObjectDisposedException(GetType().Name); Hashtable propsRetrieved = new Hashtable(); ushort attributeCount = _editor.GetAttributeCount(0); for(ushort i = 0; i < attributeCount; i++) { MetadataItemType attributeType; StringBuilder attributeName = null; byte[] attributeValue = null; ushort attributeNameLength = 0; ushort attributeValueLength = 0; uint reserved = 0; _editor.GetAttributeByIndex(i, ref reserved, attributeName, ref attributeNameLength, out attributeType, attributeValue, ref attributeValueLength); attributeName = new StringBuilder(attributeNameLength); attributeValue = new byte[attributeValueLength]; _editor.GetAttributeByIndex(i, ref reserved, attributeName, ref attributeNameLength, out attributeType, attributeValue, ref attributeValueLength); if (attributeName != null && attributeName.Length > 0) { object val = ParseAttributeValue( attributeType, attributeValue); string key = attributeName.ToString().TrimEnd(/'//0/'); propsRetrieved[key] = new MetadataItem( key, val, attributeType); } } return propsRetrieved;}
首先,使用 GetAttributeCount 方法来查明要检索的元数据项有多少。然后,对于每个属性,使用GetAttributeByIndex 方法检索属性名的长度和值的长度(以字节为单位)(通过将 name 和 value 参数指定为空值)。当获得长度之后,我就可以创建大小适当的缓冲区来存储数据,并且可以再次调用GetAttributeByIndex 来检索属性的真实名称和字节数组值。如果检索成功,则会根据属性的类型将存储该值的字节数组解析为适当的托管对象。我的 ParseAttributeValue 方法返回 GUID、无符号整型、无符号长整型、无符号短整型、字符串、布尔值或者原始数组(如果值是简单的二进制),这对大多数复杂的元数据属性都是通用的。然后使用该属性的名称及其类型和值构造一个新的 MetadataItem 实例,这个实例将添加到该文件的所有属性的 Hashtable 中。当所有属性都检索完毕时,此集合将返回给用户。
SetAttributes 方法的工作方式则相反。它是随 MetadataItem 对象集合提供的,其中每个对象都根据其类型格式化为适当的字节数组,然后与 SetAttribute 方法一起使用,以便设置文件的元数据属性:
public override void SetAttributes(IDictionary propsToSet){ if (_editor == null) throw new ObjectDisposedException(GetType().Name); if (propsToSet == null) throw new ArgumentNullException("propsToSet"); byte [] attributeValueBytes; foreach(DictionaryEntry entry in propsToSet) { MetadataItem item = (MetadataItem)entry.Value; if (TranslateAttributeToByteArray( item, out attributeValueBytes)) { try { _editor.SetAttribute(0, item.Name, item.Type, attributeValueBytes, (ushort)attributeValueBytes.Length); } catch(ArgumentException){} catch(COMException){} } }}
MetadataItem 是一个属性的名称、值和类型的简单包装。MetadataItemType 是有效类型(GUID、字符串、无符号整型等)的枚举。
-- 作者:admin
-- 发布时间:2005-11-26 3:24:00
--
您可能注意到 DvrmsMetadataEditor 类是从 MetadataEditor 基类派生的。我这样做是为了提供另一个类 —AsfMetadataEditor ,它也是从 MetadataEditor 派生的。AsfMetadataEditor 基于包含在 Windows Media Format SDK(从此处下载 SDK )中的示例代码。它使用 Windows Media IWMMetadataEditor 和IWMHeaderInfo3 接口来获取 WMA 和 WMV 文件(这两者都基于 ASF 文件格式)的相关元数据信息。您可能会发现,当前这些 Windows Media Format SDK 接口除了能用于处理 WMA 和 WMV 文件外,还可以处理 DVR-MS 文件,不过将来可能不再这样,而且 Microsoft 强烈建议使用 IStreamBufferRecordingAttribute 接口来处理 DVR-MS 文件。IWMHeaderInfo3 接口的相关部分与 IStreamBufferRecordingAttribute 接口几乎相同,因此 AsfMetadataEditor 类和 DvrmsMetadataEditor 类也极其相似。
在这些类就位后,将元数据从一个媒体文件复制到另一个(例如从 DVR-MS 文件复制到经过代码转换的 WMV 文件)就变得极为简单,从而让您保持与经过编码转换的 TV 录制相关联的元数据的保真度:
using(MetadataEditor sourceEditor = new DvrmsMetadataEditor(srcPath)){ using(MetadataEditor destEditor = new AsfMetadataEditor(dstPath)) { destEditor.SetAttributes(sourceEditor.GetAttributes()); }}
实际上,正是出于从一个媒体文件向另一个媒体文件复制元数据的目的,我在 MetadataEditor 类中创建了一个静态的 MigrateMetdata 方法,这个方法不仅能按上述方式迁移元数据,而且对它加以扩大,这样在 Media Player 中查看 DVR-MS 文件和在 Media Center 中播放 WMV 文件时,就可以显示更多的可用信息。
返回页首
除了转换为 WMV 之外,编辑和拼接 DVR-MS 文件可能是我在网上新闻组中看到的第二个最常请求的功能。许多人没有意识到的是,DirectShow RecComp 对象及其 IStreamBufferRecComp 接口提供了现成的拼接功能。IStreamBufferRecComp 接口用于从现有的录制片段创建新的录制,以及将来自一个或多个 DVR-MS 文件的片段连接在一起。
IStreamBufferRecComp 接口非常简单,它的一个 C# 导入如下所示:
[ComImport][Guid("9E259A9B-8815-42ae-B09F-221970B154FD")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IStreamBufferRecComp{ void Initialize( [In, MarshalAs(UnmanagedType.LPWStr)] string pszTargetFilename, [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecProfileRef); void Append( [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording); void AppendEx( [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording, [In] ulong rtStart, [In] ulong rtStop); uint GetCurrentLength(); void Close(); void Cancel();}
要拼接 DVR-MS 文件,首先要创建 RecComp 对象的实例。这可以通过本文前面介绍的ClassId.CoCreateInstance 方法来完成,代码如下:
IStreamBufferRecComp recCom = (IStreamBufferRecComp)ClassId.CoCreateInstance(ClassId.RecComp)and with ClassId.RecComp defined aspublic static readonly Guid RecComp = new Guid("D682C4BA-A90A-42FE-B9E1-03109849C423");
有了 IStreamBufferRecComp 之后,就可以使用它的 Initialize 方法来为新的录制指定输出文件名。另外,Initialize 的第二个参数应该是要拼接的其中一个 DVR-MS 输入文件的文件路径。IStreamBufferRecComp支持连接来自一个或多个文件的片段,但所有这些文件必须使用相同的配置文件录制,这意味着它们必须使用 Media Center 中的相同配置和设置进行录制。RecComp 需要知道输出文件使用什么配置文件,因此您必须指定一个输入文件作为第二个参数,以便它可以检查其配置文件信息并将该信息作为输出文件的基础。
一旦初始化了 IStreamBufferRecComp ,您就可以开始构建新文件。调用 Append 方法,指定一个 DVR-MS 输入文件的完整路径,则整个文件就会追加到输出文件中。AppendEx 方法允许您指定附加的开始和停止时间,以便只使用输入文件的一部分并将这部分追加到输出文件中。在非托管接口中,这些时间被定义为REFERENCE_TIME — 一个代表以 100 毫微秒为单位的数值的 64 位长整数值,所以在托管代码中,您可以使用如下所示的函数来将秒转换成传递给 AppendEx 的 REFERENCE_TIME 值:
internal static ulong SecondsToHundredNanoseconds(double seconds){ return (ulong)(seconds * 10000000);}
当您完成追加到输出文件时,Close 方法就会关闭输出文件。在您连接到该文件时,可以使用一个单独线程的GetCurrentLength 方法来确定输出文件的当前长度。然后您可以使用此信息和您对输入文件/片段长度的了解来计算完成拼接的百分比。请注意,这个过程非常快,因为将片段从一个 DVR-MS 文件追加到另一个文件并不需要编码和解码。
-- 作者:admin
-- 发布时间:2005-11-26 3:25:00
--
为了演示此接口,我构建了 DVR-MS 编辑器应用程序(如图 15 所示),并将它作为与本文有关的代码下载的一部分。
图 15. DVR-MS 编辑器
这个应用程序其实非常简单,用了一个多小时就实现了。它使用 Windows Media Player ActiveX 控件来显示输入的视频文件。为了加载视频文件,它将 AxWindowsMediaPlayer.URL 属性设置为 DVR-MS 文件的路径,这样可以使 Media Player 加载该视频(如果 AxWindowsMediaPlayer.settings.autoStart 属性为真,它还会开始播放)。
一旦加载了视频,用户就可以使用“Media Player”工具栏对它进行控制,这个工具栏可以使用户完全控制视频的播放和搜索。当到达用户想要开始或停止一段视频的位置时,就会查询AxWindowsMediaPlayer.Ctlcontrols.currentPosition 属性。然后,刚才描述的 IStreamBufferRecComp接口可以使用这些时间来创建输出文件。
另外,Media Player 对视频的当前位置提供了细粒度的编程控制。您可以使用如下所示的代码来逐帧移动视频:
((WMPLib.IWMPControls2)player.Ctlcontrols).step(1);
或者,还可以通过设置刚才讨论的 AxWindowsMediaPlayer.Ctlcontrols.currentPosition 来跳转到视频中的特定位置。
DVR-MS 编辑器应用程序还利用了本文前面描述的一些其他技术,例如将元数据从源视频文件复制到输出视频文件。
返回页首
这是令人惊讶的技术,不是吗?DirectShow 和 Windows XP Media Center Edition 团队为开发人员提供了许多处理 DVR-MS 文件的工具(包括非托管代码的和托管代码的)。通过使用这些工具,可以创建新的应用程序来提供大多数人没有意识到他们能够使用的真正强大的功能。本文所讨论的主题只涉及到您可以用来处理 DVR-MS 文件的各种技术的一部分,而在人们编写的使用这些库和工具的解决方案中,它们所占的比例则甚至更小。我期待着获悉您使用这种功能来开发解决方案。
现在,我要回去看会电视了。
•
Programming Microsoft DirectShow for Digital Video and Television (Microsoft Press, 2003)
•
Fundamentals of Audio and Video Programming for Games (Microsoft Press, 2003)