diff --git a/features/video-studio/frontend-live/src/components/DisguiseVideoParticipantVideo.tsx b/features/video-studio/frontend-live/src/components/DisguiseVideoParticipantVideo.tsx index 501c61f7b..10d41d9ad 100644 --- a/features/video-studio/frontend-live/src/components/DisguiseVideoParticipantVideo.tsx +++ b/features/video-studio/frontend-live/src/components/DisguiseVideoParticipantVideo.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, type CSSProperties } from 'react'; +import { useEffect, useRef, type CSSProperties, type ReactElement } from 'react'; import { useFaceDetection } from '../hooks/useFaceDetection'; import { applyBlur } from '../renderers/BlurRenderer'; import { drawGimpMask } from '../renderers/GimpMaskRenderer'; @@ -36,7 +36,8 @@ export interface DisguiseVideoParticipantVideoProps { * The output canvas can be consumed visually or captured as a MediaStream via * `onCaptureStream` for WebRTC integration. * - * Production deployments must set these headers for MediaPipe WASM threading: + * Production deployments must set these response headers for MediaPipe WASM + * thread-pool support (SharedArrayBuffer): * Cross-Origin-Opener-Policy: same-origin * Cross-Origin-Embedder-Policy: require-corp */ @@ -49,13 +50,16 @@ export function DisguiseVideoParticipantVideo({ onCaptureStream, className, style, -}: DisguiseVideoParticipantVideoProps): JSX.Element { +}: DisguiseVideoParticipantVideoProps): ReactElement { const videoRef = useRef(null); const canvasRef = useRef(null); + // Stable ref for the 2D context so the render loop doesn't need to call + // getContext() on every frame. + const ctxRef = useRef(null); const localStreamRef = useRef(null); - // These refs let the render loop read the latest prop values without being - // re-created on every render cycle. + // Mirror the latest prop values into refs so the render loop closure can + // read current values without being re-created on every render. const disguiseRef = useRef(disguise); disguiseRef.current = disguise; @@ -70,6 +74,12 @@ export function DisguiseVideoParticipantVideo({ const detectRef = useRef(detectForVideo); detectRef.current = detectForVideo; + // ── Context initialisation ───────────────────────────────────────────────── + useEffect(() => { + if (!canvasRef.current) return; + ctxRef.current = canvasRef.current.getContext('2d'); + }, []); + // ── Stream setup ─────────────────────────────────────────────────────────── useEffect(() => { const video = videoRef.current; @@ -99,8 +109,8 @@ export function DisguiseVideoParticipantVideo({ } setup().catch((_err: unknown) => { - // Stream errors surface via the video element's error event or the - // useFaceDetection hook — no additional handling needed here. + // getUserMedia / play errors are surfaced to the user via the video + // element's error event; no additional handling is needed here. }); return () => { @@ -113,18 +123,13 @@ export function DisguiseVideoParticipantVideo({ // ── Render loop ──────────────────────────────────────────────────────────── useEffect(() => { - const video = videoRef.current; - const canvas = canvasRef.current; - if (!video || !canvas) return; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - let animId: number; function render(): void { - // Wait until video has decoded enough data to draw. - if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { + const video = videoRef.current; + const ctx = ctxRef.current; + + if (video && ctx && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { ctx.drawImage(video, 0, 0, width, height); if (isReadyRef.current && disguiseRef.current !== 'none') { @@ -146,7 +151,7 @@ export function DisguiseVideoParticipantVideo({ animId = requestAnimationFrame(render); return () => cancelAnimationFrame(animId); - }, [width, height]); // Only restart loop when dimensions change. + }, [width, height]); // Only restart when output dimensions change. // ── Capture stream ───────────────────────────────────────────────────────── useEffect(() => {