class CameraDevice { static EVENT_HAS_CAMERA = 'camera-state-update'; static EVENT_CAPTURE = 'camera-capture'; static EVENT_START = 'camera-start'; static EVENT_STOP = 'camera-stop'; static EVENT_STOPPED = 'camera-stopped'; static EVENT_START_REQUEST = 'camera-start-request'; constructor() { this.mediaDevices = navigator.mediaDevices; this.devicesList = this.mediaDevices.enumerateDevices(); this.currentFace = null; this.currentStream = null; this.currentDevicePtr = 0; this.started = false; this.initialize(); } initialize() { document.addEventListener('DOMContentLoaded', () => { this.mediaDevices.addEventListener("devicechange", this.detectWebcam.bind(this)); }); document.addEventListener(CameraDevice.EVENT_HAS_CAMERA, this.onCameraUpdate.bind(this)); document.addEventListener(CameraDevice.EVENT_START_REQUEST, () => this.launchRearCamera()); document.addEventListener('click', this.onCameraControlClick.bind(this)) this.detectWebcam(); } /** * @param {PointerEvent} event */ onCameraControlClick(event) { let target = event.target; if (!target.closest('#camera-video-controls')) { return; } let button = target.closest('button'); if (!button) { return; } switch (button.dataset.action) { case 'camera-start': switch (button.dataset.toFacing) { case 'next': return this.launchCameraOnNextDevice(); case 'front': return this.launchFrontCamera(); case 'rear': return this.launchRearCamera(); default: return this.launchCamera(this.handleVideo()) } case 'camera-take': return this.capture(); case 'camera-stop': return this.stopCamera(); } } /** * @param {Event} event */ onCameraUpdate(event) { if (this.started && !event.detail.hasCamera) { this.stopCamera(); } } /** * @param {Event} event */ onDeviceChanged(event) { this.detectWebcam(); } capture() { let video = document.getElementById("camera-video-stream-object"); let canvas = document.createElement('canvas'); const context = canvas.getContext("2d"); let stream = video.srcObject; let height = stream.getVideoTracks()[0].getSettings().height; let width = stream.getVideoTracks()[0].getSettings().width; canvas.height = height; /* DEFAULT_PHOTO_HEIGHT */ canvas.width = width; /* DEFAULT_PHOTO_WIDTH */ context.drawImage(video, 0, 0); document.dispatchEvent(new CustomEvent(CameraDevice.EVENT_CAPTURE, { detail: { target: video, canvas: canvas, image: canvas.toDataURL("image/png"), width: width, height: height, } })) this.stopCamera(); } detectWebcam() { this.mediaDevices = navigator.mediaDevices; if (!this.mediaDevices || !this.mediaDevices.enumerateDevices) { return this.hasCamera = false; } this.mediaDevices.enumerateDevices().then(devices => { this.devicesList = devices.filter(device => 'videoinput' === device.kind); this.hasCamera = this.devicesList.length > 0; document.dispatchEvent(new CustomEvent(CameraDevice.EVENT_HAS_CAMERA, {detail: {hasCamera: this.hasCamera}})); }) } /** * @param {string|null} cameraFacing * @return {*} */ handleVideo(cameraFacing = null) { if (cameraFacing === null) { return { audio: false, video: {} } } return { audio: false, video: { facingMode: cameraFacing } } }; launchCameraOnNextDevice() { this.currentDevicePtr++; this.launchCameraOnDevice(this.devicesList[this.currentDevicePtr % this.devicesList.length]); } /** * @param {InputDeviceInfo} device */ launchCameraOnDevice(device) { this.launchCamera({ audio: false, video: { deviceId: { exact: device.deviceId } } }); } /** * @param {object|null} initialConstraints */ launchCamera(initialConstraints = null) { let constraints = initialConstraints; if (constraints === null) { constraints = this.handleVideo(); } this.hideStartButton(); if (this.currentStream) { this.currentStream.getTracks().forEach(track => track.stop()); } navigator.mediaDevices.getUserMedia(constraints) .then((stream) => { this.currentFace = null; stream.getTracks().forEach(track => { if (track.getCapabilities().facingMode) { track.getCapabilities().facingMode.forEach(mode => { if (mode === 'user' || mode === 'environment') { this.currentFace = mode; } }) } }) this.currentStream = stream; this.showVideo(stream); this.updateLayoutWhenCameraStarts(); this.started = true; document.dispatchEvent(new CustomEvent(CameraDevice.EVENT_START)); }) .catch(ex => { // Fallback if (initialConstraints !== null) { console.log('fallback') this.launchCamera(); return; } this.showStartButton(); document.dispatchEvent(new CustomEvent(CameraDevice.EVENT_STOP)); PageNotifier.notify({ type: 'error', message: ex.name === 'NotAllowedError' ? __('app.camera.errors.denied') : ex.message }) // throw ex; }) } launchRearCamera() { this.launchCamera( this.handleVideo("environment") ); } launchFrontCamera() { this.launchCamera( this.handleVideo("user") ); } stopCamera() { document.dispatchEvent(new CustomEvent(CameraDevice.EVENT_STOP)); let ph = document.getElementById("camera-video-stream-placeholder"); let video = document.getElementById("camera-video-stream-object"); let stream = video.srcObject; let tracks = stream.getTracks(); tracks.forEach(track => track.stop()); video.srcObject = null; video.style.display = 'none'; ph.style.display = 'block'; let startButton = document.querySelector('[data-action="camera-start"]'); startButton.style.display = 'inline'; startButton.querySelector('[data-type="switch-icon"]').style.display = 'none'; startButton.querySelector('[data-type="play-icon"]').style.display = 'inline'; startButton.querySelector('span').textContent = __('app.camera.action.start'); let takePictureButton = document.querySelector('[data-action="camera-take"]'); takePictureButton.style.display = 'none'; let cameraStop = document.querySelector('[data-action="camera-stop"]'); cameraStop.style.display = 'none'; this.started = false; document.dispatchEvent(new CustomEvent(CameraDevice.EVENT_STOPPED)); } /** * @param {MediaStream} stream */ showVideo(stream) { let video = document.getElementById("camera-video-stream-object"); let ph = document.getElementById("camera-video-stream-placeholder"); video.srcObject = stream video.play() video.onloadeddata = () => { ph.style.display = 'none'; video.style.display = 'block'; } } updateLayoutWhenCameraStarts() { switch (this.currentFace) { case 'user': this.showSwitchButton('rear') break; case 'environment': this.showSwitchButton('front') break; default: this.showSwitchButton('rear') break; } let takePictureButton = document.querySelector('[data-action="camera-take"]'); takePictureButton.style.display = 'inline'; let cameraStop = document.querySelector('[data-action="camera-stop"]'); cameraStop.style.display = 'inline'; } /** * @param {string} face */ showSwitchButton(face) { let startButton = document.querySelector('[data-action="camera-start"]'); startButton.dataset.toFacing = face; startButton.style.display = 'inline'; startButton.querySelector('[data-type="play-icon"]').style.display = 'none'; startButton.querySelector('[data-type="switch-icon"]').style.display = 'inline'; startButton.querySelector('span').textContent = __('app.camera.action.switch'); } hideStartButton() { let startButton = document.querySelector('[data-action="camera-start"]'); startButton.style.display = 'none'; } showStartButton() { let startButton = document.querySelector('[data-action="camera-start"]'); startButton.style.display = 'inline'; } } __cd = new CameraDevice();