@powercred/liveness-web 0.1.0 → 0.1.1
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/README.md +8 -3
- package/dist/liveness.iife.js +5 -5
- package/dist/liveness.js +225 -210
- package/dist/sdk.d.ts +2 -0
- package/dist/ui/strings.d.ts +1 -0
- package/package.json +1 -1
package/dist/liveness.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const oe = { gettingReady: "Getting ready…", center: "Center your face in the oval", moveCloser: "Move a little closer", moveBack: "Move a little back", lookStraight: "Look straight at the camera", noFace: "We can't see your face", multipleFaces: "Only one person, please", turnLeft: "Turn your head to your LEFT", turnRight: "Turn your head to your RIGHT", lookUp: "Look UP", lookDown: "Look DOWN", smile: "Smile", blink: "Blink once", openMouth: "Open your mouth", holdStill: "Hold still", great: "Great!", wrongDirection: "That was the other way — let's try again", faceLost: "We lost your face — let's try again", interrupted: "The check was interrupted — let's try again", timeUp: "Time's up — let's try again", tooSlow: "That took a while — let's try again", flashIntro: "Hold still — the screen will flash colours", checking: "Checking…", passedTitle: "Done", passedDetail: "Thank you, your liveness check is complete.", submittedTitle: "Thank you", failedTitle: "We couldn't confirm that", failedDetail: "Please try again in good light, facing the camera.", noAttemptsDetail: "Please contact the company that asked you to do this check.", tryAgain: "Try again", cameraDenied: "Camera access is needed. Allow it in your browser settings and try again.", cameraUnavailable: "We couldn't start your camera.", unsupportedBrowser: "This browser can't run the check. Please use an up-to-date Chrome, Safari, Edge or Firefox.", insecureContext: "This page must be opened over https.", network: "Connection problem. Please check your internet and try again.", sessionExpired: "This check has expired. Please start again.", serviceUnavailable: "The check is temporarily unavailable. Please try again shortly.", genericError: "Something went wrong. Please try again." }, le = `
|
|
2
2
|
:host { all: initial; display: block; }
|
|
3
3
|
.pcl { --primary: var(--pcl-primary, #4b45a1); --ok: #1f9d55; --warn: #e3a008; --bad: #d64545;
|
|
4
4
|
font-family: var(--pcl-font, system-ui, -apple-system, "Segoe UI", sans-serif); color: #fff;
|
|
@@ -43,11 +43,11 @@ button { font: inherit; font-size: 16px; font-weight: 600; color: #fff; backgrou
|
|
|
43
43
|
button:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
|
|
44
44
|
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
45
45
|
`;
|
|
46
|
-
class
|
|
46
|
+
class ce {
|
|
47
47
|
constructor(e = {}) {
|
|
48
48
|
this.host = document.createElement("div");
|
|
49
49
|
const s = this.host.attachShadow({ mode: "open" });
|
|
50
|
-
s.innerHTML = `<style>${
|
|
50
|
+
s.innerHTML = `<style>${le}</style>
|
|
51
51
|
<div class="pcl">
|
|
52
52
|
<div class="stage">
|
|
53
53
|
<video playsinline muted autoplay></video>
|
|
@@ -95,35 +95,35 @@ class se {
|
|
|
95
95
|
showPanel(e, s, i = "", n) {
|
|
96
96
|
const a = e === "working" ? '<div class="spinner" aria-hidden="true"></div>' : `<div class="icon ${e}" aria-hidden="true">${e === "ok" ? "✓" : "!"}</div>`;
|
|
97
97
|
if (this.panel.innerHTML = `${a}<p class="title"></p>${i ? '<p class="detail"></p>' : ""}${n ? '<button type="button"></button>' : ""}`, this.panel.querySelector(".title").textContent = s, i && (this.panel.querySelector(".detail").textContent = i), this.panel.hidden = !1, !n) return Promise.resolve();
|
|
98
|
-
const
|
|
99
|
-
return
|
|
98
|
+
const r = this.panel.querySelector("button");
|
|
99
|
+
return r.textContent = n, r.focus(), new Promise((o) => r.addEventListener("click", () => o(), { once: !0 }));
|
|
100
100
|
}
|
|
101
101
|
hidePanel() {
|
|
102
102
|
this.panel.hidden = !0, this.panel.innerHTML = "";
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
const
|
|
106
|
-
function
|
|
107
|
-
const e = new URL(t ||
|
|
105
|
+
const de = { runtime: "liveness-runtime", runtimeCompat: "liveness-runtime-compat", model: "liveness-face.bin" }, he = import.meta.url, ue = "assets/";
|
|
106
|
+
function pe(t) {
|
|
107
|
+
const e = new URL(t || ue, t ? document.baseURI : he).href;
|
|
108
108
|
return e.endsWith("/") ? e : e + "/";
|
|
109
109
|
}
|
|
110
|
-
class
|
|
110
|
+
class U extends Error {
|
|
111
111
|
}
|
|
112
|
-
class
|
|
112
|
+
class I {
|
|
113
113
|
constructor(e) {
|
|
114
114
|
this.engine = e, this.lastTs = 0, this.inferenceMs = [];
|
|
115
115
|
}
|
|
116
116
|
static async create(e, s = 2e4) {
|
|
117
|
-
const i =
|
|
117
|
+
const i = pe(e), n = import("./liveness-device-C5ygqTPx.js").then((r) => r.createEngine(i, de));
|
|
118
118
|
let a;
|
|
119
119
|
try {
|
|
120
|
-
const
|
|
121
|
-
a = setTimeout(() => l(new
|
|
120
|
+
const r = await Promise.race([n, new Promise((o, l) => {
|
|
121
|
+
a = setTimeout(() => l(new U("face tracker load timed out")), s);
|
|
122
122
|
})]);
|
|
123
|
-
return new
|
|
124
|
-
} catch (
|
|
125
|
-
throw n.then((
|
|
126
|
-
}),
|
|
123
|
+
return new I(r);
|
|
124
|
+
} catch (r) {
|
|
125
|
+
throw n.then((o) => o.close(), () => {
|
|
126
|
+
}), r instanceof U ? r : new U(String(r));
|
|
127
127
|
} finally {
|
|
128
128
|
clearTimeout(a);
|
|
129
129
|
}
|
|
@@ -143,24 +143,24 @@ class O {
|
|
|
143
143
|
this.engine.close();
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
function
|
|
146
|
+
function x(t) {
|
|
147
147
|
return t.faceCount >= 1 && t.yaw !== void 0;
|
|
148
148
|
}
|
|
149
|
-
class
|
|
149
|
+
class me {
|
|
150
150
|
constructor(e = 0.5) {
|
|
151
151
|
this.alpha = e;
|
|
152
152
|
}
|
|
153
153
|
apply(e) {
|
|
154
|
-
return
|
|
154
|
+
return x(e) ? (this.yaw = this.yaw === void 0 ? e.yaw : this.alpha * e.yaw + (1 - this.alpha) * this.yaw, this.pitch = this.pitch === void 0 ? e.pitch : this.alpha * e.pitch + (1 - this.alpha) * this.pitch, { ...e, yaw: this.yaw, pitch: this.pitch }) : e;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
function
|
|
157
|
+
function fe(t) {
|
|
158
158
|
if (t.length === 0) return;
|
|
159
159
|
const e = [...t].sort((i, n) => i - n), s = Math.floor(e.length / 2);
|
|
160
160
|
return e.length % 2 ? e[s] : (e[s - 1] + e[s]) / 2;
|
|
161
161
|
}
|
|
162
|
-
const
|
|
163
|
-
class
|
|
162
|
+
const w = { turnHeadDeg: 20, nodDeg: 15, jawOpen: 0.4, smile: 0.5, blinkClosed: 0.6, blinkOpen: 0.25, blinkMaxMs: 600, poseHoldMs: 150, expressionHoldMs: 300, baselineMs: 300 };
|
|
163
|
+
class R {
|
|
164
164
|
constructor(e) {
|
|
165
165
|
this.ms = e, this.since = null, this.misses = 0;
|
|
166
166
|
}
|
|
@@ -168,17 +168,17 @@ class A {
|
|
|
168
168
|
return s ? (this.since === null && (this.since = e), this.misses = 0, e - this.since >= this.ms) : this.since !== null && this.misses === 0 ? (this.misses = 1, !1) : (this.since = null, this.misses = 0, !1);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
-
class
|
|
171
|
+
class G {
|
|
172
172
|
constructor(e, s, i) {
|
|
173
|
-
this.axis = e, this.negative = s, this.threshold = i, this.passedAt = null, this.start = null, this.samples = [], this.extreme = 0, this.hold = new
|
|
173
|
+
this.axis = e, this.negative = s, this.threshold = i, this.passedAt = null, this.start = null, this.samples = [], this.extreme = 0, this.hold = new R(w.poseHoldMs), this.opposite = new R(w.poseHoldMs);
|
|
174
174
|
}
|
|
175
175
|
feed(e) {
|
|
176
176
|
if (this.passedAt !== null) return "passed";
|
|
177
|
-
if (this.start === null && (this.start = e.t), !
|
|
177
|
+
if (this.start === null && (this.start = e.t), !x(e)) return this.hold.update(e.t, !1), this.opposite.update(e.t, !1), "pending";
|
|
178
178
|
const s = e[this.axis];
|
|
179
179
|
if (this.baseline === void 0) {
|
|
180
|
-
if (this.samples.push(s), e.t - this.start <
|
|
181
|
-
this.baseline =
|
|
180
|
+
if (this.samples.push(s), e.t - this.start < w.baselineMs) return "pending";
|
|
181
|
+
this.baseline = fe(this.samples);
|
|
182
182
|
}
|
|
183
183
|
const i = s - this.baseline;
|
|
184
184
|
(this.negative ? i < this.extreme : i > this.extreme) && (this.extreme = i);
|
|
@@ -186,80 +186,80 @@ class z {
|
|
|
186
186
|
return this.opposite.update(e.t, a) ? "wrong_direction" : this.hold.update(e.t, n) ? (this.passedAt = e.t, "passed") : "pending";
|
|
187
187
|
}
|
|
188
188
|
get peak() {
|
|
189
|
-
return this.axis === "yaw" ? { yaw:
|
|
189
|
+
return this.axis === "yaw" ? { yaw: D(this.extreme) } : { pitch: D(this.extreme) };
|
|
190
190
|
}
|
|
191
191
|
get confidence() {
|
|
192
|
-
return
|
|
192
|
+
return j(Math.abs(this.extreme) / (this.threshold * 1.5));
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
-
class
|
|
195
|
+
class X {
|
|
196
196
|
constructor(e, s) {
|
|
197
|
-
this.attr = e, this.threshold = s, this.passedAt = null, this.highest = 0, this.hold = new
|
|
197
|
+
this.attr = e, this.threshold = s, this.passedAt = null, this.highest = 0, this.hold = new R(w.expressionHoldMs);
|
|
198
198
|
}
|
|
199
199
|
feed(e) {
|
|
200
200
|
if (this.passedAt !== null) return "passed";
|
|
201
|
-
const s =
|
|
202
|
-
return this.highest = Math.max(this.highest, s), this.hold.update(e.t,
|
|
201
|
+
const s = x(e) ? e[this.attr] ?? 0 : 0;
|
|
202
|
+
return this.highest = Math.max(this.highest, s), this.hold.update(e.t, x(e) && s >= this.threshold) ? (this.passedAt = e.t, "passed") : "pending";
|
|
203
203
|
}
|
|
204
204
|
get peak() {
|
|
205
|
-
return { score:
|
|
205
|
+
return { score: D(this.highest, 3) };
|
|
206
206
|
}
|
|
207
207
|
get confidence() {
|
|
208
|
-
return
|
|
208
|
+
return j(this.highest);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
class
|
|
211
|
+
class we {
|
|
212
212
|
constructor() {
|
|
213
213
|
this.passedAt = null, this.closedAt = null, this.highest = 0;
|
|
214
214
|
}
|
|
215
215
|
feed(e) {
|
|
216
216
|
if (this.passedAt !== null) return "passed";
|
|
217
|
-
if (!
|
|
218
|
-
if (this.highest = Math.max(this.highest, e.blink), e.blink >=
|
|
217
|
+
if (!x(e)) return "pending";
|
|
218
|
+
if (this.highest = Math.max(this.highest, e.blink), e.blink >= w.blinkClosed) this.closedAt === null && (this.closedAt = e.t);
|
|
219
219
|
else if (this.closedAt !== null) {
|
|
220
|
-
if (e.t - this.closedAt >
|
|
221
|
-
else if (e.blink <=
|
|
220
|
+
if (e.t - this.closedAt > w.blinkMaxMs) this.closedAt = null;
|
|
221
|
+
else if (e.blink <= w.blinkOpen) return this.passedAt = this.closedAt, "passed";
|
|
222
222
|
}
|
|
223
223
|
return "pending";
|
|
224
224
|
}
|
|
225
225
|
get peak() {
|
|
226
|
-
return { score:
|
|
226
|
+
return { score: D(this.highest, 3) };
|
|
227
227
|
}
|
|
228
228
|
get confidence() {
|
|
229
|
-
return
|
|
229
|
+
return j(this.highest);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
function
|
|
232
|
+
function J(t, e) {
|
|
233
233
|
switch (t) {
|
|
234
234
|
case "turn_head":
|
|
235
|
-
return new
|
|
235
|
+
return new G("yaw", e === "left", w.turnHeadDeg);
|
|
236
236
|
case "nod":
|
|
237
|
-
return new
|
|
237
|
+
return new G("pitch", e === "up", w.nodDeg);
|
|
238
238
|
case "smile":
|
|
239
|
-
return new
|
|
239
|
+
return new X("smile", w.smile);
|
|
240
240
|
case "open_mouth":
|
|
241
|
-
return new
|
|
241
|
+
return new X("jawOpen", w.jawOpen);
|
|
242
242
|
case "blink":
|
|
243
|
-
return new
|
|
243
|
+
return new we();
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
const
|
|
247
|
-
function
|
|
248
|
-
return t.faceCount > 1 ? "multiple_faces" :
|
|
246
|
+
const k = { holdMs: 800, center: 0.1, minHeight: 0.35, maxHeight: 0.7, maxAngle: 12 };
|
|
247
|
+
function ge(t) {
|
|
248
|
+
return t.faceCount > 1 ? "multiple_faces" : x(t) ? t.boxHeight < k.minHeight ? "move_closer" : t.boxHeight > k.maxHeight ? "move_back" : Math.abs(t.centerX - 0.5) > k.center || Math.abs(t.centerY - 0.5) > k.center ? "center" : Math.abs(t.yaw) > k.maxAngle || Math.abs(t.pitch) > k.maxAngle ? "look_straight" : "ok" : "no_face";
|
|
249
249
|
}
|
|
250
|
-
class
|
|
250
|
+
class ve {
|
|
251
251
|
constructor() {
|
|
252
|
-
this.hold = new
|
|
252
|
+
this.hold = new R(k.holdMs);
|
|
253
253
|
}
|
|
254
254
|
feed(e) {
|
|
255
|
-
const s =
|
|
255
|
+
const s = ge(e);
|
|
256
256
|
return { hint: s, ready: this.hold.update(e.t, s === "ok") };
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
-
function
|
|
260
|
-
return
|
|
259
|
+
function Y(t) {
|
|
260
|
+
return x(t) && t.faceCount === 1 && Math.abs(t.yaw) < 10 && Math.abs(t.pitch) < 10 && t.blink < 0.3;
|
|
261
261
|
}
|
|
262
|
-
class
|
|
262
|
+
class be {
|
|
263
263
|
constructor() {
|
|
264
264
|
this.multi = 0, this.lostSince = null;
|
|
265
265
|
}
|
|
@@ -271,24 +271,24 @@ class ue {
|
|
|
271
271
|
return null;
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
function
|
|
274
|
+
function j(t) {
|
|
275
275
|
return Math.max(0, Math.min(1, t));
|
|
276
276
|
}
|
|
277
|
-
function
|
|
277
|
+
function D(t, e = 1) {
|
|
278
278
|
const s = 10 ** e;
|
|
279
279
|
return Math.round(t * s) / s;
|
|
280
280
|
}
|
|
281
|
-
const
|
|
282
|
-
function
|
|
281
|
+
const ye = "liveness-web", ke = "0.1.1";
|
|
282
|
+
function _e(t) {
|
|
283
283
|
const e = navigator.userAgent, s = navigator.userAgentData;
|
|
284
|
-
return { schema_version: 1, liveness_session_id: t.session.liveness_session_id, sdk: { name:
|
|
284
|
+
return { schema_version: 1, liveness_session_id: t.session.liveness_session_id, sdk: { name: ye, version: ke }, platform: { os: (s?.platform || navigator.platform || "").slice(0, 64) || null, device: null, browser: e.slice(0, 64) }, camera: { width: t.camera.width ?? null, height: t.camera.height ?? null, fps_measured: t.camera.frameRate ?? null, facing: "user" }, video: { mime: t.video.mime, duration_ms: t.video.durationMs, started_at_epoch_ms: t.video.startedAtEpochMs }, mode: t.session.mode, challenges: t.challenges, flash: t.flash, stills: t.stills, device_result: { passed: !0, aborted_reason: null, mode: t.mode, fallback_reason: t.fallbackReason, frames_total: t.tally.framesTotal, frames_with_face: t.tally.framesWithFace, frames_with_two_faces: t.tally.framesWithTwoFaces } };
|
|
285
285
|
}
|
|
286
286
|
class p extends Error {
|
|
287
287
|
constructor(e, s) {
|
|
288
288
|
super(s || e), this.code = e;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
-
async function
|
|
291
|
+
async function K() {
|
|
292
292
|
if (!window.isSecureContext) throw new p("insecure_context", "The camera needs an https page");
|
|
293
293
|
if (!navigator.mediaDevices?.getUserMedia) throw new p("unsupported_browser");
|
|
294
294
|
if (typeof MediaRecorder > "u") throw new p("unsupported_browser");
|
|
@@ -299,16 +299,16 @@ async function we() {
|
|
|
299
299
|
throw e === "NotAllowedError" || e === "SecurityError" ? new p("camera_denied") : new p("camera_unavailable", e);
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
-
function
|
|
302
|
+
function Q(t) {
|
|
303
303
|
t?.getTracks().forEach((e) => e.stop());
|
|
304
304
|
}
|
|
305
|
-
function
|
|
305
|
+
function xe(t = (e) => MediaRecorder.isTypeSupported(e)) {
|
|
306
306
|
return ["video/mp4;codecs=avc1.42E01E", "video/mp4", "video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"].find((e) => t(e)) || "";
|
|
307
307
|
}
|
|
308
|
-
class
|
|
308
|
+
class Me {
|
|
309
309
|
constructor(e, s = 12e5) {
|
|
310
310
|
this.chunks = [], this.t0 = 0, this.epoch = 0;
|
|
311
|
-
const i =
|
|
311
|
+
const i = xe();
|
|
312
312
|
this.recorder = new MediaRecorder(e, i ? { mimeType: i, videoBitsPerSecond: s } : { videoBitsPerSecond: s }), this.recorder.ondataavailable = (n) => {
|
|
313
313
|
n.data && n.data.size && this.chunks.push(n.data);
|
|
314
314
|
};
|
|
@@ -332,21 +332,23 @@ class be {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
|
-
async function
|
|
335
|
+
async function Pe(t, e) {
|
|
336
336
|
const s = t.videoWidth, i = t.videoHeight, n = Math.min(1, 1280 / Math.max(s, i)), a = document.createElement("canvas");
|
|
337
337
|
a.width = Math.round(s * n), a.height = Math.round(i * n), a.getContext("2d").drawImage(t, 0, 0, a.width, a.height);
|
|
338
|
-
let
|
|
338
|
+
let r = 0.9;
|
|
339
339
|
for (; ; ) {
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
342
|
-
if (
|
|
343
|
-
|
|
340
|
+
const o = await new Promise((l) => a.toBlob(l, "image/jpeg", r));
|
|
341
|
+
if (!o) throw new p("capture_failed");
|
|
342
|
+
if (o.size <= e || r <= 0.45) return o;
|
|
343
|
+
r -= 0.1;
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
|
-
const
|
|
347
|
-
class
|
|
346
|
+
const _ = (t) => new Promise((e) => setTimeout(e, t)), Ce = () => new Promise((t) => requestAnimationFrame(t)), Te = 120, Se = 700, Ee = 400, Ae = 1e3, Re = 5e3, De = 2200;
|
|
347
|
+
class A extends Error {
|
|
348
|
+
}
|
|
349
|
+
class Z extends Error {
|
|
348
350
|
}
|
|
349
|
-
function
|
|
351
|
+
function W(t, e) {
|
|
350
352
|
switch (t.type) {
|
|
351
353
|
case "turn_head":
|
|
352
354
|
return t.direction === "left" ? e.turnLeft : e.turnRight;
|
|
@@ -360,7 +362,7 @@ function H(t, e) {
|
|
|
360
362
|
return e.openMouth;
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
|
-
function
|
|
365
|
+
function Fe(t, e) {
|
|
364
366
|
switch (t) {
|
|
365
367
|
case "ok":
|
|
366
368
|
return e.lookStraight;
|
|
@@ -378,131 +380,141 @@ function Te(t, e) {
|
|
|
378
380
|
return e.lookStraight;
|
|
379
381
|
}
|
|
380
382
|
}
|
|
381
|
-
async function
|
|
382
|
-
const { session: e, view: s, strings: i } = t, n = t.mode === "device" && t.tracker !== null, a = new
|
|
383
|
-
let
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
383
|
+
async function Le(t) {
|
|
384
|
+
const { session: e, view: s, strings: i } = t, n = t.mode === "device" && t.tracker !== null, a = new me(), r = { framesTotal: 0, framesWithFace: 0, framesWithTwoFaces: 0 };
|
|
385
|
+
let o = null, l = !1, m = () => {
|
|
386
|
+
};
|
|
387
|
+
const P = new Promise((d) => {
|
|
388
|
+
m = d;
|
|
389
|
+
}), M = () => {
|
|
390
|
+
document.visibilityState === "hidden" && (l = !0, m());
|
|
391
|
+
};
|
|
392
|
+
M(), document.addEventListener("visibilitychange", M);
|
|
393
|
+
const c = () => {
|
|
394
|
+
if (t.isCancelled()) throw new A();
|
|
395
|
+
if (l) throw new Z();
|
|
396
|
+
}, C = async () => {
|
|
397
|
+
const d = await Promise.race([Ce(), P.then(() => NaN)]);
|
|
398
|
+
return c(), d;
|
|
399
|
+
}, E = () => new Promise((d) => {
|
|
400
|
+
const f = s.video;
|
|
401
|
+
f.requestVideoFrameCallback ? f.requestVideoFrameCallback(() => d()) : requestAnimationFrame(() => d()), setTimeout(d, 250);
|
|
402
|
+
}), u = async (d, f) => {
|
|
403
|
+
await E(), c();
|
|
404
|
+
const b = a.apply(t.tracker.detect(s.video, d));
|
|
405
|
+
return f && (r.framesTotal++, b.faceCount >= 1 && r.framesWithFace++, b.faceCount >= 2 && r.framesWithTwoFaces++), b;
|
|
393
406
|
};
|
|
394
407
|
try {
|
|
395
|
-
let
|
|
408
|
+
let d = null;
|
|
396
409
|
if (n) {
|
|
397
|
-
const
|
|
398
|
-
let
|
|
410
|
+
const g = new ve(), v = performance.now();
|
|
411
|
+
let h = 0;
|
|
399
412
|
for (; ; ) {
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
403
|
-
const { hint:
|
|
404
|
-
if (s.setPrompt(
|
|
413
|
+
d = await u(performance.now() - v, !1), h++;
|
|
414
|
+
const T = t.tracker.medianInferenceMs();
|
|
415
|
+
if (h >= 15 && T !== null && T > Te) return { kind: "fallback", reason: "slow_device" };
|
|
416
|
+
const { hint: y, ready: S } = g.feed(d);
|
|
417
|
+
if (s.setPrompt(Fe(y, i)), s.setOval(y === "ok" ? "ok" : "warn"), S) break;
|
|
405
418
|
}
|
|
406
419
|
}
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
420
|
+
o = new Me(t.stream), await o.start(), t.emit({ type: "recording_started" });
|
|
421
|
+
const f = e.limits.max_video_seconds * 1e3, b = [], $ = [], F = async (g) => {
|
|
422
|
+
$.push({ index: b.length, captured_at_ms: o.now(), reason: g }), b.push(await Pe(s.video, e.limits.max_still_bytes));
|
|
410
423
|
};
|
|
411
424
|
if (n) {
|
|
412
|
-
const
|
|
413
|
-
for (;
|
|
414
|
-
await
|
|
415
|
-
} else s.setOval("idle"), s.setPrompt(i.lookStraight), await
|
|
416
|
-
const
|
|
425
|
+
const g = performance.now() + 1e3;
|
|
426
|
+
for (; d && !Y(d) && performance.now() < g; ) d = await u(o.now(), !0);
|
|
427
|
+
await F("positioning");
|
|
428
|
+
} else s.setOval("idle"), s.setPrompt(i.lookStraight), await _(1500), c(), await F("positioning");
|
|
429
|
+
const L = [], q = [], H = e.challenges.length;
|
|
417
430
|
if (e.mode === "active") {
|
|
418
|
-
const
|
|
419
|
-
for (const [
|
|
420
|
-
s.setProgress(
|
|
421
|
-
const
|
|
431
|
+
const g = new be();
|
|
432
|
+
for (const [v, h] of e.challenges.entries()) {
|
|
433
|
+
s.setProgress(H, v, v), s.setPrompt(W(h, i)), s.setOval("idle"), t.emit({ type: "challenge_started", challenge: h });
|
|
434
|
+
const T = o.now();
|
|
422
435
|
if (n) {
|
|
423
|
-
let
|
|
436
|
+
let y = J(h.type, h.direction), S = 1, z = performance.now();
|
|
424
437
|
for (; ; ) {
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
427
|
-
if (
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
if (
|
|
431
|
-
|
|
438
|
+
const B = await u(o.now(), !0), N = g.feed(B);
|
|
439
|
+
if (N) return { kind: "aborted", reason: N };
|
|
440
|
+
if (o.now() > f - Re) return { kind: "aborted", reason: "too_slow" };
|
|
441
|
+
const V = y.feed(B);
|
|
442
|
+
if (V === "wrong_direction") return { kind: "aborted", reason: "wrong_direction" };
|
|
443
|
+
if (V === "passed") {
|
|
444
|
+
L.push({ order: h.order, type: h.type, direction: h.direction, start_ms: T, end_ms: o.now(), passed: !0, confidence: Oe(y.confidence), timed_out: S > 1, peak: y.peak }), q.push(h.type), s.setOval("ok"), s.setPrompt(i.great), s.setProgress(H, v + 1, null), t.emit({ type: "challenge_passed", challenge: h }), await _(Ee);
|
|
432
445
|
break;
|
|
433
446
|
}
|
|
434
|
-
if (performance.now() -
|
|
435
|
-
if (
|
|
436
|
-
|
|
447
|
+
if (performance.now() - z > h.timeout_ms) {
|
|
448
|
+
if (S > 1) return { kind: "aborted", reason: `challenge_timeout:${h.type}` };
|
|
449
|
+
S = 2, y = J(h.type, h.direction), z = performance.now(), s.setPrompt(W(h, i));
|
|
437
450
|
}
|
|
438
451
|
}
|
|
439
|
-
} else await
|
|
452
|
+
} else await ee(s, c, t.timedWindowMs, W(h, i)), L.push({ order: h.order, type: h.type, direction: h.direction, start_ms: T, end_ms: o.now(), passed: null, confidence: null, timed_out: !1, peak: null }), s.setProgress(H, v + 1, null), s.setPrompt(i.lookStraight), await _(Se), c();
|
|
440
453
|
}
|
|
441
|
-
} else await
|
|
442
|
-
const
|
|
443
|
-
if (s.setFlash(null), s.setPrompt(i.lookStraight), n) {
|
|
444
|
-
const
|
|
454
|
+
} else await ee(s, c, De, i.holdStill);
|
|
455
|
+
const ie = await He(t, o, c, C);
|
|
456
|
+
if (c(), s.setFlash(null), s.setPrompt(i.lookStraight), n) {
|
|
457
|
+
const g = performance.now() + 2e3;
|
|
445
458
|
for (; ; ) {
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
459
|
+
const v = await u(o.now(), !0);
|
|
460
|
+
if (Y(v) || performance.now() > g) break;
|
|
448
461
|
}
|
|
449
|
-
} else await
|
|
450
|
-
await
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
453
|
-
const
|
|
454
|
-
return { kind: "recorded", video:
|
|
462
|
+
} else await _(1500), c();
|
|
463
|
+
await F("return_to_centre"), c();
|
|
464
|
+
const ne = o.now(), ae = o.epoch, O = await o.stop();
|
|
465
|
+
if (o = null, O.size > e.limits.max_video_bytes) throw new p("recording_too_large");
|
|
466
|
+
const re = _e({ session: e, mode: t.mode, fallbackReason: t.fallbackReason, camera: t.stream.getVideoTracks()[0]?.getSettings() ?? {}, video: { mime: O.type, durationMs: ne, startedAtEpochMs: ae }, challenges: L, flash: ie, stills: $, tally: r });
|
|
467
|
+
return { kind: "recorded", video: O, stills: b, metadata: re, passedChallenges: q };
|
|
468
|
+
} catch (d) {
|
|
469
|
+
if (d instanceof Z) return { kind: "aborted", reason: "interrupted" };
|
|
470
|
+
throw d;
|
|
455
471
|
} finally {
|
|
456
|
-
|
|
472
|
+
document.removeEventListener("visibilitychange", M), o?.abort(), s.setFlash(null), s.setCountdown(null), s.setDebug(null);
|
|
457
473
|
}
|
|
458
474
|
}
|
|
459
|
-
async function
|
|
475
|
+
async function ee(t, e, s, i) {
|
|
460
476
|
t.setPrompt(i);
|
|
461
477
|
const n = performance.now() + s;
|
|
462
|
-
for (; performance.now() < n; )
|
|
463
|
-
if (e.isCancelled()) throw new C();
|
|
464
|
-
t.setCountdown(Math.ceil((n - performance.now()) / 1e3)), await M(100);
|
|
465
|
-
}
|
|
478
|
+
for (; performance.now() < n; ) e(), t.setCountdown(Math.ceil((n - performance.now()) / 1e3)), await _(100);
|
|
466
479
|
t.setCountdown(null);
|
|
467
480
|
}
|
|
468
|
-
async function
|
|
469
|
-
const
|
|
470
|
-
if (!
|
|
471
|
-
if (!t.allowFlash) return { ...
|
|
472
|
-
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return { ...
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
await
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
function Ae(t) {
|
|
481
|
+
async function He(t, e, s, i) {
|
|
482
|
+
const n = t.session.flash, a = { enabled: !1, started_at_ms: null, segments: [], screen_brightness: null, exposure_locked: !1, skipped_reason: null };
|
|
483
|
+
if (!n.enabled) return a;
|
|
484
|
+
if (!t.allowFlash) return { ...a, skipped_reason: "disabled_by_host" };
|
|
485
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return { ...a, skipped_reason: "reduced_motion" };
|
|
486
|
+
t.emit({ type: "flash_started" }), t.view.setFlash("#000000"), t.view.setPrompt(t.strings.flashIntro), await _(Ae), s(), t.view.setPrompt(""), a.enabled = !0;
|
|
487
|
+
for (const r of n.segments) {
|
|
488
|
+
await i(), t.view.setFlash(r.color);
|
|
489
|
+
const o = await i(), l = Math.round(o - e.t0);
|
|
490
|
+
a.started_at_ms === null && (a.started_at_ms = l), await _(Math.max(0, r.duration_ms - (performance.now() - o))), a.segments.push({ color: r.color, start_ms: l, end_ms: e.now() });
|
|
491
|
+
}
|
|
492
|
+
return t.view.setFlash(null), t.emit({ type: "flash_ended" }), a;
|
|
493
|
+
}
|
|
494
|
+
function Oe(t) {
|
|
483
495
|
return Math.round(t * 100) / 100;
|
|
484
496
|
}
|
|
485
|
-
const
|
|
486
|
-
async function
|
|
497
|
+
const Ue = 6e4;
|
|
498
|
+
async function We(t, e, s) {
|
|
487
499
|
const i = new FormData();
|
|
488
|
-
i.append("video", s.video, s.video.type === "video/mp4" ? "attempt.mp4" : "attempt.webm"), s.stills.forEach((a,
|
|
500
|
+
i.append("video", s.video, s.video.type === "video/mp4" ? "attempt.mp4" : "attempt.webm"), s.stills.forEach((a, r) => i.append("stills", a, `${r}.jpg`)), i.append("metadata", JSON.stringify(s.metadata)), i.append("token", e.token);
|
|
489
501
|
const n = t.replace(/\/+$/, "") + e.upload_url;
|
|
490
502
|
for (let a = 1; ; a++) {
|
|
491
|
-
const
|
|
503
|
+
const r = new AbortController(), o = setTimeout(() => r.abort(), Ue);
|
|
492
504
|
let l;
|
|
493
505
|
try {
|
|
494
|
-
l = await fetch(n, { method: "POST", body: i, signal:
|
|
495
|
-
} catch (
|
|
496
|
-
if (clearTimeout(
|
|
497
|
-
throw new p("network", String(
|
|
506
|
+
l = await fetch(n, { method: "POST", body: i, signal: r.signal });
|
|
507
|
+
} catch (P) {
|
|
508
|
+
if (clearTimeout(o), a < 2) continue;
|
|
509
|
+
throw new p("network", String(P));
|
|
498
510
|
}
|
|
499
|
-
clearTimeout(
|
|
500
|
-
const
|
|
501
|
-
if (l.ok) return
|
|
502
|
-
throw new p(
|
|
511
|
+
clearTimeout(o);
|
|
512
|
+
const m = await l.json().catch(() => ({}));
|
|
513
|
+
if (l.ok) return m;
|
|
514
|
+
throw new p(Ie(l.status), je(m) || `HTTP ${l.status}`);
|
|
503
515
|
}
|
|
504
516
|
}
|
|
505
|
-
function
|
|
517
|
+
function Ie(t) {
|
|
506
518
|
switch (t) {
|
|
507
519
|
case 401:
|
|
508
520
|
return "session_invalid";
|
|
@@ -522,73 +534,76 @@ function Fe(t) {
|
|
|
522
534
|
return "server_error";
|
|
523
535
|
}
|
|
524
536
|
}
|
|
525
|
-
function
|
|
537
|
+
function je(t) {
|
|
526
538
|
const e = t?.detail;
|
|
527
539
|
return e && typeof e == "object" && "error" in e ? String(e.error) : typeof e == "string" ? e : null;
|
|
528
540
|
}
|
|
529
|
-
const
|
|
530
|
-
class
|
|
541
|
+
const $e = { interrupted: "interrupted", wrong_direction: "wrongDirection", face_lost: "faceLost", multiple_faces: "multipleFaces", too_slow: "tooSlow" }, qe = { camera_denied: "cameraDenied", camera_unavailable: "cameraUnavailable", unsupported_browser: "unsupportedBrowser", insecure_context: "insecureContext", network: "network", session_expired: "sessionExpired", session_invalid: "sessionExpired", service_unavailable: "serviceUnavailable", attempts_exhausted: "noAttemptsDetail" };
|
|
542
|
+
class ze {
|
|
531
543
|
constructor(e) {
|
|
532
544
|
if (this.config = e, this.view = null, this.stream = null, this.tracker = null, this.cancelled = !1, this.running = !1, this.wakeOnCancel = null, !e.baseUrl) throw new Error("PowerCredLiveness: baseUrl is required");
|
|
533
545
|
if (!e.session && !e.sessionProvider) throw new Error("PowerCredLiveness: pass session or sessionProvider");
|
|
534
|
-
this.strings = { ...
|
|
546
|
+
this.strings = { ...oe, ...e.strings || {} };
|
|
535
547
|
}
|
|
536
548
|
async start(e) {
|
|
537
549
|
if (this.running) throw new Error("PowerCredLiveness: already running");
|
|
538
550
|
this.running = !0, this.cancelled = !1;
|
|
539
|
-
const s = this.strings, i = this.view = new
|
|
551
|
+
const s = this.strings, i = this.view = new ce(this.config.theme);
|
|
540
552
|
i.mount(e), i.setPrompt(s.gettingReady);
|
|
541
553
|
let n = null, a = null;
|
|
542
554
|
try {
|
|
543
555
|
if (n = this.config.session ?? await this.config.sessionProvider(), Date.parse(n.expires_at) < Date.now()) throw new p("session_expired");
|
|
544
|
-
const
|
|
545
|
-
this.stream = await
|
|
546
|
-
let l =
|
|
547
|
-
this.emit({ type: "mode", mode: l, fallbackReason:
|
|
548
|
-
const
|
|
549
|
-
let
|
|
556
|
+
const r = n.client_checks === "none" ? "none" : "device", o = r === "device" ? I.create(this.config.assetsUrl).catch(() => null) : Promise.resolve(null);
|
|
557
|
+
this.stream = await K(), await i.attach(this.stream), this.emit({ type: "camera_ready" }), this.tracker = await o;
|
|
558
|
+
let l = r === "device" && this.tracker ? "device" : "none", m = r === "device" && !this.tracker ? "model_unavailable" : null;
|
|
559
|
+
this.emit({ type: "mode", mode: l, fallbackReason: m });
|
|
560
|
+
const P = this.config.maxLocalRetries ?? 2;
|
|
561
|
+
let M = 0;
|
|
550
562
|
for (; ; ) {
|
|
551
|
-
const
|
|
552
|
-
if (
|
|
553
|
-
this.tracker?.close(), this.tracker = null, l = "none",
|
|
563
|
+
const c = await Le({ session: n, view: i, strings: s, stream: this.stream, tracker: l === "device" ? this.tracker : null, mode: l, fallbackReason: m, allowFlash: this.config.flash !== !1, timedWindowMs: Math.round((this.config.timedWindowSeconds ?? 3.5) * 1e3), debug: !1, emit: (d) => this.emit(d), isCancelled: () => this.cancelled });
|
|
564
|
+
if (c.kind === "fallback") {
|
|
565
|
+
this.tracker?.close(), this.tracker = null, l = "none", m = c.reason, this.emit({ type: "mode", mode: l, fallbackReason: m });
|
|
554
566
|
continue;
|
|
555
567
|
}
|
|
556
|
-
if (a = { mode: l, fallbackReason:
|
|
557
|
-
this.emit({ type: "attempt_aborted", reason:
|
|
558
|
-
const
|
|
559
|
-
if (
|
|
560
|
-
await this.untilCancelled(i.showPanel("bad",
|
|
568
|
+
if (a = { mode: l, fallbackReason: m, passedChallenges: c.kind === "recorded" ? c.passedChallenges : [] }, c.kind === "aborted") {
|
|
569
|
+
this.emit({ type: "attempt_aborted", reason: c.reason }), c.reason !== "interrupted" && M++;
|
|
570
|
+
const d = s[$e[c.reason] ?? "timeUp"];
|
|
571
|
+
if (M > P) return this.config.showResult !== !1 && await i.showPanel("bad", s.failedTitle, s.failedDetail), this.finish({ status: "failed", livenessSessionId: n.liveness_session_id, device: a, reason: c.reason });
|
|
572
|
+
await this.untilCancelled(i.showPanel("bad", d, "", s.tryAgain)), i.hidePanel(), await this.ensureCamera(i);
|
|
561
573
|
continue;
|
|
562
574
|
}
|
|
563
|
-
const
|
|
564
|
-
if (this.emit({ type: "uploading", bytes:
|
|
565
|
-
const
|
|
566
|
-
return
|
|
575
|
+
const C = c.stills[c.stills.length - 1], E = () => We(this.config.baseUrl, n, c);
|
|
576
|
+
if (this.emit({ type: "uploading", bytes: c.video.size }), this.config.waitForServer === !1) {
|
|
577
|
+
const d = n.liveness_session_id;
|
|
578
|
+
return E().then((f) => this.serverResult(f), (f) => this.emit({ type: "error", code: te(f), message: se(f) })), this.config.showResult !== !1 && await i.showPanel("ok", s.submittedTitle), this.finish({ status: "submitted", livenessSessionId: d, device: a, selfie: C });
|
|
567
579
|
}
|
|
568
580
|
await i.showPanel("working", s.checking);
|
|
569
|
-
const u = await
|
|
570
|
-
if (this.serverResult(u), u.status === "passed") return this.config.showResult !== !1 && await i.showPanel("ok", s.passedTitle, s.passedDetail), this.finish({ status: "passed", livenessSessionId: u.liveness_session_id, attempt: u.attempt, device: a, server: u, retryAllowed: !1, selfie:
|
|
581
|
+
const u = await E();
|
|
582
|
+
if (this.serverResult(u), u.status === "passed") return this.config.showResult !== !1 && await i.showPanel("ok", s.passedTitle, s.passedDetail), this.finish({ status: "passed", livenessSessionId: u.liveness_session_id, attempt: u.attempt, device: a, server: u, retryAllowed: !1, selfie: C });
|
|
571
583
|
if (u.retry_allowed) {
|
|
572
|
-
await this.untilCancelled(i.showPanel("bad", s.failedTitle, s.failedDetail, s.tryAgain)), i.hidePanel();
|
|
584
|
+
await this.untilCancelled(i.showPanel("bad", s.failedTitle, s.failedDetail, s.tryAgain)), i.hidePanel(), await this.ensureCamera(i);
|
|
573
585
|
continue;
|
|
574
586
|
}
|
|
575
|
-
return this.config.showResult !== !1 && await i.showPanel("bad", s.failedTitle, s.noAttemptsDetail), this.finish({ status: u.status === "inconclusive" ? "inconclusive" : "failed", livenessSessionId: u.liveness_session_id, attempt: u.attempt, device: a, server: u, retryAllowed: !1, selfie:
|
|
587
|
+
return this.config.showResult !== !1 && await i.showPanel("bad", s.failedTitle, s.noAttemptsDetail), this.finish({ status: u.status === "inconclusive" ? "inconclusive" : "failed", livenessSessionId: u.liveness_session_id, attempt: u.attempt, device: a, server: u, retryAllowed: !1, selfie: C });
|
|
576
588
|
}
|
|
577
|
-
} catch (
|
|
578
|
-
if (
|
|
579
|
-
const
|
|
580
|
-
this.emit({ type: "error", code:
|
|
581
|
-
const l = s[
|
|
582
|
-
return this.config.showResult !== !1 && await i.showPanel("bad", l), this.finish({ status: "error", livenessSessionId: n?.liveness_session_id ?? null, device: a, reason:
|
|
589
|
+
} catch (r) {
|
|
590
|
+
if (r instanceof A) return this.finish({ status: "cancelled", livenessSessionId: n?.liveness_session_id ?? null, device: a, reason: "cancelled" });
|
|
591
|
+
const o = te(r);
|
|
592
|
+
this.emit({ type: "error", code: o, message: se(r) });
|
|
593
|
+
const l = s[qe[o] ?? "genericError"];
|
|
594
|
+
return this.config.showResult !== !1 && await i.showPanel("bad", l), this.finish({ status: "error", livenessSessionId: n?.liveness_session_id ?? null, device: a, reason: o });
|
|
583
595
|
}
|
|
584
596
|
}
|
|
597
|
+
async ensureCamera(e) {
|
|
598
|
+
this.stream?.getVideoTracks().some((s) => s.readyState === "live") || (Q(this.stream), this.stream = await K(), await e.attach(this.stream));
|
|
599
|
+
}
|
|
585
600
|
cancel() {
|
|
586
601
|
this.cancelled = !0, this.wakeOnCancel?.();
|
|
587
602
|
}
|
|
588
603
|
untilCancelled(e) {
|
|
589
604
|
return new Promise((s, i) => {
|
|
590
|
-
if (this.cancelled) return i(new
|
|
591
|
-
this.wakeOnCancel = () => i(new
|
|
605
|
+
if (this.cancelled) return i(new A());
|
|
606
|
+
this.wakeOnCancel = () => i(new A()), e.then(() => {
|
|
592
607
|
this.wakeOnCancel = null, s();
|
|
593
608
|
});
|
|
594
609
|
});
|
|
@@ -600,7 +615,7 @@ class Ue {
|
|
|
600
615
|
return this.release(), this.running = !1, e;
|
|
601
616
|
}
|
|
602
617
|
release() {
|
|
603
|
-
|
|
618
|
+
Q(this.stream), this.stream = null, this.tracker?.close(), this.tracker = null;
|
|
604
619
|
}
|
|
605
620
|
serverResult(e) {
|
|
606
621
|
this.emit({ type: "server_result", result: e });
|
|
@@ -616,15 +631,15 @@ class Ue {
|
|
|
616
631
|
}
|
|
617
632
|
}
|
|
618
633
|
}
|
|
619
|
-
function
|
|
634
|
+
function te(t) {
|
|
620
635
|
return t instanceof p ? t.code : "unexpected";
|
|
621
636
|
}
|
|
622
|
-
function
|
|
637
|
+
function se(t) {
|
|
623
638
|
return t instanceof p ? t.message : "Unexpected error";
|
|
624
639
|
}
|
|
625
|
-
const
|
|
640
|
+
const Be = "0.1.1";
|
|
626
641
|
export {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
642
|
+
ze as PowerCredLiveness,
|
|
643
|
+
Be as VERSION,
|
|
644
|
+
oe as defaultStrings
|
|
630
645
|
};
|