Thursday, May 02, 2013

Implementing Camera Switch, Resolution Change and Option Popup with MediaCapture API in WinJS

WinJS provides CameraCaptureUI and MediaCapture API to access camera. The CameraCaptureUI has predefined UI but if you want more control such as custom UI, MediaCapture is the only option.

David Rousset's posts, Tutorial Series: using WinJS & WinRT to build a fun HTML5 Camera Application for Windows 8 is great resource to learn how to work with MediaCapture API. But some common features available on CameraCaptureUI are not included in David's example. After some research and testing I implemented camera switching, resolution selection and camera option popup with MediaCapture API.

  • Change camera:
    An option to let user to switch another camera if multiple cameras available. The key is set the MediaCaptureInitializationSettings.videoDeviceId property, and pass MediaCaptureInitializationSettings to initialize the MediaCapture object. We can use Windows.Devices.Enumeration.DeviceInformation.finaAllAsync method to loop through all video capture devices and get all cameras' device ID.
  • Change resolution :
    User can change camera's resolution for photo taking. We use MediaCapture.videoDeviceController.getAvailableMediaStreamProperties method to get a list of available resolutions for the camera tied to the MediaCapture object, and use MediaCapture.videoDeviceController.setMediaStreamPropertiesAsync method to set the camera resultion.
  • Open other options dialog:
    A button to open camera's option dialog by calling Windows.Media.Capture.CameraOptionsUI.show(mediaCapture) method.

Following is code example implementing above three features. Although the code is targeting to photo functions for demo purpose, it applies to video capturing as well. Refer to David's posts for more details on how to work on videos using MediaCapture API.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>CustomCameraPage</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- CustomCameraPage references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
</head>
<body>
    <div id="cameraPage">
        <video id="cameraPreview"></video>
        <img id="imgPhotoTaken" style="display:none;"/>
        <div class="footer">
            <button id="btnSave" title="Save a photo">Save Photo</button>
            <select id="selectResolutions" title="Camera Resolutions"></select>
            <span id="cameraOption" title="Set camera option">&#xe15e</span>
            <span id="cameraSwitch" title="Switch to another camera">&#xe124</span>
            <button id="btnTake" title="Take a photo">Take Photo</button>
        </div>
    </div>
</body>
</html>
CSS:
#cameraPreview, #imgPhotoTaken { width: 100%; height: 100%; }
#cameraPage .footer { position: absolute; bottom: 0px; left: calc(50% - 200px); }
#cameraPage .footer > * { background-color: rgba(0,0,0,0.5); color: #ffffff; margin-left: 10px; }
#cameraPage .footer  { font-size: 1.4em; }
JavaScript:
// For an introduction to the Blank template, see the following documentation:
// http://go.microsoft.com/fwlink/?LinkId=232509
(function () {
    "use strict";

    WinJS.Binding.optimizeBindingReferences = true;

    var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

    var mediaCapture;
    var cameraSettings;
    var allCameras = [];
    var cameraResolutions = [];
    var liveMode = true;
    var mediaType = Windows.Media.Capture.MediaStreamType;
    var tempFile = "photo.jpg";
    var tempFolder = Windows.Storage.ApplicationData.current.localFolder;
    var fileSaver = new Windows.Storage.Pickers.FileSavePicker();

    app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
                // TODO: This application has been newly launched. Initialize
                // your application here.
            } else {
                // TODO: This application has been reactivated from suspension.
                // Restore application state here.
            }
            args.setPromise(WinJS.UI.processAll());

            initPage();
        }
    };

    app.start();

    // Page initialization
    function initPage() {
        fileSaver.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary;
        fileSaver.fileTypeChoices.insert("JPG image", [".jpg"]);
        getCamera().then(function (camera) {
            if (camera) {
                wireControls();
                if (allCameras.length <= 1) {
                    cameraSwitch.disabled = "disabled";
                }
                startCamera(camera);
            } else {
                showError("No camera found.", "Error");
            }
        },
        function (error) {
            onCameraError(error);
        });
    }

    // UI element event binding 
    function wireControls() {
        btnSave.addEventListener("click", saveImage);
        btnTake.addEventListener("click", takePhoto);
        cameraSwitch.addEventListener("click", switchCamera);
        cameraOption.addEventListener("click", setCameraOtherOption);
        selectResolutions.addEventListener("change", setCameraResolution);
    }

    // Creating a new mediaCapture object based on a selected camera, then get all its available resolutions;
    // Return a Promise when completed
    function startCamera(camera) {
        if (camera) {
            cameraSettings = new Windows.Media.Capture.MediaCaptureInitializationSettings();
            cameraSettings.photoCaptureSource = Windows.Media.Capture.PhotoCaptureSource.videoPreview;
            cameraSettings.streamingCaptureMode = Windows.Media.Capture.StreamingCaptureMode.video;
            cameraSettings.videoDeviceId = camera.id;
            mediaCapture = new Windows.Media.Capture.MediaCapture();
            mediaCapture.addEventListener("failed", onCameraError, false);
            return mediaCapture.initializeAsync(cameraSettings).then(function () {
                mediaCapture.setPreviewRotation(Windows.Media.Capture.VideoRotation.none);
                return getCameraResolutions();
            }).then(function () {
                cameraPreview.src = URL.createObjectURL(mediaCapture, { oneTimeOnly: true });
                cameraPreview.play();
            },
            function (error) {
                showError(error);
            });;
        } else {
            return WinJS.Promise.wrapError("No camera to start");
        }
    }

    // Release resource of live video preview and mediaCapture object
    function releaseCamera() {
        cameraView.src = null;
        mediaCapture = null;
    }

    // Loop through all cameras
    // Return a Promise with a default camera
    function getCamera() {
        allCameras = [];
        return new WinJS.Promise(function (c, e) {
            var deviceInfo = Windows.Devices.Enumeration.DeviceInformation;
            return deviceInfo.findAllAsync(Windows.Devices.Enumeration.DeviceClass.videoCapture).then(function (devices) {
                if (devices.length > 0) { // Camera found
                    var defaultCamera = devices[0]
                    for (var i = 0; i < devices.length; i++) {
                        var device = devices[i];
                        allCameras.push(device);
                        // Default to embedded front camera
                        if (device.enclosureLocation && device.enclosureLocation.panel == Windows.Devices.Enumeration.Panel.front)
                            defaultCamera = device;
                    }
                    c(defaultCamera);
                } else { // No camera found
                    c();
                }
            },
            function (error) { // Error
                e(error);
            });
        });
    }

    // Switch to different camera if mulitple cameras available
    function switchCamera() {
        if (mediaCapture && allCameras && allCameras.length > 1) {
            var currentCameraId = mediaCapture.mediaCaptureSettings.videoDeviceId;
            var nextCamera = null;
            var cameraCount = allCameras.length;
            for (var i = 0; i < cameraCount; i++) {
                var cam = allCameras[i];
                if (cam.id === currentCameraId) {
                    if (i == (cameraCount - 1)) {
                        nextCamera = allCameras[0];
                    } else {
                        nextCamera = allCameras[i + 1];
                    }
                    break;
                }
            }
            if (nextCamera) {
                releaseCamera();
                startCamera(nextCamera);
            }
        }
    }

    // Take a photo, save it to temporary folder then display it on the screen
    // Return a Prmoise when completed
    function takePhoto() {
        if (mediaCapture) {
            if (!liveMode) { // Show the live video preview if clicking "Retake"
                toggleView();
                return WinJS.Promise.as();
            } else { // Save and display taken photo if clicking "Take"
                var saveOption = Windows.Storage.CreationCollisionOption;
                return tempFolder.createFileAsync(tempFile, saveOption.replaceExisting).then(function (file) {
                    var photoProperties = Windows.Media.MediaProperties.ImageEncodingProperties.createJpeg();
                    return mediaCapture.capturePhotoToStorageFileAsync(photoProperties, file).then(function (result) {
                        return tempFolder.getFileAsync(tempFile).then(function (newFile) {
                            if (newFile) {
                                var imageUrl = URL.createObjectURL(file, { oneTimeOnly: true });
                                imgPhotoTaken.src = imageUrl;
                                toggleView();
                            }   
                        });
                        
                    });
                });
            }
        }
    }

    // Toggle display to show taken photo or video live preview, and switch button text to "Take" or "Retake"
    function toggleView () {
        if (liveMode) { // when showing live video preview
            liveMode = false;
            btnTake.textContent = "Retake";
            imgPhotoTaken.style.display = "block";
            cameraPreview.style.display = "none";
            cameraPreview.pause();
            
        } else { // when showing taken image 
            liveMode = true;
            btnTake.textContent = "Take";
            cameraPreview.style.display = "block";
            imgPhotoTaken.style.display = "none";
            cameraPreview.play();
        }
    }

    // Get all resolutions of current MediaCapture object and populate resolutions option in UI 
    // Return a Promise when completed
    function getCameraResolutions() {
        if (mediaCapture) {
            cameraResolutions = [];
            while (selectResolutions.length > 0) {
                selectResolutions.remove(0);
            }

            var currentResolution = mediaCapture.videoDeviceController.getMediaStreamProperties(mediaType.videoPreview);
            var allResolutions = mediaCapture.videoDeviceController.getAvailableMediaStreamProperties(mediaType.videoPreview);

            // Save all unique resolutions
            var tempResolutions = [];
            for (var i = 0; i < allResolutions.length; i++) {
                var res = allResolutions[i];
                if (res.width < 600)
                    continue;
                var hasDuplicated = false;
                tempResolutions.forEach(function (item) {
                    if (item.width == res.width && item.height == res.height)
                        hasDuplicated = true;
                });
                if (!hasDuplicated)
                    tempResolutions.push(res);
            }

            // Sort all unique resolutions from low to high based on width
            tempResolutions.sort(function (item1, item2) {
                return item1.width == item2.width ? item1.height - item2.height : item1.width - item2.width;
            });

            // Save camera resolutions and populate resolution options in UI 
            for (var i = 0; i < tempResolutions.length; i++) {
                var res = tempResolutions[i];
                cameraResolutions.push(res);
                selectResolutions.add(new Option(res.width + "x" + res.height));
                if (currentResolution.width == res.width && currentResolution.height == res.height) {
                    selectResolutions.selectedIndex = i;
                }
            }
            return mediaCapture.videoDeviceController.setMediaStreamPropertiesAsync(mediaType.videoPreview, currentResolution);
        } else {
            return WinJS.Promise.as();
        }
    }

    // Switch to a different resolution on a camera
    function setCameraResolution() {
        if (mediaCapture) {
            try {
                var index = selectResolutions.selectedIndex;
                if (cameraResolutions.length > index) {
                    var resolution = cameraResolutions[index];
                    mediaCapture.videoDeviceController.setMediaStreamPropertiesAsync(mediaType.videoPreview, resolution).then(function () {
                        cameraPreview.src = null;
                        cameraPreview.src = URL.createObjectURL(mediaCapture, { oneTimeOnly: true });
                        cameraPreview.play();
                    },
                    function (error) {
                        onCameraError(error);
                    });
                }
            } catch (e) {
                showError(e);
            }
        }
    }

    // Popup camera option menu
    function setCameraOtherOption() {
        if (mediaCapture) {
            Windows.Media.Capture.CameraOptionsUI.show(mediaCapture);
        }
    }

    // When user select to save a photo 
    function saveImage() {
        if (liveMode) {
            takePhoto().then(function () {
                saveImageToStorage();
            });
        } else {
            saveImageToStorage();
        }         
    }

    // Open file picker and save photo to specific name and location
    function saveImageToStorage() {
        fileSaver.pickSaveFileAsync().then(function (file) {
            if (file) {
                tempFolder.getFileAsync(tempFile).then(function (tmpImage) {
                    tmpImage.copyAndReplaceAsync(file).done(function () {
                        console.log("Photo saved to " + file.path + " successfully.");
                    });
                });
            }
        });
    }

    // Error occurs during camera operation
    function onCameraError(error) {
        showError(error);
    }

    // Ddisplay error information
    function showError(error, title) {
        var message = (error && error.message) ? error.message : error;
        var msgPopup = new Windows.UI.Popups.MessageDialog(message);
        if (title)
            msgPopup.title = title;
        msgPopup.commands.append(new Windows.UI.Popups.UICommand("Ok"));
        msgPopup.showAsync();
    }
})();

The resolution option now is available for user to select:

When camera other options dialog pops up:

When a photo is taken, you have option to retake or save it: