@sekyro/web 0.1.0-rc.4 → 0.1.0-rc.6
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/CHANGELOG.md +30 -0
- package/README.md +44 -0
- package/dist/{capture-session-v3-uXJr1Wlm.d.ts → capture-session-v3-Okq0EQHG.d.ts} +9 -2
- package/dist/{capture-session-v3-DEInCOUg.d.cts → capture-session-v3-UQU_WjO2.d.cts} +9 -2
- package/dist/{chunk-TBANUT3X.js → chunk-R3SJQHYU.js} +296 -8
- package/dist/chunk-R3SJQHYU.js.map +1 -0
- package/dist/{chunk-EWRNPKTR.js → chunk-TG5IE552.js} +7 -2
- package/dist/{chunk-EWRNPKTR.js.map → chunk-TG5IE552.js.map} +1 -1
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/core/index.cjs +6 -1
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{create-verification-Dkb2tXuY.d.cts → create-verification-BKc9NFQj.d.cts} +92 -8
- package/dist/{create-verification-Ce1y5kJR.d.ts → create-verification-DjcMZHAQ.d.ts} +92 -8
- package/dist/{hosted-client-DWzCRYJK.d.cts → hosted-client-DJXq_CB9.d.cts} +29 -1
- package/dist/{hosted-client-DWzCRYJK.d.ts → hosted-client-DJXq_CB9.d.ts} +29 -1
- package/dist/index.cjs +341 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -3
- package/dist/index.d.ts +25 -3
- package/dist/index.js +61 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/index.cjs +218 -7
- package/dist/internal/index.cjs.map +1 -1
- package/dist/internal/index.d.cts +4 -4
- package/dist/internal/index.d.ts +4 -4
- package/dist/internal/index.js +12 -2
- package/dist/internal/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-TBANUT3X.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @sekyro/web
|
|
2
2
|
|
|
3
|
+
## 0.1.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 54b153c: `HostedSessionView.voice` + `Verification.voiceClips()` — Sekyro-hosted voice clips of the session (used by `@sekyro/react` verification mode).
|
|
8
|
+
|
|
9
|
+
- **`HostedVoiceView` / `HostedVoiceLocale`** (new types): `{ setId, clips: { ar?, fr?, en? } }`, each locale mapping a voice key (`fcv3.*`) to an absolute clip URL on the Sekyro assets host (`assets.sekyro.ai`). A page with a Content-Security-Policy must allow that origin in `media-src`.
|
|
10
|
+
- **`HostedSessionView.voice?: HostedVoiceView | null`** — `null` when no clip set is active; absent on older APIs.
|
|
11
|
+
- **`verification.voiceClips()`** resolves the session's clips (fetching the hosted view if needed), or `null`. It never rejects: a failed bootstrap or a destroyed verification resolves `null`.
|
|
12
|
+
|
|
13
|
+
## 0.1.0-rc.5
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- a14ac15: Pre-capture building blocks: choose the camera at start, warm the face detector ahead of time, and read the camera state for a "get ready" screen.
|
|
18
|
+
|
|
19
|
+
- **Camera at start.** `handle.start({ facing: "user" | "environment" })` (new `LivenessStartOptions`, `CameraFacing` types) picks the camera for this attempt; `retry()` keeps it and `switchCamera()` still flips it. `start()` with no options behaves as before, and `LivenessCaptureOptions.facing` remains the default. `verification.liveness()` handles pass the option through.
|
|
20
|
+
- **Detector preload.** `preloadLivenessAssets({ assetBaseUrl?, idle?, respectSaveData? })` downloads and initialises the face detector so the capture starts instantly instead of racing the cold-start deadline. It is idempotent per `assetBaseUrl`, never rejects, and resolves the settled status (`LivenessAssetsStatus`): `'ready'`; `'failed'` (call it again to retry); or `'idle'`, meaning nothing was preloaded (server-side rendering, no camera API, or Data-Saver with `respectSaveData: true`) — start normally. `idle: true` defers the load to browser idle time; a capture that needs the detector meanwhile starts it at once. The preload warms the blaze (MediaPipe) tier only, with a generous 120 s init deadline; captures without a preload keep the existing detector ladder and deadline, unchanged.
|
|
21
|
+
- **Used automatically.** Every `createLivenessCapture` / `verification.liveness()` capture with the same `assetBaseUrl` reuses the preloaded detector — no extra wiring — and destroying or retrying a capture never frees it. The preload is shared across duplicate copies of `@sekyro/web` on the page.
|
|
22
|
+
- **Status store.** `getLivenessAssetsStatus(assetBaseUrl?)` and `subscribeLivenessAssets(listener, assetBaseUrl?)` (the `useSyncExternalStore` contract: no emit on subscribe) drive a Start button's loading / retry state.
|
|
23
|
+
- **Release.** `releaseLivenessAssets(assetBaseUrl?)` frees the warmed detector (or cancels a pending idle load) when the user leaves the flow. Never call it while a capture is mounted.
|
|
24
|
+
- **Camera probes.** `queryCameraPermission()` and `subscribeCameraPermission(listener)` report `CameraPermissionState` (`'granted' | 'prompt' | 'denied' | 'unknown'`; the subscription emits the current state first, then every change). `hasMultipleCameras()` tells whether a front/back choice makes sense (phones and tablets always; desktops with two or more cameras). All are safe to call during server-side rendering and never throw.
|
|
25
|
+
- 66522c3: The liveness capture now keeps the screen awake while the camera is live.
|
|
26
|
+
|
|
27
|
+
- **Screen Wake Lock.** From `starting` through `gating` and `recording`, the capture holds a Screen Wake Lock so the phone does not dim or lock while the person positions their face. It is released on `captured`, `error` and `destroy()`, taken again on `retry()`, and re-taken automatically when the person returns to the page (browsers drop the lock when a page is hidden).
|
|
28
|
+
- **On by default, best-effort.** Browsers without the Screen Wake Lock API, or a refused request (page not visible, insecure context, blocked by Permissions-Policy), leave the capture exactly as before — nothing throws and no `error` is emitted.
|
|
29
|
+
- **Opt out:** `keepScreenAwake: false` on `createLivenessCapture({ … })` / `verification.liveness({ … })` never requests the lock.
|
|
30
|
+
- **Iframes:** if you embed the capture in an iframe, allow the permission alongside the camera — `<iframe allow="camera; screen-wake-lock">` — and do not send `Permissions-Policy: screen-wake-lock=()`. Without it the capture still works; the screen may just dim.
|
|
31
|
+
- `@sekyro/react`'s `<LivenessCapture>` gets this automatically once it runs on this `@sekyro/web` version.
|
|
32
|
+
|
|
3
33
|
## 0.1.0-rc.4
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -31,6 +31,50 @@ under `/models-v3` (or `assetBaseUrl`), and your page origin registered with Sek
|
|
|
31
31
|
> task (Changesets + Trusted Publishing). Sekyro's own hosted page consumes it via a
|
|
32
32
|
> `file:` link today. Formerly `@ibtikar/identity-web-sdk` (internal).
|
|
33
33
|
|
|
34
|
+
## Preload & camera probes
|
|
35
|
+
|
|
36
|
+
Warm the face detector before the user taps Start (on the step before the
|
|
37
|
+
capture, or at mount), and read the camera permission for your "get ready" UI:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createLivenessCapture, getLivenessAssetsStatus, hasMultipleCameras,
|
|
41
|
+
preloadLivenessAssets, subscribeCameraPermission, subscribeLivenessAssets } from "@sekyro/web";
|
|
42
|
+
|
|
43
|
+
void preloadLivenessAssets({ assetBaseUrl: "/models-v3", idle: true }); // never rejects
|
|
44
|
+
const gateStart = (s: string) => (startBtn.disabled = s === "loading");
|
|
45
|
+
gateStart(getLivenessAssetsStatus("/models-v3")); // subscribe does not emit the current status
|
|
46
|
+
subscribeLivenessAssets(gateStart, "/models-v3");
|
|
47
|
+
subscribeCameraPermission((p) => showCameraRow(p)); // granted|prompt|denied|unknown
|
|
48
|
+
const showFacingChoice = await hasMultipleCameras();
|
|
49
|
+
|
|
50
|
+
const cap = createLivenessCapture({ video, assetBaseUrl: "/models-v3" });
|
|
51
|
+
await cap.start({ facing: "user" }); // reuses the warm detector automatically
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `preloadLivenessAssets` is idempotent per `assetBaseUrl` and resolves the settled
|
|
55
|
+
status: `'ready'`, `'failed'` (call it again to retry), or `'idle'` — nothing was
|
|
56
|
+
preloaded (SSR, no camera API, Data-Saver with `respectSaveData`); just start
|
|
57
|
+
normally. `idle: true` defers the load to browser idle time; a capture that
|
|
58
|
+
starts meanwhile begins it at once.
|
|
59
|
+
- The preload warms the blaze (MediaPipe) tier with a generous deadline. Every
|
|
60
|
+
capture created afterwards with the same `assetBaseUrl` uses it without any extra
|
|
61
|
+
wiring, and destroying a capture never frees it; call
|
|
62
|
+
`releaseLivenessAssets(assetBaseUrl)` when the user leaves the flow (never while a
|
|
63
|
+
capture is mounted). Without a preload the capture loads the detector itself, as
|
|
64
|
+
before.
|
|
65
|
+
- `getLivenessAssetsStatus` / `subscribeLivenessAssets` follow the
|
|
66
|
+
`useSyncExternalStore` contract (no emit on subscribe). `subscribeCameraPermission`
|
|
67
|
+
emits the current state first, then every change; `queryCameraPermission` is the
|
|
68
|
+
one-shot read. Both report `'unknown'` where the Permissions API cannot tell.
|
|
69
|
+
- `start({ facing })` picks the camera for this attempt and its retries;
|
|
70
|
+
`switchCamera()` still flips it mid-capture.
|
|
71
|
+
- `keepScreenAwake` (capture option, default `true`) holds a Screen Wake Lock while
|
|
72
|
+
the camera is live (`starting` / `gating` / `recording`), released on `captured`,
|
|
73
|
+
`error` and `destroy()`, re-taken on `retry()` and when the page is visible again.
|
|
74
|
+
Best-effort — unsupported or refused requests never affect the capture. In an
|
|
75
|
+
iframe, allow it: `<iframe allow="camera; screen-wake-lock">`. Pass `false` to
|
|
76
|
+
never request it.
|
|
77
|
+
|
|
34
78
|
## Entry points
|
|
35
79
|
|
|
36
80
|
| Export | Contents | Public API |
|
|
@@ -429,6 +429,11 @@ interface CaptureSessionV3Options {
|
|
|
429
429
|
now?: () => number;
|
|
430
430
|
}
|
|
431
431
|
type Handler<K extends keyof CaptureSessionV3Events> = (payload: CaptureSessionV3Events[K]) => void;
|
|
432
|
+
/** Per-start options (the public handle's `start({ facing })`). */
|
|
433
|
+
interface CaptureSessionV3StartOptions {
|
|
434
|
+
/** Camera for this attempt AND later retries; absent → keep the current one. */
|
|
435
|
+
facingMode?: "user" | "environment";
|
|
436
|
+
}
|
|
432
437
|
declare class CaptureSessionV3 {
|
|
433
438
|
private readonly options;
|
|
434
439
|
private readonly handlers;
|
|
@@ -453,7 +458,9 @@ declare class CaptureSessionV3 {
|
|
|
453
458
|
get state(): CaptureState;
|
|
454
459
|
on<K extends keyof CaptureSessionV3Events>(event: K, handler: Handler<K>): () => void;
|
|
455
460
|
private emit;
|
|
456
|
-
start(): Promise<void>;
|
|
461
|
+
start(options?: CaptureSessionV3StartOptions): Promise<void>;
|
|
462
|
+
/** The camera the current/next attempt requests (start option, switchCamera toggles). */
|
|
463
|
+
get facingMode(): "user" | "environment";
|
|
457
464
|
/** Full reset + camera restart — the documented recovery path (v1 parity). */
|
|
458
465
|
retry(): void;
|
|
459
466
|
switchCamera(): void;
|
|
@@ -484,4 +491,4 @@ declare class CaptureSessionV3 {
|
|
|
484
491
|
private now;
|
|
485
492
|
}
|
|
486
493
|
|
|
487
|
-
export { type
|
|
494
|
+
export { type V3GateTickInput as A, type V3TelemetryEvent as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, V3_GATE_DEFAULTS as H, VIDEO_BPS as I, buildLivenessV2FormData as J, KEYFRAME_CANDIDATES as K, cameraPreflight as L, MAX_CLIP_BYTES as M, classifyDevice as N, extForMime as O, isClipTooLarge as P, isMediaRecorderSupported as Q, mapGetUserMediaError as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, pickMimeType as U, type V3ChecksPayload as V, probeDevice as W, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureSessionV3StartOptions as h, type CaptureState as i, type CaptureTiming as j, ClipError as k, type ClipErrorKind as l, ClipRecorder as m, type ClipResult as n, DEVICE_PROFILES as o, type DeviceClass as p, type DeviceProbe as q, type DeviceProfile as r, DeviceProfiler as s, type EscalationHook as t, KEYFRAME_WINDOW_MS as u, MIME_CANDIDATES as v, type V3GateFactor as w, type V3GateQuality as x, type V3GateThresholds as y, type V3GateTick as z };
|
|
@@ -429,6 +429,11 @@ interface CaptureSessionV3Options {
|
|
|
429
429
|
now?: () => number;
|
|
430
430
|
}
|
|
431
431
|
type Handler<K extends keyof CaptureSessionV3Events> = (payload: CaptureSessionV3Events[K]) => void;
|
|
432
|
+
/** Per-start options (the public handle's `start({ facing })`). */
|
|
433
|
+
interface CaptureSessionV3StartOptions {
|
|
434
|
+
/** Camera for this attempt AND later retries; absent → keep the current one. */
|
|
435
|
+
facingMode?: "user" | "environment";
|
|
436
|
+
}
|
|
432
437
|
declare class CaptureSessionV3 {
|
|
433
438
|
private readonly options;
|
|
434
439
|
private readonly handlers;
|
|
@@ -453,7 +458,9 @@ declare class CaptureSessionV3 {
|
|
|
453
458
|
get state(): CaptureState;
|
|
454
459
|
on<K extends keyof CaptureSessionV3Events>(event: K, handler: Handler<K>): () => void;
|
|
455
460
|
private emit;
|
|
456
|
-
start(): Promise<void>;
|
|
461
|
+
start(options?: CaptureSessionV3StartOptions): Promise<void>;
|
|
462
|
+
/** The camera the current/next attempt requests (start option, switchCamera toggles). */
|
|
463
|
+
get facingMode(): "user" | "environment";
|
|
457
464
|
/** Full reset + camera restart — the documented recovery path (v1 parity). */
|
|
458
465
|
retry(): void;
|
|
459
466
|
switchCamera(): void;
|
|
@@ -484,4 +491,4 @@ declare class CaptureSessionV3 {
|
|
|
484
491
|
private now;
|
|
485
492
|
}
|
|
486
493
|
|
|
487
|
-
export { type
|
|
494
|
+
export { type V3GateTickInput as A, type V3TelemetryEvent as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, V3_GATE_DEFAULTS as H, VIDEO_BPS as I, buildLivenessV2FormData as J, KEYFRAME_CANDIDATES as K, cameraPreflight as L, MAX_CLIP_BYTES as M, classifyDevice as N, extForMime as O, isClipTooLarge as P, isMediaRecorderSupported as Q, mapGetUserMediaError as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, pickMimeType as U, type V3ChecksPayload as V, probeDevice as W, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureSessionV3StartOptions as h, type CaptureState as i, type CaptureTiming as j, ClipError as k, type ClipErrorKind as l, ClipRecorder as m, type ClipResult as n, DEVICE_PROFILES as o, type DeviceClass as p, type DeviceProbe as q, type DeviceProfile as r, DeviceProfiler as s, type EscalationHook as t, KEYFRAME_WINDOW_MS as u, MIME_CANDIDATES as v, type V3GateFactor as w, type V3GateQuality as x, type V3GateThresholds as y, type V3GateTick as z };
|
|
@@ -2,12 +2,191 @@ import {
|
|
|
2
2
|
CaptureSessionV3,
|
|
3
3
|
detectIsMobile,
|
|
4
4
|
isMediaRecorderSupported
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TG5IE552.js";
|
|
6
6
|
import {
|
|
7
7
|
HostedApiError,
|
|
8
8
|
HostedClient,
|
|
9
9
|
stripMimeParams
|
|
10
10
|
} from "./chunk-YVFF4Q4G.js";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_V3_ASSET_BASE_URL,
|
|
13
|
+
createDetector
|
|
14
|
+
} from "./chunk-NWF46H7K.js";
|
|
15
|
+
|
|
16
|
+
// src/capture/preload.ts
|
|
17
|
+
var PRELOAD_BLAZE_INIT_TIMEOUT_MS = 12e4;
|
|
18
|
+
var PRELOAD_BUDGET_MS = 13e4;
|
|
19
|
+
var IDLE_TIMEOUT_MS = 3e3;
|
|
20
|
+
var IDLE_FALLBACK_MS = 1200;
|
|
21
|
+
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@sekyro/web.preload.v1");
|
|
22
|
+
function registry() {
|
|
23
|
+
const host = globalThis;
|
|
24
|
+
host[REGISTRY_KEY] ?? (host[REGISTRY_KEY] = /* @__PURE__ */ new Map());
|
|
25
|
+
return host[REGISTRY_KEY];
|
|
26
|
+
}
|
|
27
|
+
function normalizeBase(assetBaseUrl) {
|
|
28
|
+
const base = (assetBaseUrl ?? DEFAULT_V3_ASSET_BASE_URL).replace(/\/+$/, "");
|
|
29
|
+
return base === "" ? "/" : base;
|
|
30
|
+
}
|
|
31
|
+
function entryFor(base) {
|
|
32
|
+
const map = registry();
|
|
33
|
+
let entry = map.get(base);
|
|
34
|
+
if (entry === void 0) {
|
|
35
|
+
entry = { status: "idle", promise: null, scheduled: null, detectorPromise: null, detector: null, listeners: /* @__PURE__ */ new Set() };
|
|
36
|
+
map.set(base, entry);
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
function setStatus(entry, next) {
|
|
41
|
+
if (entry.status === next) return;
|
|
42
|
+
entry.status = next;
|
|
43
|
+
for (const listener of [...entry.listeners]) listener(next);
|
|
44
|
+
}
|
|
45
|
+
var defaultLoader = (base) => createDetector({ preferred: ["blaze"], v3AssetBaseUrl: base, blazeInitTimeoutMs: PRELOAD_BLAZE_INIT_TIMEOUT_MS });
|
|
46
|
+
var loader = defaultLoader;
|
|
47
|
+
function setPreloadLoaderForTests(next) {
|
|
48
|
+
loader = next ?? defaultLoader;
|
|
49
|
+
}
|
|
50
|
+
function resetLivenessAssetsForTests() {
|
|
51
|
+
for (const entry of registry().values()) entry.detector?.destroy();
|
|
52
|
+
registry().clear();
|
|
53
|
+
loader = defaultLoader;
|
|
54
|
+
}
|
|
55
|
+
function canCapture() {
|
|
56
|
+
return typeof window !== "undefined" && typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
|
|
57
|
+
}
|
|
58
|
+
function saveDataOn() {
|
|
59
|
+
return navigator.connection?.saveData === true;
|
|
60
|
+
}
|
|
61
|
+
function scheduleIdle(run) {
|
|
62
|
+
const w = window;
|
|
63
|
+
if (typeof w.requestIdleCallback === "function") {
|
|
64
|
+
const handle = w.requestIdleCallback(run, { timeout: IDLE_TIMEOUT_MS });
|
|
65
|
+
return () => w.cancelIdleCallback?.(handle);
|
|
66
|
+
}
|
|
67
|
+
const timer = setTimeout(run, IDLE_FALLBACK_MS);
|
|
68
|
+
return () => clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
function schedule(entry, base) {
|
|
71
|
+
let resolve;
|
|
72
|
+
const promise = new Promise((res) => {
|
|
73
|
+
resolve = res;
|
|
74
|
+
});
|
|
75
|
+
let done = false;
|
|
76
|
+
let cancel = () => void 0;
|
|
77
|
+
const scheduled = {
|
|
78
|
+
promise,
|
|
79
|
+
settle: (outcome) => {
|
|
80
|
+
if (done) return;
|
|
81
|
+
done = true;
|
|
82
|
+
cancel();
|
|
83
|
+
if (entry.scheduled === scheduled) entry.scheduled = null;
|
|
84
|
+
resolve(outcome);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
entry.scheduled = scheduled;
|
|
88
|
+
cancel = scheduleIdle(() => {
|
|
89
|
+
if (done) return;
|
|
90
|
+
scheduled.settle(begin(entry, base));
|
|
91
|
+
});
|
|
92
|
+
return promise;
|
|
93
|
+
}
|
|
94
|
+
function begin(entry, base) {
|
|
95
|
+
if ((entry.status === "loading" || entry.status === "ready") && entry.promise !== null) return entry.promise;
|
|
96
|
+
setStatus(entry, "loading");
|
|
97
|
+
const detectorPromise = new Promise((resolve) => resolve(loader(base)));
|
|
98
|
+
entry.detectorPromise = detectorPromise;
|
|
99
|
+
let timer;
|
|
100
|
+
const budget = new Promise((_, reject) => {
|
|
101
|
+
timer = setTimeout(() => {
|
|
102
|
+
detectorPromise.then((late) => late.destroy(), () => void 0);
|
|
103
|
+
reject(new Error(`detector preload exceeded ${PRELOAD_BUDGET_MS} ms`));
|
|
104
|
+
}, PRELOAD_BUDGET_MS);
|
|
105
|
+
});
|
|
106
|
+
const settled = Promise.race([detectorPromise, budget]).then(
|
|
107
|
+
(detector) => {
|
|
108
|
+
if (entry.detectorPromise !== detectorPromise) {
|
|
109
|
+
detector.destroy();
|
|
110
|
+
return entry.status;
|
|
111
|
+
}
|
|
112
|
+
entry.detector = detector;
|
|
113
|
+
setStatus(entry, "ready");
|
|
114
|
+
return "ready";
|
|
115
|
+
},
|
|
116
|
+
(err) => {
|
|
117
|
+
if (entry.detectorPromise === detectorPromise) {
|
|
118
|
+
entry.detectorPromise = null;
|
|
119
|
+
entry.detector = null;
|
|
120
|
+
setStatus(entry, "failed");
|
|
121
|
+
console.warn("[@sekyro/web] face-detector preload failed:", err);
|
|
122
|
+
}
|
|
123
|
+
return entry.status;
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
const promise = settled.finally(() => clearTimeout(timer));
|
|
127
|
+
entry.promise = promise;
|
|
128
|
+
entry.scheduled?.settle(promise);
|
|
129
|
+
return promise;
|
|
130
|
+
}
|
|
131
|
+
function preloadLivenessAssets(options = {}) {
|
|
132
|
+
if (!canCapture()) return Promise.resolve("idle");
|
|
133
|
+
const base = normalizeBase(options.assetBaseUrl);
|
|
134
|
+
const entry = entryFor(base);
|
|
135
|
+
if (options.respectSaveData === true && saveDataOn()) return Promise.resolve(entry.status);
|
|
136
|
+
if ((entry.status === "loading" || entry.status === "ready") && entry.promise !== null) return entry.promise;
|
|
137
|
+
if (options.idle === true) return entry.scheduled?.promise ?? schedule(entry, base);
|
|
138
|
+
return begin(entry, base);
|
|
139
|
+
}
|
|
140
|
+
function getLivenessAssetsStatus(assetBaseUrl) {
|
|
141
|
+
return registry().get(normalizeBase(assetBaseUrl))?.status ?? "idle";
|
|
142
|
+
}
|
|
143
|
+
function subscribeLivenessAssets(listener, assetBaseUrl) {
|
|
144
|
+
const entry = entryFor(normalizeBase(assetBaseUrl));
|
|
145
|
+
entry.listeners.add(listener);
|
|
146
|
+
return () => {
|
|
147
|
+
entry.listeners.delete(listener);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function releaseLivenessAssets(assetBaseUrl) {
|
|
151
|
+
const entry = registry().get(normalizeBase(assetBaseUrl));
|
|
152
|
+
if (entry === void 0) return;
|
|
153
|
+
entry.scheduled?.settle("idle");
|
|
154
|
+
entry.detector?.destroy();
|
|
155
|
+
entry.detector = null;
|
|
156
|
+
entry.detectorPromise = null;
|
|
157
|
+
entry.promise = null;
|
|
158
|
+
setStatus(entry, "idle");
|
|
159
|
+
}
|
|
160
|
+
function nonOwningView(shared) {
|
|
161
|
+
return {
|
|
162
|
+
get tier() {
|
|
163
|
+
return shared.tier;
|
|
164
|
+
},
|
|
165
|
+
get fallbackReason() {
|
|
166
|
+
return shared.fallbackReason;
|
|
167
|
+
},
|
|
168
|
+
detect: (...args) => shared.detect(...args),
|
|
169
|
+
destroy: () => {
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function preloadedDetectorFactory(assetBaseUrl) {
|
|
174
|
+
const base = normalizeBase(assetBaseUrl);
|
|
175
|
+
const entry = registry().get(base);
|
|
176
|
+
if (entry === void 0 || entry.detector === null && entry.detectorPromise === null && entry.scheduled === null) {
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
return async () => {
|
|
180
|
+
if (entry.scheduled !== null) void begin(entry, base);
|
|
181
|
+
let waitingOn = entry.promise;
|
|
182
|
+
while (entry.detector === null && waitingOn !== null) {
|
|
183
|
+
await waitingOn;
|
|
184
|
+
if (entry.promise === waitingOn) break;
|
|
185
|
+
waitingOn = entry.promise;
|
|
186
|
+
}
|
|
187
|
+
return entry.detector !== null ? nonOwningView(entry.detector) : createDetector({ v3AssetBaseUrl: base });
|
|
188
|
+
};
|
|
189
|
+
}
|
|
11
190
|
|
|
12
191
|
// src/verification/emitter.ts
|
|
13
192
|
function createEmitter() {
|
|
@@ -38,6 +217,83 @@ function createEmitter() {
|
|
|
38
217
|
};
|
|
39
218
|
}
|
|
40
219
|
|
|
220
|
+
// src/capture/wake-lock.ts
|
|
221
|
+
function wakeLockApi() {
|
|
222
|
+
if (typeof navigator === "undefined" || !("wakeLock" in navigator)) return null;
|
|
223
|
+
const api = navigator.wakeLock;
|
|
224
|
+
return typeof api?.request === "function" ? api : null;
|
|
225
|
+
}
|
|
226
|
+
function isVisible() {
|
|
227
|
+
return typeof document === "undefined" || document.visibilityState === "visible";
|
|
228
|
+
}
|
|
229
|
+
function releaseQuietly(sentinel) {
|
|
230
|
+
try {
|
|
231
|
+
void sentinel.release().catch(() => void 0);
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function createScreenWakeLock(options = {}) {
|
|
236
|
+
if (options.enabled === false) return { setActive: () => void 0 };
|
|
237
|
+
let wanted = false;
|
|
238
|
+
let sentinel = null;
|
|
239
|
+
let inFlight = false;
|
|
240
|
+
let listening = false;
|
|
241
|
+
const acquire = () => {
|
|
242
|
+
if (!wanted || inFlight || sentinel !== null && !sentinel.released) return;
|
|
243
|
+
const api = wakeLockApi();
|
|
244
|
+
if (api === null || !isVisible()) return;
|
|
245
|
+
let request;
|
|
246
|
+
try {
|
|
247
|
+
request = api.request("screen");
|
|
248
|
+
} catch {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
inFlight = true;
|
|
252
|
+
request.then(
|
|
253
|
+
(granted) => {
|
|
254
|
+
inFlight = false;
|
|
255
|
+
if (!wanted) {
|
|
256
|
+
releaseQuietly(granted);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
sentinel = granted;
|
|
260
|
+
granted.addEventListener?.("release", () => {
|
|
261
|
+
if (sentinel === granted) sentinel = null;
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
() => {
|
|
265
|
+
inFlight = false;
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
const onVisibilityChange = () => {
|
|
270
|
+
if (isVisible()) acquire();
|
|
271
|
+
};
|
|
272
|
+
const listen = (on) => {
|
|
273
|
+
if (typeof document === "undefined" || on === listening) return;
|
|
274
|
+
listening = on;
|
|
275
|
+
if (on) document.addEventListener("visibilitychange", onVisibilityChange);
|
|
276
|
+
else document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
setActive: (active) => {
|
|
280
|
+
wanted = active;
|
|
281
|
+
if (active) {
|
|
282
|
+
if (wakeLockApi() === null) return;
|
|
283
|
+
listen(true);
|
|
284
|
+
acquire();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
listen(false);
|
|
288
|
+
if (sentinel !== null) {
|
|
289
|
+
const held = sentinel;
|
|
290
|
+
sentinel = null;
|
|
291
|
+
releaseQuietly(held);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
41
297
|
// src/capture/create-liveness-capture.ts
|
|
42
298
|
function mapGuidanceHint(key) {
|
|
43
299
|
switch (key) {
|
|
@@ -101,12 +357,22 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
101
357
|
const now = internals.now ?? (() => performance.now());
|
|
102
358
|
const emitter = createEmitter();
|
|
103
359
|
const degraded = !isMediaRecorderSupported();
|
|
360
|
+
const hostSeams = internals.engineOptions;
|
|
361
|
+
const preloadAware = hostSeams?.createDetector === void 0 && hostSeams?.createWorker === void 0 ? {
|
|
362
|
+
createDetector: () => {
|
|
363
|
+
const warmed = preloadedDetectorFactory(options.assetBaseUrl);
|
|
364
|
+
return warmed !== void 0 ? warmed() : createDetector(
|
|
365
|
+
options.assetBaseUrl !== void 0 ? { v3AssetBaseUrl: options.assetBaseUrl } : {}
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
} : {};
|
|
104
369
|
const engineOptions = {
|
|
105
370
|
video: options.video,
|
|
106
371
|
mode: degraded ? "photo" : "clip",
|
|
107
372
|
...options.facing !== void 0 ? { facingMode: options.facing } : {},
|
|
108
373
|
...options.assetBaseUrl !== void 0 ? { v3AssetBaseUrl: options.assetBaseUrl } : {},
|
|
109
|
-
...internals.engineOptions ?? {}
|
|
374
|
+
...internals.engineOptions ?? {},
|
|
375
|
+
...preloadAware
|
|
110
376
|
};
|
|
111
377
|
const engine = internals.createEngine?.(engineOptions) ?? new CaptureSessionV3(engineOptions);
|
|
112
378
|
let state = "idle";
|
|
@@ -124,8 +390,10 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
124
390
|
frameHeight = options.video.videoHeight;
|
|
125
391
|
}
|
|
126
392
|
};
|
|
393
|
+
const wakeLock = createScreenWakeLock({ enabled: options.keepScreenAwake !== false });
|
|
127
394
|
const setState = (next) => {
|
|
128
395
|
state = next;
|
|
396
|
+
wakeLock.setActive(next === "starting" || next === "gating" || next === "recording");
|
|
129
397
|
emitter.emit("state", next);
|
|
130
398
|
};
|
|
131
399
|
const emitGate = () => emitter.emit("gate", { progress, hint, faceDetected });
|
|
@@ -199,11 +467,12 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
199
467
|
return state;
|
|
200
468
|
},
|
|
201
469
|
on: (event, handler) => emitter.on(event, handler),
|
|
202
|
-
start: () => engine.start(),
|
|
470
|
+
start: (startOptions) => engine.start(startOptions?.facing !== void 0 ? { facingMode: startOptions.facing } : {}),
|
|
203
471
|
retry: () => engine.retry(),
|
|
204
472
|
switchCamera: () => engine.switchCamera(),
|
|
205
473
|
destroy: () => {
|
|
206
474
|
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
475
|
+
wakeLock.setActive(false);
|
|
207
476
|
engine.destroy();
|
|
208
477
|
emitter.clear();
|
|
209
478
|
}
|
|
@@ -671,7 +940,7 @@ function createVerificationInternal(options, internals) {
|
|
|
671
940
|
}
|
|
672
941
|
emitter.emit("result", result);
|
|
673
942
|
};
|
|
674
|
-
let
|
|
943
|
+
let registry2 = null;
|
|
675
944
|
let registryFailed = false;
|
|
676
945
|
let registryUnbound = false;
|
|
677
946
|
const submitRegistry = () => runSubmission(
|
|
@@ -706,8 +975,8 @@ function createVerificationInternal(options, internals) {
|
|
|
706
975
|
if (view !== null && isTerminalView(view)) emitResult(mapSessionResult(view, { autoRetry }));
|
|
707
976
|
};
|
|
708
977
|
const startRegistry = () => {
|
|
709
|
-
|
|
710
|
-
return
|
|
978
|
+
registry2 ?? (registry2 = runRegistry());
|
|
979
|
+
return registry2;
|
|
711
980
|
};
|
|
712
981
|
const getAttestation = async () => {
|
|
713
982
|
const current = await ensureView();
|
|
@@ -822,7 +1091,7 @@ function createVerificationInternal(options, internals) {
|
|
|
822
1091
|
return inner.state;
|
|
823
1092
|
},
|
|
824
1093
|
on: (event, handler) => inner.on(event, handler),
|
|
825
|
-
start: () => inner.start(),
|
|
1094
|
+
start: (startOptions) => inner.start(startOptions),
|
|
826
1095
|
retry: () => inner.retry(),
|
|
827
1096
|
switchCamera: () => inner.switchCamera(),
|
|
828
1097
|
destroy: () => {
|
|
@@ -836,6 +1105,16 @@ function createVerificationInternal(options, internals) {
|
|
|
836
1105
|
consent: () => ensureConsent(),
|
|
837
1106
|
startRegistry,
|
|
838
1107
|
liveness,
|
|
1108
|
+
voiceClips: async () => {
|
|
1109
|
+
if (destroyed) return null;
|
|
1110
|
+
try {
|
|
1111
|
+
const current = await ensureView();
|
|
1112
|
+
if (destroyed) return null;
|
|
1113
|
+
return current.voice ?? null;
|
|
1114
|
+
} catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
839
1118
|
on: (event, handler) => emitter.on(event, handler),
|
|
840
1119
|
destroy: () => {
|
|
841
1120
|
destroyed = true;
|
|
@@ -851,6 +1130,15 @@ function createVerificationInternal(options, internals) {
|
|
|
851
1130
|
}
|
|
852
1131
|
|
|
853
1132
|
export {
|
|
1133
|
+
PRELOAD_BLAZE_INIT_TIMEOUT_MS,
|
|
1134
|
+
PRELOAD_BUDGET_MS,
|
|
1135
|
+
setPreloadLoaderForTests,
|
|
1136
|
+
resetLivenessAssetsForTests,
|
|
1137
|
+
preloadLivenessAssets,
|
|
1138
|
+
getLivenessAssetsStatus,
|
|
1139
|
+
subscribeLivenessAssets,
|
|
1140
|
+
releaseLivenessAssets,
|
|
1141
|
+
preloadedDetectorFactory,
|
|
854
1142
|
mapGuidanceHint,
|
|
855
1143
|
mapEngineState,
|
|
856
1144
|
mapErrorKind,
|
|
@@ -882,4 +1170,4 @@ export {
|
|
|
882
1170
|
createVerification,
|
|
883
1171
|
createVerificationInternal
|
|
884
1172
|
};
|
|
885
|
-
//# sourceMappingURL=chunk-
|
|
1173
|
+
//# sourceMappingURL=chunk-R3SJQHYU.js.map
|