@incodetech/core 2.3.0-rc.9 → 2.3.0

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 (86) hide show
  1. package/dist/{BrowserDiagnosticsCryptoProvider-HO8YXP4h.esm.js → BrowserDiagnosticsCryptoProvider-BuzqvbhN.esm.js} +6 -24
  2. package/dist/{FaceDetectionProvider-C7bUZsH9.esm.js → FaceDetectionProvider-Lb6i4Ilt.esm.js} +7 -7
  3. package/dist/{MotionSensorProvider-BgTYwg9L.esm.js → MotionSensorProvider-DyhbM4Si.esm.js} +1 -1
  4. package/dist/ae-signature.esm.js +1 -1
  5. package/dist/{apiError-BWlcyGbw.esm.js → apiError-Dkl9_HOx.esm.js} +1 -1
  6. package/dist/authentication.d.ts +2 -2
  7. package/dist/authentication.esm.js +2 -2
  8. package/dist/{authenticationManager-DcTHmYmL.d.ts → authenticationManager-B23V74p7.d.ts} +1 -1
  9. package/dist/{authenticationManager-B321A87w.esm.js → authenticationManager-C7BaQC99.esm.js} +4 -4
  10. package/dist/{authenticationStateMachine-CSJtf13E.esm.js → authenticationStateMachine-CW7a9qpV.esm.js} +3 -3
  11. package/dist/camera.esm.js +1 -1
  12. package/dist/{createCameraCycleOwnership-CX3hJGKc.esm.js → createCameraCycleOwnership-DnwTl1ad.esm.js} +6 -1
  13. package/dist/{deepsightLoader-FOktaF-9.esm.js → deepsightLoader-DgaaacGJ.esm.js} +6 -6
  14. package/dist/document-capture.esm.js +2 -2
  15. package/dist/{documentCaptureManager-pprJUhsN.esm.js → documentCaptureManager-BRPxqYD2.esm.js} +1 -1
  16. package/dist/{documentCaptureStateMachine-DN2YOxdu.esm.js → documentCaptureStateMachine-D312sSIu.esm.js} +2 -2
  17. package/dist/dynamic-forms.esm.js +2 -2
  18. package/dist/{dynamicFormsManager-Pj67RmHb.esm.js → dynamicFormsManager-Cm33oGqC.esm.js} +1 -1
  19. package/dist/{dynamicFormsStateMachine-B00MVHEa.esm.js → dynamicFormsStateMachine-4EVsg15i.esm.js} +1 -1
  20. package/dist/ekyb.esm.js +3 -3
  21. package/dist/{ekybManager-CovxKSIS.esm.js → ekybManager-CgjQ2Thl.esm.js} +1 -1
  22. package/dist/{ekybStateMachine-DHCg1B5d.esm.js → ekybStateMachine-Dwgt5Dqa.esm.js} +1 -1
  23. package/dist/electronic-signature.esm.js +1 -1
  24. package/dist/{electronicSignatureManager-BnlLBzH6.esm.js → electronicSignatureManager-DGEUq9nY.esm.js} +1 -1
  25. package/dist/extensibility.d.ts +6 -6
  26. package/dist/extensibility.esm.js +12 -12
  27. package/dist/{faceCaptureManagerFactory-DTqP1t1a.esm.js → faceCaptureManagerFactory-3x7qaM76.esm.js} +3 -3
  28. package/dist/{faceCaptureManagerFactory-Cx-8SqRy.d.ts → faceCaptureManagerFactory-DMLstvui.d.ts} +35 -3
  29. package/dist/{faceCaptureSetup-DfetEHJm.esm.js → faceCaptureSetup-CJzsvv1I.esm.js} +10 -9
  30. package/dist/{feature-management-BjROP6fF.esm.js → feature-management-BG6sf4O4.esm.js} +1 -1
  31. package/dist/feature-management.esm.js +1 -1
  32. package/dist/flow.esm.js +3 -3
  33. package/dist/{flowCompletionService-iyH7fw8r.esm.js → flowCompletionService-BusrkMdp.esm.js} +1 -1
  34. package/dist/{flowServices-C8CBt9j3.esm.js → flowServices-pIc1cq8q.esm.js} +5 -5
  35. package/dist/geolocation.esm.js +2 -2
  36. package/dist/{geolocationManager-DQ5tHq5k.esm.js → geolocationManager-BbgMboZ-.esm.js} +1 -1
  37. package/dist/{geolocationStateMachine-BfNIvTHA.esm.js → geolocationStateMachine-Bixn87pI.esm.js} +1 -1
  38. package/dist/id-selfie-flow.d.ts +1 -1
  39. package/dist/id-selfie-flow.esm.js +9 -10
  40. package/dist/id.d.ts +1 -1
  41. package/dist/id.esm.js +3 -3
  42. package/dist/{idCaptureDefaults-DCmwAxkz.esm.js → idCaptureDefaults-BmnsDgZZ.esm.js} +3 -3
  43. package/dist/{idCaptureManager-De7O0TOD.d.ts → idCaptureManager-BaHEyQyr.d.ts} +0 -1
  44. package/dist/{idCaptureManager-C4gJNe18.esm.js → idCaptureManager-BpdRLyqZ.esm.js} +5 -5
  45. package/dist/{idCaptureServices-CPh--aZR.esm.js → idCaptureServices-Di_APvz_.esm.js} +9 -8
  46. package/dist/{idCaptureStateMachine-Du4Q40uJ.esm.js → idCaptureStateMachine-hY-RpDH_.esm.js} +14 -14
  47. package/dist/identity-reuse.d.ts +1 -1
  48. package/dist/{identityReuseManager-v2TH5g7e.d.ts → identityReuseManager-BFDI7iFK.d.ts} +4 -4
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.esm.js +1 -1
  51. package/dist/mandatory-consent.d.ts +1 -1
  52. package/dist/{mandatoryConsentManager-2O5wztkg.d.ts → mandatoryConsentManager-Csfl7_XQ.d.ts} +4 -4
  53. package/dist/{multipartRecordingService-DDAOXQKm.esm.js → multipartRecordingService-mwKFTlhR.esm.js} +730 -8
  54. package/dist/{permissionGuards-uYZYFu4J.esm.js → permissionGuards-D6TfVop2.esm.js} +1 -1
  55. package/dist/{permissionServices-Bj_fyWtv.esm.js → permissionServices-EY3_Vx6c.esm.js} +2 -2
  56. package/dist/qe-signature.esm.js +1 -1
  57. package/dist/{runChildModule-CHjWYSMf.esm.js → runChildModule-kZQwHJ21.esm.js} +1 -1
  58. package/dist/{safeStorage-B-trhxg6.esm.js → safeStorage-BdU9_m2E.esm.js} +1 -1
  59. package/dist/{sdkVersion-DCLQtexS.esm.js → sdkVersion-KY79FtZx.esm.js} +1 -1
  60. package/dist/selfie.d.ts +2 -2
  61. package/dist/selfie.esm.js +7 -7
  62. package/dist/{selfieManager-ys-X4hkv.esm.js → selfieManager-Bjane9c3.esm.js} +4 -4
  63. package/dist/{selfieManager-jXqwqosi.d.ts → selfieManager-CDCTOrcg.d.ts} +1 -1
  64. package/dist/{selfieStateMachine-BhzbXmKN.esm.js → selfieStateMachine-OsbjyRR6.esm.js} +3 -3
  65. package/dist/{session-oKiwoHdR.esm.js → session-D4EAR74k.esm.js} +3 -3
  66. package/dist/session.esm.js +4 -4
  67. package/dist/{sessionInitializer-DSf0jyab.esm.js → sessionInitializer-BqGb0lFN.esm.js} +3 -3
  68. package/dist/{setup-BttZ27hw.esm.js → setup-Cc6MzR5Q.esm.js} +7 -57
  69. package/dist/{setup-CLfBztP0.d.ts → setup-Cyko6cAn.d.ts} +4 -17
  70. package/dist/{trueSightService-C1JG8gAU.esm.js → trueSightService-Di5RGIls.esm.js} +16 -554
  71. package/dist/video-selfie.d.ts +61 -3
  72. package/dist/video-selfie.esm.js +60 -14
  73. package/dist/{videoSelfieCameraWarmer-BpaLtLvD.esm.js → videoSelfieCameraWarmer-PV8PLwmD.esm.js} +5 -5
  74. package/dist/{videoSelfieStepDetector-Co0Gu2k2.d.ts → videoSelfieStepDetector-CAqAG2G3.d.ts} +49 -0
  75. package/dist/{warmup-CUUnwYxe.esm.js → warmup-ChDwHSel.esm.js} +5 -0
  76. package/dist/wasm.d.ts +1 -1
  77. package/dist/wasm.esm.js +3 -3
  78. package/dist/{wasmDsFlagSync-B2e8p268.esm.js → wasmDsFlagSync-DYrLUEII.esm.js} +1 -1
  79. package/dist/workflow.d.ts +4 -4
  80. package/dist/workflow.esm.js +15 -15
  81. package/package.json +2 -7
  82. package/dist/FaceDetectionProvider-LdZ47bWy.d.ts +0 -36
  83. package/dist/StreamCanvasProcessingSession-CBnbTnOr.esm.js +0 -42
  84. package/dist/personhood.d.ts +0 -538
  85. package/dist/personhood.esm.js +0 -1574
  86. package/dist/types-BT_Oalip.d.ts +0 -1
@@ -2,6 +2,47 @@ import { t as BrowserTimerProvider } from "./BrowserTimerProvider-BXhgNxLt.esm.j
2
2
  import { t as api } from "./api-Bx3gKW_y.esm.js";
3
3
  import { t as endpoints } from "./endpoints-B-3Q-LUw.esm.js";
4
4
  import { a as isIOS } from "./platform-DkcRc9JV.esm.js";
5
+ //#region ../infra/src/media/StreamCanvasProcessingSession.ts
6
+ var StreamCanvasProcessingSession = class {
7
+ /**
8
+ * Creates a processing session that reacts to `StreamCanvasCapture` frame events
9
+ * and drives a provider's `processFrame()` method with backpressure.
10
+ */
11
+ constructor(params) {
12
+ this.disposed = false;
13
+ this.isProcessing = false;
14
+ this.onFrameEvent = () => {
15
+ if (this.disposed || this.isProcessing) return;
16
+ const frame = this.capturer.getLatestFrame();
17
+ if (!frame) return;
18
+ this.onFrame?.(frame);
19
+ this.isProcessing = true;
20
+ this.provider.processFrame(frame).catch(() => {}).finally(() => {
21
+ this.isProcessing = false;
22
+ });
23
+ };
24
+ this.capturer = params.capturer;
25
+ this.provider = params.provider;
26
+ this.onFrame = params.onFrame;
27
+ this.capturer.addEventListener("frame", this.onFrameEvent);
28
+ }
29
+ /**
30
+ * Unsubscribes from frame events and resets the provider.
31
+ */
32
+ dispose() {
33
+ if (this.disposed) return;
34
+ this.disposed = true;
35
+ this.capturer.removeEventListener("frame", this.onFrameEvent);
36
+ this.provider.reset();
37
+ }
38
+ /**
39
+ * Returns whether the session has been disposed.
40
+ */
41
+ isDisposed() {
42
+ return this.disposed;
43
+ }
44
+ };
45
+ //#endregion
5
46
  //#region ../infra/src/media/mediaRecorderOptions.ts
6
47
  /**
7
48
  * Picks a `MediaRecorder` mime type the current browser actually supports.
@@ -42,6 +83,33 @@ function getAdaptiveMediaRecorderOptions() {
42
83
  bitsPerSecond: 5e5
43
84
  };
44
85
  }
86
+ /**
87
+ * Worker-encoded multipart recording profile (ENG-55397). 2 GB-class devices
88
+ * (`navigator.deviceMemory` ≤ 2) record 360p at 15 fps: on a Galaxy A01 Core,
89
+ * 720p30 plus WASM detection exhausted memory (renderer swapped, GPU process
90
+ * killed), and even the in-worker encoder starved the camera preview to
91
+ * 6 fps. They also upload 2 s parts: every part is one request through the
92
+ * WASM HTTP client on the main thread, and at 0.5 s parts the uploads fell
93
+ * behind during face detection, stretching the final "processing" wait.
94
+ * Other devices, and browsers without `deviceMemory`, keep 720p30 at the
95
+ * MediaRecorder bitrate and 0.5 s parts.
96
+ */
97
+ function getWorkerRecordingProfile(deviceMemoryGb = typeof navigator !== "undefined" ? navigator.deviceMemory : void 0) {
98
+ if (deviceMemoryGb !== void 0 && deviceMemoryGb <= 2) return {
99
+ maxLongEdge: 640,
100
+ maxShortEdge: 360,
101
+ fps: 15,
102
+ bitrate: 6e5,
103
+ timesliceMs: 2e3
104
+ };
105
+ return {
106
+ maxLongEdge: 1280,
107
+ maxShortEdge: 720,
108
+ fps: 30,
109
+ bitrate: 25e5,
110
+ timesliceMs: 500
111
+ };
112
+ }
45
113
  //#endregion
46
114
  //#region ../infra/src/media/cameraCompositorWorker.ts
47
115
  /**
@@ -81,6 +149,9 @@ function cameraCompositorWorkerMain(scope) {
81
149
  let generation = 0;
82
150
  let paused = false;
83
151
  let announceNextFrame = false;
152
+ let minFrameIntervalUs = 0;
153
+ let lastWrittenUs = -Infinity;
154
+ let encodeConfig = null;
84
155
  const boundedSize = (width, height) => {
85
156
  const scale = Math.min(1, maxLongEdge / Math.max(width, height), maxShortEdge / Math.min(width, height));
86
157
  return {
@@ -100,6 +171,423 @@ function cameraCompositorWorkerMain(scope) {
100
171
  frame.close();
101
172
  return out;
102
173
  };
174
+ const video = {
175
+ number: 1,
176
+ offsetUs: null,
177
+ lastUs: -1
178
+ };
179
+ const audio = {
180
+ number: 2,
181
+ offsetUs: null,
182
+ lastUs: -1
183
+ };
184
+ let videoEncoder = null;
185
+ let audioEncoder = null;
186
+ let hasAudioSource = false;
187
+ let audioWanted = false;
188
+ let audioFormat = null;
189
+ let audioInHeader = false;
190
+ /**
191
+ * The Opus encoder numbers its output by sample count from its first input,
192
+ * so track where that count has reached on our timeline (µs).
193
+ */
194
+ let audioClockStartUs = null;
195
+ let audioFedUs = 0;
196
+ let recording = "off";
197
+ let encodedSize = {
198
+ width: 0,
199
+ height: 0
200
+ };
201
+ let originMs = 0;
202
+ let pausedMs = 0;
203
+ let pauseStartedMs = 0;
204
+ /** Pacer: source time the next frame is due (schedule, not last frame). */
205
+ let nextDueUs = -Infinity;
206
+ let lastKeyUs = -Infinity;
207
+ let forceKey = true;
208
+ let headerSent = false;
209
+ let lastFlushMs = 0;
210
+ /** Oldest pending block timestamp (µs); pending is small but hot. */
211
+ let oldestPendingUs = Infinity;
212
+ let pending = [];
213
+ const concat = (parts) => {
214
+ let length = 0;
215
+ for (const part of parts) length += part.length;
216
+ const out = new Uint8Array(length);
217
+ let offset = 0;
218
+ for (const part of parts) {
219
+ out.set(part, offset);
220
+ offset += part.length;
221
+ }
222
+ return out;
223
+ };
224
+ const uint = (value) => {
225
+ const out = [];
226
+ let rest = value;
227
+ do {
228
+ out.unshift(rest % 256);
229
+ rest = Math.floor(rest / 256);
230
+ } while (rest > 0);
231
+ return out;
232
+ };
233
+ /** EBML variable-length size, shortest form. */
234
+ const vsize = (size) => {
235
+ for (let length = 1; length <= 8; length += 1) if (size < 2 ** (7 * length) - 1) {
236
+ const out = uint(size);
237
+ while (out.length < length) out.unshift(0);
238
+ out[0] |= 128 >> length - 1;
239
+ return out;
240
+ }
241
+ throw new Error("EBML element too large");
242
+ };
243
+ const element = (id, ...body) => {
244
+ const data = concat(body);
245
+ return concat([
246
+ id,
247
+ vsize(data.length),
248
+ data
249
+ ]);
250
+ };
251
+ const text = (value) => Array.from(value, (char) => char.charCodeAt(0));
252
+ const float64 = (value) => {
253
+ const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
254
+ view.setFloat64(0, value);
255
+ return Array.from(new Uint8Array(view.buffer));
256
+ };
257
+ /** RFC 7845 identification header: version 1, pre-skip 312, family 0. */
258
+ const opusHead = (channels, sampleRate) => [
259
+ ...text("OpusHead"),
260
+ 1,
261
+ channels,
262
+ 56,
263
+ 1,
264
+ sampleRate & 255,
265
+ sampleRate >> 8 & 255,
266
+ sampleRate >> 16 & 255,
267
+ sampleRate >> 24 & 255,
268
+ 0,
269
+ 0,
270
+ 0
271
+ ];
272
+ const webmHeader = () => {
273
+ const videoEntry = element([174], element([215], [1]), element([115, 197], [1]), element([131], [1]), element([134], text("V_VP8")), element([224], element([176], uint(encodedSize.width)), element([186], uint(encodedSize.height))));
274
+ const audioEntry = audioInHeader && audioFormat ? element([174], element([215], [2]), element([115, 197], [2]), element([131], [2]), element([134], text("A_OPUS")), element([99, 162], opusHead(audioFormat.channels, audioFormat.sampleRate)), element([225], element([181], float64(48e3)), element([159], [audioFormat.channels]))) : /* @__PURE__ */ new Uint8Array(0);
275
+ return concat([
276
+ element([
277
+ 26,
278
+ 69,
279
+ 223,
280
+ 163
281
+ ], element([66, 134], [1]), element([66, 247], [1]), element([66, 242], [4]), element([66, 243], [8]), element([66, 130], text("webm")), element([66, 135], [4]), element([66, 133], [2])),
282
+ [
283
+ 24,
284
+ 83,
285
+ 128,
286
+ 103,
287
+ 1,
288
+ 255,
289
+ 255,
290
+ 255,
291
+ 255,
292
+ 255,
293
+ 255,
294
+ 255
295
+ ],
296
+ element([
297
+ 21,
298
+ 73,
299
+ 169,
300
+ 102
301
+ ], element([
302
+ 42,
303
+ 215,
304
+ 177
305
+ ], uint(1e6)), element([77, 128], text("incode")), element([87, 65], text("incode"))),
306
+ element([
307
+ 22,
308
+ 84,
309
+ 174,
310
+ 107
311
+ ], videoEntry, audioEntry)
312
+ ]);
313
+ };
314
+ /**
315
+ * Writes every pending block older than `limitUs` as one cluster (one
316
+ * upload part). The first part carries the header, so it waits (up to
317
+ * 3 s) for the microphone's format when a microphone is attached.
318
+ */
319
+ const flushBefore = (limitUs, force = false) => {
320
+ if (pending.length === 0) return;
321
+ const oldestUs = oldestPendingUs;
322
+ const timesliceMs = encodeConfig?.timesliceMs ?? 500;
323
+ if (!force && performance.now() - lastFlushMs < timesliceMs * .8) return;
324
+ if (!force && !headerSent && audioWanted && !audioFormat && limitUs - oldestUs < 3e6) return;
325
+ const ready = pending.filter((block) => block.us < limitUs).sort((a, b) => a.us - b.us || a.track - b.track);
326
+ if (ready.length === 0) return;
327
+ pending = pending.filter((block) => block.us >= limitUs);
328
+ oldestPendingUs = pending.reduce((min, block) => Math.min(min, block.us), Infinity);
329
+ if (!headerSent) audioInHeader = audioFormat !== null;
330
+ const blocks = ready.filter((block) => block.track === 1 || audioInHeader);
331
+ if (blocks.length === 0) return;
332
+ const clusters = [];
333
+ let run = [];
334
+ const closeRun = () => {
335
+ if (run.length === 0) return;
336
+ const clusterMs = Math.floor(run[0].us / 1e3);
337
+ clusters.push(element([
338
+ 31,
339
+ 67,
340
+ 182,
341
+ 117
342
+ ], element([231], uint(clusterMs)), ...run.map((block) => {
343
+ const relative = Math.round(block.us / 1e3) - clusterMs;
344
+ return element([163], [
345
+ 128 | block.track,
346
+ relative >> 8 & 255,
347
+ relative & 255,
348
+ block.key ? 128 : 0
349
+ ], block.data);
350
+ })));
351
+ run = [];
352
+ };
353
+ for (const block of blocks) {
354
+ if (run.length > 0 && block.us - run[0].us > 3e7) closeRun();
355
+ run.push(block);
356
+ }
357
+ closeRun();
358
+ const cluster = concat(clusters);
359
+ const part = headerSent ? cluster : concat([webmHeader(), cluster]);
360
+ headerSent = true;
361
+ lastFlushMs = performance.now();
362
+ scope.postMessage({
363
+ type: "part",
364
+ data: part.buffer
365
+ }, [part.buffer]);
366
+ };
367
+ const onEncoded = (track) => (chunk) => {
368
+ const data = new Uint8Array(chunk.byteLength);
369
+ chunk.copyTo(data);
370
+ pending.push({
371
+ us: chunk.timestamp,
372
+ data,
373
+ key: chunk.type === "key",
374
+ track: track.number
375
+ });
376
+ oldestPendingUs = Math.min(oldestPendingUs, chunk.timestamp);
377
+ const timesliceUs = (encodeConfig?.timesliceMs ?? 500) * 1e3;
378
+ const spanUs = chunk.timestamp - oldestPendingUs;
379
+ if (track === video && chunk.type === "key" && spanUs >= timesliceUs / 2) flushBefore(chunk.timestamp);
380
+ else if (spanUs > timesliceUs * 3) flushBefore(chunk.timestamp - 2e5);
381
+ };
382
+ const timelineNowUs = () => (performance.now() - originMs - pausedMs) * 1e3;
383
+ const anchor = (track, sourceUs) => {
384
+ const nowUs = timelineNowUs();
385
+ if (track === audio && track.offsetUs !== null && nowUs - (sourceUs + track.offsetUs) > 25e4) track.offsetUs = null;
386
+ if (track.offsetUs === null) track.offsetUs = nowUs - sourceUs;
387
+ const us = Math.max(Math.round(sourceUs + track.offsetUs), track.lastUs + 1);
388
+ track.lastUs = us;
389
+ return us;
390
+ };
391
+ const reanchor = () => {
392
+ video.offsetUs = null;
393
+ audio.offsetUs = null;
394
+ nextDueUs = -Infinity;
395
+ forceKey = true;
396
+ };
397
+ const resetRecording = () => {
398
+ videoEncoder = null;
399
+ audioEncoder = null;
400
+ audioWanted = hasAudioSource;
401
+ audioFormat = null;
402
+ audioInHeader = false;
403
+ audioClockStartUs = null;
404
+ audioFedUs = 0;
405
+ originMs = performance.now();
406
+ pausedMs = 0;
407
+ video.lastUs = -1;
408
+ audio.lastUs = -1;
409
+ reanchor();
410
+ lastKeyUs = -Infinity;
411
+ headerSent = false;
412
+ lastFlushMs = 0;
413
+ oldestPendingUs = Infinity;
414
+ pending = [];
415
+ };
416
+ const encodeVideo = (frame) => {
417
+ const config = encodeConfig;
418
+ if (!config) {
419
+ frame.close();
420
+ return;
421
+ }
422
+ if (minFrameIntervalUs > 0) {
423
+ const intervalUs = 1e6 / config.fps;
424
+ if (frame.timestamp < nextDueUs - intervalUs * .2) {
425
+ frame.close();
426
+ return;
427
+ }
428
+ nextDueUs = nextDueUs === -Infinity || frame.timestamp - nextDueUs > intervalUs ? frame.timestamp + intervalUs : nextDueUs + intervalUs;
429
+ }
430
+ if (!videoEncoder) {
431
+ const size = boundedSize(frame.displayWidth, frame.displayHeight);
432
+ encodedSize = {
433
+ width: Math.max(2, size.width - size.width % 2),
434
+ height: Math.max(2, size.height - size.height % 2)
435
+ };
436
+ videoEncoder = new g.VideoEncoder({
437
+ output: onEncoded(video),
438
+ error: (error) => scope.postMessage({
439
+ type: "record-error",
440
+ message: String(error)
441
+ })
442
+ });
443
+ videoEncoder.configure({
444
+ codec: config.codec,
445
+ width: encodedSize.width,
446
+ height: encodedSize.height,
447
+ bitrate: config.bitrate,
448
+ framerate: config.fps,
449
+ latencyMode: "realtime"
450
+ });
451
+ }
452
+ if (videoEncoder.state !== "configured" || videoEncoder.encodeQueueSize > 4) {
453
+ frame.close();
454
+ return;
455
+ }
456
+ const us = anchor(video, frame.timestamp);
457
+ const keyFrame = forceKey || us - lastKeyUs >= config.timesliceMs * 1e3;
458
+ if (keyFrame) {
459
+ lastKeyUs = us;
460
+ forceKey = false;
461
+ }
462
+ const rebased = new g.VideoFrame(frame, { timestamp: us });
463
+ frame.close();
464
+ videoEncoder.encode(rebased, { keyFrame });
465
+ rebased.close();
466
+ };
467
+ /** AudioData cannot be re-timestamped in place; copying 10 ms is cheap. */
468
+ const retimed = (data, us) => {
469
+ const planes = data.format.endsWith("-planar") ? data.numberOfChannels : 1;
470
+ const sizes = [];
471
+ for (let plane = 0; plane < planes; plane += 1) sizes.push(data.allocationSize({ planeIndex: plane }));
472
+ const buffer = new Uint8Array(sizes.reduce((sum, size) => sum + size, 0));
473
+ let offset = 0;
474
+ for (let plane = 0; plane < planes; plane += 1) {
475
+ data.copyTo(buffer.subarray(offset, offset + sizes[plane]), { planeIndex: plane });
476
+ offset += sizes[plane];
477
+ }
478
+ return new g.AudioData({
479
+ format: data.format,
480
+ sampleRate: data.sampleRate,
481
+ numberOfFrames: data.numberOfFrames,
482
+ numberOfChannels: data.numberOfChannels,
483
+ timestamp: us,
484
+ data: buffer
485
+ });
486
+ };
487
+ const bytesPerSample = (format) => format.startsWith("u8") ? 1 : format.startsWith("s16") ? 2 : 4;
488
+ /** Zero-filled AudioData (silence for every signed / float format). */
489
+ const silence = (like, us, durationUs) => {
490
+ const frames = Math.max(1, Math.round(durationUs * like.sampleRate / 1e6));
491
+ return new g.AudioData({
492
+ format: like.format,
493
+ sampleRate: like.sampleRate,
494
+ numberOfFrames: frames,
495
+ numberOfChannels: like.numberOfChannels,
496
+ timestamp: us,
497
+ data: new Uint8Array(frames * like.numberOfChannels * bytesPerSample(like.format))
498
+ });
499
+ };
500
+ const encodeAudio = (data) => {
501
+ const config = encodeConfig;
502
+ if (!config || !audioWanted) {
503
+ data.close();
504
+ return;
505
+ }
506
+ if (!audioEncoder) try {
507
+ const encoderInstance = new g.AudioEncoder({
508
+ output: onEncoded(audio),
509
+ error: (error) => {
510
+ audioWanted = false;
511
+ audioEncoder = null;
512
+ scope.postMessage({
513
+ type: "record-error",
514
+ message: `audio encoder: ${String(error)}`
515
+ });
516
+ }
517
+ });
518
+ encoderInstance.configure({
519
+ codec: "opus",
520
+ sampleRate: data.sampleRate,
521
+ numberOfChannels: data.numberOfChannels,
522
+ bitrate: config.audioBitrate ?? 64e3
523
+ });
524
+ audioEncoder = encoderInstance;
525
+ audioFormat = {
526
+ sampleRate: data.sampleRate,
527
+ channels: data.numberOfChannels
528
+ };
529
+ } catch (error) {
530
+ audioWanted = false;
531
+ scope.postMessage({
532
+ type: "record-error",
533
+ message: `audio configure: ${String(error)}`
534
+ });
535
+ data.close();
536
+ return;
537
+ }
538
+ if (audioEncoder.state !== "configured" || audioEncoder.encodeQueueSize > 50) {
539
+ data.close();
540
+ return;
541
+ }
542
+ const us = anchor(audio, data.timestamp);
543
+ audioClockStartUs ??= us;
544
+ const driftUs = us - (audioClockStartUs + audioFedUs);
545
+ if (driftUs < -2e5) {
546
+ data.close();
547
+ return;
548
+ }
549
+ if (driftUs > 1e5) {
550
+ const fillUs = Math.min(driftUs, 5e6);
551
+ const filler = silence(data, audioClockStartUs + audioFedUs, fillUs);
552
+ audioEncoder.encode(filler);
553
+ filler.close();
554
+ audioFedUs += filler.numberOfFrames * 1e6 / filler.sampleRate;
555
+ }
556
+ audioFedUs += data.numberOfFrames * 1e6 / data.sampleRate;
557
+ const copy = retimed(data, us);
558
+ data.close();
559
+ audioEncoder.encode(copy);
560
+ copy.close();
561
+ };
562
+ const pumpAudio = async (readable) => {
563
+ const reader = readable.getReader();
564
+ try {
565
+ for (;;) {
566
+ const { value, done } = await reader.read();
567
+ if (done) return;
568
+ if (paused || recording !== "on") value.close();
569
+ else encodeAudio(value);
570
+ }
571
+ } catch (error) {
572
+ audioWanted = false;
573
+ scope.postMessage({
574
+ type: "record-error",
575
+ message: `audio source: ${String(error)}`
576
+ });
577
+ }
578
+ };
579
+ const stopRecording = async () => {
580
+ recording = "off";
581
+ for (const current of [videoEncoder, audioEncoder]) try {
582
+ if (current && current.state === "configured") await current.flush();
583
+ } catch {}
584
+ flushBefore(Infinity, true);
585
+ for (const current of [videoEncoder, audioEncoder]) try {
586
+ current?.close();
587
+ } catch {}
588
+ resetRecording();
589
+ scope.postMessage({ type: "record-stopped" });
590
+ };
103
591
  const pump = async (readable, gen) => {
104
592
  const reader = readable.getReader();
105
593
  activeReader = reader;
@@ -114,10 +602,21 @@ function cameraCompositorWorkerMain(scope) {
114
602
  frame.close();
115
603
  return;
116
604
  }
117
- if (paused || !writer) {
605
+ if (encodeConfig) {
606
+ if (paused || recording !== "on") frame.close();
607
+ else encodeVideo(frame);
608
+ if (announceNextFrame && !paused) {
609
+ announceNextFrame = false;
610
+ scope.postMessage({ type: "frame" });
611
+ }
612
+ continue;
613
+ }
614
+ const sinceLastUs = frame.timestamp - lastWrittenUs;
615
+ if (paused || !writer || sinceLastUs >= 0 && sinceLastUs < minFrameIntervalUs) {
118
616
  frame.close();
119
617
  continue;
120
618
  }
619
+ lastWrittenUs = frame.timestamp;
121
620
  await writer.write(scaled(frame));
122
621
  if (announceNextFrame) {
123
622
  announceNextFrame = false;
@@ -133,6 +632,8 @@ function cameraCompositorWorkerMain(scope) {
133
632
  };
134
633
  const switchSource = (readable) => {
135
634
  generation += 1;
635
+ lastWrittenUs = -Infinity;
636
+ reanchor();
136
637
  const previous = activeReader;
137
638
  activeReader = null;
138
639
  previous?.cancel().catch(() => {});
@@ -144,6 +645,12 @@ function cameraCompositorWorkerMain(scope) {
144
645
  case "start":
145
646
  maxLongEdge = message.maxLongEdge;
146
647
  maxShortEdge = message.maxShortEdge;
648
+ minFrameIntervalUs = message.minFrameIntervalUs ?? 0;
649
+ encodeConfig = message.encode ?? null;
650
+ if (encodeConfig && message.audio) {
651
+ hasAudioSource = true;
652
+ pumpAudio(message.audio);
653
+ }
147
654
  canvas = new g.OffscreenCanvas(1, 1);
148
655
  ctx = canvas.getContext("2d");
149
656
  writer = message.writable.getWriter();
@@ -159,6 +666,27 @@ function cameraCompositorWorkerMain(scope) {
159
666
  case "resume":
160
667
  paused = false;
161
668
  announceNextFrame = true;
669
+ reanchor();
670
+ break;
671
+ case "record-start":
672
+ resetRecording();
673
+ recording = "on";
674
+ break;
675
+ case "record-pause":
676
+ if (recording === "on") {
677
+ recording = "paused";
678
+ pauseStartedMs = performance.now();
679
+ }
680
+ break;
681
+ case "record-resume":
682
+ if (recording === "paused") {
683
+ pausedMs += performance.now() - pauseStartedMs;
684
+ recording = "on";
685
+ reanchor();
686
+ }
687
+ break;
688
+ case "record-stop":
689
+ stopRecording();
162
690
  break;
163
691
  default: break;
164
692
  }
@@ -175,6 +703,26 @@ const MAX_COMPOSITE_SHORT_EDGE = 720;
175
703
  const FRAME_INTERVAL_TOLERANCE_RATIO = .9;
176
704
  const DEFAULT_RESUME_TIMEOUT_MS = 2e3;
177
705
  /**
706
+ * Compositor for a multipart recording (ID, Selfie, Video Selfie): encodes
707
+ * inside its worker at the device's {@link getWorkerRecordingProfile} tier.
708
+ * Pair it with
709
+ * `WorkerEncoderRecordingProvider`, which falls back to `MediaRecorder` when
710
+ * in-worker encoding is unavailable.
711
+ */
712
+ function createRecordingCompositor() {
713
+ const profile = getWorkerRecordingProfile();
714
+ return new CameraCompositor({
715
+ fps: profile.fps,
716
+ maxLongEdge: profile.maxLongEdge,
717
+ maxShortEdge: profile.maxShortEdge,
718
+ paceWorkerFrames: profile.fps < 30,
719
+ encodeInWorker: {
720
+ bitrate: profile.bitrate,
721
+ timesliceMs: profile.timesliceMs
722
+ }
723
+ });
724
+ }
725
+ /**
178
726
  * Composites the *active* camera into a single, stable `MediaStream` to
179
727
  * record. Switching cameras (front ↔ back) just repoints the source — the
180
728
  * output stream and its audio track are never replaced — so a single
@@ -192,6 +740,10 @@ const DEFAULT_RESUME_TIMEOUT_MS = 2e3;
192
740
  * `captureStream`, for Safari, Firefox and strict-CSP hosts that block blob
193
741
  * workers.
194
742
  *
743
+ * With `encodeInWorker`, the worker transport also records: it encodes and
744
+ * muxes the WebM itself (see `startWorkerRecording`), so no `MediaRecorder`
745
+ * — which needs the main thread for every frame — is involved.
746
+ *
195
747
  * A worker that dies mid-recording (GPU-process crash, OOM) is released at
196
748
  * once; the composite track stays valid but receives no further frames, and
197
749
  * the next `resume()` reports `timed-out` so the caller reacquires the camera
@@ -202,6 +754,10 @@ const DEFAULT_RESUME_TIMEOUT_MS = 2e3;
202
754
  */
203
755
  var CameraCompositor = class {
204
756
  constructor(options = {}) {
757
+ this.workerEncoding = false;
758
+ this.onWorkerPart = null;
759
+ this.workerRecordingError = null;
760
+ this.workerRecordingStopped = null;
205
761
  this.canvas = null;
206
762
  this.ctx = null;
207
763
  this.video = null;
@@ -216,6 +772,10 @@ var CameraCompositor = class {
216
772
  this.videoTrack = null;
217
773
  this.composite = null;
218
774
  this.fps = options.fps ?? 30;
775
+ this.maxLongEdge = options.maxLongEdge ?? MAX_COMPOSITE_LONG_EDGE;
776
+ this.maxShortEdge = options.maxShortEdge ?? MAX_COMPOSITE_SHORT_EDGE;
777
+ this.paceWorkerFrames = options.paceWorkerFrames ?? false;
778
+ this.encodeInWorker = options.encodeInWorker;
219
779
  this.frameIntervalMs = this.fps > 0 ? 1e3 / this.fps : 0;
220
780
  this.resumeTimeoutMs = options.resumeTimeoutMs ?? DEFAULT_RESUME_TIMEOUT_MS;
221
781
  }
@@ -254,6 +814,66 @@ var CameraCompositor = class {
254
814
  } else this.videoTrack = stream.getVideoTracks()[0] ?? null;
255
815
  if (previousVideoTrack) previousVideoTrack.stop();
256
816
  }
817
+ /**
818
+ * True when this compositor records by itself: the worker transport started
819
+ * with `encodeInWorker` and the browser has `VideoEncoder`. Use the
820
+ * `*WorkerRecording` methods instead of a `MediaRecorder` then; the
821
+ * composite stream receives no frames.
822
+ */
823
+ get encodesInWorker() {
824
+ return this.workerEncoding && this.transport !== null;
825
+ }
826
+ /** Last in-worker encoder error, if any. */
827
+ get workerRecordingErrorMessage() {
828
+ return this.workerRecordingError;
829
+ }
830
+ /**
831
+ * Start a new WebM recording inside the worker. `onPart` receives whole
832
+ * WebM clusters (~`timesliceMs` each, keyframe first; the first part also
833
+ * carries the header), in order. Returns false when not encoding in-worker.
834
+ */
835
+ startWorkerRecording(onPart) {
836
+ if (!this.encodesInWorker) return false;
837
+ this.onWorkerPart = onPart;
838
+ this.workerRecordingError = null;
839
+ this.postToWorker({ type: "record-start" });
840
+ return true;
841
+ }
842
+ pauseWorkerRecording() {
843
+ if (!this.encodesInWorker || !this.onWorkerPart) return false;
844
+ this.postToWorker({ type: "record-pause" });
845
+ return true;
846
+ }
847
+ resumeWorkerRecording() {
848
+ if (!this.encodesInWorker || !this.onWorkerPart) return false;
849
+ this.postToWorker({ type: "record-resume" });
850
+ return true;
851
+ }
852
+ /**
853
+ * Flush the encoder and resolve once every part has reached `onPart`.
854
+ * Resolves at once if the worker is gone (its buffered frames are lost).
855
+ */
856
+ stopWorkerRecording() {
857
+ if (!this.onWorkerPart) return Promise.resolve();
858
+ if (!this.transport) {
859
+ this.onWorkerPart = null;
860
+ return Promise.resolve();
861
+ }
862
+ return new Promise((resolve) => {
863
+ this.workerRecordingStopped = () => {
864
+ this.workerRecordingStopped = null;
865
+ this.onWorkerPart = null;
866
+ resolve();
867
+ };
868
+ this.postToWorker({ type: "record-stop" });
869
+ });
870
+ }
871
+ /** Stop in-worker recording and drop whatever it still emits. */
872
+ cancelWorkerRecording() {
873
+ if (this.onWorkerPart && this.transport) this.postToWorker({ type: "record-stop" });
874
+ this.onWorkerPart = null;
875
+ this.workerRecordingStopped?.();
876
+ }
257
877
  /** The composite stream being recorded, if compositing is active. */
258
878
  get stream() {
259
879
  return this.composite ?? void 0;
@@ -345,6 +965,13 @@ var CameraCompositor = class {
345
965
  let worker = null;
346
966
  let generator = null;
347
967
  let blobUrl = null;
968
+ const codecs = globalThis;
969
+ const encode = this.encodeInWorker && typeof codecs.VideoEncoder === "function" && (!this.audioTrack || typeof codecs.AudioEncoder === "function") ? {
970
+ codec: "vp8",
971
+ bitrate: this.encodeInWorker.bitrate,
972
+ fps: this.fps,
973
+ timesliceMs: this.encodeInWorker.timesliceMs
974
+ } : null;
348
975
  try {
349
976
  const processor = createTrackProcessor(track);
350
977
  generator = createTrackGenerator();
@@ -355,14 +982,22 @@ var CameraCompositor = class {
355
982
  type: "fatal",
356
983
  message: event?.message || "worker error"
357
984
  });
985
+ const audioReadable = encode && this.audioTrack ? createTrackProcessor(this.audioTrack).readable : void 0;
358
986
  const message = {
359
987
  type: "start",
988
+ audio: audioReadable,
360
989
  readable: processor.readable,
361
990
  writable: generator.writable,
362
- maxLongEdge: MAX_COMPOSITE_LONG_EDGE,
363
- maxShortEdge: MAX_COMPOSITE_SHORT_EDGE
991
+ maxLongEdge: this.maxLongEdge,
992
+ maxShortEdge: this.maxShortEdge,
993
+ minFrameIntervalUs: this.paceWorkerFrames && this.frameIntervalMs > 0 ? this.frameIntervalMs * 1e3 * FRAME_INTERVAL_TOLERANCE_RATIO : 0,
994
+ encode: encode ?? void 0
364
995
  };
365
- worker.postMessage(message, [processor.readable, generator.writable]);
996
+ worker.postMessage(message, [
997
+ processor.readable,
998
+ generator.writable,
999
+ ...audioReadable ? [audioReadable] : []
1000
+ ]);
366
1001
  } catch {
367
1002
  worker?.terminate();
368
1003
  generator?.stop();
@@ -375,6 +1010,7 @@ var CameraCompositor = class {
375
1010
  blobUrl
376
1011
  };
377
1012
  this.generator = generator;
1013
+ this.workerEncoding = encode !== null;
378
1014
  return new MediaStream([generator]);
379
1015
  }
380
1016
  /** Terminates the worker and frees its blob; idempotent. */
@@ -383,6 +1019,7 @@ var CameraCompositor = class {
383
1019
  this.transport.worker.terminate();
384
1020
  URL.revokeObjectURL(this.transport.blobUrl);
385
1021
  this.transport = null;
1022
+ this.workerRecordingStopped?.();
386
1023
  }
387
1024
  startMainThread(stream) {
388
1025
  this.canvas = document.createElement("canvas");
@@ -407,6 +1044,15 @@ var CameraCompositor = class {
407
1044
  case "ended":
408
1045
  this.finishResume("video-ended");
409
1046
  break;
1047
+ case "part":
1048
+ this.onWorkerPart?.(new Blob([message.data], { type: "video/webm" }));
1049
+ break;
1050
+ case "record-stopped":
1051
+ this.workerRecordingStopped?.();
1052
+ break;
1053
+ case "record-error":
1054
+ this.workerRecordingError = message.message;
1055
+ break;
410
1056
  case "fatal":
411
1057
  this.releaseWorker();
412
1058
  this.finishResume("timed-out");
@@ -480,7 +1126,7 @@ var CameraCompositor = class {
480
1126
  getBoundedDimensions(width, height) {
481
1127
  const longEdge = Math.max(width, height);
482
1128
  const shortEdge = Math.min(width, height);
483
- const scale = Math.min(1, MAX_COMPOSITE_LONG_EDGE / longEdge, MAX_COMPOSITE_SHORT_EDGE / shortEdge);
1129
+ const scale = Math.min(1, this.maxLongEdge / longEdge, this.maxShortEdge / shortEdge);
484
1130
  return {
485
1131
  width: Math.max(1, Math.round(width * scale)),
486
1132
  height: Math.max(1, Math.round(height * scale))
@@ -632,6 +1278,82 @@ var MultipartRecordingProvider = class {
632
1278
  }
633
1279
  };
634
1280
  //#endregion
1281
+ //#region ../infra/src/providers/browser/WorkerEncoderRecordingProvider.ts
1282
+ /**
1283
+ * Multipart recorder for a `CameraCompositor` that encodes inside its worker
1284
+ * (ENG-55397): emits the worker's WebM clusters as parts, in order. When the
1285
+ * compositor could not start in-worker encoding (no insertable streams or
1286
+ * WebCodecs, strict CSP), it records the composite stream with
1287
+ * `MediaRecorder` instead, so callers wire it unconditionally.
1288
+ *
1289
+ * Why not always `MediaRecorder`: Chrome hands every recorded frame through
1290
+ * the page main thread and drops frames while it is blocked. On a Galaxy A01
1291
+ * Core the ID flow blocks it for 3–5 s (WASM + 1080p readbacks); a
1292
+ * `MediaRecorder` fed by the worker kept 74 of 149 frames under 1.5 s
1293
+ * freezes, the in-worker encoder 150 of 151.
1294
+ */
1295
+ var WorkerEncoderRecordingProvider = class {
1296
+ constructor(compositor) {
1297
+ this.compositor = compositor;
1298
+ this._isRecording = false;
1299
+ this._error = null;
1300
+ this.partNumber = 0;
1301
+ this.fallback = null;
1302
+ }
1303
+ get isRecording() {
1304
+ return this.fallback ? this.fallback.isRecording : this._isRecording;
1305
+ }
1306
+ get hasError() {
1307
+ return this.error !== null;
1308
+ }
1309
+ get error() {
1310
+ if (this.fallback) return this.fallback.error;
1311
+ return this._error ?? this.compositor.workerRecordingErrorMessage;
1312
+ }
1313
+ startRecording(stream, onPart) {
1314
+ this.reset();
1315
+ if (!this.compositor.encodesInWorker) {
1316
+ this.fallback = new MultipartRecordingProvider();
1317
+ this.fallback.startRecording(stream, onPart);
1318
+ return;
1319
+ }
1320
+ this.partNumber = 0;
1321
+ const started = this.compositor.startWorkerRecording((chunk) => {
1322
+ this.partNumber += 1;
1323
+ onPart({
1324
+ chunk,
1325
+ partNumber: this.partNumber
1326
+ });
1327
+ });
1328
+ this._isRecording = started;
1329
+ this._error = started ? null : "Failed to start recording: no encoder";
1330
+ }
1331
+ pauseRecording() {
1332
+ if (this.fallback) return this.fallback.pauseRecording();
1333
+ return this._isRecording && this.compositor.pauseWorkerRecording();
1334
+ }
1335
+ resumeRecording() {
1336
+ if (this.fallback) return this.fallback.resumeRecording();
1337
+ return this._isRecording && this.compositor.resumeWorkerRecording();
1338
+ }
1339
+ async stopRecording() {
1340
+ if (this.fallback) {
1341
+ await this.fallback.stopRecording();
1342
+ return;
1343
+ }
1344
+ if (!this._isRecording) return;
1345
+ this._isRecording = false;
1346
+ await this.compositor.stopWorkerRecording();
1347
+ }
1348
+ reset() {
1349
+ this.fallback?.reset();
1350
+ this.fallback = null;
1351
+ this.compositor.cancelWorkerRecording();
1352
+ this._isRecording = false;
1353
+ this._error = null;
1354
+ }
1355
+ };
1356
+ //#endregion
635
1357
  //#region ../infra/src/providers/browser/BrowserNetworkStatusProvider.ts
636
1358
  var BrowserNetworkStatusProvider = class {
637
1359
  onOnline(listener) {
@@ -916,15 +1638,15 @@ function httpStatusOf(error) {
916
1638
  * (S3 SSE-AES-256).
917
1639
  */
918
1640
  var MultipartRecordingService = class {
919
- constructor(scope = "videoselfie") {
1641
+ constructor(scope = "videoselfie", recorder = new MultipartRecordingProvider()) {
920
1642
  this.scope = scope;
921
- this.recorder = new MultipartRecordingProvider();
922
1643
  this.timer = BrowserTimerProvider.getInstance();
923
1644
  this.session = null;
924
1645
  this.uploader = null;
925
1646
  this.chunksEmitted = 0;
926
1647
  this.stopPromise = null;
927
1648
  this.abandonRequested = false;
1649
+ this.recorder = recorder;
928
1650
  }
929
1651
  get isRecording() {
930
1652
  return this.recorder.isRecording;
@@ -1043,4 +1765,4 @@ var MultipartRecordingService = class {
1043
1765
  }
1044
1766
  };
1045
1767
  //#endregion
1046
- export { CameraCompositor as n, getAdaptiveMediaRecorderOptions as r, MultipartRecordingService as t };
1768
+ export { getAdaptiveMediaRecorderOptions as a, createRecordingCompositor as i, WorkerEncoderRecordingProvider as n, StreamCanvasProcessingSession as o, CameraCompositor as r, MultipartRecordingService as t };