使用 Windows Mobile 5.0 中的图片、视频和照相机

http://msdn.microsoft.com/zh-cn/library/aa454909.aspx

简介

Windows Mobile version 5.0 通过集成的照相机应用程序编程接口 (API) 提供优秀的多媒体支持,这些 API 使开发人员能够将照相机、图片和视频功能直接嵌入到应用程序中。它还包括大量新增的多媒体 API。Microsoft DirectShow 在视频流和音频流的捕获和播放方面为开发人员提供更强的控制和更高的灵活性。Microsoft Windows Media Player 10 Mobile 使开发人员能够将媒体播放器功能(包括媒体库的管理和播放)集成到他们的应用程序中。Microsoft DirectDraw 在呈现高速的二维图形方面为开发人员提供更强的控制和更高的灵活性。Microsoft Direct3D 使开发人员能够交付更丰富的三维游戏环境,该 API 也可以在托管代码中使用。

本文通过一个基于业务方案(检查)的实际示例应用程序,使您了解如何使用集成的照相机 API 来捕获照片和视频。该示例说明新的高级构造(如图片选择和照相机捕获对话框)如何轻松地包括到企业应用程序之中。您将了解如何通过使用本机代码以及新增的托管组件对象模型 (COM) 互操作性功能(具有一些优秀的社区外接程序代码),在应用程序中使用媒体播放器控件。您还将了解到,如果需要对媒体和照相机的交互进行更多控制,如何使用 DirectShow API 中更高级的功能。包括的 DirectShow 示例代码用于说明视频和音频的播放和捕获。

本文首先描述为常见业务方案创建一些多媒体支持的基本任务。

检查业务过程

尽管在许多不同行业中都有检查任务,但这类任务通常是现场执行的。检查员一般是移动工作人员,他们的大部分工作实际上是收集数据。他们所面临的问题是需要收集尽可能多的信息,这样才能将因检查的不同感知而产生差异的风险降至最低,并最大限度地扩大涉及到的各方之间的信任。例如,一辆汽车在变更车主之前,车检员要进行检查,检查的准确性可能是买卖双方最关注的。然而,该工作在实地进行并且需要收集大量信息时,就出现了一个非常棘手的情况。

在检查过程中使用传统的解决方案(如在纸上手写记录)可能看似有效率,但是不以数字方式记录信息在检查之后可能会产生很多问题。手写记录需要输入到信息系统中,这通常由除检查员之外的其他人输入信息。这种情况通常会导致较差的信息质量,有时甚至会丢失信息。

由于文本不可能记录所有的信息,因此检查员在很长一段时间内都使用照片。但是传统的照片也会在检查后引发问题,因为照片的质量可能不合格,而且只有在这些照片由开发过程返回后才会发现质量问题。

对于所有这些问题,使用配备有照相机(能够捕获记录、照片甚至视频)的移动设备这一想法似乎是最佳解决方案。但在诸如 Pocket PC 这样的移动设备上以文本形式捕获信息仍然是个挑战,所以多媒体的作用变得更为重要。将通过集成手段捕获文本、照片、口述内容以及具有音频的视频合为一体,可能是成功的诀窍。本文重点讲述如何使这种愿景成为现实,不过首先要确定需求。用于检查的一般业务过程定义如下:

  • 从后台(服务器)签出必要的检查。

  • 利用注释记录检查。

  • 收集诸如照片和视频这样的展示内容。

  • 将检查签入到后台(服务器)。

该业务过程如图 1 所示。

1. 检查业务过程

如图 1 所示,业务过程中主要步骤包括了大量作为新解决方案重要需求的活动。即使第二个步骤(记录检查)和第三个步骤(收集展示内容)按顺次进行,这些任务在执行时也可能会反复。要了解更多具体的需求,可通过将每个过程步骤扩展到它自己的子过程流来深入研究业务过程。

当您满意地定义了新过程后,下一步是查看该解决方案的设计。


应用程序设计

Northwind Pocket Service: Field Service for Windows Mobile-based Pocket PCs 一文很好地介绍了移动解决方案中体系结构的工作。Northwind Pocket Inventory: Logistics for Windows Mobile 2003-based PocketPCs 一文包括对该应用程序设计(该设计实际上从用例的定义开始)中最重要的可交付物(构件)的描述。因为本文主要关注多媒体,因此图 2 显示示例应用程序中实现的一些最有趣的用例。

图 2. 用于收集展示的用例模型

设计过程初期要创建的其他重要构件是对话框模型和示例对话框。图 3 显示示例应用程序的对话框模型。

working_with_multimedia_03

3. 对话框模型

该对话框模型概括应用程序中包括的对话框(窗体)以及这些对话框之间的导航。请注意,虚线边框的对话框在本文该示例中不实现。

图 4 显示一些示例对话框。

4. 示例对话框

这些对话框示例是用常规的绘图工具(本例使用 Microsoft PowerPoint)绘制的,创建此类对话框时应该使用懂得用户界面设计的人员。及早的将这些应用程序对话框图示化则为用户和其他股东提供一个了解应用程序外观(以及工作方式)的机会,从而在该阶段进行更改时能轻松实现。

下载代码示例将上述所有图片作为一个 PowerPoint 演示文稿提供,以便您在创建自己的关系图时可以重用它们。


示例应用程序演练

该示例客户端方案是通过 Microsoft Visual Studio .NET 2005 用 C# 编写的 Pocket PC 应用程序,它针对的是 Microsoft .NET Compact Framework 版本 2.0

该应用程序显示如何使用 Pocket PC 支持检查业务过程。为了使该示例更真实,使用了一个具体的检查方案 - 车检员。该应用程序的用户执行不同目的检查。例如,用户可以在发生交通事故或者车辆进行修理后进行检查。其他的示例还有,在签订新保险策略之前或者在车辆变更车主时进行的常规检查。该演练涵盖一般类型的检查(用户界面有时称之为"检验")。该方案是在后台创建检查,当检查员与后台服务器同步时,新的检查就下载到设备上。

本文将在该应用程序的演练过程中对一些设计选择进行评论。另请注意,本文在描述该应用程序的用户界面设计之后探究部分代码。

主屏幕

启动该应用程序时,第一个屏幕是可用于搜索和选择检查的主屏幕,如图 5 所示。

working_with_multimedia_05

5. 主屏幕(检查)

首先,可以搜索检查的名称和类型。然后,当按左软键 (Find) 时,该应用程序将搜索可用的检查。请注意可以搜索的可用检查类型。

右软键 (Menu) 提供某些常规应用程序功能,如应用程序选项以及与服务器同步。因为本文重点关注多媒体,所以该示例中没有实现这些功能,但 MSDN 上有许多涉及这些主题的文章(例如,Northwind Pocket Sales: Field Sales for Windows Mobile 2003 Software for Pocket PCs)。

当该应用程序进行搜索时,它将搜索结果添加到检查列表中。点击并按住列表中的某个检查会显示一个快捷菜单,该快捷菜单带有执行所选检查的命令 (Inspect),如图 6 所示。

6. 搜索结果

检查

选择 Inspect 命令时(如前面的图 6 所示),该应用程序将显示 Inspection 屏幕,如图 7 所示。

7. Inspection 屏幕

Inspection 屏幕上,可以编辑有关检查的信息。可以修改名称和类型(例如,如果名称拼写错误),然后即可以自由文本方式输入检查注释。图 7 中输入的注释涉及机动车辆的不同部件,在这些注释中加入实际照片是非常有价值的。这就是屏幕上 Exhibits 部分(图 7 的下面部分)的作用。图 7 还显示以下情况:如果在 Exhibits 部分中点击并按住鼠标,将显示一个快捷菜单,这样就可以对附加到该检查的展示进行添加 (New)、编辑 (Edit) 或删除 (Delete) 操作。

展示

如果在 Exhibits 部分的快捷菜单中选择 New 命令,则显示 Exhibit 屏幕。在此处,可以输入展示的名称。然后,当按下左软键 (New) 时,将显示一个快捷菜单,可从中选择从文件添加展示 (FromFile),方法是拍一张新照片 (Photo),或者用内置照相机(如果该设备上有照相机)制作新视频 (Video),如图 8 所示。

8. Exhibit 屏幕

选择 From File 命令将显示一个屏幕,用于选择一个媒体文件,如图 9 中所示。

Working_with_Multimedia_09_thumb

9. 文件选择屏幕。

您可以使用硬件导航台(上、下、左、右)在媒体文件列表中浏览,可以通过按 Action 键(通常在硬件导航台中间)选择一个文件(如照片或视频)。如果选择一张照片,则返回到您可以看见所选照片的 Exhibit 屏幕。然后,当按 Menu 软键时,一个快捷菜单将出现并包含一个 View 命令,如图 10 所示。

Working_with_Multimedia_10_thumb

10. 具有所选照片的 Exhibit 屏幕。

当选择 View 命令时,图片查看器应用程序将启动。在该应用程序中,您可以更详细地查看该图片,甚至可以对该图片进行操作。图 11 中显示的命令(可通过点击 Menu 软键显示)表示您可以将该图片 (Beam picture) 发送到另一个设备(例如,Pocket PC、Smartphone 或桌面计算机),并且能够以限制方式(例如,旋转)Edit 它。

11. 图片查看器。

点击 OK,返回到 Exhibit 屏幕。

在文件选择屏幕上(如图 9 所示),如果选择添加一个视频文件,将出现 Exhibit 屏幕。如果随后点击 Menu,将显示一个快捷菜单,如图 12 所示。

12. 选择了视频的 Exhibit 屏幕。

当针对一个视频展示选择 View 命令时,媒体播放器将启动并播放该视频,如图 13 所示。

13. 媒体播放器中显示的视频。

请注意,因为展示的标识(一个 uniqueidentifier 类型的值)用作该文件的名称,所以媒体播放器中就显示这一名称。

Exhibit 屏幕上(如前面的图 8 所示),如果选择 Photo 命令来添加一张新照片,该应用程序将显示一个可在其中捕获新照片的屏幕,如图 14 所示。

14. 照片捕获屏幕。

录制新视频时,捕获屏幕如图 14 中显示(然后胶片的符号替换为摄影机的符号)。但请注意,图 14 是一个概念性的屏幕快照,其外观在基于 Windows Mobile 5.0 的实际 Pocket PC 上可能稍有不同。

客户端应用程序的演练到此已经结束,现在该看一些源代码了。


代码演练

本部分将为您介绍示例客户端方案的一些源代码。您将看到如何在托管代码和本机代码中使用选择图片对话框和照相机捕获对话框。不过,本文的附带源代码中不包括本机代码示例。然后,您将看到如何使用其他包含媒体的应用程序,以及如何将媒体存储到数据库中。

选择图片(托管代码)

要将多媒体与企业应用程序集成,最重要的是能够使用已经存储在文件系统中的图片(包括照片)和视频。如果该设备没有配备内置照相机,您仍然可以使用该方法集成多媒体。例如,您可能使用单独的数字照相机来拍照或者捕获视频,然后使用红外线端口或蓝牙将照片或视频传输到 Pocket PC。为了满足该需要,Windows Mobile 5.0 平台中包括了一个现成的对话框,该对话框可以在托管 (.NET Compact Framework) 代码中作为"Microsoft.WindowsMobile.Forms"命名空间中的 SelectPictureDialog 类使用。

要创建前面图 9 中显示的文件选择屏幕,可以使用以下代码示例。

private void fromFileMenuItem_Click(object sender, EventArgs e)
{
  SelectPictureDialog selectPictureDialog = new SelectPictureDialog();
  selectPictureDialog.Owner = this;
  selectPictureDialog.Title = "Select Exhibit Photo or Video";
  selectPictureDialog.CameraAccess = false;
  selectPictureDialog.Filter = "All files|*.*";
  if(selectPictureDialog.ShowDialog() == DialogResult.OK &&

    selectPictureDialog.FileName.Length > 0)
  {
    fileExtension = Path.GetExtension(selectPictureDialog.FileName);
    File.Copy(selectPictureDialog.FileName, fileName());
    if(fileExtension.ToLower() == ".jpg")
      pictureBox.Image = new Bitmap(fileName());
    else
    {
      ComponentResourceManager resources =
        new ComponentResourceManager(typeof(ExhibitForm));
      pictureBox.Image = ((System.Drawing.Image)
        (resources.GetObject("pictureBox.Image")));
    }
  }
}

在上面的代码中,您可以看到该对话框的所有者设置为具有该标题的当前窗口。CameraAccess 属性指示照相机是否应该从该对话框窗口使用。如果该属性设置为 true 且照相机可用,则可以在文件列表中看到照相机符号。当选择照相机符号时,将显示一个 CameraCaptureDialog 对话框,这样就可以照相或者制作视频记录。请注意,因为 SelectPictureDialog 对话框能够链接到 CameraCaptureDialog 对话框(虽然该示例中未显示它),所以这的确就是从文件系统和内置照相机中启用包含的媒体而需进行的最小集成。

Filter 属性定义当应用程序显示文件列表时将应用的搜索筛选器。该属性的默认值为"Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF"。

上述代码中接下来要发生的是对话框的实际呈现。呈现通过 ShowDialog 方法进行,该方法直至对话框关闭时才停止执行。通常,ShowDialog 方法返回一个值,指示对话框的关闭方式。如果用户按设备上的 Action 键或点击 OK,该对话框返回 DialogResult.OK。如果选择一张图片,则 FileName 属性包括所选文件的名称。如果这两个条件都满足,则保存所选文件的扩展名,将该文件复制到示例的媒体文件夹。如果所选文件是照片(在本示例中,只有 JPEG 文件可用于照片),使用所选图片更新 Exhibit 屏幕上的图片框 (pictureBox)。如果所选文件是视频,默认图像(图 12 显示的放大的媒体播放器文档符号)从应用程序资源加载。

fileName 方法的代码如下所示。

private string fileName()
{
  return Common.Values.MediaPath + Path.DirectorySeparatorChar +
      exhibitID.ToString() + fileExtension;
}

上述代码创建了展示的完整文件名,该文件名是展示的标识(唯一标识符)和所选文件的文件扩展名的组合。所有媒体文件都存储在一个公共文件夹 (Common.Values.MediaPath) 中。

图 15 显示 SelectPictureDialog 类的完整定义。

15. SelectPictureDialog

因为 Filter 属性可以包括多个筛选器,所以可以使用 FilterIndex 属性设置默认筛选器索引。例如,如果 Filter 属性设置为"Bitmap Files|*.bmp|JPEG Files|*.jpg|GIF Files|*.gif",FilterIndex 设置为 1,则该对话框将搜索 JPEG 文件。

除了已经提到的属性,还可以用 InitialDirectory 属性选择一个初始文件夹来搜索图片。可以将 LockDirectory 属性设置为 true 以防止用户更改文件夹。可以使用 SortOrder 属性设置找到的文件的初始排序,还可以将该属性设置为根据日期、名称或大小按升序和降序进行排序。可以使用 ShowDrmContent 属性指定受数字版权管理(Digital Rights Management,DRM)保护的文件是否应该显示在对话框中。如果受 DRM 保护的文件得到保护,则它们不会被转发,只有当 ShowForwardLockedContent 属性设置为 true 时,它们才会显示在该对话框中。

选择图片(本机代码)

要在本机代码中使用上一部分中描述的对话框,可以使用具有 OPENFILENAMEEX 结构的 GetOpenFileNameEx 函数,如下所示。

TCHAR szFile[MAX_PATH];
OPENFILENAMEEX ofnex = {0};

ofnex.lStructSize     = sizeof(ofnex);
ofnex.hwndOwner       = g_hWnd;
ofnex.lpstrFile       = szFile;
ofnex.nMaxFile        = sizeof(szFile) / sizeof(szFile[0]);
ofnex.lpstrFilter     = TEXT("All Files (*.*)/0*.*/0");
ofnex.lpstrTitle      = TEXT("Select Exhibit Photo or Video");
ofnex.ExFlags         = OFN_EXFLAG_THUMBNAILVIEW;
ofnex.lpstrInitialDir = NULL;

if(GetOpenFileNameEx(&ofnex))
{
  // The selected file name is in szFile
}

上述代码中的所有者窗口是主应用程序窗口(全局定义为 g_hWnd)。请注意,筛选器的各部分用 NULL 字符分隔。使用 OFN_EXFLAG_THUMBNAILVIEW 以缩略图格式显示 ListView 控件,ExFlags 成员可以包括 OFN_EXFLAG_DETAILSVIEW 标志以详细信息格式显示 ListView。其他选项是 OFN_EXFLAG_HIDEDRMPROTECTEDOFN_EXFLAG_HIDEDRMFORWARDLOCKED,前者用于排除受 DRM 保护的文件的显示,后者用于排除无法转发的受 DRM 保护的文件。

捕获照片和视频(托管代码)

尽管许多移动设备早已配备了内置照相机,但刚开始时缺乏对使用这些照相机的开发支持。几乎没有制造商制造可以从托管代码使用的照相机,甚至本机 API 都难以找到。对于那些希望将媒体集成到应用程序中的企业开发人员而言,唯一可用的选择是进行文件级集成(如前面图片选择对话框的讨论中描述的那样)。

有了 Windows Mobile 5.0 软件,所有一切都改变了。现在定义了设备制造商将支持的通用照相机 API。此外,为了满足那些使用 .NET Compact Framework 构建应用程序的企业开发人员的需要,Windows Mobile 5.0 软件还包括了一个现成的对话框。该对话框名为 CameraCaptureDialog,可在"Microsoft.WindowsMobile.Forms"命名空间中找到它。

要创建前面图 14 中显示的照片捕获屏幕,可以使用以下代码示例。

private void photoMenuItem_Click(object sender, EventArgs e)
{
  CameraCaptureDialog cameraCaptureDialog = new CameraCaptureDialog();
  cameraCaptureDialog.Owner = this;
  cameraCaptureDialog.Title = "Take Exhibit Photo";
  cameraCaptureDialog.Mode = CameraCaptureMode.Still;
  if(cameraCaptureDialog.ShowDialog() == DialogResult.OK &&

    cameraCaptureDialog.FileName.Length > 0)
  {
    fileExtension = Path.GetExtension(cameraCaptureDialog.FileName);
    File.Copy(cameraCaptureDialog.FileName, fileName());
    pictureBox.Image = new Bitmap(fileName());
  }
}

CameraCaptureDialog 类的工作方式与前面讨论的 SelectPictureDialog 类非常类似。在上述代码中,该对话框的所有者设置为具有该标题的当前窗口。Mode 属性指示照相机捕获对话框应该用于拍照还是进行视频录制。在本例中,用户需要一张照片 (CameraCaptureMode.Still)。对话框通过 ShowDialog 方法显示,该方法返回一个指示对话框关闭方式的值。如果用户点击 OK,则对话框返回 DialogResult.OK。如果拍了一张照片,FileName 属性包括新建的图片(照片)文件的名称。如果这两个条件都满足,则保存所选文件的扩展名,将该文件复制到示例的媒体文件夹,使用所选图片更新 Exhibit 屏幕上的图片框 (pictureBox)。

要使用同一个对话框捕获视频,使用以下代码示例。

private void videoMenuItem_Click(object sender, EventArgs e)
{
  CameraCaptureDialog cameraCaptureDialog = new CameraCaptureDialog();
  cameraCaptureDialog.Owner = this;
  cameraCaptureDialog.Title = "Take Exhibit Video";
  cameraCaptureDialog.Mode = CameraCaptureMode.VideoWithAudio;
  if(cameraCaptureDialog.ShowDialog() == DialogResult.OK &&

    cameraCaptureDialog.FileName.Length > 0)
  {
    fileExtension = Path.GetExtension(cameraCaptureDialog.FileName);
    File.Copy(cameraCaptureDialog.FileName, fileName());
    ComponentResourceManager resources =
      new ComponentResourceManager(typeof(ExhibitForm));
    pictureBox.Image = ((System.Drawing.Image)
      (resources.GetObject("pictureBox.Image"))); ;
  }
}

请注意,上面的代码与捕获照片的代码非常类似,但是 Mode 属性却设置为捕获带有声音的视频记录 (CameraCaptureMode.VideoWithAudio)。Mode 属性也可以设置为捕获没有声音的视频 (CameraCaptureMode.VideoOnly)。此外,默认图像(图 12 中显示的放大的媒体播放器文档符号)从该应用程序的资源加载到图片框中。

图 16 显示 CameraCaptureDialog 类的完整定义。

16. CameraCaptureDialog

除了已经提到的属性,如果在使用 ShowDialog 方法显示该对话框之前设置 DefaultFileName 属性,该名称将用作新照片或视频的文件名。可以使用 InitialDirectory 属性指定捕获的文件(照片或视频)将存储到何处。要请求捕获的照片或视频的分辨率,将 Resolution 属性设置为一个 Size 实例,如下所示。

cameraCaptureDialog.Resolution = new Size(320, 240);

使用 StillQuality 属性设置该照片的压缩级别。在 CameraCaptureStillQuality 枚举中,High 选项意味着低压缩率用于保持照片的高质量。Low 选项与其相反(结果产生具有较低质量的高压缩)。Normal 选项产生介于 HighLow 选项之间的质量。

使用 VideoTimeLimit 属性设置新视频的最大记录时间。默认值为零,即没有时间限制。

最后一个属性是 VideoTypes,它可用于选择要捕获哪种类型的视频。基本上,在基于 Windows Mobile 5.0 的设备上,可以使用两种不同的视频类型 - 多媒体消息传递服务(Multimedia Messaging Service,MMS)和 Windows Media Video (WMV)。CameraCaptureVideoTypes 枚举提供可能的值。使用 Messaging 录制 MMS 视频,使用 Standard 录制 WMV 视频。还有一个 All 枚举值,用于使 Resolution 属性确定要录制的视频类型。如果使用 All 选项且 Resolution 高度和宽度为零,则再次使用上次使用的分辨率。您还需要设置 MessagingStandard 枚举值的 Resolution 属性。

捕获照片和视频(本机代码)

如果想在本机代码中使用相同的对话框(如前所述),可以使用具有 SHCAMERACAPTURE 结构的 SHCameraCapture 函数,如下所示。

HRESULT hResult;
SHCAMERACAPTURE shcc;
ZeroMemory(&shcc, sizeof(shcc));

shcc.cbSize     = sizeof(shcc);
shcc.hwndOwner  = g_hWnd;
shcc.pszTitle   = TEXT("Take Exhibit Video");
shcc.Mode       = CAMERACAPTURE_MODE_VIDEOWITHAUDIO;
shcc.VideoTypes = CAMERACAPTURE_VIDEOTYPE_ALL;

if(S_OK == SHCameraCapture(&shcc))
{
  // The selected file name is in shcc.szFile
}

上述代码中的所有者窗口是主应用程序窗口(全局定义为 g_hWnd)。Mode 成员的其他值是针对照片的 CAMERACAPTURE_MODE_STILL(默认值)和针对无声音视频的 CAMERACAPTURE_MODE_VIDEOONLYVideoTypes 成员的其他值是针对 WMV 视频的 CAMERACAPTURE_VIDEOTYPE_STANDARD 以及针对 MMS 视频的 CAMERACAPTURE_VIDEOTYPE_MESSAGING。当使用后两个值之一时,nResolutionWidthnResolutionHeight 成员均不能为零。

查看照片和视频

要打开前面图 11(图片查看器)和图 13(媒体播放器)中显示的屏幕,可以使用以下代码。

private void viewMenuItem_Click(object sender, EventArgs e)
{
  Process process = new Process();
  process.StartInfo.FileName = fileName();
  process.StartInfo.Verb = "Open";
  process.StartInfo.UseShellExecute = true;
  process.Start();
}

使用"System.Diagnostics"命名空间的 Process 类为特定文件新建一个进程。请注意,如果 UseShellExecute 属性设置为 true,文件名可以是与具有默认打开操作的可执行文件相关联的任何文件类型。在基于 Windows Mobile 5.0 的 Pocket PC 上,.jpg 扩展名与图片和视频查看器可执行文件 (pimg.exe) 相关联,.wmv 扩展名与媒体播放器可执行文件 (wmplayer.exe) 相关联。

将媒体保存到数据库

在前面的示例中,媒体保存在一个文件中,该文件复制到特定的(媒体)文件夹以便稍后与服务器同步。媒体(文件)信息也可以通过如下代码示例的方式存储在数据库中。

FileStream fs = File.Open(fileName(), FileMode.Open);
byte[] imageData = new byte[fs.Length];
fs.Read(imageData, 0, imageData.Length);
fs.Close();

SqlCeCommand cmd = cn.CreateCommand();
cmd.CommandText = "UPDATE Exhibit SET Media=?" +
  " WHERE ExhibitID='" + exhibitID + "'";
SqlCeParameter param = cmd.Parameters.Add("p0", SqlDbType.Image);
param.Value = imageData;
cmd.ExecuteNonQuery();

将文件读入一个字节数组,然后该数组用于更新数据库中正确的展示行。在上述代码中,保存媒体数据的数据库列 (Media) 被假定为 image 类型。请注意,要使用外部程序(pimg.exe 和 wmplayer.exe)播放媒体,需要将媒体再次写入到一个文件中,然后才能调用外部程序。

将媒体文件存储在文件系统中还是存储在数据库中,这取决于应用程序的要求。这两项技术都有优缺点。将媒体文件存储在文件系统中可以使录制、播放和操作更简单,并且能更容易地分离数据和媒体的存储。例如,数据库可以位于设备上较快的内存中从而提高访问性能,而媒体则存储在存储卡上,这样就可以存储更多信息,但访问速度较慢。然而,当这些文件需要与服务器同步时,就需要自定义解决方案(如 HTTP 上载和 XML Web 服务的附件)。如果媒体存储在数据库中,那么每次需要播放或操作时,都需要将媒体提取到一个文件中。但是这一同步操作较容易,因为数据库同步(如 Microsoft SQL Server Mobile Edition 中的远程数据访问和合并复制)和 XML Web 服务同步(SOAP 上的 DataSet)都支持该数据传输。任何情况下,您都应该考虑在设备上本地压缩媒体,以及当媒体在设备和服务器间传输时压缩媒体这两方面。

多媒体之外的代码重点

既然已经演练了示例应用程序中实现的多媒体功能,那么您可以再演练其他一些代码,因为从企业应用程序角度看,这是值得研究一下的。例如,使用 .NET Compact Framework 2.0 命名空间"Microsoft.Win32"中的 Registry 类获取注册表设置,如下代码示例所示。

string registryKey = @"SOFTWARE/Microsoft/MsdnSamples/Inspection";
RegistryKey key = Registry.LocalMachine.CreateSubKey(registryKey);
string userName = key.GetValue("UserName", "<default>").ToString();

要保存相同的注册表设置,使用以下代码。

RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey, true);
key.SetValue("UserName", userName);

该代码示例的另一个非常有趣的部分是数据访问。首先是用户界面实现,接着是用来填充 Type 组合框的代码(如前面的图 5 所示)。

try
{
  DataTable dt;
  using(InspectionHandler inspectionHandler = new InspectionHandler())
    dt = inspectionHandler.GetAllTypes().Tables[0];
  DataRow dr = dt.NewRow();
  dr["TypeName"] = "<All types>";
  dr["InspectionTypeID"] = Guid.Empty;
  dt.Rows.InsertAt(dr, 0);
  typeComboBox.DisplayMember = "TypeName";
  typeComboBox.ValueMember = "InspectionTypeID";
  typeComboBox.DataSource = dt;
  typeComboBox.SelectedIndex = 0;
}
catch(Exception)
{
  MessageBox.Show("Could not load inspection types!", this.Text);
}

通过检查处理程序类实例 (inspectionHandler) 检索具有所有类型的数据表 (dt)。第一行先添加到数据表中,然后它作为数据源添加到组合框中。该检查处理程序中方法 (GetAllTypes) 的实现如下所示。

public DataSet GetAllTypes()
{
  return SqlHelper.ExecuteDataset(cn, CommandType.Text,
    "SELECT * FROM InspectionType", "InspectionType");
}

SqlHelper 类实际上可以算是 Data Access Application Block(包括在 OpenNETCF 的 Application Blocks 1.0 中)的增强版。Application Blocks 1.0 是桌面计算机应用程序块的一个端口。方法 (ExecuteDataSet) 获取以下参数:数据库连接(SqlCeConnection 类型的 connection)、命令类型 (commandType)、语句 (commandText) 以及返回的表的名称 (tableName)。此处使用的重载如下代码示例所示。

public static DataSet ExecuteDataset(SqlCeConnection connection,
  CommandType commandType, string commandText, string tableName)
{
  DataSet ds = ExecuteDataset(connection, commandType, commandText,
   (SqlCeParameter[])null);
  ds.Tables[0].TableName = tableName;
  return ds;
}

调用的另一个重载按以下代码示例的方式实现。

public static DataSet ExecuteDataset(SqlCeConnection connection,
  CommandType commandType, string commandText,
  params SqlCeParameter[] commandParameters)
{
  SqlCeCommand cmd = new SqlCeCommand();
  bool mustCloseConnection = false;
  PrepareCommand(cmd, connection, (SqlCeTransaction)null, commandType,
    commandText, commandParameters, out mustCloseConnection );
          
  SqlCeDataAdapter da = new SqlCeDataAdapter(cmd);
  DataSet ds = new DataSet();
  ds.Locale = CultureInfo.InvariantCulture;
  da.Fill(ds, 0, 100, "Table");
      
  cmd.Parameters.Clear();

  if(mustCloseConnection)
    connection.Close();

  return ds;
}

创建一个 SQL Mobile 命令 (SqlCeCommand) 实例 (cmd) 并准备执行它。然后,创建一个数据适配器 (da) 和一个数据集 (ds),该数据适配器用于使用数据填充数据集。注意如何才能指定只返回前 100 行。这种限制可能是个好点子;很少需要一个大的结果列表,一方面是因为对性能的影响,也因为在移动设备上浏览大数据集的效率并不太高。所有参数都与该命令对象分离,这样它们才可以再次使用。最后,返回创建的数据集。

您按照以下代码示例所示准备该命令。

private static void PrepareCommand(SqlCeCommand command,
  SqlCeConnection connection, SqlCeTransaction transaction,
  CommandType commandType, string commandText, SqlCeParameter[]
  commandParameters, out bool mustCloseConnection )
{
  if(connection.State != ConnectionState.Open)
  {
    mustCloseConnection = true;
    connection.Open();
  }
  else
    mustCloseConnection = false;

  command.Connection = connection;
  command.CommandText = commandText;
  command.CommandType = commandType;

  if(transaction != null)
    command.Transaction = transaction;

  if(commandParameters != null)
    AttachParameters(command, commandParameters);

  return;
}

如果连接尚未打开,则将其打开,根据这些参数配置该命令。有关 SqlHelper 的实现的更多信息,请参阅本文的下载示例代码以及 Data Access Application Block 文档。

对于任何企业开发人员,大大改进对 .NET Compact Framework 2.0 和 Visual Studio .NET 2005 中的数据访问支持不仅仅是受欢迎这么简单的。您可以直接从开发工具中使用数据库这一事实,将大大提高工作效率。典型的开发过程如下所示:在 SQL Server Management Studio 中创建 SQL Server Mobile Edition 数据库文件(sdf 扩展名)。也可以使用该工具添加所有表和数据。然而,您也可以从 SQL Server Management Studio 中的 Server Explorer 窗口连接到 SQL Server Mobile Edition 数据库,可以在其中添加表和数据、测试查询以及类似的项。进行调试时,您可以部署相同的数据库文件以及该应用程序。还有许多其他新增的数据访问功能有待您去探究。

媒体播放器控件

在许多情况下,将标准媒体播放器作为单独的进程启动(如前面所示)可能会满足业务需求,但如果需要更多地控制视频播放,则可以使用媒体播放器控件。媒体播放器控件(版本 10)包括在 Windows Mobile 5.0 软件中,它使开发人员能够将媒体播放器用作自己应用程序的一个自定义控件。

宿主媒体播放器控件(本机代码)

媒体播放器控件是一个常规 ActiveX 控件(.ocx 文件),熟悉 COM 的本机开发人员使用它时不会有很多问题。首先,您应该查看 Windows Media Player Mobile 代码示例,这些示例包括用于宿主媒体播放器控件的代码。然而,因为这些示例是针对本机智能设备开发的上一代工具 (Microsoft eMbedded Visual C++) 而编写的,所以本文的下载代码示例包括尝试通过 eMbedded Visual C++ Upgrade Wizard for Visual Studio 2005 Beta 2 方法转换媒体播放器示例 (CEWMPHostML) 的结果。

宿主窗口和该控件的创建如下代码示例所示。

CAxWindow                 m_wndView;
CComPtr<IWMPPlayer>       m_spWMPPlayer;
CComPtr<IConnectionPoint> m_spConnectionPoint;
DWORD                      m_dwAdviseCookie;

RECT rcClient;
GetClientRect(&rcClient);
m_wndView.Create(m_hWnd, rcClient, NULL, 
  WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
if(NULL == m_wndView.m_hWnd)
  goto FAILURE;

CComPtr<IAxWinHostWindow> spHost;
HRESULT hr = m_wndView.QueryHost(&spHost);
if(FAILMSG(hr))
  goto FAILURE;

hr = spHost->CreateControl(CComBSTR(
  _T("{6BF52A52-394A-11d3-B153-00C04F79FAA6}")), m_wndView, 0);
if(FAILMSG(hr))
  goto FAILURE;

hr = m_wndView.QueryControl(&m_spWMPPlayer);
if(FAILMSG(hr))
  goto FAILURE;

CComWMPEventDispatch *pEventListener = NULL;
hr = CComWMPEventDispatch::CreateInstance(&pEventListener);
CComPtr<IWMPEvents> spEventListener = pEventListener;
if(FAILMSG(hr))
  goto FAILURE;

CComPtr<IConnectionPointContainer> spConnectionContainer;
hr = m_spWMPPlayer->QueryInterface(__uuidof(IConnectionPointContainer),
  (void**)&spConnectionContainer);
if(FAILMSG(hr))
  goto FAILURE;

hr = spConnectionContainer->FindConnectionPoint(__uuidof(IWMPEvents),
  &m_spConnectionPoint);
if(FAILMSG(hr))
  goto FAILURE;

m_dwAdviseCookie = 0; 
hr = m_spConnectionPoint->Advise(spEventListener, &m_dwAdviseCookie);
if(FAILMSG(hr))
  goto FAILURE;

全局声明(前四行)之后,控件宿主窗口 (m_wndView) 作为主应用程序窗口 (m_hWnd) 的子窗口(并且具有和客户端区域相同的大小)进行创建。然后,检索宿主窗口的接口引用 (spHost) 并用它来创建媒体播放器控件。GUID(对应于与版本无关的 ProgID WMPlayer.OCX)用于创建该控件,从该宿主窗口检索该控件的实际接口引用 (m_spWMPPlayer)。最后,使用侦听所有类型事件的事件调度程序 (pEventHandler) 建立事件处理。请注意每个调用上的大量错误处理,您在调试和测试时将从中大大受益。

下一个主要操作是用户选择使用菜单打开某个文件,如下代码示例所示。

CFileOpenDlg dlgOpen;
if(dlgOpen.DoModal(m_hWnd) == IDOK)
{
  HRESULT hr = m_spWMPPlayer->put_URL(_T(dlgOpen.m_bstrName);
  if(FAILMSG(hr))
    return 0;
}
return 0;

显示一个对话框 (dlgOpen),用户可从中选择文件名。如果用户点击 OK 关闭该对话框,则媒体播放器的 URL 属性设置为在该对话框中输入的名称。然后媒体播放器控件播放该文件。

该事件处理以如下方式实现。

HRESULT CWMPEventDispatch::Invoke(DISPID dispIdMember, REFIID riid,
  LCID lcid, WORD wFlags, DISPPARAMS FAR* pDispParams,
  VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo,
  unsigned int FAR*  puArgErr)
{
  if (!pDispParams)
    return E_POINTER;

  if (pDispParams->cNamedArgs != 0)
    return DISP_E_NONAMEDARGS;

  HRESULT hr = DISP_E_MEMBERNOTFOUND;

  switch(dispIdMember)
  {
    case DISPID_WMPCOREEVENT_OPENSTATECHANGE:
      OpenStateChange(pDispParams->rgvarg[0].lVal);
      break;

    case DISPID_WMPCOREEVENT_PLAYSTATECHANGE:
      PlayStateChange(pDispParams->rgvarg[0].lVal);
      break;

    case DISPID_WMPCOREEVENT_AUDIOLANGUAGECHANGE:
      AudioLanguageChange(pDispParams->rgvarg[0].lVal);
      break;

    case DISPID_WMPCOREEVENT_STATUSCHANGE:
      StatusChange();
      break;

    // Here comes another 40 events

    case DISPID_WMPOCXEVENT_MOUSEUP:
      MouseUp(pDispParams->rgvarg[3].iVal,
        pDispParams->rgvarg[2].iVal, pDispParams->rgvarg[1].lVal,
        pDispParams->rgvarg[0].lVal);
      break;
  }
  return( hr);
}

对于每个事件调度标识 (dispIdMember),调用一个单独的方法来操作该事件。正如您在上述代码中所看到的,另外 40 个事件在下载代码示例中实现。媒体播放器控件发布的广泛的属性、方法和事件集,使其成为进行媒体播放时的重点考虑对象。

宿主媒体播放器控件(托管代码)

尽管 .NET Compact Framework 2.0 包括对 COM 互操作性的支持,但是没有对 ActiveX 控件的内置支持。然而,多亏我的同事 Alex Feinman,他提供了一种在托管代码中使用 ActiveX 控件的方式,您将在他的文章 (Hosting ActiveX Controls in Compact Framework 2.0 Applications) 中找到在托管代码中使用 ActiveX 控件的方式。通过使用该技术并在前面的示例基础上进行生成,图 17 显示具有集成视频播放的 Exhibit 屏幕在 Smartphone 版本的示例应用程序中可能的外观。

17. 通过媒体播放器控件进行的集成播放。

正如在 Pocket PC 示例(如图 10 所示)中一样,可以编辑展示名称,但此处播放可以从同一屏幕启动。

通过下面的设计器代码开始创建窗体的代码。

private AxWMPLib.AxWindowsMediaPlayer windowsMediaPlayer;
this.windowsMediaPlayer = new AxWMPLib.AxWindowsMediaPlayer();

this.windowsMediaPlayer.Location = new System.Drawing.Point(0, 28);
this.windowsMediaPlayer.Name = "windowsMediaPlayer";
this.windowsMediaPlayer.Size = new System.Drawing.Size(176, 152);
this.windowsMediaPlayer.TabIndex = 0;
this.windowsMediaPlayer.Text = "windowsMediaPlayer";

正如您所看到的,当 ActiveX 控件的包装准备就绪时,可以像处理任何托管控件那样处理该控件。在前面的代码示例基础上生成,该媒体播放器控件可以按以下方式使用展示视频加载。

windowsMediaPlayer.URL = fileName();

URL 属性可以是指向流式源或远程文件的 URL,也可以是本地文件系统中的文件。Menu 命令(PlayPauseStop)的后台代码如下代码示例所示。

private void playMenuItem_Click(object sender, EventArgs e)
{
  windowsMediaPlayer.Ctlcontrols.play();
}

private void pauseMenuItem_Click(object sender, EventArgs e)
{
  windowsMediaPlayer.Ctlcontrols.pause();
}

private void stopMenuItem_Click(object sender, EventArgs e)
{
  windowsMediaPlayer.Ctlcontrols.stop();
}

与该媒体播放器的基本交互不是非常困难,设置事件处理程序的方式也不会更困难。就像任何其他托管控件一样,以如下方式添加事件处理程序。

this.windowsMediaPlayer.StatusChange +=
  new System.EventHandler(windowsMediaPlayer_StatusChange);

然后,该事件处理程序按如下方式实现。

void windowsMediaPlayer_StatusChange(object sender, System.EventArgs e)
{
  string status = windowsMediaPlayer.status;
  // Do something with status string
}

正如在关于从本机代码使用多媒体播放器控件的讨论中提到的一样,该控件有许多可能性。您在独立媒体播放器中可以进行的任何操作在该控件中几乎都可以实现。示例包括使用播放列表、连接、远程媒体、播放器状态以及很多事件(如播放和结束流)。

DirectShow

既然您已经了解了基础知识,现在该看看 Windows Mobile 5.0 软件中多媒体支持的更高级的用法了。一个最有趣的开发是在 Windows Mobile 5.0 软件中包含 DirectShow API。查看该 API 的主要原因是您需要对应用程序中使用的多媒体进行更多的控制。如果前面描述的高级支持不足以满足应用程序的需求,DirectShow 提供更多的控制和更大的灵活性。DirectShow 是一个本机 API,因此可以通过本机工具集 (Visual C++) 使用,如本文后面的代码示例所示。

如果想在托管代码中访问 DirectShow,一个解决方案是将 DirectShow 的使用打包到一个本机 DLL 中,托管代码可以使用平台调用来调用该 DLL。这对于媒体捕获这样的功能而言可能是个很好的解决方案,这些功能并不依赖于用户界面组件(如媒体播放)。另一个解决方案是使用 .NET Compact Framework 2.0 中包括的 COM 互操作性来包装 DirectShow COM 组件。有一个称为 DirectShowNET 库的有趣项目,它为桌面计算机上的 DirectShow 提供托管包装,但目前没有可用于 .NET Compact Framework 的版本。

一个不错的常用方法是利用托管代码来实现用户界面、业务逻辑、数据库访问和基本的照相机交互。然后,由于需要更多的控制和灵活性,您可以使用媒体播放器控件进行媒体播放,使用 DirectShow 进行媒体捕获(功能包装在一个本机 DLL 中,通过平台调用从托管代码调用)。

开发过程中,DirectShow 在内部称为 Quartz,Quartz 还是主 DLL 的名称。1996 年 7 月,它首次作为 Microsoft ActiveMovie 版本 1.0 发布,当时它针对媒体播放提供了一个 ActiveX 控件,该控件除了支持音频文件外,还支持 Motion Picture Experts Group (MPEG) 1、影音交叉存取技术(Audio-Video Interleaved,AVI)和 QuickTime 视频。DirectShow SDK 作为 Microsoft DirectX SDK 的一部分由来已久,但 DirectShow SDK 现在包括在平台 SDK 中,它为开发 DirectShow 筛选器和应用程序提供工具和信息。

Windows Mobile 5.0 软件的 DirectShow 是用于流媒体的一种体系结构,它提供多媒体流的高质量播放和捕获。它支持很多格式,如波形 (WAV)、MP3(MPEG Audio Layer-3)、AVI、高级数据流格式(Advanced Streaming Format,ASF)和 MPEG。

DirectShow 与 Windows Mobile 5.0 支持的其他两种 DirectX 技术(DirectDraw 和 Direct3D)相集成。DirectShow 使用任何可用的视频和音频加速硬件,也支持不使用加速硬件的系统。

DirectShow 简化了媒体播放和格式转换,但对于需要自定义解决方案的应用程序而言,它还提供对基础流控制体系结构的访问。例如,您可以创建自己的组件来支持新媒体格式或自定义效果。您可以使用 DirectShow 编写的应用程序示例包括:AVI 和 MP3 播放器、AVI 到 ASF 的转换器,以及音频/视频捕获和编辑应用程序。DirectShow 基于 COM 并提供大量 COM 组件。要扩展 DirectShow,您需要实现自己的 COM 组件。

筛选器和筛选器图形

DirectShow 的主构造块是一个称为筛选器的组件。筛选器是一个在多媒体流上执行操作的软件(实际上是一个 COM)组件。例如,筛选器可以读取文件,从视频捕获设备获取视频,解码各种流格式,以及将数据传递到图形卡或声卡。

筛选器接收输入和产生输出,信息通过筛选器针在筛选器之间传递。一个针是一个筛选器端口,它可以是输入端口也可以是输出端口。如果筛选器解码 WMV 视频,则输入是 WMV 编码的流,输出是一系列未压缩的视频帧。在 DirectShow 中,一个应用程序通过将筛选器链连接在一起来执行任何任务,这样一个筛选器的输出就成为另一个筛选器的输入。一组连接的筛选器称为一个筛选器图形,图 18 显示一个用于播放带声音的视频文件的筛选器图形。

18. 典型视频文件的筛选器图形

筛选器图形必须遵循某些原则,第一个原则是需要一个源筛选器。这是数据的最初来源,无论它是文件、流媒体的 URL,还是诸如内置照相机这样的设备。然后,源筛选器的输出运行通过任意数量的转换筛选器。转换筛选器是这样的中间筛选器:它们接收某种类型的输入数据,修改传入的数据,然后将修改的数据传递到其输出。图的最后一部分是输出程序筛选器。输出程序筛选器是筛选器图形中处理的任何数据的最终目的地。输出程序可以代表以下内容:用于在屏幕上显示视频的窗口、用来发出声音的声卡,或者用来将数据存储到磁盘的筛选器编写器。

在图 18 中,这些筛选器如下所示:

  • 文件源筛选器从文件系统读取视频文件。

  • 拆分器筛选器将文件内容解析为两个流:一个压缩的视频流和一个音频流。

  • 视频解码器筛选器对视频帧进行解码。

  • 视频输出程序筛选器使用 DirectDraw 或图形设备接口 (GDI) 将这些帧绘制到显示器。

  • 声音设备筛选器使用 DirectSound 播放音频流。

请注意,图 18 中筛选器边缘的小正方形表示每个筛选器的针。

DirectShow SDK 附带一个名为 GraphEdit 的工具,用于处理筛选器图形。图 19 显示 GraphEdit 工具已经呈现的 WMV 视频文件。

19. GraphEdit 工具中 .wmv 文件的筛选器图形。

可以使用 GraphEdit 工具呈现文件、生成自定义图形、测试自定义筛选器、逐步(一帧接一帧)显示一个图形,以及处理类似的任务。如果要在应用程序中使用的图形已经确认在 GraphEdit 中运行,使用该工具将节省大量开发时间。(需要牢记的重要一点是,如果筛选器图形无法在 GraphEdit 中工作,它也无法在您的应用程序中工作。)GraphEdit 甚至可以将一个完整的筛选器图形保存为一个文件(.grf 扩展名),应用程序稍后可以加载该文件。

DirectShow 应用程序结构

DirectShow 应用程序进行的第一个操作是使用最重要的 COM 组件,即 Filter Graph Manager。为了使应用程序开发人员免于管理筛选器及其交互的复杂任务,该组件作为一个高级构造可以简化对筛选器图形及其筛选器的控制。您可以通过将筛选器连接在一起来使用 Filter Graph Manager 生成筛选器图形,然后应用程序可以进行诸如 RunPauseStop 这样的简单调用,以便通过筛选器图形控制数据流。在筛选器图形的处理过程中,Filter Graph Manager 还将事件通知传递给应用程序。如需对流过程进行更多控制,也可以通过这些筛选器的 COM 接口直接访问它们。在任何情况下,很好地了解 COM 在使用 DirectShow 时都很有帮助。

简言之,DirectShow 应用程序的典型步骤是:

  • 创建 Filter Graph Manager 实例。

  • 使用 Filter Graph Manager 实例生成一个筛选器图形,方法是直接使用呈现功能或筛选器。

  • 通过对 Filter Graph Manager 实例进行高级调用来控制媒体流,并响应 Filter Graph Manager 实例引发的事件。

处理完成后,释放 Filter Graph Manager 实例以及使用的所有筛选器。

当使用 DirectShow 播放媒体时,需要注意的重要一点是,它使用单独的线程运行筛选器图形。在 DirectShow 筛选器图形的执行过程中,您会看到创建并运行了若干个线程,因为 DirectShow 为 Filter Graph Manager 创建一个线程,然后为筛选器图形中的每个筛选器都创建一个单独的线程。因此,应用程序将在 DirectShow 播放该媒体文件时继续运行,在大多数应用程序中,这对用户界面响应是件好事。然而,您需要为 DirectShow 线程留出足够的时间运行。例如,如果应用程序的主线程在播放媒体文件的同时进行大量处理,由于这些线程的优先级较低,媒体播放时将时断时续。

视频播放

简单任务(如显示视频文件)以及一些基本的控制台应用程序代码如下所示:

#include <dshow.h>
void __cdecl main(void)
{
  IGraphBuilder *pGraphBuilder;
  IMediaControl *pMediaControl;
  CoInitialize(NULL);
    
  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC, 
    IID_IGraphBuilder, (void **)&pGraphBuilder);
  pGraphBuilder->QueryInterface(IID_IMediaControl,
    (void **)&pMediaControl);

  pGraphBuilder->RenderFile(L"//test.wmv", NULL);

  pMediaControl->Run();

  MessageBox(NULL, "Click OK to end playback.", "DirectShow", MB_OK);

  pMediaControl->Release();
  pGraphBuilder->Release();
  CoUninitialize();
}

上述代码示例首先声明和初始化 COM 库,然后该 COM 库用于创建 Filter Graph Manager 实例,该实例引用了图形生成器 (IGraphBuilder) 和媒体控件 (IMediaControl) 接口。图形生成器通过呈现(使用 RenderFile 方法)视频文件 (test.wmv) 来创建筛选器图形,然后媒体控件接口 (pMediaControl) 启动(使用 Run 方法)筛选器图形处理。显示一个消息框以防止应用程序关闭,但这不会影响视频的呈现,因为筛选器图形运行在单独的线程上。当该用户在消息框中点击 OK 时,接口引用随 COM 库一起释放。

更复杂的解决方案是使用以下代码侦听来自 Filter Graph Manager 实例的事件。

#include <dshow.h>
void __cdecl main(void)
{
  IGraphBuilder *pGraphBuilder;
  IMediaControl *pMediaControl;
  IMediaEvent   *pMediaEvent;
  CoInitialize(NULL);
    
  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC, 
    IID_IGraphBuilder, (void **)&pGraphBuilder);
  pGraphBuilder->QueryInterface(IID_IMediaControl,
    (void **)&pMediaControl);
  pGraphBuilder->QueryInterface(IID_IMediaEvent,
    (void **)&pMediaEvent);

  pGraphBuilder->RenderFile(L"//test.wmv", NULL);

  pMediaControl->Run();

  long eventCode;
  pMediaEvent->WaitForCompletion(INFINITE, &eventCode);

  pMediaControl->Release();
  pGraphBuilder->Release();
  CoUninitialize();
}

请注意,上述代码除了以粗体显示的新加内容外,几乎与之前的基本代码示例相同。在此处,您创建一个对事件 (IMediaEvent) 接口的引用,它用于等待筛选器图形处理完成。然而,在实际应用程序中,您应该避免使用 INFINITE,因为它可能会导致应用程序无限期阻塞。

如果不指定其他内容,该播放在单独的弹出窗口中进行。但在许多情况中,您可能想使播放窗口成为应用程序的子窗口。要指定播放窗口的所有者、类型和位置,可以使用以下代码(从上面代码示例修改而来)。

#include <dshow.h>
void __cdecl main(void)
{
  IGraphBuilder *pGraphBuilder;
  IMediaControl *pMediaControl;
  IMediaEvent   *pMediaEvent;
  IVideoWindow  *pVideoWindow;
  CoInitialize(NULL);
    
  CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC, 
    IID_IGraphBuilder, (void **)&pGraphBuilder);
  pGraphBuilder->QueryInterface(IID_IMediaControl,
    (void **)&pMediaControl);
  pGraphBuilder->QueryInterface(IID_IMediaEvent,
    (void **)&pMediaEvent);
  pGraphBuilder->QueryInterface(IID_IVideoWindow,
    (void **)&pVideoWindow);

  pGraphBuilder->RenderFile(L"//test.wmv", NULL);

  pVideoWindow->put_Owner((OAHWND)g_hwnd);
  pVideoWindow->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);

  RECT rect;
  GetClientRect(g_hWnd, &rect);
  pVideoWindow->SetWindowPosition(0, 0, rect.right, rect.bottom);

  pMediaControl->Run();

  long eventCode;
  pMediaEvent->WaitForCompletion(INFINITE, &eventCode);

  pVideoWindow->Release();
  pMediaControl->Release();
  pGraphBuilder->Release();
  CoUninitialize();
}

改动的内容还是用粗体表示。首先,设置典型子窗口的所有者和样式。然后,将视频播放窗口的大小和位置设置为与主应用程序窗口 (g_hWnd) 的客户端区域(在 rect 中加载)的大小和位置相同。

请注意,在前面的代码示例中,排除了错误处理以便容易阅读。为完整起见,包括错误处理的相同代码如下所示。

#include <dshow.h>
void __cdecl main(void)
{
  IGraphBuilder *pGraphBuilder;
  IMediaControl *pMediaControl;
  IMediaEvent   *pMediaEvent;
  IVideoWindow  *pVideoWindow;
  HRESULT hr = CoInitialize(NULL);
  if(FAILED(hr))
  {
    printf("ERROR: Couldn't initialize COM library!");
    return;
  }

  hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC, 
    IID_IGraphBuilder, (void **)&pGraphBuilder);
  if(FAILED(hr))
  {
    printf("ERROR: Couldn't create Filter Graph Manager instance!");
    return;
  }

  pGraphBuilder->QueryInterface(IID_IMediaControl,
    (void **)&pMediaControl);
  pGraphBuilder->QueryInterface(IID_IMediaEvent,
    (void **)&pMediaEvent);
  pGraphBuilder->QueryInterface(IID_IVideoWindow,
    (void **)&pVideoWindow);

  hr = pGraphBuilder->RenderFile(L"//test.wmv", NULL);
  if(SUCCEEDED(hr))
  {
    pVideoWindow->put_Owner((OAHWND)g_hwnd);
    pVideoWindow->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);

    RECT rect;
    GetClientRect(g_hWnd, &rect);
    pVideoWindow->SetWindowPosition(0, 0, rect.right, rect.bottom);

    hr = pMediaControl->Run();
    if(SUCCEEDED(hr))
    {
      long eventCode;
      pMediaEvent->WaitForCompletion(INFINITE, &eventCode);
    }
    else
      printf("ERROR: Couldn't run filter graph!");
  }
  else
    printf("ERROR: Couldn't render video file!");

  pVideoWindow->Release();
  pMediaControl->Release();
  pGraphBuilder->Release();
  CoUninitialize();
}

要接收 Filter Graph Manager 引发的事件,需要以下两个全局声明。

#define WM_FILTERGRAPHNOTIFY WM_APP + 1
IMediaEventEx *g_pMediaEventEx = NULL;

请注意,常量 WM_FILTERGRAPHNOTIFY 可设置为任何值,WM_APP + 1 就是一个示例。在播放该流(运行筛选器图形)之前,请使用以下代码。

g_pGraphBuilder->QueryInterface(IID_IMediaEventEx,
  (void **)&g_pMediaEventEx);
g_pMediaEventEx->SetNotifyWindow((OAHWND)g_hWnd,
  WM_FILTERGRAPHNOTIFY, 0);

上述代码指示 Filter Graph Manager 使用第二个参数 (WM_FILTERGRAPHNOTIFY) 的消息标识将事件发送到主应用程序窗口 (g_hWnd)。请注意,SetNotifyWindow 调用的第三个参数将作为窗口消息 (WM_FILTERGRAPHNOTIFY) 的 lParam 参数返回到应用程序。在前面的示例代码中不使用该参数,因此它设为零。然而,该参数可用于传递实例数据和事件。

现在,可以将以下代码添加到应用程序的消息循环(通常在 WndProc 函数中)。

case WM_FILTERGRAPHNOTIFY:
  HandleFilterGraphEvent();
  break;

然后,可以使用以下代码处理事件。

void HandleFilterGraphEvent()
{
  if (g_pMediaEventEx == NULL)
    return;

  long eventCode;
  LONG_PTR param1, param2;
  HRESULT hr;
  while(SUCCEEDED(g_pMediaEventEx->GetEvent(&eventCode,
    ¶m1, ¶m2, 0)))
  {
    g_pMediaEventEx->FreeEventParams(eventCode, param1, param2);
    switch(eventCode)
    {
      case EC_COMPLETE:
      case EC_USERABORT:
      case EC_ERRORABORT:
        g_pMediaControl->Stop();
        long eventCode;
        g_pMediaEvent->WaitForCompletion(INFINITE, &eventCode);
        g_pMediaEventEx->SetNotifyWindow(NULL, 0, 0);
        g_pMediaEventEx->Release();
        g_pMediaEventEx = NULL;
        // Do other clean-up (releases, etc.)
        PostQuitMessage(0);
        return;
    }
  } 
}

如果没有设置事件指针,将退出该处理,然后检索该队列上的所有事件。GetEvent 方法的第四个参数是等待事件的时间(以毫秒为单位)。因为来自 Filter Graph Manager 的事件已经在队列中,所以该参数可以设为零,这意味着不等待。请注意,EC_COMPLETE 事件不会自动停止筛选器图形的处理,因此在接收到该事件时停止该筛选器图形是一个好做法。

该介绍将使您能够在自己的应用程序中开始实现媒体播放,因此,现在本文将解决更复杂的捕获视频和声音的任务。

视频捕获

创建针对视频和音频捕获的筛选器图形比创建针对播放的筛选器更复杂。图 20 显示带有声音的视频捕获的典型筛选器图形。

20. 带有声音的视频捕获的筛选器图形

为了帮助创建和控制筛选器图形,DirectShow 提供了一个名为 Capture Graph Builder 的组件。就像对播放筛选器图形一样,首先创建一个 Filter Graph Manager 实例。然后,创建 Capture Graph Builder 实例并将两者相连。该本机代码如下所示。

IGraphBuilder *pGraphBuilder;
ICaptureGraphBuilder2 *pCaptureGraphBuilder;

CoCreateInstance(CLSID_FilterGraph, 0, CLSCTX_INPROC_SERVER,
  IID_IGraphBuilder, (void**)&pGraphBuilder);

CoCreateInstance(CLSID_CaptureGraphBuilder, NULL,
  CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2,
  (void**)&pCaptureGraphBuilder);

pCaptureGraphBuilder->SetFiltergraph(pGraphBuilder);

因为它将节省一些编码工作(特别是关于 COM 的代码),您可以使用以下活动模板库 (ATL) 代码。

CComPtr<IGraphBuilder>         pGraphBuilder;
CComPtr<ICaptureGraphBuilder2> pCaptureGraphBuilder;

pCaptureGraphBuilder.CoCreateInstance(CLSID_CaptureGraphBuilder);
pGraphBuilder.CoCreateInstance(CLSID_FilterGraph);
pCaptureGraphBuilder->SetFiltergraph(pGraphBuilder);

继续 ATL,下一步是使用以下代码初始化视频捕获筛选器(如前面的图 20 所示)。

CComPtr<IBaseFilter>         pVideoCapture;
CComPtr<IPersistPropertyBag> pPropertyBag;
DEVMGR_DEVICE_INFORMATION    di;
CPropertyBag                 PropBag;
CComVariant                  varCamName;
GUID guidCamera = { 0xCB998A05, 0x122C, 0x4166, 0x84, 0x6A,
                    0x93, 0x3E, 0x4D, 0x7E, 0x3C, 0x86 };

di.dwSize = sizeof(di);
HANDLE handle = FindFirstDevice(DeviceSearchByGuid, &guidCamera, &di);
FindClose(handle);
pVideoCapture.CoCreateInstance(CLSID_VideoCapture)); 
pVideoCapture.QueryInterface(&pPropertyBag));
varCamName = di.szLegacyName;
PropBag.Write(L"VCapName", &varCamName);   
pPropertyBag->Load(&PropBag, NULL);
pPropertyBag.Release();
pGraphBuilder->AddFilter(pVideoCapture, L"Video capture source");

第一个照相机捕获设备通过 FindFirstDevice 函数检索,该函数的第二个参数设置为 DEVCLASS_CAMERA_GUID(对应于在前面的代码中硬编码的 GUID [CB998A05-122C-4166-846A-933E4D7E3C86]),该检索方式是查找捕获设备的最可靠方式。属性包实例(PropBag 是实现 IPropertyBag 接口的自定义类 [CPropertyBag] 的实例)用于将捕获设备名称信息传递到捕获筛选器,然后该视频捕获筛选器添加到筛选器图形。

下一步是初始化音频捕获筛选器,为此您可以使用以下代码。

CComPtr<IBaseFilter> pAudioCaptureFilter;

pAudioCaptureFilter.CoCreateInstance(CLSID_AudioCapture);
pAudioCaptureFilter.QueryInterface(&pPropertyBag);
pPropertyBag->Load(NULL, NULL);
pGraphBuilder->AddFilter(pAudioCaptureFilter, L"Audio Capture Filter");

创建音频捕获筛选器并将其添加到筛选器图形。现在应该初始化视频编码器并将其添加到筛选器图形。您可以借助于DMO Wrapper 筛选器在筛选器图形中使用 DirectX 媒体对象 (DMO) 实例。要使用 DMO Wrapper 筛选器用 WMV 9 DMO 对视频进行编码,可以使用以下代码。

CComPtr<IBaseFilter>       pVideoEncoder;
CComPtr<IDMOWrapperFilter> pVideoWrapperFilter;

pVideoEncoder.CoCreateInstance(CLSID_DMOWrapperFilter);
pVideoEncoder.QueryInterface(&pVideoWrapperFilter);
pVideoWrapperFilter->Init(CLSID_CWMV9EncMediaObject,
  DMOCATEGORY_VIDEO_ENCODER);
pGraphBuilder->AddFilter(pVideoEncoder, L"WMV9 DMO Encoder");

WMV 9 编码器加载到 DMO Wrapper 筛选器中之后,就该加载 ASF 多路复用器和设置多路复用器的名称了(使用对多路复用器的文件接收接口的引用)。进行此操作的代码如下所示。

CComPtr<IBaseFilter>     pAsfWriter;
CComPtr<IFileSinkFilter> pFileSink;

pAsfWriter.CoCreateInstance(CLSID_ASFWriter);
pAsfWriter->QueryInterface(IID_IFileSinkFilter, (void**) &pFileSink);
pFileSink->SetFileName(L"//My Documents//test.asf", NULL);

现在已经创建了该图形中需要的所有筛选器。下一步是使用以下代码将这些筛选器的针连接在一起。

pCaptureGraphBuilder->RenderStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Video, pVideoCapture, pVideoEncoder, pAsfWriter );

pCaptureGraphBuilder->RenderStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Audio, pAudioCaptureFilter, NULL, pAsfWriter );

pCaptureGraphBuilder->RenderStream(&PIN_CATEGORY_PREVIEW,
  &MEDIATYPE_Video, pVideoCapture, NULL, NULL );

视频捕获通过视频编码器连接到多路复用器,然后音频捕获也连接到多路复用器。最后,视频捕获筛选器的预览针连接到视频输出程序。不需要指定视频输出程序(作为最后一个参数),因为它是默认指定的。

既然已经对这些筛选器进行了初始化,将这些筛选器添加到筛选器图形并且连接了所有针,现在就已经准备好使用下面的代码来捕获数据了。

CComPtr<IMediaControl> pMediaControl;
CComPtr<IMediaEvent>   pMediaEvent;
CComPtr<IMediaSeeking> pMediaSeeking;

pCaptureGraphBuilder->ControlStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Video, pVideoCapture, 0, 0 , 0, 0);
pCaptureGraphBuilder->ControlStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Audio, pAudioCaptureFilter, 0, 0, 0, 0);

pGraphBuilder.QueryInterface(&pMediaControl);
pMediaControl->Run();
Sleep(1000);

LONGLONG dwStart = 0;
LONGLONG dwEnd = MAXLONGLONG;
OutputDebugString(L"Starting to capture the first file" );
pCaptureGraphBuilder->ControlStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Video, pVideoCapture, &dwStart, &dwEnd, 0, 0);
pCaptureGraphBuilder->ControlStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Audio, pAudioCaptureFilter, &dwStart, &dwEnd, 0, 0);
Sleep(5000);

OutputDebugString(L"Stopping the capture");
pGraphBuilder.QueryInterface(&pMediaSeeking);
pMediaSeeking->GetCurrentPosition(&dwEnd);
pCaptureGraphBuilder->ControlStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Video, pVideoCapture, &dwStart, &dwEnd, 1, 2);
pCaptureGraphBuilder->ControlStream(&PIN_CATEGORY_CAPTURE,
  &MEDIATYPE_Audio, pAudioCaptureFilter, &dwStart, &dwEnd, 1, 2);

OutputDebugString(L"Wating for the control stream events");
pGraphBuilder.QueryInterface(&pMediaEvent);
long lEventCode;
LONG_PTR lParam1, lParam2;
do
{
  pMediaEvent->GetEvent(&lEventCode, &lParam1, &lParam2, INFINITE);
  pMediaEvent->FreeEventParams(lEventCode, lParam1, lParam2);

  if(lEventCode == EC_STREAM_CONTROL_STOPPED)
  {
    OutputDebugString(L"Received a control stream stop event");
    count++;
  }
} while(count < 2);

OutputDebugString(L"The file has been captured");

该捕获图形由视频和音频控制流阻塞,然后在实际捕获数据之前允许它运行一秒。该延迟为捕获图形提供了时间,以确保分配它所有的缓冲区以及同步所有进程。一个捕获进行 5 秒,然后停止。视频和音频的控制流用于停止该流,最后,一个循环等待一个标志该流停止的事件。

下面的代码片段显示一种更明确的方式来捕获图形正在运行的时刻(与等待 1 秒相比)。

OAFilterState state = State_Stopped;
pMediaControl->Run();  
while(state != State_Running)  
  pMediaControl->GetState(100, &state);

GetState 方法的第一个参数是超时(以毫秒为单位),因此该代码每 1/10 秒将尝试一次,以查看该捕获图形是否已经启动且正在运行。

有关视频捕获的更多详细信息,请参阅 Windows Mobile 5.0 Pocket PC SDK 附带的 CameraCapture 示例。

自定义筛选器

正如前面提到的,基本上有三种筛选器类型:源筛选器、转换筛选器和输出程序筛选器。源筛选器提供来自源的原始多媒体数据,如文件、URL 或类似照相机的实时源。源筛选器可以将原始数据传递到分析器或拆分器筛选器,也可以自己进行分析或拆分。输出程序筛选器接受完全处理的数据并在显示器或扬声器上进行呈现,它们包括编写文件的筛选器。源筛选器和输出程序筛选器之间的所有筛选器都是转换筛选器。转换筛选器使用原始数据或部分处理的数据,并在将其传递到下一个筛选器之前进行处理。有许多不同类型的转换筛选器;一些筛选器将字节流解析为示例或帧,而其他筛选器进行压缩或解压缩,甚至进行格式转换。

虽然 DirectShow 包括大量用于播放、转换和捕获许多不同媒体格式的现成筛选器,但是开发人员可以生成自己的自定义筛选器以便处理自定义或标准数据格式。实现自定义筛选器时,它可能是一个转换筛选器。它可能是一个将效果(如淡入或淡出)添加到视频流的筛选器。

DirectShow SDK 包括大量自定义筛选器,SDK 文档提供针对编写自定义筛选器的优秀介绍。以下来自 SDK 文档的摘要提供创建转换筛选器的基本步骤:

  • 确定筛选器是必须复制媒体示例还是适当地处理它们。媒体流中进行的复制越少越好。然而,某些筛选器需要一个复制操作;该要求影响基类的选择。

  • 确定使用哪些基类并从基类派生筛选器类(如果需要,也可以派生针类)。在该步骤中,为筛选器创建一个或多个标头。在许多情况中,可以使用转换基类,从正确的转换筛选器类派生类,以及重写几个成员函数。在其他情况中,可以使用更通用的基类。这些类实现了大部分的连接和协商机制;但这些类也提供在重写更多成员函数的开销方面的灵活性。

  • 添加实例化筛选器所需的代码。该步骤需要将静态 CreateInstance 成员函数添加到派生的类中,该类还是一个全局数组,包含筛选器名称、CLSID 和指向该成员函数的指针。

  • 调用 NonDelegatingQueryInterface 函数在您的筛选器中分布任何唯一的接口。该步骤强调实现接口的 COM 方面,而不是基类中的其他方面。

  • 重写基类成员函数。该步骤包括编写对于筛选器而言唯一的转换函数,以及重写连接过程所需的几个成员函数,如设置分配器大小或提供媒体类型。

小结

Windows Mobile 5.0 软件中针对多媒体的扩展支持可以增强您的托管和本机应用程序,诸如图片选择和照相机捕获对话框这样的高级构造可以轻松集成到应用程序中。对于许多高级媒体播放方案,媒体播放器控件是一个有效的选择。要获取更多的控制和灵活性,DirectShow API 提供低级别功能来捕获、解码、呈现和转换视频和音频流。使用这些资源,您可以实现用户需要的多媒体要求。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值