使用 MediaCapture 处理设备方向

当你的应用捕获要在应用之外查看的照片或视频时,如保存到用户设备上的某个文件中或在线共享,请务必使用正确的方向元数据对该图像进行编码,以便当其他应用或设备显示该图像时,方向正确。 确定要包含在媒体文件中的正确方向数据可能是项非常复杂的任务,因为有几个变量需要考虑,如设备底盘的方向、屏幕的方向以及相机在底盘上的位置(是前置相机还是后置相机)。

若要简化处理方向的过程,我们建议使用帮助程序类 CameraRotationHelper,该帮助程序类的完整定义在本文末尾提供。 你可以将此类添加到项目中,然后按照本文中的步骤将方向支持添加到相机应用中。 借助该帮助程序类,还可使旋转相机 UI 中的控件更加简单,这样从用户的角度来看,这些控件显示正确。

注意

本文以使用 MediaCapture 捕获基本的照片、视频和音频文章中介绍的代码和概念为基础。 我们建议,在将方向支持添加到应用之前,应熟悉使用 MediaCapture 类的基本概念。

本文中使用的命名空间

本文中的示例代码将使用以下应包含在代码中的命名空间中的 API。

using Windows.Devices.Enumeration;
using Windows.UI.Core;

将方向支持添加到应用的第一步是锁定屏幕,以便当设备旋转时,屏幕不会自动跟着旋转。 自动 UI 旋转功能可良好适用于大多数类型的应用,但是当相机预览旋转时,该功能将使用户无法直观地进行查看。 锁定屏幕方向的方法是将 DisplayInformation.AutoRotationPreferences 属性设置为 DisplayOrientations.Landscape

DisplayInformation.AutoRotationPreferences = DisplayOrientations.Landscape;

跟踪相机设备位置

若要计算捕获的媒体的正确方向,应用必须确定相机在底盘上的位置。 添加一个布尔成员变量来跟踪相机是否在设备外部,如 USB Web 摄像头。 添加另一个布尔变量来跟踪在前置相机已使用的情况下,是否应镜像预览。 此外,再添加一个变量,用来存储表示所选相机的 DeviceInformation 对象。

private bool _externalCamera;
private bool _mirroringPreview;
DeviceInformation _cameraDevice;

选择相机设备并初始化 MediaCapture 对象

文章使用 MediaCapture 捕获基本的照片、视频和音频将向你展示如何仅使用几行代码来初始化 MediaCapture 对象。 若要支持相机方向,我们需要向初始化过程添加额外几个步骤。

首先,调用 DeviceInformation.FindAllAsync,传入设备选择器 DeviceClass.VideoCapture,以获取所有可用视频捕获设备的列表。 然后,在相机面板位置已知并且该位置与提供的值相匹配的列表中选择第一台设备,在本示例中,该设备为前置相机。 如果在期望的面板上未找到任何相机,将使用第一个或默认可用的相机。

如果找到相机设备,将创建一个新的 MediaCaptureInitializationSettings 对象,并且 VideoDeviceId 属性将设置为所选设备。 接下来,创建 MediaCapture 对象并调用 InitializeAsync,传入设置对象以告诉系统使用所选相机。

最后,查看所选设备面板是否为 null 或未知。 如果是,则相机在外部,意味着它的旋转与设备的旋转无关。 如果面板为已知,并且在设备底盘的前面,我们知道应该镜像预览,以便设置跟踪此信息的变量。

var allVideoDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
DeviceInformation desiredDevice = allVideoDevices.FirstOrDefault(x => x.EnclosureLocation != null 
    && x.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front);
_cameraDevice = desiredDevice ?? allVideoDevices.FirstOrDefault();


if (_cameraDevice == null)
{
    System.Diagnostics.Debug.WriteLine("No camera device found!");
    return;
}

var settings = new MediaCaptureInitializationSettings { VideoDeviceId = _cameraDevice.Id };

mediaCapture = new MediaCapture();
mediaCapture.RecordLimitationExceeded += MediaCapture_RecordLimitationExceeded;
mediaCapture.Failed += MediaCapture_Failed;

try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (UnauthorizedAccessException)
{
    System.Diagnostics.Debug.WriteLine("The app was denied access to the camera");
    return;
}

// Handle camera device location
if (_cameraDevice.EnclosureLocation == null || 
    _cameraDevice.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Unknown)
{
    _externalCamera = true;
}
else
{
    _externalCamera = false;
    _mirroringPreview = (_cameraDevice.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front);
}

初始化 CameraRotationHelper 类

现在我们开始使用 CameraRotationHelper 类。 声明类成员变量以存储对象。 调用构造函数,传入所选相机的机箱位置。 帮助程序类使用此信息来计算捕获的媒体的正确方向、预览流和 UI。 为该帮助程序类的 OrientationChanged 注册一个处理程序,当我们需要更新 UI 或预览流的方向时,就会引发该事件。

private CameraRotationHelper _rotationHelper;
_rotationHelper = new CameraRotationHelper(_cameraDevice.EnclosureLocation);
_rotationHelper.OrientationChanged += RotationHelper_OrientationChanged;

将方向数据添加到相机预览流

将正确方向添加到预览流的元数据不会影响预览向用户显示的方式,但这有助于系统编码从预览流正确捕获的任何帧。

通过调用 MediaCapture.StartPreviewAsync 启动相机预览。 在执行此操作之前,请检查成员变量,以查看是否应镜像预览(针对前置相机)。 如果应镜像,请将 CaptureElementFlowDirection 属性(在本示例中名为 PreviewControl)设置为 FlowDirection.RightToLeft。 在启动预览之后,请调用帮助程序方法 SetPreviewRotationAsync 来设置预览旋转。 下面是此方法的实现。

PreviewControl.Source = mediaCapture;
PreviewControl.FlowDirection = _mirroringPreview ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;

await mediaCapture.StartPreviewAsync();
await SetPreviewRotationAsync();

我们单独使用一种方法来设置预览旋转,以便它可以在手机方向发生改变时更新,而无需重新初始化预览流。 如果相机在设备外部,则不采取任何操作。 否则,将调用 CameraRotationHelper 方法 GetCameraPreviewOrientation,并返回预览流的正确方向。

若要设置元数据,请通过调用 VideoDeviceController.GetMediaStreamProperties 来检索预览流属性。 然后,为视频流旋转创建表示媒体基础转换 (MFT) 属性的 GUID。 在 C++ 中,你可以使用常量 MF_MT_VIDEO_ROTATION,但在 C# 中,必须手动指定 GUID 值。

向流属性对象添加属性值,指定 GUID 作为项,指定预览旋转作为值。 此属性希望值以逆时针度数为单位,因此 CameraRotationHelper 方法 ConvertSimpleOrientationToClockwiseDegrees 用于转换简单的方向值。 最后,调用 SetEncodingPropertiesAsync 将新的旋转属性应用到流。

private async Task SetPreviewRotationAsync()
{
    if (!_externalCamera)
    {
        // Add rotation metadata to the preview stream to make sure the aspect ratio / dimensions match when rendering and getting preview frames
        var rotation = _rotationHelper.GetCameraPreviewOrientation();
        var props = mediaCapture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview);
        Guid RotationKey = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1");
        props.Properties.Add(RotationKey, CameraRotationHelper.ConvertSimpleOrientationToClockwiseDegrees(rotation));
        await mediaCapture.SetEncodingPropertiesAsync(MediaStreamType.VideoPreview, props, null);
    }
}

接下来,为 CameraRotationHelper.OrientationChanged 事件添加处理程序。 此事件将传入一个参数,通过该参数,你可以确定预览流是否需要旋转。 如果设备的方向更改为面朝上或面朝下,此值将为 false。 如果预览需要旋转,请调用之前已定义的 SetPreviewRotationAsync

然后,在 OrientationChanged 事件处理程序中,更新 UI(如有需要)。 通过调用 GetUIOrientation 从帮助程序类获取当前建议的 UI 方向,并将值转换为顺时针度数,此操作的用途是执行 XAML 转换。 通过方向值创建 RotateTransform,并设置 XAML 控件的 RenderTransform 属性。 除了简单地旋转控件之外,你可能还需要在此处进行其他调整,具体取决于你的 UI 布局。 另外请记住,对 UI 的所有更新都必须在 UI 线程上进行,因此你应当将此代码放置在对 RunAsync 的调用中。

private async void RotationHelper_OrientationChanged(object sender, bool updatePreview)
{
    if (updatePreview)
    {
        await SetPreviewRotationAsync();
    }
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
        // Rotate the buttons in the UI to match the rotation of the device
        var angle = CameraRotationHelper.ConvertSimpleOrientationToClockwiseDegrees(_rotationHelper.GetUIOrientation());
        var transform = new RotateTransform { Angle = angle };

        // The RenderTransform is safe to use (i.e. it won't cause layout issues) in this case, because these buttons have a 1:1 aspect ratio
        CapturePhotoButton.RenderTransform = transform;
        CapturePhotoButton.RenderTransform = transform;
    });
}

使用方向数据捕获照片

文章使用 MediaCapture 捕获基本的照片、视频和音频展示了如何通过以下方式将照片捕获到文件:先捕获到内存中的流,然后使用解码器从该流中读取图像数据,再使用编码器将图像数据转码为文件。 从 CameraRotationHelper 类中获取的方向数据可以在执行转码操作期间添加到该图像文件。

在以下示例中,通过调用 CapturePhotoToStreamAsync 将照片捕获到 InMemoryRandomAccessStream,并从该流中创建 BitmapDecoder。 然后创建并打开 StorageFile,检索 IRandomAccessStream,以便写入到该文件中。

在对该文件进行转码之前,请通过帮助程序类方法 GetCameraCaptureOrientation 检索照片的方向。 此方法将返回一个 SimpleOrientation 对象,该对象可使用帮助程序方法 ConvertSimpleOrientationToPhotoOrientation 转换为 PhotoOrientation 对象。 接下来,创建新的 BitmapPropertySet 对象并添加属性,其中项是“System.Photo.Orientation”,值是照片方向,以 BitmapTypedValue 表示。 “System.Photo.Orientation”是许多可以作为元数据添加到图像文件的 Windows 属性之一。 有关所有与照片相关的属性列表,请参阅 Windows 属性 - 照片。 有关使用图像元数据的详细信息,请参阅图像元数据

最后,通过调用 SetPropertiesAsync 为编码器设置属性集(包括方向数据),通过调用 FlushAsync 对图像进行转码。

private async Task CapturePhotoWithOrientationAsync()
{
    var captureStream = new InMemoryRandomAccessStream();

    try
    {
        await mediaCapture.CapturePhotoToStreamAsync(ImageEncodingProperties.CreateJpeg(), captureStream);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("Exception when taking a photo: {0}", ex.ToString());
        return;
    }


    var decoder = await BitmapDecoder.CreateAsync(captureStream);
    var file = await KnownFolders.PicturesLibrary.CreateFileAsync("SimplePhoto.jpeg", CreationCollisionOption.GenerateUniqueName);

    using (var outputStream = await file.OpenAsync(FileAccessMode.ReadWrite))
    {
        var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
        var photoOrientation = CameraRotationHelper.ConvertSimpleOrientationToPhotoOrientation(
            _rotationHelper.GetCameraCaptureOrientation());
        var properties = new BitmapPropertySet {
            { "System.Photo.Orientation", new BitmapTypedValue(photoOrientation, PropertyType.UInt16) } };
        await encoder.BitmapProperties.SetPropertiesAsync(properties);
        await encoder.FlushAsync();
    }
}

使用方向数据捕获视频

使用 MediaCapture 捕获基本的照片、视频和音频文章中介绍了基本视频捕获。 使用前面将方向数据添加到预览流部分中介绍的相同技术,在编码捕获的视频时添加方向数据。

在以下示例中,创建一个文件,以便将捕获的视频写入其中。 使用静态方法 CreateMp4 创建 MP4 编码配置文件。 通过调用 GetCameraCaptureOrientationCameraRotationHelper 类获取正确的视频方向。由于旋转属性要求方向以逆时针度数表示,因此调用 ConvertSimpleOrientationToClockwiseDegrees 帮助程序方法来转换方向值。 然后,为视频流旋转创建表示媒体基础转换 (MFT) 属性的 GUID。 在 C++ 中,你可以使用常量 MF_MT_VIDEO_ROTATION,但在 C# 中,必须手动指定 GUID 值。 向流属性对象添加属性值,指定 GUID 作为项,指定旋转作为值。 最后调用 StartRecordToStorageFileAsync 开始录制使用方向数据编码的视频。

private async Task StartRecordingWithOrientationAsync()
{
    try
    {
        var videoFile = await KnownFolders.VideosLibrary.CreateFileAsync("SimpleVideo.mp4", CreationCollisionOption.GenerateUniqueName);

        var encodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto);

        var rotationAngle = CameraRotationHelper.ConvertSimpleOrientationToClockwiseDegrees(
            _rotationHelper.GetCameraCaptureOrientation());
        Guid RotationKey = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1");
        encodingProfile.Video.Properties.Add(RotationKey, PropertyValue.CreateInt32(rotationAngle));

        await mediaCapture.StartRecordToStorageFileAsync(encodingProfile, videoFile);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("Exception when starting video recording: {0}", ex.ToString());
    }
}

CameraRotationHelper 完整代码一览

以下代码段列出管理硬件方向传感器、计算照片和视频的正确方向值并提供帮助程序方法在不同 Windows 功能使用的不同方向表示法之间进行转换的 CameraRotationHelper 类的完整代码。 如果你按照上文中介绍的指南进行操作,可以将此类按原样添加到项目中,无需进行任何更改。 当然,你可以随意自定义以下代码,以满足特定场景的需求。

此帮助程序类使用设备的 SimpleOrientationSensor 来确定设备底盘的当前方向,使用 DisplayInformation 类来确定屏幕的当前方向。 其中每个类都可以提供在当前方向发生变化时引发的事件。 装载捕获设备的面板(前置、后置或外部)用于确定是否应镜像预览流。 此外,受部分设备支持的 EnclosureLocation.RotationAngleInDegreesClockwise 属性用于确定相机在底盘上的装载方向。

可以使用以下方法为指定的相机应用任务获取建议的方向值:

  • GetUIOrientation - 为相机 UI 元素返回建议的方向。
  • GetCameraCaptureOrientation - 返回要编码到图像元数据中的建议方向。
  • GetCameraPreviewOrientation - 为预览流返回建议方向,以提供自然的用户体验。
class CameraRotationHelper
{
    private EnclosureLocation _cameraEnclosureLocation;
    private DisplayInformation _displayInformation = DisplayInformation.GetForCurrentView();
    private SimpleOrientationSensor _orientationSensor = SimpleOrientationSensor.GetDefault();
    public event EventHandler<bool> OrientationChanged;

    public CameraRotationHelper(EnclosureLocation cameraEnclosureLocation)
    {
        _cameraEnclosureLocation = cameraEnclosureLocation;
        if (!IsEnclosureLocationExternal(_cameraEnclosureLocation))
        {
            _orientationSensor.OrientationChanged += SimpleOrientationSensor_OrientationChanged;
        }
        _displayInformation.OrientationChanged += DisplayInformation_OrientationChanged;
    }

    private void SimpleOrientationSensor_OrientationChanged(SimpleOrientationSensor sender, SimpleOrientationSensorOrientationChangedEventArgs args)
    {
        if (args.Orientation != SimpleOrientation.Faceup && args.Orientation != SimpleOrientation.Facedown)
        {
            HandleOrientationChanged(false);
        }
    }

    private void DisplayInformation_OrientationChanged(DisplayInformation sender, object args)
    {
        HandleOrientationChanged(true);
    }

    private void HandleOrientationChanged(bool updatePreviewStreamRequired)
    {
        var handler = OrientationChanged;
        if (handler != null)
        {
            handler(this, updatePreviewStreamRequired);
        }
    }

    public static bool IsEnclosureLocationExternal(EnclosureLocation enclosureLocation)
    {
        return (enclosureLocation == null || enclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Unknown);
    }

    private bool IsCameraMirrored()
    {
        // Front panel cameras are mirrored by default
        return (_cameraEnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front);
    }

    private SimpleOrientation GetCameraOrientationRelativeToNativeOrientation()
    {
        // Get the rotation angle of the camera enclosure
        return ConvertClockwiseDegreesToSimpleOrientation((int)_cameraEnclosureLocation.RotationAngleInDegreesClockwise);
    }

    // Gets the rotation to rotate ui elements
    public SimpleOrientation GetUIOrientation()
    {
        if (IsEnclosureLocationExternal(_cameraEnclosureLocation))
        {
            // Cameras that are not attached to the device do not rotate along with it, so apply no rotation
            return SimpleOrientation.NotRotated;
        }

        // Return the difference between the orientation of the device and the orientation of the app display
        var deviceOrientation = _orientationSensor.GetCurrentOrientation();
        var displayOrientation = ConvertDisplayOrientationToSimpleOrientation(_displayInformation.CurrentOrientation);
        return SubOrientations(displayOrientation, deviceOrientation);
    }

    // Gets the rotation of the camera to rotate pictures/videos when saving to file
    public SimpleOrientation GetCameraCaptureOrientation()
    {
        if (IsEnclosureLocationExternal(_cameraEnclosureLocation))
        {
            // Cameras that are not attached to the device do not rotate along with it, so apply no rotation
            return SimpleOrientation.NotRotated;
        }

        // Get the device orienation offset by the camera hardware offset
        var deviceOrientation = _orientationSensor.GetCurrentOrientation();
        var result = SubOrientations(deviceOrientation, GetCameraOrientationRelativeToNativeOrientation());

        // If the preview is being mirrored for a front-facing camera, then the rotation should be inverted
        if (IsCameraMirrored())
        {
            result = MirrorOrientation(result);
        }
        return result;
    }

    // Gets the rotation of the camera to display the camera preview
    public SimpleOrientation GetCameraPreviewOrientation()
    {
        if (IsEnclosureLocationExternal(_cameraEnclosureLocation))
        {
            // Cameras that are not attached to the device do not rotate along with it, so apply no rotation
            return SimpleOrientation.NotRotated;
        }

        // Get the app display rotation offset by the camera hardware offset
        var result = ConvertDisplayOrientationToSimpleOrientation(_displayInformation.CurrentOrientation);
        result = SubOrientations(result, GetCameraOrientationRelativeToNativeOrientation());

        // If the preview is being mirrored for a front-facing camera, then the rotation should be inverted
        if (IsCameraMirrored())
        {
            result = MirrorOrientation(result);
        }
        return result;
    }

    public static PhotoOrientation ConvertSimpleOrientationToPhotoOrientation(SimpleOrientation orientation)
    {
        switch (orientation)
        {
            case SimpleOrientation.Rotated90DegreesCounterclockwise:
                return PhotoOrientation.Rotate90;
            case SimpleOrientation.Rotated180DegreesCounterclockwise:
                return PhotoOrientation.Rotate180;
            case SimpleOrientation.Rotated270DegreesCounterclockwise:
                return PhotoOrientation.Rotate270;
            case SimpleOrientation.NotRotated:
            default:
                return PhotoOrientation.Normal;
        }
    }

    public static int ConvertSimpleOrientationToClockwiseDegrees(SimpleOrientation orientation)
    {
        switch (orientation)
        {
            case SimpleOrientation.Rotated90DegreesCounterclockwise:
                return 270;
            case SimpleOrientation.Rotated180DegreesCounterclockwise:
                return 180;
            case SimpleOrientation.Rotated270DegreesCounterclockwise:
                return 90;
            case SimpleOrientation.NotRotated:
            default:
                return 0;
        }
    }

    private SimpleOrientation ConvertDisplayOrientationToSimpleOrientation(DisplayOrientations orientation)
    {
        SimpleOrientation result;
        switch (orientation)
        {
            case DisplayOrientations.Landscape:
                result = SimpleOrientation.NotRotated;
                break;
            case DisplayOrientations.PortraitFlipped:
                result = SimpleOrientation.Rotated90DegreesCounterclockwise;
                break;
            case DisplayOrientations.LandscapeFlipped:
                result = SimpleOrientation.Rotated180DegreesCounterclockwise;
                break;
            case DisplayOrientations.Portrait:
            default:
                result = SimpleOrientation.Rotated270DegreesCounterclockwise;
                break;
        }

        // Above assumes landscape; offset is needed if native orientation is portrait
        if (_displayInformation.NativeOrientation == DisplayOrientations.Portrait)
        {
            result = AddOrientations(result, SimpleOrientation.Rotated90DegreesCounterclockwise);
        }

        return result;
    }

    private static SimpleOrientation MirrorOrientation(SimpleOrientation orientation)
    {
        // This only affects the 90 and 270 degree cases, because rotating 0 and 180 degrees is the same clockwise and counter-clockwise
        switch (orientation)
        {
            case SimpleOrientation.Rotated90DegreesCounterclockwise:
                return SimpleOrientation.Rotated270DegreesCounterclockwise;
            case SimpleOrientation.Rotated270DegreesCounterclockwise:
                return SimpleOrientation.Rotated90DegreesCounterclockwise;
        }
        return orientation;
    }

    private static SimpleOrientation AddOrientations(SimpleOrientation a, SimpleOrientation b)
    {
        var aRot = ConvertSimpleOrientationToClockwiseDegrees(a);
        var bRot = ConvertSimpleOrientationToClockwiseDegrees(b);
        var result = (aRot + bRot) % 360;
        return ConvertClockwiseDegreesToSimpleOrientation(result);
    }

    private static SimpleOrientation SubOrientations(SimpleOrientation a, SimpleOrientation b)
    {
        var aRot = ConvertSimpleOrientationToClockwiseDegrees(a);
        var bRot = ConvertSimpleOrientationToClockwiseDegrees(b);
        //add 360 to ensure the modulus operator does not operate on a negative
        var result = (360 + (aRot - bRot)) % 360;
        return ConvertClockwiseDegreesToSimpleOrientation(result);
    }

    private static VideoRotation ConvertSimpleOrientationToVideoRotation(SimpleOrientation orientation)
    {
        switch (orientation)
        {
            case SimpleOrientation.Rotated90DegreesCounterclockwise:
                return VideoRotation.Clockwise270Degrees;
            case SimpleOrientation.Rotated180DegreesCounterclockwise:
                return VideoRotation.Clockwise180Degrees;
            case SimpleOrientation.Rotated270DegreesCounterclockwise:
                return VideoRotation.Clockwise90Degrees;
            case SimpleOrientation.NotRotated:
            default:
                return VideoRotation.None;
        }
    }

    private static SimpleOrientation ConvertClockwiseDegreesToSimpleOrientation(int orientation)
    {
        switch (orientation)
        {
            case 270:
                return SimpleOrientation.Rotated90DegreesCounterclockwise;
            case 180:
                return SimpleOrientation.Rotated180DegreesCounterclockwise;
            case 90:
                return SimpleOrientation.Rotated270DegreesCounterclockwise;
            case 0:
            default:
                return SimpleOrientation.NotRotated;
        }
    }
}