使用 MediaCapture 處理裝置方向

當您的應用程式擷取想要在應用程式外部檢視的相片或視訊時,例如儲存到使用者裝置上的檔案或線上共用時,請務必使用適當的方向中繼資料將影像編碼,以便在另一個應用程式或裝置顯示影像時正確導向。 判斷媒體檔案中要包含的正確方向資料可能是複雜的工作,因為有數個變數需要考慮,包括裝置底座的方向、顯示器的方向,以及相機在底座上的位置 (無論是正面或背面相機)。

為了簡化處理方向的程序,我們建議使用協助程式類別 CameraRotationHelper,本文結尾會提供完整定義。 您可以將這個類別新增至您的專案,然後遵循本文中的步驟,將方向支援新增至相機應用程式。 協助程式類別也可讓您更輕鬆地在相機 UI 中旋轉控制項,使其從使用者的觀點正確轉譯。

注意

本文基於使用 MediaCapture 進行基本相片、視訊和音訊擷取一文中討論的程式碼和概念。 建議您先熟悉使用 MediaCapture 類別的基本概念,再將方向支援新增至您的應用程式。

本文中使用的命名空間

本文中的範例程式碼會使用下列命名空間中的 API,您應該包含在程式碼中。

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

將方向支援新增至應用程式的第一個步驟是鎖定顯示器,使其在裝置旋轉時不會自動旋轉。 自動 UI 旋轉適用於大部分類型的應用程式,但在相機預覽旋轉時,對使用者來說並非直覺式的。 透過將 DisplayInformation.AutoRotationPreferences 屬性設定為 DisplayOrientations.Landscape 來鎖定顯示方向。

DisplayInformation.AutoRotationPreferences = DisplayOrientations.Landscape;

追蹤相機裝置位置

若要計算所擷取媒體的正確方向,應用程式必須判斷相機裝置在底座上的位置。 新增布林成員變數來追蹤相機是否位於裝置外部,例如 USB 網路相機。 新增另一個布林變數來追蹤預覽是否應該鏡像,如果使用正面相機,則為這種情況。 此外,新增變數來儲存代表所選相機的 DeviceInformation 物件。

private bool _externalCamera;
private bool _mirroringPreview;
DeviceInformation _cameraDevice;

選取攝影裝置並初始化 MediaCapture 物件

使用 MediaCapture 進行基本相片、視訊和音訊擷取一文向您示範,如何僅使用幾行程式碼來初始化 MediaCapture 物件。 為了支援相機方向,我們會在初始化程序中再新增幾個步驟。

首先,呼叫傳入裝置選取器 DeviceClass.VideoCaptureDeviceInformation.FindAllAsync 以取得所有可用視訊擷取裝置的清單。 接下來,選取清單中的第一個裝置,其中已知相機機的面板位置,以及它與提供的值相符的位置,在此範例中為正面相機。 如果在所需的面板上找不到相機,則會使用第一個或預設可用的相機。

如果找到相機裝置,則會建立一個新的 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 來啟動相機預覽。 在這麼做之前,請檢查成員變數以查看預覽是否應該鏡像 (對於正面相機)。 如果是這樣,請將 CaptureElement (本範例中名為 PreviewControl) 的 FlowDirection 屬性設為 FlowDirection.RightToLeft。 啟動預覽之後,呼叫協助程式方法 SetPreviewRotationAsync 來設定預覽旋轉。 以下是此方法的實作。

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

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

我們會以個別方法設定預覽旋轉,以便在手機方向變更時更新,而不重新初始化預覽串流。 如果相機位於裝置外部,則不會採取任何動作。 否則,將呼叫 CameraRotationHelper 方法 GetCameraPreviewOrientation,並傳回預覽串流的正確方向。

若要設定中繼資料,會呼叫 VideoDeviceController.GetMediaStreamProperties 來擷取預覽串流屬性。 接下來,建立 GUID,代表視訊串流旋轉的 Media Foundation Transform (MFT) 屬性。 在 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 進行基本相片、視訊和音訊擷取一文中介紹了基本視訊擷取。 將方向資料新增至所擷取視訊的編碼方式,會使用與先前關於將方向資料新增至預覽串流一節中所述的相同技術來完成。

在下列範例中,會建立要寫入所擷取視訊的檔案。 MP4 編碼設定檔是使用靜態方法 CreateMp4 建立的。 影片的正確方向是透過呼叫 GetCameraCaptureOrientationCameraRotationHelper 類別取得的。因為旋轉屬性要求方向以逆時針角度表示,所以呼叫 ConvertSimpleOrientationToClockwiseDegrees 協助程式方法來轉換方向值。 接下來,建立 GUID,代表視訊串流旋轉的 Media Foundation Transform (MFT) 屬性。 在 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 完整程式碼清單

以下程式碼片段列出了 CameraRotationHelper 類別的完整程式碼,該類別管理硬體方向感應器、計算影片和影片的正確方向值,並提供協助程式方法以在不同 Windows 功能使用的不同方向表示形式之間進行轉換。 如果您遵循上述文章所示的指引,您可以將此類別依現成新增至專案,而不需要進行任何變更。 當然,您可以隨意自訂下列程式碼,以符合特定案例的需求。

此協助程式類別使用裝置的 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;
        }
    }
}