@daboss2003/liveness-web 1.0.2 → 1.0.4
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/engine.js +11 -11
- package/dist/ui.js +97 -15
- package/package.json +1 -1
- package/src/engine.ts +11 -11
- package/src/ui.ts +99 -15
package/dist/engine.js
CHANGED
|
@@ -110,14 +110,14 @@ const config = {
|
|
|
110
110
|
// Number of frames averaged to produce the resting baseline per step
|
|
111
111
|
baselineFrames: 8,
|
|
112
112
|
// ── Head turns (relative to baseline) ─────────────────────────────────────
|
|
113
|
-
yawTurnDelta:
|
|
113
|
+
yawTurnDelta: 9, // degrees of YAW change needed from rest
|
|
114
114
|
yawWrongDirDelta: 16, // block if turned clearly the WRONG way
|
|
115
|
-
headTurnHoldMs:
|
|
115
|
+
headTurnHoldMs: 80, // sustain the turned pose for this long
|
|
116
116
|
// ── Nod (relative to baseline) ────────────────────────────────────────────
|
|
117
|
-
nodDownDelta:
|
|
118
|
-
nodReturnFraction: 0.
|
|
119
|
-
nodReturnMaxDelta:
|
|
120
|
-
maxYawDuringNod:
|
|
117
|
+
nodDownDelta: 4, // chin must DROP by this many degrees from baseline
|
|
118
|
+
nodReturnFraction: 0.75, // return to 75% of peak nod depth to complete
|
|
119
|
+
nodReturnMaxDelta: 9, // cap: never require returning past 9° from baseline
|
|
120
|
+
maxYawDuringNod: 32,
|
|
121
121
|
// ── Blink ──────────────────────────────────────────────────────────────────
|
|
122
122
|
blinkClosedThreshold: 0.35, // blendshape score = eyes closed
|
|
123
123
|
blinkOpenThreshold: 0.20, // blendshape score = eyes open
|
|
@@ -127,11 +127,11 @@ const config = {
|
|
|
127
127
|
maxYawDuringBlink: 25,
|
|
128
128
|
maxPitchDuringBlink: 25,
|
|
129
129
|
// ── Mouth ──────────────────────────────────────────────────────────────────
|
|
130
|
-
mouthOpenThreshold: 0.
|
|
131
|
-
mouthOpenMarThreshold: 0.
|
|
132
|
-
mouthHoldMs:
|
|
133
|
-
maxYawDuringMouth:
|
|
134
|
-
maxPitchDuringMouth:
|
|
130
|
+
mouthOpenThreshold: 0.20, // jawOpen blendshape
|
|
131
|
+
mouthOpenMarThreshold: 0.20,
|
|
132
|
+
mouthHoldMs: 50,
|
|
133
|
+
maxYawDuringMouth: 35,
|
|
134
|
+
maxPitchDuringMouth: 35,
|
|
135
135
|
// ── Face-in-oval ───────────────────────────────────────────────────────────
|
|
136
136
|
ovalCx: 0.50,
|
|
137
137
|
ovalCy: 0.42,
|
package/dist/ui.js
CHANGED
|
@@ -64,8 +64,58 @@ function createStyles() {
|
|
|
64
64
|
object-fit: cover;
|
|
65
65
|
/* Mirror so it feels like a selfie camera */
|
|
66
66
|
transform: scaleX(-1);
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transition: opacity 0.2s ease;
|
|
67
69
|
/* Clip the video to the oval using clip-path on the parent */
|
|
68
70
|
}
|
|
71
|
+
.lv-video.is-playing { opacity: 1; }
|
|
72
|
+
.lv-video::-webkit-media-controls,
|
|
73
|
+
.lv-video::-webkit-media-controls-panel,
|
|
74
|
+
.lv-video::-webkit-media-controls-play-button,
|
|
75
|
+
.lv-video::-webkit-media-controls-start-playback-button,
|
|
76
|
+
.lv-video::-webkit-media-controls-overlay-play-button,
|
|
77
|
+
.lv-video::-webkit-media-controls-enclosure {
|
|
78
|
+
display: none !important;
|
|
79
|
+
-webkit-appearance: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.lv-root.lv-is-loading .lv-ring-wrap,
|
|
83
|
+
.lv-root.lv-is-loading .lv-dots,
|
|
84
|
+
.lv-root.lv-is-loading .lv-instruction,
|
|
85
|
+
.lv-root.lv-is-loading .lv-pos-hint,
|
|
86
|
+
.lv-root.lv-is-loading .lv-hint-icon {
|
|
87
|
+
opacity: 0;
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
.lv-root:not(.lv-is-loading) .lv-loading { display: none; }
|
|
91
|
+
.lv-loading {
|
|
92
|
+
position: absolute;
|
|
93
|
+
z-index: 2;
|
|
94
|
+
left: 50%;
|
|
95
|
+
top: ${OVAL_TOP_PCT}%;
|
|
96
|
+
transform: translate(-50%, -50%);
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 8px;
|
|
101
|
+
text-align: center;
|
|
102
|
+
color: var(--lv-white);
|
|
103
|
+
text-shadow: 0 1px 6px rgba(0,0,0,0.5);
|
|
104
|
+
}
|
|
105
|
+
.lv-spinner {
|
|
106
|
+
width: 32px;
|
|
107
|
+
height: 32px;
|
|
108
|
+
border: 3px solid rgba(255,255,255,0.25);
|
|
109
|
+
border-top-color: var(--lv-white);
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
animation: lv-spin 0.9s linear infinite;
|
|
112
|
+
}
|
|
113
|
+
.lv-loading-text {
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
opacity: 0.9;
|
|
117
|
+
}
|
|
118
|
+
@keyframes lv-spin { to { transform: rotate(360deg); } }
|
|
69
119
|
|
|
70
120
|
/* ── Dark overlay with oval cutout ──────────────────────────────────── */
|
|
71
121
|
.lv-overlay {
|
|
@@ -257,7 +307,7 @@ export function startLivenessWithUI(options) {
|
|
|
257
307
|
const container = options.container ?? document.body;
|
|
258
308
|
// ── Root shell ─────────────────────────────────────────────────────────────
|
|
259
309
|
const root = document.createElement("div");
|
|
260
|
-
root.className = "lv-root";
|
|
310
|
+
root.className = "lv-root lv-is-loading";
|
|
261
311
|
root.appendChild(createStyles());
|
|
262
312
|
// ── Video background ───────────────────────────────────────────────────────
|
|
263
313
|
const videoBg = document.createElement("div");
|
|
@@ -266,6 +316,7 @@ export function startLivenessWithUI(options) {
|
|
|
266
316
|
video.className = "lv-video";
|
|
267
317
|
video.setAttribute("autoplay", "");
|
|
268
318
|
video.setAttribute("playsinline", "");
|
|
319
|
+
video.setAttribute("webkit-playsinline", "");
|
|
269
320
|
video.setAttribute("muted", "");
|
|
270
321
|
videoBg.appendChild(video);
|
|
271
322
|
root.appendChild(videoBg);
|
|
@@ -292,6 +343,14 @@ export function startLivenessWithUI(options) {
|
|
|
292
343
|
hintIcon.className = "lv-hint-icon";
|
|
293
344
|
hintIcon.setAttribute("aria-hidden", "true");
|
|
294
345
|
root.appendChild(hintIcon);
|
|
346
|
+
// ── Loading spinner (shown until model is ready) ───────────────────────────
|
|
347
|
+
const loading = document.createElement("div");
|
|
348
|
+
loading.className = "lv-loading";
|
|
349
|
+
loading.innerHTML = `
|
|
350
|
+
<div class="lv-spinner" aria-hidden="true"></div>
|
|
351
|
+
<div class="lv-loading-text">Preparing camera...</div>
|
|
352
|
+
`;
|
|
353
|
+
root.appendChild(loading);
|
|
295
354
|
// ── Header ─────────────────────────────────────────────────────────────────
|
|
296
355
|
// const header = document.createElement("div");
|
|
297
356
|
// header.className = "lv-header";
|
|
@@ -323,6 +382,8 @@ export function startLivenessWithUI(options) {
|
|
|
323
382
|
const ringEl = ringWrap.querySelector(".lv-ring-progress");
|
|
324
383
|
const dots = Array.from(dotsEl.querySelectorAll(".lv-dot"));
|
|
325
384
|
const P = ELLIPSE_PERIMETER;
|
|
385
|
+
let isLoading = true;
|
|
386
|
+
let pendingChallenge = null;
|
|
326
387
|
// ── UI helpers ──────────────────────────────────────────────────────────────
|
|
327
388
|
function setProgress(completedSteps) {
|
|
328
389
|
if (!ringEl)
|
|
@@ -355,8 +416,26 @@ export function startLivenessWithUI(options) {
|
|
|
355
416
|
posHint.classList.toggle("visible", !inside);
|
|
356
417
|
posHint.textContent = inside ? "" : (reason ?? "Move your face into the oval");
|
|
357
418
|
}
|
|
419
|
+
function renderChallenge(stepIndex, stepLabel) {
|
|
420
|
+
if (stepIndex === -1) {
|
|
421
|
+
setProgress(LIVENESS_STEP_COUNT);
|
|
422
|
+
setCapturePulse();
|
|
423
|
+
setHint(null);
|
|
424
|
+
setDots(LIVENESS_STEP_COUNT);
|
|
425
|
+
instruction.textContent = stepLabel;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
setProgress(stepIndex);
|
|
429
|
+
setDots(stepIndex);
|
|
430
|
+
setHint(stepLabel);
|
|
431
|
+
instruction.textContent = stepLabel;
|
|
432
|
+
ringEl?.classList.remove("lv-ring-pulse");
|
|
433
|
+
}
|
|
358
434
|
function cleanup() {
|
|
359
435
|
engine.stop();
|
|
436
|
+
video.removeEventListener("playing", onVideoPlaying);
|
|
437
|
+
video.removeEventListener("pause", onVideoPause);
|
|
438
|
+
video.removeEventListener("waiting", onVideoPause);
|
|
360
439
|
root.remove();
|
|
361
440
|
}
|
|
362
441
|
// ── Engine ─────────────────────────────────────────────────────────────────
|
|
@@ -364,6 +443,11 @@ export function startLivenessWithUI(options) {
|
|
|
364
443
|
const sounds = options.sounds ?? {
|
|
365
444
|
...(Object.keys(DEFAULT_SOUND_DATA_URLS).length > 0 ? DEFAULT_SOUND_DATA_URLS : { baseUrl: "audios/" }),
|
|
366
445
|
};
|
|
446
|
+
const onVideoPlaying = () => video.classList.add("is-playing");
|
|
447
|
+
const onVideoPause = () => video.classList.remove("is-playing");
|
|
448
|
+
video.addEventListener("playing", onVideoPlaying);
|
|
449
|
+
video.addEventListener("pause", onVideoPause);
|
|
450
|
+
video.addEventListener("waiting", onVideoPause);
|
|
367
451
|
const engine = new LivenessEngine({
|
|
368
452
|
videoElement: video,
|
|
369
453
|
canvasElement: canvas,
|
|
@@ -372,20 +456,13 @@ export function startLivenessWithUI(options) {
|
|
|
372
456
|
sounds,
|
|
373
457
|
callbacks: {
|
|
374
458
|
onChallengeChanged: (stepIndex, stepLabel) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return;
|
|
459
|
+
pendingChallenge = { stepIndex, stepLabel };
|
|
460
|
+
if (!isLoading) {
|
|
461
|
+
renderChallenge(stepIndex, stepLabel);
|
|
462
|
+
}
|
|
463
|
+
if (stepIndex !== -1) {
|
|
464
|
+
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
382
465
|
}
|
|
383
|
-
setProgress(stepIndex);
|
|
384
|
-
setDots(stepIndex);
|
|
385
|
-
setHint(stepLabel);
|
|
386
|
-
instruction.textContent = stepLabel;
|
|
387
|
-
ringEl?.classList.remove("lv-ring-pulse");
|
|
388
|
-
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
389
466
|
},
|
|
390
467
|
onFaceInOval: (inside, reason) => {
|
|
391
468
|
setFaceInOval(inside, reason);
|
|
@@ -408,7 +485,12 @@ export function startLivenessWithUI(options) {
|
|
|
408
485
|
setProgress(0);
|
|
409
486
|
setDots(0);
|
|
410
487
|
setHint(null);
|
|
411
|
-
engine.start().then(() => {
|
|
488
|
+
engine.start().then(() => {
|
|
489
|
+
isLoading = false;
|
|
490
|
+
root.classList.remove("lv-is-loading");
|
|
491
|
+
if (pendingChallenge)
|
|
492
|
+
renderChallenge(pendingChallenge.stepIndex, pendingChallenge.stepLabel);
|
|
493
|
+
}, (err) => {
|
|
412
494
|
cleanup();
|
|
413
495
|
const reason = err instanceof LivenessError
|
|
414
496
|
? err.code
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -188,15 +188,15 @@ const config = {
|
|
|
188
188
|
baselineFrames: 8,
|
|
189
189
|
|
|
190
190
|
// ── Head turns (relative to baseline) ─────────────────────────────────────
|
|
191
|
-
yawTurnDelta:
|
|
191
|
+
yawTurnDelta: 9, // degrees of YAW change needed from rest
|
|
192
192
|
yawWrongDirDelta: 16, // block if turned clearly the WRONG way
|
|
193
|
-
headTurnHoldMs:
|
|
193
|
+
headTurnHoldMs: 80, // sustain the turned pose for this long
|
|
194
194
|
|
|
195
195
|
// ── Nod (relative to baseline) ────────────────────────────────────────────
|
|
196
|
-
nodDownDelta:
|
|
197
|
-
nodReturnFraction: 0.
|
|
198
|
-
nodReturnMaxDelta:
|
|
199
|
-
maxYawDuringNod:
|
|
196
|
+
nodDownDelta: 4, // chin must DROP by this many degrees from baseline
|
|
197
|
+
nodReturnFraction: 0.75, // return to 75% of peak nod depth to complete
|
|
198
|
+
nodReturnMaxDelta: 9, // cap: never require returning past 9° from baseline
|
|
199
|
+
maxYawDuringNod: 32,
|
|
200
200
|
|
|
201
201
|
// ── Blink ──────────────────────────────────────────────────────────────────
|
|
202
202
|
blinkClosedThreshold: 0.35, // blendshape score = eyes closed
|
|
@@ -208,11 +208,11 @@ const config = {
|
|
|
208
208
|
maxPitchDuringBlink: 25,
|
|
209
209
|
|
|
210
210
|
// ── Mouth ──────────────────────────────────────────────────────────────────
|
|
211
|
-
mouthOpenThreshold: 0.
|
|
212
|
-
mouthOpenMarThreshold: 0.
|
|
213
|
-
mouthHoldMs:
|
|
214
|
-
maxYawDuringMouth:
|
|
215
|
-
maxPitchDuringMouth:
|
|
211
|
+
mouthOpenThreshold: 0.20, // jawOpen blendshape
|
|
212
|
+
mouthOpenMarThreshold: 0.20,
|
|
213
|
+
mouthHoldMs: 50,
|
|
214
|
+
maxYawDuringMouth: 35,
|
|
215
|
+
maxPitchDuringMouth: 35,
|
|
216
216
|
|
|
217
217
|
// ── Face-in-oval ───────────────────────────────────────────────────────────
|
|
218
218
|
ovalCx: 0.50,
|
package/src/ui.ts
CHANGED
|
@@ -79,8 +79,58 @@ function createStyles(): HTMLStyleElement {
|
|
|
79
79
|
object-fit: cover;
|
|
80
80
|
/* Mirror so it feels like a selfie camera */
|
|
81
81
|
transform: scaleX(-1);
|
|
82
|
+
opacity: 0;
|
|
83
|
+
transition: opacity 0.2s ease;
|
|
82
84
|
/* Clip the video to the oval using clip-path on the parent */
|
|
83
85
|
}
|
|
86
|
+
.lv-video.is-playing { opacity: 1; }
|
|
87
|
+
.lv-video::-webkit-media-controls,
|
|
88
|
+
.lv-video::-webkit-media-controls-panel,
|
|
89
|
+
.lv-video::-webkit-media-controls-play-button,
|
|
90
|
+
.lv-video::-webkit-media-controls-start-playback-button,
|
|
91
|
+
.lv-video::-webkit-media-controls-overlay-play-button,
|
|
92
|
+
.lv-video::-webkit-media-controls-enclosure {
|
|
93
|
+
display: none !important;
|
|
94
|
+
-webkit-appearance: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.lv-root.lv-is-loading .lv-ring-wrap,
|
|
98
|
+
.lv-root.lv-is-loading .lv-dots,
|
|
99
|
+
.lv-root.lv-is-loading .lv-instruction,
|
|
100
|
+
.lv-root.lv-is-loading .lv-pos-hint,
|
|
101
|
+
.lv-root.lv-is-loading .lv-hint-icon {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
.lv-root:not(.lv-is-loading) .lv-loading { display: none; }
|
|
106
|
+
.lv-loading {
|
|
107
|
+
position: absolute;
|
|
108
|
+
z-index: 2;
|
|
109
|
+
left: 50%;
|
|
110
|
+
top: ${OVAL_TOP_PCT}%;
|
|
111
|
+
transform: translate(-50%, -50%);
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
text-align: center;
|
|
117
|
+
color: var(--lv-white);
|
|
118
|
+
text-shadow: 0 1px 6px rgba(0,0,0,0.5);
|
|
119
|
+
}
|
|
120
|
+
.lv-spinner {
|
|
121
|
+
width: 32px;
|
|
122
|
+
height: 32px;
|
|
123
|
+
border: 3px solid rgba(255,255,255,0.25);
|
|
124
|
+
border-top-color: var(--lv-white);
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
animation: lv-spin 0.9s linear infinite;
|
|
127
|
+
}
|
|
128
|
+
.lv-loading-text {
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
font-weight: 500;
|
|
131
|
+
opacity: 0.9;
|
|
132
|
+
}
|
|
133
|
+
@keyframes lv-spin { to { transform: rotate(360deg); } }
|
|
84
134
|
|
|
85
135
|
/* ── Dark overlay with oval cutout ──────────────────────────────────── */
|
|
86
136
|
.lv-overlay {
|
|
@@ -277,7 +327,7 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
277
327
|
|
|
278
328
|
// ── Root shell ─────────────────────────────────────────────────────────────
|
|
279
329
|
const root = document.createElement("div");
|
|
280
|
-
root.className = "lv-root";
|
|
330
|
+
root.className = "lv-root lv-is-loading";
|
|
281
331
|
root.appendChild(createStyles());
|
|
282
332
|
|
|
283
333
|
// ── Video background ───────────────────────────────────────────────────────
|
|
@@ -287,6 +337,7 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
287
337
|
video.className = "lv-video";
|
|
288
338
|
video.setAttribute("autoplay", "");
|
|
289
339
|
video.setAttribute("playsinline", "");
|
|
340
|
+
video.setAttribute("webkit-playsinline", "");
|
|
290
341
|
video.setAttribute("muted", "");
|
|
291
342
|
videoBg.appendChild(video);
|
|
292
343
|
root.appendChild(videoBg);
|
|
@@ -317,6 +368,15 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
317
368
|
hintIcon.setAttribute("aria-hidden", "true");
|
|
318
369
|
root.appendChild(hintIcon);
|
|
319
370
|
|
|
371
|
+
// ── Loading spinner (shown until model is ready) ───────────────────────────
|
|
372
|
+
const loading = document.createElement("div");
|
|
373
|
+
loading.className = "lv-loading";
|
|
374
|
+
loading.innerHTML = `
|
|
375
|
+
<div class="lv-spinner" aria-hidden="true"></div>
|
|
376
|
+
<div class="lv-loading-text">Preparing camera...</div>
|
|
377
|
+
`;
|
|
378
|
+
root.appendChild(loading);
|
|
379
|
+
|
|
320
380
|
// ── Header ─────────────────────────────────────────────────────────────────
|
|
321
381
|
// const header = document.createElement("div");
|
|
322
382
|
// header.className = "lv-header";
|
|
@@ -354,6 +414,8 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
354
414
|
const ringEl = ringWrap.querySelector(".lv-ring-progress") as SVGEllipseElement | null;
|
|
355
415
|
const dots = Array.from(dotsEl.querySelectorAll(".lv-dot"));
|
|
356
416
|
const P = ELLIPSE_PERIMETER;
|
|
417
|
+
let isLoading = true;
|
|
418
|
+
let pendingChallenge: { stepIndex: number; stepLabel: string } | null = null;
|
|
357
419
|
|
|
358
420
|
// ── UI helpers ──────────────────────────────────────────────────────────────
|
|
359
421
|
|
|
@@ -389,8 +451,27 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
389
451
|
posHint.textContent = inside ? "" : (reason ?? "Move your face into the oval");
|
|
390
452
|
}
|
|
391
453
|
|
|
454
|
+
function renderChallenge(stepIndex: number, stepLabel: string): void {
|
|
455
|
+
if (stepIndex === -1) {
|
|
456
|
+
setProgress(LIVENESS_STEP_COUNT);
|
|
457
|
+
setCapturePulse();
|
|
458
|
+
setHint(null);
|
|
459
|
+
setDots(LIVENESS_STEP_COUNT);
|
|
460
|
+
instruction.textContent = stepLabel;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
setProgress(stepIndex);
|
|
464
|
+
setDots(stepIndex);
|
|
465
|
+
setHint(stepLabel);
|
|
466
|
+
instruction.textContent = stepLabel;
|
|
467
|
+
ringEl?.classList.remove("lv-ring-pulse");
|
|
468
|
+
}
|
|
469
|
+
|
|
392
470
|
function cleanup(): void {
|
|
393
471
|
engine.stop();
|
|
472
|
+
video.removeEventListener("playing", onVideoPlaying);
|
|
473
|
+
video.removeEventListener("pause", onVideoPause);
|
|
474
|
+
video.removeEventListener("waiting", onVideoPause);
|
|
394
475
|
root.remove();
|
|
395
476
|
}
|
|
396
477
|
|
|
@@ -400,6 +481,12 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
400
481
|
...(Object.keys(DEFAULT_SOUND_DATA_URLS).length > 0 ? DEFAULT_SOUND_DATA_URLS : { baseUrl: "audios/" }),
|
|
401
482
|
};
|
|
402
483
|
|
|
484
|
+
const onVideoPlaying = () => video.classList.add("is-playing");
|
|
485
|
+
const onVideoPause = () => video.classList.remove("is-playing");
|
|
486
|
+
video.addEventListener("playing", onVideoPlaying);
|
|
487
|
+
video.addEventListener("pause", onVideoPause);
|
|
488
|
+
video.addEventListener("waiting", onVideoPause);
|
|
489
|
+
|
|
403
490
|
const engine = new LivenessEngine({
|
|
404
491
|
videoElement: video,
|
|
405
492
|
canvasElement: canvas,
|
|
@@ -408,20 +495,13 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
408
495
|
sounds,
|
|
409
496
|
callbacks: {
|
|
410
497
|
onChallengeChanged: (stepIndex, stepLabel) => {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return;
|
|
498
|
+
pendingChallenge = { stepIndex, stepLabel };
|
|
499
|
+
if (!isLoading) {
|
|
500
|
+
renderChallenge(stepIndex, stepLabel);
|
|
501
|
+
}
|
|
502
|
+
if (stepIndex !== -1) {
|
|
503
|
+
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
418
504
|
}
|
|
419
|
-
setProgress(stepIndex);
|
|
420
|
-
setDots(stepIndex);
|
|
421
|
-
setHint(stepLabel);
|
|
422
|
-
instruction.textContent = stepLabel;
|
|
423
|
-
ringEl?.classList.remove("lv-ring-pulse");
|
|
424
|
-
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
425
505
|
},
|
|
426
506
|
onFaceInOval: (inside, reason) => {
|
|
427
507
|
setFaceInOval(inside, reason);
|
|
@@ -447,7 +527,11 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
447
527
|
setHint(null);
|
|
448
528
|
|
|
449
529
|
engine.start().then(
|
|
450
|
-
() => {
|
|
530
|
+
() => {
|
|
531
|
+
isLoading = false;
|
|
532
|
+
root.classList.remove("lv-is-loading");
|
|
533
|
+
if (pendingChallenge) renderChallenge(pendingChallenge.stepIndex, pendingChallenge.stepLabel);
|
|
534
|
+
},
|
|
451
535
|
(err) => {
|
|
452
536
|
cleanup();
|
|
453
537
|
const reason =
|