import { useEffect, useRef } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import * as THREE from 'three'; import { setBroadcastSource } from '@/lib/webrtc/broadcastSource'; interface XRBroadcastMirrorProps { enabled: boolean; width?: number; height?: number; /** Solid background color used in place of real passthrough (privacy + clarity). */ backgroundColor?: string; } /** * Renders the current scene to an OFFSCREEN opaque canvas every frame so a * MediaStream can capture it. The main XR canvas uses alpha:true for * passthrough — viewers receiving captureStream() of that would see only * the 3D content over transparency. This mirror gives them a usable picture. */ export function XRBroadcastMirror({ enabled, width = 1280, height = 720, backgroundColor = '#1a1a1a', }: XRBroadcastMirrorProps) { const { gl, scene } = useThree(); const canvasRef = useRef(null); const rendererRef = useRef(null); const mirrorCamRef = useRef(null); // Create / dispose the offscreen renderer useEffect(() => { if (!enabled) return; const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvasRef.current = canvas; const renderer = new THREE.WebGLRenderer({ canvas, alpha: false, antialias: true, powerPreference: 'high-performance', preserveDrawingBuffer: false, }); renderer.setPixelRatio(1); renderer.setSize(width, height, false); renderer.setClearColor(new THREE.Color(backgroundColor), 1); renderer.outputColorSpace = THREE.SRGBColorSpace; rendererRef.current = renderer; mirrorCamRef.current = new THREE.PerspectiveCamera(70, width / height, 0.01, 100); setBroadcastSource(canvas); return () => { setBroadcastSource(null); renderer.dispose(); rendererRef.current = null; canvasRef.current = null; mirrorCamRef.current = null; }; }, [enabled, width, height, backgroundColor]); // Per-frame mirror render (throttled to ~24 fps to save GPU) const lastTime = useRef(0); useFrame(() => { if (!enabled) return; const renderer = rendererRef.current; const mirrorCam = mirrorCamRef.current; if (!renderer || !mirrorCam) return; const now = performance.now(); if (now - lastTime.current < 42) return; // ~24 fps cap lastTime.current = now; // Pick the best available camera: XR camera if in session, else main camera let srcCam: THREE.Camera | null = null; const xrCam = (gl.xr as unknown as { isPresenting?: boolean; getCamera?: () => THREE.ArrayCamera }).getCamera?.(); const isPresenting = (gl.xr as unknown as { isPresenting?: boolean }).isPresenting; if (isPresenting && xrCam && (xrCam as THREE.ArrayCamera).cameras?.length) { srcCam = (xrCam as THREE.ArrayCamera).cameras[0]; // left eye } if (srcCam instanceof THREE.PerspectiveCamera) { mirrorCam.fov = srcCam.fov; mirrorCam.near = srcCam.near; mirrorCam.far = srcCam.far; } if (srcCam) { srcCam.updateMatrixWorld(true); mirrorCam.position.setFromMatrixPosition(srcCam.matrixWorld); mirrorCam.quaternion.setFromRotationMatrix(srcCam.matrixWorld); } else { // Fallback: use the main R3F camera (desktop preview) const main = (gl as unknown as { __mainCam?: THREE.Camera }).__mainCam; if (main) { main.updateMatrixWorld(true); mirrorCam.position.setFromMatrixPosition(main.matrixWorld); mirrorCam.quaternion.setFromRotationMatrix(main.matrixWorld); if (main instanceof THREE.PerspectiveCamera) { mirrorCam.fov = main.fov; mirrorCam.near = main.near; mirrorCam.far = main.far; } } } mirrorCam.aspect = width / height; mirrorCam.updateProjectionMatrix(); try { renderer.render(scene, mirrorCam); } catch (e) { console.warn('[XRBroadcastMirror] render failed', e); } }); return null; }