在 WinUI 3 应用中显示相机预览

在本快速入门中,你将了解如何创建显示相机预览的基本 WinUI 3 相机应用。 在 WinUI 3 应用中,使用 Microsoft.UI.Xaml.Controls 命名空间中的 MediaPlayerElement 控件来呈现相机预览和 WinRT 类 MediaCapture 以访问设备的相机预览流。 MediaCapture 提供用于执行各种与相机相关的任务的 API,例如捕获照片和视频以及配置相机的设备驱动程序。 有关其他 MediaCapture 功能的详细信息,请参阅本节中的其他文章。

本演练中的代码改编自 github 上的 MediaCapture WinUI 3 示例

小窍门

有关本文的 UWP 版本,请参阅 UWP 文档中 的“显示相机预览 ”。

先决条件

  • 设备必须启用开发人员模式。 有关详细信息,请参阅启用用于开发的设备
  • 使用 WinUI 应用程序开发工作负载的 Visual Studio 2022 或更高版本。

创建新的 WinUI 3 应用

在 Visual Studio 中,创建新项目。 在“ 创建新项目 ”对话框中,将语言筛选器设置为“C#”,并将平台筛选器设置为“Windows”,然后选择“空白应用,打包(桌面中的 WinUI 3)”项目模板。

创建 UI

此示例的简单 UI 包括一个用于显示相机预览的 MediaPlayerElement 控件、一个 ComboBox ,可用于从设备的相机中进行选择,以及用于初始化 MediaCapture 类的按钮、启动和停止相机预览以及重置示例。 我们还包含用于显示状态消息的 TextBlock

在项目的 MainWindow.xml 文件中,将默认 StackPanel 控件替换为以下 XAML。

<Grid ColumnDefinitions="4*,*" ColumnSpacing="4">
    <MediaPlayerElement x:Name="mpePreview" Grid.Row="0" Grid.Column="0"  AreTransportControlsEnabled="False" ManipulationMode="None"/>
    <StackPanel Orientation="Vertical"  Grid.Row="0" Grid.Column="1" HorizontalAlignment="Stretch"  VerticalAlignment="Top">
        <TextBlock Text="Status:" Margin="0,0,10,0"/>
        <TextBlock x:Name="tbStatus" Text=""/>
        <TextBlock Text="Preview Source:" Margin="0,0,10,0"/>
        <ComboBox x:Name="cbDeviceList" HorizontalAlignment="Stretch" SelectionChanged="cbDeviceList_SelectionChanged"></ComboBox>
        <Button x:Name="bStartMediaCapture" Content="Initialize MediaCapture" IsEnabled="False" Click="bStartMediaCapture_Click"/>
        <Button x:Name="bStartPreview" Content="Start preview" IsEnabled="False" Click="bStartPreview_Click"/>
        <Button x:Name="bStopPreview" Content="Stop preview" IsEnabled="False" Click="bStopPreview_Click"/>
        <Button x:Name="bReset" Content="Reset" Click="bReset_Click" />
    </StackPanel>
</Grid>

更新 MainWindow 类定义

本文中的其余代码将添加到项目的 MainWindow.xaml.cs 文件中的 MainWindow 类定义中。 首先,添加几个类变量,这些变量将在窗口的整个生存期内保留。 这些变量包括:

  • 一个 DeviceInformationCollection ,用于存储每个可用相机的 DeviceInformation 对象。 DeviceInformation 对象传达信息,例如唯一标识符和相机的友好名称。
  • 一个 MediaCapture 对象,该对象处理与所选相机驱动程序的交互,并允许检索相机的视频流。
  • 一个 MediaFrameSource 对象,该对象表示媒体帧的源,例如视频流。
  • 一个布尔值,用于跟踪相机预览运行时。 预览运行时无法更改某些相机设置,因此最好跟踪相机预览的状态。
private DeviceInformationCollection m_deviceList;
private MediaCapture m_mediaCapture;
private MediaFrameSource m_frameSource;
private MediaPlayer m_mediaPlayer;
private bool m_isPreviewing;

填充可用相机的列表

接下来,我们将创建一个帮助程序方法来检测当前设备上存在的相机,并使用相机名称填充 UI 中的 ComboBox ,允许用户选择相机进行预览。 DeviceInformation.FindAllAsync 允许查询多种不同类型的设备。 我们使用 MediaDevice.GetVideoCaptureSelector 检索标识符,该标识符指定我们只想检索视频捕获设备。

private async void PopulateCameraList()
{
    cbDeviceList.Items.Clear();

    m_deviceList = await DeviceInformation.FindAllAsync(MediaDevice.GetVideoCaptureSelector());

    if(m_deviceList.Count == 0)
    {
        tbStatus.Text = "No video capture devices found.";
        return;
    } 

    foreach (var device in m_deviceList)
    {
        cbDeviceList.Items.Add(device.Name);
        bStartMediaCapture.IsEnabled = true;
    }
}

MainWindow 类构造函数添加对此帮助程序方法的调用,以便在窗口加载时填充 ComboBox

public MainWindow()
{
    this.InitializeComponent();

    PopulateCameraList();
    
}

初始化 MediaCapture 对象

通过调用 InitializeAsync 初始化 MediaCapture 对象,传入包含所请求初始化参数的 MediaCaptureInitializationSettings 对象。 有许多可选的初始化参数可以启用不同的方案。 有关完整列表,请参阅 API 参考页。 在此简单示例中,我们指定了一些基本设置,包括:

  • VideoDeviceId 属性指定 MediaCapture 将附加到的相机的唯一标识符。 我们使用 ComboBox 的选定索引从 DeviceInformationCollection 获取设备 ID。
  • SharingMode 属性指定应用是请求对相机的共享、只读访问权限,这允许你从视频流查看和捕获,还是允许你更改相机配置的专用控制。 多个应用可以同时从相机读取,但一次只能有一个应用具有独占控制权。
  • StreamingCaptureMode 属性指定是要捕获视频、音频还是音频和视频。
  • MediaCaptureMemoryPreference 允许我们请求专门将 CPU 内存用于视频帧。 如果系统可用,“ 自动” 值允许系统使用 GPU 内存。

在初始化 MediaCapture 对象之前,我们调用 AppCapability.CheckAccess 方法来确定用户在 Windows 设置中是否拒绝了应用访问相机的权限。

注释

Windows 允许用户在“ 隐私和安全 -> 相机”下授予或拒绝访问 Windows 设置应用中设备的相机。 初始化捕获设备时,应用应检查他们是否有权访问相机并处理用户拒绝访问的情况。 有关详细信息,请参阅 “处理 Windows 相机隐私设置”。

InitializeAsync 调用是从 try 块内部进行的,因此,如果初始化失败,我们可以恢复。 应用应正常处理初始化失败。 在此简单示例中,我们只会在失败时显示一条错误消息。

private async void bStartMediaCapture_Click(object sender, RoutedEventArgs e)
{
    if (m_mediaCapture != null)
    {
        tbStatus.Text = "MediaCapture already initialized.";
        return;
    }

    // Supported in Windows Build 18362 and later
    if(AppCapability.Create("Webcam").CheckAccess() != AppCapabilityAccessStatus.Allowed)
    {
        tbStatus.Text = "Camera access denied. Launching settings.";

        bool result = await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-settings:privacy-webcam"));

        if (AppCapability.Create("Webcam").CheckAccess() != AppCapabilityAccessStatus.Allowed)
        {
            tbStatus.Text = "Camera access denied in privacy settings.";
            return;
        }
    }

    try
    {  
        m_mediaCapture = new MediaCapture();
        var mediaCaptureInitializationSettings = new MediaCaptureInitializationSettings()
        {
            VideoDeviceId = m_deviceList[cbDeviceList.SelectedIndex].Id,
            SharingMode = MediaCaptureSharingMode.ExclusiveControl,
            StreamingCaptureMode = StreamingCaptureMode.Video,
            MemoryPreference = MediaCaptureMemoryPreference.Auto
        };

        await m_mediaCapture.InitializeAsync(mediaCaptureInitializationSettings);

        tbStatus.Text = "MediaCapture initialized successfully.";

        bStartPreview.IsEnabled = true;
    }
    catch (Exception ex)
    {
        tbStatus.Text = "Initialize media capture failed: " + ex.Message;
    }
}

初始化相机预览版

当用户单击“开始预览”按钮时,我们将尝试从初始化 MediaCapture 对象的相机设备为视频流创建 MediaFrameSourceMediaCapture.FrameSources 属性公开可用的帧源。

若要查找颜色视频数据的帧源,而非深度相机,我们需查找具有 ColorSourceKind 帧源。 某些相机驱动程序提供独立于记录流的专用预览流。 若要获取预览视频流,我们尝试选择具有 MediaStreamType of VideoPreview 的帧源。 如果未找到预览流,可以通过选择 VideoRecordMediaStreamType 来获取录制视频流。 如果这两个帧源都不可用,则无法将此捕获设备用于视频预览。

选择帧源后,我们将创建一个新的 MediaPlayer 对象,该对象将由 MediaPlayerElement 在 UI 中呈现。 我们将 MediaPlayerSource 属性设置为从所选 MediaFrameSource 创建的新 MediaSource 对象。

MediaPlayer 对象上调用“播放”以开始呈现视频流。

private void bStartPreview_Click(object sender, RoutedEventArgs e)
{
    
    m_frameSource = null;

    // Find preview source.
    // The preferred preview stream from a camera is defined by MediaStreamType.VideoPreview on the RGB camera (SourceKind == color).
    var previewSource = m_mediaCapture.FrameSources.FirstOrDefault(source => source.Value.Info.MediaStreamType == MediaStreamType.VideoPreview
                                                                                && source.Value.Info.SourceKind == MediaFrameSourceKind.Color).Value;

    if (previewSource != null)
    {
        m_frameSource = previewSource;
    }
    else
    {
        var recordSource = m_mediaCapture.FrameSources.FirstOrDefault(source => source.Value.Info.MediaStreamType == MediaStreamType.VideoRecord
                                                                                   && source.Value.Info.SourceKind == MediaFrameSourceKind.Color).Value;
        if (recordSource != null)
        {
            m_frameSource = recordSource;
        }
    }

    if (m_frameSource == null)
    {
        tbStatus.Text = "No video preview or record stream found.";
        return;
    }



    // Create MediaPlayer with the preview source
    m_mediaPlayer = new MediaPlayer();
    m_mediaPlayer.RealTimePlayback = true;
    m_mediaPlayer.AutoPlay = false;
    m_mediaPlayer.Source = MediaSource.CreateFromMediaFrameSource(m_frameSource);
    m_mediaPlayer.MediaFailed += MediaPlayer_MediaFailed; ;

    // Set the mediaPlayer on the MediaPlayerElement
    mpePreview.SetMediaPlayer(m_mediaPlayer);

    // Start preview
    m_mediaPlayer.Play();


    tbStatus.Text = "Start preview succeeded!";
    m_isPreviewing = true;
    bStartPreview.IsEnabled = false;
    bStopPreview.IsEnabled = true;
}

MediaFailed 事件实现处理程序,以便处理呈现预览的错误。

private void MediaPlayer_MediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args)
{
    tbStatus.Text = "MediaPlayer error: " + args.ErrorMessage;
}

停止相机预览

若要停止相机预览,请对 MediaPlayer 对象调用 Pause

private void bStopPreview_Click(object sender, RoutedEventArgs e)
{
    // Stop preview
    m_mediaPlayer.Pause();
    m_isPreviewing = false;
    bStartPreview.IsEnabled = true;
    bStopPreview.IsEnabled = false;
}

重置应用

若要更轻松地测试示例应用,请添加一个方法来重置应用的状态。 不再需要相机时,相机应用应始终释放相机和相关资源。

private void bReset_Click(object sender, RoutedEventArgs e)
{
    if (m_mediaCapture != null)
    {
        m_mediaCapture.Dispose();
        m_mediaCapture = null;
    }

    if(m_mediaPlayer != null)
    {
        m_mediaPlayer.Dispose();
        m_mediaPlayer = null;
    }
    
    m_frameSource = null;
    

    bStartMediaCapture.IsEnabled = false;
    bStartPreview.IsEnabled = false;
    bStopPreview.IsEnabled = false;

    PopulateCameraList();

}