drawface-live / js /trackworker.js
ingon1's picture
sync from a4623ed60bfdc48500dc98de93020c59fe69bf34
4635d45 verified
Raw
History Blame Contribute Delete
3.16 kB
// Module worker: runs the MediaPipe FaceLandmarker off the main thread so a
// slow detect no longer janks the render loop. Protocol —
// in : {type:"frame", bitmap:ImageBitmap (transferred), ts}
// out: {type:"ready"} once initialized, {type:"fail", message} on init error,
// {type:"result", ts, obs|null} per frame (obs matches tracker.js detect()).
import { CDN_URL, WASM_BASE, MODEL_URL, eulerFromMatrix } from "./trackconfig.js?v=20260729.6";
// tasks-vision loads its classic wasm loader via importScripts(), which throws
// in module workers. Shim it with sync XHR + indirect eval: synchronous like
// the original, and non-strict global eval so the loader's top-level
// `var ModuleFactory` lands on `self` where the bundle expects it.
self.importScripts = (...urls) => {
for (const url of urls) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.send();
if (xhr.status !== 200) throw new Error(`importScripts shim: HTTP ${xhr.status} for ${url}`);
(0, eval)(xhr.responseText);
}
};
let landmarker = null;
async function init() {
const { FaceLandmarker, FilesetResolver } = await import(CDN_URL);
const vision = await FilesetResolver.forVisionTasks(WASM_BASE);
const options = {
baseOptions: { modelAssetPath: MODEL_URL, delegate: "GPU" },
runningMode: "VIDEO",
numFaces: 1,
outputFaceBlendshapes: true,
outputFacialTransformationMatrixes: true,
};
try {
landmarker = await FaceLandmarker.createFromOptions(vision, options);
} catch {
options.baseOptions.delegate = "CPU"; // GPU delegate unavailable — retry on CPU
landmarker = await FaceLandmarker.createFromOptions(vision, options);
}
}
init().then(
() => self.postMessage({ type: "ready" }),
(e) => self.postMessage({ type: "fail", message: e?.message ?? String(e) }),
);
function detect(bitmap, ts) {
const result = landmarker.detectForVideo(bitmap, ts);
if (!result.faceBlendshapes || result.faceBlendshapes.length === 0) return null;
const blend = {};
for (const c of result.faceBlendshapes[0].categories) blend[c.categoryName] = c.score;
// Plain {x, y} copies — the observation must survive structured clone.
const landmarks = result.faceLandmarks?.[0]?.map((p) => ({ x: p.x, y: p.y })) ?? null;
let yaw = 0, pitch = 0, roll = 0;
if (result.facialTransformationMatrixes && result.facialTransformationMatrixes.length) {
({ yaw, pitch, roll } = eulerFromMatrix(result.facialTransformationMatrixes[0].data));
}
return { blend, yaw, pitch, roll, landmarks };
}
self.onmessage = (ev) => {
const { type, bitmap, ts } = ev.data ?? {};
if (type !== "frame") return;
let obs = null;
let error = null;
try {
if (landmarker) obs = detect(bitmap, ts);
} catch (err) {
// Never let one malformed frame or MediaPipe runtime fault kill the worker:
// the main thread needs a message to release its latest-wins gate and switch
// to the synchronous fallback.
error = err?.message ?? String(err);
} finally {
bitmap.close();
self.postMessage(error ? { type: "error", ts, message: error } : { type: "result", ts, obs });
}
};