Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update browser plugin #435

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ Optional parameters to customize the camera settings.
| saveToPhotoAlbum | <code>Boolean</code> | | Save the image to the photo album on the device after capture. |
| popoverOptions | <code>[CameraPopoverOptions](#module_CameraPopoverOptions)</code> | | iOS-only options that specify popover location in iPad. |
| cameraDirection | <code>[Direction](#module_Camera.Direction)</code> | <code>BACK</code> | Choose the camera to use (front- or back-facing). |
| customCameraContainer | <code>string</code> | | Browser-only option, specify the id of custom camera's container element. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[1] IMO, I think it would be better if these new options accept either a query string or DOMElement.

  • Query string would give more flexibility in how the element is discovered vs an ID only. In this case, we would need to use the document.querySelector method instead of the getElementById.

  • DOMElement could also be a viable option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customCameraContainer sounds like is something you need a specific DOMElement, so allowing flexibility of querySelector I think promotes bad coding practice.

If you need a specific element, the element should be tagged by an ID as to guarantee that you will receive the expected DOMElement. Using querySelector makes code brittle by picking the first occurrence of the selected term, which may not be the node you want.

The YouTube JS API[1] for example does something similar where it needs to insert HTML in a specific node of your choice. Their API signature accepts either an id, or the HTMLElement itself.

So I would prefer to keep customCameraContainer accepting an id rather than a query string. For flexibility, it could also optionally accept a DOM node and it would be up to the developer to find that reference as they see fit (which they could use querySelector if they wanted to, or it could be a fresh node that isn't even in the DOM tree yet (via document.createElement), etc.

[1] https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player

Copy link
Member

@erisu erisu Oct 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this PR is no longer valid and can be closed.

There was a discussion after the PR creation and a decision that users should maybe use WebRTC camera functionality instead of this. After that discussion, the progression of this PR halted.

If memory serves, iOS has limitation/problems with WebRTC camera functionality so the plugin will still be needed for iOS. It can be conditionally wrapped to decided when to use WebRTC over Plugin.

The PR author created this blog post: "Cross-Platform development with Cordova and Electron" which uses the WebRTC camera functionality for the sample application.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must be this limitation:

Does not work in standalone running ("installed") PWAs, getUserMedia returns no video input devices in UIWebView or WKWebView, but only directly in Safari.

https://caniuse.com/#search=getUserMedia

| customCaptureButton | <code>string</code> | | Browser-only option, specify the id of custom camera's capture button. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above [1].

| customCancelButton | <code>string</code> | | Browser-only option, specify the id of custom camera's cancel button. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above [1].

| customSourceInput | <code>string</code> | | Browser-only option, specify the id of custom source input element, when using diffrerent mode than the default `sourceType`. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above [1].


---

Expand Down
210 changes: 147 additions & 63 deletions src/browser/CameraProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,103 +19,187 @@
*
*/

var HIGHEST_POSSIBLE_Z_INDEX = 2147483647;
let localMediaStream;

function takePicture (success, error, opts) {
function takePicture (successCallback, errorCallback, opts) {
if (opts && opts[2] === 1) {
capture(success, error, opts);
capture(successCallback, errorCallback, opts);
} else {
var input = document.createElement('input');
input.style.position = 'relative';
input.style.zIndex = HIGHEST_POSSIBLE_Z_INDEX;
input.className = 'cordova-camera-select';
input.type = 'file';
input.name = 'files[]';
const customSourceInput = opts[15];

input.onchange = function (inputEvent) {
var reader = new FileReader(); /* eslint no-undef : 0 */
reader.onload = function (readerEvent) {
input.parentNode.removeChild(input);
let sourceInput = customSourceInput ? document.getElementById(customSourceInput) : createSourceInput();

var imageData = readerEvent.target.result;
handleSourceInput(successCallback, sourceInput);

return success(imageData.substr(imageData.indexOf(',') + 1));
};

reader.readAsDataURL(inputEvent.target.files[0]);
};
if (!customSourceInput) {
document.body.appendChild(sourceInput);
}
}
}

document.body.appendChild(input);
function capture (successCallback, errorCallback, opts) {
let targetWidth = opts[3];
let targetHeight = opts[4];
const customCameraContainer = opts[12];
const customCaptureButton = opts[13];
const customCancelButton = opts[14];

const customElements = {customCameraContainer, customCaptureButton, customCancelButton};

let parent = customCameraContainer ? document.getElementById(customCameraContainer) : createCameraContainer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let parent = customCameraContainer ? document.getElementById(customCameraContainer) : createCameraContainer();
const parent = customCameraContainer ? document.getElementById(customCameraContainer) : createCameraContainer();

let video = createVideoStreamContainer(parent, targetWidth, targetHeight);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let video = createVideoStreamContainer(parent, targetWidth, targetHeight);
const video = createVideoStreamContainer(parent, targetWidth, targetHeight);

let captureButton = customCaptureButton ? document.getElementById(customCaptureButton) : createButton(parent, 'Capture');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let captureButton = customCaptureButton ? document.getElementById(customCaptureButton) : createButton(parent, 'Capture');
const captureButton = customCaptureButton ? document.getElementById(customCaptureButton) : createButton(parent, 'Capture');

let cancelButton = customCancelButton ? document.getElementById(customCancelButton) : createButton(parent, 'Cancel');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let cancelButton = customCancelButton ? document.getElementById(customCancelButton) : createButton(parent, 'Cancel');
const cancelButton = customCancelButton ? document.getElementById(customCancelButton) : createButton(parent, 'Cancel');

// start video stream
startLocalMediaStream(errorCallback, video);

// if custom camera container is not set by the user,
// append parent to the document.body
if (!customCameraContainer) {
document.body.appendChild(video.parentNode);
}

// handle button click events
handleCaptureButton(successCallback, errorCallback, captureButton, video, customElements);
handleCancelButton(cancelButton, video, customElements);
}

function capture (success, errorCallback, opts) {
var localMediaStream;
var targetWidth = opts[3];
var targetHeight = opts[4];
function createCameraContainer () {
let parent = document.createElement('div');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let parent = document.createElement('div');
const parent = document.createElement('div');

parent.style.position = 'relative';
parent.style.zIndex = '2147483647'; // set highest possible z index
parent.className = 'cordova-camera-capture';

return parent;
}

function createVideoStreamContainer (parent, targetWidth, targetHeight) {
targetWidth = targetWidth === -1 ? 320 : targetWidth;
targetHeight = targetHeight === -1 ? 240 : targetHeight;

var video = document.createElement('video');
var button = document.createElement('button');
var parent = document.createElement('div');
parent.style.position = 'relative';
parent.style.zIndex = HIGHEST_POSSIBLE_Z_INDEX;
parent.className = 'cordova-camera-capture';
let video = document.createElement('video');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let video = document.createElement('video');
const video = document.createElement('video');

video.width = targetWidth;
video.height = targetHeight;

parent.appendChild(video);

return video;
}

function createButton (parent, innerText) {
let button = document.createElement('button');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let button = document.createElement('button');
const button = document.createElement('button');

button.innerHTML = innerText;

parent.appendChild(button);

video.width = targetWidth;
video.height = targetHeight;
button.innerHTML = 'Capture!';
return button;
}

button.onclick = function () {
function handleCaptureButton (successCallback, errorCallback, captureButton, video, customElements) {
captureButton.onclick = function () {
// create a canvas and capture a frame from video stream
var canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
canvas.getContext('2d').drawImage(video, 0, 0, targetWidth, targetHeight);
let canvas = document.createElement('canvas');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let canvas = document.createElement('canvas');
const canvas = document.createElement('canvas');

canvas.width = video.width;
canvas.height = video.height;
canvas.getContext('2d').drawImage(video, 0, 0, video.width, video.height);

// convert image stored in canvas to base64 encoded image
var imageData = canvas.toDataURL('image/png');
let imageData = canvas.toDataURL('image/png');
imageData = imageData.replace('data:image/png;base64,', '');

// stop video stream, remove video and button.
// Note that MediaStream.stop() is deprecated as of Chrome 47.
if (localMediaStream.stop) {
localMediaStream.stop();
} else {
localMediaStream.getTracks().forEach(function (track) {
track.stop();
});
}
parent.parentNode.removeChild(parent);
// stop video stream
stopLocalMediaStream(video, customElements);

return success(imageData);
return successCallback(imageData);
};
}

navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
function handleCancelButton (cancelButton, video, customElements) {
cancelButton.onclick = function () {
// stop video stream
stopLocalMediaStream(video, customElements);
};
}

function startLocalMediaStream (errorCallback, video) {

var successCallback = function (stream) {
const successCallback = function (stream) {
localMediaStream = stream;
if ('srcObject' in video) {
video.srcObject = localMediaStream;
} else {
video.src = window.URL.createObjectURL(localMediaStream);
}
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
document.body.appendChild(parent);
};

navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;

if (navigator.getUserMedia) {
navigator.getUserMedia({video: true, audio: false}, successCallback, errorCallback);
navigator.getUserMedia({video: true, audio: true}, successCallback, errorCallback);
} else {
alert('Your browser does not support camera.');
}
}

function stopLocalMediaStream (video, customElements) {
// stop video stream, remove video and captureButton.
// note: MediaStream.stop() is deprecated as of Chrome 47.
if (localMediaStream.stop) {
localMediaStream.stop();
} else {
alert('Browser does not support camera :(');
localMediaStream.getTracks().forEach(function (track) {
track.stop();
});
}

// remove newly created elements
removeAppendedCameraElements(video, customElements);
}

function removeAppendedCameraElements (video, customElements) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

const parent = video.parentNode;

if (!customElements.customCameraContainer) {
parent.parentNode.removeChild(parent);
} else if (!customElements.customCaptureButton && !customElements.customCancelButton) {
while (parent.hasChildNodes()) {
parent.removeChild(parent.lastChild);
}
} else if (parent.hasChildNodes() && !customElements.customCaptureButton) {
parent.removeChild(video);
parent.removeChild(parent.lastChild);
} else if (parent.hasChildNodes() && !customElements.customCancelButton) {
parent.removeChild(video);
parent.removeChild(parent.lastChild);
} else {
parent.removeChild(video);
}
}

function createSourceInput () {
let input = document.createElement('input');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let input = document.createElement('input');
const input = document.createElement('input');

input.style.position = 'relative';
input.style.zIndex = '2147483647'; // set highest possible z index
input.className = 'cordova-camera-select';
input.type = 'file';
input.name = 'files[]';

return input;
}

function handleSourceInput (successCallback, sourceInput) {
sourceInput.onchange = function (inputEvent) {
let reader = new FileReader(); /* eslint no-undef : 0 */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let reader = new FileReader(); /* eslint no-undef : 0 */
const reader = new FileReader(); /* eslint no-undef : 0 */

reader.onload = function (readerEvent) {
sourceInput.parentNode.removeChild(sourceInput);

const imageData = readerEvent.target.result;

return successCallback(imageData.substr(imageData.indexOf(',') + 1));
};
reader.readAsDataURL(inputEvent.target.files[0]);
};
}

module.exports = {
Expand Down
50 changes: 26 additions & 24 deletions www/Camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
*
*/

var argscheck = require('cordova/argscheck');
var exec = require('cordova/exec');
var Camera = require('./Camera');
// XXX: commented out
// CameraPopoverHandle = require('./CameraPopoverHandle');
const argscheck = require('cordova/argscheck');
const exec = require('cordova/exec');
const Camera = require('./Camera');

/**
* @namespace navigator
Expand All @@ -32,10 +30,10 @@ var Camera = require('./Camera');
/**
* @exports camera
*/
var cameraExport = {};
const cameraExport = {};

// Tack on the Camera Constants to the base camera plugin.
for (var key in Camera) {
for (let key in Camera) {
cameraExport[key] = Camera[key];
}

Expand Down Expand Up @@ -134,27 +132,31 @@ for (var key in Camera) {
cameraExport.getPicture = function (successCallback, errorCallback, options) {
argscheck.checkArgs('fFO', 'Camera.getPicture', arguments);
options = options || {};
var getValue = argscheck.getValue;
const getValue = argscheck.getValue;

var quality = getValue(options.quality, 50);
var destinationType = getValue(options.destinationType, Camera.DestinationType.FILE_URI);
var sourceType = getValue(options.sourceType, Camera.PictureSourceType.CAMERA);
var targetWidth = getValue(options.targetWidth, -1);
var targetHeight = getValue(options.targetHeight, -1);
var encodingType = getValue(options.encodingType, Camera.EncodingType.JPEG);
var mediaType = getValue(options.mediaType, Camera.MediaType.PICTURE);
var allowEdit = !!options.allowEdit;
var correctOrientation = !!options.correctOrientation;
var saveToPhotoAlbum = !!options.saveToPhotoAlbum;
var popoverOptions = getValue(options.popoverOptions, null);
var cameraDirection = getValue(options.cameraDirection, Camera.Direction.BACK);
const quality = getValue(options.quality, 50);
const destinationType = getValue(options.destinationType, Camera.DestinationType.FILE_URI);
const sourceType = getValue(options.sourceType, Camera.PictureSourceType.CAMERA);
const targetWidth = getValue(options.targetWidth, -1);
const targetHeight = getValue(options.targetHeight, -1);
const encodingType = getValue(options.encodingType, Camera.EncodingType.JPEG);
const mediaType = getValue(options.mediaType, Camera.MediaType.PICTURE);
const allowEdit = !!options.allowEdit;
const correctOrientation = !!options.correctOrientation;
const saveToPhotoAlbum = !!options.saveToPhotoAlbum;
const popoverOptions = getValue(options.popoverOptions, null);
const cameraDirection = getValue(options.cameraDirection, Camera.Direction.BACK);

var args = [quality, destinationType, sourceType, targetWidth, targetHeight, encodingType,
mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions, cameraDirection];
const customCameraContainer = getValue(options.customCameraContainer, null);
const customCaptureButton = getValue(options.customCaptureButton, null);
const customCancelButton = getValue(options.customCancelButton, null);
const customSourceInput = getValue(options.customSourceInput, null);

const args = [quality, destinationType, sourceType, targetWidth, targetHeight, encodingType,
mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions, cameraDirection,
customCameraContainer, customCaptureButton, customCancelButton, customSourceInput];

exec(successCallback, errorCallback, 'Camera', 'takePicture', args);
// XXX: commented out
// return new CameraPopoverHandle();
};

/**
Expand Down