@sekyro/web 0.1.0-rc.4 → 0.1.0-rc.5

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @sekyro/web
2
2
 
3
+ ## 0.1.0-rc.5
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+ - **Status store.** `getLivenessAssetsStatus(assetBaseUrl?)` and `subscribeLivenessAssets(listener, assetBaseUrl?)` (the `useSyncExternalStore` contract: no emit on subscribe) drive a Start button's loading / retry state.
13
+ - **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.
14
+ - **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.
15
+ - 66522c3: The liveness capture now keeps the screen awake while the camera is live.
16
+
17
+ - **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).
18
+ - **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.
19
+ - **Opt out:** `keepScreenAwake: false` on `createLivenessCapture({ … })` / `verification.liveness({ … })` never requests the lock.
20
+ - **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.
21
+ - `@sekyro/react`'s `<LivenessCapture>` gets this automatically once it runs on this `@sekyro/web` version.
22
+
3
23
  ## 0.1.0-rc.4
4
24
 
5
25
  ### 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 V3TelemetryEvent as A, V3_GATE_DEFAULTS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, VIDEO_BPS as H, buildLivenessV2FormData as I, cameraPreflight as J, KEYFRAME_CANDIDATES as K, classifyDevice as L, MAX_CLIP_BYTES as M, extForMime as N, isClipTooLarge as O, isMediaRecorderSupported as P, mapGetUserMediaError as Q, pickMimeType as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, probeDevice as U, type V3ChecksPayload as V, 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 CaptureState as h, type CaptureTiming as i, ClipError as j, type ClipErrorKind as k, ClipRecorder as l, type ClipResult as m, DEVICE_PROFILES as n, type DeviceClass as o, type DeviceProbe as p, type DeviceProfile as q, DeviceProfiler as r, type EscalationHook as s, KEYFRAME_WINDOW_MS as t, MIME_CANDIDATES as u, type V3GateFactor as v, type V3GateQuality as w, type V3GateThresholds as x, type V3GateTick as y, type V3GateTickInput as z };
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 V3TelemetryEvent as A, V3_GATE_DEFAULTS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, VIDEO_BPS as H, buildLivenessV2FormData as I, cameraPreflight as J, KEYFRAME_CANDIDATES as K, classifyDevice as L, MAX_CLIP_BYTES as M, extForMime as N, isClipTooLarge as O, isMediaRecorderSupported as P, mapGetUserMediaError as Q, pickMimeType as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, probeDevice as U, type V3ChecksPayload as V, 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 CaptureState as h, type CaptureTiming as i, ClipError as j, type ClipErrorKind as k, ClipRecorder as l, type ClipResult as m, DEVICE_PROFILES as n, type DeviceClass as o, type DeviceProbe as p, type DeviceProfile as q, DeviceProfiler as r, type EscalationHook as s, KEYFRAME_WINDOW_MS as t, MIME_CANDIDATES as u, type V3GateFactor as v, type V3GateQuality as w, type V3GateThresholds as x, type V3GateTick as y, type V3GateTickInput as z };
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-EWRNPKTR.js";
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 registry = null;
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
- registry ?? (registry = runRegistry());
710
- return registry;
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: () => {
@@ -851,6 +1120,15 @@ function createVerificationInternal(options, internals) {
851
1120
  }
852
1121
 
853
1122
  export {
1123
+ PRELOAD_BLAZE_INIT_TIMEOUT_MS,
1124
+ PRELOAD_BUDGET_MS,
1125
+ setPreloadLoaderForTests,
1126
+ resetLivenessAssetsForTests,
1127
+ preloadLivenessAssets,
1128
+ getLivenessAssetsStatus,
1129
+ subscribeLivenessAssets,
1130
+ releaseLivenessAssets,
1131
+ preloadedDetectorFactory,
854
1132
  mapGuidanceHint,
855
1133
  mapEngineState,
856
1134
  mapErrorKind,
@@ -882,4 +1160,4 @@ export {
882
1160
  createVerification,
883
1161
  createVerificationInternal
884
1162
  };
885
- //# sourceMappingURL=chunk-TBANUT3X.js.map
1163
+ //# sourceMappingURL=chunk-SGDXLAXR.js.map