| import { useCallback, useEffect, useRef, useState } from 'react' |
|
|
| export type CameraStatus = 'idle' | 'requesting' | 'live' | 'denied' | 'nocamera' | 'inuse' | 'error' |
|
|
| |
| |
| |
| |
| export function useCamera() { |
| const videoRef = useRef<HTMLVideoElement | null>(null) |
| const streamRef = useRef<MediaStream | null>(null) |
| const [status, setStatus] = useState<CameraStatus>('idle') |
| const [devices, setDevices] = useState<MediaDeviceInfo[]>([]) |
| const [activeDeviceId, setActiveDeviceId] = useState<string | null>(null) |
|
|
| const stop = useCallback(() => { |
| const s = streamRef.current |
| if (s) { |
| s.getTracks().forEach((t) => t.stop()) |
| 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 { } |
| } |
| 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 { } |
| } 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]) |
|
|
| |
| |
| |
| useEffect(() => { |
| const v = videoRef.current |
| if (status === 'live' && v && streamRef.current && v.srcObject !== streamRef.current) { |
| v.srcObject = streamRef.current |
| void v.play().catch(() => { }) |
| } |
| }, [status]) |
|
|
| const switchTo = useCallback((deviceId: string) => start(deviceId), [start]) |
|
|
| |
| const capture = useCallback((maxW = 640, quality = 0.85): string | null => { |
| const v = videoRef.current |
| if (!v || !v.videoWidth) return null |
| const scale = Math.min(1, maxW / v.videoWidth) |
| const canvas = document.createElement('canvas') |
| canvas.width = Math.round(v.videoWidth * scale) |
| canvas.height = Math.round(v.videoHeight * scale) |
| const ctx = canvas.getContext('2d') |
| if (!ctx) return null |
| ctx.drawImage(v, 0, 0, canvas.width, canvas.height) |
| return canvas.toDataURL('image/jpeg', quality) |
| }, []) |
|
|
| useEffect(() => stop, [stop]) |
|
|
| return { videoRef, status, devices, activeDeviceId, start, stop, switchTo, capture } |
| } |
|
|