/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ /* global Windows:true */ var MediaFile = require('cordova-plugin-media-capture.MediaFile'); var CaptureError = require('cordova-plugin-media-capture.CaptureError'); var CaptureAudioOptions = require('cordova-plugin-media-capture.CaptureAudioOptions'); var CaptureVideoOptions = require('cordova-plugin-media-capture.CaptureVideoOptions'); var MediaFileData = require('cordova-plugin-media-capture.MediaFileData'); /* * Class that combines all logic for capturing picture and video on WP8.1 */ function MediaCaptureProxy() { var previewContainer, capturePreview = null, captureCancelButton = null, captureSettings = null, captureStarted = false, capturedPictureFile, capturedVideoFile, capture = null; var CaptureNS = Windows.Media.Capture; /** * Helper function that toggles visibility of DOM elements with provided ids * @param {String} variable number of elements' ids which visibility needs to be toggled */ function toggleElements() { // convert arguments to array var args = Array.prototype.slice.call(arguments); args.forEach(function(buttonId) { var buttonEl = document.getElementById(buttonId); if (buttonEl) { var curDisplayStyle = buttonEl.style.display; buttonEl.style.display = curDisplayStyle === 'none' ? 'block' : 'none'; } }); } /** * Creates basic camera UI with preview 'video' element and 'Cancel' button * Capture starts, when you clicking on preview. */ function createCameraUI() { var buttonStyle = "margin: 7px; border: 2.5px solid white; width: 45%; height: 35px; color: white; background-color: black;"; previewContainer = document.createElement('div'); previewContainer.style.cssText = "background-position: 50% 50%; background-repeat: no-repeat; background-size: contain; background-color: black; left: 0px; top: 0px; width: 100%; height: 100%; position: fixed; z-index: 9999"; previewContainer.innerHTML = '' + '
' + '' + '' + '' + '' + '
'; document.body.appendChild(previewContainer); // Create fullscreen preview capturePreview = document.getElementById('capturePreview'); // Create cancel button captureCancelButton = document.getElementById('cancelCapture'); capture = new CaptureNS.MediaCapture(); captureSettings = new CaptureNS.MediaCaptureInitializationSettings(); captureSettings.streamingCaptureMode = CaptureNS.StreamingCaptureMode.audioAndVideo; } /** * Starts camera preview and binds provided callbacks to controls * @param {function} takeCallback Callback for Take button * @param {function} errorCallback Callback for Cancel button + default error callback * @param {function} selectCallback Callback for Select button * @param {function} retakeCallback Callback for Retake button */ function startCameraPreview(takeCallback, errorCallback, selectCallback, retakeCallback) { // try to select appropriate device for capture // rear camera is preferred option var expectedPanel = Windows.Devices.Enumeration.Panel.back; Windows.Devices.Enumeration.DeviceInformation.findAllAsync(Windows.Devices.Enumeration.DeviceClass.videoCapture).done(function (devices) { if (devices.length > 0) { devices.forEach(function (currDev) { if (currDev.enclosureLocation && currDev.enclosureLocation.panel && currDev.enclosureLocation.panel == expectedPanel) { captureSettings.videoDeviceId = currDev.id; } }); capture.initializeAsync(captureSettings).done(function () { // This is necessary since WP8.1 MediaCapture outputs video stream rotated 90 degrees CCW // TODO: This can be not consistent across devices, need additional testing on various devices // msdn.microsoft.com/en-us/library/windows/apps/hh452807.aspx capture.setPreviewRotation(Windows.Media.Capture.VideoRotation.clockwise90Degrees); capturePreview.msZoom = true; capturePreview.src = URL.createObjectURL(capture); capturePreview.play(); previewContainer.style.display = 'block'; // Bind events to controls capturePreview.onclick = takeCallback; document.getElementById('takePicture').onclick = takeCallback; document.getElementById('cancelCapture').onclick = function () { errorCallback(CaptureError.CAPTURE_NO_MEDIA_FILES); }; document.getElementById('selectPicture').onclick = selectCallback; document.getElementById('retakePicture').onclick = retakeCallback; }, function (err) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err); }); } else { // no appropriate devices found destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR); } }); } /** * Destroys camera preview, removes all elements created */ function destroyCameraPreview() { capturePreview.pause(); capturePreview.src = null; if (previewContainer) { document.body.removeChild(previewContainer); } if (capture) { capture.stopRecordAsync(); capture = null; } } return { /** * Initiate video capture using MediaCapture class * @param {function} successCallback Called, when user clicked on preview, with captured file object * @param {function} errorCallback Called on any error */ captureVideo: function (successCallback, errorCallback) { try { createCameraUI(); startCameraPreview(function () { // This callback called twice: whem video capture started and when it ended // so we need to check capture status if (!captureStarted) { // remove cancel button and rename 'Take' button to 'Stop' toggleElements('cancelCapture'); document.getElementById('takePicture').text = 'Stop'; var encodingProperties = Windows.Media.MediaProperties.MediaEncodingProfile.createMp4(Windows.Media.MediaProperties.VideoEncodingQuality.auto), generateUniqueCollisionOption = Windows.Storage.CreationCollisionOption.generateUniqueName, localFolder = Windows.Storage.ApplicationData.current.localFolder; localFolder.createFileAsync("cameraCaptureVideo.mp4", generateUniqueCollisionOption).done(function(capturedFile) { capture.startRecordToStorageFileAsync(encodingProperties, capturedFile).done(function() { capturedVideoFile = capturedFile; captureStarted = true; }, function(err) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err); }); }, function(err) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err); }); } else { capture.stopRecordAsync().done(function () { destroyCameraPreview(); successCallback(capturedVideoFile); }); } }, errorCallback); } catch (ex) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, ex); } }, /** * Initiate image capture using MediaCapture class * @param {function} successCallback Called, when user clicked on preview, with captured file object * @param {function} errorCallback Called on any error */ capturePhoto: function (successCallback, errorCallback) { try { createCameraUI(); startCameraPreview( // Callback for Take button - captures intermediate image file. function () { var encodingProperties = Windows.Media.MediaProperties.ImageEncodingProperties.createJpeg(), overwriteCollisionOption = Windows.Storage.CreationCollisionOption.replaceExisting, tempFolder = Windows.Storage.ApplicationData.current.temporaryFolder; tempFolder.createFileAsync("cameraCaptureImage.jpg", overwriteCollisionOption).done(function (capturedFile) { capture.capturePhotoToStorageFileAsync(encodingProperties, capturedFile).done(function () { // store intermediate result in object's global variable capturedPictureFile = capturedFile; // show pre-captured image and toggle visibility of all buttons previewContainer.style.backgroundImage = 'url("' + 'ms-appdata:///temp/' + capturedFile.name + '")'; toggleElements('capturePreview', 'takePicture', 'cancelCapture', 'selectPicture', 'retakePicture'); }, function (err) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err); }); }, function (err) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err); }); }, // error + cancel callback function (err) { destroyCameraPreview(); errorCallback(err); }, // Callback for Select button - copies intermediate file into persistent application's storage function () { var generateUniqueCollisionOption = Windows.Storage.CreationCollisionOption.generateUniqueName, localFolder = Windows.Storage.ApplicationData.current.localFolder; capturedPictureFile.copyAsync(localFolder, capturedPictureFile.name, generateUniqueCollisionOption).done(function (copiedFile) { destroyCameraPreview(); successCallback(copiedFile); }, function(err) { destroyCameraPreview(); errorCallback(err); }); }, // Callback for retake button - just toggles visibility of necessary elements function () { toggleElements('capturePreview', 'takePicture', 'cancelCapture', 'selectPicture', 'retakePicture'); } ); } catch (ex) { destroyCameraPreview(); errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, ex); } } }; } module.exports = { captureAudio:function(successCallback, errorCallback, args) { var options = args[0]; var audioOptions = new CaptureAudioOptions(); if (typeof(options.duration) == 'undefined') { audioOptions.duration = 3600; // Arbitrary amount, need to change later } else if (options.duration > 0) { audioOptions.duration = options.duration; } else { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); return; } // Some shortcuts for long namespaces var CaptureNS = Windows.Media.Capture, MediaPropsNS = Windows.Media.MediaProperties, localAppData = Windows.Storage.ApplicationData.current.localFolder, generateUniqueName = Windows.Storage.NameCollisionOption.generateUniqueName; var mediaCapture = new CaptureNS.MediaCapture(), mediaCaptureSettings = new CaptureNS.MediaCaptureInitializationSettings(), mp3EncodingProfile = new MediaPropsNS.MediaEncodingProfile.createMp3(MediaPropsNS.AudioEncodingQuality.auto), m4aEncodingProfile = new MediaPropsNS.MediaEncodingProfile.createM4a(MediaPropsNS.AudioEncodingQuality.auto); mediaCaptureSettings.streamingCaptureMode = CaptureNS.StreamingCaptureMode.audio; var capturedFile, stopRecordTimeout; var stopRecord = function () { mediaCapture.stopRecordAsync().then(function() { capturedFile.getBasicPropertiesAsync().then(function (basicProperties) { var result = new MediaFile(capturedFile.name, 'ms-appdata:///local/' + capturedFile.name, capturedFile.contentType, basicProperties.dateModified, basicProperties.size); result.fullPath = capturedFile.path; successCallback([result]); }, function() { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); }, function() { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); }; mediaCapture.initializeAsync(mediaCaptureSettings).done(function () { localAppData.createFileAsync("captureAudio.mp3", generateUniqueName).then(function (storageFile) { capturedFile = storageFile; mediaCapture.startRecordToStorageFileAsync(mp3EncodingProfile, capturedFile).then(function () { stopRecordTimeout = setTimeout(stopRecord, audioOptions.duration * 1000); }, function (err) { // -1072868846 is the error code for "No suitable transform was found to encode or decode the content." // so we try to use another (m4a) format if (err.number === -1072868846) { // first we clear existing timeout to prevent success callback to be called with invalid arguments // second we start same actions to try to record m4a audio clearTimeout(stopRecordTimeout); localAppData.createFileAsync("captureAudio.m4a", generateUniqueName).then(function (storageFile) { capturedFile = storageFile; mediaCapture.startRecordToStorageFileAsync(m4aEncodingProfile, capturedFile).then(function () { stopRecordTimeout = setTimeout(stopRecord, audioOptions.duration * 1000); }, function() { // if we here, we're totally failed to record either mp3 or m4a errorCallback(new CaptureError(CaptureError.CAPTURE_INTERNAL_ERR)); return; }); }); } else { errorCallback(new CaptureError(CaptureError.CAPTURE_INTERNAL_ERR)); return; } }); }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); }); }, captureImage:function (successCallback, errorCallback, args) { var CaptureNS = Windows.Media.Capture; function fail(code, data) { var err = new CaptureError(code); err.message = data; errorCallback(err); } // Check if necessary API available if (!CaptureNS.CameraCaptureUI) { // We are running on WP8.1 which lacks CameraCaptureUI class // so we need to use MediaCapture class instead and implement custom UI for camera var proxy = new MediaCaptureProxy(); proxy.capturePhoto(function (photoFile) { photoFile.getBasicPropertiesAsync().done(function (basicProperties) { var result = new MediaFile(photoFile.name, 'ms-appdata:///local/' + photoFile.name, photoFile.contentType, basicProperties.dateModified, basicProperties.size); result.fullPath = photoFile.path; successCallback([result]); }, function (err) { fail(CaptureError.CAPTURE_INTERNAL_ERR, err); }); }, function (err) { fail(err); }); } else { var cameraCaptureUI = new Windows.Media.Capture.CameraCaptureUI(); cameraCaptureUI.photoSettings.allowCropping = true; cameraCaptureUI.photoSettings.maxResolution = Windows.Media.Capture.CameraCaptureUIMaxPhotoResolution.highestAvailable; cameraCaptureUI.photoSettings.format = Windows.Media.Capture.CameraCaptureUIPhotoFormat.jpeg; cameraCaptureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo).done(function (file) { if (file) { file.moveAsync(Windows.Storage.ApplicationData.current.localFolder, "cameraCaptureImage.jpg", Windows.Storage.NameCollisionOption.generateUniqueName).then(function () { file.getBasicPropertiesAsync().then(function (basicProperties) { var result = new MediaFile(file.name, 'ms-appdata:///local/' + file.name, file.contentType, basicProperties.dateModified, basicProperties.size); result.fullPath = file.path; successCallback([result]); }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); } else { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); } }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); } }, captureVideo:function (successCallback, errorCallback, args) { var options = args[0]; var CaptureNS = Windows.Media.Capture; function fail(code, data) { var err = new CaptureError(code); err.message = data; errorCallback(err); } // Check if necessary API available if (!CaptureNS.CameraCaptureUI) { // We are running on WP8.1 which lacks CameraCaptureUI class // so we need to use MediaCapture class instead and implement custom UI for camera var proxy = new MediaCaptureProxy(); proxy.captureVideo(function (videoFile) { videoFile.getBasicPropertiesAsync().done(function (basicProperties) { var result = new MediaFile(videoFile.name, 'ms-appdata:///local/' + videoFile.name, videoFile.contentType, basicProperties.dateModified, basicProperties.size); result.fullPath = videoFile.path; successCallback([result]); }, function (err) { fail(CaptureError.CAPTURE_INTERNAL_ERR, err); }); }, fail); } else { var videoOptions = new CaptureVideoOptions(); if (options.duration && options.duration > 0) { videoOptions.duration = options.duration; } if (options.limit > 1) { videoOptions.limit = options.limit; } var cameraCaptureUI = new Windows.Media.Capture.CameraCaptureUI(); cameraCaptureUI.videoSettings.allowTrimming = true; cameraCaptureUI.videoSettings.format = Windows.Media.Capture.CameraCaptureUIVideoFormat.mp4; cameraCaptureUI.videoSettings.maxDurationInSeconds = videoOptions.duration; cameraCaptureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.video).then(function(file) { if (file) { file.moveAsync(Windows.Storage.ApplicationData.current.localFolder, "cameraCaptureVideo.mp4", Windows.Storage.NameCollisionOption.generateUniqueName).then(function () { file.getBasicPropertiesAsync().then(function(basicProperties) { var result = new MediaFile(file.name, 'ms-appdata:///local/' + file.name, file.contentType, basicProperties.dateModified, basicProperties.size); result.fullPath = file.path; successCallback([result]); }, function() { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); }, function() { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); } else { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); } }, function() { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); }); } }, getFormatData: function (successCallback, errorCallback, args) { Windows.Storage.StorageFile.getFileFromPathAsync(args[0]).then( function (storageFile) { var mediaTypeFlag = String(storageFile.contentType).split("/")[0].toLowerCase(); if (mediaTypeFlag === "audio") { storageFile.properties.getMusicPropertiesAsync().then(function (audioProperties) { successCallback(new MediaFileData(null, audioProperties.bitrate, 0, 0, audioProperties.duration / 1000)); }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); }); } else if (mediaTypeFlag === "video") { storageFile.properties.getVideoPropertiesAsync().then(function (videoProperties) { successCallback(new MediaFileData(null, videoProperties.bitrate, videoProperties.height, videoProperties.width, videoProperties.duration / 1000)); }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); }); } else if (mediaTypeFlag === "image") { storageFile.properties.getImagePropertiesAsync().then(function (imageProperties) { successCallback(new MediaFileData(null, 0, imageProperties.height, imageProperties.width, 0)); }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); }); } else { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); } }, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); } ); } }; require("cordova/exec/proxy").add("Capture",module.exports);