import React, { useRef, useState, useEffect, useCallback } from 'react'; import Webcam from 'react-webcam'; import { Camera, StopCircle, RefreshCw, CheckCircle, X, RotateCcw, Settings2, AlertCircle, Video } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useToast } from '@/hooks/use-toast'; interface CameraRecorderProps { onCapture: (file: File, options?: { stabilize?: boolean }) => void; onCancel: () => void; } // Quality configurations type VideoQuality = "4k" | "1080p" | "720p"; interface QualityConfig { label: string; width: number; height: number; bitrate: number; // in bps } const QUALITY_CONFIGS: Record = { "4k": { label: "4K Vertical", width: 2160, height: 3840, bitrate: 40000000 }, "1080p": { label: "Full HD (Reel)", width: 1080, height: 1920, bitrate: 12000000 }, // Align with 12Mbps max spec "720p": { label: "HD Vertical", width: 720, height: 1280, bitrate: 8000000 }, }; export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) { const webcamRef = useRef(null); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const { toast } = useToast(); const [isRecording, setIsRecording] = useState(false); const [recordedBlob, setRecordedBlob] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [timer, setTimer] = useState(0); // Devices & Settings const [devices, setDevices] = useState([]); const [selectedDeviceId, setSelectedDeviceId] = useState(''); const [facingMode, setFacingMode] = useState<"user" | "environment">("environment"); const [quality, setQuality] = useState('1080p'); const [showSettings, setShowSettings] = useState(false); const [cameraError, setCameraError] = useState(null); const [stabilize, setStabilize] = useState(true); // Stabilisation serveur activée par défaut // Fetch devices const handleDevices = useCallback((mediaDevices: MediaDeviceInfo[]) => { const videoDevices = mediaDevices.filter(({ kind }) => kind === "videoinput"); setDevices(videoDevices); if (videoDevices.length > 0 && !selectedDeviceId) { setSelectedDeviceId(videoDevices[0].deviceId); } }, [selectedDeviceId]); useEffect(() => { navigator.mediaDevices.enumerateDevices().then(handleDevices).catch(err => { console.error("Error enumerating devices", err); setCameraError("Erreur liste périphériques: " + err.message); }); }, [handleDevices]); // Timer useEffect(() => { let interval: NodeJS.Timeout; if (isRecording) { interval = setInterval(() => setTimer(t => t + 1), 1000); } else { setTimer(0); } return () => clearInterval(interval); }, [isRecording]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; // Recording Logic const startRecording = useCallback(() => { if (!webcamRef.current || !webcamRef.current.stream) return; chunksRef.current = []; const stream = webcamRef.current.stream; const mimeTypes = ['video/mp4', 'video/webm;codecs=vp9,opus', 'video/webm']; const mimeType = mimeTypes.find(type => MediaRecorder.isTypeSupported(type)) || ''; const currentConfig = QUALITY_CONFIGS[quality]; try { const mediaRecorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: currentConfig.bitrate // Bitrate dynamique selon qualité }); mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; mediaRecorder.onstop = () => { const blob = new Blob(chunksRef.current, { type: mimeType || 'video/webm' }); setRecordedBlob(blob); setPreviewUrl(URL.createObjectURL(blob)); }; mediaRecorder.start(); mediaRecorderRef.current = mediaRecorder; setIsRecording(true); } catch (e: any) { console.error("Recording error", e); toast({ title: "Erreur", description: "Impossible de lancer l'enregistrement: " + e.message, variant: "destructive" }); } }, [webcamRef, toast]); const stopRecording = useCallback(() => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); } }, [isRecording]); const handleConfirm = () => { if (recordedBlob) { onCapture( new File([recordedBlob], `capture-${Date.now()}.mp4`, { type: recordedBlob.type }), { stabilize } ); } }; const handleUserMediaError = useCallback((error: string | DOMException) => { console.error("Webcam Error:", error); const msg = typeof error === 'string' ? error : error.message; setCameraError(msg); }, []); const toggleCamera = () => { setFacingMode(prev => prev === "user" ? "environment" : "user"); setSelectedDeviceId(''); }; // Review Mode if (recordedBlob && previewUrl) { return (
); } const config = QUALITY_CONFIGS[quality]; const videoConstraints = { width: { ideal: config.width }, height: { ideal: config.height }, frameRate: { ideal: 30 }, // 30fps pour une capture fluide deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined, facingMode: selectedDeviceId ? undefined : facingMode }; return (
{/* Header */}
{isRecording ? (
{formatTime(timer)}
) : (
)}
{/* Error Overlay */} {cameraError && (

Erreur Caméra

{cameraError}

Vérifiez vos permissions et que vous êtes bien en HTTPS (ou localhost).

)} {/* Settings Overlay */} {showSettings && !isRecording && (

Paramètres

{devices.map((device, idx) => ( ))}
{(Object.keys(QUALITY_CONFIGS) as VideoQuality[]).map((q) => ( ))}

Corrige les tremblements après capture (traitement serveur)

)} {/* Webcam Feed */}
{/* Footer Controls */}
); } export default CameraRecorder;