Spaces:
Running
Running
| import { useEffect, useRef, useState } from 'react'; | |
| import { Camera, CameraOff, RefreshCcw, CheckCircle2 } from 'lucide-react'; | |
| import styles from './CameraCapture.module.css'; | |
| export default function CameraCapture({ onCapture }) { | |
| const videoRef = useRef(null); | |
| const canvasRef = useRef(null); | |
| const streamRef = useRef(null); | |
| const [status, setStatus] = useState('idle'); // idle | requesting | ready | error | |
| const [error, setError] = useState(''); | |
| useEffect(() => { | |
| return () => { | |
| if (streamRef.current) { | |
| streamRef.current.getTracks().forEach((track) => track.stop()); | |
| } | |
| }; | |
| }, []); | |
| useEffect(() => { | |
| const video = videoRef.current; | |
| const stream = streamRef.current; | |
| if (!video || !stream) return; | |
| let cancelled = false; | |
| const attachStream = async () => { | |
| try { | |
| video.srcObject = stream; | |
| await video.play(); | |
| } catch (cameraError) { | |
| if (!cancelled) { | |
| setStatus('error'); | |
| setError(cameraError?.message || 'Unable to render camera preview'); | |
| } | |
| } | |
| }; | |
| attachStream(); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [status]); | |
| const startCamera = async () => { | |
| setStatus('requesting'); | |
| setError(''); | |
| try { | |
| if (streamRef.current) { | |
| streamRef.current.getTracks().forEach((track) => track.stop()); | |
| } | |
| const stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: 'user', | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 }, | |
| }, | |
| audio: false, | |
| }); | |
| streamRef.current = stream; | |
| setStatus('ready'); | |
| } catch (cameraError) { | |
| setStatus('error'); | |
| setError(cameraError?.message || 'Camera access failed'); | |
| } | |
| }; | |
| const stopCamera = () => { | |
| if (streamRef.current) { | |
| streamRef.current.getTracks().forEach((track) => track.stop()); | |
| streamRef.current = null; | |
| } | |
| if (videoRef.current) { | |
| videoRef.current.srcObject = null; | |
| } | |
| setStatus('idle'); | |
| }; | |
| const capturePhoto = async () => { | |
| if (!videoRef.current || !canvasRef.current) return; | |
| const video = videoRef.current; | |
| const canvas = canvasRef.current; | |
| canvas.width = video.videoWidth || 1280; | |
| canvas.height = video.videoHeight || 720; | |
| const context = canvas.getContext('2d'); | |
| if (!context) return; | |
| context.drawImage(video, 0, 0, canvas.width, canvas.height); | |
| const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png', 0.95)); | |
| if (!blob) return; | |
| const file = new File([blob], `uaide-capture-${Date.now()}.png`, { type: 'image/png' }); | |
| onCapture(file); | |
| }; | |
| return ( | |
| <div className={styles.card}> | |
| <div className={styles.header}> | |
| <div className={styles.titleWrap}> | |
| <Camera size={16} /> | |
| <span>Real-Time Capture</span> | |
| </div> | |
| {status === 'ready' && ( | |
| <button type="button" className={styles.secondaryBtn} onClick={stopCamera}> | |
| <CameraOff size={14} /> | |
| <span>Stop</span> | |
| </button> | |
| )} | |
| </div> | |
| <div className={styles.viewport}> | |
| <video | |
| ref={videoRef} | |
| playsInline | |
| muted | |
| autoPlay | |
| className={`${styles.video} ${status === 'ready' ? styles.videoVisible : styles.videoHidden}`} | |
| /> | |
| {status !== 'ready' && ( | |
| <div className={styles.placeholder}> | |
| <Camera size={26} /> | |
| <p>Capture a live image for immediate forensic analysis.</p> | |
| </div> | |
| )} | |
| <canvas ref={canvasRef} className={styles.canvas} /> | |
| </div> | |
| <div className={styles.actions}> | |
| {status === 'idle' && ( | |
| <button type="button" className={styles.primaryBtn} onClick={startCamera}> | |
| <Camera size={15} /> | |
| <span>Open Camera</span> | |
| </button> | |
| )} | |
| {status === 'requesting' && ( | |
| <button type="button" className={styles.primaryBtn} disabled> | |
| <RefreshCcw size={15} className={styles.spin} /> | |
| <span>Requesting Access…</span> | |
| </button> | |
| )} | |
| {status === 'ready' && ( | |
| <button type="button" className={styles.primaryBtn} onClick={capturePhoto}> | |
| <CheckCircle2 size={15} /> | |
| <span>Capture & Analyse</span> | |
| </button> | |
| )} | |
| {status === 'error' && ( | |
| <> | |
| <p className={styles.error}>{error}</p> | |
| <button type="button" className={styles.primaryBtn} onClick={startCamera}> | |
| <RefreshCcw size={15} /> | |
| <span>Retry Camera</span> | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |