handspew / components /CameraSetup.js
Tina Tarighian
mobile ratio
5f82b57
raw
history blame
6.22 kB
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;