video-auth-js-sdk
Version:
A SDK to authenticate users with camera through a realtime stream
217 lines (196 loc) • 9.44 kB
JavaScript
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