UNPKG

@microblink/blinkid-in-browser-sdk

Version:

A simple ID scanning library for WebAssembly-enabled browsers.

615 lines (509 loc) 19.6 kB
/** * Copyright (c) Microblink Ltd. All rights reserved. */ import * as BlinkIDSDK from "../../../es/blinkid-sdk"; import { AnonymizationMode, AvailableRecognizers, AvailableRecognizerOptions, CameraExperience, Code, EventFatalError, EventReady, VideoRecognitionConfiguration, ImageRecognitionConfiguration, RecognizerInstance, RecognitionEvent, RecognitionStatus, RecognitionResults, SdkSettings } from './data-structures'; export interface CheckConclusion { status: boolean; message?: string; } export class SdkService { private sdk: BlinkIDSDK.WasmSDK; private eventEmitter$: HTMLAnchorElement; private cancelInitiatedFromOutside: boolean = false; private recognizerName: string; private videoRecognizer: BlinkIDSDK.VideoRecognizer; public showOverlay: boolean = false; constructor() { this.eventEmitter$ = document.createElement('a'); } public initialize(licenseKey: string, sdkSettings: SdkSettings): Promise<EventReady|EventFatalError> { const loadSettings = new BlinkIDSDK.WasmSDKLoadSettings(licenseKey); loadSettings.allowHelloMessage = sdkSettings.allowHelloMessage; loadSettings.engineLocation = sdkSettings.engineLocation; return new Promise((resolve) => { BlinkIDSDK.loadWasmModule(loadSettings) .then((sdk: BlinkIDSDK.WasmSDK) => { this.sdk = sdk; this.showOverlay = sdk.showOverlay; resolve(new EventReady(this.sdk)); }) .catch(error => { resolve(new EventFatalError(Code.SdkLoadFailed, 'Failed to load SDK!', error)); }); }); } public checkRecognizers(recognizers: Array<string>): CheckConclusion { if (!recognizers || !recognizers.length) { return { status: false, message: 'There are no provided recognizers!' } } for (const recognizer of recognizers) { if (!this.isRecognizerAvailable(recognizer)) { return { status: false, message: `Recognizer "${ recognizer }" doesn't exist!` } } if (recognizer === 'BlinkIdCombinedRecognizer' && recognizers.length > 1) { return { status: false, message: 'Recognizer "BlinkIdCombinedRecognizer" cannot be used in combination with other recognizers!' }; } } return { status: true } } public checkRecognizerOptions(recognizers: Array<string>, recognizerOptions: Array<string>): CheckConclusion { if (!recognizerOptions || !recognizerOptions.length) { return { status: true } } for (const recognizerOption of recognizerOptions) { let optionExistInProvidedRecognizers = false; for (const recognizer of recognizers) { const availableOptions = AvailableRecognizerOptions[recognizer]; if (availableOptions.indexOf(recognizerOption) > -1) { optionExistInProvidedRecognizers = true; break; } } if (!optionExistInProvidedRecognizers) { return { status: false, message: `Recognizer option "${ recognizerOption }" is not supported by available recognizers!` } } } return { status: true } } public getDesiredCameraExperience(recognizers: Array<string>, _recognizerOptions: Array<string> = []): CameraExperience { if (recognizers.indexOf('BlinkIdCombinedRecognizer') > -1) { return CameraExperience.CardCombined; } if (recognizers.indexOf('BlinkIdRecognizer') > -1) { return CameraExperience.CardSingleSide; } return CameraExperience.Barcode; } public async scanFromCamera( configuration: VideoRecognitionConfiguration, eventCallback: (ev: RecognitionEvent) => void ): Promise<void> { eventCallback({ status: RecognitionStatus.Preparing }); this.cancelInitiatedFromOutside = false; const recognizers = await this.createRecognizers( configuration.recognizers, configuration.recognizerOptions, configuration.anonymization, configuration.successFrame ); const recognizerRunner = await this.createRecognizerRunner( recognizers, eventCallback ); try { this.videoRecognizer = await BlinkIDSDK.VideoRecognizer.createVideoRecognizerFromCameraStream( configuration.cameraFeed, recognizerRunner, configuration.cameraId ); await this.videoRecognizer.setVideoRecognitionMode(BlinkIDSDK.VideoRecognitionMode.Recognition); this.eventEmitter$.addEventListener('terminate', async () => { if (this.videoRecognizer && typeof this.videoRecognizer.cancelRecognition === 'function') { this.videoRecognizer.cancelRecognition(); } if (recognizerRunner) { try { await recognizerRunner.delete(); } catch (error) { // Psst, this error should not happen. } } for (const recognizer of recognizers) { if (!recognizer) { continue; } if ( recognizer.recognizer && recognizer.recognizer.objectHandle > -1 && typeof recognizer.recognizer.delete === 'function' ) { recognizer.recognizer.delete() } if ( recognizer.successFrame && recognizer.successFrame.objectHandle > -1 && typeof recognizer.successFrame.delete === 'function' ) { recognizer.successFrame.delete(); } } window.setTimeout(() => { if (this.videoRecognizer) { this.videoRecognizer.releaseVideoFeed(); } }, 1); }); this.videoRecognizer.startRecognition( async (recognitionState: BlinkIDSDK.RecognizerResultState) => { this.videoRecognizer.pauseRecognition(); eventCallback({ status: RecognitionStatus.Processing }); if (recognitionState !== BlinkIDSDK.RecognizerResultState.Empty) { for (const recognizer of recognizers) { const results = await recognizer.recognizer.getResult(); this.recognizerName = recognizer.recognizer.recognizerName; if (!results || results.state === BlinkIDSDK.RecognizerResultState.Empty) { eventCallback({ status: RecognitionStatus.EmptyResultState, data: { initiatedByUser: this.cancelInitiatedFromOutside, recognizerName: this.recognizerName } }); } else { const recognitionResults: RecognitionResults = { recognizer: results, recognizerName: this.recognizerName } if (recognizer.successFrame) { const successFrameResults = await recognizer.successFrame.getResult(); if (successFrameResults && successFrameResults.state !== BlinkIDSDK.RecognizerResultState.Empty) { recognitionResults.successFrame = successFrameResults; } } eventCallback({ status: RecognitionStatus.ScanSuccessful, data: { result: recognitionResults, initiatedByUser: this.cancelInitiatedFromOutside, imageCapture: this.recognizerName === 'BlinkIdImageCaptureRecognizer' } }); break; } } } else { eventCallback({ status: RecognitionStatus.EmptyResultState, data: { initiatedByUser: this.cancelInitiatedFromOutside, recognizerName: '' } }); } if (this.recognizerName !== 'BlinkIdImageCaptureRecognizer') { window.setTimeout(() => void this.cancelRecognition(), 400); } }); } catch (error) { if (error && error.name === 'VideoRecognizerError') { const reason = (error as BlinkIDSDK.VideoRecognizerError).reason; switch (reason) { case BlinkIDSDK.NotSupportedReason.MediaDevicesNotSupported: eventCallback({ status: RecognitionStatus.NoSupportForMediaDevices }); break; case BlinkIDSDK.NotSupportedReason.CameraNotFound: eventCallback({ status: RecognitionStatus.CameraNotFound }); break; case BlinkIDSDK.NotSupportedReason.CameraNotAllowed: eventCallback({ status: RecognitionStatus.CameraNotAllowed }); break; case BlinkIDSDK.NotSupportedReason.CameraInUse: eventCallback({ status: RecognitionStatus.CameraInUse }); break; default: eventCallback({ status: RecognitionStatus.UnableToAccessCamera }); } console.warn('VideoRecognizerError', error.name, '[' + reason + ']:', error.message); void this.cancelRecognition(); } else { eventCallback({ status: RecognitionStatus.UnknownError }); } } } public async flipCamera(): Promise<void> { await this.videoRecognizer.flipCamera(); } public isCameraFlipped(): boolean { if (!this.videoRecognizer) { return false; } return this.videoRecognizer.cameraFlipped; } public isScanFromImageAvailable(recognizers: Array<string>, _recognizerOptions: Array<string> = []): boolean { return recognizers.indexOf('BlinkIdCombinedRecognizer') === -1; } public async scanFromImage( configuration: ImageRecognitionConfiguration, eventCallback: (ev: RecognitionEvent) => void ): Promise<void> { eventCallback({ status: RecognitionStatus.Preparing }); const recognizers = await this.createRecognizers( configuration.recognizers, configuration.recognizerOptions, configuration.anonymization ); const recognizerRunner = await this.createRecognizerRunner( recognizers, eventCallback ); // Get image file const imageRegex = RegExp(/^image\//); const file: File|null = (() => { for (let i = 0; i < configuration.fileList.length; ++i) { if (imageRegex.exec(configuration.fileList[i].type)) { return configuration.fileList[i]; } } return null; })(); if (!file) { eventCallback({ status: RecognitionStatus.NoImageFileFound }); return; } const imageElement = new Image(); imageElement.src = URL.createObjectURL(file); await imageElement.decode(); const imageFrame = BlinkIDSDK.captureFrame(imageElement); this.eventEmitter$.addEventListener('terminate', async () => { if (recognizerRunner) { try { await recognizerRunner.delete(); } catch (error) { // Psst, this error should not happen. } } for (const recognizer of recognizers) { if (!recognizer) { continue; } if ( recognizer.recognizer && recognizer.recognizer.objectHandle > -1 && typeof recognizer.recognizer.delete === 'function' ) { await recognizer.recognizer.delete(); } } this.eventEmitter$.dispatchEvent(new Event('terminate:done')); }); // Get results eventCallback({ status: RecognitionStatus.Processing }); const processResult = await recognizerRunner.processImage(imageFrame); if (processResult !== BlinkIDSDK.RecognizerResultState.Empty) { for (const recognizer of recognizers) { const results = await recognizer.recognizer.getResult(); if (!results || results.state === BlinkIDSDK.RecognizerResultState.Empty) { eventCallback({ status: RecognitionStatus.EmptyResultState, data: { initiatedByUser: this.cancelInitiatedFromOutside, recognizerName: recognizer.name } }); } else { const recognitionResults: RecognitionResults = { recognizer: results, imageCapture: recognizer.name === 'BlinkIdImageCaptureRecognizer', recognizerName: recognizer.name }; eventCallback({ status: RecognitionStatus.ScanSuccessful, data: recognitionResults }); break; } } } else { // If necessary, scan the image once again with different settings if ( configuration.thoroughScan && ( configuration.recognizers.indexOf('BlinkIdRecognizer') > -1 || configuration.recognizers.indexOf('BlinkIdCombinedRecognizer') > -1 ) ) { configuration.thoroughScan = false; if ( !Array.isArray(configuration.recognizerOptions) || configuration.recognizerOptions.indexOf('scanCroppedDocumentImage') === -1 ) { if (!Array.isArray(configuration.recognizerOptions)) { configuration.recognizerOptions = []; } configuration.recognizerOptions.push('scanCroppedDocumentImage'); } else { const position = configuration.recognizerOptions.indexOf('scanCroppedDocumentImage'); configuration.recognizerOptions.splice(position, 1); } const eventHandler = (recognitionEvent: RecognitionEvent) => eventCallback(recognitionEvent); const handleTerminateDone = () => { this.eventEmitter$.removeEventListener('terminate:done', handleTerminateDone); this.scanFromImage(configuration, eventHandler); } this.cancelRecognition(); this.eventEmitter$.addEventListener('terminate:done', handleTerminateDone); return; } eventCallback({ status: RecognitionStatus.EmptyResultState, data: { initiatedByUser: this.cancelInitiatedFromOutside, recognizerName: '' } }); } window.setTimeout(() => void this.cancelRecognition(), 500); } public async stopRecognition() { void await this.cancelRecognition(true); } public async resumeRecognition(): Promise<void> { this.videoRecognizer.resumeRecognition(true); } ////////////////////////////////////////////////////////////////////////////// // // PRIVATE METHODS private isRecognizerAvailable(recognizer: string): boolean { return !!AvailableRecognizers[recognizer]; } private async createRecognizers( recognizers: Array<string>, recognizerOptions?: Array<string>, anonymization?: AnonymizationMode, successFrame: boolean = false ): Promise<Array<RecognizerInstance>> { const pureRecognizers = []; for (const recognizer of recognizers) { const instance = await BlinkIDSDK[AvailableRecognizers[recognizer]](this.sdk); pureRecognizers.push(instance); } if (recognizerOptions && recognizerOptions.length) { for (const recognizer of pureRecognizers) { let settingsUpdated = false; const settings = await recognizer.currentSettings(); for (const setting of recognizerOptions) { if (setting in settings) { settings[setting] = true; settingsUpdated = true; } } if (settingsUpdated) { await recognizer.updateSettings(settings); } } } if (typeof anonymization !== 'undefined') { for (const recognizer of pureRecognizers) { const settings = await recognizer.currentSettings(); settings.anonymizationMode = anonymization; await recognizer.updateSettings(settings); } } const recognizerInstances = []; for (let i = 0; i < pureRecognizers.length; ++i) { const recognizer = pureRecognizers[i]; const instance: RecognizerInstance = { name: recognizers[i], recognizer } if (successFrame) { const successFrameGrabber = await BlinkIDSDK.createSuccessFrameGrabberRecognizer(this.sdk, recognizer); instance.successFrame = successFrameGrabber; } recognizerInstances.push(instance) } return recognizerInstances; } private async createRecognizerRunner( recognizers: Array<RecognizerInstance>, eventCallback: (ev: RecognitionEvent) => void ): Promise<BlinkIDSDK.RecognizerRunner> { const metadataCallbacks: BlinkIDSDK.MetadataCallbacks = { onDetectionFailed: () => eventCallback({ status: RecognitionStatus.DetectionFailed }), onQuadDetection: (quad: BlinkIDSDK.Displayable) => { eventCallback({ status: RecognitionStatus.DetectionStatusChange, data: quad }); const detectionStatus = quad.detectionStatus; switch (detectionStatus) { case BlinkIDSDK.DetectionStatus.Fail: eventCallback({ status: RecognitionStatus.DetectionStatusSuccess }); break; case BlinkIDSDK.DetectionStatus.Success: eventCallback({ status: RecognitionStatus.DetectionStatusSuccess }); break; case BlinkIDSDK.DetectionStatus.CameraTooHigh: eventCallback({ status: RecognitionStatus.DetectionStatusCameraTooHigh }); break; case BlinkIDSDK.DetectionStatus.FallbackSuccess: eventCallback({ status: RecognitionStatus.DetectionStatusFallbackSuccess }); break; case BlinkIDSDK.DetectionStatus.Partial: eventCallback({ status: RecognitionStatus.DetectionStatusPartial }); break; case BlinkIDSDK.DetectionStatus.CameraAtAngle: eventCallback({ status: RecognitionStatus.DetectionStatusCameraAtAngle }); break; case BlinkIDSDK.DetectionStatus.CameraTooNear: eventCallback({ status: RecognitionStatus.DetectionStatusCameraTooNear }); break; case BlinkIDSDK.DetectionStatus.DocumentTooCloseToEdge: eventCallback({ status: RecognitionStatus.DetectionStatusDocumentTooCloseToEdge }); break; default: // Send nothing } } } const blinkIdGeneric = recognizers.find(el => el.recognizer.recognizerName === 'BlinkIdRecognizer'); const blinkIdCombined = recognizers.find(el => el.recognizer.recognizerName === 'BlinkIdCombinedRecognizer'); if (blinkIdGeneric || blinkIdCombined) { for (const el of recognizers) { if ( el.recognizer.recognizerName === 'BlinkIdRecognizer' || el.recognizer.recognizerName === 'BlinkIdCombinedRecognizer' ) { const settings = await el.recognizer.currentSettings() as BlinkIDSDK.BlinkIdRecognizerSettings; settings.classifierCallback = (supported: boolean) => { eventCallback({ status: RecognitionStatus.DocumentClassified, data: supported }); } await el.recognizer.updateSettings(settings); } } } if (blinkIdCombined) { metadataCallbacks.onFirstSideResult = () => eventCallback({ status: RecognitionStatus.OnFirstSideResult }); } const recognizerRunner = await BlinkIDSDK.createRecognizerRunner( this.sdk, recognizers.map((el: RecognizerInstance) => el.successFrame || el.recognizer), false, metadataCallbacks ); return recognizerRunner; } private async cancelRecognition(initiatedFromOutside: boolean = false): Promise<void> { this.cancelInitiatedFromOutside = initiatedFromOutside; this.eventEmitter$.dispatchEvent(new Event('terminate')); } }