@incodetech/core 0.0.0-dev-20260803-2bd16802 → 0.0.0-dev-20260803-e2b5d35d
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/{3dAvatarProvider-D9H4rR6_.esm.js → 3dAvatarProvider-1zpeX-h8.esm.js} +2 -2
- package/dist/Face2DProvider-TLwFIRn4.esm.js +437 -0
- package/dist/{PrivacyLensProvider-DA3rp6sf.esm.js → PrivacyLensProvider-D8PS7AJa.esm.js} +3 -325
- package/dist/ae-signature.esm.js +1 -1
- package/dist/{apiError-0e7XVmLE.esm.js → apiError-C_08gwg2.esm.js} +1 -1
- package/dist/authentication.esm.js +2 -2
- package/dist/{authenticationManager-CArZpDf_.esm.js → authenticationManager-D76jxYNX.esm.js} +2 -2
- package/dist/{authenticationStateMachine-3w56jRTO.esm.js → authenticationStateMachine-CJILuEbH.esm.js} +2 -2
- package/dist/avatar.esm.js +1 -1
- package/dist/avatar2dWarm-DqucNFX4.esm.js +1330 -0
- package/dist/{avatarStats-BIB3SREM.esm.js → avatarStats-CdUOb3VD.esm.js} +12 -22
- package/dist/consent.d.ts +1 -1
- package/dist/{consentManager-3Pbtj-RV.d.ts → consentManager-uhDPdU6i.d.ts} +4 -4
- package/dist/createFaceAvatar-AF92I3xf.esm.js +103 -0
- package/dist/{deepsightLoader-DYVd_8gR.esm.js → deepsightLoader-DqEfPplN.esm.js} +3 -3
- package/dist/document-upload.d.ts +4 -4
- package/dist/dynamic-forms.esm.js +2 -2
- package/dist/{dynamicFormsManager-Jgr8OAlG.esm.js → dynamicFormsManager-BHfeedY2.esm.js} +1 -1
- package/dist/{dynamicFormsStateMachine-Cm6YJQBA.esm.js → dynamicFormsStateMachine-BeKhow9E.esm.js} +1 -1
- package/dist/ekyb.esm.js +3 -3
- package/dist/{ekybManager-DLlenLG2.esm.js → ekybManager-3pPpvD6Z.esm.js} +1 -1
- package/dist/{ekybStateMachine-DpaO3WdV.esm.js → ekybStateMachine-BH8TaB7v.esm.js} +1 -1
- package/dist/electronic-signature.esm.js +1 -1
- package/dist/{electronicSignatureManager-CPWJcuFy.esm.js → electronicSignatureManager-DHC0bqr9.esm.js} +1 -1
- package/dist/extensibility.d.ts +2 -2
- package/dist/extensibility.esm.js +8 -8
- package/dist/{faceCaptureManagerFactory-C58fhn5l.esm.js → faceCaptureManagerFactory-DqQCDsDY.esm.js} +1 -1
- package/dist/{faceCaptureSetup-a7dsfqui.esm.js → faceCaptureSetup-5Ye-LXbB.esm.js} +25 -10
- package/dist/flow.esm.js +3 -3
- package/dist/{flowCompletionService-BZWoO8E3.esm.js → flowCompletionService-DEO2OqAI.esm.js} +1 -1
- package/dist/{flowServices-DlsSs-nq.esm.js → flowServices-D6T8fwIf.esm.js} +3 -3
- package/dist/geolocation.esm.js +2 -2
- package/dist/{geolocationManager-C7LEmSRJ.esm.js → geolocationManager-IbghOO6X.esm.js} +1 -1
- package/dist/{geolocationStateMachine-CV2cecNj.esm.js → geolocationStateMachine-B5WkRLnz.esm.js} +1 -1
- package/dist/id-selfie-flow.esm.js +6 -6
- package/dist/id.esm.js +3 -3
- package/dist/{idCaptureManager-DtcaSiRd.esm.js → idCaptureManager-C_q4C_aV.esm.js} +1 -1
- package/dist/{idCaptureServices-DcGxxfYS.esm.js → idCaptureServices-DlnEwYJJ.esm.js} +2 -2
- package/dist/{idCaptureStateMachine-cWGQatgx.esm.js → idCaptureStateMachine-CKJ_aV6s.esm.js} +7 -7
- package/dist/index.esm.js +1 -1
- package/dist/mandatory-consent.d.ts +1 -1
- package/dist/{mandatoryConsentManager-DyPzUmsZ.d.ts → mandatoryConsentManager-B29kR0XA.d.ts} +4 -4
- package/dist/{mediapipeShared-B0iQQ5k7.esm.js → mediapipeShared-D0syL_Gp.esm.js} +6 -1
- package/dist/{onDeviceFaceResults-BuTjLnON.esm.js → onDeviceFaceResults-D53Rr_B1.esm.js} +3 -3
- package/dist/personhood.esm.js +3 -3
- package/dist/privacyLensWorker-DI-Rg83h.esm.js +355 -0
- package/dist/qe-signature.esm.js +1 -1
- package/dist/{recordingService-DdljWMN1.esm.js → recordingService-Dv5Ztm5Y.esm.js} +1 -1
- package/dist/{runChildModule-BSTzbivl.esm.js → runChildModule-Dh9HjxbH.esm.js} +1 -1
- package/dist/selfie.esm.js +4 -4
- package/dist/{selfieManager-BzCnteUg.esm.js → selfieManager-CwLK0icS.esm.js} +2 -2
- package/dist/{selfieStateMachine-TC6ctOex.esm.js → selfieStateMachine-C1ErWDmF.esm.js} +2 -2
- package/dist/{session-DY1mtqPs.esm.js → session-DvCKSn2k.esm.js} +4 -4
- package/dist/session.esm.js +4 -4
- package/dist/{sessionInitializer-q_Sx6L7L.esm.js → sessionInitializer-DR77TDcE.esm.js} +2 -2
- package/dist/{setup-DZLzxBYo.esm.js → setup-DSLbrjHG.esm.js} +4 -4
- package/dist/trueSight-CGZ_9YL1.esm.js +1 -0
- package/dist/{trueSightService-mUItB6iL.esm.js → trueSightService-PXOP5bS9.esm.js} +1 -1
- package/dist/video-selfie.esm.js +3 -3
- package/dist/{videoSelfieCameraWarmer-DAG8CmBp.esm.js → videoSelfieCameraWarmer-BLrAyxUW.esm.js} +2 -2
- package/dist/wasm.esm.js +1 -1
- package/dist/workflow.d.ts +4 -4
- package/dist/workflow.esm.js +14 -14
- package/package.json +1 -1
- package/dist/Face2DProvider-BZCsS_sf.esm.js +0 -824
- package/dist/createFaceAvatar-x5aaJwVM.esm.js +0 -42
- package/dist/trueSight-CxKqRMOO.esm.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as createHiddenVideoElement } from "./video-BtSWtHy1.esm.js";
|
|
2
|
-
import { n as createFaceLandmarker, r as dtAlpha, t as asRvfc } from "./mediapipeShared-
|
|
2
|
+
import { n as createFaceLandmarker, r as dtAlpha, t as asRvfc } from "./mediapipeShared-D0syL_Gp.esm.js";
|
|
3
3
|
/**
|
|
4
4
|
* @license
|
|
5
5
|
* Copyright 2010-2024 Three.js Authors
|
|
@@ -28142,4 +28142,4 @@ function createProvider() {
|
|
|
28142
28142
|
return new Avatar3DProvider();
|
|
28143
28143
|
}
|
|
28144
28144
|
//#endregion
|
|
28145
|
-
export { createProvider };
|
|
28145
|
+
export { Avatar3DProvider, createProvider };
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { t as createHiddenVideoElement } from "./video-BtSWtHy1.esm.js";
|
|
2
|
+
import { a as publishAvatarStats, r as isAvatarStatsEnabled, t as createAvatarStatsReporter } from "./avatarStats-CdUOb3VD.esm.js";
|
|
3
|
+
import { n as createFaceLandmarker, t as asRvfc } from "./mediapipeShared-D0syL_Gp.esm.js";
|
|
4
|
+
import { i as detectWorkerSourceInstrumentation, o as createFramePacer, r as createTrackProcessor } from "./privacyLensWorker-DI-Rg83h.esm.js";
|
|
5
|
+
import { a as createAvatar2DPoseCore, c as SatinSmileyRenderer, i as AVATAR2D_POSE_CORE_DEFAULTS, l as resolveAvatar2DAssets, n as buildAvatar2DTrackWorkerSource, o as createAvatar2DPoseCoreDeps, r as canUseAvatar2DWorker, s as extractCompactFace, t as buildAvatar2DRenderWorkerSource, takeWarmAvatar2DWorker } from "./avatar2dWarm-DqucNFX4.esm.js";
|
|
6
|
+
//#region ../infra/src/avatar/2d-default-avatar/Face2DProvider.ts
|
|
7
|
+
/**
|
|
8
|
+
* 2D satin-face avatar — MediaPipe-tracked, 2D sprite renderer.
|
|
9
|
+
* **Self-contained** (its own MediaPipe loop; see FACE_AVATAR.md → Isolation and ownership).
|
|
10
|
+
* Carries mobile robustness patterns (FACE_AVATAR.md → Mobile hardening): `window.Module` shield,
|
|
11
|
+
* CPU delegate, transformation matrix OFF, landmark-derived pose, NaN guards.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_MAX_FPS = 30;
|
|
14
|
+
const DETECT_MAX_EDGE = 320;
|
|
15
|
+
const DETECT_MIN_INTERVAL_MS = 66;
|
|
16
|
+
const DETECT_INTERVAL_TOLERANCE_MS = 8;
|
|
17
|
+
const MAX_DETECT_FAILURES = 30;
|
|
18
|
+
const WORKER_START_TIMEOUT_MS = 5e3;
|
|
19
|
+
/** Grace period for the worker to ack 'stop' before hard termination. */
|
|
20
|
+
const WORKER_STOP_GRACE_MS = 500;
|
|
21
|
+
/** Cadence of the main→worker dev statsEnabled/state-override bridge. */
|
|
22
|
+
const BRIDGE_INTERVAL_MS = 250;
|
|
23
|
+
/** Dev override global (poc demos): workers can't see it, so it is bridged. */
|
|
24
|
+
function readAvatarStateOverride() {
|
|
25
|
+
const v = globalThis.__incodeAvatarState;
|
|
26
|
+
return v === "detecting" || v === "success" || v === "error" ? v : null;
|
|
27
|
+
}
|
|
28
|
+
var Avatar2DProvider = class {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.renderer = new SatinSmileyRenderer(512);
|
|
31
|
+
this.videoHandle = null;
|
|
32
|
+
this.landmarker = null;
|
|
33
|
+
this.inputCanvas = document.createElement("canvas");
|
|
34
|
+
this.inputCtx = null;
|
|
35
|
+
this.rafId = null;
|
|
36
|
+
this.pacer = createFramePacer(DEFAULT_MAX_FPS);
|
|
37
|
+
this.stats = createAvatarStatsReporter("avatar-2d");
|
|
38
|
+
this.stopped = false;
|
|
39
|
+
this.renderWorker = null;
|
|
40
|
+
this.trackWorker = null;
|
|
41
|
+
this.clonedTrack = null;
|
|
42
|
+
this.bridgeId = null;
|
|
43
|
+
this.lastBridgedStatsEnabled = false;
|
|
44
|
+
this.lastBridgedOverride = null;
|
|
45
|
+
this.mediapipeModulePath = "";
|
|
46
|
+
this.mediapipeWasmPath = "";
|
|
47
|
+
this.faceLandmarkerModelPath = "";
|
|
48
|
+
this.detectFailures = 0;
|
|
49
|
+
this.poseCore = createAvatar2DPoseCore(AVATAR2D_POSE_CORE_DEFAULTS, createAvatar2DPoseCoreDeps());
|
|
50
|
+
this.lastDetectAt = 0;
|
|
51
|
+
this.usingRvfc = false;
|
|
52
|
+
this.rvfcHandle = null;
|
|
53
|
+
this.lastSetState = null;
|
|
54
|
+
this.loop = (now) => {
|
|
55
|
+
if (this.stopped) return;
|
|
56
|
+
this.scheduleNextFrame();
|
|
57
|
+
if (!this.pacer.shouldRender(now)) {
|
|
58
|
+
this.stats.frameDropped();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const measure = isAvatarStatsEnabled();
|
|
62
|
+
const frameStart = measure ? performance.now() : 0;
|
|
63
|
+
let detectMs;
|
|
64
|
+
const video = this.videoHandle?.element;
|
|
65
|
+
if (video && video.readyState >= 2 && this.landmarker) this.detectOnMainThread(video, now, measure, (ms) => {
|
|
66
|
+
detectMs = ms;
|
|
67
|
+
});
|
|
68
|
+
this.renderer.render(this.poseCore.tick(now));
|
|
69
|
+
if (measure) this.stats.frameRendered(now, detectMs, performance.now() - frameStart);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
getCanvas() {
|
|
73
|
+
return this.renderer.getCanvas();
|
|
74
|
+
}
|
|
75
|
+
setState(state) {
|
|
76
|
+
this.renderer.setState(state);
|
|
77
|
+
this.lastSetState = state;
|
|
78
|
+
try {
|
|
79
|
+
this.renderWorker?.postMessage({
|
|
80
|
+
type: "state",
|
|
81
|
+
state
|
|
82
|
+
});
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
async start(stream, options = {}) {
|
|
86
|
+
const assets = resolveAvatar2DAssets({
|
|
87
|
+
mediapipeModulePath: options.assets?.mediapipeModulePath,
|
|
88
|
+
mediapipeWasmPath: options.assets?.mediapipeWasmPath,
|
|
89
|
+
faceLandmarkerModelPath: options.assets?.faceLandmarkerModelPath
|
|
90
|
+
});
|
|
91
|
+
this.mediapipeModulePath = assets.mediapipeModulePath;
|
|
92
|
+
this.mediapipeWasmPath = assets.mediapipeWasmPath;
|
|
93
|
+
this.faceLandmarkerModelPath = assets.faceLandmarkerModelPath;
|
|
94
|
+
const maxFps = options.maxFps ?? DEFAULT_MAX_FPS;
|
|
95
|
+
this.pacer = createFramePacer(maxFps);
|
|
96
|
+
this.poseCore = createAvatar2DPoseCore({
|
|
97
|
+
...AVATAR2D_POSE_CORE_DEFAULTS,
|
|
98
|
+
dtSeedMs: 1e3 / maxFps
|
|
99
|
+
}, createAvatar2DPoseCoreDeps());
|
|
100
|
+
if (await this.tryStartWorker(stream, maxFps)) return;
|
|
101
|
+
if (this.stopped) return;
|
|
102
|
+
this.videoHandle = createHiddenVideoElement(stream, { keepStreamAlive: true });
|
|
103
|
+
try {
|
|
104
|
+
this.renderer.init();
|
|
105
|
+
const video = this.videoHandle.element;
|
|
106
|
+
this.usingRvfc = !!asRvfc(video);
|
|
107
|
+
this.scheduleNextFrame();
|
|
108
|
+
this.startMainTracking();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.stop();
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Background landmarker bring-up for the MAIN-THREAD path. On failure the
|
|
116
|
+
* avatar stays in its neutral searching state (the face remains concealed)
|
|
117
|
+
* rather than tearing down the already-mounted canvas.
|
|
118
|
+
*/
|
|
119
|
+
async startMainTracking() {
|
|
120
|
+
try {
|
|
121
|
+
this.landmarker = await this.createLandmarker();
|
|
122
|
+
if (this.stopped) {
|
|
123
|
+
this.landmarker?.close();
|
|
124
|
+
this.landmarker = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.stats.dispose();
|
|
128
|
+
this.stats = createAvatarStatsReporter("avatar-2d", "main");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn("[face-avatar] avatar-2d tracking failed to start — avatar stays in searching state (capture unaffected)", error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** rVFC (one callback per camera frame) when available, else capped rAF. (§1.) */
|
|
134
|
+
scheduleNextFrame() {
|
|
135
|
+
if (this.stopped) return;
|
|
136
|
+
const video = this.videoHandle?.element;
|
|
137
|
+
const rvfc = this.usingRvfc && video ? asRvfc(video) : null;
|
|
138
|
+
if (rvfc) this.rvfcHandle = rvfc.requestVideoFrameCallback(this.loop);
|
|
139
|
+
else this.rafId = requestAnimationFrame(this.loop);
|
|
140
|
+
}
|
|
141
|
+
stop() {
|
|
142
|
+
this.stopped = true;
|
|
143
|
+
this.stats.dispose();
|
|
144
|
+
if (this.bridgeId != null) {
|
|
145
|
+
clearInterval(this.bridgeId);
|
|
146
|
+
this.bridgeId = null;
|
|
147
|
+
}
|
|
148
|
+
if (this.renderWorker || this.trackWorker) {
|
|
149
|
+
try {
|
|
150
|
+
this.renderWorker?.postMessage({ type: "stop" });
|
|
151
|
+
this.trackWorker?.postMessage({ type: "stop" });
|
|
152
|
+
} catch {}
|
|
153
|
+
const pendingRender = this.renderWorker;
|
|
154
|
+
const pendingTrack = this.trackWorker;
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
if (this.renderWorker === pendingRender || this.trackWorker === pendingTrack) this.finalizeWorkers();
|
|
157
|
+
}, WORKER_STOP_GRACE_MS);
|
|
158
|
+
}
|
|
159
|
+
if (this.rafId != null) {
|
|
160
|
+
cancelAnimationFrame(this.rafId);
|
|
161
|
+
this.rafId = null;
|
|
162
|
+
}
|
|
163
|
+
const stoppingVideo = this.videoHandle?.element;
|
|
164
|
+
if (stoppingVideo && this.rvfcHandle != null) {
|
|
165
|
+
asRvfc(stoppingVideo)?.cancelVideoFrameCallback(this.rvfcHandle);
|
|
166
|
+
this.rvfcHandle = null;
|
|
167
|
+
}
|
|
168
|
+
this.landmarker?.close();
|
|
169
|
+
this.landmarker = null;
|
|
170
|
+
this.renderer.dispose();
|
|
171
|
+
this.videoHandle?.dispose();
|
|
172
|
+
this.videoHandle = null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Worker transport, TWO workers: clone the track, feed it to a
|
|
176
|
+
* MediaStreamTrackProcessor, adopt (or boot) the TRACK worker, boot the
|
|
177
|
+
* RENDER worker, wire them with a MessageChannel, transfer the renderer's
|
|
178
|
+
* canvas + the readable, and wait for the render worker's ms-fast 'started'
|
|
179
|
+
* handshake (the track worker's MediaPipe bring-up runs in the background —
|
|
180
|
+
* or already ran via the tutorial warm-up). Any failure tears down —
|
|
181
|
+
* recreating the renderer if its canvas was already transferred — and
|
|
182
|
+
* returns false so the main-thread path takes over. Never throws.
|
|
183
|
+
*/
|
|
184
|
+
async tryStartWorker(stream, maxFps) {
|
|
185
|
+
if (!canUseAvatar2DWorker()) return false;
|
|
186
|
+
const sourceTrack = stream.getVideoTracks()[0];
|
|
187
|
+
if (!sourceTrack) return false;
|
|
188
|
+
let track = null;
|
|
189
|
+
let processor;
|
|
190
|
+
try {
|
|
191
|
+
track = sourceTrack.clone();
|
|
192
|
+
processor = createTrackProcessor(track);
|
|
193
|
+
} catch {
|
|
194
|
+
track?.stop();
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
let trackWorker = takeWarmAvatar2DWorker({
|
|
198
|
+
mediapipeModulePath: this.mediapipeModulePath,
|
|
199
|
+
mediapipeWasmPath: this.mediapipeWasmPath,
|
|
200
|
+
faceLandmarkerModelPath: this.faceLandmarkerModelPath
|
|
201
|
+
});
|
|
202
|
+
let trackBlobUrl = null;
|
|
203
|
+
if (!trackWorker) {
|
|
204
|
+
const trackSource = buildAvatar2DTrackWorkerSource();
|
|
205
|
+
const trackInstrumenter = detectWorkerSourceInstrumentation(trackSource);
|
|
206
|
+
if (trackInstrumenter) {
|
|
207
|
+
console.error(`[face-avatar] avatar-2d worker source was instrumented by "${trackInstrumenter}" and cannot run in a worker; using the slower main-thread avatar. Exclude packages/infra from that transform (see docs/features/FACE_AVATAR.md → Worker transport).`);
|
|
208
|
+
track.stop();
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
trackBlobUrl = URL.createObjectURL(new Blob([trackSource], { type: "text/javascript" }));
|
|
213
|
+
trackWorker = new Worker(trackBlobUrl);
|
|
214
|
+
} catch {
|
|
215
|
+
if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
|
|
216
|
+
track.stop();
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const renderSource = buildAvatar2DRenderWorkerSource();
|
|
221
|
+
const renderInstrumenter = detectWorkerSourceInstrumentation(renderSource);
|
|
222
|
+
let renderWorker = null;
|
|
223
|
+
let renderBlobUrl = null;
|
|
224
|
+
if (!renderInstrumenter) try {
|
|
225
|
+
renderBlobUrl = URL.createObjectURL(new Blob([renderSource], { type: "text/javascript" }));
|
|
226
|
+
renderWorker = new Worker(renderBlobUrl);
|
|
227
|
+
} catch {
|
|
228
|
+
if (renderBlobUrl) URL.revokeObjectURL(renderBlobUrl);
|
|
229
|
+
renderBlobUrl = null;
|
|
230
|
+
renderWorker = null;
|
|
231
|
+
}
|
|
232
|
+
if (!renderWorker) {
|
|
233
|
+
if (renderInstrumenter) console.error(`[face-avatar] avatar-2d worker source was instrumented by "${renderInstrumenter}" and cannot run in a worker; using the slower main-thread avatar. Exclude packages/infra from that transform (see docs/features/FACE_AVATAR.md → Worker transport).`);
|
|
234
|
+
trackWorker.terminate();
|
|
235
|
+
if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
|
|
236
|
+
track.stop();
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
trackWorker.onmessage = (event) => {
|
|
240
|
+
const message = event.data;
|
|
241
|
+
if (message?.type === "trackingFatal") {
|
|
242
|
+
console.warn("[face-avatar] avatar-2d worker tracking failed to start — avatar stays in searching state (capture unaffected):", message.message);
|
|
243
|
+
trackWorker.terminate();
|
|
244
|
+
if (this.trackWorker === trackWorker) this.trackWorker = null;
|
|
245
|
+
track.stop();
|
|
246
|
+
if (this.clonedTrack === track) this.clonedTrack = null;
|
|
247
|
+
} else if (message?.type === "stopped") {
|
|
248
|
+
trackWorker.terminate();
|
|
249
|
+
if (this.trackWorker === trackWorker) this.trackWorker = null;
|
|
250
|
+
track.stop();
|
|
251
|
+
if (this.clonedTrack === track) this.clonedTrack = null;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
trackWorker.onerror = () => {
|
|
255
|
+
console.warn("[face-avatar] avatar-2d track worker error — pose frozen (capture unaffected)");
|
|
256
|
+
trackWorker.terminate();
|
|
257
|
+
if (this.trackWorker === trackWorker) this.trackWorker = null;
|
|
258
|
+
track.stop();
|
|
259
|
+
if (this.clonedTrack === track) this.clonedTrack = null;
|
|
260
|
+
};
|
|
261
|
+
let started;
|
|
262
|
+
try {
|
|
263
|
+
const channel = new MessageChannel();
|
|
264
|
+
const offscreen = this.renderer.getCanvas().transferControlToOffscreen();
|
|
265
|
+
const handshakeWorker = renderWorker;
|
|
266
|
+
const startedTrackWorker = trackWorker;
|
|
267
|
+
started = await new Promise((resolve) => {
|
|
268
|
+
const timer = setTimeout(() => resolve(false), WORKER_START_TIMEOUT_MS);
|
|
269
|
+
handshakeWorker.onmessage = (event) => {
|
|
270
|
+
if (event.data?.type === "started") {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
resolve(true);
|
|
273
|
+
} else if (event.data?.type === "fatal") {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
resolve(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
handshakeWorker.onerror = () => {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
resolve(false);
|
|
281
|
+
};
|
|
282
|
+
handshakeWorker.postMessage({
|
|
283
|
+
type: "start",
|
|
284
|
+
canvas: offscreen,
|
|
285
|
+
port: channel.port1,
|
|
286
|
+
maxFps,
|
|
287
|
+
poseConfig: {
|
|
288
|
+
...AVATAR2D_POSE_CORE_DEFAULTS,
|
|
289
|
+
dtSeedMs: 1e3 / maxFps
|
|
290
|
+
},
|
|
291
|
+
statsEnabled: isAvatarStatsEnabled()
|
|
292
|
+
}, [offscreen, channel.port1]);
|
|
293
|
+
startedTrackWorker.postMessage({
|
|
294
|
+
type: "start",
|
|
295
|
+
readable: processor.readable,
|
|
296
|
+
port: channel.port2,
|
|
297
|
+
mediapipeModulePath: this.mediapipeModulePath,
|
|
298
|
+
mediapipeWasmPath: this.mediapipeWasmPath,
|
|
299
|
+
faceLandmarkerModelPath: this.faceLandmarkerModelPath,
|
|
300
|
+
detectMaxEdge: DETECT_MAX_EDGE,
|
|
301
|
+
detectMinIntervalMs: DETECT_MIN_INTERVAL_MS,
|
|
302
|
+
detectIntervalToleranceMs: DETECT_INTERVAL_TOLERANCE_MS
|
|
303
|
+
}, [processor.readable, channel.port2]);
|
|
304
|
+
});
|
|
305
|
+
} catch {
|
|
306
|
+
renderWorker.terminate();
|
|
307
|
+
trackWorker.terminate();
|
|
308
|
+
if (renderBlobUrl) URL.revokeObjectURL(renderBlobUrl);
|
|
309
|
+
if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
|
|
310
|
+
track.stop();
|
|
311
|
+
if (!this.stopped) this.renderer = new SatinSmileyRenderer(512);
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (renderBlobUrl) URL.revokeObjectURL(renderBlobUrl);
|
|
315
|
+
if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
|
|
316
|
+
if (!started || this.stopped) {
|
|
317
|
+
renderWorker.terminate();
|
|
318
|
+
trackWorker.terminate();
|
|
319
|
+
track.stop();
|
|
320
|
+
if (!this.stopped) this.renderer = new SatinSmileyRenderer(512);
|
|
321
|
+
return this.stopped;
|
|
322
|
+
}
|
|
323
|
+
this.renderWorker = renderWorker;
|
|
324
|
+
this.trackWorker = trackWorker;
|
|
325
|
+
this.clonedTrack = track;
|
|
326
|
+
this.lastBridgedStatsEnabled = isAvatarStatsEnabled();
|
|
327
|
+
this.lastBridgedOverride = null;
|
|
328
|
+
renderWorker.onmessage = (event) => {
|
|
329
|
+
const message = event.data;
|
|
330
|
+
if (message?.type === "stats") {
|
|
331
|
+
if (!this.stopped) publishAvatarStats(message.snapshot);
|
|
332
|
+
} else if (message?.type === "stopped") {
|
|
333
|
+
this.renderWorker?.terminate();
|
|
334
|
+
this.renderWorker = null;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
renderWorker.onerror = () => {
|
|
338
|
+
console.warn("[face-avatar] avatar-2d render worker error — avatar frozen (capture unaffected)");
|
|
339
|
+
if (this.bridgeId != null) {
|
|
340
|
+
clearInterval(this.bridgeId);
|
|
341
|
+
this.bridgeId = null;
|
|
342
|
+
}
|
|
343
|
+
this.finalizeWorkers();
|
|
344
|
+
};
|
|
345
|
+
if (this.lastSetState) renderWorker.postMessage({
|
|
346
|
+
type: "state",
|
|
347
|
+
state: this.lastSetState
|
|
348
|
+
});
|
|
349
|
+
this.bridgeId = setInterval(() => {
|
|
350
|
+
if (!this.renderWorker) return;
|
|
351
|
+
const enabled = isAvatarStatsEnabled();
|
|
352
|
+
if (enabled !== this.lastBridgedStatsEnabled) {
|
|
353
|
+
this.lastBridgedStatsEnabled = enabled;
|
|
354
|
+
this.renderWorker.postMessage({
|
|
355
|
+
type: "statsEnabled",
|
|
356
|
+
value: enabled
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const override = readAvatarStateOverride();
|
|
360
|
+
if (override !== this.lastBridgedOverride) {
|
|
361
|
+
this.lastBridgedOverride = override;
|
|
362
|
+
this.renderWorker.postMessage({
|
|
363
|
+
type: "override",
|
|
364
|
+
state: override
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}, BRIDGE_INTERVAL_MS);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
finalizeWorkers() {
|
|
371
|
+
this.renderWorker?.terminate();
|
|
372
|
+
this.renderWorker = null;
|
|
373
|
+
this.trackWorker?.terminate();
|
|
374
|
+
this.trackWorker = null;
|
|
375
|
+
this.clonedTrack?.stop();
|
|
376
|
+
this.clonedTrack = null;
|
|
377
|
+
}
|
|
378
|
+
/** Loads the FaceLandmarker via the shared shielded init (see mediapipeShared). */
|
|
379
|
+
createLandmarker() {
|
|
380
|
+
return createFaceLandmarker({
|
|
381
|
+
mediapipeModulePath: this.mediapipeModulePath,
|
|
382
|
+
mediapipeWasmPath: this.mediapipeWasmPath,
|
|
383
|
+
faceLandmarkerModelPath: this.faceLandmarkerModelPath
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/** Main-thread detect fallback: downscale → detectForVideo → ingest. */
|
|
387
|
+
detectOnMainThread(video, now, measure, onDetectMs) {
|
|
388
|
+
if (!this.landmarker) return;
|
|
389
|
+
const vw = video.videoWidth;
|
|
390
|
+
const vh = video.videoHeight;
|
|
391
|
+
const detectDue = now - this.lastDetectAt >= DETECT_MIN_INTERVAL_MS - DETECT_INTERVAL_TOLERANCE_MS;
|
|
392
|
+
if (vw > 0 && vh > 0 && detectDue) {
|
|
393
|
+
if (!this.inputCtx) this.inputCtx = this.inputCanvas.getContext("2d");
|
|
394
|
+
if (this.inputCtx) {
|
|
395
|
+
this.lastDetectAt = now;
|
|
396
|
+
const dscale = Math.min(1, DETECT_MAX_EDGE / Math.max(vw, vh));
|
|
397
|
+
const dw = Math.max(1, Math.round(vw * dscale));
|
|
398
|
+
const dh = Math.max(1, Math.round(vh * dscale));
|
|
399
|
+
if (this.inputCanvas.width !== dw || this.inputCanvas.height !== dh) {
|
|
400
|
+
this.inputCanvas.width = dw;
|
|
401
|
+
this.inputCanvas.height = dh;
|
|
402
|
+
}
|
|
403
|
+
this.inputCtx.drawImage(video, 0, 0, dw, dh);
|
|
404
|
+
let result = null;
|
|
405
|
+
const detectStart = measure ? performance.now() : 0;
|
|
406
|
+
try {
|
|
407
|
+
result = this.landmarker.detectForVideo(this.inputCanvas, now);
|
|
408
|
+
this.detectFailures = 0;
|
|
409
|
+
} catch {
|
|
410
|
+
result = null;
|
|
411
|
+
this.detectFailures += 1;
|
|
412
|
+
if (this.detectFailures >= MAX_DETECT_FAILURES) this.recreateLandmarker();
|
|
413
|
+
}
|
|
414
|
+
if (measure) onDetectMs(performance.now() - detectStart);
|
|
415
|
+
this.poseCore.ingest(result ? extractCompactFace(result) : null, dw, dh, now);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
recreateLandmarker() {
|
|
420
|
+
this.detectFailures = 0;
|
|
421
|
+
this.landmarker?.close();
|
|
422
|
+
this.landmarker = null;
|
|
423
|
+
this.createLandmarker().then((lm) => {
|
|
424
|
+
if (this.stopped) {
|
|
425
|
+
lm.close();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this.landmarker = lm;
|
|
429
|
+
}).catch(() => {});
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
/** Factory used by the avatar actor's provider selection (2D satin-face avatar). */
|
|
433
|
+
function createProvider() {
|
|
434
|
+
return new Avatar2DProvider();
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
export { Avatar2DProvider, createProvider };
|