使用 MediaFrameReader 處理媒體畫面

本文示範如何將 MediaFrameReaderMediaCapture 結合使用,從一個或多個可用來源獲取媒體畫面,包括色彩、深度和紅外線相機、音訊裝置,甚至自訂畫面來源 (例如生成骨骼追蹤畫面的畫面來源)。 此功能是針對要讓執行媒體畫面即時處理的應用程式使用所設計,例如擴增實境及深度感知相機應用程式。

如果您只想要擷取視訊或相片,例如典型的攝影應用程式,您可能想要使用 MediaCapture 支援的其中一種其他擷取技術。 如需可用媒體擷取技術和文章的清單,示範如何使用它們,請參閱 相機

注意

本文討論的功能僅從 Windows 10 版本 1607 開始可用。

注意

有一個通用 Windows 應用程式範例示範如何使用 MediaFrameReader 來顯示來自不同畫面來源的畫面,包括色彩、深度和紅外線相機。 如需詳細資訊,請參閱ClaimsAwareWebService

注意

Windows 10 版本 1803 中引入了一組新的 API,用於將 MediaFrameReader 與音訊資料結合使用。 如需詳細資訊,請參閱使用 MediaFrameReader 處理音訊畫面

設定您的專案

如同任何使用 MediaCapture 的應用程式,您必須先宣告您的應用程式使用網路攝影機功能,才能嘗試存取任何攝影機裝置。 如果您的應用程式會從音訊裝置擷取,您也應該宣告麥克風裝置功能。

將功能新增到應用程式資訊清單

  1. 在 Microsoft Visual Studio 的 [方案總管] 中,按兩下 package.appxmanifest 項目,開啟應用程式資訊清單的設計工具。
  2. 選取 [功能] 索引標籤。
  3. 核取 [網路相機] 方塊和 [麥克風] 方塊。
  4. 若要存取圖片和影片媒體櫃,請勾選圖片媒體櫃的方塊和影片媒體櫃的方塊。

本文中的範例程式碼除了預設專案範本所包含的 API 之外,也會使用來自下列命名空間的 API。

using Windows.Media.Capture.Frames;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Media.MediaProperties;
using Windows.Graphics.Imaging;
using System.Threading;
using Windows.UI.Core;
using System.Threading.Tasks;
using Windows.Media.Core;
using System.Diagnostics;
using Windows.Media;
using Windows.Media.Devices;
using Windows.Media.Audio;

選取畫面來源和畫面來源群組

處理媒體畫面的許多應用程式需要一次從多個來源取得畫面,例如裝置的色彩和深度相機。 MediaFrameSourceGroup 物件代表一組可以同時使用的媒體畫面來源。 呼叫靜態方法 MediaFrameSourceGroup.FindAllAsync,以取得目前裝置所支援之所有畫面來源群組的清單。

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

您也可以使用 DeviceInformation.CreateWatcherMediaFrameSourceGroup.GetDeviceSelector 傳回的值來建立 DeviceWatcher,以在裝置上可用的畫面來源群組變更時接收通知,例如插入外部相機時。 如需詳細資訊,請參閱列舉裝置

MediaFrameSourceGroup 有一 MediaFrameSourceInfo 集合物件,可描述群組中包含的畫面來源。 擷取裝置上可用的畫面來源群組之後,您可以選取公開您感興趣的畫面來源群組。

下列範例顯示選取畫面來源群組的最簡單方式。 此程式碼只會迴圈所有可用的群組,然後迴圈查看 SourceInfos 集合中的每個專案。 系統會檢查每個 MediaFrameSourceInfo,以查看它是否支援我們所尋求的功能。 在此案例中,會檢查 MediaStreamType 屬性是否為 VideoPreview 值,這表示裝置會提供視訊預覽串流,並檢查 SourceKind 屬性的值 Color,指出來源提供色彩畫面。

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

MediaFrameSourceGroup selectedGroup = null;
MediaFrameSourceInfo colorSourceInfo = null;

foreach (var sourceGroup in frameSourceGroups)
{
    foreach (var sourceInfo in sourceGroup.SourceInfos)
    {
        if (sourceInfo.MediaStreamType == MediaStreamType.VideoPreview
            && sourceInfo.SourceKind == MediaFrameSourceKind.Color)
        {
            colorSourceInfo = sourceInfo;
            break;
        }
    }
    if (colorSourceInfo != null)
    {
        selectedGroup = sourceGroup;
        break;
    }
}

這個識別所需畫面來源群組和畫面來源的方法適用於簡單的案例,但如果您想要根據更複雜的準則來選取畫面來源,它很快就會變得麻煩。 另一種方法是使用 Linq 語法和匿名物件進行選取。 下列範例會使用 Select 擴充方法,將 frameSourceGroups 清單中的 MediaFrameSourceGroup 物件轉換成具有兩個欄位的匿名物件:sourceGroup,代表群組本身,以及代表群組中色彩畫面來源的 colorSourceInfocolorSourceInfo 欄位會設定為 FirstOrDefault 的結果,它會選取所提供述詞解析為 true 的第一個物件。 在此案例中,如果串流類型為 VideoPreview,則述詞為 true,來源類型為 Color,如果相機位於裝置的前面板上,則為 True。

從上述查詢傳回的匿名物件清單中,使用 Where 擴充方法只選取 colorSourceInfo 欄位不是 null 的物件。 最後,會呼叫 FirstOrDefault 來選取清單中的第一個項目。

現在,您可以使用所選物件的欄位來取得所選取 MediaFrameSourceGroup 和 代表色彩相機的 MediaFrameSourceInfo 物件參考。 稍後會使用這些物件來初始化 MediaCapture 物件,並為選取的來源建立 MediaFrameReader 。 最後,您應該測試來源群組是否為 Null,這表示目前的裝置沒有您要求的擷取來源。

var selectedGroupObjects = frameSourceGroups.Select(group =>
   new
   {
       sourceGroup = group,
       colorSourceInfo = group.SourceInfos.FirstOrDefault((sourceInfo) =>
       {
           // On Xbox/Kinect, omit the MediaStreamType and EnclosureLocation tests
           return sourceInfo.MediaStreamType == MediaStreamType.VideoPreview
           && sourceInfo.SourceKind == MediaFrameSourceKind.Color
           && sourceInfo.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front;
       })

   }).Where(t => t.colorSourceInfo != null)
   .FirstOrDefault();

MediaFrameSourceGroup selectedGroup = selectedGroupObjects?.sourceGroup;
MediaFrameSourceInfo colorSourceInfo = selectedGroupObjects?.colorSourceInfo;

if (selectedGroup == null)
{
    return;
}

下列範例會使用上述類似的技術來選取包含色彩、深度和紅外線相機的來源群組。

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Depth),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Infrared),
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with color, depth or infrared found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo colorSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[0];
MediaFrameSourceInfo infraredSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[1];
MediaFrameSourceInfo depthSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[2];

注意

從 Windows 10 版本 1803 開始,您可以使用 MediaCaptureVideoProfile 類別來選取具有一組所需功能的媒體畫面來源。 如需詳細資訊,請參閱本文稍後使用影片設定檔選取畫面來源一節

初始化 MediaCapture 物件以使用選取的畫面來源群組

下一個步驟是初始化 MediaCapture 物件,以使用您在上一個步驟中選取的畫面來源群組。

MediaCapture 物件通常會從應用程式內的多個位置使用,因此您應該宣告類別成員變數來保存它。

MediaCapture mediaCapture;

透過叫用構造函式建立 MediaCapture 物件的執行個體。 接下來,建立 MediaCaptureInitializationSettings 設定將用來初始化 MediaCapture 物件的物件。 此範例使用下列的設定:

  • SourceGroup - 這會告知系統您要用來取得畫面的來源群組。 請記住,來源群組會定義一組可同時使用的媒體畫面來源。
  • SharingMode - 這會告知系統您是否需要對擷取來源裝置進行獨佔控制。 如果您將此設定為 ExclusiveControl,表示您可以變更擷取裝置的設定,例如它所產生的畫面格式,但這表示如果另一個應用程式已經有獨佔控制項,當應用程式嘗試初始化媒體擷取裝置時,您的應用程式將會失敗。 如果您將此設定為 SharedReadOnly,即使畫面來源正在使用另一個應用程式,您仍可接收來自畫面來源的畫面,但無法變更裝置的設定。
  • MemoryPreference - 如果您指定 CPU,系統會使用 CPU 記憶體,保證畫面到達時,它們會以 SoftwareBitmap 物件的形式提供。 如果您指定 [自動],系統會動態選擇最佳的記憶體位置來儲存畫面。 如果系統選擇使用 GPU 記憶體,媒體畫面就會以 IDirect3DSurface 物件的形式抵達,而不是 SoftwareBitmap
  • StreamingCaptureMode - 將此設定為 Video,表示不需要串流處理音訊。

呼叫 InitializeAsync 以使用所需的設定來初始化 MediaCapture 。 如果初始化失敗,請務必在 try 區塊內呼叫此專案。

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

設定畫面來源的慣用格式

若要設定畫面來源的慣用格式,您需要取得代表來源的 MediaFrameSource 物件。 您可以藉由存取初始化之 MediaCapture 物件的 Frames 字典來取得這個物件,並指定您想要使用的畫面來源識別碼。 這就是為什麼我們在選取畫面來源群組時儲存 MediaFrameSourceInfo 物件的原因。

MediaFrameSource.SupportedFormats 屬性包含一份 MediaFrameFormat 物件清單,描述畫面來源支援的格式。 使用 Where Linq 擴充方法,根據所需的屬性選取格式。 在此範例中,選取格式的寬度為 1080 像素,而且可以提供 32 位 RGB 格式的畫面。 FirstOrDefault 擴充方法會選取清單中的第一個項目。 如果選取的格式為 Null,則畫面來源不支援所要求的格式。 如果支援格式,您可以呼叫 SetFormatAsync 來要求來源使用此格式。

var colorFrameSource = mediaCapture.FrameSources[colorSourceInfo.Id];
var preferredFormat = colorFrameSource.SupportedFormats.Where(format =>
{
    return format.VideoFormat.Width >= 1080
    && format.Subtype == MediaEncodingSubtypes.Argb32;

}).FirstOrDefault();

if (preferredFormat == null)
{
    // Our desired format is not supported
    return;
}

await colorFrameSource.SetFormatAsync(preferredFormat);

建立畫面來源的畫面讀取器

若要接收媒體畫面來源的畫面,請使用 MediaFrameReader

MediaFrameReader mediaFrameReader;

在初始化的 MediaCapture 物件上呼叫 CreateFrameReaderAsync 來具現化畫面讀取器。 此方法的第一個引數是您要接收畫面的畫面來源。 您可以為每個您想要使用的畫面來源建立個別的畫面讀取器。 第二個引數會告知系統您想要畫面到達的輸出格式。 這可讓您不必在畫面到達時進行自己的轉換到畫面。 請注意,如果您指定畫面來源不支援的格式,則會擲回例外狀況,因此請確定此值位於 SupportedFormats 集合中。

建立畫面讀取器之後,請註冊 FrameArrived 事件的處理常式,每當來源有新的畫面可用時,就會引發此事件。

呼叫 StartAsync,告知系統開始從來源讀取畫面。

mediaFrameReader = await mediaCapture.CreateFrameReaderAsync(colorFrameSource, MediaEncodingSubtypes.Argb32);
mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await mediaFrameReader.StartAsync();

處理畫面到達事件

只要有新畫面可用,就會引發 MediaFrameReader.FrameArrived 事件。 您可以選擇處理抵達的每個畫面,或只在需要畫面時使用畫面。 因為畫面讀取器會在自己的執行緒上引發事件,因此您可能需要實作一些同步處理邏輯,以確保您不會嘗試從多個執行緒存取相同的資料。 本節說明如何將繪圖色彩畫面同步處理至 XAML 頁面中的影像控制項。 此案例可解決需要在 UI 執行緒上執行 XAML 控制項之所有更新的額外同步處理條件約束。

在 XAML 中顯示畫面的第一個步驟是建立影像控制項。

<Image x:Name="imageElement" Width="320" Height="240" />

在您的程式碼後置頁面中,宣告 SoftwareBitmap 類型的類別成員變數,該變數將做為所有傳入影像將複製到其中的後端緩衝區。 請注意,影像資料本身不會複製,只會複製物件參考。 另外,請宣告一個布林值來追蹤我們的 UI 作業目前是否正在執行。

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

由於畫面會以 SoftwareBitmap 物件的形式抵達,因此您必須建立 SoftwareBitmapSource 物件,這可讓您使用 SoftwareBitmap 做為 XAML 控制項的來源。 您應該先在程式碼的某個位置設定影像來源,再啟動畫面讀取器。

imageElement.Source = new SoftwareBitmapSource();

現在是時候實作 FrameArrived 事件處理常式了。 呼叫處理常式時,sender 參數會包含引發事件的 MediaFrameReader 物件的參考。 在此物件上呼叫 TryAcquireLatestFrame,以嘗試取得最新的畫面。 顧名思義,TryAcquireLatestFrame 可能無法成功傳回畫面。 因此,當您存取 VideoMediaFrame 和 SoftwareBitmap 屬性時,請務必測試 null。 在此範例中,null 條件運算子? 用來存取 SoftwareBitmap,然後檢查擷取的物件是否為 null。

影像控制項只能以 BRGA8 格式顯示預先乘法或沒有 Alpha 的影像。 如果到達的畫面不是該格式,靜態方法 Convert 會用來將軟體點陣圖轉換成正確的格式。

接下來,Interlocked.Exchange 方法用於將到達點陣圖的參考與後端緩衝點陣圖交換。 這個方法會在安全執行緒的不可部分完成作業中交換這些參考。 交換之後,現在在 softwareBitmap 變數中會處置舊的後端緩衝區影像,以清除其資源。

接下來,與 Image 元素相關聯的 CoreDispatcher 會用來建立工作,以呼叫 RunAsync 在 UI 執行緒上執行。 因為非同步工作會在工作內執行,因此傳遞至 RunAsync 的 Lambda 運算式會以 async 關鍵字宣告。

在工作中,會檢查 _taskRunning 變數,以確保一次只執行一個工作執行個案。 如果工作尚未執行,_taskRunning 會設定為 true,以防止工作再次執行。 在 while 迴圈中,會呼叫 Interlocked.Exchange,從後端緩衝區複製到暫存 SoftwareBitmap,直到後端緩衝區影像為 null 為止。 每次填入暫存點陣圖時,ImageSource 屬性都會轉換成 SoftwareBitmapSource,然後呼叫 SetBitmapAsync 來設定影像的來源。

最後,_taskRunning 變數會設定回 false,以便在下次呼叫處理常式時再次執行工作。

注意

如果您存取 MediaFrameReferenceVideoMediaFrame 屬性所提供的 SoftwareBitmapDirect3DSurface 物件,系統會對這些物件建立強式參考,這表示當您在包含 MediaFrameReference 上呼叫 Dispose 時,將不會處置這些物件。 您必須明確地呼叫SoftwareBitmapDirect3DSurfaceDispose 方法,才能立即處置物件。 否則,垃圾收集器最終會釋放這些物件的記憶體,但您不知道何時會發生此情況,而且如果配置的點陣圖或表面數目超過系統允許的最大數量,新畫面的流程將會停止。 例如,您可以使用 SoftwareBitmap.Copy 方法來複製擷取的畫面,然後釋放原始畫面以克服這項限制。 另外,如果使用多載 CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize)CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype),建立 MediaFrameReader,傳回的畫面是原始畫面資料的複本,因此它們在保留時不會導致畫面擷取停止。

private void ColorFrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
    var mediaFrameReference = sender.TryAcquireLatestFrame();
    var videoMediaFrame = mediaFrameReference?.VideoMediaFrame;
    var softwareBitmap = videoMediaFrame?.SoftwareBitmap;

    if (softwareBitmap != null)
    {
        if (softwareBitmap.BitmapPixelFormat != Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8 ||
            softwareBitmap.BitmapAlphaMode != Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)
        {
            softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }

        // Swap the processed frame to _backBuffer and dispose of the unused image.
        softwareBitmap = Interlocked.Exchange(ref backBuffer, softwareBitmap);
        softwareBitmap?.Dispose();

        // Changes to XAML ImageElement must happen on UI thread through Dispatcher
        var task = imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
            async () =>
            {
                // Don't let two copies of this task run at the same time.
                if (taskRunning)
                {
                    return;
                }
                taskRunning = true;

                // Keep draining frames from the backbuffer until the backbuffer is empty.
                SoftwareBitmap latestBitmap;
                while ((latestBitmap = Interlocked.Exchange(ref backBuffer, null)) != null)
                {
                    var imageSource = (SoftwareBitmapSource)imageElement.Source;
                    await imageSource.SetBitmapAsync(latestBitmap);
                    latestBitmap.Dispose();
                }

                taskRunning = false;
            });
    }

    mediaFrameReference.Dispose();
}

清除資源

當您完成讀取畫面時,請務必呼叫 StopAsync、取消註冊 FrameArrived 處理常式,以及處置 MediaCapture 物件,來停止媒體畫面讀取器。

await mediaFrameReader.StopAsync();
mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
mediaCapture.Dispose();
mediaCapture = null;

如需在應用程式暫停時清除媒體擷取物件的詳細資訊,請參閱顯示相機預覽

FrameRenderer 協助程式類別

通用 Windows 相機畫面範例提供協助程式類別,可讓您輕鬆地從應用程式中的色彩、紅外線和深度來源顯示畫面。 一般而言,除了將深度和紅外線資料顯示到畫面之外,您會想要執行更多動作,但此協助程式類別是示範畫面讀取器功能並偵錯您自己的畫面讀取器實作的實用工具。

FrameRenderer 協助程式類別會實作下列方法。

  • FrameRenderer 建構函式 - 建構函式會初始化協助程式類別,以使用您傳入的 XAML Image 元素來顯示媒體畫面。
  • ProcessFrame - 這個方法會在您傳入建構函式的 Image 元素中,顯示 MediaFrameReference 所代表的媒體畫面。 您通常應該從 FrameArrived 事件處理常式呼叫這個方法,並傳入 TryAcquireLatestFrame 所傳回的畫面。
  • ConvertToDisplayableImage - 此方法會檢查媒體畫面的格式,並視需要將它轉換成可顯示的格式。 對於彩色影像,這表示確定色彩格式為 BGRA8,且點陣圖 Alpha 模式為預乘。 針對深度或紅外線畫面,會處理每個掃描線,以使用範例中包含的 PsuedoColorHelper 類別,將深度或紅外線值轉換成偽彩色漸層。

注意

若要對 SoftwareBitmap 影像執行像素操作,您必須存取原生記憶體緩衝區。 若要這樣做,您必須使用下列程式碼清單中所包含的 IMemoryBufferByteAccess COM 介面,而且您必須更新專案屬性,以允許編譯不安全的程式碼。 如需詳細資訊,請參閱 建立、編輯和儲存點陣圖影像

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

class FrameRenderer
{
    private Image _imageElement;
    private SoftwareBitmap _backBuffer;
    private bool _taskRunning = false;

    public FrameRenderer(Image imageElement)
    {
        _imageElement = imageElement;
        _imageElement.Source = new SoftwareBitmapSource();
    }

    // Processes a MediaFrameReference and displays it in a XAML image control
    public void ProcessFrame(MediaFrameReference frame)
    {
        var softwareBitmap = FrameRenderer.ConvertToDisplayableImage(frame?.VideoMediaFrame);
        if (softwareBitmap != null)
        {
            // Swap the processed frame to _backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);

            // UI thread always reset _backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                async () =>
                {
                    // Don't let two copies of this task run at the same time.
                    if (_taskRunning)
                    {
                        return;
                    }
                    _taskRunning = true;

                    // Keep draining frames from the backbuffer until the backbuffer is empty.
                    SoftwareBitmap latestBitmap;
                    while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }



    // Function delegate that transforms a scanline from an input image to an output image.
    private unsafe delegate void TransformScanline(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes);
    /// <summary>
    /// Determines the subtype to request from the MediaFrameReader that will result in
    /// a frame that can be rendered by ConvertToDisplayableImage.
    /// </summary>
    /// <returns>Subtype string to request, or null if subtype is not renderable.</returns>

    public static string GetSubtypeForFrameReader(MediaFrameSourceKind kind, MediaFrameFormat format)
    {
        // Note that media encoding subtypes may differ in case.
        // https://docs.microsoft.com/en-us/uwp/api/Windows.Media.MediaProperties.MediaEncodingSubtypes

        string subtype = format.Subtype;
        switch (kind)
        {
            // For color sources, we accept anything and request that it be converted to Bgra8.
            case MediaFrameSourceKind.Color:
                return Windows.Media.MediaProperties.MediaEncodingSubtypes.Bgra8;

            // The only depth format we can render is D16.
            case MediaFrameSourceKind.Depth:
                return String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.D16, StringComparison.OrdinalIgnoreCase) ? subtype : null;

            // The only infrared formats we can render are L8 and L16.
            case MediaFrameSourceKind.Infrared:
                return (String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L8, StringComparison.OrdinalIgnoreCase) ||
                    String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L16, StringComparison.OrdinalIgnoreCase)) ? subtype : null;

            // No other source kinds are supported by this class.
            default:
                return null;
        }
    }

    /// <summary>
    /// Converts a frame to a SoftwareBitmap of a valid format to display in an Image control.
    /// </summary>
    /// <param name="inputFrame">Frame to convert.</param>

    public static unsafe SoftwareBitmap ConvertToDisplayableImage(VideoMediaFrame inputFrame)
    {
        SoftwareBitmap result = null;
        using (var inputBitmap = inputFrame?.SoftwareBitmap)
        {
            if (inputBitmap != null)
            {
                switch (inputFrame.FrameReference.SourceKind)
                {
                    case MediaFrameSourceKind.Color:
                        // XAML requires Bgra8 with premultiplied alpha.
                        // We requested Bgra8 from the MediaFrameReader, so all that's
                        // left is fixing the alpha channel if necessary.
                        if (inputBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8)
                        {
                            System.Diagnostics.Debug.WriteLine("Color frame in unexpected format.");
                        }
                        else if (inputBitmap.BitmapAlphaMode == BitmapAlphaMode.Premultiplied)
                        {
                            // Already in the correct format.
                            result = SoftwareBitmap.Copy(inputBitmap);
                        }
                        else
                        {
                            // Convert to premultiplied alpha.
                            result = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
                        }
                        break;

                    case MediaFrameSourceKind.Depth:
                        // We requested D16 from the MediaFrameReader, so the frame should
                        // be in Gray16 format.
                        if (inputBitmap.BitmapPixelFormat == BitmapPixelFormat.Gray16)
                        {
                            // Use a special pseudo color to render 16 bits depth frame.
                            var depthScale = (float)inputFrame.DepthMediaFrame.DepthFormat.DepthScaleInMeters;
                            var minReliableDepth = inputFrame.DepthMediaFrame.MinReliableDepth;
                            var maxReliableDepth = inputFrame.DepthMediaFrame.MaxReliableDepth;
                            result = TransformBitmap(inputBitmap, (w, i, o) => PseudoColorHelper.PseudoColorForDepth(w, i, o, depthScale, minReliableDepth, maxReliableDepth));
                        }
                        else
                        {
                            System.Diagnostics.Debug.WriteLine("Depth frame in unexpected format.");
                        }
                        break;

                    case MediaFrameSourceKind.Infrared:
                        // We requested L8 or L16 from the MediaFrameReader, so the frame should
                        // be in Gray8 or Gray16 format. 
                        switch (inputBitmap.BitmapPixelFormat)
                        {
                            case BitmapPixelFormat.Gray16:
                                // Use pseudo color to render 16 bits frames.
                                result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor16BitInfrared);
                                break;

                            case BitmapPixelFormat.Gray8:
                                // Use pseudo color to render 8 bits frames.
                                result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor8BitInfrared);
                                break;
                            default:
                                System.Diagnostics.Debug.WriteLine("Infrared frame in unexpected format.");
                                break;
                        }
                        break;
                }
            }
        }

        return result;
    }



    /// <summary>
    /// Transform image into Bgra8 image using given transform method.
    /// </summary>
    /// <param name="softwareBitmap">Input image to transform.</param>
    /// <param name="transformScanline">Method to map pixels in a scanline.</param>

    private static unsafe SoftwareBitmap TransformBitmap(SoftwareBitmap softwareBitmap, TransformScanline transformScanline)
    {
        // XAML Image control only supports premultiplied Bgra8 format.
        var outputBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8,
            softwareBitmap.PixelWidth, softwareBitmap.PixelHeight, BitmapAlphaMode.Premultiplied);

        using (var input = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
        using (var output = outputBitmap.LockBuffer(BitmapBufferAccessMode.Write))
        {
            // Get stride values to calculate buffer position for a given pixel x and y position.
            int inputStride = input.GetPlaneDescription(0).Stride;
            int outputStride = output.GetPlaneDescription(0).Stride;
            int pixelWidth = softwareBitmap.PixelWidth;
            int pixelHeight = softwareBitmap.PixelHeight;

            using (var outputReference = output.CreateReference())
            using (var inputReference = input.CreateReference())
            {
                // Get input and output byte access buffers.
                byte* inputBytes;
                uint inputCapacity;
                ((IMemoryBufferByteAccess)inputReference).GetBuffer(out inputBytes, out inputCapacity);
                byte* outputBytes;
                uint outputCapacity;
                ((IMemoryBufferByteAccess)outputReference).GetBuffer(out outputBytes, out outputCapacity);

                // Iterate over all pixels and store converted value.
                for (int y = 0; y < pixelHeight; y++)
                {
                    byte* inputRowBytes = inputBytes + y * inputStride;
                    byte* outputRowBytes = outputBytes + y * outputStride;

                    transformScanline(pixelWidth, inputRowBytes, outputRowBytes);
                }
            }
        }

        return outputBitmap;
    }



    /// <summary>
    /// A helper class to manage look-up-table for pseudo-colors.
    /// </summary>

    private static class PseudoColorHelper
    {
        #region Constructor, private members and methods

        private const int TableSize = 1024;   // Look up table size
        private static readonly uint[] PseudoColorTable;
        private static readonly uint[] InfraredRampTable;

        // Color palette mapping value from 0 to 1 to blue to red colors.
        private static readonly Color[] ColorRamp =
        {
            Color.FromArgb(a:0xFF, r:0x7F, g:0x00, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0x00, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0x7F, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0xFF, b:0x00),
            Color.FromArgb(a:0xFF, r:0x7F, g:0xFF, b:0x7F),
            Color.FromArgb(a:0xFF, r:0x00, g:0xFF, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x7F, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0x7F),
        };

        static PseudoColorHelper()
        {
            PseudoColorTable = InitializePseudoColorLut();
            InfraredRampTable = InitializeInfraredRampLut();
        }

        /// <summary>
        /// Maps an input infrared value between [0, 1] to corrected value between [0, 1].
        /// </summary>
        /// <param name="value">Input value between [0, 1].</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]  // Tell the compiler to inline this method to improve performance

        private static uint InfraredColor(float value)
        {
            int index = (int)(value * TableSize);
            index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
            return InfraredRampTable[index];
        }

        /// <summary>
        /// Initializes the pseudo-color look up table for infrared pixels
        /// </summary>

        private static uint[] InitializeInfraredRampLut()
        {
            uint[] lut = new uint[TableSize];
            for (int i = 0; i < TableSize; i++)
            {
                var value = (float)i / TableSize;
                // Adjust to increase color change between lower values in infrared images

                var alpha = (float)Math.Pow(1 - value, 12);
                lut[i] = ColorRampInterpolation(alpha);
            }

            return lut;
        }



        /// <summary>
        /// Initializes pseudo-color look up table for depth pixels
        /// </summary>
        private static uint[] InitializePseudoColorLut()
        {
            uint[] lut = new uint[TableSize];
            for (int i = 0; i < TableSize; i++)
            {
                lut[i] = ColorRampInterpolation((float)i / TableSize);
            }

            return lut;
        }



        /// <summary>
        /// Maps a float value to a pseudo-color pixel
        /// </summary>
        private static uint ColorRampInterpolation(float value)
        {
            // Map value to surrounding indexes on the color ramp
            int rampSteps = ColorRamp.Length - 1;
            float scaled = value * rampSteps;
            int integer = (int)scaled;
            int index =
                integer < 0 ? 0 :
                integer >= rampSteps - 1 ? rampSteps - 1 :
                integer;

            Color prev = ColorRamp[index];
            Color next = ColorRamp[index + 1];

            // Set color based on ratio of closeness between the surrounding colors
            uint alpha = (uint)((scaled - integer) * 255);
            uint beta = 255 - alpha;
            return
                ((prev.A * beta + next.A * alpha) / 255) << 24 | // Alpha
                ((prev.R * beta + next.R * alpha) / 255) << 16 | // Red
                ((prev.G * beta + next.G * alpha) / 255) << 8 |  // Green
                ((prev.B * beta + next.B * alpha) / 255);        // Blue
        }


        /// <summary>
        /// Maps a value in [0, 1] to a pseudo RGBA color.
        /// </summary>
        /// <param name="value">Input value between [0, 1].</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]

        private static uint PseudoColor(float value)
        {
            int index = (int)(value * TableSize);
            index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
            return PseudoColorTable[index];
        }

        #endregion

        /// <summary>
        /// Maps each pixel in a scanline from a 16 bit depth value to a pseudo-color pixel.
        /// </summary>
        /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>
        /// <param name="depthScale">Physical distance that corresponds to one unit in the input scanline.</param>
        /// <param name="minReliableDepth">Shortest distance at which the sensor can provide reliable measurements.</param>
        /// <param name="maxReliableDepth">Furthest distance at which the sensor can provide reliable measurements.</param>

        public static unsafe void PseudoColorForDepth(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes, float depthScale, float minReliableDepth, float maxReliableDepth)
        {
            // Visualize space in front of your desktop.
            float minInMeters = minReliableDepth * depthScale;
            float maxInMeters = maxReliableDepth * depthScale;
            float one_min = 1.0f / minInMeters;
            float range = 1.0f / maxInMeters - one_min;

            ushort* inputRow = (ushort*)inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                var depth = inputRow[x] * depthScale;

                if (depth == 0)
                {
                    // Map invalid depth values to transparent pixels.
                    // This happens when depth information cannot be calculated, e.g. when objects are too close.
                    outputRow[x] = 0;
                }
                else
                {
                    var alpha = (1.0f / depth - one_min) / range;
                    outputRow[x] = PseudoColor(alpha * alpha);
                }
            }
        }



        /// <summary>
        /// Maps each pixel in a scanline from a 8 bit infrared value to a pseudo-color pixel.
        /// </summary>
        /// /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>

        public static unsafe void PseudoColorFor8BitInfrared(
            int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
        {
            byte* inputRow = inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                outputRow[x] = InfraredColor(inputRow[x] / (float)Byte.MaxValue);
            }
        }

        /// <summary>
        /// Maps each pixel in a scanline from a 16 bit infrared value to a pseudo-color pixel.
        /// </summary>
        /// <param name="pixelWidth">Width of the input scanline.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>

        public static unsafe void PseudoColorFor16BitInfrared(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
        {
            ushort* inputRow = (ushort*)inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                outputRow[x] = InfraredColor(inputRow[x] / (float)UInt16.MaxValue);
            }
        }
    }


    // Displays the provided softwareBitmap in a XAML image control.
    public void PresentSoftwareBitmap(SoftwareBitmap softwareBitmap)
    {
        if (softwareBitmap != null)
        {
            // Swap the processed frame to _backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);

            // UI thread always reset _backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                async () =>
                {
                    // Don't let two copies of this task run at the same time.
                    if (_taskRunning)
                    {
                        return;
                    }
                    _taskRunning = true;

                    // Keep draining frames from the backbuffer until the backbuffer is empty.
                    SoftwareBitmap latestBitmap;
                    while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }
}

使用 MultiSourceMediaFrameReader 從多個來源取得與時間相互關聯的畫面。

從 Windows 10 版本 1607 開始,您可以使用 MultiSourceMediaFrameReader 接收來自多個來源的時間核心畫面。 此 API 可讓您更輕鬆地進行處理,此處理需要來自多個來源的畫面,這些畫面需要接近時態性,例如使用 DepthCorrelatedCoordinateMapper 類別。 使用這個新方法的其中一個限制是,畫面到達的事件只會以最慢的擷取來源速率提高。 將會卸除來自較快來源的額外畫面。 此外,由於系統預期畫面會以不同的速率從不同的來源抵達,因此不會自動辨識來源是否已完全停止產生畫面。 本節中的範例程式碼示範如何使用 事件來建立您自己的逾時邏輯,如果相互關聯的畫面未到達應用程式定義的時間限制,就會叫用該邏輯。

使用 MultiSourceMediaFrameReader 的步驟與本文先前所述的使用 MediaFrameReader 的步驟類似。 此範例會使用色彩來源和深度來源。 宣告一些字串變數來儲存將用來從每個來源選取畫面的媒體畫面來源識別碼。 接下來,宣告 ManualResetEventSlimCancellationTokenSource,以及將用來執行個體範例逾時邏輯的 EventHandler

private MultiSourceMediaFrameReader _multiFrameReader = null;
private string _colorSourceId = null;
private string _depthSourceId = null;


private readonly ManualResetEventSlim _frameReceived = new ManualResetEventSlim(false);
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
public event EventHandler CorrelationFailed;

使用本文先前所述的技術,查詢 MediaFrameSourceGroup,其中包含此範例案例所需的色彩和深度來源。 選取所需的畫面來源群組之後,取得每個畫面來源的 MediaFrameSourceInfo

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Depth)
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with color, depth or infrared found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo colorSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[0];
MediaFrameSourceInfo depthSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[1];

建立並初始化 MediaCapture 物件,並在初始化設定中傳遞選取的畫面來源群組。

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};

await mediaCapture.InitializeAsync(settings);

初始化 MediaCapture 使用者之後,擷取色彩和深度相機的 MediaFrameSource 物件。 儲存每個來源的識別碼,以便選取對應來源的抵達畫面。

MediaFrameSource colorSource =
    mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Color);

MediaFrameSource depthSource =
    mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Depth);

if (colorSource == null || depthSource == null)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture doesn't have the Color and Depth streams");
    return;
}

_colorSourceId = colorSource.Info.Id;
_depthSourceId = depthSource.Info.Id;

呼叫 CreateMultiSourceFrameReaderAsync 並傳遞讀取器將使用的畫面來源陣列,以建立和初始化 MultiSourceMediaFrameReader 。 註冊 FrameArrived 事件的事件處理常式。 本範例會建立 FrameRenderer 協助程式類別的執行個體,如本文先前所述,將畫面轉譯至 Image 控制項。 最後,呼叫 StartAsync 來啟動畫面讀取器。

為範例稍早宣告的 CorellationFailed 事件註冊事件處理常式。 如果使用的其中一個媒體畫面來源會停止產生畫面,我們會發出此事件訊號。 最後,呼叫 Task.Run 在個別執行緒上呼叫逾時協助程式方法 NotifyAboutCorrelationFailure。 本文稍後會顯示此方法的實作。

_multiFrameReader = await mediaCapture.CreateMultiSourceFrameReaderAsync(
    new[] { colorSource, depthSource });

_multiFrameReader.FrameArrived += MultiFrameReader_FrameArrived;

_frameRenderer = new FrameRenderer(imageElement);

MultiSourceMediaFrameReaderStartStatus startStatus =
    await _multiFrameReader.StartAsync();

if (startStatus != MultiSourceMediaFrameReaderStartStatus.Success)
{
    throw new InvalidOperationException(
        "Unable to start reader: " + startStatus);
}

this.CorrelationFailed += MainPage_CorrelationFailed;
Task.Run(() => NotifyAboutCorrelationFailure(_tokenSource.Token));

每當 MultiSourceMediaFrameReader 管理的所有媒體畫面來源都可使用新的畫面時,就會引發 FrameArrived 事件。 這表示事件會以最慢媒體來源的步調引發。 如果某個來源在較慢的來源產生一個畫面時產生多個畫面,則會卸除來自快速來源的額外畫面。

透過呼叫 TryAcquireLatestFrame 取得與事件關聯的 MultiSourceMediaFrameReference。 呼叫 TryGetFrameReferenceBySourceId,以取得與每個媒體畫面來源相關聯的 MediaFrameReference,並傳入初始化畫面讀取器時所儲存的識別碼字串。

呼叫 ManualResetEventSlim 物件的 Set 方法,以發出畫面已送達的訊號。 我們將在個別執行緒中執行的 NotifyCorrelationFailure 方法中檢查此事件。

最後,對時間相互關聯的媒體畫面執行任何處理。 此範例只會顯示來自深度來源的畫面。

private void MultiFrameReader_FrameArrived(MultiSourceMediaFrameReader sender, MultiSourceMediaFrameArrivedEventArgs args)
{
    using (MultiSourceMediaFrameReference muxedFrame =
        sender.TryAcquireLatestFrame())
    using (MediaFrameReference colorFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(_colorSourceId))
    using (MediaFrameReference depthFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(_depthSourceId))
    {
        // Notify the listener thread that the frame has been received.
        _frameReceived.Set();
        _frameRenderer.ProcessFrame(depthFrame);
    }
}

啟動畫面讀取器之後,NotifyCorrelationFailure 協助程式方法會在個別的執行緒上執行。 在此方法中,檢查是否已收到畫面接收事件訊號。 請記住,在 FrameArrived 處理常式中,每當一組相互關聯的畫面到達時,我們就會設定此事件。 如果某個應用程式定義的時段 - 5 秒未收到事件訊號,且工作未使用 CancellationToken 取消,則可能是其中一個媒體畫面來源已停止讀取畫面。 在此情況下,您通常會想要關閉畫面讀取器,因此請引發應用程式定義的 CorrelationFailed 事件。 在此事件的處理常式中,您可以停止畫面讀取器,並清除其相關聯的資源,如本文先前所示。

private void NotifyAboutCorrelationFailure(CancellationToken token)
{
    // If in 5 seconds the token is not cancelled and frame event is not signaled,
    // correlation is most likely failed.
    if (WaitHandle.WaitAny(new[] { token.WaitHandle, _frameReceived.WaitHandle }, 5000)
            == WaitHandle.WaitTimeout)
    {
        CorrelationFailed?.Invoke(this, EventArgs.Empty);
    }
}
private async void MainPage_CorrelationFailed(object sender, EventArgs e)
{
    await _multiFrameReader.StopAsync();
    _multiFrameReader.FrameArrived -= MultiFrameReader_FrameArrived;
    mediaCapture.Dispose();
    mediaCapture = null;
}

使用緩衝畫面擷取模式來保留已取得畫面的序列

從 Windows 10 版本 1709 開始,您可以將 MediaFrameReaderMultiSourceMediaFrameReaderAcquisitionMode 屬性設為 Buffered,以保留從畫面源傳遞到應用程式的畫面序列。

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

在預設的擷取模式中,Realtime,如果應用程式仍在處理上一個畫面的 FrameArrived 事件時,從來源取得多個畫面,系統將會傳送您應用程式最近取得的畫面,並卸除在緩衝區等候的其他畫面。 這可讓您的應用程式隨時提供最新的可用畫面。 這通常是即時電腦視覺應用程式最有用的模式。

Buffered 擷取模式中,系統會保留緩衝區中的所有畫面,並依收到的順序,透過 FrameArrived 事件提供給您的應用程式。 請注意,在此模式中,當系統為畫面的緩衝區填滿時,系統會停止取得新的畫面,直到您的應用程式完成 先前畫面的 FrameArrived 事件為止,釋放緩衝區中的更多空間。

使用 MediaSource 在 MediaPlayerElement 中顯示畫面

從 Windows 版本 1709 開始,您可以在 XAML 頁面的 MediaPlayerElement 控制項中,直接顯示從 MediaFrameReader取得的畫面。 這是使用 MediaSource.CreateFromMediaFrameSource 來建立 MediaSource 物件,而該物件可以直接由與 MediaPlayerElement 相關聯的 MediaPlayer 使用。 如需使用 MediaPlayerMediaPlayerElement 的詳細資訊,請參閱使用 MediaPlayer 播放音訊和視訊

下列程式碼範例示範簡單的實作,該實作會在 XAML 頁面中同時顯示來自正面和反向相機的畫面。

首先,將兩個 MediaPlayerElement 控制項新增至 XAML 頁面。

<MediaPlayerElement x:Name="mediaPlayerElement1" Width="320" Height="240"/>
<MediaPlayerElement x:Name="mediaPlayerElement2" Width="320" Height="240"/>

接下來,使用本文上一節所示的技術,選取 MediaFrameSourceGroup,其中包含 前面板和背面面板上彩色相機的 MediaFrameSourceInfo 物件。 請注意,MediaPlayer 不會自動將非色彩格式的畫面,例如深度或紅外線資料轉換成色彩資料。 使用其他感應器類型可能會產生非預期的結果。

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front
            && info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Back
            && info.SourceKind == MediaFrameSourceKind.Color)
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with front and back-facing camera found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo frontSourceInfo = selectedGroup.SourceInfos[0];
MediaFrameSourceInfo backSourceInfo = selectedGroup.SourceInfos[1];

初始化 MediaCapture 物件以使用選取的 MediaFrameSourceGroup

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

最後,呼叫 MediaSource.CreateFromMediaFrameSource,使用相關聯 MediaFrameSourceInfo 物件的 Id 屬性,為每個畫面來源建立 MediaSource,以選取 MediaCapture 物件的 FrameSources 集合中的其中一個畫面來源。 呼叫 SetMediaPlayer,初始化新的 MediaPlayer 物件,並將其指派給 MediaPlayerElement 。 然後將 Source 屬性設定為新建立的 MediaSource 物件。

var frameMediaSource1 = MediaSource.CreateFromMediaFrameSource(mediaCapture.FrameSources[frontSourceInfo.Id]);
mediaPlayerElement1.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement1.MediaPlayer.Source = frameMediaSource1;
mediaPlayerElement1.AutoPlay = true;

var frameMediaSource2 = MediaSource.CreateFromMediaFrameSource(mediaCapture.FrameSources[backSourceInfo.Id]);
mediaPlayerElement2.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement2.MediaPlayer.Source = frameMediaSource2;
mediaPlayerElement2.AutoPlay = true;

使用影片設定檔來選取畫面來源

MediaCaptureVideoProfile 物件代表的相機設定檔,代表特定擷取裝置所提供的一組功能,例如畫面速率、解析度或 HDR 擷取等進階功能。 擷取裝置可能支援多個設定檔,讓您選取已針對擷取案例最佳化的設定檔。 從 Windows 10 版本 1803 開始,可以在初始化 MediaCapture 物件之前使用 MediaCaptureVideoProfile 選取具有特定功能的媒體畫面來源。 下列範例方法會尋找支援使用Wide Color Gamut (WCG) HDR的視訊設定檔,並傳回 MediaCaptureInitializationSettings 設定物件,可用來初始化 MediaCapture 以使用選取的裝置和設定檔。

首先,呼叫 MediaFrameSourceGroup.FindAllAsync,以取得目前裝置上所有可用的媒體畫面來源群組清單。 在每個來源群組之間執行迴圈,並呼叫 MediaCapture.FindKnownVideoProfiles,以取得支援指定設定檔目前來源群組的所有視訊設定檔案清單,在此案例中為具有 WCG 相片的 HDR。 如果找到符合準則的設定檔,請建立新的 MediaCaptureInitializationSettings 物件,並將 VideoProfile 設定為選取設定檔以及將 VideoDeviceId 設定為目前媒體畫面來源群組的 Id 屬性。

public async Task<MediaCaptureInitializationSettings> FindHdrWithWcgPhotoProfile()
{
    IReadOnlyList<MediaFrameSourceGroup> sourceGroups = await MediaFrameSourceGroup.FindAllAsync();
    MediaCaptureInitializationSettings settings = null;

    foreach (MediaFrameSourceGroup sourceGroup in sourceGroups)
    {
        // Find a device that support AdvancedColorPhoto
        IReadOnlyList<MediaCaptureVideoProfile> profileList = MediaCapture.FindKnownVideoProfiles(
                                      sourceGroup.Id,
                                      KnownVideoProfile.HdrWithWcgPhoto);

        if (profileList.Count > 0)
        {
            settings = new MediaCaptureInitializationSettings();
            settings.VideoProfile = profileList[0];
            settings.VideoDeviceId = sourceGroup.Id;
            break;
        }
    }
    return settings;
}

private void StartDeviceWatcherButton_Click(object sender, RoutedEventArgs e)
{
    var remoteCameraHelper = new RemoteCameraPairingHelper(this.Dispatcher);
}

如需使用相機設定檔的詳細資訊,請參閱相機設定檔