123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- // @flow
- import 'image-capture';
- import './createImageBitmap';
-
- import { getCurrentConference } from '../base/conference';
- import { getLocalParticipant, getParticipantCount } from '../base/participants';
- import { getLocalVideoTrack } from '../base/tracks';
- import { getBaseUrl } from '../base/util';
-
- import {
- ADD_FACE_EXPRESSION,
- ADD_TO_FACE_EXPRESSIONS_BUFFER,
- CLEAR_FACE_EXPRESSIONS_BUFFER,
- START_FACE_LANDMARKS_DETECTION,
- STOP_FACE_LANDMARKS_DETECTION,
- UPDATE_FACE_COORDINATES
- } from './actionTypes';
- import {
- DETECTION_TYPES,
- INIT_WORKER,
- WEBHOOK_SEND_TIME_INTERVAL
- } from './constants';
- import {
- getDetectionInterval,
- sendDataToWorker,
- sendFaceBoxToParticipants,
- sendFaceExpressionsWebhook
- } from './functions';
- import logger from './logger';
-
- declare var APP: Object;
-
- /**
- * Object containing a image capture of the local track.
- */
- let imageCapture;
-
- /**
- * Object where the face landmarks worker is stored.
- */
- let worker;
-
- /**
- * The last face expression received from the worker.
- */
- let lastFaceExpression;
-
- /**
- * The last face expression timestamp.
- */
- let lastFaceExpressionTimestamp;
-
- /**
- * How many duplicate consecutive expression occurred.
- * If a expression that is not the same as the last one it is reset to 0.
- */
- let duplicateConsecutiveExpressions = 0;
-
- /**
- * Variable that keeps the interval for sending expressions to webhook.
- */
- let webhookSendInterval;
-
- /**
- * Variable that keeps the interval for detecting faces in a frame.
- */
- let detectionInterval;
-
- /**
- * Loads the worker that detects the face landmarks.
- *
- * @returns {void}
- */
- export function loadWorker() {
- return function(dispatch: Function, getState: Function) {
- if (worker) {
- logger.info('Worker has already been initialized');
-
- return;
- }
-
- if (navigator.product === 'ReactNative') {
- logger.warn('Unsupported environment for face recognition');
-
- return;
- }
-
- const baseUrl = `${getBaseUrl()}libs/`;
- let workerUrl = `${baseUrl}face-landmarks-worker.min.js`;
-
- const workerBlob = new Blob([ `importScripts("${workerUrl}");` ], { type: 'application/javascript' });
-
- workerUrl = window.URL.createObjectURL(workerBlob);
- worker = new Worker(workerUrl, { name: 'Face Recognition Worker' });
- worker.onmessage = function(e: Object) {
- const { faceExpression, faceBox } = e.data;
-
- if (faceExpression) {
- if (faceExpression === lastFaceExpression) {
- duplicateConsecutiveExpressions++;
- } else {
- if (lastFaceExpression && lastFaceExpressionTimestamp) {
- dispatch(addFaceExpression(
- lastFaceExpression,
- duplicateConsecutiveExpressions + 1,
- lastFaceExpressionTimestamp
- ));
- }
- lastFaceExpression = faceExpression;
- lastFaceExpressionTimestamp = Date.now();
- duplicateConsecutiveExpressions = 0;
- }
- }
-
- if (faceBox) {
- const state = getState();
- const conference = getCurrentConference(state);
- const localParticipant = getLocalParticipant(state);
-
- if (getParticipantCount(state) > 1) {
- sendFaceBoxToParticipants(conference, faceBox);
- }
-
- dispatch({
- type: UPDATE_FACE_COORDINATES,
- faceBox,
- id: localParticipant.id
- });
- }
-
- APP.API.notifyFaceLandmarkDetected(faceBox, faceExpression);
- };
-
- const { faceLandmarks } = getState()['features/base/config'];
- const detectionTypes = [
- faceLandmarks?.enableFaceCentering && DETECTION_TYPES.FACE_BOX,
- faceLandmarks?.enableFaceExpressionsDetection && DETECTION_TYPES.FACE_EXPRESSIONS
- ].filter(Boolean);
-
- worker.postMessage({
- type: INIT_WORKER,
- baseUrl,
- detectionTypes,
- maxFacesDetected: faceLandmarks?.maxFacesDetected
- });
-
- dispatch(startFaceLandmarksDetection());
- };
- }
-
- /**
- * Starts the recognition and detection of face expressions.
- *
- * @param {Track | undefined} track - Track for which to start detecting faces.
- * @returns {Function}
- */
- export function startFaceLandmarksDetection(track) {
- return async function(dispatch: Function, getState: Function) {
- if (!worker) {
- return;
- }
-
- const state = getState();
- const { recognitionActive } = state['features/face-landmarks'];
-
- if (recognitionActive) {
- logger.log('Face recognition already active.');
-
- return;
- }
-
- const localVideoTrack = track || getLocalVideoTrack(state['features/base/tracks']);
-
- if (localVideoTrack === undefined) {
- logger.warn('Face landmarks detection is disabled due to missing local track.');
-
- return;
- }
-
- const stream = localVideoTrack.jitsiTrack.getOriginalStream();
-
- dispatch({ type: START_FACE_LANDMARKS_DETECTION });
- logger.log('Start face recognition');
-
- const firstVideoTrack = stream.getVideoTracks()[0];
- const { faceLandmarks } = state['features/base/config'];
-
- imageCapture = new ImageCapture(firstVideoTrack);
-
- detectionInterval = setInterval(() => {
- sendDataToWorker(
- worker,
- imageCapture,
- faceLandmarks?.faceCenteringThreshold
- );
- }, getDetectionInterval(state));
-
- if (faceLandmarks?.enableFaceExpressionsDetection) {
- webhookSendInterval = setInterval(async () => {
- const result = await sendFaceExpressionsWebhook(getState());
-
- if (result) {
- dispatch(clearFaceExpressionBuffer());
- }
- }, WEBHOOK_SEND_TIME_INTERVAL);
- }
- };
- }
-
- /**
- * Stops the recognition and detection of face expressions.
- *
- * @returns {void}
- */
- export function stopFaceLandmarksDetection() {
- return function(dispatch: Function) {
- if (lastFaceExpression && lastFaceExpressionTimestamp) {
- dispatch(
- addFaceExpression(
- lastFaceExpression,
- duplicateConsecutiveExpressions + 1,
- lastFaceExpressionTimestamp
- )
- );
- }
-
- clearInterval(webhookSendInterval);
- clearInterval(detectionInterval);
-
- duplicateConsecutiveExpressions = 0;
- webhookSendInterval = null;
- detectionInterval = null;
- imageCapture = null;
-
- dispatch({ type: STOP_FACE_LANDMARKS_DETECTION });
- logger.log('Stop face recognition');
- };
- }
-
- /**
- * Adds a new face expression and its duration.
- *
- * @param {string} faceExpression - Face expression to be added.
- * @param {number} duration - Duration in seconds of the face expression.
- * @param {number} timestamp - Duration in seconds of the face expression.
- * @returns {Object}
- */
- function addFaceExpression(faceExpression: string, duration: number, timestamp: number) {
- return function(dispatch: Function, getState: Function) {
- const finalDuration = duration * getDetectionInterval(getState()) / 1000;
-
- dispatch({
- type: ADD_FACE_EXPRESSION,
- faceExpression,
- duration: finalDuration,
- timestamp
- });
- };
- }
-
- /**
- * Adds a face expression with its timestamp to the face expression buffer.
- *
- * @param {Object} faceExpression - Object containing face expression string and its timestamp.
- * @returns {Object}
- */
- export function addToFaceExpressionsBuffer(faceExpression: Object) {
- return {
- type: ADD_TO_FACE_EXPRESSIONS_BUFFER,
- faceExpression
- };
- }
-
- /**
- * Clears the face expressions array in the state.
- *
- * @returns {Object}
- */
- function clearFaceExpressionBuffer() {
- return {
- type: CLEAR_FACE_EXPRESSIONS_BUFFER
- };
- }
|