/** * @ai-summary Hook for managing camera permission and media stream * @ai-context Handles getUserMedia API, permission state, and camera switching */ import { useState, useCallback, useRef, useEffect } from 'react'; import type { PermissionState, FacingMode } from './types'; interface UseCameraPermissionOptions { /** Initial facing mode preference */ initialFacingMode?: FacingMode; } interface UseCameraPermissionReturn { /** Current permission state */ permissionState: PermissionState; /** Active media stream */ stream: MediaStream | null; /** Error message if any */ error: string | null; /** Current camera facing mode */ facingMode: FacingMode; /** Whether the device has multiple cameras */ hasMultipleCameras: boolean; /** Request camera access */ requestPermission: () => Promise; /** Switch between front and rear camera */ switchCamera: () => Promise; /** Stop the media stream */ stopStream: () => void; } /** * Hook for managing camera permissions and media streams */ export function useCameraPermission( options: UseCameraPermissionOptions = {} ): UseCameraPermissionReturn { const { initialFacingMode = 'environment' } = options; const [permissionState, setPermissionState] = useState('prompt'); const [stream, setStream] = useState(null); const [error, setError] = useState(null); const [facingMode, setFacingMode] = useState(initialFacingMode); const [hasMultipleCameras, setHasMultipleCameras] = useState(false); const streamRef = useRef(null); // Check for multiple cameras on mount useEffect(() => { const checkCameras = async () => { try { if (!navigator.mediaDevices?.enumerateDevices) { return; } const devices = await navigator.mediaDevices.enumerateDevices(); const videoInputs = devices.filter((d) => d.kind === 'videoinput'); setHasMultipleCameras(videoInputs.length > 1); } catch { // Silently fail - camera enumeration is not critical } }; checkCameras(); }, []); // Clean up stream on unmount useEffect(() => { return () => { if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); } }; }, []); const stopStream = useCallback(() => { if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; setStream(null); } }, []); const requestPermission = useCallback(async () => { // Check if getUserMedia is available if (!navigator.mediaDevices?.getUserMedia) { setPermissionState('unavailable'); setError('Camera access is not supported in this browser'); return; } // Stop any existing stream stopStream(); setError(null); try { const constraints: MediaStreamConstraints = { video: { facingMode: { ideal: facingMode }, width: { ideal: 1920 }, height: { ideal: 1080 }, }, audio: false, }; const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); streamRef.current = mediaStream; setStream(mediaStream); setPermissionState('granted'); // Re-check for multiple cameras after permission granted const devices = await navigator.mediaDevices.enumerateDevices(); const videoInputs = devices.filter((d) => d.kind === 'videoinput'); setHasMultipleCameras(videoInputs.length > 1); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; if ( err instanceof DOMException && (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') ) { setPermissionState('denied'); setError('Camera permission was denied. Please enable camera access in your browser settings.'); } else if ( err instanceof DOMException && (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') ) { setPermissionState('unavailable'); setError('No camera found on this device'); } else if ( err instanceof DOMException && (err.name === 'NotReadableError' || err.name === 'TrackStartError') ) { setPermissionState('unavailable'); setError('Camera is already in use by another application'); } else { setPermissionState('unavailable'); setError(`Failed to access camera: ${errorMessage}`); } } }, [facingMode, stopStream]); const switchCamera = useCallback(async () => { const newFacingMode: FacingMode = facingMode === 'environment' ? 'user' : 'environment'; setFacingMode(newFacingMode); // If we have an active stream, restart with new facing mode if (stream) { stopStream(); try { const constraints: MediaStreamConstraints = { video: { facingMode: { exact: newFacingMode }, width: { ideal: 1920 }, height: { ideal: 1080 }, }, audio: false, }; const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); streamRef.current = mediaStream; setStream(mediaStream); } catch { // If exact fails, try ideal try { const constraints: MediaStreamConstraints = { video: { facingMode: { ideal: newFacingMode }, width: { ideal: 1920 }, height: { ideal: 1080 }, }, audio: false, }; const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); streamRef.current = mediaStream; setStream(mediaStream); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; setError(`Failed to switch camera: ${errorMessage}`); // Revert facing mode setFacingMode(facingMode); } } } }, [facingMode, stream, stopStream]); return { permissionState, stream, error, facingMode, hasMultipleCameras, requestPermission, switchCamera, stopStream, }; } export default useCameraPermission;