@incodetech/core 0.0.0-dev-20260727-4e22043f → 0.0.0-dev-20260727-6d540cf0

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 (54) hide show
  1. package/dist/{Face2DProvider-xWUeSJZI.esm.js → Face2DProvider-BZCsS_sf.esm.js} +16 -4
  2. package/dist/PrivacyLensProvider-DA3rp6sf.esm.js +727 -0
  3. package/dist/authentication.d.ts +2 -2
  4. package/dist/authentication.esm.js +2 -2
  5. package/dist/{authenticationManager-CqcUarZL.esm.js → authenticationManager-BugCSsnK.esm.js} +2 -2
  6. package/dist/{authenticationManager-Ce1nLm5y.d.ts → authenticationManager-DqyCzK3H.d.ts} +1 -1
  7. package/dist/{authenticationStateMachine-C65ym-xr.esm.js → authenticationStateMachine-B15J5KEE.esm.js} +2 -2
  8. package/dist/avatar.esm.js +1 -1
  9. package/dist/avatarStats-BIB3SREM.esm.js +146 -0
  10. package/dist/consent.d.ts +1 -1
  11. package/dist/{consentManager-3Pbtj-RV.d.ts → consentManager-uhDPdU6i.d.ts} +4 -4
  12. package/dist/{createFaceAvatar-CwVapQiu.esm.js → createFaceAvatar-x5aaJwVM.esm.js} +2 -2
  13. package/dist/{deepsightLoader-P9BYMXpo.esm.js → deepsightLoader-3f48VAWS.esm.js} +1 -1
  14. package/dist/ekyb.esm.js +3 -3
  15. package/dist/{ekybManager-viscdI3d.esm.js → ekybManager-D86lOKbu.esm.js} +1 -1
  16. package/dist/{ekybStateMachine-DR_8mE1j.esm.js → ekybStateMachine-CscfKZxA.esm.js} +1 -1
  17. package/dist/extensibility.d.ts +8 -8
  18. package/dist/extensibility.esm.js +6 -6
  19. package/dist/{faceCaptureManagerFactory-BUE6z9iY.d.ts → faceCaptureManagerFactory-BpVpExwX.d.ts} +0 -6
  20. package/dist/{faceCaptureManagerFactory-BVZ3fonQ.esm.js → faceCaptureManagerFactory-CWqmMqXq.esm.js} +1 -2
  21. package/dist/{faceCaptureSetup-DWPFfh6J.esm.js → faceCaptureSetup-DJSy9KIH.esm.js} +3 -11
  22. package/dist/flow.d.ts +2 -2
  23. package/dist/flow.esm.js +1 -1
  24. package/dist/{flowServices-DR1uBo_E.esm.js → flowServices-BxqjVAV0.esm.js} +2 -2
  25. package/dist/id-ocr.d.ts +2 -2
  26. package/dist/id.d.ts +2 -2
  27. package/dist/id.esm.js +2 -2
  28. package/dist/{idCaptureManager-DZjNKCN0.esm.js → idCaptureManager-CpqVWwfk.esm.js} +1 -2
  29. package/dist/{idCaptureManager-CtsYvX-Z.d.ts → idCaptureManager-DECjdrKj.d.ts} +1 -2
  30. package/dist/{idCaptureStateMachine-CXMJ1Yjg.esm.js → idCaptureStateMachine-f9guPQn1.esm.js} +6 -19
  31. package/dist/{idOcrManager-DlYG277_.d.ts → idOcrManager-5vi0uwLZ.d.ts} +1 -1
  32. package/dist/index.esm.js +1 -1
  33. package/dist/mandatory-consent.d.ts +1 -1
  34. package/dist/{mandatoryConsentManager-BobEyNBc.d.ts → mandatoryConsentManager-Ck2e2W4o.d.ts} +4 -4
  35. package/dist/{onDeviceFaceResults-Ck6PjuaN.esm.js → onDeviceFaceResults-CXPJhFPL.esm.js} +1 -1
  36. package/dist/personhood.esm.js +2 -2
  37. package/dist/{recordingService-C37o41ar.esm.js → recordingService-rNoh1PIz.esm.js} +1 -1
  38. package/dist/selfie.d.ts +2 -2
  39. package/dist/selfie.esm.js +4 -4
  40. package/dist/{selfieManager-CUSnS_Ty.esm.js → selfieManager-Br-jyeHB.esm.js} +2 -2
  41. package/dist/{selfieManager-LabfZln4.d.ts → selfieManager-DR-EIsRb.d.ts} +1 -1
  42. package/dist/{selfieStateMachine-CSTR3bTY.esm.js → selfieStateMachine-BKelTRVc.esm.js} +2 -2
  43. package/dist/session-Dvgjq8G1.esm.js +1 -0
  44. package/dist/session.esm.js +2 -2
  45. package/dist/{sessionInitializer-B_GKYIAU.esm.js → sessionInitializer-JZoRl_kK.esm.js} +1 -1
  46. package/dist/{setup-Dr_yQTok.esm.js → setup-D8Ptyi5X.esm.js} +2 -2
  47. package/dist/{types-D0ohdBDu.d.ts → types-B02qlnfB.d.ts} +0 -4
  48. package/dist/video-selfie.esm.js +3 -3
  49. package/dist/wasm.esm.js +1 -1
  50. package/dist/workflow.esm.js +6 -6
  51. package/package.json +1 -1
  52. package/dist/PrivacyLensProvider-BrYSd1dr.esm.js +0 -336
  53. package/dist/session-DTuRfMtr.esm.js +0 -1
  54. /package/dist/{types-CBJI9n6h.d.ts → types-CBJI9n6h2.d.ts} +0 -0
@@ -0,0 +1,727 @@
1
+ import { t as createHiddenVideoElement } from "./video-BtSWtHy1.esm.js";
2
+ import { a as createFramePacer, i as publishAvatarStats, n as createStatsWindow, r as isAvatarStatsEnabled, t as createAvatarStatsReporter } from "./avatarStats-BIB3SREM.esm.js";
3
+ //#region ../infra/src/avatar/privacy-lens/privacyLensShaders.ts
4
+ /** Starting point; tuned live via the privacy-lens demo sliders. */
5
+ const PRIVACY_LENS_DEFAULTS = {
6
+ frost: .55,
7
+ sandblast: .28,
8
+ grainSize: .5,
9
+ brightness: 1.12,
10
+ shadowLift: .12,
11
+ warmth: .5,
12
+ sun: .71,
13
+ sunSpeed: .26
14
+ };
15
+ const PRIVACY_LENS_VERT = `
16
+ attribute vec2 aPos;
17
+ varying vec2 vUv;
18
+ void main() {
19
+ vUv = aPos * 0.5 + 0.5;
20
+ gl_Position = vec4(aPos, 0.0, 1.0);
21
+ }`;
22
+ const GOLDEN_ANGLE = 2.399963;
23
+ /** GLSL float literal (toFixed always keeps the `.` GLSL ES 1.00 requires). */
24
+ const f = (x) => x.toFixed(7);
25
+ const PRIVACY_LENS_FRAG = `
26
+ // The grain hash (h21 below) feeds large gl_FragCoord-derived values through
27
+ // sin()*43758.5 — that overflows mediump's range on many mobile GPUs and
28
+ // collapses the grain to a flat artifact. Use highp where available (all WebGL2
29
+ // and most WebGL1 devices); fall back to mediump on the rare device without it.
30
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
31
+ precision highp float;
32
+ #else
33
+ precision mediump float;
34
+ #endif
35
+ varying vec2 vUv;
36
+ uniform sampler2D uVideo;
37
+ uniform vec2 uRes; // canvas pixel size (square)
38
+ uniform vec2 uVideoRes; // decoded video pixel size
39
+ uniform float uTime; // seconds (sun drift only)
40
+ uniform float uFrost;
41
+ uniform float uSandblast;
42
+ uniform float uGrainSize;
43
+ uniform float uWarmth;
44
+ uniform float uBright;
45
+ uniform float uShadow;
46
+ uniform float uSun;
47
+ uniform float uSunSpeed;
48
+
49
+ // Static per-cell hash — NO time term, so the grain is a fixed matte surface.
50
+ float h21(vec2 p) {
51
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
52
+ }
53
+
54
+ // Cover-fit the (non-square) video into the square output, centre-cropped, so
55
+ // faces are never stretched. Also flips V: the texture is uploaded without
56
+ // UNPACK_FLIP_Y_WEBGL (see PrivacyLensProvider), so row 0 is the TOP of the
57
+ // frame; the flip is symmetric around 0.5, so it composes with the cover crop.
58
+ vec2 coverUv(vec2 uv) {
59
+ float va = uVideoRes.x / max(uVideoRes.y, 1.0);
60
+ vec2 o = uv;
61
+ if (va > 1.0) o.x = (uv.x - 0.5) / va + 0.5; // landscape: crop sides
62
+ else o.y = (uv.y - 0.5) * va + 0.5; // portrait: crop top/bottom
63
+ return vec2(o.x, 1.0 - o.y);
64
+ }
65
+
66
+ void main() {
67
+ vec2 uv = vUv;
68
+ vec2 base = coverUv(uv);
69
+ float aspect = uRes.y / max(uRes.x, 1.0);
70
+
71
+ // Per-fragment static seed, clustered into grain cells. Small cells = ultra
72
+ // fine sandblast (effectively invisible pattern); larger = coarser frosting.
73
+ vec2 cell = floor(gl_FragCoord.xy / max(uGrainSize, 0.5));
74
+ float rnd = h21(cell);
75
+ float baseAngle = rnd * 6.2831853;
76
+
77
+ // Stochastic sandblasted scatter: an even golden-angle disk, but each pixel
78
+ // cell rotates + jitters the kernel differently → a fine matte grain rather
79
+ // than a smooth blur. The video moves UNDER this fixed glass grain. Taps are
80
+ // unrolled with baked constants; the per-cell rotation is the single
81
+ // cos/sin pair below (see TAPS_GLSL).
82
+ float cb = cos(baseAngle);
83
+ float sb = sin(baseAngle);
84
+ float j; float r; vec2 off;
85
+ vec3 col = vec3(0.0);
86
+ ${Array.from({ length: 12 }, (_, i) => {
87
+ const fi = i + .5;
88
+ const ux = Math.cos(fi * GOLDEN_ANGLE);
89
+ const uy = Math.sin(fi * GOLDEN_ANGLE);
90
+ const rad = Math.sqrt(fi / 12);
91
+ return ` j = h21(cell + vec2(${f(fi * 7.13)}, ${f(fi * 3.71)}));
92
+ r = uFrost * 0.06 * ${f(rad)} + (j - 0.5) * uSandblast * 0.03;
93
+ off = vec2(cb * ${f(ux)} - sb * ${f(uy)}, sb * ${f(ux)} + cb * ${f(uy)}) * r;
94
+ off.x *= aspect;
95
+ col += texture2D(uVideo, base + off).rgb;`;
96
+ }).join("\n")}
97
+ col /= ${f(12)};
98
+
99
+ // Airy + warm grade (luminous frosted glass, not murky).
100
+ col = col + uShadow * (1.0 - col);
101
+ col *= uBright;
102
+ vec3 warm = vec3(1.0, 0.92, 0.80);
103
+ col = mix(col, col * warm + vec3(0.03, 0.015, 0.0), uWarmth);
104
+
105
+ // Drifting sun: a broad soft glow + a fine STATIC sparkle (per-cell) that
106
+ // catches the ground-glass grain only where the light falls — not all-over.
107
+ vec2 sun = vec2(0.34 + 0.16 * sin(uTime * uSunSpeed),
108
+ 0.26 + 0.10 * cos(uTime * uSunSpeed * 0.8));
109
+ float glow = smoothstep(0.7, 0.0, distance(uv, sun));
110
+ vec3 sunCol = vec3(1.0, 0.96, 0.88);
111
+ col += sunCol * glow * (0.20 + (rnd - 0.5) * 0.18 * uSandblast) * uSun;
112
+
113
+ gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
114
+ }`;
115
+ //#endregion
116
+ //#region ../infra/src/avatar/privacy-lens/privacyLensGlCore.ts
117
+ /**
118
+ * Builds the lens GL pipeline on `canvas`. Throws if WebGL is unavailable or
119
+ * the program fails to compile/link (callers fall back / surface the error).
120
+ * SELF-CONTAINED — see the module doc before editing.
121
+ */
122
+ function createPrivacyLensGl(canvas, vertSrc, fragSrc, renderSize) {
123
+ canvas.width = renderSize;
124
+ canvas.height = renderSize;
125
+ const gl = canvas.getContext("webgl", {
126
+ alpha: false,
127
+ antialias: false,
128
+ premultipliedAlpha: false,
129
+ preserveDrawingBuffer: false
130
+ }) ?? canvas.getContext("experimental-webgl");
131
+ if (!gl) throw new Error("privacy-lens: WebGL is not available");
132
+ const compile = (type, src) => {
133
+ const shader = gl.createShader(type);
134
+ if (!shader) throw new Error("privacy-lens: createShader failed");
135
+ gl.shaderSource(shader, src);
136
+ gl.compileShader(shader);
137
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
138
+ const log = gl.getShaderInfoLog(shader);
139
+ gl.deleteShader(shader);
140
+ throw new Error(`privacy-lens: shader compile failed: ${log}`);
141
+ }
142
+ return shader;
143
+ };
144
+ const vs = compile(gl.VERTEX_SHADER, vertSrc);
145
+ const fs = compile(gl.FRAGMENT_SHADER, fragSrc);
146
+ const program = gl.createProgram();
147
+ if (!program) throw new Error("privacy-lens: createProgram failed");
148
+ gl.attachShader(program, vs);
149
+ gl.attachShader(program, fs);
150
+ gl.linkProgram(program);
151
+ gl.deleteShader(vs);
152
+ gl.deleteShader(fs);
153
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
154
+ const log = gl.getProgramInfoLog(program);
155
+ gl.deleteProgram(program);
156
+ throw new Error(`privacy-lens: program link failed: ${log}`);
157
+ }
158
+ gl.useProgram(program);
159
+ const quad = gl.createBuffer();
160
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
161
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
162
+ -1,
163
+ -1,
164
+ 1,
165
+ -1,
166
+ -1,
167
+ 1,
168
+ 1,
169
+ 1
170
+ ]), gl.STATIC_DRAW);
171
+ const aPos = gl.getAttribLocation(program, "aPos");
172
+ gl.enableVertexAttribArray(aPos);
173
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
174
+ const videoTex = gl.createTexture();
175
+ if (!videoTex) throw new Error("privacy-lens: createTexture failed");
176
+ gl.bindTexture(gl.TEXTURE_2D, videoTex);
177
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
178
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
179
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
180
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
181
+ const uniformNames = [
182
+ "uVideo",
183
+ "uRes",
184
+ "uVideoRes",
185
+ "uTime",
186
+ "uFrost",
187
+ "uSandblast",
188
+ "uGrainSize",
189
+ "uWarmth",
190
+ "uBright",
191
+ "uShadow",
192
+ "uSun",
193
+ "uSunSpeed"
194
+ ];
195
+ const uniforms = {};
196
+ for (let i = 0; i < uniformNames.length; i += 1) uniforms[uniformNames[i]] = gl.getUniformLocation(program, uniformNames[i]);
197
+ gl.uniform1i(uniforms.uVideo, 0);
198
+ if (uniforms.uRes) gl.uniform2f(uniforms.uRes, renderSize, renderSize);
199
+ gl.viewport(0, 0, renderSize, renderSize);
200
+ gl.clearColor(.88, .86, .83, 1);
201
+ let texWidth = 0;
202
+ let texHeight = 0;
203
+ let disposed = false;
204
+ const setF = (name, value) => {
205
+ const loc = uniforms[name];
206
+ if (loc) gl.uniform1f(loc, value);
207
+ };
208
+ return {
209
+ clear() {
210
+ if (disposed) return;
211
+ gl.clear(gl.COLOR_BUFFER_BIT);
212
+ },
213
+ render(source, srcWidth, srcHeight, timeSec, params, measure) {
214
+ if (disposed) return null;
215
+ const t0 = measure ? performance.now() : 0;
216
+ gl.activeTexture(gl.TEXTURE0);
217
+ gl.bindTexture(gl.TEXTURE_2D, videoTex);
218
+ if (srcWidth !== texWidth || srcHeight !== texHeight) {
219
+ texWidth = srcWidth;
220
+ texHeight = srcHeight;
221
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texWidth, texHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
222
+ if (uniforms.uVideoRes) gl.uniform2f(uniforms.uVideoRes, texWidth, texHeight);
223
+ }
224
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, source);
225
+ const t1 = measure ? performance.now() : 0;
226
+ setF("uTime", timeSec);
227
+ setF("uFrost", params.frost);
228
+ setF("uSandblast", params.sandblast);
229
+ setF("uGrainSize", params.grainSize);
230
+ setF("uWarmth", params.warmth);
231
+ setF("uBright", params.brightness);
232
+ setF("uShadow", params.shadowLift);
233
+ setF("uSun", params.sun);
234
+ setF("uSunSpeed", params.sunSpeed);
235
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
236
+ if (!measure) return null;
237
+ const t2 = performance.now();
238
+ return {
239
+ uploadMs: t1 - t0,
240
+ drawMs: t2 - t1
241
+ };
242
+ },
243
+ dispose() {
244
+ if (disposed) return;
245
+ disposed = true;
246
+ gl.deleteProgram(program);
247
+ gl.deleteBuffer(quad);
248
+ gl.deleteTexture(videoTex);
249
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
250
+ }
251
+ };
252
+ }
253
+ //#endregion
254
+ //#region ../infra/src/avatar/privacy-lens/privacyLensWorker.ts
255
+ /**
256
+ * Worker mode for the privacy lens (ENG-49806 Plan B).
257
+ *
258
+ * Under a saturated main thread (the synchronous WASM detection pipeline can
259
+ * occupy it back-to-back on throttled/low-tier devices) a main-thread lens is
260
+ * starved to single-digit fps regardless of how cheap its frame is — measured
261
+ * at 1.6-3 fps with 0ms upload/draw. The fix is to take the main thread out of
262
+ * the frame path entirely: camera track → `MediaStreamTrackProcessor` →
263
+ * `VideoFrame` stream → worker → OffscreenCanvas WebGL. The main thread only
264
+ * bridges dev-tuning params and stats at 4 Hz.
265
+ *
266
+ * Support: Chromium 94+ (Chrome/Edge/Android WebView/Samsung Internet) — the
267
+ * low-tier Android target segment. Safari/Firefox and strict-CSP hosts that
268
+ * block blob workers fail the probe or the Worker constructor and fall back to
269
+ * the main-thread path automatically (`PrivacyLensProvider`).
270
+ *
271
+ * Packaging: the worker script is assembled AT RUNTIME from
272
+ * `Function.prototype.toString()` of the shared, self-contained functions
273
+ * (`createPrivacyLensGl`, `createFramePacer`, `createStatsWindow`,
274
+ * `privacyLensWorkerMain`) plus JSON-injected shader sources and stats/pacer
275
+ * constants, and loaded via a Blob URL. No separate worker chunk to ship —
276
+ * bundler-agnostic for npm and CDN consumers alike. The serialized functions
277
+ * must therefore never close over module-level values.
278
+ */
279
+ /**
280
+ * Whether the worker path is available in this environment. All three probes
281
+ * must hold; anything missing (Safari's worker-only MSTP shape, Firefox, old
282
+ * WebView) routes to the main-thread fallback.
283
+ */
284
+ function canUsePrivacyLensWorker() {
285
+ const g = globalThis;
286
+ if (typeof g.MediaStreamTrackProcessor !== "function" || typeof g.Worker !== "function" || typeof g.HTMLCanvasElement !== "function") return false;
287
+ return "transferControlToOffscreen" in g.HTMLCanvasElement.prototype;
288
+ }
289
+ /** Constructs a MediaStreamTrackProcessor (Chromium main-thread shape). */
290
+ function createTrackProcessor(track) {
291
+ const Ctor = globalThis.MediaStreamTrackProcessor;
292
+ if (!Ctor) throw new Error("MediaStreamTrackProcessor unavailable");
293
+ return new Ctor({ track });
294
+ }
295
+ /**
296
+ * The worker's entire logic. SELF-CONTAINED (serialized via toString into the
297
+ * blob): everything it needs arrives via `deps` / messages; no module refs.
298
+ * Frame loop: sequential read → pace → render → close. MSTP buffers at most
299
+ * one frame internally, so reads always yield the freshest frame and closing
300
+ * every frame promptly keeps the camera's frame pool healthy — an unclosed
301
+ * VideoFrame can stall the CAPTURE pipeline, which the lens must never do.
302
+ */
303
+ function privacyLensWorkerMain(scope, deps) {
304
+ let gl = null;
305
+ let pacer = null;
306
+ let reader = null;
307
+ let params = null;
308
+ let statsEnabled = false;
309
+ let stopped = false;
310
+ let stats = null;
311
+ const pump = async () => {
312
+ const startTime = performance.now();
313
+ while (!stopped && reader && gl && pacer && params) {
314
+ let result;
315
+ try {
316
+ result = await reader.read();
317
+ } catch {
318
+ break;
319
+ }
320
+ if (result.done) break;
321
+ const frame = result.value;
322
+ try {
323
+ const now = performance.now();
324
+ if (!pacer.shouldRender(now)) {
325
+ if (statsEnabled) stats?.noteDropped();
326
+ continue;
327
+ }
328
+ const timing = gl.render(frame, frame.displayWidth, frame.displayHeight, (now - startTime) / 1e3, params, statsEnabled);
329
+ if (statsEnabled) stats?.noteRendered(now, timing?.uploadMs, timing?.drawMs);
330
+ } catch {} finally {
331
+ frame.close();
332
+ }
333
+ }
334
+ };
335
+ scope.onmessage = (event) => {
336
+ const msg = event.data;
337
+ if (msg.type === "start") {
338
+ try {
339
+ gl = deps.createGl(msg.canvas, deps.vertSrc, deps.fragSrc, deps.renderSize);
340
+ gl.clear();
341
+ } catch (error) {
342
+ scope.postMessage({
343
+ type: "fatal",
344
+ message: String(error)
345
+ });
346
+ return;
347
+ }
348
+ params = msg.params;
349
+ statsEnabled = msg.statsEnabled;
350
+ stats = deps.createStats({
351
+ variant: "privacy-lens",
352
+ mode: "worker",
353
+ publishIntervalMs: deps.publishIntervalMs,
354
+ gapWindow: deps.gapWindow,
355
+ publish: (snapshot) => scope.postMessage({
356
+ type: "stats",
357
+ snapshot
358
+ })
359
+ });
360
+ pacer = deps.createPacer(msg.maxFps, deps.frameToleranceMs);
361
+ reader = msg.readable.getReader();
362
+ scope.postMessage({ type: "started" });
363
+ pump();
364
+ } else if (msg.type === "params") params = msg.params;
365
+ else if (msg.type === "statsEnabled") statsEnabled = msg.value;
366
+ else if (msg.type === "stop") {
367
+ stopped = true;
368
+ try {
369
+ reader?.cancel();
370
+ } catch {}
371
+ gl?.dispose();
372
+ gl = null;
373
+ scope.postMessage({ type: "stopped" });
374
+ }
375
+ };
376
+ }
377
+ /**
378
+ * Dev/build transforms that inject module-level references into function bodies.
379
+ * These pass `new Function(src)` (syntactically valid) but throw a
380
+ * `ReferenceError` at worker boot because the injected identifier
381
+ * (`$RefreshSig$`, `cov_xxx()`, …) doesn't exist in the worker scope — silently
382
+ * downgrading everyone to the slow main-thread path via the fatal→fallback net.
383
+ * The vite config excludes `packages/infra` from prefresh for this reason; this
384
+ * list is the runtime backstop for any other instrumenter (coverage, SSR, a
385
+ * consumer's bundler) that the config can't reach.
386
+ */
387
+ const WORKER_SOURCE_SENTINELS = [
388
+ {
389
+ name: "prefresh / react-refresh",
390
+ pattern: /\$Refresh(Sig|Reg)\$/
391
+ },
392
+ {
393
+ name: "istanbul coverage",
394
+ pattern: /\bcov_[0-9a-zA-Z]+\(/
395
+ },
396
+ {
397
+ name: "vite SSR transform",
398
+ pattern: /__vite_ssr_/
399
+ }
400
+ ];
401
+ /**
402
+ * Returns the name of the instrumenter that corrupted the serialized worker
403
+ * source, or null if the source is clean. Callers log loudly and fall back to
404
+ * the main-thread lens rather than booting a worker that will `ReferenceError`.
405
+ */
406
+ function detectWorkerSourceInstrumentation(source) {
407
+ for (const sentinel of WORKER_SOURCE_SENTINELS) if (sentinel.pattern.test(source)) return sentinel.name;
408
+ return null;
409
+ }
410
+ /**
411
+ * Assembles the worker script from the serialized shared functions + injected
412
+ * shader sources and stats/pacer constants. Everything referenced must be
413
+ * self-contained (see module doc); the shaders and the numeric constants
414
+ * (`PUBLISH_INTERVAL_MS`, `GAP_WINDOW`, `FRAME_PACER_TOLERANCE_MS`) are injected
415
+ * as literals so the worker never depends on this module's scope.
416
+ */
417
+ function buildPrivacyLensWorkerSource(options) {
418
+ if (!Number.isFinite(options.renderSize)) throw new Error(`privacy-lens: renderSize must be finite, got ${String(options.renderSize)}`);
419
+ return [
420
+ "\"use strict\";",
421
+ `const createGl = ${createPrivacyLensGl.toString()};`,
422
+ `const createPacer = ${createFramePacer.toString()};`,
423
+ `const createStats = ${createStatsWindow.toString()};`,
424
+ `const workerMain = ${privacyLensWorkerMain.toString()};`,
425
+ `workerMain(self, {
426
+ createGl,
427
+ createPacer,
428
+ createStats,
429
+ vertSrc: ${JSON.stringify(options.vertSrc)},
430
+ fragSrc: ${JSON.stringify(options.fragSrc)},
431
+ renderSize: ${JSON.stringify(options.renderSize)},
432
+ publishIntervalMs: ${JSON.stringify(500)},
433
+ gapWindow: ${JSON.stringify(128)},
434
+ frameToleranceMs: ${JSON.stringify(8)},
435
+ });`
436
+ ].join("\n");
437
+ }
438
+ //#endregion
439
+ //#region ../infra/src/avatar/privacy-lens/PrivacyLensProvider.ts
440
+ /**
441
+ * Privacy-lens avatar — sandblasted-glass effect over the **real** selfie video.
442
+ * **Self-contained** (see FACE_AVATAR.md → Isolation and ownership). No MediaPipe, no landmarks, no
443
+ * three/VRM, no npm deps, no textures but the live video — just a raw-WebGL
444
+ * fragment shader run on the video each frame (see `privacyLensShaders.ts`,
445
+ * `privacyLensGlCore.ts`).
446
+ *
447
+ * Two render transports (ENG-49806):
448
+ * - **Worker mode (preferred, Chromium 94+):** camera track (cloned) →
449
+ * `MediaStreamTrackProcessor` → VideoFrame stream → worker → OffscreenCanvas.
450
+ * The main thread is NOT in the frame path, so the lens stays smooth even
451
+ * when the synchronous WASM detection pipeline saturates the main thread
452
+ * (measured: main-thread lens starves to ~1.6-3fps under low-tier CPU
453
+ * throttle with 0ms upload/draw — scheduling, not work, is the bottleneck).
454
+ * - **Main-thread mode (fallback):** hidden decode `<video>` + rVFC/rAF loop,
455
+ * for Safari/Firefox/old WebView and strict-CSP hosts that block blob
456
+ * workers.
457
+ *
458
+ * Implements `IFaceAvatarCapability` (the generic `start/getCanvas/stop`
459
+ * contract) so it rides the existing concealment seam
460
+ * (`selfieConcealmentOption: 'OPTION_SILHOUETTE'`). Cosmetic-only: the
461
+ * authoritative pipeline still runs on the real frames; the worker consumes a
462
+ * CLONED track so lens lifecycle can never affect capture. Mirroring is the
463
+ * UI's job (CSS), like every other provider — the shader renders forward.
464
+ */
465
+ const DEFAULT_MAX_FPS = 30;
466
+ const RENDER_SIZE = 512;
467
+ /** Worker must boot + init GL within this budget, else fall back. */
468
+ const WORKER_START_TIMEOUT_MS = 5e3;
469
+ /** Cadence of the main→worker dev-params/stats-flag bridge. */
470
+ const PARAM_BRIDGE_INTERVAL_MS = 250;
471
+ /** Grace period for the worker to ack 'stop' before hard termination. */
472
+ const WORKER_STOP_GRACE_MS = 500;
473
+ const asRvfc = (video) => {
474
+ const v = video;
475
+ return typeof v.requestVideoFrameCallback === "function" && typeof v.cancelVideoFrameCallback === "function" ? v : null;
476
+ };
477
+ const readParams = () => {
478
+ if (typeof window === "undefined") return PRIVACY_LENS_DEFAULTS;
479
+ const override = window.__incodePrivacyLensParams;
480
+ return override ? {
481
+ ...PRIVACY_LENS_DEFAULTS,
482
+ ...override
483
+ } : PRIVACY_LENS_DEFAULTS;
484
+ };
485
+ var PrivacyLensProvider = class PrivacyLensProvider {
486
+ constructor() {
487
+ this.glCore = null;
488
+ this.videoHandle = null;
489
+ this.rafId = null;
490
+ this.rvfcHandle = null;
491
+ this.usingRvfc = false;
492
+ this.stopped = false;
493
+ this.pacer = createFramePacer(DEFAULT_MAX_FPS);
494
+ this.stats = createAvatarStatsReporter("privacy-lens", "main");
495
+ this.startTime = 0;
496
+ this.lastVideoTime = -1;
497
+ this.worker = null;
498
+ this.clonedTrack = null;
499
+ this.paramBridgeId = null;
500
+ this.lastBridgedParams = "";
501
+ this.lastBridgedStatsEnabled = false;
502
+ this.loop = (now) => {
503
+ if (this.stopped) return;
504
+ this.scheduleNextFrame();
505
+ const glCore = this.glCore;
506
+ const video = this.videoHandle?.element;
507
+ if (!glCore) return;
508
+ if (!video || video.readyState < 2 || !video.videoWidth) {
509
+ glCore.clear();
510
+ return;
511
+ }
512
+ if (!this.usingRvfc) {
513
+ if (video.currentTime === this.lastVideoTime) return;
514
+ this.lastVideoTime = video.currentTime;
515
+ }
516
+ if (!this.pacer.shouldRender(now)) {
517
+ this.stats.frameDropped();
518
+ return;
519
+ }
520
+ const measure = isAvatarStatsEnabled();
521
+ const timing = glCore.render(video, video.videoWidth, video.videoHeight, (now - this.startTime) / 1e3, readParams(), measure);
522
+ if (measure) this.stats.frameRendered(now, timing?.uploadMs, timing?.drawMs);
523
+ };
524
+ this.canvas = PrivacyLensProvider.createCanvas();
525
+ }
526
+ static createCanvas() {
527
+ const canvas = document.createElement("canvas");
528
+ canvas.width = RENDER_SIZE;
529
+ canvas.height = RENDER_SIZE;
530
+ return canvas;
531
+ }
532
+ getCanvas() {
533
+ return this.canvas;
534
+ }
535
+ async start(stream, options = {}) {
536
+ const maxFps = options.maxFps ?? DEFAULT_MAX_FPS;
537
+ this.pacer = createFramePacer(maxFps);
538
+ if (await this.tryStartWorker(stream, maxFps)) return;
539
+ if (this.stopped) return;
540
+ this.startMainThread(stream);
541
+ }
542
+ /**
543
+ * Worker transport: clone the track, feed it to a MediaStreamTrackProcessor,
544
+ * boot a blob worker, transfer the readable + the canvas, and wait for the
545
+ * 'started' handshake. Any failure (unsupported browser, CSP-blocked blob
546
+ * worker, GL init failure in the worker) tears down and returns false so the
547
+ * main-thread path takes over. Never throws.
548
+ */
549
+ async tryStartWorker(stream, maxFps) {
550
+ if (!canUsePrivacyLensWorker()) return false;
551
+ const sourceTrack = stream.getVideoTracks()[0];
552
+ if (!sourceTrack) return false;
553
+ let track = null;
554
+ let processor;
555
+ try {
556
+ track = sourceTrack.clone();
557
+ processor = createTrackProcessor(track);
558
+ } catch {
559
+ track?.stop();
560
+ return false;
561
+ }
562
+ const source = buildPrivacyLensWorkerSource({
563
+ vertSrc: PRIVACY_LENS_VERT,
564
+ fragSrc: PRIVACY_LENS_FRAG,
565
+ renderSize: RENDER_SIZE
566
+ });
567
+ const instrumenter = detectWorkerSourceInstrumentation(source);
568
+ if (instrumenter) {
569
+ console.error(`[privacy-lens] render worker source was instrumented by "${instrumenter}" and cannot run in a worker; using the slower main-thread lens. Exclude packages/infra from that transform (see docs/features/FACE_AVATAR.md → Worker transport).`);
570
+ track.stop();
571
+ return false;
572
+ }
573
+ let worker = null;
574
+ let blobUrl = null;
575
+ try {
576
+ blobUrl = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
577
+ worker = new Worker(blobUrl);
578
+ } catch {
579
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
580
+ track.stop();
581
+ return false;
582
+ }
583
+ let started;
584
+ try {
585
+ const offscreen = this.canvas.transferControlToOffscreen();
586
+ const startedWorker = worker;
587
+ started = await new Promise((resolve) => {
588
+ const timer = setTimeout(() => resolve(false), WORKER_START_TIMEOUT_MS);
589
+ startedWorker.onmessage = (event) => {
590
+ if (event.data?.type === "started") {
591
+ clearTimeout(timer);
592
+ resolve(true);
593
+ } else if (event.data?.type === "fatal") {
594
+ clearTimeout(timer);
595
+ resolve(false);
596
+ }
597
+ };
598
+ startedWorker.onerror = () => {
599
+ clearTimeout(timer);
600
+ resolve(false);
601
+ };
602
+ startedWorker.postMessage({
603
+ type: "start",
604
+ canvas: offscreen,
605
+ readable: processor.readable,
606
+ maxFps,
607
+ params: readParams(),
608
+ statsEnabled: isAvatarStatsEnabled()
609
+ }, [offscreen, processor.readable]);
610
+ });
611
+ } catch {
612
+ worker.terminate();
613
+ URL.revokeObjectURL(blobUrl);
614
+ track.stop();
615
+ if (!this.stopped) this.canvas = PrivacyLensProvider.createCanvas();
616
+ return false;
617
+ }
618
+ URL.revokeObjectURL(blobUrl);
619
+ if (!started || this.stopped) {
620
+ worker.terminate();
621
+ track.stop();
622
+ if (!this.stopped) this.canvas = PrivacyLensProvider.createCanvas();
623
+ return this.stopped;
624
+ }
625
+ this.worker = worker;
626
+ this.clonedTrack = track;
627
+ this.lastBridgedParams = JSON.stringify(readParams());
628
+ this.lastBridgedStatsEnabled = isAvatarStatsEnabled();
629
+ worker.onmessage = (event) => {
630
+ const message = event.data;
631
+ if (message?.type === "stats") {
632
+ if (!this.stopped) publishAvatarStats(message.snapshot);
633
+ } else if (message?.type === "stopped") this.finalizeWorker();
634
+ };
635
+ worker.onerror = () => {
636
+ console.warn("[privacy-lens] render worker error — lens frozen (capture unaffected)");
637
+ if (this.paramBridgeId != null) {
638
+ clearInterval(this.paramBridgeId);
639
+ this.paramBridgeId = null;
640
+ }
641
+ this.finalizeWorker();
642
+ };
643
+ this.paramBridgeId = setInterval(() => {
644
+ if (!this.worker) return;
645
+ const params = readParams();
646
+ const serialized = JSON.stringify(params);
647
+ if (serialized !== this.lastBridgedParams) {
648
+ this.lastBridgedParams = serialized;
649
+ this.worker.postMessage({
650
+ type: "params",
651
+ params
652
+ });
653
+ }
654
+ const statsEnabled = isAvatarStatsEnabled();
655
+ if (statsEnabled !== this.lastBridgedStatsEnabled) {
656
+ this.lastBridgedStatsEnabled = statsEnabled;
657
+ this.worker.postMessage({
658
+ type: "statsEnabled",
659
+ value: statsEnabled
660
+ });
661
+ }
662
+ }, PARAM_BRIDGE_INTERVAL_MS);
663
+ return true;
664
+ }
665
+ /** Main-thread transport: hidden decode video + rVFC/rAF render loop. */
666
+ startMainThread(stream) {
667
+ this.videoHandle = createHiddenVideoElement(stream, { keepStreamAlive: true });
668
+ try {
669
+ this.glCore = createPrivacyLensGl(this.canvas, PRIVACY_LENS_VERT, PRIVACY_LENS_FRAG, RENDER_SIZE);
670
+ if (this.stopped) return;
671
+ this.startTime = performance.now();
672
+ const video = this.videoHandle.element;
673
+ this.usingRvfc = !!asRvfc(video);
674
+ this.scheduleNextFrame();
675
+ } catch (error) {
676
+ this.stop();
677
+ throw error;
678
+ }
679
+ }
680
+ stop() {
681
+ this.stopped = true;
682
+ this.stats.dispose();
683
+ if (this.paramBridgeId != null) {
684
+ clearInterval(this.paramBridgeId);
685
+ this.paramBridgeId = null;
686
+ }
687
+ if (this.worker) {
688
+ this.worker.postMessage({ type: "stop" });
689
+ const pending = this.worker;
690
+ setTimeout(() => {
691
+ if (this.worker === pending) this.finalizeWorker();
692
+ }, WORKER_STOP_GRACE_MS);
693
+ }
694
+ if (this.rafId != null) {
695
+ cancelAnimationFrame(this.rafId);
696
+ this.rafId = null;
697
+ }
698
+ const video = this.videoHandle?.element;
699
+ if (video && this.rvfcHandle != null) {
700
+ asRvfc(video)?.cancelVideoFrameCallback(this.rvfcHandle);
701
+ this.rvfcHandle = null;
702
+ }
703
+ this.glCore?.dispose();
704
+ this.glCore = null;
705
+ this.videoHandle?.dispose();
706
+ this.videoHandle = null;
707
+ }
708
+ finalizeWorker() {
709
+ this.worker?.terminate();
710
+ this.worker = null;
711
+ this.clonedTrack?.stop();
712
+ this.clonedTrack = null;
713
+ }
714
+ scheduleNextFrame() {
715
+ if (this.stopped) return;
716
+ const video = this.videoHandle?.element;
717
+ const rvfc = this.usingRvfc && video ? asRvfc(video) : null;
718
+ if (rvfc) this.rvfcHandle = rvfc.requestVideoFrameCallback(this.loop);
719
+ else this.rafId = requestAnimationFrame(this.loop);
720
+ }
721
+ };
722
+ /** Factory used by the avatar actor's provider selection (privacy-lens avatar). */
723
+ function createProvider() {
724
+ return new PrivacyLensProvider();
725
+ }
726
+ //#endregion
727
+ export { createProvider };