现在的位置: 首页 > 综合 > 正文

玩转 DVR-MS

2012年11月13日 ⁄ 综合 ⁄ 共 24164字 ⁄ 字号 评论关闭
玩转 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 文件播放 DVR-MS 文件
DirectShow 和 GraphEdit 简介DirectShow 和 GraphEdit 简介
DirectShow 接口DirectShow 接口
将编码转换为 WMV将编码转换为 WMV
调试筛选器图形调试筛选器图形
非托管资源清理非托管资源清理
将 WmvConverter 投入使用: WmvTranscoderPlugin将 WmvConverter 投入使用: WmvTranscoderPlugin
访问 DVR-MS 元数据访问 DVR-MS 元数据
编辑 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。

播放 DVR-MS 文件

谈到视频文件时,播放或许是可以执行的最重要的操作,所以我将从此入手。在您自己的应用程序中可以有多种播放 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,这个问题可以通过订阅 ProcessExited 事件加以解决,而不是用 WaitForExit 方法阻止。

总而言之,该解决方案编码简单方便,但非常不灵活,而且是在应用程序的外部播放视频。它可能只在以下情况下才适用,您想允许用户查看指定的文件,不过是在应用程序不必关心视频内容而且应用程序根本不与视频交互的情况下查看。例如,如果您的应用程序是一个下载代理,而且您想允许用户查看已经复制到本地的视频文件,则可能适合采用这种方式。

由于我们知道 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 中同样的库执行导入)。

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 和 GraphEdit 简介

在本质上,使用 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 文件提供三个流。

第一个是音频流。如果您检查第一个针的针属性(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 接口的一部分)。

这种用于自动构建筛选器图形的机制称为 Intelligent Connect。由于您并不真的需要知道 Intelligent Connect 的具体内容(除非您正在实现自己的筛选器并想让它们可以自动构建图形),因此在这里此主题我不想介绍得太多,而是让您参考 DirectX SDK 中该主题的详细文档。然而,简单地说,RenderFile 方法是一个简单的包装,它包装了 IGraphBuilder 中的另外两个方法:AddSourceFilterRenderRenderFile 首先调用 AddSourceFilter,对于本地文件,它只需在注册表中查找正在播放的文件的扩展名所必需的源筛选器的类型,将适当的筛选器实例添加到筛选器图形中,并对它进行配置以使其指向指定的源文件。对于此源筛选器的每个输出针,RenderFile 再调用 Render 方法,该方法试图查找从此针到图形中的呈现程序的一条路径。如果该针实现了 IStreamBuilder 接口,则 Render 只是委托该实现,将所有细节都交给该筛选器的实现。否则,Render 会试图查找此针可以连接的筛选器。为此,它会查找在图形构建过程前期可能缓存的缓存筛选器,查找已经成为图形的一部分且有未连接的输入针的任何筛选器,并使用 IFilterMapper 接口查找注册表中兼容的筛选器类型。如果找到了一个筛选器,则它会再对这个新的筛选器重复此过程,直到到达呈现筛选器,此时就成功地停止。如果没有找到筛选器,则 Intelligent Connect 构建图形未成功。这就是依赖 Intelligent Connect 的一个缺点:它并非始终有效。另外,如果您的机器上安装了新的筛选器,则 Intelligent Connect 可能会选择这些新的筛选器,而不是您当前期望在应用程序中使用的筛选器。因此,您在设计时可能要选择避免这种情况(我后面将要介绍,如果您确切地知道想在图形中使用哪些筛选器,则显式构建图形而不使用 Intelligent Connect 是很容易的)。

既然您对 DirectShow 已有所了解,我们将要以编程方式使用它,以便对 DVR-MS 文件进行许多很合适的操作。毕竟,一旦 DVR-MS 源筛选器加载到图形中,我们就可以像处理其他音频和视频数据流那样处理来自 DVR-MS 的数据,操作它们的方法是无限的。

DirectShow 接口

然而,我们首先需要的是能够以编程方式处理 DirectShow。对于非托管代码,这可能是立即可行的,因为 SDK 包含了通过 C++ 访问 DirectShow 库所需要的所有头文件。对于托管代码,问题就有些棘手。虽然 Managed DirectX 确实包含前面讨论的 AudioVideoPlayback.dll 库,但该库级别很高,它提供 VideoAudio 级别的抽象,而我们需要的是能够在筛选器和针级别对筛选器图形进行操作。虽然我觉得这个问题将来会得到改善,但至少当前版本的 Managed DirectX 对我们爱莫能助。

quartz.dll 是什么?quartz.dll 的类型库公开了一些我们需要的功能,这里列出所公开接口的完整列表:

接口

描述

IAMCollection

筛选器图形对象集合,例如筛选器或针。

IAMStats

允许应用程序从图形管理器中检索性能数据。筛选器可以使用此接口记录性能数据。

IBasicAudio

允许应用程序控制音频流的音量和平衡。

IBasicVideo

允许应用程序设置视频属性,例如目标矩形和源矩形

IBasicVideo2

从 IBasicVideo 接口派生,为应用程序提供了一个附加方法,通过它可以检索视频流的首选纵横比。

IDeferredCommand

允许应用程序取消或修改该应用程序先前使用 IQueueCommand 接口排入队列的图形-控制命令。

IFilterInfo

管理筛选器的信息并提供访问筛选器和表示筛选器上的针的 IPinInfo 接口。

IMediaControl

提供方法来控制经过筛选器图形的数据流。它包含运行、暂停和停止图形的方法。

IMediaEvent

包含用来检索事件通知和用于重写筛选器图形管理器的默认事件处理的方法。

IMediaEventEx

从 IMediaEvent 派生并添加方法来启用一个应用程序窗口,以便在事件发生时接收消息。

IMediaPosition

包含用于查找流中一个位置的方法。

IMediaTypeInfo

包含用于检索针连接的媒体类型的方法。

IPinInfo

包含用于检索针信息和连接针的方法。

IQueueCommand

允许应用程序预先将图形-控制命令排入队列。

IRegFilterInfo

提供对 Windows 注册表中的筛选器的访问,以及向筛选器图形中添加已注册的筛选器。

IVideoWindow

包含用于设置窗口所有者、窗口的位置和尺寸及其他窗口属性的方法。

这确实是个很好的开头,但它没有为我们提供一些处理图形和筛选器的最重要的接口。例如,手动构造图形比较常用的接口之一,IGraphBuilder 接口,并没有包括在内。表示特定筛选器实例和提供对其针访问的 IBaseFilter 接口也没有包括在内。下表列出了在本文中要完成图形需要访问的主要接口:

接口

描述

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) 文件。

正如 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 文件并没有描述我需要的所有接口,例如 IMediaEventIMediaControl。其次,即使我需要的所有接口都描述了,但通常需要对 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 实例的引用。

基于所有这些原因,以及 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,现在我们就来看看如何利用它做一些很酷的事情。

将编码转换为 WMV

如果 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 graph
object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);
DisposalCleanup.Add(filterGraph);
IGraphBuilder graph = (IGraphBuilder)filterGraph;
// Add the ASF writer and set the output name
IBaseFilter 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 graph
graph.RenderFile(InputFilePath, null);
RunGraph(graph, asfWriterFilter);

先创建一个筛选器图形,将 WM ASF Writer 筛选器添加到其中并配置为指向适当的输出文件路径,然后将 DVR-MS 文件添加到该图形中并使用图形的 RenderFile 方法来呈现。遗憾的是,这在控制 WMV 文件编码方式上并没有提供很多灵活性。为了做到这一点,我们需要用一个配置文件配置 WM ASF Writer,这可以通过在调用 RenderFile 之前插入以下代码来完成:

// Set the profile to be used for conversion
if (_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 方法实现的。该方法首先获取指定图形的 IMediaControlIMediaEvent 接口。它还试图获取可用于跟踪源 DVR-MS 文件处理进度的 IMediaSeeking 接口。然后使用 IMediaControl 接口来运行图形,从此时开始,方法中的剩余代码仅仅是用来跟踪转换的处理进度。在图形结束运行前,代码会不断轮询 IMediaEvent.WaitForCompletion 方法,如果等待时间已到但图形还没完成运行,则该方法将返回状态代码 EventCode.None (0x0)。如果发生这种情况,则会使用 IMediaSeeking 接口来查询已经处理多少 DVR-MS 文件以及该文件的持续时间,由此我可以计算文件处理的百分比。

当图形最终完成运行时,IMediaEvent.WaitForCompletion 会返回 EventCode.Complete (0x1),并使用 IMediaControl.Stop 来停止图形。

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 公开了使调试图形变得更加简单的功能。

调试筛选器图形

您将在 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 = GetRunni

抱歉!评论已关闭.