blob: b881d665313406de9f48074124911c5277230d26 [file] [log] [blame]
/*
*
* 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 */
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;
var capturePreview = null;
var captureCancelButton = null; // eslint-disable-line no-unused-vars
var captureSettings = null;
var captureStarted = false;
var capturedPictureFile;
var capturedVideoFile;
var 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 =
'<video id="capturePreview" style="width: 100%; height: 100%"></video>' +
'<div id="previewButtons" style="width: 100%; bottom: 0px; display: flex; position: absolute; justify-content: space-around; background-color: black;">' +
'<button id="takePicture" style="' +
buttonStyle +
'">Capture</button>' +
'<button id="cancelCapture" style="' +
buttonStyle +
'">Cancel</button>' +
'<button id="selectPicture" style="display: none; ' +
buttonStyle +
'">Accept</button>' +
'<button id="retakePicture" style="display: none; ' +
buttonStyle +
'">Retake</button>' +
'</div>';
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); // eslint-disable-line no-undef
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
);
var generateUniqueCollisionOption = Windows.Storage.CreationCollisionOption.generateUniqueName;
var 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();
var overwriteCollisionOption = Windows.Storage.CreationCollisionOption.replaceExisting;
var 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;
var 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;
var MediaPropsNS = Windows.Media.MediaProperties;
var localAppData = Windows.Storage.ApplicationData.current.localFolder;
var generateUniqueName = Windows.Storage.NameCollisionOption.generateUniqueName;
var mediaCapture = new CaptureNS.MediaCapture();
var mediaCaptureSettings = new CaptureNS.MediaCaptureInitializationSettings();
var mp3EncodingProfile = new MediaPropsNS.MediaEncodingProfile.createMp3(MediaPropsNS.AudioEncodingQuality.auto); // eslint-disable-line new-cap
var m4aEncodingProfile = new MediaPropsNS.MediaEncodingProfile.createM4a(MediaPropsNS.AudioEncodingQuality.auto); // eslint-disable-line new-cap
mediaCaptureSettings.streamingCaptureMode = CaptureNS.StreamingCaptureMode.audio;
var capturedFile;
var 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));
}
);
});
} else {
errorCallback(new CaptureError(CaptureError.CAPTURE_INTERNAL_ERR));
}
}
);
},
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);