@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.
- package/dist/{Face2DProvider-xWUeSJZI.esm.js → Face2DProvider-BZCsS_sf.esm.js} +16 -4
- package/dist/PrivacyLensProvider-DA3rp6sf.esm.js +727 -0
- package/dist/authentication.d.ts +2 -2
- package/dist/authentication.esm.js +2 -2
- package/dist/{authenticationManager-CqcUarZL.esm.js → authenticationManager-BugCSsnK.esm.js} +2 -2
- package/dist/{authenticationManager-Ce1nLm5y.d.ts → authenticationManager-DqyCzK3H.d.ts} +1 -1
- package/dist/{authenticationStateMachine-C65ym-xr.esm.js → authenticationStateMachine-B15J5KEE.esm.js} +2 -2
- package/dist/avatar.esm.js +1 -1
- package/dist/avatarStats-BIB3SREM.esm.js +146 -0
- package/dist/consent.d.ts +1 -1
- package/dist/{consentManager-3Pbtj-RV.d.ts → consentManager-uhDPdU6i.d.ts} +4 -4
- package/dist/{createFaceAvatar-CwVapQiu.esm.js → createFaceAvatar-x5aaJwVM.esm.js} +2 -2
- package/dist/{deepsightLoader-P9BYMXpo.esm.js → deepsightLoader-3f48VAWS.esm.js} +1 -1
- package/dist/ekyb.esm.js +3 -3
- package/dist/{ekybManager-viscdI3d.esm.js → ekybManager-D86lOKbu.esm.js} +1 -1
- package/dist/{ekybStateMachine-DR_8mE1j.esm.js → ekybStateMachine-CscfKZxA.esm.js} +1 -1
- package/dist/extensibility.d.ts +8 -8
- package/dist/extensibility.esm.js +6 -6
- package/dist/{faceCaptureManagerFactory-BUE6z9iY.d.ts → faceCaptureManagerFactory-BpVpExwX.d.ts} +0 -6
- package/dist/{faceCaptureManagerFactory-BVZ3fonQ.esm.js → faceCaptureManagerFactory-CWqmMqXq.esm.js} +1 -2
- package/dist/{faceCaptureSetup-DWPFfh6J.esm.js → faceCaptureSetup-DJSy9KIH.esm.js} +3 -11
- package/dist/flow.d.ts +2 -2
- package/dist/flow.esm.js +1 -1
- package/dist/{flowServices-DR1uBo_E.esm.js → flowServices-BxqjVAV0.esm.js} +2 -2
- package/dist/id-ocr.d.ts +2 -2
- package/dist/id.d.ts +2 -2
- package/dist/id.esm.js +2 -2
- package/dist/{idCaptureManager-DZjNKCN0.esm.js → idCaptureManager-CpqVWwfk.esm.js} +1 -2
- package/dist/{idCaptureManager-CtsYvX-Z.d.ts → idCaptureManager-DECjdrKj.d.ts} +1 -2
- package/dist/{idCaptureStateMachine-CXMJ1Yjg.esm.js → idCaptureStateMachine-f9guPQn1.esm.js} +6 -19
- package/dist/{idOcrManager-DlYG277_.d.ts → idOcrManager-5vi0uwLZ.d.ts} +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/mandatory-consent.d.ts +1 -1
- package/dist/{mandatoryConsentManager-BobEyNBc.d.ts → mandatoryConsentManager-Ck2e2W4o.d.ts} +4 -4
- package/dist/{onDeviceFaceResults-Ck6PjuaN.esm.js → onDeviceFaceResults-CXPJhFPL.esm.js} +1 -1
- package/dist/personhood.esm.js +2 -2
- package/dist/{recordingService-C37o41ar.esm.js → recordingService-rNoh1PIz.esm.js} +1 -1
- package/dist/selfie.d.ts +2 -2
- package/dist/selfie.esm.js +4 -4
- package/dist/{selfieManager-CUSnS_Ty.esm.js → selfieManager-Br-jyeHB.esm.js} +2 -2
- package/dist/{selfieManager-LabfZln4.d.ts → selfieManager-DR-EIsRb.d.ts} +1 -1
- package/dist/{selfieStateMachine-CSTR3bTY.esm.js → selfieStateMachine-BKelTRVc.esm.js} +2 -2
- package/dist/session-Dvgjq8G1.esm.js +1 -0
- package/dist/session.esm.js +2 -2
- package/dist/{sessionInitializer-B_GKYIAU.esm.js → sessionInitializer-JZoRl_kK.esm.js} +1 -1
- package/dist/{setup-Dr_yQTok.esm.js → setup-D8Ptyi5X.esm.js} +2 -2
- package/dist/{types-D0ohdBDu.d.ts → types-B02qlnfB.d.ts} +0 -4
- package/dist/video-selfie.esm.js +3 -3
- package/dist/wasm.esm.js +1 -1
- package/dist/workflow.esm.js +6 -6
- package/package.json +1 -1
- package/dist/PrivacyLensProvider-BrYSd1dr.esm.js +0 -336
- package/dist/session-DTuRfMtr.esm.js +0 -1
- /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 };
|