Spaces:
Sleeping
Sleeping
| import { useEffect, useRef } from 'react'; | |
| const CameraSetup = ({ | |
| videoRef, | |
| canvasRef, | |
| containerRef, | |
| facingMode, | |
| setFacingMode, | |
| setCameraError, | |
| setVideoAspectRatio, | |
| updateCanvasSize, | |
| isMobile | |
| }) => { | |
| // Track if component is mounted to prevent state updates after unmount | |
| const isMounted = useRef(true); | |
| // Track if camera is currently being set up | |
| const isSettingUpCamera = useRef(false); | |
| const updateDimensions = () => { | |
| if (!videoRef.current || !isMounted.current) return; | |
| const videoWidth = videoRef.current.videoWidth; | |
| const videoHeight = videoRef.current.videoHeight; | |
| // Only update if we have valid dimensions | |
| if (videoWidth && videoHeight) { | |
| const aspectRatio = videoWidth / videoHeight; | |
| setVideoAspectRatio(aspectRatio); | |
| updateCanvasSize(aspectRatio); | |
| } | |
| }; | |
| // Set up the webcam | |
| useEffect(() => { | |
| // Set mounted flag | |
| isMounted.current = true; | |
| const setupCamera = async () => { | |
| if (!videoRef.current || !canvasRef.current || !containerRef.current) return; | |
| // Prevent multiple simultaneous setup attempts | |
| if (isSettingUpCamera.current) return; | |
| isSettingUpCamera.current = true; | |
| try { | |
| // Stop any existing stream first | |
| if (videoRef.current.srcObject) { | |
| const tracks = videoRef.current.srcObject.getTracks(); | |
| tracks.forEach(track => track.stop()); | |
| } | |
| // Get camera constraints based on device | |
| const constraints = { | |
| video: { | |
| facingMode: facingMode, | |
| // For mobile, prioritize height to prevent letterboxing | |
| ...(isMobile ? { | |
| height: { ideal: 1080, min: 720 }, | |
| width: { ideal: 1920, min: 1280 } | |
| } : { | |
| width: { ideal: 1920 }, | |
| height: { ideal: 1080 } | |
| }) | |
| }, | |
| audio: false | |
| }; | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| if (!isMounted.current) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| return; | |
| } | |
| videoRef.current.srcObject = stream; | |
| // Add loadeddata event listener for more reliable dimension detection | |
| videoRef.current.addEventListener('loadeddata', updateDimensions); | |
| try { | |
| await videoRef.current.play(); | |
| // Double check dimensions after a short delay to ensure they're correct | |
| setTimeout(updateDimensions, 100); | |
| } catch (playError) { | |
| console.log("Play interrupted, this is normal if component remounted:", playError); | |
| if (playError.name !== "AbortError") { | |
| throw playError; | |
| } | |
| } | |
| if (isMounted.current) { | |
| setCameraError(false); | |
| console.log("Camera set up successfully"); | |
| } | |
| } catch (error) { | |
| console.error('Error accessing webcam:', error); | |
| if (isMounted.current) { | |
| setCameraError(true); | |
| } | |
| } finally { | |
| isSettingUpCamera.current = false; | |
| } | |
| }; | |
| setupCamera(); | |
| return () => { | |
| // Set mounted flag to false to prevent state updates after unmount | |
| isMounted.current = false; | |
| if (videoRef.current) { | |
| videoRef.current.removeEventListener('loadeddata', updateDimensions); | |
| if (videoRef.current.srcObject) { | |
| const tracks = videoRef.current.srcObject.getTracks(); | |
| tracks.forEach(track => track.stop()); | |
| } | |
| } | |
| }; | |
| }, [videoRef, canvasRef, containerRef, facingMode]); | |
| // Function to switch camera | |
| const switchCamera = async () => { | |
| if (!videoRef.current || isSettingUpCamera.current) return; | |
| isSettingUpCamera.current = true; | |
| // Stop current stream | |
| if (videoRef.current.srcObject) { | |
| const tracks = videoRef.current.srcObject.getTracks(); | |
| tracks.forEach(track => track.stop()); | |
| } | |
| // Toggle facing mode | |
| const newFacingMode = facingMode === 'user' ? 'environment' : 'user'; | |
| setFacingMode(newFacingMode); | |
| try { | |
| // Get new camera stream with updated facing mode | |
| const stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: newFacingMode, | |
| width: { ideal: 1920 }, | |
| height: { ideal: 1080 } | |
| }, | |
| audio: false | |
| }); | |
| if (!isMounted.current) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| return; | |
| } | |
| videoRef.current.srcObject = stream; | |
| try { | |
| await videoRef.current.play(); | |
| } catch (playError) { | |
| console.log("Play interrupted during camera switch:", playError); | |
| // Don't treat play interruptions as fatal errors | |
| if (playError.name !== "AbortError") { | |
| throw playError; | |
| } | |
| } | |
| if (isMounted.current) { | |
| setCameraError(false); | |
| console.log(`Camera switched to ${newFacingMode === 'user' ? 'front' : 'back'} camera`); | |
| } | |
| } catch (error) { | |
| console.error('Error switching camera:', error); | |
| if (isMounted.current) { | |
| setCameraError(true); | |
| } | |
| } finally { | |
| isSettingUpCamera.current = false; | |
| } | |
| }; | |
| return ( | |
| <> | |
| <video | |
| ref={videoRef} | |
| className="hidden" | |
| width="1280" | |
| height="720" | |
| autoPlay | |
| playsInline | |
| muted | |
| /> | |
| {/* Camera switch button - only shown on mobile */} | |
| {isMobile && ( | |
| <button | |
| onClick={switchCamera} | |
| className="absolute top-4 right-4 bg-white bg-opacity-70 p-2 rounded-full shadow-md z-10 hover:bg-opacity-90 transition-all flex items-center justify-center" | |
| aria-label={`Switch to ${facingMode === 'user' ? 'back' : 'front'} camera`} | |
| style={{ width: '40px', height: '40px' }} | |
| > | |
| <span className="material-symbols-outlined text-gray-800" style={{ display: 'flex', lineHeight: 1 }}> | |
| cameraswitch | |
| </span> | |
| </button> | |
| )} | |
| </> | |
| ); | |
| }; | |
| export default CameraSetup; |