概述
Windows Community Toolkit 3.0 于 2018 年 6 月 2 日 Release,同时正式更名为 Windows Community Toolkit,原名为 UWP Community Toolkit。顾名思义,3.0 版本会更注重整个 Windows 平台的工具实现,而不再只局限于 UWP 应用,这从 Release Note 也可以看出来:https://github.com/Microsoft/WindowsCommunityToolkit/releases
我们从今年 3 月份开始陆续针对 Windows Community Toolkit 2.2 版本的特性和代码实现做了分析,从本篇开始,我们会对 3.0 版本做持续的分享,首先本篇带来的关于 CameraPreview 相关的分享。
CameraPreview 控件允许在 MediaPlayerElement 中简单预览摄像机帧源组的视频,开发者可以在所选摄像机实时获取 Video Frame 和 Bitmap,仅显示支持彩色视频预览或视频记录流。
这是一个非常有用的控件,之前在 Face++ 工作时,我们做的很多事情都是对摄像头传出的视频帧做人脸检测或关键点标注等操作。所以该控件对摄像头的控制,以及对视频帧的传出,就成了我们工作的资源源头,我们对视频帧做规范化,再进行算法处理,再把处理后的视频帧反馈到视频播放控件中,就可以完成检测,人脸美颜处理等很多操作。

Windows Community Toolkit Doc - CameraPreview
Windows Community Toolkit Source Code - CameraPreview
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;
开发过程
代码分析
首先来看 CameraPreview 的类结构:
- CameraPreview.Cpmstants.cs - 定义了 CameraPreview 的两个常量字符串;
- CameraPreview.Events.cs - 定义了 CameraPreview 的事件处理 PreviewFailed;
- CameraPreview.Properties.cs - 定义了 CameraPreview 的依赖属性 IsFrameSourceGroupButtonVisible;
- CameraPreview.cs - CameraPreview 的主要处理逻辑;
- CameraPreview.xaml - CameraPreview 的样式文件;
- PreviewFailedEventArgs.cs - 定义了 CameraPreview 的事件处理 PreviewFailed 的参数;

接下来我们主要关注 CameraPreview.xaml 和 CameraPreview.cs 的代码实现:
1. CameraPreview.xaml
CameraPreview 控件的样式文件组成很简单,就是用户播放预览视频帧的 MediaPlayerElement 和 FrameSourceGroup 按钮。
- <Style TargetType="local:CameraPreview" >
- <Setter Property="Template">
- <Setter.Value>
- <ControlTemplate TargetType="local:CameraPreview">
- <Grid Background="{TemplateBinding Background}">
- <MediaPlayerElement x:Name="MediaPlayerElementControl" HorizontalAlignment="Left">
- </MediaPlayerElement>
- <Button x:Name="FrameSourceGroupButton" Background="{ThemeResource SystemBaseLowColor}"
- VerticalAlignment="Top" HorizontalAlignment="Left" Margin="5">
- <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" Foreground="{ThemeResource SystemAltHighColor}" />
- </Button>
- </Grid>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
- </Style>
2. CameraPreview.cs
我们先来看一下 CameraPreview 的类组成:

整体的处理逻辑很清晰:
- 通过 OnApplyTemplate(), InitializeAsync(), SetUIControls(), SetMediaPlayerSource() 等方法初始化控件,初始化摄像头视频源组,选择视频源赋值 MediaPleyerElement 做展示;
- 通过 StartAsync() 方法开始使用摄像头视频源,开发者用于展示和获取每一帧图像 Bitmap;
- 使用完成后,调用 Stop() 来结束并释放摄像头资源;
而 CameraPreview 类中出现了一个很重要的帮助类 CameraHelper,它的作用是对摄像头资源的获取和视频帧的获取/处理,它是 CameraPreview 中的核心部分,下面我们来看 CameraHelper 的实现:

我们看到 CameraHelper 类中包括了获取摄像头视频源组,初始化和开始获取视频帧,接收视频帧进行处理,释放资源等方法,我们来看几个主要方法实现:
1. GetFrameSourceGroupsAsync()
获取视频源组的方法,使用 DeviceInformation 类获取所有类别为 VideoCapture 的设备,再使用 MediaFrameSourceGroup 类获取所有 mediaFrameSourceGroup,在 groups 中获取彩色视频预览和视频录制的所有 group。
- public static async Task<IReadOnlyList<MediaFrameSourceGroup>> GetFrameSourceGroupsAsync()
- {
- if (_frameSourceGroups == null)
- {
- var videoDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
- var groups = await MediaFrameSourceGroup.FindAllAsync();
- // Filter out color video preview and video record type sources and remove duplicates video devices.
- _frameSourceGroups = groups.Where(g => g.SourceInfos.Any(s => s.SourceKind == MediaFrameSourceKind.Color &&
- (s.MediaStreamType == MediaStreamType.VideoPreview || s.MediaStreamType == MediaStreamType.VideoRecord))
- && g.SourceInfos.All(sourceInfo => videoDevices.Any(vd => vd.Id == sourceInfo.DeviceInformation.Id))).ToList();
- }
- return _frameSourceGroups;
- }
2. InitializeAndStartCaptureAsync()
使用 GetFrameSourceGroupsAsync() 和 InitializeMediaCaptureAsync() 对视频源组和 MediaCapture 进行初始化;利用 MediaCapture 读取选择的视频源组对应的预览帧源,注册 Reader_FrameArrived 事件,开始读取操作,返回操作结果;
- public async Task<CameraHelperResult> InitializeAndStartCaptureAsync()
- {
- CameraHelperResult result;
- try
- {
- await semaphoreSlim.WaitAsync();
- ...
- result = await InitializeMediaCaptureAsync();
- if (_previewFrameSource != null)
- {
- _frameReader = await _mediaCapture.CreateFrameReaderAsync(_previewFrameSource);
- if (Windows.Foundation.Metadata.ApiInformation.IsPropertyPresent("Windows.Media.Capture.Frames.MediaFrameReader", "AcquisitionMode"))
- {
- _frameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Realtime;
- }
- _frameReader.FrameArrived += Reader_FrameArrived;
- if (_frameReader == null)
- {
- result = CameraHelperResult.CreateFrameReaderFailed;
- }
- else
- {
- MediaFrameReaderStartStatus statusResult = await _frameReader.StartAsync();
- if (statusResult != MediaFrameReaderStartStatus.Success)
- {
- result = CameraHelperResult.StartFrameReaderFailed;
- }
- }
- }
- _initialized = result == CameraHelperResult.Success;
- return result;
- }
- ...
- }
3. InitializeMediaCaptureAsync()
上面方法中使用的初始化 MediaCapture 的方法,首先获取预览帧源,获取顺序是彩色预览 -> 视频录制;接着判断它支持的格式,包括视频帧率(>= 15 帧),媒体编码格式的支持(Nv12,Bgra8,Yuy2,Rgb32),按照视频宽高进行排序;对支持状态进行判断,如果状态可用,则返回默认最高分辨率;同时该方法会对权限等进行判断,对错误状态返回对应状态;只有状态为 CameraHelperResult.Success 时才是正确状态。
CameraHelperResult 中对应的错误状态有:CreateFrameReaderFailed,StartFrameReaderFailed,NoFrameSourceGroupAvailable,NoFrameSourceAvailable,CameraAccessDenied,InitializationFailed_UnknownError,NoCompatibleFrameFormatAvailable。
- private async Task<CameraHelperResult> InitializeMediaCaptureAsync()
- {
- ...
- try
- {
- await _mediaCapture.InitializeAsync(settings);
- // Find the first video preview or record stream available
- _previewFrameSource = _mediaCapture.FrameSources.FirstOrDefault(source => source.Value.Info.MediaStreamType == MediaStreamType.VideoPreview
- && source.Value.Info.SourceKind == MediaFrameSourceKind.Color).Value;
- if (_previewFrameSource == null)
- {
- _previewFrameSource = _mediaCapture.FrameSources.FirstOrDefault(source => source.Value.Info.MediaStreamType == MediaStreamType.VideoRecord
- && source.Value.Info.SourceKind == MediaFrameSourceKind.Color).Value;
- }
- if (_previewFrameSource == null)
- {
- return CameraHelperResult.NoFrameSourceAvailable;
- }
- // get only formats of a certain framerate and compatible subtype for previewing, order them by resolution
- _frameFormatsAvailable = _previewFrameSource.SupportedFormats.Where(format =>
- format.FrameRate.Numerator / format.FrameRate.Denominator >= 15 // fps
- && (string.Compare(format.Subtype, MediaEncodingSubtypes.Nv12, true) == 0
- || string.Compare(format.Subtype, MediaEncodingSubtypes.Bgra8, true) == 0
- || string.Compare(format.Subtype, MediaEncodingSubtypes.Yuy2, true) == 0
- || string.Compare(format.Subtype, MediaEncodingSubtypes.Rgb32, true) == 0))?.OrderBy(format => format.VideoFormat.Width * format.VideoFormat.Height).ToList();
- if (_frameFormatsAvailable == null || !_frameFormatsAvailable.Any())
- {
- return CameraHelperResult.NoCompatibleFrameFormatAvailable;
- }
- // set the format with the higest resolution available by default
- var defaultFormat = _frameFormatsAvailable.Last();
- await _previewFrameSource.SetFormatAsync(defaultFormat);
- }
- catch (UnauthorizedAccessException)
- { ... }
- catch (Exception)
- { ... }
- return CameraHelperResult.Success;
- }
4. Reader_FrameArrived(sender, args)
获取到视频帧的处理,触发 FrameArrived 事件,传入 VideoFrame,开发者可以对 frame 做自己的处理。
- private void Reader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
- {
- // TryAcquireLatestFrame will return the latest frame that has not yet been acquired.
- // This can return null if there is no such frame, or if the reader is not in the
- // "Started" state. The latter can occur if a FrameArrived event was in flight
- // when the reader was stopped.
- var frame = sender.TryAcquireLatestFrame();
- if (frame != null)
- {
- var vmf = frame.VideoMediaFrame;
- EventHandler<FrameEventArgs> handler = FrameArrived;
- var frameArgs = new FrameEventArgs() { VideoFrame = vmf.GetVideoFrame() };
- handler?.Invoke(sender, frameArgs);
- }
- }
调用示例
- <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
- mc:Ignorable="d">
-
- <StackPanel Orientation="Vertical" Margin="20">
- <controls:CameraPreview x:Name="CameraPreviewControl">
- </controls:CameraPreview>
- <Image x:Name="CurrentFrameImage" MinWidth="300" Width="400" HorizontalAlignment="Left"></Image>
- </StackPanel>
- </Page>
- // Initialize the CameraPreview control and subscribe to the events
- CameraPreviewControl.PreviewFailed += CameraPreviewControl_PreviewFailed;
- await CameraPreviewControl.StartAsync();
- CameraPreviewControl.CameraHelper.FrameArrived += CameraPreviewControl_FrameArrived;
- // Create a software bitmap source and set it to the Xaml Image control source.
- var softwareBitmapSource = new SoftwareBitmapSource();
- CurrentFrameImage.Source = softwareBitmapSource;
- private void CameraPreviewControl_FrameArrived(object sender, FrameEventArgs e)
- {
- var videoFrame = e.VideoFrame;
- var softwareBitmap = e.VideoFrame.SoftwareBitmap;
- var targetSoftwareBitmap = softwareBitmap;
- if (softwareBitmap != null)
- {
- if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 || softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
- {
- targetSoftwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
- }
- await softwareBitmapSource.SetBitmapAsync(targetSoftwareBitmap);
- }
- }
总结
到这里我们就把 Windows Community Toolkit 3.0 中的 CameraPreview 的源代码实现过程讲解完成了,希望能对大家更好的理解和使用这个扩展有所帮助。
相信大家在做到很多跟摄像头有关的功能,比如人脸检测,视频直播的美颜处理,贴纸操作等操作时都会用到这个控件。如果大家有好玩的应用场景,欢迎多多交流,谢谢!
最后,再跟大家安利一下 WindowsCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通过微博关注最新动态。
衷心感谢 WindowsCommunityToolkit 的作者们杰出的工作,感谢每一位贡献者,Thank you so much, ALL WindowsCommunityToolkit AUTHORS !!!