fix: improve handle error in face-mesh
This commit is contained in:
parent
39de9fdf8b
commit
04c3eed7e0
|
|
@ -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
Loading…
Reference in New Issue