.net, UWP, Windows Store Apps

How to use the camera on your device with C# in a UWP application: Part #4, cleaning up resources (and other bits)

In the final part of this series (here are links for Part 1, Part 2 and Part 3), I’ll describe how to apply some of the finishing touches to the application, such as how to handle application suspension, and disposing of resources. I’ll also show how to make sure the screen doesn’t go to sleep when the app is switched on, and how to make sure the preview image rotates to fill the whole screen. I finish the post (and series) by including all the code necessary for this small project.

Resource disposal and application suspension

It’s always good practice to clean up resources when we aren’t using them, and two intensive resources used in this application are the _mediaCapture member variable, and the PreviewControl which is used in the XAML. A disposal method which we can call to release these is very simple and would look like the code below:

private void Dispose()
{
    if (_mediaCapture != null)
    {
        _mediaCapture.Dispose();
        _mediaCapture = null;
    }
 
    if (PreviewControl.Source != null)
    {
        PreviewControl.Source.Dispose();
        PreviewControl.Source = null;
    }
}

When we navigate away from the app, the Windows Mobile OS suspends it – but while it is suspended, the OS may also terminate the app while it’s suspended to free resources for the device. Therefore we should always handle the event when the application transitions into suspension. When this event fires, this is the only chance we have to do something (e.g. save data perhaps) before the app suspends – and fortunately, one of the event arguments gives us the opportunity to delay app suspension so we can clean up resources.

The event registration for suspension looks like this:

Application.Current.Suspending += Application_Suspending;

My app’s suspension handler looks like the code below:

private void Application_Suspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    Dispose();
    deferral.Complete();
}

Additionally, I have overridden the OnNavigatedFrom event, and added the Dispose() method in here too.

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
    Dispose();
}

Stopping the app from going to sleep

Presently our app goes to sleep when there’s no active use, just the same as any other Windows store app. This can be very annoying when we’re watching the screen’s preview control is being updated! Fortunately Microsoft have given us an object to allow us to manage this – the DisplayRequest class. We can declare this as a member variable…

// This object allows us to manage whether the display goes to sleep 
// or not while our app is active.
private readonly DisplayRequest _displayRequest = new DisplayRequest();

…and then use it in the InitialiseCameraAsync to request the app stays active when the user has navigated to it.

// Stop the screen from timing out.
_displayRequest.RequestActive();

Rotating the image to fill the screen

Finally, if you’ve built this app and deployed it to a phone, you’ll have seen that the camera previewer doesn’t actually fill the screen.

This is because the video feed has a default rotation stored in the feed’s meta data – but we can change this by detecting the device’s rotation, and alter the meta data. Of course, if we have an external camera we don’t want to rotate the feed, so we have to treat these devices in different ways.

Let’s set up a couple of member variables, one to track whether the device is an external camera or not, and one to store the property name (a Guid) associated with rotation in the video feed meta data.

// Taken from https://msdn.microsoft.com/en-us/library/windows/apps/xaml/hh868174.aspx
private static readonly Guid RotationKey = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1");
 
private bool _externalCamera = false;

The code below is the asynchronous method to set the meta data in the video feed. We call as a last step in the InitialiseCameraAsync() method.

private async Task SetPreviewRotationAsync()
{
    // Only need to update the orientation if the camera is mounted on the device
    if (_externalCamera) return;
 
    // Calculate which way and how far to rotate the preview
    int rotation = ConvertDisplayOrientationToDegrees(DisplayInformation.GetForCurrentView().CurrentOrientation);
 
    // Get the property meta data about the video.
    var props = _mediaCapture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview);
 
    // Change the meta data to rotate the preview to fill the screen with the preview.
    props.Properties.Add(RotationKey, rotation);
 
    // Now set the updated meta data into the video preview.
    await _mediaCapture.SetEncodingPropertiesAsync(MediaStreamType.VideoPreview, props, null);
}
 
// Taken from https://msdn.microsoft.com/en-us/windows/uwp/audio-video-camera/capture-photos-and-video-with-mediacapture
private static int ConvertDisplayOrientationToDegrees(DisplayOrientations orientation)
{
    switch (orientation)
    {
        case DisplayOrientations.Portrait:
            return 90;
        case DisplayOrientations.LandscapeFlipped:
            return 180;
        case DisplayOrientations.PortraitFlipped:
            return 270;
        case DisplayOrientations.Landscape:
        default:
            return 0;
    }
}

Finally, we add one more line to the InitialiseCameraAsync method – this just tracks whether we’re connected to an external camera or not.

// Store whether the camera is onboard of if it's external.
_externalCamera = backFacingDevice == null;

Conclusion

That’s it for this series – I’ve pasted the code below which includes everything we’ve covered over the last four parts. You might have to refer back to Part 1, Part 2 and Part 3 for some additional information on how to set up the UWP project. I hope this code is helpful to you – if I wanted to improve this further, I would probably refactor it to reduce the length of InitialiseCameraAsync method, and maybe try to create a CameraEngine class in a NuGet package.

I’ve been impressed with how much UWP gives you in such a small amount of code – 200 lines to preview a camera output, focus, rotate, and capture an image. It’s especially impressive that this app can run on my phone, and just as well on my laptop with an integrated webcam (I’d probably need to include a software button to let my laptop capture an image).

Anyway, I hope you have found this helpful and interesting!

using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Devices.Enumeration;
using Windows.Foundation.Metadata;
using Windows.Graphics.Display;
using Windows.Media.Capture;
using Windows.Media.Devices;
using Windows.Media.MediaProperties;
using Windows.Phone.UI.Input;
using Windows.Storage;
using Windows.System.Display;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
 
namespace BasicCamera
{
    public sealed partial class MainPage : Page
    {
        // Provides functionality to capture the output from the camera
        private MediaCapture _mediaCapture;
 
        // This object allows us to manage whether the display goes to sleep 
        // or not while our app is active.
        private readonly DisplayRequest _displayRequest = new DisplayRequest();
 
        // Taken from https://msdn.microsoft.com/en-us/library/windows/apps/xaml/hh868174.aspx
        private static readonly Guid RotationKey = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1");
 
        // Tells us if the camera is external or on board.
        private bool _externalCamera = false;
 
        public MainPage()
        {
            InitializeComponent();
 
            // https://msdn.microsoft.com/en-gb/library/windows/apps/hh465088.aspx
            Application.Current.Resuming += Application_Resuming;
            Application.Current.Suspending += Application_Suspending;
 
            if (ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))
            {
                HardwareButtons.CameraHalfPressed += HardwareButtons_CameraHalfPressed;
                HardwareButtons.CameraPressed += HardwareButtons_CameraPressed;
            }
        }
 
        private void Application_Suspending(object sender, SuspendingEventArgs e)
        {
            var deferral = e.SuspendingOperation.GetDeferral();
            Dispose();
            deferral.Complete();
        }
 
        private async void Application_Resuming(object sender, object o)
        {
            await InitializeCameraAsync();
        }
 
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            await InitializeCameraAsync();
        }
 
        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            Dispose();
        }
 
        private async Task InitializeCameraAsync()
        {
            if (_mediaCapture == null)
            {
                // Get the camera devices
                var cameraDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
 
                // try to get the back facing device for a phone
                var backFacingDevice = cameraDevices
                    .FirstOrDefault(c => c.EnclosureLocation?.Panel == Windows.Devices.Enumeration.Panel.Back);
 
                // but if that doesn't exist, take the first camera device available
                var preferredDevice = backFacingDevice ?? cameraDevices.FirstOrDefault();
 
                // Store whether the camera is onboard of if it's external.
                _externalCamera = backFacingDevice == null;
 
                // Create MediaCapture
                _mediaCapture = new MediaCapture();
 
                // Stop the screen from timing out.
                _displayRequest.RequestActive();
 
                // Initialize MediaCapture and settings
                await _mediaCapture.InitializeAsync(
                    new MediaCaptureInitializationSettings
                    {
                        VideoDeviceId = preferredDevice.Id
                    });
 
                // Set the preview source for the CaptureElement
                PreviewControl.Source = _mediaCapture;
 
                // Start viewing through the CaptureElement 
                await _mediaCapture.StartPreviewAsync();
 
                // Set rotation properties to ensure the screen is filled with the preview.
                await SetPreviewRotationPropertiesAsync();
            }
        }
 
        private async void HardwareButtons_CameraHalfPressed(object sender, CameraEventArgs e)
        {
            // test if focus is supported
            if (_mediaCapture.VideoDeviceController.FocusControl.Supported)
            {
                // get the focus control from the _mediaCapture object
                var focusControl = _mediaCapture.VideoDeviceController.FocusControl;
 
                // try to get full range, but settle for the first supported one.
                var focusRange = focusControl.SupportedFocusRanges.Contains(AutoFocusRange.FullRange) ? AutoFocusRange.FullRange : focusControl.SupportedFocusRanges.FirstOrDefault();
 
                // try to get the focus mode for focussing just once, but settle for the first supported one.
                var focusMode = focusControl.SupportedFocusModes.Contains(FocusMode.Single) ? FocusMode.Single : focusControl.SupportedFocusModes.FirstOrDefault();
 
                // now configure the focus control with the range and mode as settings
                focusControl.Configure(
                    new FocusSettings
                    {
                        Mode = focusMode,
                        AutoFocusRange = focusRange
                    });
 
                // finally wait for the camera to focus
                await focusControl.FocusAsync();
            }
        }
 
        private async void HardwareButtons_CameraPressed(object sender, CameraEventArgs e)
        {
            // This is where we want to save to.
            var storageFolder = KnownFolders.SavedPictures;
 
            // Create the file that we're going to save the photo to.
            var file = await storageFolder.CreateFileAsync("sample.jpg"CreationCollisionOption.ReplaceExisting);
 
            // Update the file with the contents of the photograph.
            await _mediaCapture.CapturePhotoToStorageFileAsync(ImageEncodingProperties.CreateJpeg(), file);
        }
 
        private void Dispose()
        {
            if (_mediaCapture != null)
            {
                _mediaCapture.Dispose();
                _mediaCapture = null;
            }
 
            if (PreviewControl.Source != null)
            {
                PreviewControl.Source.Dispose();
                PreviewControl.Source = null;
            }
        }
 
        private async Task SetPreviewRotationPropertiesAsync()
        {
            // Only need to update the orientation if the camera is mounted on the device
            if (_externalCamera) return;
 
            // Calculate which way and how far to rotate the preview
            int rotation = ConvertDisplayOrientationToDegrees(DisplayInformation.GetForCurrentView().CurrentOrientation);
 
            // Get the property meta data about the video.
            var props = _mediaCapture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview);
 
            // Change the meta data to rotate the preview to fill the screen with the preview.
            props.Properties.Add(RotationKey, rotation);
 
            // Now set the updated meta data into the video preview.
            await _mediaCapture.SetEncodingPropertiesAsync(MediaStreamType.VideoPreview, props, null);
        }
 
        // Taken from https://msdn.microsoft.com/en-us/windows/uwp/audio-video-camera/capture-photos-and-video-with-mediacapture
        private static int ConvertDisplayOrientationToDegrees(DisplayOrientations orientation)
        {
            switch (orientation)
            {
                case DisplayOrientations.Portrait:
                    return 90;
                case DisplayOrientations.LandscapeFlipped:
                    return 180;
                case DisplayOrientations.PortraitFlipped:
                    return 270;
                case DisplayOrientations.Landscape:
                default:
                    return 0;
            }
        }
    }
}