@incodetech/core 2.0.0 → 2.0.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Actor-CI32dTbG.d.ts +2 -0
- package/dist/BaseWasmProvider-C_DLEI40.esm.js +1118 -0
- package/dist/BrowserStorageProvider-CuOW1Er2.esm.js +55 -0
- package/dist/BrowserTimerProvider-DhNc_x02.esm.js +22 -0
- package/dist/ITimerCapability-C67ZRskg.esm.js +7 -0
- package/dist/IpifyProvider-D7jx52AL.esm.js +139 -0
- package/dist/Manager-C8PrhBOx.d.ts +19 -0
- package/dist/MotionSensorProvider-4v7xkqAp.esm.js +254 -0
- package/dist/OpenViduRecordingProvider-CMu6XVdc.esm.js +87 -0
- package/dist/StateMachine-BCQrZJhf.d.ts +2 -0
- package/dist/WasmUtilProvider-j98OJf-S.esm.js +114 -0
- package/dist/addressSearch-BpTbTWCa.esm.js +430 -0
- package/dist/ae-signature-DDDZmWXj.esm.js +12 -0
- package/dist/ae-signature.d.ts +25 -0
- package/dist/ae-signature.esm.js +8 -0
- package/dist/antifraud.d.ts +57 -0
- package/dist/antifraud.esm.js +45 -0
- package/dist/antifraudStateMachine-O0TMf6yc.esm.js +39 -0
- package/dist/api-CESGtpbH.esm.js +53 -0
- package/dist/authentication.d.ts +12 -0
- package/dist/authentication.esm.js +25 -0
- package/dist/authenticationManager-5M-fKzXx.esm.js +67 -0
- package/dist/authenticationManager-C83GNIhl.d.ts +66 -0
- package/dist/authenticationStateMachine-BMZqatiF.esm.js +139 -0
- package/dist/backCameraStream-DMdMeGk2.esm.js +346 -0
- package/dist/browserSimulation-gxD8cSpM.esm.js +20 -0
- package/dist/camera-DBSxa6ML.d.ts +4 -0
- package/dist/camera-PA2Ljri3.esm.js +22 -0
- package/dist/camera.d.ts +15 -0
- package/dist/camera.esm.js +5 -0
- package/dist/chunk-CRF6K_H_.esm.js +49 -0
- package/dist/consent.d.ts +398 -0
- package/dist/consent.esm.js +79 -0
- package/dist/consentStateMachine-CCT-B60O.esm.js +151 -0
- package/dist/cpf-PPz2Njto.esm.js +38 -0
- package/dist/cpf-ocr.d.ts +204 -0
- package/dist/cpf-ocr.esm.js +177 -0
- package/dist/cross-document-data-match.d.ts +34 -0
- package/dist/cross-document-data-match.esm.js +71 -0
- package/dist/curp-validation.d.ts +188 -0
- package/dist/curp-validation.esm.js +110 -0
- package/dist/curpValidationStateMachine-CitWLr2c.esm.js +595 -0
- package/dist/custom-fields.d.ts +115 -0
- package/dist/custom-fields.esm.js +177 -0
- package/dist/custom-watchlist.d.ts +66 -0
- package/dist/custom-watchlist.esm.js +86 -0
- package/dist/dateUtils-UoN5xswP.esm.js +23 -0
- package/dist/deepsightLoader-Cm4JIT_z.esm.js +52 -0
- package/dist/deepsightService-CEVxzehb.d.ts +412 -0
- package/dist/deepsightService-O74l4Y__.esm.js +489 -0
- package/dist/device.d.ts +46 -0
- package/dist/device.esm.js +106 -0
- package/dist/displayErrors-DqJ_IbsG.d.ts +39 -0
- package/dist/document-capture.d.ts +906 -0
- package/dist/document-capture.esm.js +156 -0
- package/dist/document-upload.d.ts +331 -0
- package/dist/document-upload.esm.js +203 -0
- package/dist/documentCaptureStateMachine-BqzTDy9k.esm.js +394 -0
- package/dist/dynamic-forms.d.ts +178 -0
- package/dist/dynamic-forms.esm.js +323 -0
- package/dist/ekyb.d.ts +148 -0
- package/dist/ekyb.esm.js +127 -0
- package/dist/ekybStateMachine-BR2let5f.esm.js +674 -0
- package/dist/ekyc.d.ts +164 -0
- package/dist/ekyc.esm.js +104 -0
- package/dist/ekycStateMachine-oeO0Iekd.esm.js +10626 -0
- package/dist/electronic-signature.d.ts +4 -0
- package/dist/electronic-signature.esm.js +7 -0
- package/dist/electronicSignatureManager-D9OHzTpG.esm.js +428 -0
- package/dist/email.d.ts +4 -0
- package/dist/email.esm.js +9 -0
- package/dist/emailManager-DIfnS5g1.d.ts +352 -0
- package/dist/emailManager-wAV0LE-H.esm.js +238 -0
- package/dist/emailStateMachine-DOf4j58N.esm.js +292 -0
- package/dist/endpoints-CnN3SyDa.esm.js +87 -0
- package/dist/events-D6-e4vok.esm.js +596 -0
- package/dist/events.d.ts +265 -0
- package/dist/events.esm.js +4 -0
- package/dist/extensibility.d.ts +122 -0
- package/dist/extensibility.esm.js +43 -0
- package/dist/face-match.d.ts +228 -0
- package/dist/face-match.esm.js +173 -0
- package/dist/faceCaptureManagerFactory-Dh2PdGlF.esm.js +290 -0
- package/dist/faceCaptureManagerFactory-yqtpxjnN.d.ts +690 -0
- package/dist/faceCaptureSetup-B3faSpYA.esm.js +873 -0
- package/dist/faceMatchStateMachine-DNFrxTFS.esm.js +127 -0
- package/dist/flow-events.d.ts +6 -0
- package/dist/flow-events.esm.js +0 -0
- package/dist/flow.d.ts +358 -0
- package/dist/flow.esm.js +825 -0
- package/dist/flowCompletionService-DhkT4SRY.d.ts +10 -0
- package/dist/flowCompletionService-P54yzGvA.esm.js +13 -0
- package/dist/flowServices-DG3IdWw6.esm.js +188 -0
- package/dist/geolocation.d.ts +127 -0
- package/dist/geolocation.esm.js +89 -0
- package/dist/geolocationStateMachine-asasuHY2.esm.js +105 -0
- package/dist/getBrowser-BSXUTWXw.esm.js +41 -0
- package/dist/getDeviceClass-BSntT9_j.esm.js +14 -0
- package/dist/government-validation.d.ts +67 -0
- package/dist/government-validation.esm.js +81 -0
- package/dist/governmentValidationStateMachine-BDDYrJTo.esm.js +271 -0
- package/dist/home.d.ts +99 -0
- package/dist/home.esm.js +61 -0
- package/dist/http.d.ts +68 -0
- package/dist/http.esm.js +3 -0
- package/dist/id-ocr.d.ts +635 -0
- package/dist/id-ocr.esm.js +86 -0
- package/dist/id-verification.d.ts +190 -0
- package/dist/id-verification.esm.js +43 -0
- package/dist/id.d.ts +24 -0
- package/dist/id.esm.js +164 -0
- package/dist/idCaptureManager-Fyd0eam-.d.ts +958 -0
- package/dist/idCaptureManager-ZPkD7Gjk.esm.js +581 -0
- package/dist/idCaptureStateMachine-BK0bPHoc.esm.js +2963 -0
- package/dist/idOcrStateMachine-YbjjC_Gg.esm.js +388 -0
- package/dist/idVerificationStateMachine-xbw9HP1Z.esm.js +71 -0
- package/dist/identity-reuse.d.ts +530 -0
- package/dist/identity-reuse.esm.js +274 -0
- package/dist/index-BcRG8rtJ.d.ts +97 -0
- package/dist/index-DZoqeAo9.d.ts +1177 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.esm.js +12 -0
- package/dist/invokeOnCaptureCallback-rc6kBHo5.esm.js +30 -0
- package/dist/lib-BB0B_qQX.esm.js +12499 -0
- package/dist/mandatory-consent.d.ts +412 -0
- package/dist/mandatory-consent.esm.js +78 -0
- package/dist/mandatoryConsentStateMachine-Cnco1jvn.esm.js +126 -0
- package/dist/openviduLazy-Cm0XFh_v.esm.js +3 -0
- package/dist/openviduLazy-Cok70ZSg.esm.js +12 -0
- package/dist/permissionServices-D_i6nzEw.esm.js +50 -0
- package/dist/phone.d.ts +4 -0
- package/dist/phone.esm.js +9 -0
- package/dist/phoneManager-B6M30hKE.d.ts +397 -0
- package/dist/phoneManager-DAJbGhlY.esm.js +256 -0
- package/dist/phoneStateMachine-CuPARRaT.esm.js +351 -0
- package/dist/platform-CfrjKhmi.esm.js +83 -0
- package/dist/qe-signature-DFo_Cc-I.esm.js +12 -0
- package/dist/qe-signature.d.ts +25 -0
- package/dist/qe-signature.esm.js +8 -0
- package/dist/recordingService-Ig2UgbLv.esm.js +1003 -0
- package/dist/redirect-to-mobile.d.ts +107 -0
- package/dist/redirect-to-mobile.esm.js +102 -0
- package/dist/redirectToMobileStateMachine-BOEqe46A.esm.js +249 -0
- package/dist/runChildModule-CqqwqAkW.esm.js +219 -0
- package/dist/selfie.d.ts +26 -0
- package/dist/selfie.esm.js +146 -0
- package/dist/selfieManager-D0lSgd-J.d.ts +68 -0
- package/dist/selfieManager-Duisl7qN.esm.js +60 -0
- package/dist/selfieStateMachine-D76whWEf.esm.js +68 -0
- package/dist/session-CGtQJJzB.esm.js +3206 -0
- package/dist/session.d.ts +217 -0
- package/dist/session.esm.js +9 -0
- package/dist/setup-C5AITV8m.d.ts +254 -0
- package/dist/setup-DsM8IG7k.esm.js +887 -0
- package/dist/signature.d.ts +94 -0
- package/dist/signature.esm.js +66 -0
- package/dist/signatureStateMachine-B5-QVUve.esm.js +132 -0
- package/dist/stats-CIfiPzb1.esm.js +16 -0
- package/dist/stats.d.ts +16 -0
- package/dist/stats.esm.js +4 -0
- package/dist/trust-graph.d.ts +54 -0
- package/dist/trust-graph.esm.js +56 -0
- package/dist/types-B06Ypu2F.d.ts +49 -0
- package/dist/types-BP1m8VRw.d.ts +340 -0
- package/dist/types-CAD4va6a.d.ts +5 -0
- package/dist/types-CFV9G_7j.d.ts +24 -0
- package/dist/warmup-CEcppFiS.d.ts +63 -0
- package/dist/wasm.d.ts +15 -0
- package/dist/wasm.esm.js +12 -0
- package/dist/watchlist-for-business.d.ts +79 -0
- package/dist/watchlist-for-business.esm.js +148 -0
- package/dist/watchlist.d.ts +62 -0
- package/dist/watchlist.esm.js +86 -0
- package/dist/watchlistServices-DMbUhkBX.esm.js +12 -0
- package/dist/workflow.d.ts +907 -0
- package/dist/workflow.esm.js +702 -0
- package/dist/xstate.esm-B70JrNqo.esm.js +3404 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import { l as WasmPipelineType, r as mlWasmJSApi_default, t as BaseWasmProvider } from "./BaseWasmProvider-C_DLEI40.esm.js";
|
|
2
|
+
import { t as api } from "./api-CESGtpbH.esm.js";
|
|
3
|
+
import { t as endpoints } from "./endpoints-CnN3SyDa.esm.js";
|
|
4
|
+
import { i as stopCameraStream, r as requestCameraAccess } from "./camera-PA2Ljri3.esm.js";
|
|
5
|
+
import { n as IncodeCanvas } from "./deepsightService-O74l4Y__.esm.js";
|
|
6
|
+
import { a as isIPhone14OrHigher, n as isDesktop, r as isIOS, t as isAndroid } from "./platform-CfrjKhmi.esm.js";
|
|
7
|
+
import { n as getDesktopCameraStream, t as getBackCameraStream } from "./backCameraStream-DMdMeGk2.esm.js";
|
|
8
|
+
import { t as addDeviceStats } from "./stats-CIfiPzb1.esm.js";
|
|
9
|
+
import { t as getDeviceClass } from "./getDeviceClass-BSntT9_j.esm.js";
|
|
10
|
+
|
|
11
|
+
//#region ../infra/src/providers/wasm/FaceDetectionProvider.ts
|
|
12
|
+
var FaceDetectionProvider = class extends BaseWasmProvider {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(WasmPipelineType.SelfieWithQualityMetrics);
|
|
15
|
+
this.defaultThresholds = {
|
|
16
|
+
frameMinX: 0,
|
|
17
|
+
frameMinY: 0,
|
|
18
|
+
frameMaxX: 1,
|
|
19
|
+
frameMaxY: 1,
|
|
20
|
+
brightnessThreshold: 50,
|
|
21
|
+
blurrinessThreshold: 50,
|
|
22
|
+
tiltRotationAngleThreshold: 15,
|
|
23
|
+
minMagicCropSize: 200,
|
|
24
|
+
headwearThreshold: .86,
|
|
25
|
+
lensesThreshold: .95,
|
|
26
|
+
closedEyesThreshold: .9,
|
|
27
|
+
maskThreshold: .85,
|
|
28
|
+
minFaceQualityScore: .63,
|
|
29
|
+
faceOcclusionThreshold: .3,
|
|
30
|
+
getReadyDelay: 2e3,
|
|
31
|
+
framesAggregationInterval: 2e3,
|
|
32
|
+
minFramesWithFace: 3
|
|
33
|
+
};
|
|
34
|
+
this.currentFrame = null;
|
|
35
|
+
}
|
|
36
|
+
async processFrame(image) {
|
|
37
|
+
this.currentFrame = image;
|
|
38
|
+
await this.processFrameWasm(image);
|
|
39
|
+
}
|
|
40
|
+
async initialize(config) {
|
|
41
|
+
if (config.useOnDeviceWorkflow === true) {
|
|
42
|
+
this.pipelineType = WasmPipelineType.OnDeviceSelfieWorkflow;
|
|
43
|
+
await this.initializeBase(config, "onDeviceSelfie");
|
|
44
|
+
} else await this.initializeBase(config, "selfie");
|
|
45
|
+
this.applyDefaults(config.autocaptureInterval ?? 0);
|
|
46
|
+
}
|
|
47
|
+
processPhoto(canvas) {
|
|
48
|
+
this.ensureInitialized();
|
|
49
|
+
mlWasmJSApi_default.processPhoto(canvas);
|
|
50
|
+
}
|
|
51
|
+
async postFaceResults(config) {
|
|
52
|
+
this.ensureInitialized();
|
|
53
|
+
return mlWasmJSApi_default.postFaceResults(config);
|
|
54
|
+
}
|
|
55
|
+
setCallbacks(callbacks) {
|
|
56
|
+
this.ensureInitialized();
|
|
57
|
+
const onCaptureWrapper = (wasmCanvas, face) => {
|
|
58
|
+
let frameCanvas = null;
|
|
59
|
+
if (this.currentFrame) frameCanvas = IncodeCanvas.fromImageData(this.currentFrame);
|
|
60
|
+
else if (wasmCanvas) frameCanvas = new IncodeCanvas(wasmCanvas);
|
|
61
|
+
if (!frameCanvas) return;
|
|
62
|
+
const faceCoordinates = face ? this.formatFaceCoordinates(face) : this.createDefaultFaceCoordinates(frameCanvas);
|
|
63
|
+
try {
|
|
64
|
+
frameCanvas.updateBase64Image();
|
|
65
|
+
frameCanvas.updateBlob();
|
|
66
|
+
} catch {}
|
|
67
|
+
callbacks.onCapture?.(frameCanvas, faceCoordinates);
|
|
68
|
+
};
|
|
69
|
+
mlWasmJSApi_default.setFaceDetectionCallbacks(this.getPipelineType(), callbacks.onFarAway ?? (() => {}), callbacks.onTooClose ?? (() => {}), callbacks.onTooManyFaces ?? (() => {}), callbacks.onNoFace ?? (() => {}), onCaptureWrapper, callbacks.onGetReady ?? (() => {}), callbacks.onGetReadyFinished ?? (() => {}), callbacks.onCenterFace ?? (() => {}), callbacks.onDark ?? (() => {}), callbacks.onBlur ?? (() => {}), callbacks.onFaceAngle ?? (() => {}), callbacks.onLenses ?? (() => {}), callbacks.onMask ?? (() => {}), callbacks.onEyesClosed ?? (() => {}), callbacks.onHeadWear ?? (() => {}), callbacks.onSwitchToManualCapture ?? (() => {}), callbacks.onFaceOccluded ?? (() => {}));
|
|
70
|
+
}
|
|
71
|
+
setPositionConstraints(constraints) {
|
|
72
|
+
this.ensureInitialized();
|
|
73
|
+
mlWasmJSApi_default.setFacePositionConstraints(this.getPipelineType(), constraints.minX, constraints.minY, constraints.maxX, constraints.maxY);
|
|
74
|
+
}
|
|
75
|
+
applyDefaults(autocaptureInterval = 0) {
|
|
76
|
+
this.ensureInitialized();
|
|
77
|
+
this.setThresholds({
|
|
78
|
+
brightnessThreshold: this.defaultThresholds.brightnessThreshold,
|
|
79
|
+
blurrinessThreshold: this.defaultThresholds.blurrinessThreshold,
|
|
80
|
+
tiltRotationAngleThreshold: this.defaultThresholds.tiltRotationAngleThreshold,
|
|
81
|
+
minMagicCropSize: this.defaultThresholds.minMagicCropSize,
|
|
82
|
+
autocaptureInterval,
|
|
83
|
+
minFaceQualityScore: this.defaultThresholds.minFaceQualityScore,
|
|
84
|
+
faceOcclusionThreshold: this.defaultThresholds.faceOcclusionThreshold,
|
|
85
|
+
getReadyDelay: this.defaultThresholds.getReadyDelay,
|
|
86
|
+
framesAggregationInterval: this.defaultThresholds.framesAggregationInterval,
|
|
87
|
+
minFramesWithFace: this.defaultThresholds.minFramesWithFace
|
|
88
|
+
});
|
|
89
|
+
this.setPositionConstraints({
|
|
90
|
+
minX: this.defaultThresholds.frameMinX,
|
|
91
|
+
minY: this.defaultThresholds.frameMinY,
|
|
92
|
+
maxX: this.defaultThresholds.frameMaxX,
|
|
93
|
+
maxY: this.defaultThresholds.frameMaxY
|
|
94
|
+
});
|
|
95
|
+
this.setAttributesThresholds({
|
|
96
|
+
headwearThreshold: this.defaultThresholds.headwearThreshold,
|
|
97
|
+
lensesThreshold: this.defaultThresholds.lensesThreshold,
|
|
98
|
+
closedEyesThreshold: this.defaultThresholds.closedEyesThreshold,
|
|
99
|
+
maskThreshold: this.defaultThresholds.maskThreshold
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
setAutocaptureInterval(interval) {
|
|
103
|
+
this.ensureInitialized();
|
|
104
|
+
if (!this.currentThresholds) {
|
|
105
|
+
this.applyDefaults(interval);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.setThresholds({
|
|
109
|
+
...this.currentThresholds,
|
|
110
|
+
autocaptureInterval: interval
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
setThresholds(thresholds) {
|
|
114
|
+
this.ensureInitialized();
|
|
115
|
+
this.currentThresholds = { ...thresholds };
|
|
116
|
+
mlWasmJSApi_default.setFaceDetectionThresholds(this.getPipelineType(), thresholds.brightnessThreshold, thresholds.blurrinessThreshold, thresholds.tiltRotationAngleThreshold, thresholds.minMagicCropSize, thresholds.autocaptureInterval, thresholds.minFaceQualityScore, thresholds.faceOcclusionThreshold, thresholds.getReadyDelay, thresholds.framesAggregationInterval, thresholds.minFramesWithFace);
|
|
117
|
+
}
|
|
118
|
+
setAttributesThresholds(thresholds) {
|
|
119
|
+
this.ensureInitialized();
|
|
120
|
+
mlWasmJSApi_default.setFaceAttributesThresholds(this.getPipelineType(), thresholds.headwearThreshold, thresholds.lensesThreshold, thresholds.closedEyesThreshold, thresholds.maskThreshold);
|
|
121
|
+
}
|
|
122
|
+
setChecksEnabled(config) {
|
|
123
|
+
this.ensureInitialized();
|
|
124
|
+
mlWasmJSApi_default.setFaceChecksEnabled(this.getPipelineType(), config.lenses, config.mask, config.closedEyes, config.headWear, config.occlusion);
|
|
125
|
+
}
|
|
126
|
+
setVideoSelfieMode(enabled) {
|
|
127
|
+
this.ensureInitialized();
|
|
128
|
+
mlWasmJSApi_default.setFaceDetectionMode(this.getPipelineType(), enabled);
|
|
129
|
+
}
|
|
130
|
+
reset() {
|
|
131
|
+
super.reset();
|
|
132
|
+
this.currentFrame = null;
|
|
133
|
+
}
|
|
134
|
+
createDefaultFaceCoordinates(canvas) {
|
|
135
|
+
return {
|
|
136
|
+
rightEyeX: 0,
|
|
137
|
+
rightEyeY: 0,
|
|
138
|
+
leftEyeX: 0,
|
|
139
|
+
leftEyeY: 0,
|
|
140
|
+
noseTipX: 0,
|
|
141
|
+
noseTipY: 0,
|
|
142
|
+
rightMouthX: 0,
|
|
143
|
+
rightMouthY: 0,
|
|
144
|
+
mouthX: 0,
|
|
145
|
+
mouthY: 0,
|
|
146
|
+
x: 0,
|
|
147
|
+
y: 0,
|
|
148
|
+
width: canvas.width() ?? 0,
|
|
149
|
+
height: canvas.height() ?? 0
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
formatFaceCoordinates(face) {
|
|
153
|
+
return {
|
|
154
|
+
rightEyeX: face.rightEye.x,
|
|
155
|
+
rightEyeY: face.rightEye.y,
|
|
156
|
+
leftEyeX: face.leftEye.x,
|
|
157
|
+
leftEyeY: face.leftEye.y,
|
|
158
|
+
noseTipX: face.noseTip.x,
|
|
159
|
+
noseTipY: face.noseTip.y,
|
|
160
|
+
rightMouthX: face.rightMouthCorner.x,
|
|
161
|
+
rightMouthY: face.rightMouthCorner.y,
|
|
162
|
+
mouthX: face.leftMouthCorner.x,
|
|
163
|
+
mouthY: face.leftMouthCorner.y,
|
|
164
|
+
x: face.rect.x,
|
|
165
|
+
y: face.rect.y,
|
|
166
|
+
width: face.rect.width,
|
|
167
|
+
height: face.rect.height
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region ../infra/src/media/StreamCanvasProcessingSession.ts
|
|
174
|
+
var StreamCanvasProcessingSession = class {
|
|
175
|
+
/**
|
|
176
|
+
* Creates a processing session that reacts to `StreamCanvasCapture` frame events
|
|
177
|
+
* and drives a provider's `processFrame()` method with backpressure.
|
|
178
|
+
*/
|
|
179
|
+
constructor(params) {
|
|
180
|
+
this.disposed = false;
|
|
181
|
+
this.isProcessing = false;
|
|
182
|
+
this.onFrameEvent = () => {
|
|
183
|
+
if (this.disposed || this.isProcessing) return;
|
|
184
|
+
const frame = this.capturer.getLatestFrame();
|
|
185
|
+
if (!frame) return;
|
|
186
|
+
this.onFrame?.(frame);
|
|
187
|
+
this.isProcessing = true;
|
|
188
|
+
this.provider.processFrame(frame).catch(() => {}).finally(() => {
|
|
189
|
+
this.isProcessing = false;
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
this.capturer = params.capturer;
|
|
193
|
+
this.provider = params.provider;
|
|
194
|
+
this.onFrame = params.onFrame;
|
|
195
|
+
this.capturer.addEventListener("frame", this.onFrameEvent);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Unsubscribes from frame events and resets the provider.
|
|
199
|
+
*/
|
|
200
|
+
dispose() {
|
|
201
|
+
if (this.disposed) return;
|
|
202
|
+
this.disposed = true;
|
|
203
|
+
this.capturer.removeEventListener("frame", this.onFrameEvent);
|
|
204
|
+
this.provider.reset();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Returns whether the session has been disposed.
|
|
208
|
+
*/
|
|
209
|
+
isDisposed() {
|
|
210
|
+
return this.disposed;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/internal/recordings/recordingsRepository.ts
|
|
216
|
+
async function createRecordingSession(type) {
|
|
217
|
+
return (await api.post(endpoints.recordingCreateSessionV2, { type })).data;
|
|
218
|
+
}
|
|
219
|
+
async function startRecording(params) {
|
|
220
|
+
return (await api.post(endpoints.recordingStartV2, {
|
|
221
|
+
videoRecordingId: params.videoRecordingId,
|
|
222
|
+
frameRate: 30,
|
|
223
|
+
outputMode: "COMPOSED",
|
|
224
|
+
resolution: params.resolution,
|
|
225
|
+
type: params.type,
|
|
226
|
+
hasAudio: params.hasAudio ?? false
|
|
227
|
+
})).data;
|
|
228
|
+
}
|
|
229
|
+
async function stopRecording$1(videoRecordingId) {
|
|
230
|
+
return (await api.post(endpoints.recordingStopV2, { videoRecordingId })).data;
|
|
231
|
+
}
|
|
232
|
+
async function uploadDeepsightVideo(encryptedVideo, token) {
|
|
233
|
+
try {
|
|
234
|
+
return (await api.post(endpoints.deepsightVideoImport, {
|
|
235
|
+
video: encryptedVideo,
|
|
236
|
+
type: "selfie"
|
|
237
|
+
}, { headers: { "X-Incode-Hardware-Id": token } })).data.recordingId ?? "";
|
|
238
|
+
} catch {
|
|
239
|
+
return "";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/internal/faceCapture/types.ts
|
|
245
|
+
const FACE_ERROR_CODES = {
|
|
246
|
+
FACE_OCCLUDED: "FACE_OCCLUDED",
|
|
247
|
+
LIVENESS: "LIVENESS_ERROR",
|
|
248
|
+
BRIGHTNESS: "BRIGHTNESS_ERROR",
|
|
249
|
+
LENSES: "LENSES_ERROR",
|
|
250
|
+
MASK: "MASK_ERROR",
|
|
251
|
+
CLOSED_EYES: "CLOSED_EYES_ERROR",
|
|
252
|
+
HEAD_COVER: "HEAD_COVER_ERROR",
|
|
253
|
+
SERVER: "SERVER_ERROR",
|
|
254
|
+
FACE_NOT_FOUND: "FACE_NOT_FOUND",
|
|
255
|
+
MULTIPLE_FACES: "MULTIPLE_FACES",
|
|
256
|
+
TOO_BLURRY: "TOO_BLURRY_ERROR",
|
|
257
|
+
TOO_DARK: "TOO_DARK_ERROR",
|
|
258
|
+
USER_IS_NOT_RECOGNIZED: "USER_IS_NOT_RECOGNIZED",
|
|
259
|
+
SPOOF_ATTEMPT_DETECTED: "SPOOF_ATTEMPT_DETECTED",
|
|
260
|
+
FACE_TOO_DARK: "FACE_TOO_DARK",
|
|
261
|
+
LENSES_DETECTED: "LENSES_DETECTED",
|
|
262
|
+
FACE_MASK_DETECTED: "FACE_MASK_DETECTED",
|
|
263
|
+
CLOSED_EYES_DETECTED: "CLOSED_EYES_DETECTED",
|
|
264
|
+
HEAD_COVER_DETECTED: "HEAD_COVER_DETECTED",
|
|
265
|
+
FACE_CROPPING_FAILED: "FACE_CROPPING_FAILED",
|
|
266
|
+
FACE_TOO_SMALL: "FACE_TOO_SMALL",
|
|
267
|
+
FACE_TOO_BLURRY: "FACE_TOO_BLURRY",
|
|
268
|
+
BAD_PHOTO_QUALITY: "BAD_PHOTO_QUALITY",
|
|
269
|
+
PROCESSING_ERROR: "PROCESSING_ERROR",
|
|
270
|
+
BAD_REQUEST: "BAD_REQUEST",
|
|
271
|
+
NONEXISTENT_CUSTOMER: "NONEXISTENT_CUSTOMER",
|
|
272
|
+
HINT_NOT_PROVIDED: "HINT_NOT_PROVIDED",
|
|
273
|
+
SELFIE_IMAGE_LOW_QUALITY: "SELFIE_IMAGE_LOW_QUALITY"
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/modules/selfie/selfieUploadService.ts
|
|
278
|
+
/**
|
|
279
|
+
* Uploads a selfie image to the backend.
|
|
280
|
+
*/
|
|
281
|
+
async function uploadSelfie({ encryptedBase64Image, faceCoordinates, signal, metadata, recordingId }) {
|
|
282
|
+
try {
|
|
283
|
+
const payload = {
|
|
284
|
+
base64Image: encryptedBase64Image,
|
|
285
|
+
faceCoordinates: faceCoordinates ?? void 0,
|
|
286
|
+
encrypted: true,
|
|
287
|
+
clientInfo: { deviceClass: getDeviceClass() },
|
|
288
|
+
metadata: metadata ?? void 0
|
|
289
|
+
};
|
|
290
|
+
const query = { imageType: "selfie" };
|
|
291
|
+
if (recordingId) query.recordingId = recordingId;
|
|
292
|
+
const res = await api.post(endpoints.selfie, payload, {
|
|
293
|
+
signal,
|
|
294
|
+
query
|
|
295
|
+
});
|
|
296
|
+
if (!res.ok) throw new Error(`POST ${endpoints.selfie} failed: ${res.status} ${res.statusText}`);
|
|
297
|
+
return res.data;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
const errorCode = getFaceErrorCodeFromHttpError(error);
|
|
300
|
+
if (errorCode) throw new Error(errorCode);
|
|
301
|
+
throw new Error(FACE_ERROR_CODES.SERVER);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const getFaceErrorCodeFromHttpError = (error) => {
|
|
305
|
+
const err = error;
|
|
306
|
+
if (err.ok !== false || typeof err.status !== "number") return;
|
|
307
|
+
if (err.status !== 400) return FACE_ERROR_CODES.SERVER;
|
|
308
|
+
if (typeof err.data?.status !== "number") return FACE_ERROR_CODES.BAD_REQUEST;
|
|
309
|
+
return {
|
|
310
|
+
3004: FACE_ERROR_CODES.FACE_NOT_FOUND,
|
|
311
|
+
3005: FACE_ERROR_CODES.FACE_NOT_FOUND,
|
|
312
|
+
3006: FACE_ERROR_CODES.TOO_BLURRY,
|
|
313
|
+
3007: FACE_ERROR_CODES.TOO_DARK,
|
|
314
|
+
4010: FACE_ERROR_CODES.MULTIPLE_FACES,
|
|
315
|
+
4019: FACE_ERROR_CODES.FACE_NOT_FOUND,
|
|
316
|
+
4077: FACE_ERROR_CODES.BAD_PHOTO_QUALITY,
|
|
317
|
+
4078: FACE_ERROR_CODES.FACE_OCCLUDED
|
|
318
|
+
}[err.data.status] ?? FACE_ERROR_CODES.BAD_REQUEST;
|
|
319
|
+
};
|
|
320
|
+
async function processFace(imageType = "selfie", signal) {
|
|
321
|
+
return (await api.post(endpoints.processFace, {}, {
|
|
322
|
+
query: { imageType },
|
|
323
|
+
signal
|
|
324
|
+
})).data;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/internal/faceCapture/faceCaptureServices.ts
|
|
329
|
+
function sendLabelInspectionEvent() {
|
|
330
|
+
return addDeviceStats({ cameraLabelInspectionStatus: "FAIL" });
|
|
331
|
+
}
|
|
332
|
+
const MOBILE_SELFIE_CONSTRAINTS = {
|
|
333
|
+
video: {
|
|
334
|
+
facingMode: "user",
|
|
335
|
+
height: { ideal: 480 },
|
|
336
|
+
width: { ideal: 640 }
|
|
337
|
+
},
|
|
338
|
+
audio: false
|
|
339
|
+
};
|
|
340
|
+
const IPHONE14_SELFIE_CONSTRAINTS = {
|
|
341
|
+
video: { height: { ideal: 1080 } },
|
|
342
|
+
audio: false
|
|
343
|
+
};
|
|
344
|
+
function buildCameraConstraints(resolution) {
|
|
345
|
+
if (resolution) return {
|
|
346
|
+
video: {
|
|
347
|
+
facingMode: "user",
|
|
348
|
+
height: resolution.height ? { ideal: resolution.height } : void 0,
|
|
349
|
+
width: resolution.width ? { ideal: resolution.width } : void 0
|
|
350
|
+
},
|
|
351
|
+
audio: false
|
|
352
|
+
};
|
|
353
|
+
if (isIPhone14OrHigher()) return IPHONE14_SELFIE_CONSTRAINTS;
|
|
354
|
+
return MOBILE_SELFIE_CONSTRAINTS;
|
|
355
|
+
}
|
|
356
|
+
function stopStream(stream) {
|
|
357
|
+
stopCameraStream(stream);
|
|
358
|
+
}
|
|
359
|
+
async function getFrontCameraStream(resolution) {
|
|
360
|
+
if (resolution) return requestCameraAccess(buildCameraConstraints(resolution));
|
|
361
|
+
if (isDesktop()) return getDesktopCameraStream({});
|
|
362
|
+
return requestCameraAccess(buildCameraConstraints());
|
|
363
|
+
}
|
|
364
|
+
async function initializeCamera(params) {
|
|
365
|
+
const { config } = params;
|
|
366
|
+
const prcConstraints = buildCameraConstraints(config.cameraResolution);
|
|
367
|
+
const provider = new FaceDetectionProvider();
|
|
368
|
+
await provider.initialize({
|
|
369
|
+
autocaptureInterval: config.autoCaptureTimeout * 1e3,
|
|
370
|
+
useOnDeviceWorkflow: config.onDeviceFaceResultsSubmissionEnabled === true
|
|
371
|
+
});
|
|
372
|
+
provider.setChecksEnabled({
|
|
373
|
+
lenses: config.validateLenses ?? false,
|
|
374
|
+
mask: config.validateFaceMask ?? false,
|
|
375
|
+
closedEyes: config.validateClosedEyes ?? false,
|
|
376
|
+
headWear: config.validateHeadCover ?? false,
|
|
377
|
+
occlusion: false
|
|
378
|
+
});
|
|
379
|
+
await params.deepsightService.performPrcCheck({ constraints: { video: prcConstraints.video } });
|
|
380
|
+
return {
|
|
381
|
+
stream: config.assistedOnboarding ? (await getBackCameraStream()).stream : await getFrontCameraStream(config.cameraResolution),
|
|
382
|
+
provider
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Encrypts the provided selfie image using WASM.
|
|
387
|
+
*/
|
|
388
|
+
async function encryptSelfieImage({ canvas, dependencies }) {
|
|
389
|
+
const base64Image = canvas.getBase64Image();
|
|
390
|
+
if (!base64Image) throw new Error("Canvas image is empty or null");
|
|
391
|
+
return (await dependencies.getWasmUtil()).encryptImage(base64Image);
|
|
392
|
+
}
|
|
393
|
+
function startDetection(params) {
|
|
394
|
+
let lastStatus;
|
|
395
|
+
let session;
|
|
396
|
+
const { provider } = params;
|
|
397
|
+
const setStatus = (status) => {
|
|
398
|
+
if (session?.isDisposed() === true) return;
|
|
399
|
+
if (lastStatus === status) return;
|
|
400
|
+
lastStatus = status;
|
|
401
|
+
params.onUpdate(status);
|
|
402
|
+
};
|
|
403
|
+
const stopDetectionLoop = () => {
|
|
404
|
+
session?.dispose();
|
|
405
|
+
};
|
|
406
|
+
const reset = () => {
|
|
407
|
+
provider.reset();
|
|
408
|
+
};
|
|
409
|
+
const cleanup = () => {
|
|
410
|
+
stopDetectionLoop();
|
|
411
|
+
};
|
|
412
|
+
(async () => {
|
|
413
|
+
try {
|
|
414
|
+
provider.setCallbacks({
|
|
415
|
+
onFarAway: () => setStatus("tooFar"),
|
|
416
|
+
onTooClose: () => setStatus("tooClose"),
|
|
417
|
+
onTooManyFaces: () => setStatus("tooManyFaces"),
|
|
418
|
+
onNoFace: () => setStatus("idle"),
|
|
419
|
+
onCenterFace: () => setStatus("centerFace"),
|
|
420
|
+
onGetReady: () => setStatus("getReady"),
|
|
421
|
+
onGetReadyFinished: () => setStatus("getReadyFinished"),
|
|
422
|
+
onDark: () => {
|
|
423
|
+
if (params.config.validateBrightness) setStatus("dark");
|
|
424
|
+
},
|
|
425
|
+
onBlur: () => setStatus("blur"),
|
|
426
|
+
onFaceAngle: () => setStatus("faceAngle"),
|
|
427
|
+
onLenses: () => {
|
|
428
|
+
if (params.config.validateLenses) setStatus("lenses");
|
|
429
|
+
},
|
|
430
|
+
onMask: () => {
|
|
431
|
+
if (params.config.validateFaceMask) setStatus("faceMask");
|
|
432
|
+
},
|
|
433
|
+
onEyesClosed: () => {
|
|
434
|
+
if (params.config.validateClosedEyes) setStatus("eyesClosed");
|
|
435
|
+
},
|
|
436
|
+
onHeadWear: () => {
|
|
437
|
+
if (params.config.validateHeadCover) setStatus("headWear");
|
|
438
|
+
},
|
|
439
|
+
onFaceOccluded: () => setStatus("faceOcclusion"),
|
|
440
|
+
onSwitchToManualCapture: () => {
|
|
441
|
+
setStatus("manualCapture");
|
|
442
|
+
stopDetectionLoop();
|
|
443
|
+
},
|
|
444
|
+
onCapture: (canvas, faceCoordinates) => {
|
|
445
|
+
setStatus("success");
|
|
446
|
+
params.onSuccess(canvas, faceCoordinates);
|
|
447
|
+
cleanup();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
setStatus("detecting");
|
|
451
|
+
session = new StreamCanvasProcessingSession({
|
|
452
|
+
capturer: params.capturer,
|
|
453
|
+
provider,
|
|
454
|
+
onFrame: params.onFrame
|
|
455
|
+
});
|
|
456
|
+
} catch {
|
|
457
|
+
setStatus("error");
|
|
458
|
+
cleanup();
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
return {
|
|
462
|
+
cleanup,
|
|
463
|
+
reset
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function buildResolutionFromStream(stream) {
|
|
467
|
+
const track = stream.getVideoTracks()[0];
|
|
468
|
+
if (!track) return;
|
|
469
|
+
const settings = track.getSettings();
|
|
470
|
+
const width = settings.width;
|
|
471
|
+
const height = settings.height;
|
|
472
|
+
if (typeof width === "number" && typeof height === "number") return `${width}x${height}`;
|
|
473
|
+
}
|
|
474
|
+
async function createOpenViduRecordingProvider() {
|
|
475
|
+
const { OpenViduRecordingProvider } = await import("./OpenViduRecordingProvider-CMu6XVdc.esm.js");
|
|
476
|
+
return new OpenViduRecordingProvider();
|
|
477
|
+
}
|
|
478
|
+
async function startRecordingSession(params) {
|
|
479
|
+
if (params.config.deepsightLiveness === "VIDEOLIVENESS") return;
|
|
480
|
+
if (params.config.enableFaceRecording !== true) return;
|
|
481
|
+
if (params.existing) return params.existing;
|
|
482
|
+
const provider = params.config.recording?.capability ?? await createOpenViduRecordingProvider();
|
|
483
|
+
const clonedStream = params.clonedStream;
|
|
484
|
+
const hasAudio = clonedStream.getAudioTracks().length > 0;
|
|
485
|
+
const resolution = buildResolutionFromStream(clonedStream);
|
|
486
|
+
const session = await createRecordingSession("selfie");
|
|
487
|
+
const connection = await provider.connect({
|
|
488
|
+
sessionToken: session.token,
|
|
489
|
+
stream: clonedStream,
|
|
490
|
+
events: {}
|
|
491
|
+
});
|
|
492
|
+
await startRecording({
|
|
493
|
+
videoRecordingId: session.videoRecordingId,
|
|
494
|
+
type: "selfie",
|
|
495
|
+
resolution,
|
|
496
|
+
hasAudio
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
token: session.token,
|
|
500
|
+
sessionId: session.sessionId,
|
|
501
|
+
videoRecordingId: session.videoRecordingId,
|
|
502
|
+
connection,
|
|
503
|
+
resolution,
|
|
504
|
+
hasAudio
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function stopRecording(session) {
|
|
508
|
+
try {
|
|
509
|
+
await stopRecording$1(session.videoRecordingId);
|
|
510
|
+
} finally {
|
|
511
|
+
await session.connection.disconnect();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function initializeDeepsightSession(options) {
|
|
515
|
+
const { loadDeepsightSession } = await import("./deepsightLoader-Cm4JIT_z.esm.js");
|
|
516
|
+
return loadDeepsightSession({
|
|
517
|
+
ds: options.ds,
|
|
518
|
+
storage: options.storage
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region ../infra/src/media/StreamCanvasCapture.ts
|
|
524
|
+
var StreamCanvasCapture = class {
|
|
525
|
+
constructor(stream, options) {
|
|
526
|
+
this.hasFrame = false;
|
|
527
|
+
this.disposed = false;
|
|
528
|
+
this.eventTarget = new EventTarget();
|
|
529
|
+
this.video = document.createElement("video");
|
|
530
|
+
this.video.srcObject = stream;
|
|
531
|
+
this.video.autoplay = true;
|
|
532
|
+
this.video.playsInline = true;
|
|
533
|
+
this.video.muted = true;
|
|
534
|
+
const settings = stream.getVideoTracks()[0]?.getSettings();
|
|
535
|
+
const initialWidth = options?.width ?? settings?.width ?? 1280;
|
|
536
|
+
const initialHeight = options?.height ?? settings?.height ?? 720;
|
|
537
|
+
this.canvas = document.createElement("canvas");
|
|
538
|
+
this.canvas.width = initialWidth;
|
|
539
|
+
this.canvas.height = initialHeight;
|
|
540
|
+
this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
541
|
+
const fps = options?.fps ?? 10;
|
|
542
|
+
const intervalMs = fps > 0 ? Math.max(16, Math.floor(1e3 / fps)) : 0;
|
|
543
|
+
this.video.addEventListener("loadedmetadata", () => {
|
|
544
|
+
if (this.video.videoWidth > 0 && this.video.videoHeight > 0) {
|
|
545
|
+
this.canvas.width = this.video.videoWidth;
|
|
546
|
+
this.canvas.height = this.video.videoHeight;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
try {
|
|
550
|
+
this.video.play();
|
|
551
|
+
} catch {}
|
|
552
|
+
this.rafLoop(intervalMs);
|
|
553
|
+
}
|
|
554
|
+
addEventListener(type, listener, options) {
|
|
555
|
+
this.eventTarget.addEventListener(type, listener, options);
|
|
556
|
+
}
|
|
557
|
+
removeEventListener(type, listener, options) {
|
|
558
|
+
this.eventTarget.removeEventListener(type, listener, options);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Returns the latest cached frame as an {@link IncodeCanvas}.
|
|
562
|
+
*/
|
|
563
|
+
getLatestCanvas() {
|
|
564
|
+
if (!this.hasFrame) this.tick();
|
|
565
|
+
if (!this.hasFrame) return null;
|
|
566
|
+
return new IncodeCanvas(this.canvas);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Returns the latest cached frame as raw {@link ImageData}.
|
|
570
|
+
*/
|
|
571
|
+
getLatestFrame() {
|
|
572
|
+
if (!this.ctx) return null;
|
|
573
|
+
if (!this.hasFrame) this.tick();
|
|
574
|
+
if (!this.hasFrame) return null;
|
|
575
|
+
try {
|
|
576
|
+
return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Disposes internal resources and stops the capture loop.
|
|
583
|
+
*/
|
|
584
|
+
dispose() {
|
|
585
|
+
if (this.disposed) return;
|
|
586
|
+
this.disposed = true;
|
|
587
|
+
if (this.rafId !== void 0) {
|
|
588
|
+
window.cancelAnimationFrame(this.rafId);
|
|
589
|
+
this.rafId = void 0;
|
|
590
|
+
}
|
|
591
|
+
this.video.srcObject = null;
|
|
592
|
+
this.canvas.width = 0;
|
|
593
|
+
this.canvas.height = 0;
|
|
594
|
+
this.hasFrame = false;
|
|
595
|
+
}
|
|
596
|
+
rafLoop(intervalMs) {
|
|
597
|
+
const loop = (timeMs) => {
|
|
598
|
+
if (this.disposed) return;
|
|
599
|
+
if (intervalMs <= 0 || this.lastTickTimeMs === void 0 || timeMs - this.lastTickTimeMs >= intervalMs) {
|
|
600
|
+
this.lastTickTimeMs = timeMs;
|
|
601
|
+
const previousFrameTimeSeconds = this.lastFrameTimeSeconds;
|
|
602
|
+
this.tick();
|
|
603
|
+
const currentFrameTimeSeconds = this.video.currentTime;
|
|
604
|
+
if (previousFrameTimeSeconds === void 0) {
|
|
605
|
+
if (this.hasFrame) {
|
|
606
|
+
this.lastFrameTimeSeconds = currentFrameTimeSeconds;
|
|
607
|
+
this.eventTarget.dispatchEvent(new Event("frame"));
|
|
608
|
+
}
|
|
609
|
+
} else if (this.hasFrame && currentFrameTimeSeconds !== previousFrameTimeSeconds) {
|
|
610
|
+
this.lastFrameTimeSeconds = currentFrameTimeSeconds;
|
|
611
|
+
this.eventTarget.dispatchEvent(new Event("frame"));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
this.rafId = window.requestAnimationFrame(loop);
|
|
615
|
+
};
|
|
616
|
+
this.rafId = window.requestAnimationFrame(loop);
|
|
617
|
+
}
|
|
618
|
+
tick() {
|
|
619
|
+
if (!this.ctx) return;
|
|
620
|
+
if (this.video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
|
|
621
|
+
const videoWidth = this.video.videoWidth;
|
|
622
|
+
const videoHeight = this.video.videoHeight;
|
|
623
|
+
if (videoWidth === 0 || videoHeight === 0) return;
|
|
624
|
+
if (this.canvas.width !== videoWidth || this.canvas.height !== videoHeight) {
|
|
625
|
+
this.canvas.width = videoWidth;
|
|
626
|
+
this.canvas.height = videoHeight;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
this.ctx.drawImage(this.video, 0, 0);
|
|
630
|
+
this.hasFrame = true;
|
|
631
|
+
} catch {
|
|
632
|
+
this.hasFrame = false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/internal/manualReview/manualReviewService.ts
|
|
639
|
+
async function flagFaceManualReview() {
|
|
640
|
+
try {
|
|
641
|
+
await api.put(endpoints.updateSession, { manualSelfieCheckNeeded: true });
|
|
642
|
+
} catch {}
|
|
643
|
+
}
|
|
644
|
+
async function flagIdManualReview() {
|
|
645
|
+
try {
|
|
646
|
+
await api.put(endpoints.updateSession, { manualIdCheckNeeded: true });
|
|
647
|
+
} catch {}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region ../infra/src/media/VideoTrimmer.ts
|
|
652
|
+
async function getDurationBySeek(videoBlob) {
|
|
653
|
+
const video = document.createElement("video");
|
|
654
|
+
video.preload = "metadata";
|
|
655
|
+
video.src = URL.createObjectURL(videoBlob);
|
|
656
|
+
try {
|
|
657
|
+
if (!Number.isFinite(video.duration)) {
|
|
658
|
+
video.currentTime = Number.MAX_SAFE_INTEGER;
|
|
659
|
+
await new Promise((resolve) => {
|
|
660
|
+
const onDurationChange = () => {
|
|
661
|
+
if (Number.isFinite(video.duration)) {
|
|
662
|
+
video.removeEventListener("durationchange", onDurationChange);
|
|
663
|
+
video.removeEventListener("timeupdate", onTimeUpdate);
|
|
664
|
+
resolve(video.duration);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
const onTimeUpdate = () => {
|
|
668
|
+
if (Number.isFinite(video.duration)) {
|
|
669
|
+
video.removeEventListener("timeupdate", onTimeUpdate);
|
|
670
|
+
video.removeEventListener("durationchange", onDurationChange);
|
|
671
|
+
resolve(video.duration);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
video.addEventListener("durationchange", onDurationChange);
|
|
675
|
+
video.addEventListener("timeupdate", onTimeUpdate);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const duration = video.duration;
|
|
679
|
+
return Number.isFinite(duration) ? duration : null;
|
|
680
|
+
} finally {
|
|
681
|
+
URL.revokeObjectURL(video.src);
|
|
682
|
+
video.src = "";
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function trimLastNSecondsUsingPlayback(videoBlob, seconds) {
|
|
686
|
+
const video = document.createElement("video");
|
|
687
|
+
video.preload = "metadata";
|
|
688
|
+
video.playsInline = true;
|
|
689
|
+
video.muted = true;
|
|
690
|
+
const videoURL = URL.createObjectURL(videoBlob);
|
|
691
|
+
video.src = videoURL;
|
|
692
|
+
const duration = await getDurationBySeek(videoBlob);
|
|
693
|
+
if (!duration || duration < seconds) {
|
|
694
|
+
URL.revokeObjectURL(videoURL);
|
|
695
|
+
return videoBlob;
|
|
696
|
+
}
|
|
697
|
+
const startTime = Math.max(0, Math.floor(duration) - seconds);
|
|
698
|
+
await new Promise((resolve) => {
|
|
699
|
+
if (video.readyState >= 2) resolve();
|
|
700
|
+
else video.addEventListener("loadedmetadata", () => resolve(), { once: true });
|
|
701
|
+
});
|
|
702
|
+
const canvas = document.createElement("canvas");
|
|
703
|
+
canvas.width = 230;
|
|
704
|
+
canvas.height = 320;
|
|
705
|
+
const captureFps = isAndroid() ? 15 : 24;
|
|
706
|
+
const stream = canvas.captureStream(captureFps);
|
|
707
|
+
const ctx = canvas.getContext("2d");
|
|
708
|
+
video.currentTime = startTime;
|
|
709
|
+
await new Promise((resolve) => {
|
|
710
|
+
video.addEventListener("seeked", () => resolve(), { once: true });
|
|
711
|
+
});
|
|
712
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
713
|
+
const mimeType = videoBlob.type || (MediaRecorder.isTypeSupported("video/webm") ? "video/webm" : "video/mp4");
|
|
714
|
+
const mediaRecorder = new MediaRecorder(stream.clone(), {
|
|
715
|
+
mimeType,
|
|
716
|
+
videoBitsPerSecond: 5e5,
|
|
717
|
+
bitsPerSecond: 5e5
|
|
718
|
+
});
|
|
719
|
+
const chunks = [];
|
|
720
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
721
|
+
if (event.data.size > 0) chunks.push(event.data);
|
|
722
|
+
};
|
|
723
|
+
const recordingPromise = new Promise((resolve) => {
|
|
724
|
+
mediaRecorder.onstop = () => {
|
|
725
|
+
const trimmedBlob$1 = new Blob(chunks, { type: mimeType });
|
|
726
|
+
URL.revokeObjectURL(videoURL);
|
|
727
|
+
mediaRecorder.stream?.getTracks().forEach((track) => track.stop());
|
|
728
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
729
|
+
video.src = "";
|
|
730
|
+
resolve(trimmedBlob$1);
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
video.addEventListener("play", () => {
|
|
734
|
+
function drawVideo() {
|
|
735
|
+
if (video.currentTime >= duration) {
|
|
736
|
+
mediaRecorder.stop();
|
|
737
|
+
video.pause();
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
741
|
+
requestAnimationFrame(drawVideo);
|
|
742
|
+
}
|
|
743
|
+
drawVideo();
|
|
744
|
+
setTimeout(() => {
|
|
745
|
+
mediaRecorder.start(100);
|
|
746
|
+
}, 500);
|
|
747
|
+
});
|
|
748
|
+
video.play().catch(() => {
|
|
749
|
+
URL.revokeObjectURL(videoURL);
|
|
750
|
+
});
|
|
751
|
+
const trimmedBlob = await recordingPromise;
|
|
752
|
+
console.timeEnd("trimLastNSecondsUsingPlayback");
|
|
753
|
+
return trimmedBlob;
|
|
754
|
+
}
|
|
755
|
+
async function trimLastNSeconds(videoBlob, seconds) {
|
|
756
|
+
return await trimLastNSecondsUsingPlayback(videoBlob, seconds);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region ../infra/src/providers/browser/LocalRecordingProvider.ts
|
|
761
|
+
function toBase64(videoBlob) {
|
|
762
|
+
return new Promise((resolve, reject) => {
|
|
763
|
+
const reader = new FileReader();
|
|
764
|
+
reader.onloadend = () => {
|
|
765
|
+
const base64 = reader.result.split(",")[1];
|
|
766
|
+
resolve(base64);
|
|
767
|
+
};
|
|
768
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("FileReader error"));
|
|
769
|
+
reader.readAsDataURL(videoBlob);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function getSupportedMediaRecorderMimeType() {
|
|
773
|
+
const possibleTypes = isIOS() ? [
|
|
774
|
+
"video/mp4",
|
|
775
|
+
"video/webm",
|
|
776
|
+
"video/webm;codecs=vp9",
|
|
777
|
+
"video/webm;codecs=vp8"
|
|
778
|
+
] : [
|
|
779
|
+
"video/webm",
|
|
780
|
+
"video/webm;codecs=vp9",
|
|
781
|
+
"video/webm;codecs=vp8",
|
|
782
|
+
"video/mp4"
|
|
783
|
+
];
|
|
784
|
+
for (const type of possibleTypes) if (MediaRecorder.isTypeSupported(type)) return type;
|
|
785
|
+
return "";
|
|
786
|
+
}
|
|
787
|
+
function getAdaptiveMediaRecorderOptions() {
|
|
788
|
+
const mimeType = getSupportedMediaRecorderMimeType();
|
|
789
|
+
if (isIOS()) return {
|
|
790
|
+
mimeType,
|
|
791
|
+
videoBitsPerSecond: 1e6,
|
|
792
|
+
bitsPerSecond: 1e6
|
|
793
|
+
};
|
|
794
|
+
return {
|
|
795
|
+
mimeType,
|
|
796
|
+
videoBitsPerSecond: 5e5,
|
|
797
|
+
bitsPerSecond: 5e5
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
var LocalRecordingProvider = class {
|
|
801
|
+
constructor() {
|
|
802
|
+
this.mediaRecorder = null;
|
|
803
|
+
this._isRecording = false;
|
|
804
|
+
this._hasError = false;
|
|
805
|
+
this._error = null;
|
|
806
|
+
this.mimeType = "";
|
|
807
|
+
this.stream = null;
|
|
808
|
+
this.pauseRecordingBound = this.pauseRecording.bind(this);
|
|
809
|
+
}
|
|
810
|
+
get isRecording() {
|
|
811
|
+
return this._isRecording;
|
|
812
|
+
}
|
|
813
|
+
get hasError() {
|
|
814
|
+
return this._hasError;
|
|
815
|
+
}
|
|
816
|
+
get error() {
|
|
817
|
+
return this._error;
|
|
818
|
+
}
|
|
819
|
+
startRecording(stream) {
|
|
820
|
+
this.reset();
|
|
821
|
+
this.registerEventListeners();
|
|
822
|
+
this.stream = stream;
|
|
823
|
+
try {
|
|
824
|
+
const options = getAdaptiveMediaRecorderOptions();
|
|
825
|
+
this.mimeType = options.mimeType;
|
|
826
|
+
const recorder = new MediaRecorder(stream.clone(), options);
|
|
827
|
+
recorder.onerror = (event) => {
|
|
828
|
+
this._error = `Recording error: ${event}`;
|
|
829
|
+
this._isRecording = false;
|
|
830
|
+
this._hasError = true;
|
|
831
|
+
};
|
|
832
|
+
recorder.start();
|
|
833
|
+
this.mediaRecorder = recorder;
|
|
834
|
+
this._isRecording = true;
|
|
835
|
+
this._error = null;
|
|
836
|
+
this._hasError = false;
|
|
837
|
+
} catch (err) {
|
|
838
|
+
this._error = `Failed to start recording: ${err}`;
|
|
839
|
+
this._hasError = true;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async stopRecording(trimSeconds, encrypt, generateChecksum) {
|
|
843
|
+
const recorder = this.mediaRecorder;
|
|
844
|
+
return new Promise((resolve, reject) => {
|
|
845
|
+
this.removeEventListeners();
|
|
846
|
+
if (recorder && this._isRecording) {
|
|
847
|
+
const chunks = [];
|
|
848
|
+
recorder.ondataavailable = (event) => {
|
|
849
|
+
if (event.data.size > 0) chunks.push(event.data);
|
|
850
|
+
};
|
|
851
|
+
recorder.onstop = async () => {
|
|
852
|
+
try {
|
|
853
|
+
const trimmedVideo = await trimLastNSeconds(new Blob(chunks, { type: this.mimeType }), trimSeconds);
|
|
854
|
+
const encryptedVideoBase64 = encrypt(await toBase64(trimmedVideo));
|
|
855
|
+
generateChecksum(await trimmedVideo.arrayBuffer());
|
|
856
|
+
this._isRecording = false;
|
|
857
|
+
resolve({
|
|
858
|
+
trimmedBlob: trimmedVideo,
|
|
859
|
+
encryptedVideo: encryptedVideoBase64
|
|
860
|
+
});
|
|
861
|
+
} catch (error) {
|
|
862
|
+
this._isRecording = false;
|
|
863
|
+
this._error = `Recording stop failed: ${error}`;
|
|
864
|
+
this._hasError = true;
|
|
865
|
+
reject(error);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
recorder.stop();
|
|
869
|
+
this._isRecording = false;
|
|
870
|
+
} else resolve({
|
|
871
|
+
trimmedBlob: new Blob([], { type: this.mimeType }),
|
|
872
|
+
encryptedVideo: ""
|
|
873
|
+
});
|
|
874
|
+
recorder?.stream?.getTracks().forEach((track) => track.stop());
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
reset() {
|
|
878
|
+
this._isRecording = false;
|
|
879
|
+
this._error = null;
|
|
880
|
+
this._hasError = false;
|
|
881
|
+
}
|
|
882
|
+
pauseRecording() {
|
|
883
|
+
if (!this._isRecording) return;
|
|
884
|
+
if (this.mediaRecorder?.state === "recording") try {
|
|
885
|
+
this.mediaRecorder.pause();
|
|
886
|
+
} catch {}
|
|
887
|
+
}
|
|
888
|
+
registerEventListeners() {
|
|
889
|
+
document.addEventListener("visibilitychange", this.pauseRecordingBound);
|
|
890
|
+
}
|
|
891
|
+
removeEventListeners() {
|
|
892
|
+
document.removeEventListener("visibilitychange", this.pauseRecordingBound);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/internal/faceCapture/recordingService.ts
|
|
898
|
+
let openViduProviderImportPromise;
|
|
899
|
+
let openViduBrowserImportPromise;
|
|
900
|
+
function loadOpenViduProviderModule() {
|
|
901
|
+
if (!openViduProviderImportPromise) openViduProviderImportPromise = import("./OpenViduRecordingProvider-CMu6XVdc.esm.js");
|
|
902
|
+
return openViduProviderImportPromise;
|
|
903
|
+
}
|
|
904
|
+
function loadOpenViduBrowserModule() {
|
|
905
|
+
if (!openViduBrowserImportPromise) openViduBrowserImportPromise = (async () => {
|
|
906
|
+
return (await import("./openviduLazy-Cm0XFh_v.esm.js")).loadOpenVidu();
|
|
907
|
+
})();
|
|
908
|
+
return openViduBrowserImportPromise;
|
|
909
|
+
}
|
|
910
|
+
function preloadOpenViduProvider() {
|
|
911
|
+
loadOpenViduProviderModule();
|
|
912
|
+
loadOpenViduBrowserModule();
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Records on-device and uploads encrypted video for liveness.
|
|
916
|
+
*/
|
|
917
|
+
var LocalRecordingService = class {
|
|
918
|
+
constructor(wasmUtil, sessionToken) {
|
|
919
|
+
this.recorder = new LocalRecordingProvider();
|
|
920
|
+
this.sessionToken = sessionToken;
|
|
921
|
+
this.wasmUtil = wasmUtil;
|
|
922
|
+
}
|
|
923
|
+
async start(stream) {
|
|
924
|
+
this.recorder.startRecording(stream);
|
|
925
|
+
}
|
|
926
|
+
async stop() {
|
|
927
|
+
if (!this.recorder.isRecording) return { recordingId: null };
|
|
928
|
+
const result = await this.recorder.stopRecording(10, (base64) => this.wasmUtil.encryptImage(base64), (buffer) => this.wasmUtil.ckvcks(buffer));
|
|
929
|
+
if (!this.sessionToken) return { recordingId: null };
|
|
930
|
+
return { recordingId: await uploadDeepsightVideo(result.encryptedVideo, this.sessionToken) };
|
|
931
|
+
}
|
|
932
|
+
async stopAndGetVideo() {
|
|
933
|
+
if (!this.recorder.isRecording) return { videoBase64: void 0 };
|
|
934
|
+
try {
|
|
935
|
+
const videoBase64 = (await this.recorder.stopRecording(10, (base64) => base64, () => {})).encryptedVideo;
|
|
936
|
+
return { videoBase64: videoBase64 ? videoBase64 : void 0 };
|
|
937
|
+
} catch {
|
|
938
|
+
return { videoBase64: void 0 };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
cleanup() {
|
|
942
|
+
this.recorder.reset();
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
/**
|
|
946
|
+
* Records through OpenVidu and manages the session lifecycle.
|
|
947
|
+
*/
|
|
948
|
+
var OpenViduRecordingService = class {
|
|
949
|
+
constructor(config) {
|
|
950
|
+
this.config = config;
|
|
951
|
+
}
|
|
952
|
+
async start(stream) {
|
|
953
|
+
if (!this.provider) {
|
|
954
|
+
const { OpenViduRecordingProvider } = await loadOpenViduProviderModule();
|
|
955
|
+
this.provider = new OpenViduRecordingProvider();
|
|
956
|
+
}
|
|
957
|
+
this.session = await startRecordingSession({
|
|
958
|
+
config: {
|
|
959
|
+
...this.config,
|
|
960
|
+
recording: { capability: this.provider }
|
|
961
|
+
},
|
|
962
|
+
clonedStream: stream.clone(),
|
|
963
|
+
existing: this.session
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async stop() {
|
|
967
|
+
if (this.session) {
|
|
968
|
+
stopRecording(this.session);
|
|
969
|
+
this.session = void 0;
|
|
970
|
+
}
|
|
971
|
+
return { recordingId: null };
|
|
972
|
+
}
|
|
973
|
+
async stopAndGetVideo() {
|
|
974
|
+
if (this.session) {
|
|
975
|
+
stopRecording(this.session);
|
|
976
|
+
this.session = void 0;
|
|
977
|
+
}
|
|
978
|
+
return { videoBase64: void 0 };
|
|
979
|
+
}
|
|
980
|
+
cleanup() {
|
|
981
|
+
if (this.session) stopRecording(this.session);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
/**
|
|
985
|
+
* Creates the recording service for the current configuration.
|
|
986
|
+
*
|
|
987
|
+
* Order of precedence:
|
|
988
|
+
* 1. `onDeviceFaceResultsSubmissionEnabled === true` → no recording. The
|
|
989
|
+
* on-device workflow never uploads media, only the staged face-results
|
|
990
|
+
* JSON via `WebApi.postFaceResults`. Mirrors V1's
|
|
991
|
+
* `FaceFlowManager.setRecorders()` early return.
|
|
992
|
+
* 2. `deepsightLiveness === 'VIDEOLIVENESS'` → local Deepsight recorder.
|
|
993
|
+
* 3. `enableFaceRecording === true` → server-side OpenVidu recorder.
|
|
994
|
+
* 4. otherwise → no recording.
|
|
995
|
+
*/
|
|
996
|
+
function createRecordingService(params) {
|
|
997
|
+
if (params.config.onDeviceFaceResultsSubmissionEnabled === true) return;
|
|
998
|
+
if (params.config.deepsightLiveness === "VIDEOLIVENESS") return new LocalRecordingService(params.wasmUtil, params.sessionToken);
|
|
999
|
+
if (params.config.enableFaceRecording === true) return new OpenViduRecordingService(params.config);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
//#endregion
|
|
1003
|
+
export { stopRecording$1 as _, StreamCanvasCapture as a, initializeDeepsightSession as c, stopStream as d, processFace as f, startRecording as g, createRecordingSession as h, flagIdManualReview as i, sendLabelInspectionEvent as l, FACE_ERROR_CODES as m, preloadOpenViduProvider as n, encryptSelfieImage as o, uploadSelfie as p, flagFaceManualReview as r, initializeCamera as s, createRecordingService as t, startDetection as u, StreamCanvasProcessingSession as v };
|