fix: reduce text overlay font size to 10 and add camera diagnostics

This commit is contained in:
Jacques committed 2026-01-22 20:46:20 +01:00
1 parent 07ac80057e
commit 986f9741c1
2 files changed
+114 -169

No files matched your search

+113 -168
View File
@@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect, useCallback } from 'react'; import React, { useRef, useState, useEffect, useCallback } from 'react';
import { Camera, StopCircle, RefreshCw, CheckCircle, X, RotateCcw, Settings2, AlertCircle } from 'lucide-react'; import { Camera, StopCircle, RefreshCw, CheckCircle, X, RotateCcw, Settings2, AlertCircle, Bug } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -46,8 +46,9 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
const [resolutionInfo, setResolutionInfo] = useState<string>(''); const [resolutionInfo, setResolutionInfo] = useState<string>('');
const [quality, setQuality] = useState<VideoQuality>('1080p'); const [quality, setQuality] = useState<VideoQuality>('1080p');
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showDebug, setShowDebug] = useState(false);
// Fetch video devices // Initial Device Fetch
useEffect(() => { useEffect(() => {
const getDevices = async () => { const getDevices = async () => {
try { try {
@@ -57,22 +58,21 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
const allDevices = await navigator.mediaDevices.enumerateDevices(); const allDevices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = allDevices.filter(device => device.kind === 'videoinput'); const videoDevices = allDevices.filter(device => device.kind === 'videoinput');
setDevices(videoDevices); setDevices(videoDevices);
console.log("Devices found:", videoDevices);
if (videoDevices.length > 0 && !selectedDeviceId) { if (videoDevices.length > 0 && !selectedDeviceId) {
setSelectedDeviceId(videoDevices[0].deviceId); setSelectedDeviceId(videoDevices[0].deviceId);
} }
} catch (err) { } catch (err) {
console.error("Error enumerating devices:", err); console.error("Error enumerating devices:", err);
setPermissionError("Accès caméra refusé ou non disponible."); setPermissionError("Accès caméra refusé. Vérifiez les permissions du navigateur.");
} }
}; };
getDevices(); getDevices();
}, []); }, []);
// Initialisation de la caméra // Camera Start Logic
const startCamera = useCallback(async () => { const startCamera = useCallback(async () => {
if (!selectedDeviceId && devices.length > 0) return; // Wait for selection logic
try { try {
setPermissionError(null); setPermissionError(null);
@@ -83,7 +83,7 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
const config = QUALITY_CONFIGS[quality]; const config = QUALITY_CONFIGS[quality];
console.log(`📸 Starting camera: ${config.label}, deviceId: ${selectedDeviceId}`); console.log(`📸 Starting camera: ${config.label}, deviceId: ${selectedDeviceId}`);
// Constraints with specific deviceId if selected, otherwise facingMode fallback // Constraints
const constraints: MediaStreamConstraints = { const constraints: MediaStreamConstraints = {
audio: { audio: {
echoCancellation: true, echoCancellation: true,
@@ -94,7 +94,7 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
width: { ideal: config.width }, width: { ideal: config.width },
height: { ideal: config.height }, height: { ideal: config.height },
} : { } : {
facingMode: 'user', // Default fallback facingMode: 'user',
width: { ideal: config.width }, width: { ideal: config.width },
height: { ideal: config.height }, height: { ideal: config.height },
} }
@@ -104,15 +104,14 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
try { try {
newStream = await navigator.mediaDevices.getUserMedia(constraints); newStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log('✅ Camera obtained');
} catch (err) { } catch (err) {
console.warn('⚠️ Constraints failed, trying minimal fallback:', err); console.warn('⚠️ Standard constraints failed:', err);
// Fallback to minimal constraints (any video source) setPermissionError(`Erreur: ${err instanceof Error ? err.message : String(err)}`);
// Fallback
newStream = await navigator.mediaDevices.getUserMedia({ newStream = await navigator.mediaDevices.getUserMedia({
audio: true, audio: true,
video: true video: true
}); });
console.log('✅ Camera obtained with minimal constraints');
} }
setStream(newStream); setStream(newStream);
@@ -120,37 +119,32 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
if (videoRef.current) { if (videoRef.current) {
videoRef.current.srcObject = newStream; videoRef.current.srcObject = newStream;
videoRef.current.volume = 0; videoRef.current.volume = 0;
// Explicit play try await videoRef.current.play();
try {
await videoRef.current.play();
} catch (playErr) {
console.error("Video play failed:", playErr);
}
} }
// Check actual resolution // Resolution check
const videoTrack = newStream.getVideoTracks()[0]; const videoTrack = newStream.getVideoTracks()[0];
if (videoTrack) { if (videoTrack) {
const settings = videoTrack.getSettings(); const settings = videoTrack.getSettings();
const resText = `${settings.width}x${settings.height}`; setResolutionInfo(`${settings.width}x${settings.height}`);
console.log('✅ Camera started at:', resText);
setResolutionInfo(resText);
} }
} catch (error) { } catch (error) {
console.error('❌ Error accessing camera:', error); console.error('❌ Error accessing camera:', error);
setPermissionError(error instanceof Error ? error.message : "Erreur inconnue"); const msg = error instanceof Error ? error.message : "Erreur inconnue";
setPermissionError(msg);
toast({ toast({
title: "Erreur caméra", title: "Erreur caméra",
description: "Impossible d'accéder à la caméra. Vérifiez les permissions.", description: msg,
variant: "destructive" variant: "destructive"
}); });
} }
}, [selectedDeviceId, quality, toast, devices]); // Removed complex deps }, [selectedDeviceId, quality, toast]);
// Restart camera when device or quality changes // Restart on quality/device change
useEffect(() => { useEffect(() => {
if (selectedDeviceId) { if (selectedDeviceId || devices.length === 0) {
// Attempt start even if no devices found (might work with default)
startCamera(); startCamera();
} }
return () => { return () => {
@@ -174,65 +168,38 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [isRecording]); }, [isRecording]);
// Recording Functions
const startRecording = () => { const startRecording = () => {
if (!stream) return; if (!stream) return;
chunksRef.current = []; chunksRef.current = [];
// Essayer les codecs supportés pour la meilleure qualité // Codec selection
const mimeTypes = [ const mimeTypes = ['video/mp4', 'video/webm;codecs=vp9,opus', 'video/webm'];
'video/mp4', const mimeType = mimeTypes.find(type => MediaRecorder.isTypeSupported(type)) || '';
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm'
];
let mimeType = mimeTypes.find(type => MediaRecorder.isTypeSupported(type)) || '';
if (!mimeType) {
console.warn("No supported mimeType found, letting browser decide default.");
}
console.log('🎥 Recording with mimeType:', mimeType);
try { try {
const options: MediaRecorderOptions = { const mediaRecorder = new MediaRecorder(stream, {
videoBitsPerSecond: 8000000 // 8 Mbps target mimeType,
}; videoBitsPerSecond: 8000000
if (mimeType) { });
options.mimeType = mimeType;
}
const mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = (e) => { mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) { if (e.data.size > 0) chunksRef.current.push(e.data);
chunksRef.current.push(e.data);
}
}; };
mediaRecorder.onstop = () => { mediaRecorder.onstop = () => {
const blob = new Blob(chunksRef.current, { type: mimeType || 'video/webm' }); const blob = new Blob(chunksRef.current, { type: mimeType || 'video/webm' });
setRecordedBlob(blob); setRecordedBlob(blob);
const url = URL.createObjectURL(blob); setPreviewUrl(URL.createObjectURL(blob));
setPreviewUrl(url);
// Stop stream tracks to save battery while reviewing
// stream.getTracks().forEach(track => track.stop()); // Don't stop yet if we want retake to be fast?
// Actually retake restarts camera, so stopping here is fine.
stream.getTracks().forEach(track => track.stop()); stream.getTracks().forEach(track => track.stop());
}; };
mediaRecorder.start(1000); // Collect chunks every second mediaRecorder.start(1000);
mediaRecorderRef.current = mediaRecorder; mediaRecorderRef.current = mediaRecorder;
setIsRecording(true); setIsRecording(true);
} catch (e) { } catch (e) {
console.error('Failed to start recording:', e); console.error('Recording failed:', e);
toast({ toast({ title: "Erreur enregistrement", description: "Format non supporté.", variant: "destructive" });
title: "Erreur enregistrement",
description: "Format vidéo non supporté par ce navigateur.",
variant: "destructive"
});
} }
}; };
@@ -243,19 +210,9 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
} }
}; };
const handleRetake = () => {
setRecordedBlob(null);
setPreviewUrl(null);
// Force restart
startCamera();
};
const handleConfirm = () => { const handleConfirm = () => {
if (recordedBlob) { if (recordedBlob) {
const file = new File([recordedBlob], `capture-${Date.now()}.mp4`, { onCapture(new File([recordedBlob], `capture-${Date.now()}.mp4`, { type: recordedBlob.type }));
type: recordedBlob.type
});
onCapture(file);
} }
}; };
@@ -265,44 +222,27 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
return `${mins}:${secs.toString().padStart(2, '0')}`; return `${mins}:${secs.toString().padStart(2, '0')}`;
}; };
// Rendu : Mode Revue // Render Review Mode
if (recordedBlob && previewUrl) { if (recordedBlob && previewUrl) {
return ( return (
<div className="fixed inset-0 z-50 bg-black flex flex-col items-center justify-center p-4"> <div className="fixed inset-0 z-50 bg-black flex flex-col items-center justify-center p-4">
<video <video src={previewUrl} controls className="w-full max-h-[80vh] rounded-lg mb-8" autoPlay playsInline />
src={previewUrl}
controls
className="w-full max-h-[80vh] rounded-lg mb-8"
autoPlay
playsInline
/>
<div className="flex gap-4"> <div className="flex gap-4">
<Button <Button variant="secondary" size="lg" onClick={() => { setRecordedBlob(null); startCamera(); }} className="rounded-full px-8 py-6 text-lg">
variant="secondary" <RotateCcw className="mr-2 h-5 w-5" /> Refaire
size="lg"
onClick={handleRetake}
className="rounded-full px-8 py-6 text-lg"
>
<RotateCcw className="mr-2 h-5 w-5" />
Refaire
</Button> </Button>
<Button <Button size="lg" onClick={handleConfirm} className="rounded-full px-8 py-6 text-lg bg-green-600 hover:bg-green-700">
size="lg" <CheckCircle className="mr-2 h-5 w-5" /> Valider
onClick={handleConfirm}
className="rounded-full px-8 py-6 text-lg bg-green-600 hover:bg-green-700"
>
<CheckCircle className="mr-2 h-5 w-5" />
Valider
</Button> </Button>
</div> </div>
</div> </div>
); );
} }
// Rendu : Mode Capture // Render Capture Mode
return ( return (
<div className="fixed inset-0 z-50 bg-black flex flex-col"> <div className="fixed inset-0 z-50 bg-black flex flex-col">
{/* Header */} {/* Header / Top Bar */}
<div className="absolute top-0 left-0 right-0 p-4 z-10 flex justify-between items-center bg-gradient-to-b from-black/80 to-transparent pointer-events-none"> <div className="absolute top-0 left-0 right-0 p-4 z-10 flex justify-between items-center bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
<div className="pointer-events-auto"> <div className="pointer-events-auto">
<Button variant="ghost" size="icon" onClick={onCancel} className="text-white"> <Button variant="ghost" size="icon" onClick={onCancel} className="text-white">
@@ -311,7 +251,11 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
</div> </div>
<div className="flex items-center gap-2 pointer-events-auto"> <div className="flex items-center gap-2 pointer-events-auto">
{/* Recording timer or Settings Trigger */} {/* Debug Toggle */}
<button onClick={() => setShowDebug(!showDebug)} className="bg-red-900/50 p-2 rounded text-xs text-white">
<Bug className="h-4 w-4" />
</button>
{isRecording ? ( {isRecording ? (
<div className="bg-red-600 px-3 py-1 rounded-full text-white font-mono text-sm flex items-center gap-2"> <div className="bg-red-600 px-3 py-1 rounded-full text-white font-mono text-sm flex items-center gap-2">
<div className="w-2 h-2 bg-white rounded-full animate-pulse" /> <div className="w-2 h-2 bg-white rounded-full animate-pulse" />
@@ -327,119 +271,120 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
</button> </button>
)} )}
</div> </div>
<div className="w-10"></div>
<div className="w-10"></div> {/* Spacer for alignment */}
</div> </div>
{/* Error Message */} {/* DEBUG OVERLAY */}
{permissionError && ( {showDebug && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/90 text-white p-6 text-center"> <div className="absolute top-16 left-4 z-40 bg-black/80 text-green-400 p-4 rounded text-xs font-mono whitespace-pre-wrap max-w-sm pointer-events-auto overflow-auto max-h-[50vh]">
<h3 className="font-bold border-b border-green-500 mb-2">Diagnostic Caméra</h3>
<p>Secure Context: {window.isSecureContext ? "YES (HTTPS/Local)" : "NO (HTTP) - CAMERA BLOCKED"}</p>
<p>Err Permission: {permissionError || "Aucune"}</p>
<p>Resolution: {resolutionInfo}</p>
<p>Selected Device: {selectedDeviceId || "Auto"}</p>
<p>Stream Active: {stream ? "Oui" : "Non"}</p>
<hr className="border-green-800 my-2" />
<strong>Devices ({devices.length}):</strong>
{devices.map((d, i) => (
<div key={i} className="mb-1">
[{d.kind}] {d.label || "Sans étiquette"} ({d.deviceId.slice(0, 8)}...)
</div>
))}
</div>
)}
{/* Critical Error Display */}
{!window.isSecureContext && (
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center bg-black/90 p-6 text-center text-white">
<AlertCircle className="w-16 h-16 text-red-500 mb-4" />
<h2 className="text-xl font-bold text-red-400">Connexion non sécurisée (HTTP)</h2>
<p className="mt-2 text-gray-300">Les navigateurs bloquent l'accès caméra sur les sites non sécurisés (sauf localhost).</p>
<p className="mt-2 text-sm text-gray-400">Veuillez passer en HTTPS ou utiliser localhost.</p>
</div>
)}
{/* Regular Error Display */}
{permissionError && window.isSecureContext && (
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center bg-black/90 p-6 text-center text-white">
<AlertCircle className="w-12 h-12 text-red-500 mb-4" /> <AlertCircle className="w-12 h-12 text-red-500 mb-4" />
<h3 className="text-xl font-bold mb-2">Erreur Caméra</h3> <h3 className="text-xl font-bold">Erreur : {permissionError}</h3>
<p className="text-white/70 mb-6">{permissionError}</p> <Button onClick={startCamera} variant="outline" className="mt-4 text-black">
<Button onClick={onCancel} variant="outline" className="text-black"> Réessayer
Fermer
</Button> </Button>
</div> </div>
)} )}
{/* Settings Panel Overlay */} {/* Settings Modal */}
{showSettings && !isRecording && ( {showSettings && !isRecording && (
<div className="absolute inset-0 z-20 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"> <div className="absolute inset-0 z-20 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-[#1c1c1e] text-white p-6 rounded-2xl w-full max-w-sm border border-white/10 shadow-2xl"> <div className="bg-[#1c1c1e] text-white p-6 rounded-2xl w-full max-w-sm border border-white/10 shadow-2xl">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-lg font-bold">Paramètres Caméra</h2> <h2 className="text-lg font-bold">Paramètres</h2>
<Button variant="ghost" size="icon" onClick={() => setShowSettings(false)} className="h-8 w-8 text-white/50 hover:text-white"> <Button variant="ghost" size="icon" onClick={() => setShowSettings(false)}>
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</Button> </Button>
</div> </div>
<div className="space-y-4">
<div className="space-y-6"> <div>
<div className="space-y-2"> <label className="text-sm font-medium text-white/70">Source Vidéo</label>
<label className="text-sm font-medium text-white/70">Caméra</label>
<Select value={selectedDeviceId} onValueChange={setSelectedDeviceId}> <Select value={selectedDeviceId} onValueChange={setSelectedDeviceId}>
<SelectTrigger className="w-full bg-white/5 border-white/10 text-white"> <SelectTrigger className="w-full bg-white/5 border-white/10 text-white mt-1">
<SelectValue placeholder="Sélectionner une caméra" /> <SelectValue placeholder="Choisir caméra" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-[#2c2c2e] border-white/10 text-white"> <SelectContent className="bg-[#2c2c2e] border-white/10 text-white">
{devices.map((device) => ( {devices.map((device) => (
<SelectItem key={device.deviceId} value={device.deviceId} className="focus:bg-white/10 cursor-pointer"> <SelectItem key={device.deviceId} value={device.deviceId}>
{device.label || `Caméra ${device.deviceId.slice(0, 5)}...`} {device.label || `Caméra ${device.deviceId.slice(0, 5)}...`}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70">Qualité</label> <label className="text-sm font-medium text-white/70">Qualité</label>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2 mt-1">
{(Object.keys(QUALITY_CONFIGS) as VideoQuality[]).map((q) => ( {(Object.keys(QUALITY_CONFIGS) as VideoQuality[]).map((q) => (
<button <button
key={q} key={q}
onClick={() => setQuality(q)} onClick={() => setQuality(q)}
className={`px-2 py-2 rounded-lg text-sm font-medium transition-all border ${quality === q className={`px-2 py-2 rounded text-sm border ${quality === q ? 'bg-white text-black' : 'bg-transparent text-white border-white/20'}`}
? 'bg-white text-black border-white'
: 'bg-white/5 text-white border-transparent hover:bg-white/10'
}`}
> >
{q.toUpperCase()} {q.toUpperCase()}
</button> </button>
))} ))}
</div> </div>
<p className="text-xs text-white/40 text-center pt-1">
{QUALITY_CONFIGS[quality].label} - {resolutionInfo}
</p>
</div> </div>
<Button onClick={() => setShowSettings(false)} className="w-full bg-white text-black hover:bg-white/90">
Terminer
</Button>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Viewfinder */} {/* Video Feed */}
<div className="relative flex-1 bg-black flex items-center justify-center overflow-hidden"> <div className="relative flex-1 bg-black flex items-center justify-center overflow-hidden">
{!permissionError && ( <video
<video ref={videoRef}
ref={videoRef} autoPlay
autoPlay playsInline
playsInline muted
muted className="absolute w-full h-full object-cover"
className="absolute w-full h-full object-cover" />
/>
)}
</div> </div>
{/* Controls */} {/* Controls Footer */}
<div className="absolute bottom-0 left-0 right-0 p-8 pb-12 flex flex-col items-center justify-end bg-gradient-to-t from-black/80 to-transparent h-48 pointer-events-none"> <div className="absolute bottom-0 left-0 right-0 p-8 pb-12 flex flex-col items-center justify-end bg-gradient-to-t from-black/80 to-transparent h-48 pointer-events-none">
<div className="pointer-events-auto"> <div className="pointer-events-auto">
{isRecording ? ( <button
<button onClick={isRecording ? stopRecording : startRecording}
onClick={stopRecording} className="group relative flex items-center justify-center transition-all hover:scale-110 active:scale-95"
className="group relative flex items-center justify-center transition-all hover:scale-110 active:scale-95" >
> <div className={`w-20 h-20 rounded-full border-[6px] border-white flex items-center justify-center p-1 ${isRecording ? 'opacity-100' : ''}`}>
<div className="w-20 h-20 rounded-full border-[4px] border-white flex items-center justify-center"> <div className={`transition-all duration-300 ${isRecording ? 'w-8 h-8 rounded-md bg-red-500' : 'w-full h-full rounded-full bg-red-500'}`} />
<div className="w-8 h-8 rounded-md bg-red-500" /> </div>
</div> </button>
</button>
) : (
<button
onClick={startRecording}
className="group relative flex items-center justify-center transition-all hover:scale-110 active:scale-95"
>
<div className="w-20 h-20 rounded-full border-[4px] border-white flex items-center justify-center p-1">
<div className="w-full h-full rounded-full bg-red-500" />
</div>
</button>
)}
</div> </div>
</div> </div>
</div> </div>
); );
} }
// Ensure TypeScript treats this as a module
export default CameraRecorder; export default CameraRecorder;
+1 -1
View File
@@ -30,7 +30,7 @@ class ReelRequest(BaseModel):
music_id: Optional[str] = None music_id: Optional[str] = None
music_url: Optional[str] = None music_url: Optional[str] = None
word_duration: float = 0.6 word_duration: float = 0.6
font_size: int = 30 font_size: int = 10
music_volume: float = 0.25 music_volume: float = 0.25
tts_enabled: bool = False tts_enabled: bool = False
tts_voice: str = "fr-FR-VivienneMultilingualNeural" tts_voice: str = "fr-FR-VivienneMultilingualNeural"