玩转 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 = 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 中删除。

非托管资源清理

当资源使用完毕后,尽早将它们释放是非常重要的。当使用处理大量音频和视频资源的 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,它是通过 DisposalCleanupIDisposable.Dispose 方法调用的。通过使用其 Add 方法来调用添加到 DisposalCleanup 中的每个对象,EnsureCleanup 调用了一个 IDisposable 对象上的 Dispose、一个 COM 对象上的 Marshal.ReleaseComObject 和一个接口指针上的 Marshal.Release。通过这些,我的代码只需将使用许多 COM 对象的代码块放在一个创建了新的 DisposalCleanup 的 using 块中,将任何 COM 对象或接口添加到 DisposalCleanup 实例中,并在 using 块结束时调用 DisposalCleanupIDisposable.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 中添加可处置的对象;当派生类的工作执行完毕后,即使它引发异常,基类也会自动处置这些资源。

将 WmvConverter 投入使用: WmvTranscoderPlugin

显而易见,通过前面讨论的代码,您可以编写功能丰富的应用程序来处理 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.IAddInModuleMicrosoft.MediaCenter.AddIn.ListMaker.ListMaker

public class WmvTranscoderPlugin : MarshalByRefObject, 
  IAddInModule, ListMakerApp, IBrandInfo
{
  ...
}

所有 Media Center 外接程序都实现了 IAddInModuleIAddInModule 通过实现 IAddInModule.InitializeIAddInModule.Uninitialize 方法来初始化和处置要运行的代码。在许多情况下,初始化阶段需要做的事情非常少;对于我的外接程序,我只需查看一下注册表,找到用户首选项,例如经过编码转换的文件应该写到哪个磁盘(注册表中 HKLM/Software/Toub/WmvTranscoderPlugin 项的 PreferredDrive 值)以及应该使用哪个 Windows Media 配置文件来将代码转换为 WMV(注册表中的 HKLM/Software/Toub/WmvTranscoderPlugin 项的 ProfilePath 值)。如果没有指定驱动器(或者指定的驱动器无效),则我将默认值设置为从 System.IO.Directory.GetLogicalDrives 返回的第一个有效的驱动器,其中,有效的驱动器定义为 Win32 GetDriveType 函数声明的固定驱动器中的任何一个驱动器。

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 方法只是返回上述方法所维护的计数值。TimeCapacityItemCapacity 属性同时返回其类型各自的 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 中出现的位置如下表所示:

属性

描述

PageTitle

当外接程序使用时显示在右上角的文本。

CreatePageTitle

列表创建页面的标题文本。

SaveListButtonTitle

用于在列表创建之后启动处理操作的按钮上的文本。

ViewListButtonTitle

用于查看要复制以进行处理的媒体项的按钮上的文本。

ViewListPageTitle

列表查看页面的标题文本。

MainIcon

包含要作为列表生成页面上主图标(水印)使用的图标的文件路径。

StatusBarIcon

包含 Media Center 放在生成页面左下角的图标的文件路径。

ViewListIcon

Media Center 放在列表查看页面顶部的图标文件的路径。

ListMakerApp 上最有趣的方法是 LaunchCancel。一旦用户创建了要处理的文件列表并单击按钮开始处理,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 文件。ConverterProgressChanged 事件关联到外接程序中的一个私有方法 — ReportChange 上,然后由该方法调用 Media Center 的进程更新委托。另外,当前转换程序存储在一个成员变量中,因而可以使用 Cancel 方法来停止其进程。

图 13. 指定目标文件夹

Cancel 方法也非常简单。它设置了一个成员变量,用于警告在另一个线程中运行的 ConvertToWmv 方法,通知它用户已经请求取消。然而,正如您在 ConvertToWmv 方法中看到的,只有当该方法准备转换下一个 DVR-MS 文件时才会对此进行检查,所以 Cancel 方法还使用存储在一个成员变量中的 WmvConverter 对象,使用该 ConverterCancelAsync 方法取消当前执行的转换。正如我们前面所看到的,这将导致 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 配置文件,如下所示:

<application title="WMV Transcoder" 
 id="{50d449ee-c06d-43e3-a94a-48b8eed72968}">
  <entrypoint id="{a60de2e7-cade-48e3-8eb1-6f9ca898408a}"
    addin="Toub.MediaCenter.Tools.WmvTranscoderPlugin, 
        Toub.MediaCenter.Tools.WmvTranscoderPlugin, 
        Version=1.0.0.0, PublicKeyToken=6e541e2c6f2c93d2, 
        Culture=neutral"
    title="WMV Transcoder"
    description="Transcodes recorded shows to WMV"
    imageURL="./WmvTranscoderPlugin.png">
    <category category="ListMaker/ListMakerApp"/>
  </entrypoint>
</application>

您应该能够运行安装程序并直接通过一个我们都不必编写的非常时髦的 UI 来将 DVR-MS 立即转换成 WMV。(感谢你,Media Center 团队!)

图 14. 成功的编码转换

访问 DVR-MS 元数据

DVR-MS 文件格式既包含音频、视频和闭合字幕数据,也包含描述文件及其内容的元数据。一旦电视节目录制下来,节目的标题、描述、演员表和原始播放日期等信息就存储在这个位置。很酷的一点是,您的应用程序可以通过 DirectShow StreamBufferRecordingAttribute 对象实现的 IStreamBufferRecordingAttribute 接口轻松地访问此数据。这个对象可以使用它的 CLSID 来创建,正如我本文中创建其他 DirectShow 对象那样。

要使用 IStreamBufferRecordingAttribute,首先必须为它提供一个托管接口(您会在本文的代码下载中发现这段代码,它嵌套在 DvrmsMetadataEditor 类中):

[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 接口;它们几乎完全相同)。IFileSourceFilterLoad 方法可用于打开我对其元数据感兴趣的 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 方法提供的,该方法提供了 IStreamBufferRecordingAttributeGetAttributeCountGetAttributeByIndex 方法的简单抽象。

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、字符串、无符号整型等)的枚举。

您可能注意到 DvrmsMetadataEditor 类是从 MetadataEditor 基类派生的。我这样做是为了提供另一个类 — AsfMetadataEditor,它也是从 MetadataEditor 派生的。AsfMetadataEditor 基于包含在 Windows Media Format SDK( 从此处下载 SDK)中的示例代码。它使用 Windows Media IWMMetadataEditorIWMHeaderInfo3 接口来获取 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 文件时,就可以显示更多的可用信息。

编辑 DVR-MS 文件

除了转换为 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 as
public 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 位长整数值,所以在托管代码中,您可以使用如下所示的函数来将秒转换成传递给 AppendExREFERENCE_TIME 值:

internal static ulong SecondsToHundredNanoseconds(double seconds)
{
  return (ulong)(seconds * 10000000);
}

当您完成追加到输出文件时,Close 方法就会关闭输出文件。在您连接到该文件时,可以使用一个单独线程的 GetCurrentLength 方法来确定输出文件的当前长度。然后您可以使用此信息和您对输入文件/片段长度的了解来计算完成拼接的百分比。请注意,这个过程非常快,因为将片段从一个 DVR-MS 文件追加到另一个文件并不需要编码和解码。

为了演示此接口,我构建了 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)

致谢

我衷心感谢 Matthijs Gates、Aaron DeYonker、Ryan D'Aurelio、Ethan Zoller、Eric Gunnerson 和 Alex Seigler 提供他们的研究领域的专家见解,感谢 ABC 允许我使用来自他们电视节目的示例和屏幕快照,也要感谢我的好朋友 John Keefe 和 Eden Riegel,感谢他们允许我在本文中使用他们的肖像。

关于作者

Stephen Toub 是 MSDN Magazine 的技术编辑,他还为该杂志撰写 .NET Matters 专栏。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值