使用 Windows Mobile 5.0 中的圖片、視頻和照相機

使用 Windows Mobile 5.0 中的圖片、視頻和照相機

簡介

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) 時,將顯示一個快捷菜單,可從中選擇從文件添加展示 (From File),方法是拍一張新照片 (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 方面,而不是基類中的其他方面。

  • 重寫基類成員函數。該步驟包括編寫對於篩選器而言唯一的轉換函數,以及重寫連接過程所需的幾個成員函數,如設置分配器大小或提供媒體類型。

有關更多信息,請參閱 SDK 文檔

小結

Windows Mobile 5.0 軟件中針對多媒體的擴展支持可以增強您的託管和本機應用程序,諸如圖片選擇和照相機捕獲對話框這樣的高級構造可以輕鬆集成到應用程序中。對於許多高級媒體播放方案,媒體播放器控件是一個有效的選擇。要獲取更多的控制和靈活性,DirectShow API 提供低級別功能來捕獲、解碼、呈現和轉換視頻和音頻流。使用這些資源,您可以實現用戶需要的多媒體要求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章