@microblink/blinkid-in-browser-sdk
Version:
A simple ID scanning library for WebAssembly-enabled browsers.
747 lines (689 loc) • 25.7 kB
text/typescript
/**
* Copyright (c) Microblink Ltd. All rights reserved.
*/
import * as Messages from "./Messages";
import { CapturedFrame } from "../FrameCapture";
import { LicenseErrorResponse } from "../License";
import
{
RecognizerResultState,
RecognizerRunner,
WasmModuleProxy,
WasmSDK,
Recognizer,
RecognizerSettings,
RecognizerResult
} from "../DataStructures.js";
import { ClearTimeoutCallback } from "../ClearTimeoutCallback";
import { MetadataCallbacks, DisplayablePoints, DisplayableQuad } from "../MetadataCallbacks";
import { WasmSDKLoadSettings, OptionalLoadProgressCallback } from "../WasmLoadSettings";
// ============================================ /
// Web Worker Proxy Implementation /
// ============================================ /
interface EventHandler
{
( msg: Messages.ResponseMessage ): void;
}
function defaultEventHandler(
resolve: () => void,
reject: ( reason: LicenseErrorResponse | string | null ) => void
): EventHandler
{
return ( msg: Messages.ResponseMessage ) =>
{
const resultMsg = msg as Messages.StatusMessage;
if ( resultMsg.success )
{
resolve();
}
else
{
reject( resultMsg.error );
}
};
}
function defaultResultEventHandler(
successResolver: EventHandler,
reject: ( reason: LicenseErrorResponse | string | null ) => void
): EventHandler
{
return ( msg: Messages.ResponseMessage ) =>
{
const resultMsg = msg as Messages.StatusMessage;
if ( resultMsg.success )
{
successResolver( msg );
}
else
{
reject( resultMsg.error );
}
};
}
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
function wrapParameters( params: Array< any > ): Array< Messages.WrappedParameter >
{
// convert params
const wrappedPrameters = [];
for ( let param of params )
{
let paramType = Messages.ParameterType.Any;
if ( param instanceof RemoteRecognizer )
{
paramType = Messages.ParameterType.Recognizer;
param = param.getRemoteObjectHandle();
}
wrappedPrameters.push
(
{
parameter: param,
type: paramType
}
);
}
return wrappedPrameters;
}
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
export class RemoteRecognizer implements Recognizer
{
/* eslint-disable lines-between-class-members, @typescript-eslint/ban-types */
private readonly wasmSDKWorker : WasmSDKWorker;
private objectHandle : number;
readonly recognizerName: string;
private callbacks : Map< string, Function >;
/* eslint-enable lines-between-class-members */
constructor( wasmWorker: WasmSDKWorker, recognizerName: string, remoteObjHandle: number )
{
this.wasmSDKWorker = wasmWorker;
this.objectHandle = remoteObjHandle;
this.recognizerName = recognizerName;
this.callbacks = new Map< string, Function >();
}
/* eslint-enable @typescript-eslint/ban-types */
getRemoteObjectHandle(): number
{
return this.objectHandle;
}
currentSettings(): Promise< RecognizerSettings >
{
return new Promise< RecognizerSettings >
(
( resolve, reject ) =>
{
if ( this.objectHandle < 0 )
{
reject( "Invalid object handle: " + this.objectHandle.toString() );
return;
}
const msg = new Messages.InvokeObjectMethod
(
this.objectHandle,
"currentSettings",
[]
);
const handler = defaultResultEventHandler
(
( msg: Messages.ResponseMessage ) =>
{
resolve( ( msg as Messages.InvokeResultMessage ).result );
},
reject
);
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
private clearAllCallbacks()
{
this.callbacks.clear();
this.wasmSDKWorker.unregisterRecognizerCallbacks( this.objectHandle );
}
/* eslint-disable @typescript-eslint/no-explicit-any,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access,
@typescript-eslint/no-unsafe-return
*/
// convert each function member into wrapped parameter, containing address where callback needs to be delivered
private removeFunctions( settings: any ): any
{
// clear any existing callbacks
this.clearAllCallbacks();
const keys = Object.keys( settings );
let needsRegistering = false;
for ( const key of keys )
{
const data = settings[ key ];
if ( typeof data === "function" )
{
this.callbacks.set( key, data );
const wrappedFunction: Messages.WrappedParameter = {
parameter: {
recognizerHandle: this.objectHandle,
callbackName: key
} as Messages.CallbackAddress, // in order to know to which instance callback needs to be delivered
type: Messages.ParameterType.Callback
};
settings[ key ] = wrappedFunction;
needsRegistering = true;
}
}
if ( needsRegistering )
{
this.wasmSDKWorker.registerRecognizerCallbacks( this.objectHandle, this );
}
return settings;
}
/* eslint-enable @typescript-eslint/no-explicit-any,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access,
@typescript-eslint/no-unsafe-return
*/
updateSettings( newSettings: RecognizerSettings ): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
if ( this.objectHandle < 0 )
{
reject( "Invalid object handle: " + this.objectHandle.toString() );
return;
}
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const msg = new Messages.InvokeObjectMethod
(
this.objectHandle,
"updateSettings",
[
{
parameter: this.removeFunctions( newSettings ),
type: Messages.ParameterType.RecognizerSettings
}
]
);
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
invokeCallback( callbackName: string, args: any[] ): void
{
const callback = this.callbacks.get( callbackName );
if ( callback !== undefined )
{
callback( ...args );
}
else
{
console.warn( "Cannot find callback", callbackName );
}
}
/* eslint-enable @typescript-eslint/no-explicit-any */
getResult(): Promise< RecognizerResult >
{
return new Promise< RecognizerResult >
(
( resolve, reject ) =>
{
if ( this.objectHandle < 0 )
{
reject( "Invalid object handle: " + this.objectHandle.toString() );
return;
}
const msg = new Messages.InvokeObjectMethod( this.objectHandle, "getResult", [] );
const handler = defaultResultEventHandler
(
( msg: Messages.ResponseMessage ) =>
{
resolve( ( msg as Messages.InvokeResultMessage ).result );
},
reject
);
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
delete(): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
if ( this.objectHandle < 0 )
{
reject( "Invalid object handle: " + this.objectHandle.toString() );
return;
}
this.clearAllCallbacks();
const msg = new Messages.InvokeObjectMethod( this.objectHandle, "delete", [] );
const handler = defaultEventHandler
(
() =>
{
this.objectHandle = -1;
resolve();
},
reject
);
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
}
function createRegisteredCallbacks
(
metadataCallbacks: MetadataCallbacks
): Messages.RegisteredMetadataCallbacks
{
const msg = new Messages.RegisteredMetadataCallbacks();
// https://stackoverflow.com/a/20093686/213057
msg.onDebugText = !!metadataCallbacks.onDebugText;
msg.onDetectionFailed = !!metadataCallbacks.onDetectionFailed;
msg.onPointsDetection = !!metadataCallbacks.onPointsDetection;
msg.onQuadDetection = !!metadataCallbacks.onQuadDetection;
msg.onFirstSideResult = !!metadataCallbacks.onFirstSideResult;
msg.onGlare = !!metadataCallbacks.onGlare;
return msg;
}
class RemoteRecognizerRunner implements RecognizerRunner
{
private readonly wasmSDKWorker: WasmSDKWorker;
private deleted = false;
constructor( wasmWorker: WasmSDKWorker )
{
this.wasmSDKWorker = wasmWorker;
}
processImage( image: CapturedFrame ): Promise< RecognizerResultState >
{
return new Promise< RecognizerResultState >
(
( resolve, reject ) =>
{
if ( this.deleted )
{
reject( "Recognizer runner is deleted. It cannot be used anymore!" );
return;
}
const msg = new Messages.ProcessImage( image );
const handler = defaultResultEventHandler
(
( response: Messages.ResponseMessage ) =>
{
const state: RecognizerResultState = (
response as Messages.ImageProcessResultMessage
).recognitionState;
resolve( state );
},
reject
);
this.wasmSDKWorker.postTransferrableMessage( msg, handler );
}
);
}
reconfigureRecognizers
(
recognizers: Array< Recognizer >,
allowMultipleResults: boolean
): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
if ( this.deleted )
{
reject( "Recognizer runner is deleted. It cannot be used anymore!" );
return;
}
const recognizerHandles = getRecognizerHandles( recognizers as Array< RemoteRecognizer > );
const msg = new Messages.ReconfigureRecognizerRunner( recognizerHandles, allowMultipleResults );
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
setMetadataCallbacks( metadataCallbacks: MetadataCallbacks ): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
const msg = new Messages.RegisterMetadataCallbacks( createRegisteredCallbacks( metadataCallbacks ) );
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.postMessageAndRegisterCallbacks( msg, metadataCallbacks, handler );
}
);
}
resetRecognizers( hardReset: boolean ): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
const msg = new Messages.ResetRecognizers( hardReset );
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
setDetectionOnlyMode( detectionOnly: boolean ): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
const msg = new Messages.SetDetectionOnly( detectionOnly );
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
setClearTimeoutCallback( clearTimeoutCallback: ClearTimeoutCallback | null ): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
const msg = new Messages.SetClearTimeoutCallback( clearTimeoutCallback !== null );
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.registerClearTimeoutCallback( clearTimeoutCallback );
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
setCameraPreviewMirrored( mirrored: boolean ): Promise< void >
{
return new Promise< void >
(
( resolve, reject ) =>
{
const msg = new Messages.SetCameraPreviewMirrored( mirrored );
const handler = defaultEventHandler( resolve, reject );
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
delete(): Promise< void >
{
if ( this.deleted )
{
return Promise.reject( "Recognizer runner is already deleted." );
}
return new Promise< void >
(
( resolve, reject ) =>
{
const msg = new Messages.DeleteRecognizerRunner();
const handler = defaultEventHandler
(
() =>
{
this.deleted = true;
resolve();
},
reject
);
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
}
function getRecognizerHandles( remoteRecognizers: Array< RemoteRecognizer > )
{
const recognizerHandles: Array< number > = [];
for ( const remoteRecognizer of remoteRecognizers )
{
recognizerHandles.push( remoteRecognizer.getRemoteObjectHandle() );
}
return recognizerHandles;
}
class WasmModuleWorkerProxy implements WasmModuleProxy
{
private readonly wasmSDKWorker: WasmSDKWorker;
constructor( wasmSDKWorker: WasmSDKWorker )
{
this.wasmSDKWorker = wasmSDKWorker;
}
createRecognizerRunner
(
recognizers : Array< Recognizer >,
allowMultipleResults = false,
metadataCallbacks : MetadataCallbacks = {}
): Promise< RecognizerRunner >
{
return new Promise< RecognizerRunner >
(
( resolve, reject ) =>
{
const recognizerHandles = getRecognizerHandles( recognizers as Array< RemoteRecognizer > );
const msg = new Messages.CreateRecognizerRunner
(
recognizerHandles,
allowMultipleResults,
createRegisteredCallbacks( metadataCallbacks )
);
const handler = defaultEventHandler
(
() =>
{
resolve( new RemoteRecognizerRunner( this.wasmSDKWorker ) );
},
reject
);
this.wasmSDKWorker.postMessageAndRegisterCallbacks( msg, metadataCallbacks, handler );
}
);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
newRecognizer( className: string, ...constructorArgs: any[] ): Promise< Recognizer >
{
return new Promise< Recognizer >
(
( resolve, reject ) =>
{
const msg = new Messages.CreateNewRecognizer( className, wrapParameters( constructorArgs ) );
const handler = defaultResultEventHandler
(
( msg: Messages.ResponseMessage ) =>
{
const remoteRecognizer = new RemoteRecognizer
(
this.wasmSDKWorker,
className,
( msg as Messages.ObjectCreatedMessage ).objectHandle
);
resolve( remoteRecognizer );
},
reject
);
this.wasmSDKWorker.postMessage( msg, handler );
}
);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}
export class WasmSDKWorker implements WasmSDK
{
/* eslint-disable lines-between-class-members */
readonly mbWasmModule : WasmModuleWorkerProxy;
private readonly mbWasmWorker : Worker;
private eventHandlers : { [ key: number ] : EventHandler } = {};
private metadataCallbacks : MetadataCallbacks = {};
private loadCallback : OptionalLoadProgressCallback;
private clearTimeoutCallback : ClearTimeoutCallback | null = null;
private recognizersWithCallbacks: Map< number, RemoteRecognizer >;
public showOverlay : boolean;
/* eslint-enable lines-between-class-members */
private constructor
(
worker: Worker,
loadProgressCallback: OptionalLoadProgressCallback,
rejectHandler: ( message: string ) => void
)
{
this.mbWasmWorker = worker;
this.mbWasmWorker.onmessage = ( event: MessageEvent ) => { this.handleWorkerEvent( event ); };
this.mbWasmWorker.onerror = () =>
{
rejectHandler( "Problem during initialization of worker file!" );
return;
};
this.mbWasmModule = new WasmModuleWorkerProxy( this );
this.loadCallback = loadProgressCallback;
this.recognizersWithCallbacks = new Map< number, RemoteRecognizer >();
this.showOverlay = false;
}
postMessage( message: Messages.RequestMessage, eventHandler: EventHandler ): void
{
this.eventHandlers[ message.messageID ] = eventHandler;
this.mbWasmWorker.postMessage( message );
}
postTransferrableMessage
(
message : Messages.RequestMessage & Messages.TransferrableMessage,
eventHandler : EventHandler
): void
{
this.eventHandlers[ message.messageID ] = eventHandler;
this.mbWasmWorker.postMessage( message, message.getTransferrables() );
}
postMessageAndRegisterCallbacks
(
message : Messages.RequestMessage,
metadataCallbacks : MetadataCallbacks,
eventHandler : EventHandler
): void
{
this.eventHandlers[ message.messageID ] = eventHandler;
this.metadataCallbacks = metadataCallbacks;
this.mbWasmWorker.postMessage( message );
}
registerClearTimeoutCallback( callback: ClearTimeoutCallback | null ): void
{
this.clearTimeoutCallback = callback;
}
registerRecognizerCallbacks( remoteRecognizerHandle: number, recognizer: RemoteRecognizer ): void
{
this.recognizersWithCallbacks.set( remoteRecognizerHandle, recognizer );
}
unregisterRecognizerCallbacks( remoteRecognizerHandle: number ): void
{
this.recognizersWithCallbacks.delete( remoteRecognizerHandle );
}
private handleWorkerEvent( event: MessageEvent )
{
if ( "isCallbackMessage" in event.data )
{
const msg = event.data as Messages.InvokeCallbackMessage;
switch ( msg.callbackType )
{
case Messages.MetadataCallback.onDebugText:
if ( typeof this.metadataCallbacks.onDebugText === "function" )
{
this.metadataCallbacks.onDebugText( msg.callbackParameters[ 0 ] as string );
}
break;
case Messages.MetadataCallback.onDetectionFailed:
if ( typeof this.metadataCallbacks.onDetectionFailed === "function" )
{
this.metadataCallbacks.onDetectionFailed();
}
break;
case Messages.MetadataCallback.onPointsDetection:
if ( typeof this.metadataCallbacks.onPointsDetection === "function" )
{
this.metadataCallbacks.onPointsDetection( msg.callbackParameters[ 0 ] as DisplayablePoints );
}
break;
case Messages.MetadataCallback.onQuadDetection:
if ( typeof this.metadataCallbacks.onQuadDetection === "function" )
{
this.metadataCallbacks.onQuadDetection( msg.callbackParameters[ 0 ] as DisplayableQuad );
}
break;
case Messages.MetadataCallback.onFirstSideResult:
if ( typeof this.metadataCallbacks.onFirstSideResult === "function" )
{
this.metadataCallbacks.onFirstSideResult();
}
break;
case Messages.MetadataCallback.clearTimeoutCallback:
if ( this.clearTimeoutCallback && typeof this.clearTimeoutCallback.onClearTimeout === "function" )
{
this.clearTimeoutCallback.onClearTimeout();
}
break;
case Messages.MetadataCallback.onGlare:
if ( typeof this.metadataCallbacks.onGlare === "function" )
{
this.metadataCallbacks.onGlare( msg.callbackParameters[ 0 ] as boolean );
}
break;
case Messages.MetadataCallback.recognizerCallback:
{
// first parameter is address, other parameters are callback parameters
const address = msg.callbackParameters.shift() as Messages.CallbackAddress;
const recognizer = this.recognizersWithCallbacks.get( address.recognizerHandle );
if ( recognizer !== undefined )
{
recognizer.invokeCallback( address.callbackName, msg.callbackParameters );
}
else
{
console.warn
(
"Cannot find recognizer to deliver callback message. Maybe it's destroyed?",
address
);
}
break;
}
default:
throw new Error( `Unknown callback type: ${ Messages.MetadataCallback[ msg.callbackType ] }` );
}
}
else if ( "isLoadProgressMessage" in event.data )
{
const msg = event.data as Messages.LoadProgressMessage;
if ( typeof this.loadCallback === "function" )
{
this.loadCallback( msg.progress );
}
}
else
{
const msg = event.data as Messages.ResponseMessage;
const eventHandler = this.eventHandlers[ msg.messageID ];
delete this.eventHandlers[ msg.messageID ];
eventHandler( msg );
}
}
static async createWasmWorker
(
worker : Worker,
wasmLoadSettings : WasmSDKLoadSettings,
userId : string
): Promise< WasmSDKWorker >
{
return new Promise< WasmSDKWorker >
(
( resolve, reject ) =>
{
const wasmWorker = new WasmSDKWorker( worker, wasmLoadSettings.loadProgressCallback, reject );
const initMessage = new Messages.InitMessage( wasmLoadSettings, userId );
const initEventHandler = defaultResultEventHandler
(
( msg: Messages.ResponseMessage ) =>
{
wasmWorker.showOverlay = ( msg as Messages.InitSuccessMessage ).showOverlay;
resolve( wasmWorker );
},
reject
);
wasmWorker.postMessage( initMessage, initEventHandler );
}
);
}
}