fix: improve handle error in face-mesh

This commit is contained in:
anh doan 2026-03-23 16:32:25 +07:00
parent 39de9fdf8b
commit 04c3eed7e0
No known key found for this signature in database
2 changed files with 99 additions and 5 deletions

View File

@ -14,6 +14,61 @@ let isLoading = false;
let pendingDetection: Promise<Results> | null = null;
let pendingResolve: ((results: Results) => void) | null = null;
let pendingReject: ((error: Error) => void) | null = null;
let pendingTimeoutId: ReturnType<typeof setTimeout> | null = null;
/** Detection timeout in milliseconds */
const DETECTION_TIMEOUT_MS = 5000;
/** Types that MediaPipe FaceMesh accepts */
type MediaPipeImageSource =
| HTMLImageElement
| HTMLCanvasElement
| HTMLVideoElement;
/** Clear the pending timeout if it exists */
function clearPendingTimeout(): void {
if (pendingTimeoutId !== null) {
clearTimeout(pendingTimeoutId);
pendingTimeoutId = null;
}
}
/** Check if source is a valid MediaPipe image source */
function isMediaPipeImageSource(
source: CanvasImageSource,
): source is MediaPipeImageSource {
return (
source instanceof HTMLImageElement ||
source instanceof HTMLCanvasElement ||
source instanceof HTMLVideoElement
);
}
/** Convert OffscreenCanvas to HTMLCanvasElement for MediaPipe compatibility */
function toHTMLCanvas(source: OffscreenCanvas): HTMLCanvasElement {
const canvas = document.createElement("canvas");
canvas.width = source.width;
canvas.height = source.height;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(source, 0, 0);
}
return canvas;
}
/** Prepare source for MediaPipe — converts OffscreenCanvas if needed */
function prepareSourceForMediaPipe(
source: CanvasImageSource,
): MediaPipeImageSource | null {
if (isMediaPipeImageSource(source)) {
return source;
}
if (source instanceof OffscreenCanvas) {
return toHTMLCanvas(source);
}
// ImageBitmap, SVGImageElement, VideoFrame are not supported
return null;
}
/** Lazy-load MediaPipe Face Mesh WASM module */
async function loadFaceMesh(): Promise<FaceMeshType | null> {
@ -35,8 +90,10 @@ async function loadFaceMesh(): Promise<FaceMeshType | null> {
});
fm.onResults((results: Results) => {
if (pendingResolve) {
clearPendingTimeout();
pendingResolve(results);
pendingResolve = null;
pendingReject = null;
}
});
faceMeshInstance = fm;
@ -93,6 +150,13 @@ export async function detectFace(
return { faceDetected: false };
}
// Convert source to MediaPipe-compatible format
const mediaPipeSource = prepareSourceForMediaPipe(source);
if (!mediaPipeSource) {
// Source type not supported by MediaPipe
return { faceDetected: false };
}
// Reuse existing in-flight detection if one exists
if (pendingDetection) {
const results = await pendingDetection;
@ -105,14 +169,43 @@ export async function detectFace(
return landmarksToContext(results.multiFaceLandmarks[0]);
}
// Create new detection promise
// Create new detection promise with timeout
pendingDetection = new Promise<Results>((resolve, reject) => {
pendingResolve = resolve;
pendingReject = reject;
fm.send({ image: source as HTMLCanvasElement });
// Set up timeout for detection
pendingTimeoutId = setTimeout(() => {
if (pendingReject) {
pendingReject(new Error("Face detection timeout"));
pendingResolve = null;
pendingReject = null;
pendingDetection = null;
pendingTimeoutId = null;
}
}, DETECTION_TIMEOUT_MS);
// Send image for detection, catching sync errors
try {
fm.send({ image: mediaPipeSource });
} catch (error) {
clearPendingTimeout();
reject(error instanceof Error ? error : new Error(String(error)));
}
});
const results = await pendingDetection;
let results: Results;
try {
results = await pendingDetection;
} catch (error) {
// Detection failed (timeout or error) — return no face detected
pendingDetection = null;
pendingResolve = null;
pendingReject = null;
return { faceDetected: false };
}
clearPendingTimeout();
pendingDetection = null;
pendingResolve = null;
pendingReject = null;
@ -134,7 +227,8 @@ export function isFaceMeshReady(): boolean {
/** Clean up MediaPipe resources */
export function disposeFaceMesh(): void {
// Settle any pending detection before disposing
// Clear timeout and settle any pending detection before disposing
clearPendingTimeout();
if (pendingReject) {
pendingReject(new Error("Face mesh disposed"));
pendingReject = null;

File diff suppressed because one or more lines are too long