fix(handtracking): absorb React StrictMode double-mount
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
In dev, <StrictMode> intentionally mounts → unmounts → remounts each effect to surface non-idempotent code. The hand tracking hooks were calling getUserMedia and creating MediaPipe / WebSocket runtimes on every mount, which in practice ran the full start/stop/start cycle inside a few milliseconds and pushed WebGL over its limit on top of the loaded scene → context lost. Add HAND_TRACKING_RUNTIME_START_DELAY_MS (80ms) and delay the actual start() call behind a setTimeout in both useBrowserHandTracking and useRemoteHandTracking. The cleanup clears the timer, so a fast mount/unmount never reaches start(). 80ms is invisible to the user (<5 frames at 60fps) and also absorbs rapid `nearby` toggles at trigger borders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,8 @@ export const HAND_TRACKING_BROWSER_WASM_URL =
|
|||||||
export const HAND_TRACKING_BROWSER_MODEL_URL =
|
export const HAND_TRACKING_BROWSER_MODEL_URL =
|
||||||
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
|
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
|
||||||
export const HAND_TRACKING_BROWSER_DELEGATE: "CPU" | "GPU" = "CPU";
|
export const HAND_TRACKING_BROWSER_DELEGATE: "CPU" | "GPU" = "CPU";
|
||||||
|
|
||||||
|
// Delay before the runtime actually starts after `enabled` flips to true.
|
||||||
|
// Absorbs React StrictMode's mount/unmount/mount cycle in dev and rapid
|
||||||
|
// `nearby` toggles at trigger borders. Invisible to the user (~5 frames).
|
||||||
|
export const HAND_TRACKING_RUNTIME_START_DELAY_MS = 80;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
HAND_TRACKING_FRAME_HEIGHT,
|
HAND_TRACKING_FRAME_HEIGHT,
|
||||||
HAND_TRACKING_FRAME_WIDTH,
|
HAND_TRACKING_FRAME_WIDTH,
|
||||||
|
HAND_TRACKING_RUNTIME_START_DELAY_MS,
|
||||||
HAND_TRACKING_TARGET_FPS,
|
HAND_TRACKING_TARGET_FPS,
|
||||||
} from "@/data/handTrackingConfig";
|
} from "@/data/handTrackingConfig";
|
||||||
import {
|
import {
|
||||||
@@ -169,10 +170,17 @@ export function useBrowserHandTracking({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void start();
|
// Delay the actual start so that a StrictMode mount/unmount/mount
|
||||||
|
// cycle, or a rapid `enabled` toggle at a trigger border, does not
|
||||||
|
// spin up the camera + MediaPipe twice in a few milliseconds.
|
||||||
|
const startTimer = window.setTimeout(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
void start();
|
||||||
|
}, HAND_TRACKING_RUNTIME_START_DELAY_MS);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
window.clearTimeout(startTimer);
|
||||||
cleanup();
|
cleanup();
|
||||||
};
|
};
|
||||||
}, [enabled]);
|
}, [enabled]);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
HAND_TRACKING_FRAME_WIDTH,
|
HAND_TRACKING_FRAME_WIDTH,
|
||||||
HAND_TRACKING_JPEG_QUALITY,
|
HAND_TRACKING_JPEG_QUALITY,
|
||||||
HAND_TRACKING_RESPONSE_TIMEOUT_MS,
|
HAND_TRACKING_RESPONSE_TIMEOUT_MS,
|
||||||
|
HAND_TRACKING_RUNTIME_START_DELAY_MS,
|
||||||
HAND_TRACKING_TARGET_FPS,
|
HAND_TRACKING_TARGET_FPS,
|
||||||
} from "@/data/handTrackingConfig";
|
} from "@/data/handTrackingConfig";
|
||||||
import { getHandTrackingWsUrl } from "@/utils/handTracking/handTrackingEndpoint";
|
import { getHandTrackingWsUrl } from "@/utils/handTracking/handTrackingEndpoint";
|
||||||
@@ -330,10 +331,17 @@ export function useRemoteHandTracking({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void start();
|
// Delay the actual start so that a StrictMode mount/unmount/mount
|
||||||
|
// cycle, or a rapid `enabled` toggle at a trigger border, does not
|
||||||
|
// open the camera + WebSocket twice in a few milliseconds.
|
||||||
|
const startTimer = window.setTimeout(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
void start();
|
||||||
|
}, HAND_TRACKING_RUNTIME_START_DELAY_MS);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
window.clearTimeout(startTimer);
|
||||||
cleanup();
|
cleanup();
|
||||||
};
|
};
|
||||||
}, [enabled, websocketUrl]);
|
}, [enabled, websocketUrl]);
|
||||||
|
|||||||
Reference in New Issue
Block a user