diff --git a/client/src/components/camera-recorder.tsx b/client/src/components/camera-recorder.tsx new file mode 100644 index 0000000..145251d --- /dev/null +++ b/client/src/components/camera-recorder.tsx @@ -0,0 +1,268 @@ + +import React, { useRef, useState, useEffect, useCallback } from 'react'; +import { Camera, StopCircle, RefreshCw, CheckCircle, X, RotateCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { useToast } from '@/hooks/use-toast'; + +interface CameraRecorderProps { + onCapture: (file: File) => void; + onCancel: () => void; +} + +export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) { + const videoRef = useRef(null); + const mediaRecorderRef = useRef(null); + const chunksRef = useRef([]); + const { toast } = useToast(); + + const [stream, setStream] = useState(null); + const [isRecording, setIsRecording] = useState(false); + const [recordedBlob, setRecordedBlob] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [timer, setTimer] = useState(0); + const [facingMode, setFacingMode] = useState<'user' | 'environment'>('environment'); + const [resolutionInfo, setResolutionInfo] = useState(''); + + // Initialisation de la caméra + const startCamera = useCallback(async () => { + try { + if (stream) { + stream.getTracks().forEach(track => track.stop()); + } + + console.log('📸 Starting camera with facing mode:', facingMode); + + // Constraints pour forcer la meilleure qualité possible (4K ou 1080p) + const constraints: MediaStreamConstraints = { + audio: true, + video: { + facingMode: facingMode, + width: { ideal: 3840 }, // Try 4K + height: { ideal: 2160 }, + } + }; + + const newStream = await navigator.mediaDevices.getUserMedia(constraints); + setStream(newStream); + + if (videoRef.current) { + videoRef.current.srcObject = newStream; + videoRef.current.volume = 0; // Mute preview to avoid feedback loop + } + + // Check actual resolution + const videoTrack = newStream.getVideoTracks()[0]; + if (videoTrack) { + const settings = videoTrack.getSettings(); + const resText = `${settings.width}x${settings.height}`; + console.log('✅ Camera started at:', resText); + setResolutionInfo(resText); + } + + } catch (error) { + console.error('❌ Error accessing camera:', error); + toast({ + title: "Erreur caméra", + description: "Impossible d'accéder à la caméra. Vérifiez les permissions.", + variant: "destructive" + }); + onCancel(); + } + }, [facingMode, onCancel, toast]); + + useEffect(() => { + startCamera(); + return () => { + // Cleanup on unmount + if (stream) { + stream.getTracks().forEach(track => track.stop()); + } + }; + }, [startCamera]); + + // Timer logic + useEffect(() => { + let interval: NodeJS.Timeout; + if (isRecording) { + interval = setInterval(() => { + setTimer(prev => prev + 1); + }, 1000); + } else { + setTimer(0); + } + return () => clearInterval(interval); + }, [isRecording]); + + const startRecording = () => { + if (!stream) return; + + chunksRef.current = []; + + // Essayer les codecs supportés pour la meilleure qualité + // iOS (Safari) supporte bien video/mp4 ou video/webkit;codecs=h264 + let mimeType = 'video/mp4'; + if (!MediaRecorder.isTypeSupported(mimeType)) { + mimeType = 'video/webm;codecs=vp9'; + if (!MediaRecorder.isTypeSupported(mimeType)) { + mimeType = 'video/webm'; + } + } + + console.log('🎥 Recording with mimeType:', mimeType); + + try { + const mediaRecorder = new MediaRecorder(stream, { + mimeType, + videoBitsPerSecond: 8000000 // 8 Mbps target + }); + + mediaRecorder.ondataavailable = (e) => { + if (e.data.size > 0) { + chunksRef.current.push(e.data); + } + }; + + mediaRecorder.onstop = () => { + const blob = new Blob(chunksRef.current, { type: mimeType }); + setRecordedBlob(blob); + const url = URL.createObjectURL(blob); + setPreviewUrl(url); + + // Stop stream tracks to save battery while reviewing + stream.getTracks().forEach(track => track.stop()); + }; + + mediaRecorder.start(1000); // Collect chunks every second + mediaRecorderRef.current = mediaRecorder; + setIsRecording(true); + } catch (e) { + console.error('Failed to start recording:', e); + toast({ + title: "Erreur enregistrement", + description: "Format vidéo non supporté par ce navigateur.", + variant: "destructive" + }); + } + }; + + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + } + }; + + const switchCamera = () => { + setFacingMode(prev => prev === 'user' ? 'environment' : 'user'); + }; + + const handleRetake = () => { + setRecordedBlob(null); + setPreviewUrl(null); + startCamera(); // Restart stream + }; + + const handleConfirm = () => { + if (recordedBlob) { + // Create a File object + const file = new File([recordedBlob], `capture-${Date.now()}.mp4`, { + type: recordedBlob.type + }); + onCapture(file); + } + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + // Rendu : Mode Revue + if (recordedBlob && previewUrl) { + return ( +
+
+ ); + } + + // Rendu : Mode Capture + return ( +
+ {/* Header */} +
+ +
+ {isRecording ? formatTime(timer) : (resolutionInfo || 'Camera')} +
+ +
+ + {/* Viewfinder */} +
+
+ + {/* Controls */} +
+ {isRecording ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index 108be03..47b27b3 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -72,6 +72,9 @@ export default function NewReel() { const [isPlaying, setIsPlaying] = useState(null); const [musicOffset, setMusicOffset] = useState(0); + // État caméra custom + const [showCamera, setShowCamera] = useState(false); + // Récupérer les pages disponibles const { data: pages = [] } = useQuery({ queryKey: ['/api/pages'], @@ -420,7 +423,7 @@ export default function NewReel() {