Fix: Resolve camera display issues in selfie component
- Consolidate stream attachment into single useEffect - Remove race conditions from multiple competing effects - Add proper cleanup with video element reset - Simplify state management using single stream state - Add isMountedRef to prevent state updates after unmount - Improve error handling with specific error messages - Add canvas flip correction for front camera mirror effect
This commit is contained in:
@@ -26,155 +26,231 @@ interface RobustCameraProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose, open }) => {
|
export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose, open }) => {
|
||||||
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
||||||
const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user');
|
const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [cameraReady, setCameraReady] = useState(false);
|
const [cameraReady, setCameraReady] = useState(false);
|
||||||
|
|
||||||
// Use multiple refs for different purposes
|
// Single source of truth - only use state for stream, not ref
|
||||||
|
const [stream, setStream] = useState<MediaStream | null>(null);
|
||||||
|
|
||||||
|
// DOM refs only
|
||||||
const videoElementRef = useRef<HTMLVideoElement>(null);
|
const videoElementRef = useRef<HTMLVideoElement>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
|
||||||
const cameraInitRef = useRef<boolean>(false);
|
// Track initialization to prevent double-init
|
||||||
const retryCountRef = useRef<number>(0);
|
const isInitializingRef = useRef(false);
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
// Clean up stream
|
// Cleanup function - stops all tracks and clears video
|
||||||
const cleanupStream = useCallback(() => {
|
const cleanupCamera = useCallback(() => {
|
||||||
console.log('[RobustCamera] Cleaning up stream');
|
console.log('[RobustCamera] Cleaning up camera');
|
||||||
if (streamRef.current) {
|
|
||||||
streamRef.current.getTracks().forEach(track => track.stop());
|
// Stop video playback
|
||||||
streamRef.current = null;
|
if (videoElementRef.current) {
|
||||||
}
|
videoElementRef.current.pause();
|
||||||
if (videoElementRef.current && videoElementRef.current.srcObject) {
|
|
||||||
videoElementRef.current.srcObject = null;
|
videoElementRef.current.srcObject = null;
|
||||||
|
videoElementRef.current.load(); // Reset video element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop all tracks in the stream
|
||||||
|
if (stream) {
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
console.log('[RobustCamera] Stopping track:', track.kind, track.label);
|
||||||
|
track.stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear state
|
||||||
setStream(null);
|
setStream(null);
|
||||||
setCameraReady(false);
|
setCameraReady(false);
|
||||||
cameraInitRef.current = false;
|
setError(null);
|
||||||
retryCountRef.current = 0;
|
isInitializingRef.current = false;
|
||||||
}, []);
|
}, [stream]);
|
||||||
|
|
||||||
// Initialize camera
|
// Initialize camera - only gets the stream, doesn't attach to video
|
||||||
const initializeCamera = useCallback(async () => {
|
const initializeCamera = useCallback(async () => {
|
||||||
if (cameraInitRef.current || loading) {
|
// Prevent double initialization
|
||||||
console.log('[RobustCamera] Camera already initializing or loading');
|
if (isInitializingRef.current) {
|
||||||
|
console.log('[RobustCamera] Already initializing, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
console.log('[RobustCamera] Component not mounted, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[RobustCamera] Starting camera initialization');
|
console.log('[RobustCamera] Starting camera initialization');
|
||||||
cameraInitRef.current = true;
|
isInitializingRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
cleanupStream();
|
setCameraReady(false);
|
||||||
|
|
||||||
|
// Clean up any existing stream first
|
||||||
|
if (stream) {
|
||||||
|
console.log('[RobustCamera] Cleaning up existing stream first');
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const constraints = {
|
const constraints = {
|
||||||
video: {
|
video: {
|
||||||
facingMode: facingMode,
|
facingMode: facingMode,
|
||||||
width: { ideal: 1280 },
|
width: { ideal: 1280, min: 640 },
|
||||||
height: { ideal: 720 },
|
height: { ideal: 720, min: 480 },
|
||||||
},
|
},
|
||||||
audio: false,
|
audio: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[RobustCamera] Requesting camera with constraints:', constraints);
|
console.log('[RobustCamera] Requesting camera with constraints:', constraints);
|
||||||
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
|
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
console.log('[RobustCamera] Camera stream obtained successfully');
|
|
||||||
|
|
||||||
streamRef.current = mediaStream;
|
if (!isMountedRef.current) {
|
||||||
setStream(mediaStream);
|
// Component unmounted while awaiting, clean up
|
||||||
|
console.log('[RobustCamera] Component unmounted, stopping stream');
|
||||||
// Attach to video element
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
if (videoElementRef.current) {
|
return;
|
||||||
console.log('[RobustCamera] Video element found, attaching stream');
|
|
||||||
videoElementRef.current.srcObject = mediaStream;
|
|
||||||
|
|
||||||
videoElementRef.current.onloadedmetadata = () => {
|
|
||||||
console.log('[RobustCamera] Video metadata loaded');
|
|
||||||
setCameraReady(true);
|
|
||||||
setLoading(false);
|
|
||||||
videoElementRef.current?.play().catch(err => {
|
|
||||||
console.error('[RobustCamera] Video play error:', err);
|
|
||||||
setError('Camera stream obtained but video display failed. Please try again.');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
videoElementRef.current.onerror = (err) => {
|
|
||||||
console.error('[RobustCamera] Video error:', err);
|
|
||||||
setError('Failed to display camera feed.');
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log('[RobustCamera] Video element not found, will attach when ready');
|
|
||||||
setCameraReady(false);
|
|
||||||
setLoading(false);
|
|
||||||
// Stream will be attached when video element mounts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[RobustCamera] Camera stream obtained:', mediaStream.id, 'Tracks:', mediaStream.getTracks().length);
|
||||||
|
setStream(mediaStream);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[RobustCamera] Camera access error:', err);
|
console.error('[RobustCamera] Camera access error:', err);
|
||||||
cleanupStream();
|
|
||||||
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
isInitializingRef.current = false;
|
||||||
|
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
if (err.name === 'NotAllowedError') {
|
if (err.name === 'NotAllowedError') {
|
||||||
setError('Camera access denied. Please allow camera permissions to take a selfie.');
|
setError('Camera access denied. Please allow camera permissions in your browser settings.');
|
||||||
} else if (err.name === 'NotFoundError') {
|
} else if (err.name === 'NotFoundError') {
|
||||||
setError('No camera found on this device.');
|
setError('No camera found on this device.');
|
||||||
} else if (err.name === 'NotReadableError') {
|
} else if (err.name === 'NotReadableError') {
|
||||||
setError('Camera is already in use by another application.');
|
setError('Camera is already in use by another application. Please close other apps using the camera.');
|
||||||
|
} else if (err.name === 'OverconstrainedError') {
|
||||||
|
setError('Camera does not support the requested resolution. Please try again.');
|
||||||
} else {
|
} else {
|
||||||
setError('Failed to access camera. Please try again.');
|
setError(`Failed to access camera: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setError('Failed to access camera. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [facingMode, loading, cleanupStream]);
|
}, [facingMode, stream]);
|
||||||
|
|
||||||
// Attach stream when video element is available
|
// SINGLE useEffect to handle stream attachment to video
|
||||||
const attachStreamToVideo = useCallback(() => {
|
// This runs whenever stream changes or video element becomes available
|
||||||
if (videoElementRef.current && streamRef.current && !cameraReady) {
|
useEffect(() => {
|
||||||
console.log('[RobustCamera] Attaching stream to video element');
|
const video = videoElementRef.current;
|
||||||
const video = videoElementRef.current;
|
|
||||||
const stream = streamRef.current;
|
if (!video || !stream) {
|
||||||
|
console.log('[RobustCamera] Cannot attach - video:', !!video, 'stream:', !!stream);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.srcObject === stream) {
|
||||||
|
console.log('[RobustCamera] Stream already attached to video');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RobustCamera] Attaching stream to video element');
|
||||||
|
|
||||||
|
// Set up event handlers before attaching
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
console.log('[RobustCamera] Video metadata loaded, playing...');
|
||||||
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
// Clear any existing stream
|
video.play()
|
||||||
if (video.srcObject) {
|
.then(() => {
|
||||||
const oldStream = video.srcObject as MediaStream;
|
console.log('[RobustCamera] Video playing successfully');
|
||||||
oldStream.getTracks().forEach(track => track.stop());
|
if (isMountedRef.current) {
|
||||||
}
|
setCameraReady(true);
|
||||||
|
}
|
||||||
// Attach new stream
|
})
|
||||||
video.srcObject = stream;
|
.catch(err => {
|
||||||
|
console.error('[RobustCamera] Video play error:', err);
|
||||||
video.onloadedmetadata = () => {
|
if (isMountedRef.current) {
|
||||||
console.log('[RobustCamera] Video metadata loaded after attachment');
|
setError('Camera stream ready but video playback failed. Please try again.');
|
||||||
setCameraReady(true);
|
}
|
||||||
setLoading(false);
|
|
||||||
video.play().catch(err => {
|
|
||||||
console.error('[RobustCamera] Video play error after attachment:', err);
|
|
||||||
setError('Camera stream obtained but video display failed. Please try again.');
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleError = (err: Event) => {
|
||||||
|
console.error('[RobustCamera] Video error:', err);
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setError('Failed to display camera feed. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach event listeners
|
||||||
|
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
video.addEventListener('error', handleError);
|
||||||
|
|
||||||
|
// Attach the stream
|
||||||
|
video.srcObject = stream;
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
console.log('[RobustCamera] Cleaning up video event listeners');
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
video.removeEventListener('error', handleError);
|
||||||
|
};
|
||||||
|
}, [stream]);
|
||||||
|
|
||||||
video.onerror = (err) => {
|
// Initialize camera when dialog opens
|
||||||
console.error('[RobustCamera] Video error after attachment:', err);
|
useEffect(() => {
|
||||||
setError('Failed to display camera feed.');
|
if (open) {
|
||||||
setLoading(false);
|
console.log('[RobustCamera] Dialog opened');
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Small delay to ensure DOM is ready
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
initializeCamera();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.log('[RobustCamera] Cannot attach stream:', {
|
// Dialog closed - cleanup
|
||||||
videoExists: !!videoElementRef.current,
|
console.log('[RobustCamera] Dialog closed, cleaning up');
|
||||||
streamExists: !!streamRef.current,
|
cleanupCamera();
|
||||||
cameraReady
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [cameraReady]);
|
}, [open, initializeCamera, cleanupCamera]);
|
||||||
|
|
||||||
|
// Handle facing mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !stream) return;
|
||||||
|
|
||||||
|
console.log('[RobustCamera] Facing mode changed, reinitializing');
|
||||||
|
cleanupCamera();
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
isInitializingRef.current = false;
|
||||||
|
initializeCamera();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [facingMode, open]); // Only re-run when facingMode actually changes
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
console.log('[RobustCamera] Component unmounting');
|
||||||
|
isMountedRef.current = false;
|
||||||
|
cleanupCamera();
|
||||||
|
};
|
||||||
|
}, [cleanupCamera]);
|
||||||
|
|
||||||
// Capture photo
|
// Capture photo
|
||||||
const capturePhoto = useCallback(() => {
|
const capturePhoto = useCallback(() => {
|
||||||
if (!videoElementRef.current || !canvasRef.current || !cameraReady) {
|
if (!videoElementRef.current || !canvasRef.current || !cameraReady) {
|
||||||
console.log('[RobustCamera] Cannot capture: video or canvas not ready');
|
console.log('[RobustCamera] Cannot capture: not ready');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,21 +258,30 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
// Set canvas dimensions to match video
|
// Set canvas dimensions to match video
|
||||||
canvas.width = video.videoWidth;
|
canvas.width = video.videoWidth || 1280;
|
||||||
canvas.height = video.videoHeight;
|
canvas.height = video.videoHeight || 720;
|
||||||
|
|
||||||
|
console.log('[RobustCamera] Capturing photo:', canvas.width, 'x', canvas.height);
|
||||||
|
|
||||||
// Draw video frame to canvas
|
// Draw video frame to canvas
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
if (context) {
|
if (context) {
|
||||||
|
// Flip context if using front camera for mirror effect correction
|
||||||
|
if (facingMode === 'user') {
|
||||||
|
context.translate(canvas.width, 0);
|
||||||
|
context.scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Convert to data URL
|
// Convert to data URL
|
||||||
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.9);
|
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.9);
|
||||||
console.log('[RobustCamera] Photo captured successfully');
|
console.log('[RobustCamera] Photo captured');
|
||||||
|
|
||||||
onCapture(imageDataUrl);
|
onCapture(imageDataUrl);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [cameraReady, onCapture, onClose]);
|
}, [cameraReady, facingMode, onCapture, onClose]);
|
||||||
|
|
||||||
// Flip camera
|
// Flip camera
|
||||||
const flipCamera = useCallback(() => {
|
const flipCamera = useCallback(() => {
|
||||||
@@ -204,51 +289,6 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
setFacingMode(prev => prev === 'user' ? 'environment' : 'user');
|
setFacingMode(prev => prev === 'user' ? 'environment' : 'user');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initialize camera when dialog opens
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
console.log('[RobustCamera] Dialog opened, initializing camera');
|
|
||||||
// Small delay to ensure DOM is ready
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
initializeCamera();
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
cleanupStream();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [open]); // Remove initializeCamera and cleanupStream from dependencies
|
|
||||||
|
|
||||||
// Re-initialize when facing mode changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (open && cameraReady) {
|
|
||||||
console.log('[RobustCamera] Facing mode changed, re-initializing camera');
|
|
||||||
cleanupStream();
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
initializeCamera();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [facingMode]); // Remove other dependencies to prevent loops
|
|
||||||
|
|
||||||
// Attach stream when video element is available
|
|
||||||
useEffect(() => {
|
|
||||||
if (videoElementRef.current && streamRef.current && !cameraReady) {
|
|
||||||
console.log('[RobustCamera] Video element available, attaching stream');
|
|
||||||
attachStreamToVideo();
|
|
||||||
}
|
|
||||||
}, [stream, cameraReady]); // Trigger when stream changes
|
|
||||||
|
|
||||||
// Attach stream when component mounts or stream changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (open && stream && !cameraReady && videoElementRef.current) {
|
|
||||||
console.log('[RobustCamera] Stream available, attaching to video element');
|
|
||||||
attachStreamToVideo();
|
|
||||||
}
|
|
||||||
}, [open, stream]); // Remove cameraReady and attachStreamToVideo to prevent loops
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -301,71 +341,84 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && (
|
<Box sx={{
|
||||||
<Box sx={{
|
position: 'relative',
|
||||||
position: 'relative',
|
width: '100%',
|
||||||
width: '100%',
|
maxWidth: 600,
|
||||||
maxWidth: 600,
|
mx: 'auto',
|
||||||
mx: 'auto',
|
bgcolor: 'black',
|
||||||
bgcolor: 'black',
|
borderRadius: 2,
|
||||||
borderRadius: 2,
|
overflow: 'hidden',
|
||||||
overflow: 'hidden'
|
minHeight: 300,
|
||||||
}}>
|
display: 'flex',
|
||||||
<video
|
alignItems: 'center',
|
||||||
ref={videoElementRef}
|
justifyContent: 'center'
|
||||||
autoPlay
|
}}>
|
||||||
playsInline
|
{/* Video element - always rendered but styled based on readiness */}
|
||||||
muted
|
<video
|
||||||
style={{
|
ref={videoElementRef}
|
||||||
width: '100%',
|
autoPlay
|
||||||
height: 'auto',
|
playsInline
|
||||||
display: cameraReady ? 'block' : 'none',
|
muted
|
||||||
transform: facingMode === 'user' ? 'scaleX(-1)' : 'none'
|
disablePictureInPicture
|
||||||
}}
|
controls={false}
|
||||||
/>
|
style={{
|
||||||
|
width: '100%',
|
||||||
{!cameraReady && stream && (
|
height: 'auto',
|
||||||
<Box sx={{
|
maxHeight: '60vh',
|
||||||
position: 'absolute',
|
objectFit: 'contain',
|
||||||
top: 0,
|
display: cameraReady ? 'block' : 'none',
|
||||||
left: 0,
|
transform: facingMode === 'user' ? 'scaleX(-1)' : 'none'
|
||||||
right: 0,
|
}}
|
||||||
bottom: 0,
|
/>
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
{/* Loading overlay - shown when stream exists but camera not ready */}
|
||||||
justifyContent: 'center',
|
{!cameraReady && stream && (
|
||||||
bgcolor: 'rgba(0,0,0,0.8)',
|
<Box sx={{
|
||||||
color: 'white'
|
position: 'absolute',
|
||||||
}}>
|
top: 0,
|
||||||
<Typography variant="body1">
|
left: 0,
|
||||||
Camera stream ready, attaching to display...
|
right: 0,
|
||||||
</Typography>
|
bottom: 0,
|
||||||
</Box>
|
display: 'flex',
|
||||||
)}
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'rgba(0,0,0,0.9)',
|
||||||
|
color: 'white',
|
||||||
|
gap: 2,
|
||||||
|
zIndex: 1
|
||||||
|
}}>
|
||||||
|
<CircularProgress size={40} sx={{ color: 'white' }} />
|
||||||
|
<Typography variant="body1">
|
||||||
|
Starting camera feed...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{!cameraReady && !stream && !loading && (
|
{/* Initial state - no stream yet */}
|
||||||
<Box sx={{
|
{!cameraReady && !stream && !loading && !error && (
|
||||||
position: 'absolute',
|
<Box sx={{
|
||||||
top: 0,
|
position: 'absolute',
|
||||||
left: 0,
|
top: 0,
|
||||||
right: 0,
|
left: 0,
|
||||||
bottom: 0,
|
right: 0,
|
||||||
display: 'flex',
|
bottom: 0,
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
bgcolor: 'rgba(0,0,0,0.8)',
|
justifyContent: 'center',
|
||||||
color: 'white',
|
bgcolor: 'rgba(0,0,0,0.8)',
|
||||||
gap: 2
|
color: 'white',
|
||||||
}}>
|
gap: 2
|
||||||
<Camera sx={{ fontSize: 60 }} />
|
}}>
|
||||||
<Typography variant="body1" textAlign="center">
|
<Camera sx={{ fontSize: 60 }} />
|
||||||
Camera not ready
|
<Typography variant="body1" textAlign="center">
|
||||||
</Typography>
|
Camera initializing...
|
||||||
</Box>
|
</Typography>
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
)}
|
</Box>
|
||||||
|
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user