UNPKG

video-auth-js-sdk

Version:

A SDK to authenticate users with camera through a realtime stream

217 lines (196 loc) 9.44 kB
import * as faceapi from '@vladmandic/face-api'; import {errorList} from "./errorHandler"; class FaceAPI { constructor(app) { this._app = app; this.optionsSSDMobileNet = null; this.minScore = 0.2; // minimum score this.maxResults = 5; // maximum number of results to return this.modelPath = app.params.modelPath; // path to model folder that will be loaded using http this._isDistroyed = false; } async detectIsFaceCorrect(callback){ //import * as faceapi from '../dist/face-api.esm.js'; // use when in dev mode // const modelPath = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/'; // path to model folder that will be loaded using http await this.main(); } drawFaces(canvas, data, fps) { if(!data.length) { this._app.store.faceDetected = false; } const ctx = canvas.getContext('2d', { willReadFrequently: true }); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); // draw title ctx.font = 'small-caps 20px "Segoe UI"'; ctx.fillStyle = 'white'; ctx.fillText(`FPS: ${fps}`, 10, 25); for (const person of data) { // draw box around each face ctx.lineWidth = 3; ctx.strokeStyle = 'deepskyblue'; ctx.fillStyle = 'deepskyblue'; ctx.globalAlpha = 0.6; ctx.beginPath(); let ellipseBoxPosCenter = { x: parseInt(canvas.width / 2), y: parseInt(canvas.height / 2), radiusX: 70, radiusY: 100 }; if((person.detection.box.x + (person.detection.box.width / 2)) < ellipseBoxPosCenter.x + 20 && (person.detection.box.x + (person.detection.box.width / 2)) > ellipseBoxPosCenter.x - 20 && (person.detection.box.y + (person.detection.box.height / 2)) > ellipseBoxPosCenter.y - 20 && (person.detection.box.y + (person.detection.box.height / 2)) < ellipseBoxPosCenter.y + 20 && person.angle.yaw > -20 && person.angle.yaw < 20 && person.angle.pitch > -15 && person.angle.pitch < 15 ) { this._app.store.faceDetected = true; ctx.strokeStyle = 'green'; } else { ctx.strokeStyle = 'red'; this._app.store.faceDetected = false; } ctx.ellipse(ellipseBoxPosCenter.x, ellipseBoxPosCenter.y, ellipseBoxPosCenter.radiusX, ellipseBoxPosCenter.radiusY, Math.PI, 0, 2 * Math.PI); ctx.stroke(); } } async detectVideo(video, canvas) { if (!video || video.paused || this._isDistroyed) return false; const t0 = performance.now(); faceapi .detectAllFaces(video, this.optionsSSDMobileNet) .withFaceLandmarks() .withFaceExpressions() // .withFaceDescriptors() .withAgeAndGender() .then((result) => { const fps = 1000 / (performance.now() - t0); const resizedDetections = faceapi.resizeResults(result, {width: canvas.width, height: canvas.height}); this.drawFaces(canvas, resizedDetections, fps.toLocaleString()); setTimeout(()=>{ requestAnimationFrame(() => this.detectVideo(video, canvas)); }, 1000); return true; }) .catch((err) => { console.log(`Detect Error: ${err}`); return false; }); return false; } // just initialize everything and call main function async setupCamera() { const video = this._app.store.videoTag; const canvas = this._app.store.canvasTag; if (!video || !canvas) return null; // setup webcam. note that navigator.mediaDevices requires that page is accessed via https if (!navigator.mediaDevices) { console.error('Camera Error: access not supported'); this._app.publicCallbacks.onError(errorList.MEDIA_DEVICES_NOT_SUPPORTED); return null; } const constraints = { audio: false, video: { facingMode: 'user', exact: "environment", resizeMode: 'crop-and-scale' } }; if (window.innerWidth > window.innerHeight) constraints.video.width = {ideal: window.innerWidth }; //width: 1920, height: 1280 }//ideal: 1280}//window.innerWidth }; else constraints.video.height = { ideal: window.innerHeight }; try { this._app.store.localCameraStream = await navigator.mediaDevices.getUserMedia(constraints); } catch (err) { if (err.name === 'PermissionDeniedError' || err.name === 'NotAllowedError'){ console.error(`Camera Error: camera permission denied: ${err.message || err}`); this._app.publicCallbacks.onError(errorList.VIDEO_PERMISSION_ERROR); } if (err.name === 'SourceUnavailableError') { console.error(`Camera Error: camera not available: ${err.message || err}`); this._app.publicCallbacks.onError(this._app.errorHandler.getFilledErrorObject({ ...errorList.CAMERA_NOT_AVAILABLE, replacements: [(err.message || err)] })); } return null; } if (this._app.store.localCameraStream) { video.srcObject = this._app.store.localCameraStream; } else { console.error('Camera Error: stream empty'); return null; } const track = this._app.store.localCameraStream.getVideoTracks()[0]; const settings = track.getSettings(); if (settings.deviceId) delete settings.deviceId; if (settings.groupId) delete settings.groupId; // if (settings.aspectRatio) settings.aspectRatio = Math.trunc(100 * settings.aspectRatio) / 100; console.log(`Camera active: ${track.label}`); console.log(`Camera settings: `, {settings}); canvas.addEventListener('click', () => { if (video && video.readyState >= 2) { if (video.paused) { video.play(); this.detectVideo(video, canvas); } else { video.pause(); } } console.log(`Camera state: ${video.paused ? 'paused' : 'playing'}`); }); return new Promise((resolve) => { video.onloadeddata = async () => { let base = "videoWidth"; if(video.videoWidth < video.videoHeight){ base = "videoHeight"; } let aspect = video.videoWidth / video.videoHeight; if(base == "videoWidth") { canvas.width = video.offsetWidth; canvas.height = parseInt(video.offsetWidth / aspect); canvas.style.left = '0px'; canvas.style.top = parseInt((video.offsetHeight - canvas.height) / 2) + 'px'; } else { canvas.height = video.offsetHeight; canvas.width = parseInt(video.offsetHeight / aspect); canvas.style.top = '0px'; canvas.style.left = parseInt((video.offsetWidth - canvas.width) / 2) + 'px'; } setTimeout(()=>{ video.play(); this.detectVideo(video, canvas); resolve(true); }) }; }); } async setupFaceAPI() { await faceapi.nets.ssdMobilenetv1.load(this.modelPath); await faceapi.nets.ageGenderNet.load(this.modelPath); await faceapi.nets.faceLandmark68Net.load(this.modelPath); await faceapi.nets.faceRecognitionNet.load(this.modelPath); await faceapi.nets.faceExpressionNet.load(this.modelPath); this.optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence: this.minScore, maxResults: this.maxResults }); } async main() { // default is webgl backend await faceapi.tf.setBackend('webgl'); await faceapi.tf.ready(); if (faceapi.tf?.env().flagRegistry.CANVAS2D_WILL_READ_FREQUENTLY) faceapi.tf.env().set('CANVAS2D_WILL_READ_FREQUENTLY', true); if (faceapi.tf?.env().flagRegistry.WEBGL_EXP_CONV) faceapi.tf.env().set('WEBGL_EXP_CONV', true); if (faceapi.tf?.env().flagRegistry.WEBGL_EXP_CONV) faceapi.tf.env().set('WEBGL_EXP_CONV', true); await this.setupFaceAPI(); await this.setupCamera(); } destroy() { this._isDistroyed = true; if(this._app.store.videoParentTag) { this._app.store.videoParentTag.remove(); this._app.store.videoParentTag = null; } if(this._app.store.localCameraStream) { this._app.store.localCameraStream.getTracks().forEach(item => item.stop()); this._app.store.localCameraStream = null; } } } export default FaceAPI