import { useCallback, useEffect, useRef, useState } from 'react' export type CameraStatus = 'idle' | 'requesting' | 'live' | 'denied' | 'nocamera' | 'inuse' | 'error' /** Camera controller: requests the camera only on `start()`, prefers the front * (user-facing) camera, supports device switching, captures a still JPEG, and — * critically — STOPS ALL TRACKS on stop()/unmount so the camera light never * stays on in the background. */ export function useCamera() { const videoRef = useRef(null) const streamRef = useRef(null) const [status, setStatus] = useState('idle') const [devices, setDevices] = useState([]) const [activeDeviceId, setActiveDeviceId] = useState(null) const stop = useCallback(() => { const s = streamRef.current if (s) { s.getTracks().forEach((t) => t.stop()) // release camera + mic tracks streamRef.current = null } if (videoRef.current) videoRef.current.srcObject = null setStatus('idle') }, []) const start = useCallback(async (deviceId?: string) => { const md = navigator.mediaDevices if (!md || !md.getUserMedia) { setStatus('nocamera'); return } stop() setStatus('requesting') const constraints: MediaStreamConstraints = { video: deviceId ? { deviceId: { exact: deviceId } } : { facingMode: 'user' }, audio: false, } try { const stream = await md.getUserMedia(constraints) streamRef.current = stream if (videoRef.current) { videoRef.current.srcObject = stream try { await videoRef.current.play() } catch { /* autoplay guarded */ } } const id = stream.getVideoTracks()[0]?.getSettings?.().deviceId ?? deviceId ?? null setActiveDeviceId(id) setStatus('live') try { const list = await md.enumerateDevices() setDevices(list.filter((d) => d.kind === 'videoinput')) } catch { /* labels need permission; ignore */ } } catch (e) { const name = (e as Error)?.name if (name === 'NotAllowedError' || name === 'SecurityError') setStatus('denied') else if (name === 'NotFoundError' || name === 'OverconstrainedError') setStatus('nocamera') else if (name === 'NotReadableError' || name === 'AbortError') setStatus('inuse') else setStatus('error') } }, [stop]) // Attach the stream once the