@sekyro/web 0.1.0-rc.6 → 0.1.0-rc.8

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +25 -16
  3. package/README.md +6 -8
  4. package/THIRD-PARTY-NOTICES.txt +154 -0
  5. package/dist/background-monitor-A7FKJOAG.js +1 -0
  6. package/dist/capture-session-doc-YBE3UMJJ.js +1 -0
  7. package/dist/{capture-session-v3-Okq0EQHG.d.ts → capture-session-v3-CQK-G-qX.d.ts} +72 -3
  8. package/dist/{capture-session-v3-UQU_WjO2.d.cts → capture-session-v3-Dvob-uH2.d.cts} +72 -3
  9. package/dist/chunk-4RS2YIL4.js +1 -0
  10. package/dist/chunk-7V2M6Y2W.js +1 -0
  11. package/dist/chunk-7ZCZY3ZP.js +1 -0
  12. package/dist/chunk-A3FUGHAH.js +2 -0
  13. package/dist/chunk-BPE5NSQ2.js +1 -0
  14. package/dist/chunk-CJHMFPL6.js +1 -0
  15. package/dist/chunk-DFTCAWMP.js +1 -0
  16. package/dist/chunk-DK5YPBDQ.js +1 -0
  17. package/dist/chunk-DQWW4OCK.js +1 -0
  18. package/dist/chunk-FHJ7HO3M.js +1 -0
  19. package/dist/chunk-FPXL63KZ.js +1 -0
  20. package/dist/chunk-G6LKLN4U.js +1 -0
  21. package/dist/chunk-GI3UFGRQ.js +1 -0
  22. package/dist/chunk-LQ7C44JG.js +1 -0
  23. package/dist/chunk-M43ET4NJ.js +1 -0
  24. package/dist/chunk-MXWLCDFX.js +1 -0
  25. package/dist/chunk-MY6EXEL2.js +1 -0
  26. package/dist/chunk-O5WKNZ2C.js +1 -0
  27. package/dist/chunk-OKWG3PVH.js +1 -0
  28. package/dist/chunk-POIQGEZM.js +1 -0
  29. package/dist/chunk-RI3E7GBN.js +1 -0
  30. package/dist/chunk-RNRFHUJA.js +1 -0
  31. package/dist/chunk-RVHAKRAI.js +1 -0
  32. package/dist/chunk-SMKCBUJL.js +1 -0
  33. package/dist/chunk-SXZCL5NO.js +1 -0
  34. package/dist/chunk-TDLIXFDT.js +1 -0
  35. package/dist/chunk-XLN5OT25.js +1 -0
  36. package/dist/chunk-YDQBVWXG.js +1 -0
  37. package/dist/chunk-YPG7CB4O.js +1 -0
  38. package/dist/client/index.cjs +1 -357
  39. package/dist/client/index.d.cts +13 -3
  40. package/dist/client/index.d.ts +13 -3
  41. package/dist/client/index.js +1 -15
  42. package/dist/core/index.cjs +1 -1639
  43. package/dist/core/index.d.cts +3 -3
  44. package/dist/core/index.d.ts +3 -3
  45. package/dist/core/index.js +1 -139
  46. package/dist/core/ocr/worker.bundle.js +42 -0
  47. package/dist/core/ocr/worker.cjs +42 -0
  48. package/dist/core/ocr/worker.d.cts +84 -0
  49. package/dist/core/ocr/worker.d.ts +84 -0
  50. package/dist/core/ocr/worker.js +41 -0
  51. package/dist/core/v3/detect-worker.cjs +1 -392
  52. package/dist/core/v3/detect-worker.d.cts +1 -1
  53. package/dist/core/v3/detect-worker.d.ts +1 -1
  54. package/dist/core/v3/detect-worker.js +1 -13
  55. package/dist/create-document-capture-T4APHJMY.js +1 -0
  56. package/dist/create-portrait-capture-B57GE5FU.js +1 -0
  57. package/dist/create-verification-C0AVIcVW.d.ts +2787 -0
  58. package/dist/create-verification-DoFpgzok.d.cts +2787 -0
  59. package/dist/{detect-worker-CKXHlXx9.d.cts → detect-worker-DclNxkZT.d.cts} +6 -0
  60. package/dist/{detect-worker-CKXHlXx9.d.ts → detect-worker-DclNxkZT.d.ts} +6 -0
  61. package/dist/detector-scanic-YCDNLCGV.js +1 -0
  62. package/dist/icao-gate-D8qm1O2t.d.cts +1655 -0
  63. package/dist/icao-gate-D8qm1O2t.d.ts +1655 -0
  64. package/dist/icao-gate-PWBMKRGC.js +1 -0
  65. package/dist/icao-tuning-4GGVX6B4.js +1 -0
  66. package/dist/index.cjs +1 -3030
  67. package/dist/index.d.cts +52 -6
  68. package/dist/index.d.ts +52 -6
  69. package/dist/index.js +1 -85
  70. package/dist/internal/index.cjs +1 -4439
  71. package/dist/internal/index.d.cts +352 -312
  72. package/dist/internal/index.d.ts +352 -312
  73. package/dist/internal/index.js +1 -657
  74. package/dist/keyframe-rank-YAJLYJYF.js +1 -0
  75. package/dist/landmarker-KB7LVXAU.js +1 -0
  76. package/dist/mrz-band-VYHOTDHY.js +1 -0
  77. package/dist/plan-runner-UBK4B4CG.js +1 -0
  78. package/dist/popup/index.cjs +1 -76
  79. package/dist/popup/index.js +1 -11
  80. package/dist/scanic-ml-stub-KW7IZHUT.js +1 -0
  81. package/dist/segmenter-USLNBX65.js +1 -0
  82. package/dist/turnstile-FATNZ4AG.js +1 -0
  83. package/package.json +25 -10
  84. package/sbom.cdx.json +195 -0
  85. package/dist/chunk-MWNFSNMP.js +0 -52
  86. package/dist/chunk-MWNFSNMP.js.map +0 -1
  87. package/dist/chunk-NILNKLK6.js +0 -50
  88. package/dist/chunk-NILNKLK6.js.map +0 -1
  89. package/dist/chunk-NWF46H7K.js +0 -359
  90. package/dist/chunk-NWF46H7K.js.map +0 -1
  91. package/dist/chunk-R3SJQHYU.js +0 -1173
  92. package/dist/chunk-R3SJQHYU.js.map +0 -1
  93. package/dist/chunk-TG5IE552.js +0 -1128
  94. package/dist/chunk-TG5IE552.js.map +0 -1
  95. package/dist/chunk-WY6U7AIK.js +0 -193
  96. package/dist/chunk-WY6U7AIK.js.map +0 -1
  97. package/dist/chunk-YVFF4Q4G.js +0 -327
  98. package/dist/chunk-YVFF4Q4G.js.map +0 -1
  99. package/dist/client/index.cjs.map +0 -1
  100. package/dist/client/index.js.map +0 -1
  101. package/dist/core/index.cjs.map +0 -1
  102. package/dist/core/index.js.map +0 -1
  103. package/dist/core/v3/detect-worker.cjs.map +0 -1
  104. package/dist/core/v3/detect-worker.js.map +0 -1
  105. package/dist/create-verification-BKc9NFQj.d.cts +0 -455
  106. package/dist/create-verification-DjcMZHAQ.d.ts +0 -455
  107. package/dist/detector-scanic-4C6IEW2K.js +0 -787
  108. package/dist/detector-scanic-4C6IEW2K.js.map +0 -1
  109. package/dist/hosted-client-DJXq_CB9.d.cts +0 -449
  110. package/dist/hosted-client-DJXq_CB9.d.ts +0 -449
  111. package/dist/index.cjs.map +0 -1
  112. package/dist/index.js.map +0 -1
  113. package/dist/internal/index.cjs.map +0 -1
  114. package/dist/internal/index.js.map +0 -1
  115. package/dist/popup/index.cjs.map +0 -1
  116. package/dist/popup/index.js.map +0 -1
  117. package/dist/scanic-ml-stub-S2OZ6HEF.js +0 -10
  118. package/dist/scanic-ml-stub-S2OZ6HEF.js.map +0 -1
@@ -1,1639 +1 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/core/index.ts
31
- var core_exports = {};
32
- __export(core_exports, {
33
- CENTERED_FACTOR: () => CENTERED_FACTOR,
34
- CIRCLE_RADIUS_FACTOR: () => CIRCLE_RADIUS_FACTOR,
35
- CLIP_MS: () => CLIP_MS,
36
- CaptureSessionV3: () => CaptureSessionV3,
37
- ClipError: () => ClipError,
38
- ClipRecorder: () => ClipRecorder,
39
- DEFAULT_CLIP_MIME: () => DEFAULT_CLIP_MIME,
40
- DEFAULT_TIER_ORDER: () => DEFAULT_TIER_ORDER,
41
- DEFAULT_V3_ASSET_BASE_URL: () => DEFAULT_V3_ASSET_BASE_URL,
42
- DEVICE_PROFILES: () => DEVICE_PROFILES,
43
- DetectScheduler: () => DetectScheduler,
44
- DetectorLadderError: () => DetectorLadderError,
45
- DeviceProfiler: () => DeviceProfiler,
46
- EXPOSURE_BAND: () => EXPOSURE_BAND,
47
- FaceGateV3: () => FaceGateV3,
48
- IN_CIRCLE_FACTOR: () => IN_CIRCLE_FACTOR,
49
- KEYFRAME_CANDIDATES: () => KEYFRAME_CANDIDATES,
50
- KEYFRAME_WINDOW_MS: () => KEYFRAME_WINDOW_MS,
51
- MAX_CLIP_BYTES: () => MAX_CLIP_BYTES,
52
- MIME_CANDIDATES: () => MIME_CANDIDATES,
53
- SHARP_REF: () => SHARP_REF,
54
- STOP_GRACE_MS: () => STOP_GRACE_MS,
55
- TIMESLICE_MS: () => TIMESLICE_MS,
56
- V3_GATE_DEFAULTS: () => V3_GATE_DEFAULTS,
57
- VIDEO_BPS: () => VIDEO_BPS,
58
- buildCaptureConstraints: () => buildCaptureConstraints,
59
- buildLivenessV2FormData: () => buildLivenessV2FormData,
60
- cameraPreflight: () => cameraPreflight,
61
- captureMirroredKeyframe: () => captureMirroredKeyframe,
62
- classifyDevice: () => classifyDevice,
63
- createDetector: () => createDetector,
64
- detectIsMobile: () => detectIsMobile,
65
- errorCopy: () => errorCopy,
66
- exposure: () => exposure,
67
- extForMime: () => extForMime,
68
- intrinsicSize: () => intrinsicSize,
69
- isClipTooLarge: () => isClipTooLarge,
70
- isMediaRecorderSupported: () => isMediaRecorderSupported,
71
- mapGetUserMediaError: () => mapGetUserMediaError,
72
- meanLuma: () => meanLuma,
73
- pickMimeType: () => pickMimeType,
74
- probeDevice: () => probeDevice,
75
- sharpness: () => sharpness,
76
- toStreamPixels: () => toStreamPixels,
77
- uiCopy: () => uiCopy
78
- });
79
- module.exports = __toCommonJS(core_exports);
80
-
81
- // src/core/media.ts
82
- var MIME_CANDIDATES = [
83
- "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
84
- "video/mp4;codecs=avc1.42E01E",
85
- "video/mp4",
86
- "video/webm;codecs=vp9,opus",
87
- "video/webm;codecs=vp9",
88
- "video/webm;codecs=vp8,opus",
89
- "video/webm;codecs=vp8",
90
- "video/webm"
91
- ];
92
- function pickMimeType() {
93
- const MR = globalThis.MediaRecorder;
94
- if (!MR || typeof MR.isTypeSupported !== "function") return "";
95
- return MIME_CANDIDATES.find((t) => MR.isTypeSupported(t)) ?? "";
96
- }
97
- var DEFAULT_CLIP_MIME = "video/webm";
98
- function mapGetUserMediaError(err) {
99
- if (err instanceof TypeError) return "insecure";
100
- const name = err?.name ?? "";
101
- switch (name) {
102
- case "NotAllowedError":
103
- case "PermissionDeniedError":
104
- return "permission";
105
- case "NotFoundError":
106
- case "DevicesNotFoundError":
107
- return "notfound";
108
- case "NotReadableError":
109
- case "TrackStartError":
110
- return "inuse";
111
- case "OverconstrainedError":
112
- case "ConstraintNotSatisfiedError":
113
- return "overconstrained";
114
- default:
115
- return "generic";
116
- }
117
- }
118
- function cameraPreflight() {
119
- const w = globalThis;
120
- if (typeof w.window !== "undefined" && w.window.isSecureContext === false)
121
- return "INSECURE_CONTEXT";
122
- const md = w.navigator?.mediaDevices;
123
- if (!md || typeof md.getUserMedia !== "function") return "UNSUPPORTED";
124
- return "OK";
125
- }
126
- function isMediaRecorderSupported() {
127
- const MR = globalThis.MediaRecorder;
128
- return typeof MR !== "undefined" && typeof MR.isTypeSupported === "function";
129
- }
130
- var MAX_CLIP_BYTES = 8 * 1024 * 1024;
131
- function isClipTooLarge(blob) {
132
- return blob.size > MAX_CLIP_BYTES;
133
- }
134
- function extForMime(mime) {
135
- const m = (mime || "").toLowerCase();
136
- if (m.startsWith("image/jpeg")) return "jpg";
137
- if (m.startsWith("video/mp4")) return "mp4";
138
- if (m.startsWith("video/webm")) return "webm";
139
- return "webm";
140
- }
141
- function buildLivenessV2FormData(args) {
142
- const { token, clip, keyframe } = args;
143
- const ts = args.ts ?? Date.now();
144
- const clipType = clip.type || DEFAULT_CLIP_MIME;
145
- const kfType = keyframe.type || "image/jpeg";
146
- const fd = new FormData();
147
- fd.append("token", token);
148
- fd.append("clip", new File([clip], `liveness-${ts}.${extForMime(clipType)}`, { type: clipType }));
149
- fd.append("keyframe", new File([keyframe], `keyframe-${ts}.${extForMime(kfType)}`, { type: kfType }));
150
- return fd;
151
- }
152
-
153
- // src/core/keyframe.ts
154
- function captureMirroredKeyframe(video, canvas, quality = 0.92) {
155
- return new Promise((resolve, reject) => {
156
- const w = video.videoWidth;
157
- const h = video.videoHeight;
158
- if (!w || !h) {
159
- reject(new Error("video-not-ready"));
160
- return;
161
- }
162
- const ctx = canvas.getContext("2d");
163
- if (!ctx) {
164
- reject(new Error("no-2d-context"));
165
- return;
166
- }
167
- canvas.width = w;
168
- canvas.height = h;
169
- ctx.scale(-1, 1);
170
- ctx.drawImage(video, -w, 0);
171
- canvas.toBlob(
172
- (blob) => blob ? resolve(blob) : reject(new Error("empty-keyframe")),
173
- "image/jpeg",
174
- quality
175
- );
176
- });
177
- }
178
-
179
- // src/core/messages.ts
180
- var errorCopy = {
181
- permission: {
182
- key: "fcv2.err.permission",
183
- fr: "Autorisez la cam\xE9ra dans les r\xE9glages du navigateur (ic\xF4ne cam\xE9ra dans la barre d\u2019adresse), puis r\xE9essayez."
184
- },
185
- notfound: {
186
- key: "fcv2.err.notfound",
187
- fr: "Aucune cam\xE9ra d\xE9tect\xE9e. Branchez ou activez une cam\xE9ra, puis r\xE9essayez."
188
- },
189
- inuse: {
190
- key: "fcv2.err.inuse",
191
- fr: "La cam\xE9ra est utilis\xE9e par une autre application. Fermez-la puis r\xE9essayez."
192
- },
193
- overconstrained: {
194
- key: "fcv2.err.overconstrained",
195
- fr: "La cam\xE9ra ne prend pas en charge les r\xE9glages demand\xE9s. Nouvel essai en cours\u2026"
196
- },
197
- insecure: {
198
- key: "fcv2.err.insecure",
199
- fr: "La cam\xE9ra n\xE9cessite une connexion s\xE9curis\xE9e (HTTPS). Ouvrez la page en HTTPS."
200
- },
201
- unsupported: {
202
- key: "fcv2.err.unsupported",
203
- fr: "Votre navigateur ne prend pas en charge l\u2019enregistrement vid\xE9o. Utilisez un navigateur r\xE9cent."
204
- },
205
- generic: {
206
- key: "fcv2.err.generic",
207
- fr: "Cam\xE9ra indisponible. R\xE9essayez ou contactez le support."
208
- }
209
- };
210
- var uiCopy = {
211
- recording: { key: "fcv2.recording", fr: "Enregistrement en cours\u2026" },
212
- uploading: { key: "fcv2.uploading", fr: "Envoi en cours\u2026" },
213
- clipTooLarge: {
214
- key: "fcv2.err.cliptoolarge",
215
- fr: "La vid\xE9o est trop volumineuse. R\xE9essayez dans un endroit mieux \xE9clair\xE9."
216
- },
217
- retry: { key: "fcv2.retry", fr: "R\xE9essayer" },
218
- listen: { key: "fcv2.listen", fr: "\xC9couter" }
219
- };
220
-
221
- // src/core/clip-recorder.ts
222
- var CLIP_MS = 2500;
223
- var TIMESLICE_MS = 1e3;
224
- var STOP_GRACE_MS = 1500;
225
- var VIDEO_BPS = 25e5;
226
- var ClipError = class extends Error {
227
- constructor(kind, msg) {
228
- super(msg ?? kind);
229
- this.kind = kind;
230
- }
231
- };
232
- var ClipRecorder = class {
233
- constructor() {
234
- this.recorder = null;
235
- this.stream = null;
236
- // own field so teardown can stop tracks (REVIEW FIX 8)
237
- this.chunks = [];
238
- this.bytes = 0;
239
- this.hardStopTimer = null;
240
- this.graceTimer = null;
241
- this.settled = false;
242
- }
243
- clearTimers() {
244
- if (this.hardStopTimer) clearTimeout(this.hardStopTimer);
245
- if (this.graceTimer) clearTimeout(this.graceTimer);
246
- this.hardStopTimer = null;
247
- this.graceTimer = null;
248
- }
249
- /** Records CLIP_MS of the stream. Resolves with the clip blob + its container mime;
250
- * rejects with ClipError. Behavior contract is the v1 hook's `start`, verbatim
251
- * (iOS workarounds included).
252
- *
253
- * ONE RECORDING AT A TIME: chunk/byte/timer state is shared per instance, so a
254
- * second record() while one is in flight rejects ClipError("generic") instead of
255
- * corrupting the in-flight clip. (v2 guard — v1's hook was gated by its owning
256
- * component; a class API invites reuse. Zero impact on clip bytes.)
257
- *
258
- * `onStopped` (optional) fires ONCE the moment the CLIP_MS recording window
259
- * elapses (the hard-stop), BEFORE finalize — which on iOS can lag up to
260
- * STOP_GRACE_MS while onstop flushes the last chunk. It lets the host freeze
261
- * the display on the already-captured keyframe the instant recording ends,
262
- * instead of waiting for the blob. PURE NOTIFICATION — it does not touch
263
- * timers, chunks, or the finalize path, so it is calibration-neutral (no
264
- * effect on clip bytes). Never fires on a rejected capture (error /
265
- * cliptoolarge settle before the hard-stop and clear the timer). */
266
- record(stream, onStopped) {
267
- if (this.recorder && !this.settled) {
268
- return Promise.reject(
269
- new ClipError(
270
- "generic",
271
- "record() called while a recording is in flight \u2014 ClipRecorder handles one recording at a time"
272
- )
273
- );
274
- }
275
- this.recorder = null;
276
- this.clearTimers();
277
- this.chunks = [];
278
- this.bytes = 0;
279
- this.settled = false;
280
- this.stream = stream;
281
- const picked = pickMimeType();
282
- return new Promise((resolve, reject) => {
283
- let recorder;
284
- try {
285
- recorder = new MediaRecorder(
286
- stream,
287
- picked ? { mimeType: picked, videoBitsPerSecond: VIDEO_BPS } : { videoBitsPerSecond: VIDEO_BPS }
288
- );
289
- } catch {
290
- try {
291
- recorder = new MediaRecorder(stream);
292
- } catch {
293
- reject(new ClipError("unsupported"));
294
- return;
295
- }
296
- }
297
- this.recorder = recorder;
298
- const settle = (fn) => {
299
- if (this.settled) return;
300
- this.settled = true;
301
- this.clearTimers();
302
- fn();
303
- };
304
- const finalize = () => settle(() => {
305
- const type = recorder.mimeType || picked || DEFAULT_CLIP_MIME;
306
- const blob = new Blob(this.chunks, { type });
307
- if (isClipTooLarge(blob)) {
308
- reject(new ClipError("cliptoolarge"));
309
- return;
310
- }
311
- resolve({ blob, mimeType: type });
312
- });
313
- recorder.ondataavailable = (e) => {
314
- if (e.data && e.data.size) {
315
- this.chunks.push(e.data);
316
- this.bytes += e.data.size;
317
- if (this.bytes > MAX_CLIP_BYTES) {
318
- settle(() => {
319
- try {
320
- if (recorder.state !== "inactive") recorder.stop();
321
- } catch {
322
- }
323
- reject(new ClipError("cliptoolarge"));
324
- });
325
- }
326
- }
327
- };
328
- recorder.onstop = finalize;
329
- recorder.onerror = () => settle(() => reject(new ClipError("generic")));
330
- try {
331
- recorder.start(TIMESLICE_MS);
332
- } catch {
333
- settle(() => reject(new ClipError("generic")));
334
- return;
335
- }
336
- this.hardStopTimer = setTimeout(() => {
337
- if (!this.settled) onStopped?.();
338
- if (recorder.state !== "inactive") {
339
- try {
340
- recorder.stop();
341
- } catch {
342
- finalize();
343
- }
344
- this.graceTimer = setTimeout(finalize, STOP_GRACE_MS);
345
- } else {
346
- finalize();
347
- }
348
- }, CLIP_MS);
349
- });
350
- }
351
- /** Idempotent teardown: stop recorder + ALL tracks (v1 `stopAndCleanup` + the unmount
352
- * effect). Optional `stream` override mirrors v1's `stopAndCleanup(stream?)`.
353
- *
354
- * MID-RECORDING SEMANTICS (verbatim v1): calling stop() while a record() is in
355
- * flight may LATER resolve the pending promise with a TRUNCATED clip (recorder.stop()
356
- * fires onstop → finalize from whatever chunks arrived so far), or — because this
357
- * method clears the hard-stop/grace timers — never settle it at all if onstop never
358
- * fires (the iOS grace path has been torn down). Callers MUST NOT treat a resolution
359
- * arriving after stop() as a full CLIP_MS clip, and must not await a record()
360
- * promise past stop() without their own timeout. (Task 4's CaptureSession owns
361
- * sequencing on top of this contract.) */
362
- stop(stream) {
363
- this.clearTimers();
364
- const rec = this.recorder;
365
- if (rec && rec.state !== "inactive") {
366
- try {
367
- rec.stop();
368
- } catch {
369
- }
370
- }
371
- this.recorder = null;
372
- const s = stream ?? this.stream;
373
- s?.getTracks().forEach((t) => t.stop());
374
- this.stream = null;
375
- }
376
- };
377
-
378
- // src/core/capture-constraints.ts
379
- function buildCaptureConstraints(isMobile, facingMode) {
380
- void isMobile;
381
- return {
382
- video: {
383
- width: { ideal: 1280, max: 1280 },
384
- height: { ideal: 720, max: 720 },
385
- facingMode,
386
- frameRate: { ideal: 24, max: 30 }
387
- },
388
- audio: false
389
- };
390
- }
391
- function detectIsMobile() {
392
- if (typeof window === "undefined" || typeof navigator === "undefined") return false;
393
- return window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
394
- String(navigator.userAgent ?? "")
395
- );
396
- }
397
- var CIRCLE_RADIUS_FACTOR = 0.45;
398
- var IN_CIRCLE_FACTOR = 0.9;
399
- var CENTERED_FACTOR = 0.4;
400
-
401
- // src/core/v3/detector-blaze.ts
402
- var BLAZE_INIT_TIMEOUT_MS = 1e4;
403
- var BlazeDetector = class _BlazeDetector {
404
- constructor(inner, fallbackReason) {
405
- this.tier = "blaze";
406
- this.inner = inner;
407
- if (fallbackReason !== void 0) this.fallbackReason = fallbackReason;
408
- }
409
- static async create(assetBaseUrl, fallbackReason) {
410
- const vision = await import("@mediapipe/tasks-vision");
411
- const fileset = await vision.FilesetResolver.forVisionTasks(assetBaseUrl);
412
- const inner = await vision.FaceDetector.createFromOptions(fileset, {
413
- baseOptions: {
414
- modelAssetPath: `${assetBaseUrl}/blaze_face_short_range.tflite`,
415
- // CPU delegate — the iOS-Safari-safe path (see header). NEVER 'GPU'.
416
- delegate: "CPU"
417
- },
418
- runningMode: "IMAGE"
419
- });
420
- return new _BlazeDetector(inner, fallbackReason);
421
- }
422
- async detect(source, sourceWidth, sourceHeight) {
423
- let detections;
424
- try {
425
- detections = this.inner.detect(source).detections;
426
- } catch {
427
- return null;
428
- }
429
- const best = pickMostConfident(detections);
430
- if (best?.boundingBox === void 0) return null;
431
- const size = intrinsicSize(source);
432
- const eyes = extractEyes(best.keypoints, size);
433
- const observation = {
434
- box: {
435
- x: best.boundingBox.originX,
436
- y: best.boundingBox.originY,
437
- width: best.boundingBox.width,
438
- height: best.boundingBox.height
439
- },
440
- ...eyes !== void 0 ? { eyes } : {},
441
- score: best.categories[0]?.score ?? 0
442
- };
443
- return toStreamPixels(observation, source, sourceWidth, sourceHeight);
444
- }
445
- destroy() {
446
- try {
447
- this.inner.close();
448
- } catch {
449
- }
450
- }
451
- };
452
- function pickMostConfident(detections) {
453
- let best;
454
- let bestScore = -1;
455
- for (const detection of detections) {
456
- const score = detection.categories[0]?.score ?? 0;
457
- if (score > bestScore) {
458
- best = detection;
459
- bestScore = score;
460
- }
461
- }
462
- return best;
463
- }
464
- function extractEyes(keypoints, size) {
465
- const a = keypoints?.[0];
466
- const b = keypoints?.[1];
467
- if (a === void 0 || b === void 0) return void 0;
468
- const toSource = (p) => ({ x: p.x * size.width, y: p.y * size.height });
469
- const [pa, pb] = [toSource(a), toSource(b)];
470
- return pa.x <= pb.x ? { left: pa, right: pb } : { left: pb, right: pa };
471
- }
472
-
473
- // src/core/v3/detector-native.ts
474
- function smokeSource() {
475
- if (typeof OffscreenCanvas === "function") return new OffscreenCanvas(2, 2);
476
- const canvas = document.createElement("canvas");
477
- canvas.width = 2;
478
- canvas.height = 2;
479
- return canvas;
480
- }
481
- async function probeNativeDetector() {
482
- const ctor = globalThis.FaceDetector;
483
- if (typeof ctor !== "function") return { ok: false, reason: "unsupported" };
484
- try {
485
- const instance = new ctor({ fastMode: true, maxDetectedFaces: 1 });
486
- await instance.detect(smokeSource());
487
- return { ok: true, instance };
488
- } catch (err) {
489
- return { ok: false, reason: `smoke_failed:${err instanceof Error ? err.message : String(err)}` };
490
- }
491
- }
492
- var NativeDetector = class {
493
- constructor(inner, fallbackReason) {
494
- this.tier = "native";
495
- this.inner = inner;
496
- if (fallbackReason !== void 0) this.fallbackReason = fallbackReason;
497
- }
498
- async detect(source, sourceWidth, sourceHeight) {
499
- let faces;
500
- try {
501
- faces = await this.inner.detect(source);
502
- } catch {
503
- return null;
504
- }
505
- const face = faces[0];
506
- if (face === void 0) return null;
507
- const eyes = extractEyes2(face.landmarks);
508
- const observation = {
509
- box: {
510
- x: face.boundingBox.x,
511
- y: face.boundingBox.y,
512
- width: face.boundingBox.width,
513
- height: face.boundingBox.height
514
- },
515
- ...eyes !== void 0 ? { eyes } : {},
516
- // The Shape Detection API exposes no confidence — the smoke-tested
517
- // native tier treats any detection as confident.
518
- score: 1
519
- };
520
- return toStreamPixels(observation, source, sourceWidth, sourceHeight);
521
- }
522
- destroy() {
523
- }
524
- };
525
- function extractEyes2(landmarks) {
526
- if (landmarks === void 0) return void 0;
527
- const eyePoints = landmarks.filter((landmark) => landmark.type === "eye").map((landmark) => landmark.locations[0]).filter((point) => point !== void 0);
528
- const [a, b] = eyePoints;
529
- if (a === void 0 || b === void 0) return void 0;
530
- return a.x <= b.x ? { left: a, right: b } : { left: b, right: a };
531
- }
532
-
533
- // src/core/v3/detector.ts
534
- var DEFAULT_V3_ASSET_BASE_URL = "/models-v3";
535
- var DEFAULT_TIER_ORDER = ["native", "blaze"];
536
- var DetectorLadderError = class extends Error {
537
- constructor(fallbackReason) {
538
- super(`no detector tier available (${fallbackReason})`);
539
- this.name = "DetectorLadderError";
540
- this.fallbackReason = fallbackReason;
541
- }
542
- };
543
- function intrinsicSize(source) {
544
- const candidate = source;
545
- if (typeof candidate.videoWidth === "number" && candidate.videoWidth > 0) {
546
- return { width: candidate.videoWidth, height: candidate.videoHeight ?? 0 };
547
- }
548
- if (typeof candidate.naturalWidth === "number" && candidate.naturalWidth > 0) {
549
- return { width: candidate.naturalWidth, height: candidate.naturalHeight ?? 0 };
550
- }
551
- const width = typeof candidate.width === "number" ? candidate.width : 0;
552
- const height = typeof candidate.height === "number" ? candidate.height : 0;
553
- return { width, height };
554
- }
555
- function toStreamPixels(observation, source, streamWidth, streamHeight) {
556
- const size = intrinsicSize(source);
557
- const scaleX = size.width > 0 ? streamWidth / size.width : 1;
558
- const scaleY = size.height > 0 ? streamHeight / size.height : 1;
559
- const scalePoint = (p) => ({ x: p.x * scaleX, y: p.y * scaleY });
560
- return {
561
- box: {
562
- x: observation.box.x * scaleX,
563
- y: observation.box.y * scaleY,
564
- width: observation.box.width * scaleX,
565
- height: observation.box.height * scaleY
566
- },
567
- ...observation.eyes !== void 0 ? { eyes: { left: scalePoint(observation.eyes.left), right: scalePoint(observation.eyes.right) } } : {},
568
- score: observation.score
569
- };
570
- }
571
- function describeError(err) {
572
- return err instanceof Error ? err.message : String(err);
573
- }
574
- async function createDetector(options) {
575
- const order = options.preferred ?? DEFAULT_TIER_ORDER;
576
- const reasons = [];
577
- for (const tier of order) {
578
- if (tier === "native") {
579
- const probed = await probeNativeDetector();
580
- if (probed.ok) {
581
- return new NativeDetector(probed.instance, reasonOf(reasons));
582
- }
583
- reasons.push(`native:${probed.reason}`);
584
- continue;
585
- }
586
- try {
587
- return await withTimeout(
588
- BlazeDetector.create(
589
- options.v3AssetBaseUrl ?? DEFAULT_V3_ASSET_BASE_URL,
590
- reasonOf(reasons)
591
- ),
592
- options.blazeInitTimeoutMs ?? BLAZE_INIT_TIMEOUT_MS
593
- );
594
- } catch (err) {
595
- reasons.push(
596
- err instanceof InitTimeoutError ? "blaze:blaze_init_timeout" : `blaze:${describeError(err)}`
597
- );
598
- }
599
- }
600
- throw new DetectorLadderError(reasonOf(reasons) ?? "no_tier_available");
601
- }
602
- function reasonOf(reasons) {
603
- return reasons.length > 0 ? reasons.join("; ") : void 0;
604
- }
605
- var InitTimeoutError = class extends Error {
606
- };
607
- function withTimeout(promise, deadlineMs) {
608
- return new Promise((resolvePromise, rejectPromise) => {
609
- const timer = setTimeout(() => {
610
- rejectPromise(new InitTimeoutError(`init exceeded ${deadlineMs}ms`));
611
- promise.then(
612
- (value) => value.destroy(),
613
- () => void 0
614
- );
615
- }, deadlineMs);
616
- promise.then(
617
- (value) => {
618
- clearTimeout(timer);
619
- resolvePromise(value);
620
- },
621
- (err) => {
622
- clearTimeout(timer);
623
- rejectPromise(err);
624
- }
625
- );
626
- });
627
- }
628
-
629
- // src/core/v3/frame-quality.ts
630
- var SHARP_REF = 180;
631
- var EXPOSURE_BAND = [100, 190];
632
- var EXPOSURE_FALLOFF = 64;
633
- function lumaAt(data, index) {
634
- return 0.299 * (data[index] ?? 0) + 0.587 * (data[index + 1] ?? 0) + 0.114 * (data[index + 2] ?? 0);
635
- }
636
- function meanLuma(image) {
637
- const { data, width, height } = image;
638
- const pixels = width * height;
639
- if (pixels === 0) return 0;
640
- let sum = 0;
641
- for (let i = 0; i < pixels; i += 1) sum += lumaAt(data, i * 4);
642
- return sum / pixels;
643
- }
644
- function sharpness(image) {
645
- const { data, width, height } = image;
646
- if (width < 3 || height < 3) return 0;
647
- const gray = new Float32Array(width * height);
648
- for (let i = 0; i < gray.length; i += 1) gray[i] = lumaAt(data, i * 4);
649
- let sum = 0;
650
- let sumSquares = 0;
651
- let count = 0;
652
- for (let y = 1; y < height - 1; y += 1) {
653
- for (let x = 1; x < width - 1; x += 1) {
654
- const i = y * width + x;
655
- const response = (gray[i - width] ?? 0) + (gray[i - 1] ?? 0) + (gray[i + 1] ?? 0) + (gray[i + width] ?? 0) - 4 * (gray[i] ?? 0);
656
- sum += response;
657
- sumSquares += response * response;
658
- count += 1;
659
- }
660
- }
661
- if (count === 0) return 0;
662
- const mean = sum / count;
663
- const variance = sumSquares / count - mean * mean;
664
- return Math.min(1, Math.max(0, variance) / SHARP_REF);
665
- }
666
- function exposure(image) {
667
- const mean = meanLuma(image);
668
- const [low, high] = EXPOSURE_BAND;
669
- const distance = mean < low ? low - mean : mean > high ? mean - high : 0;
670
- return Math.max(0, 1 - distance / EXPOSURE_FALLOFF);
671
- }
672
-
673
- // src/core/v3/face-gate-v3.ts
674
- var V3_GATE_DEFAULTS = Object.freeze({
675
- Q_GOOD: 0.6,
676
- RISE_PER_S: 0.9,
677
- FALL_PER_S: 0.25,
678
- CENTER_FALLOFF_IOD_MULTIPLE: 1.35,
679
- BOX_EYE_OFFSET_FACTOR: 0.12,
680
- IOD_BOX_PROXY_FACTOR: 0.42,
681
- SIZE_IDEAL_MIN: 0.12,
682
- SIZE_IDEAL_MAX: 0.3,
683
- SIZE_HARD_MIN: 0.08,
684
- SIZE_HARD_MAX: 0.45,
685
- ROLL_MAX_DEG: 25,
686
- MOTION_VELOCITY_IOD_PER_S: 0.5,
687
- MOTION_TAU_S: 0.4,
688
- MOTION_DEADBAND_IOD_PER_S: 0.1,
689
- READY_WINDOW_MS: 500,
690
- // (READY_MEAN_Q, the former 0.75 window MEAN, is gone — 2026-10-17 field
691
- // bug: a steady q anywhere in [Q_GOOD, 0.75) saturated p at 1, passed every
692
- // guide ("hold still") and could never reach the mean, so the ring sat full
693
- // and green with nothing recording. Readiness now checks the window against
694
- // Q_GOOD, which keeps the one-flaky-tick forgiveness and the second-bad-tick
695
- // block without a dead band above the rise threshold.)
696
- GUIDE_SIZE: 0.6,
697
- GUIDE_CENTERING: 0.6,
698
- GUIDE_POSE: 0.6,
699
- GUIDE_EXPOSURE: 0.6,
700
- GUIDE_SHARPNESS: 0.35,
701
- GUIDE_MOTION: 0.5
702
- });
703
- var clamp01 = (value) => Math.min(1, Math.max(0, value));
704
- var FaceGateV3 = class {
705
- constructor(overrides = {}) {
706
- this.p = 0;
707
- this.lastTickAt = null;
708
- this.lastCenter = null;
709
- this.velocityEma = 0;
710
- this.qWindow = [];
711
- /** Which side of the ideal band the last size measurement fell on. */
712
- this.lastSizeDirection = "ideal";
713
- this.t = { ...V3_GATE_DEFAULTS, ...overrides };
714
- }
715
- reset() {
716
- this.p = 0;
717
- this.lastTickAt = null;
718
- this.lastCenter = null;
719
- this.velocityEma = 0;
720
- this.qWindow = [];
721
- this.lastSizeDirection = "ideal";
722
- }
723
- tick(input) {
724
- const { observation, quality, timestampMs } = input;
725
- const dtSeconds = this.lastTickAt === null ? 0 : Math.max(0, (timestampMs - this.lastTickAt) / 1e3);
726
- this.lastTickAt = timestampMs;
727
- const factors = {
728
- presence: 0,
729
- centering: 0,
730
- size: 0,
731
- pose: 0,
732
- sharpness: clamp01(quality.sharpness),
733
- exposure: clamp01(quality.exposure),
734
- motion: 1
735
- };
736
- if (observation !== null) {
737
- factors.presence = 1;
738
- const geometry = this.geometryOf(observation, input);
739
- factors.centering = geometry.centering;
740
- factors.size = geometry.size;
741
- factors.pose = geometry.pose;
742
- factors.motion = this.motionOf(
743
- geometry.center,
744
- geometry.iod,
745
- timestampMs
746
- );
747
- } else {
748
- this.lastCenter = null;
749
- this.velocityEma = 0;
750
- }
751
- const q = Math.min(
752
- factors.presence,
753
- factors.centering,
754
- factors.size,
755
- factors.pose,
756
- factors.sharpness,
757
- factors.exposure,
758
- factors.motion
759
- );
760
- this.p = clamp01(
761
- this.p + (q >= this.t.Q_GOOD ? this.t.RISE_PER_S * q : -this.t.FALL_PER_S * (1 - q)) * dtSeconds
762
- );
763
- this.qWindow.push({ at: timestampMs, q });
764
- this.qWindow = this.qWindow.filter(
765
- (entry) => entry.at >= timestampMs - this.t.READY_WINDOW_MS
766
- );
767
- let windowSamples = this.qWindow.map((entry) => entry.q);
768
- if (windowSamples.length >= 3) {
769
- const worst = Math.min(...windowSamples);
770
- const worstIndex = windowSamples.indexOf(worst);
771
- windowSamples = windowSamples.filter((_, index) => index !== worstIndex);
772
- }
773
- const ready = this.p >= 1 && windowSamples.every((sample) => sample >= this.t.Q_GOOD);
774
- return {
775
- at: timestampMs,
776
- q,
777
- p: this.p,
778
- factors,
779
- guidanceKey: this.guidanceOf(factors, quality, q),
780
- ready,
781
- accepted: q >= this.t.Q_GOOD
782
- };
783
- }
784
- geometryOf(observation, input) {
785
- const { box, eyes } = observation;
786
- const frameCenter = { x: input.frameWidth / 2, y: input.frameHeight / 2 };
787
- const shortSide = Math.min(input.frameWidth, input.frameHeight);
788
- const center = eyes !== void 0 ? {
789
- x: (eyes.left.x + eyes.right.x) / 2,
790
- y: (eyes.left.y + eyes.right.y) / 2
791
- } : {
792
- x: box.x + box.width / 2,
793
- y: box.y + box.height / 2 - this.t.BOX_EYE_OFFSET_FACTOR * box.height
794
- };
795
- const iod = eyes !== void 0 ? Math.hypot(eyes.right.x - eyes.left.x, eyes.right.y - eyes.left.y) : this.t.IOD_BOX_PROXY_FACTOR * box.width;
796
- const distance = Math.hypot(
797
- center.x - frameCenter.x,
798
- center.y - frameCenter.y
799
- );
800
- const centering = iod <= 0 ? 0 : 1 - clamp01(distance / (this.t.CENTER_FALLOFF_IOD_MULTIPLE * iod));
801
- const ratio = shortSide > 0 ? iod / shortSide : 0;
802
- this.lastSizeDirection = ratio < this.t.SIZE_IDEAL_MIN ? "far" : ratio > this.t.SIZE_IDEAL_MAX ? "close" : "ideal";
803
- let size;
804
- if (ratio < this.t.SIZE_HARD_MIN || ratio > this.t.SIZE_HARD_MAX) {
805
- size = 0;
806
- } else if (ratio < this.t.SIZE_IDEAL_MIN) {
807
- size = (ratio - this.t.SIZE_HARD_MIN) / (this.t.SIZE_IDEAL_MIN - this.t.SIZE_HARD_MIN);
808
- } else if (ratio > this.t.SIZE_IDEAL_MAX) {
809
- size = (this.t.SIZE_HARD_MAX - ratio) / (this.t.SIZE_HARD_MAX - this.t.SIZE_IDEAL_MAX);
810
- } else {
811
- size = 1;
812
- }
813
- let pose = 1;
814
- if (eyes !== void 0) {
815
- const rollDeg = Math.abs(
816
- Math.atan2(eyes.right.y - eyes.left.y, eyes.right.x - eyes.left.x)
817
- ) * 180 / Math.PI;
818
- pose = 1 - clamp01(rollDeg / this.t.ROLL_MAX_DEG);
819
- }
820
- return { center, iod, centering, size, pose };
821
- }
822
- motionOf(center, iod, at) {
823
- const previous = this.lastCenter;
824
- this.lastCenter = { ...center, at };
825
- if (previous === null || iod <= 0) return 1;
826
- const dtSeconds = (at - previous.at) / 1e3;
827
- if (dtSeconds <= 0) return 1;
828
- let velocityIodPerS = Math.hypot(center.x - previous.x, center.y - previous.y) / iod / dtSeconds;
829
- if (velocityIodPerS <= this.t.MOTION_DEADBAND_IOD_PER_S)
830
- velocityIodPerS = 0;
831
- const alpha = 1 - Math.exp(-dtSeconds / this.t.MOTION_TAU_S);
832
- this.velocityEma = alpha * velocityIodPerS + (1 - alpha) * this.velocityEma;
833
- return 1 - clamp01(this.velocityEma / this.t.MOTION_VELOCITY_IOD_PER_S);
834
- }
835
- /** Lowest failing factor speaks — fixed priority (diagnostic, incl. light):
836
- * presence → size → centering → pose → exposure → sharpness → hold.
837
- *
838
- * HONESTY FALLBACK (operator ruling 2026-07-12): when every factor clears
839
- * its guide threshold but q (= min factor) is still below Q_GOOD — i.e.
840
- * progress is DRAINING — "hold still" would be a lie ("Perfect" while
841
- * nothing advances; the field symptom was users discovering by accident
842
- * that only leaning in fires the gate). In that state the WORST factor
843
- * speaks even though it passed its guide level, so the user always hears
844
- * the change that actually unblocks recording. */
845
- guidanceOf(factors, quality, q) {
846
- if (factors.presence <= 0) return "fcv3.no_face";
847
- if (factors.size < this.t.GUIDE_SIZE) return this.sizeGuidance();
848
- if (factors.centering < this.t.GUIDE_CENTERING) return "fcv3.center_face";
849
- if (factors.pose < this.t.GUIDE_POSE) return "fcv3.straighten";
850
- if (factors.exposure < this.t.GUIDE_EXPOSURE)
851
- return this.exposureGuidance(quality);
852
- if (factors.sharpness < this.t.GUIDE_SHARPNESS) return "fcv3.steady_camera";
853
- if (factors.motion < this.t.GUIDE_MOTION) return "fcv3.hold_still";
854
- if (q < this.t.Q_GOOD) {
855
- const coachable = [
856
- ["size", this.sizeGuidance()],
857
- ["centering", "fcv3.center_face"],
858
- ["pose", "fcv3.straighten"],
859
- ["exposure", this.exposureGuidance(quality)],
860
- ["sharpness", "fcv3.steady_camera"],
861
- ["motion", "fcv3.hold_still"]
862
- ];
863
- let worst = coachable[0];
864
- for (const entry of coachable) {
865
- if (factors[entry[0]] < factors[worst[0]]) worst = entry;
866
- }
867
- return worst[1];
868
- }
869
- return "fcv3.hold_still";
870
- }
871
- /** Direction from the size band set THIS tick (never inverted at the hard
872
- * limits: past SIZE_HARD_MAX the direction is 'close' → move_back). */
873
- sizeGuidance() {
874
- return this.lastSizeDirection === "far" ? "fcv3.come_closer" : "fcv3.move_back";
875
- }
876
- /** Band midpoint — direction only (tracks EXPOSURE_BAND automatically). */
877
- exposureGuidance(quality) {
878
- const bandMid = (EXPOSURE_BAND[0] + EXPOSURE_BAND[1]) / 2;
879
- return quality.meanLuma < bandMid ? "fcv3.more_light" : "fcv3.too_bright";
880
- }
881
- };
882
-
883
- // src/core/v3/device-profile.ts
884
- var DEVICE_PROFILES = Object.freeze({
885
- LOW: Object.freeze({ cadenceHz: 5, inputWidth: 128, animations: "reduced" }),
886
- MID: Object.freeze({ cadenceHz: 8, inputWidth: 160, animations: "full" }),
887
- HIGH: Object.freeze({ cadenceHz: 10, inputWidth: 192, animations: "full" })
888
- });
889
- var SIMD_TEST_BYTES = new Uint8Array([
890
- 0,
891
- 97,
892
- 115,
893
- 109,
894
- 1,
895
- 0,
896
- 0,
897
- 0,
898
- 1,
899
- 5,
900
- 1,
901
- 96,
902
- 0,
903
- 1,
904
- 123,
905
- 3,
906
- 2,
907
- 1,
908
- 0,
909
- 10,
910
- 10,
911
- 1,
912
- 8,
913
- 0,
914
- 65,
915
- 0,
916
- 253,
917
- 15,
918
- 253,
919
- 98,
920
- 11
921
- ]);
922
- function probeDevice() {
923
- const nav = typeof navigator === "undefined" ? void 0 : navigator;
924
- let simd = false;
925
- try {
926
- simd = typeof WebAssembly !== "undefined" && WebAssembly.validate(SIMD_TEST_BYTES);
927
- } catch {
928
- simd = false;
929
- }
930
- return {
931
- cores: nav?.hardwareConcurrency ?? 2,
932
- memGB: nav?.deviceMemory ?? null,
933
- simd
934
- };
935
- }
936
- function classifyDevice(probe) {
937
- const memGB = probe.memGB ?? 4;
938
- if (probe.cores < 4 || memGB < 3) return "LOW";
939
- if (probe.cores >= 8 && memGB >= 6) return "HIGH";
940
- return "MID";
941
- }
942
- var ORDER = ["LOW", "MID", "HIGH"];
943
- var DeviceProfiler = class {
944
- constructor(initial) {
945
- this.samples = [];
946
- this.adjusted = false;
947
- this.current = initial;
948
- }
949
- get deviceClass() {
950
- return this.current;
951
- }
952
- get profile() {
953
- return DEVICE_PROFILES[this.current];
954
- }
955
- record(detectMs) {
956
- if (this.adjusted || this.samples.length >= 10) return this.current;
957
- this.samples.push(detectMs);
958
- if (this.samples.length < 10) return this.current;
959
- this.adjusted = true;
960
- const mean = this.samples.reduce((sum, value) => sum + value, 0) / this.samples.length;
961
- if (mean > 70) {
962
- this.current = "LOW";
963
- } else if (mean < 25) {
964
- const index = ORDER.indexOf(this.current);
965
- this.current = ORDER[Math.min(ORDER.length - 1, index + 1)] ?? this.current;
966
- }
967
- return this.current;
968
- }
969
- };
970
-
971
- // src/core/v3/detect-scheduler.ts
972
- var STATS_WINDOW = 20;
973
- var DetectScheduler = class {
974
- constructor(options) {
975
- this.timer = null;
976
- this.frameCallbackId = null;
977
- this.running = false;
978
- this.busy = false;
979
- this.lastRunAt = Number.NEGATIVE_INFINITY;
980
- this.degradeCount = 0;
981
- this.durations = [];
982
- this.options = options;
983
- this.periodMs = 1e3 / options.cadenceHz;
984
- }
985
- start() {
986
- if (this.running) return;
987
- this.running = true;
988
- const video = this.options.video;
989
- if (typeof video?.requestVideoFrameCallback === "function") {
990
- const loop = () => {
991
- if (!this.running) return;
992
- this.frameCallbackId = video.requestVideoFrameCallback?.(() => {
993
- this.frameCallbackId = null;
994
- if (this.now() - this.lastRunAt >= this.periodMs) this.tick();
995
- loop();
996
- });
997
- };
998
- loop();
999
- return;
1000
- }
1001
- this.timer = setInterval(() => this.tick(), this.periodMs);
1002
- }
1003
- stop() {
1004
- this.running = false;
1005
- if (this.timer !== null) {
1006
- clearInterval(this.timer);
1007
- this.timer = null;
1008
- }
1009
- if (this.frameCallbackId !== null) {
1010
- this.options.video?.cancelVideoFrameCallback?.(
1011
- this.frameCallbackId
1012
- );
1013
- this.frameCallbackId = null;
1014
- }
1015
- }
1016
- stats() {
1017
- const meanDetectMs = this.durations.length === 0 ? 0 : this.durations.reduce((sum, value) => sum + value, 0) / this.durations.length;
1018
- return { degradeCount: this.degradeCount, meanDetectMs };
1019
- }
1020
- now() {
1021
- return this.options.now?.() ?? performance.now();
1022
- }
1023
- tick() {
1024
- if (!this.running) return;
1025
- if (this.busy) {
1026
- this.degradeCount += 1;
1027
- return;
1028
- }
1029
- this.busy = true;
1030
- this.lastRunAt = this.now();
1031
- const startedAt = this.now();
1032
- this.options.run().catch(() => void 0).finally(() => {
1033
- this.busy = false;
1034
- this.durations.push(this.now() - startedAt);
1035
- if (this.durations.length > STATS_WINDOW) this.durations.shift();
1036
- });
1037
- }
1038
- };
1039
-
1040
- // src/core/v3/detect-worker.ts
1041
- var WORKER_PROTOCOL_VERSION = 1;
1042
- function workerOffloadSupported() {
1043
- return typeof OffscreenCanvas === "function" && typeof createImageBitmap === "function" && typeof Worker === "function";
1044
- }
1045
- function installWorkerDetectorHost(scope, deps = {}) {
1046
- const create = deps.createDetector ?? createDetector;
1047
- let detector = null;
1048
- scope.onmessage = (event) => {
1049
- const message = event.data;
1050
- if (message.v !== WORKER_PROTOCOL_VERSION) {
1051
- message.bitmap?.close?.();
1052
- return;
1053
- }
1054
- if (message.type === "init") {
1055
- void create(message.options).then((created) => {
1056
- detector = created;
1057
- const ready = {
1058
- v: WORKER_PROTOCOL_VERSION,
1059
- type: "ready",
1060
- tier: created.tier,
1061
- ...created.fallbackReason !== void 0 ? { fallbackReason: created.fallbackReason } : {}
1062
- };
1063
- scope.postMessage(ready);
1064
- });
1065
- return;
1066
- }
1067
- if (message.type === "detect") {
1068
- const respond = (observation) => {
1069
- const result = {
1070
- v: WORKER_PROTOCOL_VERSION,
1071
- type: "result",
1072
- id: message.id,
1073
- observation
1074
- };
1075
- scope.postMessage(result);
1076
- message.bitmap.close?.();
1077
- };
1078
- if (detector === null) {
1079
- respond(null);
1080
- return;
1081
- }
1082
- void detector.detect(message.bitmap, message.streamWidth, message.streamHeight).then(respond, () => respond(null));
1083
- }
1084
- };
1085
- }
1086
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
1087
- installWorkerDetectorHost(self);
1088
- }
1089
- var WorkerDetectorProxy = class _WorkerDetectorProxy {
1090
- constructor(worker, ready) {
1091
- this.pending = /* @__PURE__ */ new Map();
1092
- this.nextId = 1;
1093
- this.worker = worker;
1094
- this.tier = ready.tier;
1095
- if (ready.fallbackReason !== void 0) this.fallbackReason = ready.fallbackReason;
1096
- worker.addEventListener("message", (event) => {
1097
- const message = event.data;
1098
- if (message.v !== WORKER_PROTOCOL_VERSION || message.type !== "result") return;
1099
- const resolve = this.pending.get(message.id);
1100
- this.pending.delete(message.id);
1101
- resolve?.(message.observation);
1102
- });
1103
- }
1104
- static create(worker, options, readyTimeoutMs = 1e4) {
1105
- return new Promise((resolve, reject) => {
1106
- const timer = setTimeout(() => {
1107
- worker.removeEventListener("message", onReady);
1108
- worker.terminate();
1109
- reject(new Error(`worker detector not ready within ${readyTimeoutMs}ms`));
1110
- }, readyTimeoutMs);
1111
- const onReady = (event) => {
1112
- const message = event.data;
1113
- if (message.v !== WORKER_PROTOCOL_VERSION || message.type !== "ready") return;
1114
- clearTimeout(timer);
1115
- worker.removeEventListener("message", onReady);
1116
- resolve(new _WorkerDetectorProxy(worker, message));
1117
- };
1118
- worker.addEventListener("message", onReady);
1119
- const init = { v: WORKER_PROTOCOL_VERSION, type: "init", options };
1120
- worker.postMessage(init);
1121
- });
1122
- }
1123
- async detect(source, sourceWidth, sourceHeight) {
1124
- let bitmap;
1125
- try {
1126
- if (typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
1127
- bitmap = source;
1128
- } else if (typeof createImageBitmap === "function") {
1129
- bitmap = await createImageBitmap(source);
1130
- } else {
1131
- bitmap = source;
1132
- }
1133
- } catch {
1134
- return null;
1135
- }
1136
- const id = this.nextId;
1137
- this.nextId += 1;
1138
- return new Promise((resolve) => {
1139
- this.pending.set(id, resolve);
1140
- const message = {
1141
- v: WORKER_PROTOCOL_VERSION,
1142
- type: "detect",
1143
- id,
1144
- bitmap,
1145
- streamWidth: sourceWidth,
1146
- streamHeight: sourceHeight
1147
- };
1148
- this.worker.postMessage(message, [bitmap]);
1149
- });
1150
- }
1151
- destroy() {
1152
- for (const resolve of this.pending.values()) resolve(null);
1153
- this.pending.clear();
1154
- this.worker.terminate();
1155
- }
1156
- };
1157
-
1158
- // src/core/v3/capture-session-v3.ts
1159
- var KEYFRAME_CANDIDATES = 4;
1160
- var KEYFRAME_WINDOW_MS = 600;
1161
- var NEUTRAL_QUALITY = { sharpness: 1, exposure: 1, meanLuma: 128 };
1162
- var CaptureSessionV3 = class {
1163
- constructor(options) {
1164
- this.handlers = /* @__PURE__ */ new Map();
1165
- this.stateValue = "idle";
1166
- this.attempt = 0;
1167
- this.stream = null;
1168
- this.detector = null;
1169
- this.scheduler = null;
1170
- this.gate = null;
1171
- this.profiler = null;
1172
- this.lastGuidanceKey = null;
1173
- this.positioningStartedAt = 0;
1174
- // Face-capture timing marks of the CURRENT attempt (engine clock; reset per run).
1175
- this.cameraRequestedAt = null;
1176
- this.cameraReadyAt = null;
1177
- this.faceAcquiredAt = null;
1178
- this.captureCompleteAt = null;
1179
- this.fired = false;
1180
- this.carriedDegrade = 0;
1181
- this.carriedMeans = [];
1182
- /** Reused downscale canvas — falls back to the raw video where canvas 2D
1183
- * is unavailable (jsdom) so tests and exotic hosts keep detecting. */
1184
- this.snapshotCanvas = null;
1185
- this.options = options;
1186
- this.facing = options.facingMode ?? "user";
1187
- }
1188
- get state() {
1189
- return this.stateValue;
1190
- }
1191
- on(event, handler) {
1192
- let set = this.handlers.get(event);
1193
- if (!set) {
1194
- set = /* @__PURE__ */ new Set();
1195
- this.handlers.set(event, set);
1196
- }
1197
- set.add(handler);
1198
- return () => set?.delete(handler);
1199
- }
1200
- emit(event, payload) {
1201
- for (const handler of [...this.handlers.get(event) ?? []]) {
1202
- handler(payload);
1203
- }
1204
- }
1205
- async start(options = {}) {
1206
- if (options.facingMode !== void 0) this.facing = options.facingMode;
1207
- this.attempt += 1;
1208
- await this.run(this.attempt);
1209
- }
1210
- /** The camera the current/next attempt requests (start option, switchCamera toggles). */
1211
- get facingMode() {
1212
- return this.facing;
1213
- }
1214
- /** Full reset + camera restart — the documented recovery path (v1 parity). */
1215
- retry() {
1216
- this.teardownAttempt();
1217
- void this.start();
1218
- }
1219
- switchCamera() {
1220
- this.facing = this.facing === "user" ? "environment" : "user";
1221
- this.teardownAttempt();
1222
- void this.start();
1223
- }
1224
- destroy() {
1225
- this.attempt += 1;
1226
- this.teardownAttempt();
1227
- this.detector?.destroy();
1228
- this.detector = null;
1229
- this.options.video.srcObject = null;
1230
- }
1231
- // ── attempt lifecycle ──────────────────────────────────────────────────────
1232
- async run(attempt) {
1233
- const { options } = this;
1234
- this.fired = false;
1235
- this.lastGuidanceKey = null;
1236
- this.carriedDegrade = 0;
1237
- this.carriedMeans = [];
1238
- this.cameraRequestedAt = null;
1239
- this.cameraReadyAt = null;
1240
- this.faceAcquiredAt = null;
1241
- this.captureCompleteAt = null;
1242
- this.setState("preflight");
1243
- const preflight = cameraPreflight();
1244
- if (preflight !== "OK") {
1245
- this.fail(preflight === "INSECURE_CONTEXT" ? "insecure" : "unsupported");
1246
- return;
1247
- }
1248
- this.setState("acquiring");
1249
- const getUserMedia = options.getUserMedia ?? ((constraints) => navigator.mediaDevices.getUserMedia(constraints));
1250
- let stream;
1251
- this.cameraRequestedAt = this.now();
1252
- try {
1253
- stream = await getUserMedia(buildCaptureConstraints(detectIsMobile(), this.facing));
1254
- } catch (err) {
1255
- if (attempt !== this.attempt) return;
1256
- this.fail(mapGetUserMediaError(err));
1257
- return;
1258
- }
1259
- if (attempt !== this.attempt) {
1260
- for (const track of stream.getTracks()) track.stop();
1261
- return;
1262
- }
1263
- this.stream = stream;
1264
- options.video.srcObject = stream;
1265
- await options.video.play().catch(() => void 0);
1266
- if (attempt !== this.attempt) return;
1267
- this.cameraReadyAt = this.now();
1268
- if (this.detector === null) {
1269
- const ladderOptions = {
1270
- ...options.v3AssetBaseUrl !== void 0 ? { v3AssetBaseUrl: options.v3AssetBaseUrl } : {}
1271
- };
1272
- const create = options.createDetector ?? (options.createWorker !== void 0 && workerOffloadSupported() ? async () => {
1273
- try {
1274
- return await WorkerDetectorProxy.create(
1275
- options.createWorker(),
1276
- ladderOptions
1277
- );
1278
- } catch {
1279
- return createDetector(ladderOptions);
1280
- }
1281
- } : () => createDetector(ladderOptions));
1282
- let created;
1283
- try {
1284
- created = await create();
1285
- } catch (err) {
1286
- if (attempt === this.attempt) {
1287
- this.fail(
1288
- "models",
1289
- err instanceof DetectorLadderError ? err.fallbackReason : describeError2(err)
1290
- );
1291
- }
1292
- return;
1293
- }
1294
- if (attempt !== this.attempt) {
1295
- created.destroy();
1296
- return;
1297
- }
1298
- this.detector = created;
1299
- this.options.telemetry?.({
1300
- type: "detector",
1301
- tier: created.tier,
1302
- ...created.fallbackReason !== void 0 ? { fallbackReason: created.fallbackReason } : {}
1303
- });
1304
- }
1305
- const probe = probeDevice();
1306
- const staticClass = options.deviceClass ?? classifyDevice(probe);
1307
- this.profiler = new DeviceProfiler(staticClass);
1308
- this.options.telemetry?.({
1309
- type: "profile",
1310
- staticClass,
1311
- activeClass: this.profiler.deviceClass,
1312
- simd: probe.simd
1313
- });
1314
- this.gate = new FaceGateV3(options.thresholds ?? {});
1315
- this.setState("positioning");
1316
- this.positioningStartedAt = this.now();
1317
- this.startScheduler(attempt);
1318
- }
1319
- /** Chrome hint (Task 11): 'reduced' on LOW even without the media query. */
1320
- get animations() {
1321
- return this.profiler?.profile.animations ?? "full";
1322
- }
1323
- startScheduler(attempt) {
1324
- const profiler = this.profiler;
1325
- if (profiler === null) return;
1326
- if (this.scheduler !== null) {
1327
- const stats = this.scheduler.stats();
1328
- this.carriedDegrade += stats.degradeCount;
1329
- if (stats.meanDetectMs > 0) this.carriedMeans.push(stats.meanDetectMs);
1330
- this.scheduler.stop();
1331
- }
1332
- const scheduler = new DetectScheduler({
1333
- cadenceHz: profiler.profile.cadenceHz,
1334
- video: this.options.video,
1335
- ...this.options.now !== void 0 ? { now: this.options.now } : {},
1336
- run: () => this.detectTick(attempt)
1337
- });
1338
- this.scheduler = scheduler;
1339
- scheduler.start();
1340
- }
1341
- async detectTick(attempt) {
1342
- const { detector, gate, profiler } = this;
1343
- const video = this.options.video;
1344
- if (attempt !== this.attempt || detector === null || gate === null || profiler === null)
1345
- return;
1346
- if (this.fired || video.videoWidth === 0) return;
1347
- const before = profiler.deviceClass;
1348
- const t0 = this.now();
1349
- const source = this.options.makeSnapshot?.(video, profiler.profile.inputWidth) ?? this.defaultSnapshot(video, profiler.profile.inputWidth);
1350
- let observation = null;
1351
- try {
1352
- observation = await detector.detect(source, video.videoWidth, video.videoHeight);
1353
- } catch {
1354
- observation = null;
1355
- }
1356
- if (attempt !== this.attempt || this.fired) return;
1357
- profiler.record(this.now() - t0);
1358
- if (profiler.deviceClass !== before) this.startScheduler(attempt);
1359
- const quality = observation === null ? NEUTRAL_QUALITY : this.options.measureQuality?.(video, observation.box) ?? this.defaultQuality(video, observation.box);
1360
- const tick = gate.tick({
1361
- timestampMs: this.now(),
1362
- observation,
1363
- quality,
1364
- frameWidth: video.videoWidth,
1365
- frameHeight: video.videoHeight
1366
- });
1367
- if (tick.accepted && this.faceAcquiredAt === null) this.faceAcquiredAt = tick.at;
1368
- this.emit("checks", {
1369
- engine: "v3",
1370
- progress: tick.p,
1371
- q: tick.q,
1372
- factors: tick.factors,
1373
- attempt
1374
- });
1375
- if (tick.guidanceKey !== this.lastGuidanceKey) {
1376
- this.lastGuidanceKey = tick.guidanceKey;
1377
- this.emit("guidance", { key: tick.guidanceKey });
1378
- }
1379
- if (tick.ready && this.stateValue === "positioning") {
1380
- this.fired = true;
1381
- this.captureCompleteAt = tick.at;
1382
- void this.fire(attempt);
1383
- }
1384
- }
1385
- async fire(attempt) {
1386
- const { options } = this;
1387
- const scheduler = this.scheduler;
1388
- scheduler?.stop();
1389
- const stats = scheduler?.stats() ?? { degradeCount: 0, meanDetectMs: 0 };
1390
- const means = [...this.carriedMeans, ...stats.meanDetectMs > 0 ? [stats.meanDetectMs] : []];
1391
- options.telemetry?.({
1392
- type: "fired",
1393
- timeToGateMs: this.now() - this.positioningStartedAt,
1394
- degradeCount: this.carriedDegrade + stats.degradeCount,
1395
- meanDetectMs: means.length === 0 ? 0 : means.reduce((sum, value) => sum + value, 0) / means.length
1396
- });
1397
- const mode = options.mode ?? "clip";
1398
- const bestOf = this.collectKeyframes(attempt);
1399
- if (mode === "photo") {
1400
- const best2 = await bestOf;
1401
- if (attempt !== this.attempt) return;
1402
- if (best2 === null) {
1403
- this.fail("generic");
1404
- return;
1405
- }
1406
- this.finishCapture({ keyframe: best2.blob, previewDataUrl: best2.previewDataUrl });
1407
- return;
1408
- }
1409
- this.setState("recording");
1410
- const recorder = options.createRecorder?.() ?? new ClipRecorder();
1411
- const stream = this.stream;
1412
- if (stream === null) {
1413
- this.fail("generic");
1414
- return;
1415
- }
1416
- let frozenBest = null;
1417
- void bestOf.then(
1418
- (candidate) => {
1419
- frozenBest = candidate;
1420
- },
1421
- () => {
1422
- }
1423
- );
1424
- const onRecordingStopped = () => {
1425
- if (attempt !== this.attempt) return;
1426
- if (frozenBest !== null) {
1427
- this.emit("frozen", { previewDataUrl: frozenBest.previewDataUrl });
1428
- }
1429
- };
1430
- let clip;
1431
- let best;
1432
- try {
1433
- [clip, best] = await Promise.all([recorder.record(stream, onRecordingStopped), bestOf]);
1434
- } catch (err) {
1435
- if (attempt !== this.attempt) return;
1436
- const kind = err.kind;
1437
- this.fail(kind === "cliptoolarge" ? "cliptoolarge" : "recorder");
1438
- return;
1439
- }
1440
- if (attempt !== this.attempt) return;
1441
- if (isClipTooLarge(clip.blob)) {
1442
- this.fail("cliptoolarge");
1443
- return;
1444
- }
1445
- if (best === null) {
1446
- this.fail("generic");
1447
- return;
1448
- }
1449
- this.finishCapture({
1450
- clip: clip.blob,
1451
- clipMimeType: clip.mimeType,
1452
- keyframe: best.blob,
1453
- previewDataUrl: best.previewDataUrl
1454
- });
1455
- }
1456
- /** Up to KEYFRAME_CANDIDATES mirrored frames over KEYFRAME_WINDOW_MS —
1457
- * sharpest wins (reuses frame-quality.sharpness via the capture seam). */
1458
- async collectKeyframes(attempt) {
1459
- const capture = this.options.captureKeyframe ?? ((video) => this.defaultKeyframe(video));
1460
- const stepMs = KEYFRAME_WINDOW_MS / (KEYFRAME_CANDIDATES - 1);
1461
- let best = null;
1462
- for (let i = 0; i < KEYFRAME_CANDIDATES; i += 1) {
1463
- if (attempt !== this.attempt) return best;
1464
- try {
1465
- const candidate = await capture(this.options.video);
1466
- if (best === null || candidate.sharpness > best.sharpness) best = candidate;
1467
- } catch {
1468
- }
1469
- if (i < KEYFRAME_CANDIDATES - 1) await delay(stepMs);
1470
- }
1471
- return best;
1472
- }
1473
- finishCapture(artifacts) {
1474
- this.stopTracks();
1475
- this.setState("captured");
1476
- const timing = this.captureTiming();
1477
- this.emit("artifacts", timing === void 0 ? artifacts : { ...artifacts, timing });
1478
- }
1479
- /** The attempt's capture-phase timing (whole ms, ≥ 0); undefined when no mark landed. */
1480
- captureTiming() {
1481
- const span = (from, to) => from !== null && to !== null ? Math.max(0, Math.round(to - from)) : void 0;
1482
- const cameraReadyMs = span(this.cameraRequestedAt, this.cameraReadyAt);
1483
- const faceAcquireMs = span(this.cameraReadyAt, this.faceAcquiredAt);
1484
- const captureMs = span(this.faceAcquiredAt, this.captureCompleteAt);
1485
- const timing = {
1486
- ...cameraReadyMs !== void 0 ? { cameraReadyMs } : {},
1487
- ...faceAcquireMs !== void 0 ? { faceAcquireMs } : {},
1488
- ...captureMs !== void 0 ? { captureMs } : {}
1489
- };
1490
- return Object.keys(timing).length > 0 ? timing : void 0;
1491
- }
1492
- // ── defaults for the canvas-bound seams ────────────────────────────────────
1493
- async defaultKeyframe(video) {
1494
- const canvas = document.createElement("canvas");
1495
- const blob = await captureMirroredKeyframe(video, canvas);
1496
- const previewDataUrl = canvas.toDataURL("image/jpeg", 0.92);
1497
- return { blob, previewDataUrl, sharpness: this.centerSharpness(video) };
1498
- }
1499
- defaultSnapshot(video, targetWidth) {
1500
- try {
1501
- const canvas = this.snapshotCanvas ?? document.createElement("canvas");
1502
- const scale = targetWidth / Math.max(1, video.videoWidth);
1503
- const width = Math.max(1, Math.round(video.videoWidth * scale));
1504
- const height = Math.max(1, Math.round(video.videoHeight * scale));
1505
- const ctx = canvas.getContext("2d");
1506
- if (ctx === null) return video;
1507
- if (canvas.width !== width) canvas.width = width;
1508
- if (canvas.height !== height) canvas.height = height;
1509
- ctx.drawImage(video, 0, 0, width, height);
1510
- this.snapshotCanvas = canvas;
1511
- return canvas;
1512
- } catch {
1513
- return video;
1514
- }
1515
- }
1516
- defaultQuality(video, box) {
1517
- const crop = cropImageData(video, box, 96);
1518
- if (crop === null) return NEUTRAL_QUALITY;
1519
- return { sharpness: sharpness(crop), exposure: exposure(crop), meanLuma: meanLuma(crop) };
1520
- }
1521
- centerSharpness(video) {
1522
- const size = intrinsicSize(video);
1523
- const crop = cropImageData(
1524
- video,
1525
- { x: size.width * 0.25, y: size.height * 0.25, width: size.width * 0.5, height: size.height * 0.5 },
1526
- 96
1527
- );
1528
- return crop === null ? 0 : sharpness(crop);
1529
- }
1530
- // ── teardown ──────────────────────────────────────────────────────────────
1531
- teardownAttempt() {
1532
- this.scheduler?.stop();
1533
- this.scheduler = null;
1534
- this.gate = null;
1535
- this.stopTracks();
1536
- }
1537
- stopTracks() {
1538
- if (this.stream === null) return;
1539
- for (const track of this.stream.getTracks()) track.stop();
1540
- this.stream = null;
1541
- }
1542
- fail(kind, fallbackReason) {
1543
- this.scheduler?.stop();
1544
- this.stopTracks();
1545
- this.setState("error");
1546
- this.options.telemetry?.({
1547
- type: "error",
1548
- kind,
1549
- ...fallbackReason !== void 0 ? { fallbackReason } : {}
1550
- });
1551
- this.emit("error", { kind, retryable: kind !== "insecure" && kind !== "unsupported" });
1552
- }
1553
- setState(state) {
1554
- this.stateValue = state;
1555
- this.emit("statechange", { state });
1556
- }
1557
- now() {
1558
- return this.options.now?.() ?? performance.now();
1559
- }
1560
- };
1561
- function delay(ms) {
1562
- return new Promise((resolve) => setTimeout(resolve, ms));
1563
- }
1564
- function describeError2(err) {
1565
- return err instanceof Error ? err.message : String(err);
1566
- }
1567
- function cropImageData(video, box, targetWidth) {
1568
- try {
1569
- const canvas = document.createElement("canvas");
1570
- const scale = targetWidth / Math.max(1, box.width);
1571
- canvas.width = Math.max(1, Math.round(box.width * scale));
1572
- canvas.height = Math.max(1, Math.round(box.height * scale));
1573
- const ctx = canvas.getContext("2d");
1574
- if (ctx === null) return null;
1575
- ctx.drawImage(
1576
- video,
1577
- box.x,
1578
- box.y,
1579
- box.width,
1580
- box.height,
1581
- 0,
1582
- 0,
1583
- canvas.width,
1584
- canvas.height
1585
- );
1586
- return ctx.getImageData(0, 0, canvas.width, canvas.height);
1587
- } catch {
1588
- return null;
1589
- }
1590
- }
1591
- // Annotate the CommonJS export names for ESM import in node:
1592
- 0 && (module.exports = {
1593
- CENTERED_FACTOR,
1594
- CIRCLE_RADIUS_FACTOR,
1595
- CLIP_MS,
1596
- CaptureSessionV3,
1597
- ClipError,
1598
- ClipRecorder,
1599
- DEFAULT_CLIP_MIME,
1600
- DEFAULT_TIER_ORDER,
1601
- DEFAULT_V3_ASSET_BASE_URL,
1602
- DEVICE_PROFILES,
1603
- DetectScheduler,
1604
- DetectorLadderError,
1605
- DeviceProfiler,
1606
- EXPOSURE_BAND,
1607
- FaceGateV3,
1608
- IN_CIRCLE_FACTOR,
1609
- KEYFRAME_CANDIDATES,
1610
- KEYFRAME_WINDOW_MS,
1611
- MAX_CLIP_BYTES,
1612
- MIME_CANDIDATES,
1613
- SHARP_REF,
1614
- STOP_GRACE_MS,
1615
- TIMESLICE_MS,
1616
- V3_GATE_DEFAULTS,
1617
- VIDEO_BPS,
1618
- buildCaptureConstraints,
1619
- buildLivenessV2FormData,
1620
- cameraPreflight,
1621
- captureMirroredKeyframe,
1622
- classifyDevice,
1623
- createDetector,
1624
- detectIsMobile,
1625
- errorCopy,
1626
- exposure,
1627
- extForMime,
1628
- intrinsicSize,
1629
- isClipTooLarge,
1630
- isMediaRecorderSupported,
1631
- mapGetUserMediaError,
1632
- meanLuma,
1633
- pickMimeType,
1634
- probeDevice,
1635
- sharpness,
1636
- toStreamPixels,
1637
- uiCopy
1638
- });
1639
- //# sourceMappingURL=index.cjs.map
1
+ "use strict";var Xe=Object.create;var P=Object.defineProperty;var qe=Object.getOwnPropertyDescriptor;var Ye=Object.getOwnPropertyNames;var Ze=Object.getPrototypeOf,Qe=Object.prototype.hasOwnProperty;var je=(t,e)=>()=>(t&&(e=t(t=0)),e);var ue=(t,e)=>{for(var r in e)P(t,r,{get:e[r],enumerable:!0})},le=(t,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Ye(e))!Qe.call(t,n)&&n!==r&&P(t,n,{get:()=>e[n],enumerable:!(i=qe(e,n))||i.enumerable});return t};var $e=(t,e,r)=>(r=t!=null?Xe(Ze(t)):{},le(e||!t||!t.__esModule?P(r,"default",{value:t,enumerable:!0}):r,t)),Je=t=>le(P({},"__esModule",{value:!0}),t);var Fe={};ue(Fe,{RANK_CANDIDATES:()=>xt,RANK_TOLERANCE:()=>B,RANK_WINDOW_MS:()=>St,ROLL_ZERO_DEG:()=>kt,SHARPNESS_REPORT_MAX:()=>Pe,YAW_REPORT_MAX:()=>Ae,YAW_ZERO_IOD:()=>Dt,collectRankedKeyframes:()=>Le,default:()=>Ct,featuresOf:()=>Ie,frontalOf:()=>Oe,rankKeyframes:()=>Re});function Oe(t){let e=t.eyes;if(e===void 0)return{frontal:1,yaw:null,roll:null};let r=e.right.x-e.left.x,i=e.right.y-e.left.y,n=r*r+i*i;if(n===0)return{frontal:1,yaw:null,roll:null};let a=Math.abs(Math.atan2(i,Math.abs(r))*180/Math.PI),s=we(1-a/30),o=null,c=t.nose;if(c!==void 0){let d=(e.left.x+e.right.x)/2,u=(e.left.y+e.right.y)/2;o=Math.abs(((c.x-d)*r+(c.y-u)*i)/n),s*=we(1-o/.5)}return{frontal:s,yaw:o,roll:a}}function Ie(t,e){return t===null?{face:!1,frontal:0,yaw:null,roll:null,eyes:null,score:0,sharpness:e}:{face:!0,...Oe(t),eyes:null,score:t.score,sharpness:e}}function Re(t){let e=t.map((n,a)=>a),r=(n,a)=>{let s=Number.NEGATIVE_INFINITY;for(let o of e)s=Math.max(s,n(t[o]));e=e.filter(o=>n(t[o])>=s-a)};r(n=>n.frontal,B.frontal),r(n=>n.eyes??1,B.eyes),r(n=>n.score,B.score);let i=-1;for(let n of e)(i===-1||t[n].sharpness>t[i].sharpness)&&(i=n);return i}function Tt(t){return{face:t.face,frontal:x(t.frontal),...t.yaw!==null?{yaw:Math.min(x(t.yaw),Ae)}:{},...t.roll!==null?{roll:x(t.roll)}:{},...t.eyes!==null?{eyes:x(t.eyes)}:{},score:x(t.score),sharpness:Math.min(x(t.sharpness),Pe)}}async function Le(t,e){let{options:r}=t,i=r.video,n=r.now??(()=>performance.now()),a=r.captureKeyframe??(m=>t.defaultKeyframe(m)),s=600/3,o=[],c=[],d=0;for(let m=0;m<4;m+=1){if(e!==t.attempt)return null;let p=t.detector,h=Promise.resolve(null),y,I=0;try{if(p!==null){let A=t.profiler?.profile.inputWidth??160,Ue=r.makeSnapshot?.(i,A)??t.defaultSnapshot(i,A);y=a(i).then(z=>(I=n(),z));let ze=n();h=p.detect(Ue,i.videoWidth,i.videoHeight).then(z=>(d+=n()-ze,z),()=>null)}else y=a(i).then(A=>(I=n(),A));let[R,Be]=await Promise.all([y,h]);o.push(R),c.push(Ie(Be,R.sharpness))}catch{y?.catch(()=>{})}let He=I>0?Math.max(0,n()-I):0;m<3&&await new Promise(R=>setTimeout(R,Math.max(0,s-He)))}if(e!==t.attempt)return null;let u=Re(c);if(u===-1)return null;let l=0;c.forEach((m,p)=>{m.sharpness>c[l].sharpness&&(l=p)});try{r.telemetry?.({type:"keyframe",tier:t.detector?.tier??null,chosen:u,sharpest:l,detectMs:Math.round(d),candidates:c.map(Tt)})}catch{}return o[u]??null}var xt,St,Dt,kt,B,we,x,Ae,Pe,Ct,Ne=je(()=>{"use strict";xt=4,St=600,Dt=.5,kt=30,B={frontal:.1,eyes:.1,score:.05},we=t=>t<0?0:t>1?1:t;x=t=>Number.isFinite(t)?Math.round(t*1e3)/1e3:0,Ae=99,Pe=999999;Ct=Le});var Ot={};ue(Ot,{CENTERED_FACTOR:()=>st,CIRCLE_RADIUS_FACTOR:()=>it,CLIP_MS:()=>me,CaptureSessionV3:()=>ce,ClipError:()=>f,ClipRecorder:()=>D,DEFAULT_CLIP_MIME:()=>L,DEFAULT_TIER_ORDER:()=>ge,DEFAULT_V3_ASSET_BASE_URL:()=>be,DEVICE_PROFILES:()=>_e,DetectScheduler:()=>w,DetectorLadderError:()=>v,DeviceProfiler:()=>M,EXPOSURE_BAND:()=>T,FaceGateV3:()=>C,GATE_SNAPSHOT_MS:()=>We,GateBlockStats:()=>O,IN_CIRCLE_FACTOR:()=>at,KEYFRAME_CANDIDATES:()=>U,KEYFRAME_WINDOW_MS:()=>Ke,MAX_CLIP_BYTES:()=>F,MIME_CANDIDATES:()=>de,SHARP_REF:()=>zt,STOP_GRACE_MS:()=>he,TIMESLICE_MS:()=>pe,V3_GATE_DEFAULTS:()=>Ee,VIDEO_BPS:()=>j,blockReasonOf:()=>Me,buildCaptureConstraints:()=>$,buildLivenessV2FormData:()=>tt,cameraPreflight:()=>Z,captureMirroredKeyframe:()=>Q,classifyDevice:()=>ie,createDetector:()=>E,detectIsMobile:()=>J,errorCopy:()=>rt,exposure:()=>te,extForMime:()=>X,intrinsicSize:()=>g,isClipTooLarge:()=>S,isMediaRecorderSupported:()=>et,mapGetUserMediaError:()=>Y,meanLuma:()=>W,pickMimeType:()=>q,probeDevice:()=>ne,sharpness:()=>K,toStreamPixels:()=>k,uiCopy:()=>nt});module.exports=Je(Ot);var de=["video/mp4;codecs=avc1.42E01E,mp4a.40.2","video/mp4;codecs=avc1.42E01E","video/mp4","video/webm;codecs=vp9,opus","video/webm;codecs=vp9","video/webm;codecs=vp8,opus","video/webm;codecs=vp8","video/webm"];function q(){let t=globalThis.MediaRecorder;return!t||typeof t.isTypeSupported!="function"?"":de.find(e=>t.isTypeSupported(e))??""}var L="video/webm";function Y(t){if(t instanceof TypeError)return"insecure";switch(t?.name??""){case"NotAllowedError":case"PermissionDeniedError":return"permission";case"NotFoundError":case"DevicesNotFoundError":return"notfound";case"NotReadableError":case"TrackStartError":return"inuse";case"OverconstrainedError":case"ConstraintNotSatisfiedError":return"overconstrained";default:return"generic"}}function Z(){let t=globalThis;if(typeof t.window<"u"&&t.window.isSecureContext===!1)return"INSECURE_CONTEXT";let e=t.navigator?.mediaDevices;return!e||typeof e.getUserMedia!="function"?"UNSUPPORTED":"OK"}function et(){let t=globalThis.MediaRecorder;return typeof t<"u"&&typeof t.isTypeSupported=="function"}var F=8*1024*1024;function S(t){return t.size>F}function X(t){let e=(t||"").toLowerCase();return e.startsWith("image/jpeg")?"jpg":e.startsWith("video/mp4")?"mp4":(e.startsWith("video/webm"),"webm")}function tt(t){let{token:e,clip:r,keyframe:i}=t,n=t.ts??Date.now(),a=r.type||L,s=i.type||"image/jpeg",o=new FormData;return o.append("token",e),o.append("clip",new File([r],`liveness-${n}.${X(a)}`,{type:a})),o.append("keyframe",new File([i],`keyframe-${n}.${X(s)}`,{type:s})),o}function Q(t,e,r=.92){return new Promise((i,n)=>{let a=t.videoWidth,s=t.videoHeight;if(!a||!s){n(new Error("video-not-ready"));return}let o=e.getContext("2d");if(!o){n(new Error("no-2d-context"));return}e.width=a,e.height=s,o.scale(-1,1),o.drawImage(t,-a,0),e.toBlob(c=>c?i(c):n(new Error("empty-keyframe")),"image/jpeg",r)})}var rt={permission:{key:"fcv2.err.permission",fr:"Autorisez la cam\xE9ra dans les r\xE9glages du navigateur (ic\xF4ne cam\xE9ra dans la barre d\u2019adresse), puis r\xE9essayez."},notfound:{key:"fcv2.err.notfound",fr:"Aucune cam\xE9ra d\xE9tect\xE9e. Branchez ou activez une cam\xE9ra, puis r\xE9essayez."},inuse:{key:"fcv2.err.inuse",fr:"La cam\xE9ra est utilis\xE9e par une autre application. Fermez-la puis r\xE9essayez."},overconstrained:{key:"fcv2.err.overconstrained",fr:"La cam\xE9ra ne prend pas en charge les r\xE9glages demand\xE9s. Nouvel essai en cours\u2026"},insecure:{key:"fcv2.err.insecure",fr:"La cam\xE9ra n\xE9cessite une connexion s\xE9curis\xE9e (HTTPS). Ouvrez la page en HTTPS."},unsupported:{key:"fcv2.err.unsupported",fr:"Votre navigateur ne prend pas en charge l\u2019enregistrement vid\xE9o. Utilisez un navigateur r\xE9cent."},generic:{key:"fcv2.err.generic",fr:"Cam\xE9ra indisponible. R\xE9essayez ou contactez le support."}},nt={recording:{key:"fcv2.recording",fr:"Enregistrement en cours\u2026"},uploading:{key:"fcv2.uploading",fr:"Envoi en cours\u2026"},clipTooLarge:{key:"fcv2.err.cliptoolarge",fr:"La vid\xE9o est trop volumineuse. R\xE9essayez dans un endroit mieux \xE9clair\xE9."},retry:{key:"fcv2.retry",fr:"R\xE9essayer"},listen:{key:"fcv2.listen",fr:"\xC9couter"}};var me=2500,pe=1e3,he=1500,j=25e5,f=class extends Error{constructor(e,r){super(r??e),this.kind=e}},D=class{constructor(){this.recorder=null;this.stream=null;this.chunks=[];this.bytes=0;this.hardStopTimer=null;this.graceTimer=null;this.settled=!1}clearTimers(){this.hardStopTimer&&clearTimeout(this.hardStopTimer),this.graceTimer&&clearTimeout(this.graceTimer),this.hardStopTimer=null,this.graceTimer=null}record(e,r){if(this.recorder&&!this.settled)return Promise.reject(new f("generic","record() called while a recording is in flight \u2014 ClipRecorder handles one recording at a time"));this.recorder=null,this.clearTimers(),this.chunks=[],this.bytes=0,this.settled=!1,this.stream=e;let i=q();return new Promise((n,a)=>{let s;try{s=new MediaRecorder(e,i?{mimeType:i,videoBitsPerSecond:j}:{videoBitsPerSecond:j})}catch{try{s=new MediaRecorder(e)}catch{a(new f("unsupported"));return}}this.recorder=s;let o=d=>{this.settled||(this.settled=!0,this.clearTimers(),d())},c=()=>o(()=>{let d=s.mimeType||i||L,u=new Blob(this.chunks,{type:d});if(u.size===0){a(new f("generic","empty clip"));return}if(S(u)){a(new f("cliptoolarge"));return}n({blob:u,mimeType:d})});s.ondataavailable=d=>{d.data&&d.data.size&&(this.chunks.push(d.data),this.bytes+=d.data.size,this.bytes>F&&o(()=>{try{s.state!=="inactive"&&s.stop()}catch{}a(new f("cliptoolarge"))}))},s.onstop=c,s.onerror=()=>o(()=>a(new f("generic")));try{s.start(pe)}catch{o(()=>a(new f("generic")));return}this.hardStopTimer=setTimeout(()=>{if(this.settled||r?.(),s.state!=="inactive"){try{s.stop()}catch{c()}this.graceTimer=setTimeout(c,he)}else c()},me)})}stop(e){this.clearTimers();let r=this.recorder;if(r&&r.state!=="inactive")try{r.stop()}catch{}this.recorder=null,(e??this.stream)?.getTracks().forEach(n=>n.stop()),this.stream=null}};function $(t,e){return{video:{width:{ideal:1280,max:1280},height:{ideal:720,max:720},facingMode:e,frameRate:{ideal:24,max:30}},audio:!1}}function J(){return typeof window>"u"||typeof navigator>"u"?!1:window.innerWidth<=768||/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(String(navigator.userAgent??""))}var it=.45,at=.9,st=.4;var fe=1e4,N=class t{constructor(e,r){this.tier="blaze";this.inner=e,r!==void 0&&(this.fallbackReason=r)}static async create(e,r){let i=await import("@mediapipe/tasks-vision"),n=await i.FilesetResolver.forVisionTasks(e),a=await i.FaceDetector.createFromOptions(n,{baseOptions:{modelAssetPath:`${e}/blaze_face_short_range.tflite`,delegate:"CPU"},runningMode:"IMAGE"});return new t(a,r)}async detect(e,r,i){let n;try{n=this.inner.detect(e).detections}catch{return null}let a=ot(n);if(a?.boundingBox===void 0)return null;let s=g(e),o=ct(a.keypoints,s),c=a.keypoints?.[2],d={box:{x:a.boundingBox.originX,y:a.boundingBox.originY,width:a.boundingBox.width,height:a.boundingBox.height},...o!==void 0?{eyes:o}:{},...c!==void 0?{nose:{x:c.x*s.width,y:c.y*s.height}}:{},score:a.categories[0]?.score??0};return k(d,e,r,i)}destroy(){try{this.inner.close()}catch{}}};function ot(t){let e,r=-1;for(let i of t){let n=i.categories[0]?.score??0;n>r&&(e=i,r=n)}return e}function ct(t,e){let r=t?.[0],i=t?.[1];if(r===void 0||i===void 0)return;let n=o=>({x:o.x*e.width,y:o.y*e.height}),[a,s]=[n(r),n(i)];return a.x<=s.x?{left:a,right:s}:{left:s,right:a}}function ut(){if(typeof OffscreenCanvas=="function")return new OffscreenCanvas(2,2);let t=document.createElement("canvas");return t.width=2,t.height=2,t}async function ye(){let t=globalThis.FaceDetector;if(typeof t!="function")return{ok:!1,reason:"unsupported"};try{let e=new t({fastMode:!0,maxDetectedFaces:1});return await e.detect(ut()),{ok:!0,instance:e}}catch(e){return{ok:!1,reason:`smoke_failed:${e instanceof Error?e.message:String(e)}`}}}var G=class{constructor(e,r){this.tier="native";this.inner=e,r!==void 0&&(this.fallbackReason=r)}async detect(e,r,i){let n;try{n=await this.inner.detect(e)}catch{return null}let a=n[0];if(a===void 0)return null;let s=lt(a.landmarks),o={box:{x:a.boundingBox.x,y:a.boundingBox.y,width:a.boundingBox.width,height:a.boundingBox.height},...s!==void 0?{eyes:s}:{},score:1};return k(o,e,r,i)}destroy(){}};function lt(t){if(t===void 0)return;let e=t.filter(n=>n.type==="eye").map(n=>n.locations[0]).filter(n=>n!==void 0),[r,i]=e;if(!(r===void 0||i===void 0))return r.x<=i.x?{left:r,right:i}:{left:i,right:r}}var be="/models-v3",ge=["native","blaze"],v=class extends Error{constructor(e){super(`no detector tier available (${e})`),this.name="DetectorLadderError",this.fallbackReason=e}};function g(t){let e=t;if(typeof e.videoWidth=="number"&&e.videoWidth>0)return{width:e.videoWidth,height:e.videoHeight??0};if(typeof e.naturalWidth=="number"&&e.naturalWidth>0)return{width:e.naturalWidth,height:e.naturalHeight??0};let r=typeof e.width=="number"?e.width:0,i=typeof e.height=="number"?e.height:0;return{width:r,height:i}}function k(t,e,r,i){let n=g(e),a=n.width>0?r/n.width:1,s=n.height>0?i/n.height:1,o=c=>({x:c.x*a,y:c.y*s});return{box:{x:t.box.x*a,y:t.box.y*s,width:t.box.width*a,height:t.box.height*s},...t.eyes!==void 0?{eyes:{left:o(t.eyes.left),right:o(t.eyes.right)}}:{},...t.nose!==void 0?{nose:o(t.nose)}:{},score:t.score}}function dt(t){return t instanceof Error?t.message:String(t)}async function E(t){let e=t.preferred??ge,r=[];for(let i of e){if(i==="native"){let n=await ye();if(n.ok)return new G(n.instance,ee(r));r.push(`native:${n.reason}`);continue}try{return await mt(N.create(t.v3AssetBaseUrl??be,ee(r)),t.blazeInitTimeoutMs??fe)}catch(n){r.push(n instanceof V?"blaze:blaze_init_timeout":`blaze:${dt(n)}`)}}throw new v(ee(r)??"no_tier_available")}function ee(t){return t.length>0?t.join("; "):void 0}var V=class extends Error{};function mt(t,e){return new Promise((r,i)=>{let n=setTimeout(()=>{i(new V(`init exceeded ${e}ms`)),t.then(a=>a.destroy(),()=>{})},e);t.then(a=>{clearTimeout(n),r(a)},a=>{clearTimeout(n),i(a)})})}var zt=180,T=[100,190],pt=64;function ve(t,e){return .299*(t[e]??0)+.587*(t[e+1]??0)+.114*(t[e+2]??0)}function W(t){let{data:e,width:r,height:i}=t,n=r*i;if(n===0)return 0;let a=0;for(let s=0;s<n;s+=1)a+=ve(e,s*4);return a/n}function K(t){let{data:e,width:r,height:i}=t;if(r<3||i<3)return 0;let n=new Float32Array(r*i);for(let u=0;u<n.length;u+=1)n[u]=ve(e,u*4);let a=0,s=0,o=0;for(let u=1;u<i-1;u+=1)for(let l=1;l<r-1;l+=1){let m=u*r+l,p=(n[m-r]??0)+(n[m-1]??0)+(n[m+1]??0)+(n[m+r]??0)-4*(n[m]??0);a+=p,s+=p*p,o+=1}if(o===0)return 0;let c=a/o,d=s/o-c*c;return Math.min(1,Math.max(0,d)/180)}function te(t){let e=W(t),[r,i]=T,n=e<r?r-e:e>i?e-i:0;return Math.max(0,1-n/pt)}var Ee=Object.freeze({Q_GOOD:.6,RISE_PER_S:.9,FALL_PER_S:.25,CENTER_FALLOFF_IOD_MULTIPLE:1.35,BOX_EYE_OFFSET_FACTOR:.12,IOD_BOX_PROXY_FACTOR:.42,SIZE_IDEAL_MIN:.12,SIZE_IDEAL_MAX:.3,SIZE_HARD_MIN:.08,SIZE_HARD_MAX:.45,ROLL_MAX_DEG:25,MOTION_VELOCITY_IOD_PER_S:.5,MOTION_TAU_S:.4,MOTION_DEADBAND_IOD_PER_S:.1,READY_WINDOW_MS:500,GUIDE_SIZE:.6,GUIDE_CENTERING:.6,GUIDE_POSE:.6,GUIDE_EXPOSURE:.6,GUIDE_SHARPNESS:.35,GUIDE_MOTION:.5}),_=t=>Math.min(1,Math.max(0,t)),C=class{constructor(e={}){this.p=0;this.lastTickAt=null;this.lastCenter=null;this.velocityEma=0;this.qWindow=[];this.lastSizeDirection="ideal";this.t={...Ee,...e}}reset(){this.p=0,this.lastTickAt=null,this.lastCenter=null,this.velocityEma=0,this.qWindow=[],this.lastSizeDirection="ideal"}tick(e){let{observation:r,quality:i,timestampMs:n}=e,a=this.lastTickAt===null?0:Math.max(0,(n-this.lastTickAt)/1e3);this.lastTickAt=n;let s={presence:0,centering:0,size:0,pose:0,sharpness:_(i.sharpness),exposure:_(i.exposure),motion:1};if(r!==null){s.presence=1;let u=this.geometryOf(r,e);s.centering=u.centering,s.size=u.size,s.pose=u.pose,s.motion=this.motionOf(u.center,u.iod,n)}else this.lastCenter=null,this.velocityEma=0;let o=Math.min(s.presence,s.centering,s.size,s.pose,s.sharpness,s.exposure,s.motion);this.p=_(this.p+(o>=this.t.Q_GOOD?this.t.RISE_PER_S*o:-this.t.FALL_PER_S*(1-o))*a),this.qWindow.push({at:n,q:o}),this.qWindow=this.qWindow.filter(u=>u.at>=n-this.t.READY_WINDOW_MS);let c=this.qWindow.map(u=>u.q);if(c.length>=3){let u=Math.min(...c),l=c.indexOf(u);c=c.filter((m,p)=>p!==l)}let d=this.p>=1&&c.every(u=>u>=this.t.Q_GOOD);return{at:n,q:o,p:this.p,factors:s,guidanceKey:this.guidanceOf(s,i,o),ready:d,accepted:o>=this.t.Q_GOOD}}geometryOf(e,r){let{box:i,eyes:n}=e,a={x:r.frameWidth/2,y:r.frameHeight/2},s=Math.min(r.frameWidth,r.frameHeight),o=n!==void 0?{x:(n.left.x+n.right.x)/2,y:(n.left.y+n.right.y)/2}:{x:i.x+i.width/2,y:i.y+i.height/2-this.t.BOX_EYE_OFFSET_FACTOR*i.height},c=n!==void 0?Math.hypot(n.right.x-n.left.x,n.right.y-n.left.y):this.t.IOD_BOX_PROXY_FACTOR*i.width,d=Math.hypot(o.x-a.x,o.y-a.y),u=c<=0?0:1-_(d/(this.t.CENTER_FALLOFF_IOD_MULTIPLE*c)),l=s>0?c/s:0;this.lastSizeDirection=l<this.t.SIZE_IDEAL_MIN?"far":l>this.t.SIZE_IDEAL_MAX?"close":"ideal";let m;l<this.t.SIZE_HARD_MIN||l>this.t.SIZE_HARD_MAX?m=0:l<this.t.SIZE_IDEAL_MIN?m=(l-this.t.SIZE_HARD_MIN)/(this.t.SIZE_IDEAL_MIN-this.t.SIZE_HARD_MIN):l>this.t.SIZE_IDEAL_MAX?m=(this.t.SIZE_HARD_MAX-l)/(this.t.SIZE_HARD_MAX-this.t.SIZE_IDEAL_MAX):m=1;let p=1;if(n!==void 0){let h=Math.abs(Math.atan2(n.right.y-n.left.y,n.right.x-n.left.x))*180/Math.PI;p=1-_(h/this.t.ROLL_MAX_DEG)}return{center:o,iod:c,centering:u,size:m,pose:p}}motionOf(e,r,i){let n=this.lastCenter;if(this.lastCenter={...e,at:i},n===null||r<=0)return 1;let a=(i-n.at)/1e3;if(a<=0)return 1;let s=Math.hypot(e.x-n.x,e.y-n.y)/r/a;s<=this.t.MOTION_DEADBAND_IOD_PER_S&&(s=0);let o=1-Math.exp(-a/this.t.MOTION_TAU_S);return this.velocityEma=o*s+(1-o)*this.velocityEma,1-_(this.velocityEma/this.t.MOTION_VELOCITY_IOD_PER_S)}guidanceOf(e,r,i){if(e.presence<=0)return"fcv3.no_face";if(e.size<this.t.GUIDE_SIZE)return this.sizeGuidance();if(e.centering<this.t.GUIDE_CENTERING)return"fcv3.center_face";if(e.pose<this.t.GUIDE_POSE)return"fcv3.straighten";if(e.exposure<this.t.GUIDE_EXPOSURE)return this.exposureGuidance(r);if(e.sharpness<this.t.GUIDE_SHARPNESS)return"fcv3.steady_camera";if(e.motion<this.t.GUIDE_MOTION)return"fcv3.hold_still";if(i<this.t.Q_GOOD){let n=[["size",this.sizeGuidance()],["centering","fcv3.center_face"],["pose","fcv3.straighten"],["exposure",this.exposureGuidance(r)],["sharpness","fcv3.steady_camera"],["motion","fcv3.hold_still"]],a=n[0];for(let s of n)e[s[0]]<e[a[0]]&&(a=s);return a[1]}return"fcv3.hold_still"}sizeGuidance(){return this.lastSizeDirection==="far"?"fcv3.come_closer":"fcv3.move_back"}exposureGuidance(e){let r=(T[0]+T[1])/2;return e.meanLuma<r?"fcv3.more_light":"fcv3.too_bright"}};var _e=Object.freeze({LOW:Object.freeze({cadenceHz:5,inputWidth:128,animations:"reduced"}),MID:Object.freeze({cadenceHz:8,inputWidth:160,animations:"full"}),HIGH:Object.freeze({cadenceHz:10,inputWidth:192,animations:"full"})}),ht=new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]);function ne(){let t=typeof navigator>"u"?void 0:navigator,e=!1;try{e=typeof WebAssembly<"u"&&WebAssembly.validate(ht)}catch{e=!1}return{cores:t?.hardwareConcurrency??2,memGB:t?.deviceMemory??null,simd:e}}function ie(t){let e=t.memGB??4;return t.cores<4||e<3?"LOW":t.cores>=8&&e>=6?"HIGH":"MID"}var re=["LOW","MID","HIGH"],M=class{constructor(e){this.samples=[];this.adjusted=!1;this.current=e}get deviceClass(){return this.current}get profile(){return _e[this.current]}record(e){if(this.adjusted||this.samples.length>=10)return this.current;if(this.samples.push(e),this.samples.length<10)return this.current;this.adjusted=!0;let r=this.samples.reduce((i,n)=>i+n,0)/this.samples.length;if(r>70)this.current="LOW";else if(r<25){let i=re.indexOf(this.current);this.current=re[Math.min(re.length-1,i+1)]??this.current}return this.current}};var w=class{constructor(e){this.timer=null;this.frameCallbackId=null;this.running=!1;this.busy=!1;this.lastRunAt=Number.NEGATIVE_INFINITY;this.degradeCount=0;this.durations=[];this.options=e,this.periodMs=1e3/e.cadenceHz}start(){if(this.running)return;this.running=!0;let e=this.options.video;if(typeof e?.requestVideoFrameCallback=="function"){let r=()=>{this.running&&(this.frameCallbackId=e.requestVideoFrameCallback?.(()=>{this.frameCallbackId=null,this.now()-this.lastRunAt>=this.periodMs&&this.tick(),r()}))};r();return}this.timer=setInterval(()=>this.tick(),this.periodMs)}stop(){this.running=!1,this.timer!==null&&(clearInterval(this.timer),this.timer=null),this.frameCallbackId!==null&&(this.options.video?.cancelVideoFrameCallback?.(this.frameCallbackId),this.frameCallbackId=null)}stats(){let e=this.durations.length===0?0:this.durations.reduce((r,i)=>r+i,0)/this.durations.length;return{degradeCount:this.degradeCount,meanDetectMs:e}}now(){return this.options.now?.()??performance.now()}tick(){if(!this.running)return;if(this.busy){this.degradeCount+=1;return}this.busy=!0,this.lastRunAt=this.now();let e=this.now();this.options.run().catch(()=>{}).finally(()=>{this.busy=!1,this.durations.push(this.now()-e),this.durations.length>20&&this.durations.shift()})}};var xe="0.1.0-rc.8";var Se="sekyro.did",De;function ke(){let t=globalThis.crypto;try{if(typeof t?.randomUUID=="function")return t.randomUUID()}catch{}try{if(typeof t?.getRandomValues=="function"){let e=t.getRandomValues(new Uint8Array(16));return Array.from(e,r=>r.toString(16).padStart(2,"0")).join("")}}catch{}}function ft(){try{let t=localStorage.getItem(Se);if(t)return t;let e=ke();return e!==void 0&&localStorage.setItem(Se,e),e}catch{return De??(De=ke()),De}}async function yt(){let t=globalThis.crypto?.subtle;if(!t)return"";let e=navigator,r=JSON.stringify([e.userAgent,e.platform,e.hardwareConcurrency??null,e.deviceMemory??null,e.maxTouchPoints??0,screen.width,screen.height,screen.colorDepth,globalThis.devicePixelRatio??1,Intl.DateTimeFormat().resolvedOptions().timeZone,e.languages?.join(",")??e.language]),i=await t.digest("SHA-256",new TextEncoder().encode(r));return Array.from(new Uint8Array(i),n=>n.toString(16).padStart(2,"0")).join("").slice(0,32)}function bt(t){let e=new TextEncoder().encode(t),r="";for(let i of e)r+=String.fromCharCode(i);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function gt(t){let e,r;return{timezone(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone?.slice(0,64)||void 0}catch{return}},noteCameraLabel(i){i&&(e=i.slice(0,128))},async deviceHeader(){try{r??(r=yt().catch(()=>""));let i=[];navigator.webdriver&&i.push("webdriver"),/HeadlessChrome/.test(navigator.userAgent)&&i.push("headless");let n=await r,a=ft(),s={v:1,platform:"web",sdk:t.sdk.slice(0,64),ids:[...a?[{kind:"web_local",value:a.slice(0,128)}]:[],...n?[{kind:"web_fp",value:n}]:[]],screen:{w:screen.width,h:screen.height,dpr:globalThis.devicePixelRatio??1},...e?{cameraLabel:e}:{},...i.length?{signals:i}:{}};return bt(JSON.stringify(s))}catch{return}}}}var ae=Symbol.for("sekyro.web.clientContext"),se=globalThis,vt=se[ae]??(se[ae]=gt({sdk:`@sekyro/web@${xe}`}));function Te(t){try{let e=t.getVideoTracks?.()[0];e&&vt.noteCameraLabel(e.label)}catch{}}var b=1;function Ce(){return typeof OffscreenCanvas=="function"&&typeof createImageBitmap=="function"&&typeof Worker=="function"}function Et(t,e={}){let r=e.createDetector??E,i=null;t.onmessage=n=>{let a=n.data;if(a.v!==b){a.bitmap?.close?.();return}if(a.type==="init"){r(a.options).then(s=>{i=s;let o={v:b,type:"ready",tier:s.tier,...s.fallbackReason!==void 0?{fallbackReason:s.fallbackReason}:{}};t.postMessage(o)});return}if(a.type==="detect"){let s=o=>{let c={v:b,type:"result",id:a.id,observation:o};t.postMessage(c),a.bitmap.close?.()};if(i===null){s(null);return}i.detect(a.bitmap,a.streamWidth,a.streamHeight).then(s,()=>s(null))}}}typeof self<"u"&&typeof self.importScripts=="function"&&Et(self);var H=class t{constructor(e,r){this.pending=new Map;this.nextId=1;this.worker=e,this.tier=r.tier,r.fallbackReason!==void 0&&(this.fallbackReason=r.fallbackReason),e.addEventListener("message",i=>{let n=i.data;if(n.v!==b||n.type!=="result")return;let a=this.pending.get(n.id);this.pending.delete(n.id),a?.(n.observation)})}static create(e,r,i=1e4){return new Promise((n,a)=>{let s=setTimeout(()=>{e.removeEventListener("message",o),e.terminate(),a(new Error(`worker detector not ready within ${i}ms`))},i),o=d=>{let u=d.data;u.v!==b||u.type!=="ready"||(clearTimeout(s),e.removeEventListener("message",o),n(new t(e,u)))};e.addEventListener("message",o);let c={v:b,type:"init",options:r};e.postMessage(c)})}async detect(e,r,i){let n;try{typeof ImageBitmap<"u"&&e instanceof ImageBitmap?n=e:typeof createImageBitmap=="function"?n=await createImageBitmap(e):n=e}catch{return null}let a=this.nextId;return this.nextId+=1,new Promise(s=>{this.pending.set(a,s);let o={v:b,type:"detect",id:a,bitmap:n,streamWidth:r,streamHeight:i};this.worker.postMessage(o,[n])})}destroy(){for(let e of this.pending.values())e(null);this.pending.clear(),this.worker.terminate()}};var _t=["size","centering","pose","exposure","sharpness","motion"];function Me(t){if(t.accepted)return null;if(t.factors.presence<=0)return"no_face";let e="size";for(let r of _t)t.factors[r]<t.factors[e]&&(e=r);return e}var O=class{constructor(){this.blocked={};this.acceptedMs=0;this.ticks=0;this.lastAt=null}record(e){let r=this.lastAt===null?0:Math.max(0,e.at-this.lastAt);this.lastAt=e.at,this.ticks+=1;let i=Me(e);if(i===null){this.acceptedMs+=r;return}let n=this.blocked[i]??{count:0,ms:0};n.count+=1,n.ms+=r,this.blocked[i]=n}summary(){let e={};for(let[r,i]of Object.entries(this.blocked))e[r]={count:i.count,ms:Math.round(i.ms)};return{blocked:e,acceptedMs:Math.round(this.acceptedMs),ticks:this.ticks}}};var We=1e3,U=4,Ke=600,Ge={sharpness:1,exposure:1,meanLuma:128},ce=class{constructor(e){this.handlers=new Map;this.stateValue="idle";this.attempt=0;this.stream=null;this.detector=null;this.scheduler=null;this.gate=null;this.profiler=null;this.lastGuidanceKey=null;this.positioningStartedAt=0;this.cameraRequestedAt=null;this.cameraReadyAt=null;this.faceAcquiredAt=null;this.captureCompleteAt=null;this.fired=!1;this.carriedDegrade=0;this.carriedMeans=[];this.blockStats=null;this.lastGateSnapshotAt=Number.NEGATIVE_INFINITY;this.snapshotCanvas=null;this.options=e,this.facing=e.facingMode??"user"}get state(){return this.stateValue}on(e,r){let i=this.handlers.get(e);return i||(i=new Set,this.handlers.set(e,i)),i.add(r),()=>i?.delete(r)}emit(e,r){for(let i of[...this.handlers.get(e)??[]])i(r)}async start(e={}){e.facingMode!==void 0&&(this.facing=e.facingMode),this.attempt+=1,await this.run(this.attempt)}get facingMode(){return this.facing}retry(){this.teardownAttempt(),this.start()}switchCamera(){this.facing=this.facing==="user"?"environment":"user",this.teardownAttempt(),this.start()}destroy(){this.attempt+=1,this.teardownAttempt(),this.detector?.destroy(),this.detector=null,this.options.video.srcObject=null}async run(e){let{options:r}=this;this.fired=!1,this.lastGuidanceKey=null,this.carriedDegrade=0,this.carriedMeans=[],this.cameraRequestedAt=null,this.cameraReadyAt=null,this.faceAcquiredAt=null,this.captureCompleteAt=null,this.setState("preflight");let i=Z();if(i!=="OK"){this.fail(i==="INSECURE_CONTEXT"?"insecure":"unsupported");return}this.setState("acquiring");let n=r.getUserMedia??(c=>navigator.mediaDevices.getUserMedia(c)),a;this.cameraRequestedAt=this.now();try{a=await n($(J(),this.facing))}catch(c){if(e!==this.attempt)return;this.fail(Y(c));return}if(Te(a),e!==this.attempt){for(let c of a.getTracks())c.stop();return}if(this.stream=a,r.video.srcObject=a,await r.video.play().catch(()=>{}),e!==this.attempt)return;if(this.cameraReadyAt=this.now(),this.detector===null){let c={...r.v3AssetBaseUrl!==void 0?{v3AssetBaseUrl:r.v3AssetBaseUrl}:{}},d=r.createDetector??(r.createWorker!==void 0&&Ce()?async()=>{try{return await H.create(r.createWorker(),c)}catch{return E(c)}}:()=>E(c)),u;try{u=await d()}catch(l){e===this.attempt&&this.fail("models",l instanceof v?l.fallbackReason:wt(l));return}if(e!==this.attempt){u.destroy();return}this.detector=u,this.options.telemetry?.({type:"detector",tier:u.tier,fallbackReason:u.fallbackReason})}let s=ne(),o=r.deviceClass??ie(s);this.profiler=new M(o),this.options.telemetry?.({type:"profile",staticClass:o,activeClass:this.profiler.deviceClass,simd:s.simd}),this.gate=new C(r.thresholds??{}),this.blockStats=new O,this.lastGateSnapshotAt=this.now(),r.rankKeyframes?.()&&Promise.resolve().then(()=>(Ne(),Fe)).then(c=>oe=c,()=>{}),this.setState("positioning"),this.positioningStartedAt=this.now(),this.startScheduler(e)}get animations(){return this.profiler?.profile.animations??"full"}startScheduler(e){let r=this.profiler;if(r===null)return;if(this.scheduler!==null){let n=this.scheduler.stats();this.carriedDegrade+=n.degradeCount,n.meanDetectMs>0&&this.carriedMeans.push(n.meanDetectMs),this.scheduler.stop()}let i=new w({cadenceHz:r.profile.cadenceHz,video:this.options.video,now:this.options.now,run:()=>this.detectTick(e)});this.scheduler=i,i.start()}async detectTick(e){let{detector:r,gate:i,profiler:n}=this,a=this.options.video;if(e!==this.attempt||r===null||i===null||n===null||this.fired||a.videoWidth===0)return;let s=n.deviceClass,o=this.now(),c=this.options.makeSnapshot?.(a,n.profile.inputWidth)??this.defaultSnapshot(a,n.profile.inputWidth),d=null;try{d=await r.detect(c,a.videoWidth,a.videoHeight)}catch{d=null}if(e!==this.attempt||this.fired)return;n.record(this.now()-o),n.deviceClass!==s&&this.startScheduler(e);let u=d===null?Ge:this.options.measureQuality?.(a,d.box)??this.defaultQuality(a,d.box),l=i.tick({timestampMs:this.now(),observation:d,quality:u,frameWidth:a.videoWidth,frameHeight:a.videoHeight});l.accepted&&this.faceAcquiredAt===null&&(this.faceAcquiredAt=l.at),this.blockStats?.record(l),!l.ready&&l.at-this.lastGateSnapshotAt>=We&&(this.lastGateSnapshotAt=l.at,this.reportGate(!1)),this.emit("checks",{engine:"v3",progress:l.p,q:l.q,factors:l.factors,attempt:e}),l.guidanceKey!==this.lastGuidanceKey&&(this.lastGuidanceKey=l.guidanceKey,this.emit("guidance",{key:l.guidanceKey})),l.ready&&this.stateValue==="positioning"&&(this.fired=!0,this.captureCompleteAt=l.at,this.fire(e))}async fire(e){let{options:r}=this,i=this.scheduler;i?.stop(),this.reportGate(!0);let n=i?.stats()??{degradeCount:0,meanDetectMs:0},a=[...this.carriedMeans,...n.meanDetectMs>0?[n.meanDetectMs]:[]];r.telemetry?.({type:"fired",timeToGateMs:this.now()-this.positioningStartedAt,degradeCount:this.carriedDegrade+n.degradeCount,meanDetectMs:a.length===0?0:a.reduce((h,y)=>h+y,0)/a.length});let s=r.mode??"clip",o=r.rankKeyframes?.()&&oe?oe.default(this,e).catch(()=>this.collectKeyframes(e)):this.collectKeyframes(e);if(s==="photo"){let h=await o;if(e!==this.attempt)return;if(h===null){this.fail("generic");return}this.finishCapture({keyframe:h.blob,previewDataUrl:h.previewDataUrl});return}this.setState("recording");let c=r.createRecorder?.()??new D,d=this.stream;if(d===null){this.fail("generic");return}let u=null;o.then(h=>{u=h},()=>{});let l=()=>{e===this.attempt&&u!==null&&this.emit("frozen",{previewDataUrl:u.previewDataUrl})},m,p;try{[m,p]=await Promise.all([c.record(d,l),o])}catch(h){if(e!==this.attempt)return;let y=h.kind;this.fail(y==="cliptoolarge"?"cliptoolarge":"recorder");return}if(e===this.attempt){if(S(m.blob)){this.fail("cliptoolarge");return}if(p===null){this.fail("generic");return}this.finishCapture({clip:m.blob,clipMimeType:m.mimeType,keyframe:p.blob,previewDataUrl:p.previewDataUrl})}}async collectKeyframes(e){let r=this.options.captureKeyframe??(a=>this.defaultKeyframe(a)),i=Ke/(U-1),n=null;for(let a=0;a<U;a+=1){if(e!==this.attempt)return n;try{let s=await r(this.options.video);(n===null||s.sharpness>n.sharpness)&&(n=s)}catch{}a<U-1&&await Mt(i)}return n}finishCapture(e){this.stopTracks(),this.setState("captured");let r=this.captureTiming();this.emit("artifacts",r===void 0?e:{...e,timing:r})}captureTiming(){let e=(s,o)=>s!==null&&o!==null?Math.max(0,Math.round(o-s)):void 0,r=e(this.cameraRequestedAt,this.cameraReadyAt),i=e(this.cameraReadyAt,this.faceAcquiredAt),n=e(this.faceAcquiredAt,this.captureCompleteAt),a={...r!==void 0?{cameraReadyMs:r}:{},...i!==void 0?{faceAcquireMs:i}:{},...n!==void 0?{captureMs:n}:{}};return Object.keys(a).length>0?a:void 0}async defaultKeyframe(e){let r=document.createElement("canvas"),i=await Q(e,r),n=r.toDataURL("image/jpeg",.92);return{blob:i,previewDataUrl:n,sharpness:this.centerSharpness(e)}}defaultSnapshot(e,r){try{let i=this.snapshotCanvas??document.createElement("canvas"),n=r/Math.max(1,e.videoWidth),a=Math.max(1,Math.round(e.videoWidth*n)),s=Math.max(1,Math.round(e.videoHeight*n)),o=i.getContext("2d");return o===null?e:(i.width!==a&&(i.width=a),i.height!==s&&(i.height=s),o.drawImage(e,0,0,a,s),this.snapshotCanvas=i,i)}catch{return e}}defaultQuality(e,r){let i=Ve(e,r,96);return i===null?Ge:{sharpness:K(i),exposure:te(i),meanLuma:W(i)}}centerSharpness(e){let r=g(e),i=Ve(e,{x:r.width*.25,y:r.height*.25,width:r.width*.5,height:r.height*.5},96);return i===null?0:K(i)}teardownAttempt(){this.scheduler?.stop(),this.scheduler=null,this.reportGate(!0),this.gate=null,this.stopTracks()}reportGate(e){let r=this.blockStats,i=this.profiler;if(r===null||i===null)return;let n=r.summary();e&&(this.blockStats=null),n.ticks!==0&&this.options.telemetry?.({type:"gate",final:e,activeClass:i.deviceClass,cadenceHz:i.profile.cadenceHz,...n})}stopTracks(){if(this.stream!==null){for(let e of this.stream.getTracks())e.stop();this.stream=null}}fail(e,r){this.scheduler?.stop(),this.reportGate(!0),this.stopTracks(),this.setState("error"),this.options.telemetry?.({type:"error",kind:e,fallbackReason:r}),this.emit("error",{kind:e,retryable:e!=="insecure"&&e!=="unsupported"})}setState(e){this.stateValue=e,this.emit("statechange",{state:e})}now(){return this.options.now?.()??performance.now()}},oe;function Mt(t){return new Promise(e=>setTimeout(e,t))}function wt(t){return t instanceof Error?t.message:String(t)}function Ve(t,e,r){try{let i=document.createElement("canvas"),n=r/Math.max(1,e.width);i.width=Math.max(1,Math.round(e.width*n)),i.height=Math.max(1,Math.round(e.height*n));let a=i.getContext("2d");return a===null?null:(a.drawImage(t,e.x,e.y,e.width,e.height,0,0,i.width,i.height),a.getImageData(0,0,i.width,i.height))}catch{return null}}0&&(module.exports={CENTERED_FACTOR,CIRCLE_RADIUS_FACTOR,CLIP_MS,CaptureSessionV3,ClipError,ClipRecorder,DEFAULT_CLIP_MIME,DEFAULT_TIER_ORDER,DEFAULT_V3_ASSET_BASE_URL,DEVICE_PROFILES,DetectScheduler,DetectorLadderError,DeviceProfiler,EXPOSURE_BAND,FaceGateV3,GATE_SNAPSHOT_MS,GateBlockStats,IN_CIRCLE_FACTOR,KEYFRAME_CANDIDATES,KEYFRAME_WINDOW_MS,MAX_CLIP_BYTES,MIME_CANDIDATES,SHARP_REF,STOP_GRACE_MS,TIMESLICE_MS,V3_GATE_DEFAULTS,VIDEO_BPS,blockReasonOf,buildCaptureConstraints,buildLivenessV2FormData,cameraPreflight,captureMirroredKeyframe,classifyDevice,createDetector,detectIsMobile,errorCopy,exposure,extForMime,intrinsicSize,isClipTooLarge,isMediaRecorderSupported,mapGetUserMediaError,meanLuma,pickMimeType,probeDevice,sharpness,toStreamPixels,uiCopy});