mighty-webcamjs
Version:
HTML5 Webcam Image Capture Library with Flash Fallback
1,289 lines (1,115 loc) • 42.3 kB
JavaScript
/* eslint-disable */
// WebcamJS __VERSION__
// Webcam library for capturing JPEG/PNG images in JavaScript
// Attempts getUserMedia, falls back to Flash
// Author: Joseph Huckaby: http://github.com/jhuckaby
// Based on JPEGCam: http://code.google.com/p/jpegcam/
// Copyright (c) 2012 - 2016 Joseph Huckaby
// Licensed under the MIT License
require('webrtc-adapter');
var EXIF = require('exif-js');
var videoRecorder = require('./video-recorder.js');
var _userMedia;
var location = global.location;
var protocol = location && location.protocol.replace(/:/, '');
var FLASH_EMBED_ID = 'webcam_movie_embed';
var FLASH_OBJ_ID = 'webcam_movie_obj';
var css_prefix = 'webcamjs'; // prefix for all css classes
var URL = global.URL || global.webkitURL || global.mozURL || global.msURL;
// Safari fails in cookies disabled mode when even trying to access the localStorage
var localStorage;
try {
localStorage = global.localStorage;
} catch (e) {}
var navigator = global.navigator || {};
var ua = navigator.userAgent || "";
var isBrowser = typeof window !== 'undefined' && (!ua.includes("Node.js") && !ua.includes("jsdom"));
var blurMeasureInspector = isBrowser ? require('inspector-bokeh') : function() {};
var DETECT_RESOLUTIONS = [
480, 544, 576, 640, 720, 724, 792, 800, 864, 936, 960, 1024, 1180, 1200, 1280, 1440, 1536, 1600, 1920, 2048, 2560, 3032, 3072, 3264
];
function getAndroidVersion() {
var match = ua.match(/android\s([0-9\.]*)/i);
return match ? match[1] : false;
}
function getIOSVersion() {
return !global.MSStream && (function iOSversion() {
if (/iP(hone|od|ad)/.test(navigator.platform)) {
// supports iOS 2.0 and later: <http://bit.ly/TJjs1V>
var v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/) || [0, 0, 0];
return parseFloat(parseInt(v[1], 10) + '.' + parseInt(v[2], 10));
}
})();
}
var isDesktop = !getAndroidVersion() && !getIOSVersion();
// There are specific problems on some devices (Google Pixel, new Samsungs, etc).
// Forcing WebRTC detection mode seems to fix NotReadable issue.
//
// Desktops can't use WebRTC detection mode because facingMode: 'user' / 'environment' always
// brings default camera even on Microsoft Surface Pro tablet.
var forceWebrtcDetectionMode = !isDesktop;
const debouncePromise = (func) => {
// debouncePromise takes function, that returns promise
// and makes sure - there is just one concurrent call of promise.
// NOTE: arguments might be ignored
let isPromiseRunning = false;
let lastPromise;
return (...args) => {
if (!isPromiseRunning) {
lastPromise = new Promise((resolve, reject) => {
isPromiseRunning = true;
const promise = func(...args);
const end = () => { isPromiseRunning = false; }
const resolver = (...promiseArgs) => (end(), resolve(...promiseArgs));
const rejector = (...promiseArgs) => (end(), reject(...promiseArgs));
promise.then(resolver, rejector);
});
}
return lastPromise;
}
}
// declare error types
// inheritance pattern here:
// https://stackoverflow.com/questions/783818/how-do-i-create-a-custom-error-in-javascript
function FlashError() {
var temp = Error.apply(this, arguments);
temp.name = this.name = "FlashError";
this.stack = temp.stack;
this.message = temp.message;
}
function WebcamError() {
var temp = Error.apply(this, arguments);
temp.name = this.name = "WebcamError";
this.stack = temp.stack;
this.message = temp.message;
}
function IntermediateInheritor() {}
IntermediateInheritor.prototype = Error.prototype;
FlashError.prototype = new IntermediateInheritor();
WebcamError.prototype = new IntermediateInheritor();
const DETECTION_MODE_LABELS = 'DETECTION_MODE_LABELS';
const DETECTION_MODE_WEBRTC = 'DETECTION_MODE_WEBRTC';
const CAM_FRONT = 'front';
const CAM_BACK = 'back';
const CAPTURE_MODE_PHOTO = 'CAPTURE_MODE_PHOTO';
const CAPTURE_MODE_VIDEO = 'CAPTURE_MODE_VIDEO';
const whenDOMReady = (() => {
// takes function, that can be called, when DOM is loaded.
let isDOMLoaded = false;
const domReadyPromise = new Promise((resolve) => {
if (isBrowser) {
document.addEventListener('DOMContentLoaded', resolve, false);
} else {
resolve();
}
});
domReadyPromise.then(() => { isDOMLoaded = true; });
return (func) => {
// on Chrome 52 @ android mediaDevices.getUserMedia is triggered with a delay.
// functions to be triggered when camera is ready
return (...args) => {
if (isDOMLoaded) {
// synchronous call
return func.apply(Webcam, args);
} else {
// asynchronous call
return domReadyPromise.then(() => func.apply(Webcam, args));
}
}
};
})();
var Webcam = {
version: '__VERSION__',
state: {
load: false, // true when webcam movie finishes loading
live: false // true when webcam is initialized and ready to snap
},
// globals
userMedia: true, // true when getUserMedia is supported natively
constants: {
CAM_FRONT,
CAM_BACK,
DETECTION_MODE_LABELS,
DETECTION_MODE_WEBRTC,
CAPTURE_MODE_VIDEO,
CAPTURE_MODE_PHOTO
},
params: {
capture_mode: CAPTURE_MODE_PHOTO,
width: 0,
height: 0,
dest_width: 2560, // size of captured image.
dest_height: 2560, // these default to width/height
image_format: 'jpeg', // image format (may be jpeg or png)
jpeg_quality: 90, // jpeg image quality from 0 (worst) to 100 (best)
enable_flash: true, // enable flash fallback,
force_flash: false, // force flash mode
force_file: false, // force file upload mode
flip_horiz: false, // flip image horiz (mirror mode)
flip_horiz_back: undefined, // flip image horizontally on both: front and back camera
flip_horiz_on_snap: false, // flip image horiz when snaped
fps: 30, // camera frames per second (used in Flash)
webcam_path: './', // URI to webcam.swf movie (defaults to the js location)
flash_not_detected_text: 'ERROR: No Adobe Flash Player detected. Webcam.js relies on Flash for browsers that do not support getUserMedia (like yours).',
enable_file_fallback: true,
force_fresh_photo: false, // When true, it will disallow to Select Photo from Library on iOS
no_interface_found_text: 'No supported webcam interface found.',
unfreeze_snap: true, // Whether to unfreeze the camera after snap (defaults to true)
camera: CAM_BACK, // "front" or "back"
cameraId: undefined, // For browsers, that does not support facingMode
cameraDetectionMode: null, // Labels are more reliable than webrtc, but doesn't always work
switch_camera_node: null, // Contents of "Switch Camera" button. DOM Node required
maximum_blur_index: 0, // 0 to disable, less is better. Recommended value: 0.8
// read more and experiment: https://github.com/CezaryDanielNowak/inspector-bokeh
verbose: true // Set to false to hide logs.
},
errors: {
FlashError: FlashError,
WebcamError: WebcamError
},
hooks: {}, // callback hook functions
cameraInfs: [], // list of Inf objects for atteched camera devices
init() {
// initialize, check for getUserMedia support
// Setup getUserMedia, with polyfill for older browsers
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
this.mediaDevices = navigator.mediaDevices || false;
if (Webcam.params.verbose) {
global.Webcam = Webcam;
}
this.userMedia = this.userMedia && !!this.mediaDevices && !!URL;
// Older versions of firefox (< 21) apparently claim support but user media does not actually work
if (ua.match(/Firefox\D+(\d+)/)) {
if (parseInt(RegExp.$1, 10) < 21) this.userMedia = null;
}
// Make sure media stream is closed when navigating away from page
if (this.userMedia) {
detectVideoInputs(this.mediaDevices);
addEventListener('beforeunload', this.reset.bind(this));
var locked = false;
addEventListener('orientationchange', function() {
if (
locked // prevents multiple attach on rapid orientationchange
|| !Webcam.state.load // Webcam not loaded yet
|| Webcam.state.videoRecording // Recording in progress
) return;
if (Webcam.state.load) {
locked = true;
// video needs to be reinitialised after screen rotation so taken pictures aren't
// rotated.
Webcam.reattach().then(() => { locked = false; });
}
});
}
},
reattach() {
return new Promise((resolve) => {
var container = Webcam.container;
Webcam.reset().then(function() {
resolve();
Webcam.attach(container);
});
});
},
checkIfCanCapture() {
// isWebRTCBrowser: for every other browser we need to provide fallback: file or flash.
var isWebRTCBrowser = Webcam.userMedia;
// isWebRTCCameraWorking: false when camera did not start by some reason - e.g. access not granted.
var isWebRTCCameraWorking = !!(isWebRTCBrowser && Webcam.stream && Webcam.stream.active);
return !isWebRTCBrowser || isWebRTCCameraWorking;
},
switchCamera: whenDOMReady(function (...args) {
return new Promise((resolve) => {
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('switchCamera called with argument', args);
}
const cam = args[0];
if (!this.params.cameraDetectionMode && this.userMedia) {
return detectVideoInputs(this.mediaDevices).then(() => {
Webcam.switchCamera(cam).then(resolve);
});
}
// This function is much more reliable than webrtc `facingMode: 'user' / 'environment'`
var _cam = cam || (Webcam.params.camera === CAM_FRONT ? CAM_BACK : CAM_FRONT);
var resultCameraId = 0;
if (Number.isFinite(Webcam.params.cameraId) && _cam === Webcam.params.camera) {
return resolve();
}
if (!cam) {
resultCameraId = (Webcam.params.cameraId + 1) % Webcam.cameraInfs.length;
}
resultCameraId = getCameraIdByLabel(_cam) || resultCameraId;
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('Camera switching', {
'camera': _cam,
'cameraId': resultCameraId
});
}
Webcam.set({
'camera': _cam,
'cameraId': resultCameraId
});
resolve();
})
}),
attach: whenDOMReady(function (container) {
if (this.container === container) {
return this.reattach();
}
var self = this;
// create webcam preview and attach to DOM element
// pass in actual DOM reference, ID, or CSS selector
if (typeof container == 'string') {
container = document.querySelector(container);
}
if (!container) {
return this.dispatch('error', new WebcamError("Could not locate DOM element to attach to."));
}
if (this.params.cameraId === undefined) {
return this.switchCamera(this.params.camera).then(() => {
Webcam.attach(container);
});
}
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('WebcamJS Attaching', container);
}
this.container = container;
container.innerHTML = ''; // start with empty element
// insert "peg" so we can insert our preview canvas adjacent to it later on
var peg = this.peg = createElement('div', { className: css_prefix + '__peg' });
container.appendChild( peg );
// set width/height if not already set
if (!this.params.width) this.params.width = container.offsetWidth;
if (!this.params.height) this.params.height = container.offsetHeight;
// no crop, set size to desired
container.style.width = this.params.width + 'px';
container.style.height = this.params.height + 'px';
// make sure we have a nonzero width and height at this point
if (!this.params.width || !this.params.height) {
return this.dispatch('error', new WebcamError("No width and/or height for webcam. Please call set() first, or attach to a visible element."));
}
// set defaults for dest_width / dest_height if not set
if (!this.params.dest_width) this.params.dest_width = this.params.width;
if (!this.params.dest_height) this.params.dest_height = this.params.height;
this.userMedia = _userMedia === undefined ? this.userMedia : _userMedia;
// if force_flash is set, disable userMedia
if (this.params.force_flash || this.params.force_file) {
_userMedia = this.userMedia;
this.userMedia = null;
}
// check for default fps
this.params.fps = this.params.fps * 1 || 30;
var styleSheetURL = this.params.webcam_path + 'webcam.css';
if (!this.styleSheet || this.styleSheet.getAttribute('href') !== styleSheetURL) {
this.styleSheet = createElement('link', {
href: styleSheetURL,
media: 'all',
rel: 'stylesheet',
type: 'text/css'
});
document.head.appendChild(this.styleSheet);
}
blurMeasureInspector.setup({
workerURL: Webcam.params.webcam_path + 'measure_blur/measure_blur_worker.js'
});
if (this.userMedia) {
var video = this.video = createElement('video', {
className: css_prefix + '__video',
autoplay: 'autoplay',
playsinline: 'playsinline', // THIS IS IMPORTANT FOR iOS11 !
muted: 'true',
onplaying: () => {
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log(`loadedVideoDimensions ${video.videoWidth}x${video.videoHeight}`);
}
self.dispatchState('loadedVideoDimensions', {
width: video.videoWidth,
height: video.videoHeight
});
}
});
// add video element to dom
container.appendChild( video );
// ask user for access to their camera
detectVideoInputs(this.mediaDevices).then(function() {
if (!Webcam.cameraInfs.length) {
self.dispatch('error', 'Camera not detected.');
} else if (Webcam.cameraInfs.length > 1) {
Webcam.switchCamera(self.params.camera);
// setup switch camera button
var switchButton = self.switchButton = createElement('button', {
className: css_prefix + '__switch-camera-button',
onclick: () => {
Webcam.switchCamera().then(() => {
Webcam.reattach();
});
}
});
switchButton.appendChild(
self.params.switch_camera_node === null
? document.createTextNode('Switch camera')
: self.params.switch_camera_node
);
container.appendChild( switchButton );
}
var maxWidth = 9999;
const androidVersion = getAndroidVersion();
if (androidVersion && self.params.capture_mode === CAPTURE_MODE_VIDEO) {
// android-only optimisationon so video is not lagging
//
// NOTE: following code fails when maxWidth is set too low
// on some devices (Pixel XL, Huawei p8 lite, Huawei P10), Android 5 devices
if (androidVersion < 6) {
maxWidth = 1280;
} else if (parseInt(androidVersion, 10) === 6) {
maxWidth = 1280;
} else if (androidVersion >= 7) {
maxWidth = 1680;
}
if (/(HUAWEIVTR-L29|SM-G950|; Pixel|Nexus 6P)/.test(ua)) { // Huawei P10
maxWidth = 1920;
}
}
// TODO: this code should not be called until self.params.cameraId is set. Investigate.
var videoConstraints;
if (self.params.cameraDetectionMode === DETECTION_MODE_LABELS) {
// http://stackoverflow.com/a/27444179/2590921
var allSizes = DETECT_RESOLUTIONS.reduce(function(result, val) {
if (val <= maxWidth && val <= self.params.dest_width) {
result.push({ minWidth: val });
}
return result;
}, [{ sourceId: Webcam.cameraInfs[self.params.cameraId].deviceId } ]);
videoConstraints = {
optional: allSizes
};
} else {
videoConstraints = {
facingMode: self.params.camera === CAM_FRONT ? 'user' : 'environment',
width: { min: DETECT_RESOLUTIONS[0], ideal: Math.min(self.params.dest_width, maxWidth), max: maxWidth }
};
}
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('videoConstraints used to getUserMedia', videoConstraints);
}
self.mediaDevices.getUserMedia({
"audio": false,
"video": videoConstraints
}).then( function(stream) {
// mediaDevices.getUserMedia callback fires ONLY when camera access is granted.
// detectVideoInputs will provide new list with labels.
detectVideoInputs(self.mediaDevices, true);
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('stream from getUserMedia ready.', video);
}
// got access, attach stream to video
video.srcObject = stream; // NOTE: Polyfilled with adapter-js
video.onloadedmetadata = function(e) {
self.stream = stream;
self.dispatchState('load', true);
self.dispatchState('live', true);
self.flip();
};
}, function(err) {
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('getUserMedia failed', err);
}
// JH 2016-07-31 Instead of dispatching error, now falling back to Flash if userMedia fails (thx @john2014)
// JH 2016-08-07 But only if flash is actually installed -- if not, dispatch error here and now.
if (self.params.enable_flash && self.detectFlash()) {
setTimeout(function() {
self.params.force_flash = 1;
self.attach(container);
});
} else {
self.dispatch('error', err);
}
});
});
this.dispatchState('cameraMode', 'webrtc');
} else if (!this.detectFlash() && this.params.enable_file_fallback) {
container.appendChild( this.getUploadFallbackNode(container.dataset.channel) );
this.dispatchState('load', true);
this.dispatchState('cameraMode', 'file-fallback');
} else if (this.params.enable_flash && this.detectFlash()) {
// flash fallback
global.Webcam = Webcam; // needed for flash-to-js interface
container.appendChild(
createElement('div', {
className: css_prefix + '__flash-container',
innerHTML: this.getSWFHTML()
})
);
this.dispatchState('cameraMode', 'flash');
this.dispatchState('load', true);
} else {
this.dispatch('error', new WebcamError( this.params.no_interface_found_text ));
container.appendChild(
createElement('div', {
className: css_prefix + '__flash-not-detected',
innerHTML: '<a href="https://get.adobe.com/pl/flashplayer/" target="_blank"> Please click here to install or enable Flash Player</a>'
})
);
this.dispatchState('cameraMode', 'none');
}
}),
reset: debouncePromise((config = {}) => {
return new Promise(function(resolve) {
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('WebcamJS reset');
}
// shutdown camera, reset to potentially attach again
if (this.preview_active) this.unfreeze();
// attempt to fix issue #64
this.unflip();
var stream = this.stream;
if (this.userMedia) {
if (stream) {
if (stream.getVideoTracks) {
// get video track to call stop on it
var tracks = stream.getVideoTracks();
if (tracks && tracks[0] && tracks[0].stop) tracks[0].stop();
}
if (stream.getAudioTracks) {
// get video track to call stop on it
var tracks = stream.getAudioTracks();
if (tracks && tracks[0] && tracks[0].stop) tracks[0].stop();
}
if (stream.stop) {
// deprecated, may be removed in future
stream.stop();
}
}
delete this.stream;
delete this.video;
} else if (this.detectFlash()) {
// call for turn off camera in flash
var movie = this.getFlashMovie({ silent: config.silent });
if (movie && movie._releaseCamera) movie._releaseCamera();
}
if (this.container) {
this.container.innerHTML = '';
delete this.container;
}
this.dispatchState('load', false);
this.dispatchState('live', false);
delete this.fallbackImage;
Object.keys(Webcam.state).forEach((key) => {
delete Webcam.state[key];
});
function resolveWhenUnloaded() {
setTimeout(function() {
if (stream && stream.active) {
resolveWhenUnloaded();
} else {
resolve();
}
}, 150);
}
resolveWhenUnloaded();
}.bind(Webcam));
}),
set(...args) {
// set one or more params
// variable argument list: 1 param = hash, 2 params = key, value
if (args.length == 1) {
for (var key in args[0]) {
this.params[key] = args[0][key];
}
} else {
this.params[ args[0] ] = args[1];
}
},
on(name, callback) {
// set callback hook
name = name.toLowerCase().replace(/^on/, '');
if (!this.hooks[name]) this.hooks[name] = [];
this.hooks[name].push( callback );
},
off(name, callback) {
// remove callback hook
name = name.toLowerCase().replace(/^on/, '');
if (this.hooks[name]) {
if (callback) {
// remove one selected callback from list
var idx = this.hooks[name].indexOf(callback);
if (idx > -1) this.hooks[name].splice(idx, 1);
} else {
// no callback specified, so clear all
this.hooks[name] = [];
}
}
},
dispatchState(name, value) {
Webcam.state[name] = value;
Webcam.dispatch(name, value);
},
dispatch(...args) {
if (Webcam.params.verbose) {
console.info('webcam dispatch', args);
}
// fire hook callback, passing optional value to it
var name = args[0].toLowerCase().replace(/^on/, '');
var args = Array.prototype.slice.call(args, 1);
if (this.hooks[name] && this.hooks[name].length) {
for (var idx = 0, len = this.hooks[name].length; idx < len; idx++) {
var hook = this.hooks[name][idx];
if (typeof hook == 'function') {
// callback is function reference, call directly
hook.apply(this, args);
} else if ((typeof hook == 'object') && (hook.length == 2)) {
// callback is PHP-style object instance method
// TODO: review if this part is used anywhere?
hook[0][hook[1]].apply(hook[0], args);
}
} // loop
return true;
} else if (name == 'error') {
var message;
if ((args[0] instanceof FlashError) || (args[0] instanceof WebcamError)) {
message = args[0].message;
} else {
message = "Could not access webcam: " + args[0].name + ": " +
args[0].message + " " + args[0].toString();
}
// default error handler if no custom one specified
alert("Webcam.js Error: " + message);
}
return false; // no hook defined
},
detectFlash() {
return this.params.force_file ? false : detectFlash();
},
getUploadFallbackNode(communicationChannel) {
var input = createElement('input', {
type: 'file',
accept: this.params.capture_mode === CAPTURE_MODE_PHOTO ? 'image/*' : 'video/*',
capture: this.params.camera === CAM_BACK ? 'environment' : 'user',
'data-channel': communicationChannel
});
var div = createElement('div', { className: css_prefix + '__upload-fallback' });
div.appendChild(input);
var uploadHandler = this.params.capture_mode === CAPTURE_MODE_PHOTO
? this.handleImageInput.bind(this)
: this.helpers.videoRecorder.handlefileFallbackVideoInput.bind(this)
input.addEventListener('change', uploadHandler, false);
return div;
},
blurChecker(canvas) {
var self = this;
return new Promise(function(resolve, reject) {
if (self.params.maximum_blur_index) {
measureBlur(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height))
.then(function(blurScore) {
var score = blurScore.avg_edge_width_perc;
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('Blur index:', score);
}
if (score <= self.params.maximum_blur_index) {
resolve();
} else {
reject('Taken picture is too blurry, please re-take photo.');
}
});
} else {
resolve();
}
});
},
handleImageInput(e) {
var self = this;
if (!e.target.files || !e.target.files.length) {
delete self.fallbackImage;
return;
}
var imgFile = e.target.files[0];
this.fallbackImage = new Promise(function(resolve) {
handleImageInput(e.target.files[0]).then(function(img) {
var done = function(exif) {
adjustUploadedPhoto(img.data, exif)
.then(function(canvas) {
var cb = function() {
var imgData = canvas.toDataURL('image/' + self.params.image_format, self.params.jpeg_quality / 100);
var channel = e.target.dataset.channel ? ':' + e.target.dataset.channel : '';
self.dispatch('imageSelected' + channel, imgData);
resolve(imgData);
};
self.blurChecker(canvas).then(cb, function(errorMsg) {
self.dispatch('error', errorMsg);
});
});
};
var fail = function() {
self.dispatch('error', 'Please try again and select "Take Photo" option.');
};
validateUploadedPhoto(imgFile).then(done, fail);
});
});
},
getSWFHTML() {
// Return HTML for embedding flash based webcam capture movie
var params = Object.assign({}, this.params),
html = '',
webcam_path = params.webcam_path;
// make sure we aren't running locally (flash doesn't work)
if (protocol.match(/file/)) {
this.dispatch('error', new FlashError("Flash does not work from local disk. Please run from a web server."));
return '<h3 class="'+css_prefix+'__error">ERROR: the Webcam.js Flash fallback does not work from local disk. Please run it from a web server.</h3>';
}
// make sure we have flash
if (!this.detectFlash()) {
this.dispatch('error', new FlashError("Adobe Flash Player not found. Please install from get.adobe.com/flashplayer and try again."));
return '<h3 class="'+css_prefix+'__error">' + params.flash_not_detected_text + '</h3>';
}
// if this is the user's first visit, set flashvar so flash privacy settings panel is shown first
if (localStorage && !localStorage.getItem('webcamjs_visited')) {
this.params.new_user = 1;
try {
// Safari Private mode does not allow to write any data in localStorage.
// Exception is thrown instead.
localStorage.setItem('webcamjs_visited', 1);
} catch (e) {}
}
// construct flashvars string
var flashvars = '';
// HACK: Flash does not support higher resolutions so preview is distorted
params.dest_width = 640;
params.dest_height = 480;
params.width = 640;
params.height = 480;
for (var key in params) {
if (params[key] instanceof Object) continue; // exclude function, nodes etc
if (flashvars) flashvars += '&';
flashvars += key + '=' + escape(params[key]);
}
// construct object/embed tag
html += '<object class="'+css_prefix+'__flash" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" type="application/x-shockwave-flash" codebase="https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="'+params.width+'" height="'+params.height+'" id="'+FLASH_OBJ_ID+'" align="middle"><param name="wmode" value="opaque" /><param name="allowScriptAccess" value="always" /><param name="allowFullScreen" value="false" /><param name="movie" value="'+webcam_path+'webcam.swf" /><param name="loop" value="false" /><param name="menu" value="false" /><param name="quality" value="best" /><param name="bgcolor" value="#ffffff" /><param name="flashvars" value="'+flashvars+'"/><embed id="'+FLASH_EMBED_ID+'" src="'+webcam_path+'webcam.swf" wmode="opaque" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="'+params.width+'" height="'+params.height+'" name="'+FLASH_EMBED_ID+'" align="middle" allowScriptAccess="always" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="'+flashvars+'"></embed></object>';
this.dispatchState('loadedVideoDimensions', {
width: 640,
height: 480
});
return html;
},
getFlashMovie(config = {}) {
// get reference to movie object/embed in DOM
if (!this.state.load) return !config.silent && this.dispatch('error', new FlashError("Flash Movie is not loaded yet"));
var movie = document.getElementById(FLASH_OBJ_ID);
if (!movie || !movie._snap) movie = document.getElementById(FLASH_EMBED_ID);
if (!movie) this.dispatch('error', new FlashError("Cannot locate Flash movie in DOM"));
return movie;
},
freeze() {
// show preview, freeze camera
var self = this;
// kill preview if already active
if (this.preview_active) this.unfreeze();
// must unflip container as preview canvas will be pre-flipped
this.unflip();
// create canvas for holding preview
var preview_canvas = this.preview_canvas = createElement('canvas', {
className: css_prefix + '__preview'
});
// take snapshot, but fire our own callback
this.snap(preview_canvas).then(function() {
self.container.insertBefore( preview_canvas, self.peg );
// set flag for user capture (use preview)
self.preview_active = true;
});
},
unfreeze() {
// cancel preview and resume live video feed
if (this.preview_active) {
// remove preview canvas
this.container.removeChild( this.preview_canvas );
delete this.preview_canvas;
// unflag
this.preview_active = false;
// re-flip if we unflipped before
this.flip();
}
},
flip() {
// flip container horiz (mirror mode) if desired
if (this.params.flip_horiz && this.video) {
if (Webcam.params.camera === CAM_BACK && this.params.flip_horiz_back === false) {
return Webcam.unflip();
}
setPrefixedStyle(this.video, 'transform', 'scaleX(-1)');
this.video.style.filter = 'FlipH';
this.video.style.msFilter = 'FlipH';
}
},
unflip() {
// unflip container horiz (mirror mode) if desired
if (this.params.flip_horiz && this.video) {
setPrefixedStyle(this.video, 'transform', 'scaleX(1)');
this.video.style.filter = '';
this.video.style.msFilter = '';
}
},
savePreview(user_callback, user_canvas) {
// save preview freeze and fire user callback
var params = this.params;
var canvas = this.preview_canvas;
// render to user canvas if desired
if (user_canvas) {
var user_context = user_canvas.getContext('2d');
user_context.drawImage( canvas, 0, 0 );
}
// fire user callback if desired
user_callback(
user_canvas ? null : canvas.toDataURL('image/' + params.image_format, params.jpeg_quality / 100 )
);
// remove preview
if (this.params.unfreeze_snap) this.unfreeze();
},
snap(user_canvas) {
var self = this;
var params = this.params;
return new Promise(function(resolve, reject) {
// take snapshot and return image data uri
if (!this.state.load) {
return reject(
this.dispatch('error', new WebcamError("Webcam is not loaded yet"))
);
}
// if we have an active preview freeze, use that
if (this.preview_active) {
return this.savePreview( resolve, user_canvas );
}
// create offscreen canvas element to hold pixels
var canvas = document.createElement('canvas');
// HACK: if we have video (not flash fallback), use video's native width/height
canvas.width = this.video && this.video.videoWidth || this.params.dest_width;
canvas.height = this.video && this.video.videoHeight || this.params.dest_height;
var context = canvas.getContext('2d');
// flip canvas horizontally if desired
if (this.params.flip_horiz_on_snap) {
context.translate( params.dest_width, 0 );
context.scale( -1, 1 );
}
// create inline function, called after image load (flash) or immediately (native)
var func = function() {
// render image if needed (flash)
if (this && this.src && this.width && this.height) {
const useDestDimensions = this.width >= params.dest_width;
if (useDestDimensions) {
context.drawImage(this, 0, 0, params.dest_width, params.dest_height);
} else {
canvas.width = this.width;
canvas.height = this.height;
context.drawImage(this, 0, 0, this.width, this.height);
}
}
// render to user canvas if desired
if (user_canvas) {
user_canvas.width = canvas.width;
user_canvas.height = canvas.height;
drawImageScaled(canvas, user_canvas);
}
self.blurChecker(canvas).then(function() {
resolve(user_canvas ? null : canvas.toDataURL('image/' + params.image_format, params.jpeg_quality / 100 ));
}, function(errorMsg) {
self.dispatch('error', errorMsg);
reject();
});
};
// grab image frame from userMedia or flash movie
if (this.userMedia) {
context.drawImage(this.video, 0, 0, this.video.videoWidth, this.video.videoHeight);
// fire callback right away
func();
} else if (this.detectFlash()) {
// flash fallback
var raw_data = this.getFlashMovie()._snap();
// render to image, fire callback when complete
var img = new Image();
img.onload = func;
img.src = 'data:image/'+this.params.image_format+';base64,' + raw_data;
} else if (this.params.enable_file_fallback) {
if (this.fallbackImage) {
this.fallbackImage.then(function(image) {
var img = new Image();
img.src = image;
img.onload = () => {
drawImageScaled(img, canvas);
func();
};
});
} else {
return reject(this.dispatch('error', "Select picture first."));
}
} else if (this.state.load) {
return reject(this.dispatch('error', "Webcam has encountered an unknown error."));
} else {
return reject(this.dispatch('error', "Webcam is not loaded yet"));
}
}.bind(this));
},
configure(panel) {
// open flash configuration panel -- specify tab name:
// "camera", "privacy", "default", "localStorage", "microphone", "settingsManager"
if (!panel) panel = "camera";
this.getFlashMovie()._configure(panel);
},
flashNotify(type, msg) {
// receive notification from flash about event
switch (type) {
case 'flashLoadComplete':
// movie loaded successfully
this.dispatchState('load', true);
break;
case 'cameraLive':
// camera is live and ready to snap
this.dispatchState('live', true);
break;
case 'error':
// Flash error
this.dispatch('error', new FlashError(msg));
break;
default:
// catch-all event, just in case
// console.log("webcam flash_notify: " + type + ": " + msg);
break;
}
}
};
isBrowser && whenDOMReady(Webcam.init).call();
function drawImageScaled(img, canvas) {
if (process.env.NODE_ENV !== 'production') {
console.assert(canvas instanceof HTMLCanvasElement, 'canvas passed to drawImageScaled need to be a <canvas> element.');
console.assert(img instanceof HTMLImageElement || img instanceof Image || img instanceof HTMLCanvasElement, 'img passed to drawImageScaled need to be a <img> element.');
}
// stackoverflow.com/questions/23104582/scaling-an-image-to-fit-on-canvas#answer-23105310
var ctx = canvas.getContext('2d');
var hRatio = canvas.width / img.width;
var vRatio = canvas.height / img.height;
var ratio = Math.min( hRatio, vRatio );
var centerShift_x = ( canvas.width - img.width*ratio ) / 2;
var centerShift_y = ( canvas.height - img.height*ratio ) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, img.width, img.height, centerShift_x, centerShift_y, img.width*ratio, img.height*ratio);
}
function handleImageInput(rawFile) {
return new Promise(function(resolve, reject) {
if (!rawFile) return reject();
var reader = new FileReader();
reader.onload = function(readerEvent) {
var img = new Image();
img.onload = function() {
resolve({ // NOTE: This is not ImageData object!
rawFile: rawFile,
data: img,
width: img.width,
height: img.height
});
};
img.onerror = reject;
img.src = readerEvent.target.result;
};
reader.readAsDataURL(rawFile);
});
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function setPrefixedStyle(element, cssProp, cssValue) {
if (!element) return;
var prefixes = ['o', 'ms', 'moz', 'webkit'];
prefixes.forEach(function(prefix) {
element.style[prefix + capitalizeFirstLetter(cssProp)] = cssValue;
});
element.style[cssProp] = cssValue;
}
const enumerateDevices = debouncePromise((mediaDevices) => mediaDevices.enumerateDevices());
function detectVideoInputs(mediaDevices, isGetUserMediaCallback) {
var labels = '';
var promise = enumerateDevices(mediaDevices);
promise.then(function (info) {
Webcam.cameraInfs.length = 0;
for (var i in info) {
var inf = info[i];
if (inf.kind === 'videoinput') {
Webcam.cameraInfs.push(inf);
labels += inf.label || '';
}
}
// cameraDetectionMode works better and produces better picture in webrtc mode
const isLabels = labels.length && !getIOSVersion() ? true : false;
const cameraDetectionMode = Webcam.params.cameraDetectionMode;
let newDetectionMode = isLabels ? DETECTION_MODE_LABELS : DETECTION_MODE_WEBRTC;
newDetectionMode = forceWebrtcDetectionMode && DETECTION_MODE_WEBRTC || newDetectionMode;
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('detected cameraInfs:', Webcam.cameraInfs);
console.log('cameraDetectionMode set to:', newDetectionMode);
}
Webcam.set({
'cameraDetectionMode': newDetectionMode
});
// If detection mode changes, this mean user have just granted camera access. We need to find camera,
// we are already using by it's label, preventing glitches on switching camera.
const detectionModeChangedOverTime = isGetUserMediaCallback && newDetectionMode !== cameraDetectionMode;
if (detectionModeChangedOverTime) {
const resultCameraId = getCameraIdByLabel(Webcam.params.camera);
if (process.env.NODE_ENV !== 'production' && Webcam.params.verbose) {
console.log('Setting cameraID after granting access', {
'cameraId': resultCameraId
});
}
Webcam.set({ cameraId: undefined });
Webcam.switchCamera(Webcam.params.camera).then(Webcam.reattach);
}
});
return promise;
}
function detectFlash() {
// return true if browser supports flash, false otherwise
// Code snippet borrowed from: https://github.com/swfobject/swfobject
var SHOCKWAVE_FLASH = "Shockwave Flash",
SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash",
FLASH_MIME_TYPE = "application/x-shockwave-flash",
hasFlash = false;
if (navigator.plugins !== undefined && typeof navigator.plugins[SHOCKWAVE_FLASH] === "object") {
var desc = navigator.plugins[SHOCKWAVE_FLASH].description;
if (desc && (navigator.mimeTypes !== undefined && navigator.mimeTypes[FLASH_MIME_TYPE] && navigator.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) {
hasFlash = true;
}
}
else if (typeof ActiveXObject !== "undefined") {
try {
var ax = new ActiveXObject(SHOCKWAVE_FLASH_AX);
if (ax) {
var ver = ax.GetVariable("$version");
if (ver) hasFlash = true;
}
}
catch (e) {}
}
return hasFlash;
}
function createElement(elementName, props) {
var element = document.createElement(elementName);
var uniqAttrs = ['innerHTML', 'className', 'width', 'height'];
if (props.style) {
Object.assign(element.style, props.style);
}
Object.keys(props).forEach(function(prop) {
if (prop === 'style') return;
if (uniqAttrs.includes(prop) || /^on/.test(prop)) {
element[prop] = props[prop];
} else {
element.setAttribute(prop, props[prop]);
}
});
return element;
}
function validateUploadedPhoto(img) {
// When selecting "Take a photo" instead of "Select from Library"
// on iOS device, not all Exif data are sent. This how we detect,
// photo was just taken.
// Android's don't allow to pick a photo from a library, so problem
// does not need to be addressed there.
return new Promise(function(resolve, reject) {
if (getIOSVersion()) {
EXIF.getData(img, function() {
var exif = EXIF.getAllTags(this) || {};
if (Webcam.params.force_fresh_photo) {
if (
exif.Orientation &&
!exif.ExifVersion &&
!exif.DateTimeOriginal
) {
resolve(exif);
} else {
reject();
}
} else {
resolve(exif);
}
});
} else {
resolve();
}
});
}
function adjustUploadedPhoto(img, exif) {
if (process.env.NODE_ENV !== 'production') {
console.assert((img instanceof HTMLImageElement) || (img instanceof Image), 'img passed to adjustUploadedPhoto need to be a <img> element.');
exif && console.assert(exif instanceof Object, 'exif passed to adjustUploadedPhoto needs to be an object');
}
// This function resize and rotate photo if needed
//
// Read more, why rotation is required:
// - http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/
// - http://sylvana.net/jpegcrop/exif_orientation.html
return new Promise(function(resolve, reject) {
var canvas = createElement('canvas', {});
var ctx = canvas.getContext("2d");
var width = img.width;
var height = img.height;
var longerSide = Math.max(width, height);
var longerDestSize = Math.max(Webcam.params.dest_width, Webcam.params.dest_height);
var scale = Math.min(longerDestSize / longerSide, 1);
width = parseInt(scale * img.width, 10);
height = parseInt(scale * img.height, 10);
canvas.width = width;
canvas.height = height;
if (getIOSVersion()) {
// rotate photo according to exif on iOS.
if (exif && exif.Orientation) {
if (exif.Orientation >= 5) {
canvas.width = height;
canvas.height = width;
}
var transformProps = [ // http://stackoverflow.com/a/31273162/2590921
[1, 0, 0, 1, 0, 0],
[-1, 0, 0, 1, width, 0],
[-1, 0, 0, -1, width, height],
[1, 0, 0, -1, 0, height],
[0, 1, 1, 0, 0, 0],
[0, 1, -1, 0, height, 0],
[0, -1, -1, 0, height, width],
[0, -1, 1, 0, 0, width]
];
ctx.transform.apply(ctx, transformProps[exif.Orientation - 1]);
} else {
console.warn('Invalid image orientation from EXIF:', img, exif);
}
}
ctx.drawImage(img, 0, 0, width, height);
resolve(canvas);
});
}
function measureBlur(canvasData) {
return new Promise(function(resolve, reject) {
if (canvasData) {
blurMeasureInspector
.async(canvasData)
.then(resolve, reject);
} else {
reject('No canvasData');
}
});
}
function getCameraIdByLabel(_cam) {
let resultCameraId;
Webcam.cameraInfs.forEach(function (cameraInf, id) {
var label = cameraInf.label && cameraInf.label.toLowerCase() || '';
if (
_cam === CAM_FRONT && (label.includes('front') || label.includes('face')) ||
_cam === CAM_BACK && (label.includes('rear') || label.includes('back'))
) {
resultCameraId = id;
}
});
return resultCameraId;
}
Webcam.helpers = {
detectFlash: detectFlash,
detectVideoInputs: detectVideoInputs,
handleImageInput: handleImageInput,
drawImageScaled: drawImageScaled,
measureBlur: measureBlur,
videoRecorder: videoRecorder.init(Webcam)
};
module.exports = Webcam;