| 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'); |
| 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> |
| ); |
| } |
|
|