UNPKG

mighty-webcamjs

Version:

HTML5 Webcam Image Capture Library with Flash Fallback

1,289 lines (1,115 loc) 42.3 kB
/* 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;