mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-22 06:28:10 +02:00
feat(reels): add video stabilization, quality improvements and processing monitoring
This commit is contained in:
1 parent
4e20a84836
commit
aac776777a
7 files changed
+289
-11
No files matched your search
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface CameraRecorderProps {
|
||||
onCapture: (file: File) => void;
|
||||
onCapture: (file: File, options?: { stabilize?: boolean }) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@ interface QualityConfig {
|
||||
label: string;
|
||||
width: number;
|
||||
height: number;
|
||||
bitrate: number; // in bps
|
||||
}
|
||||
|
||||
const QUALITY_CONFIGS: Record<VideoQuality, QualityConfig> = {
|
||||
"4k": { label: "4K Ultra HD", width: 3840, height: 2160 },
|
||||
"1080p": { label: "Full HD", width: 1920, height: 1080 },
|
||||
"720p": { label: "HD 720p", width: 1280, height: 720 },
|
||||
"4k": { label: "4K Ultra HD", width: 3840, height: 2160, bitrate: 50000000 },
|
||||
"1080p": { label: "Full HD", width: 1920, height: 1080, bitrate: 25000000 },
|
||||
"720p": { label: "HD 720p", width: 1280, height: 720, bitrate: 12000000 },
|
||||
};
|
||||
|
||||
export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
|
||||
@@ -43,6 +44,7 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
|
||||
const [quality, setQuality] = useState<VideoQuality>('1080p');
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [cameraError, setCameraError] = useState<string | null>(null);
|
||||
const [stabilize, setStabilize] = useState(true); // Stabilisation serveur activée par défaut
|
||||
|
||||
// Fetch devices
|
||||
const handleDevices = useCallback((mediaDevices: MediaDeviceInfo[]) => {
|
||||
@@ -86,11 +88,12 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
|
||||
|
||||
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: 8000000
|
||||
videoBitsPerSecond: currentConfig.bitrate // Bitrate dynamique selon qualité
|
||||
});
|
||||
|
||||
mediaRecorder.ondataavailable = (e) => {
|
||||
@@ -121,7 +124,10 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (recordedBlob) {
|
||||
onCapture(new File([recordedBlob], `capture-${Date.now()}.mp4`, { type: recordedBlob.type }));
|
||||
onCapture(
|
||||
new File([recordedBlob], `capture-${Date.now()}.mp4`, { type: recordedBlob.type }),
|
||||
{ stabilize }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,6 +163,7 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
|
||||
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
|
||||
};
|
||||
@@ -246,6 +253,18 @@ export function CameraRecorder({ onCapture, onCancel }: CameraRecorderProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-white/70 flex items-center justify-between">
|
||||
<span>Stabilisation serveur</span>
|
||||
<button
|
||||
onClick={() => setStabilize(!stabilize)}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${stabilize ? 'bg-green-500' : 'bg-gray-600'}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${stabilize ? 'left-7' : 'left-1'}`} />
|
||||
</button>
|
||||
</label>
|
||||
<p className="text-xs text-white/50 mt-1">Corrige les tremblements après capture (traitement serveur)</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-6 bg-white text-black hover:bg-gray-200" onClick={() => setShowSettings(false)}>OK</Button>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,7 @@ export default function NewReel() {
|
||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||
const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural");
|
||||
const [drawText, setDrawText] = useState(true);
|
||||
const [shouldStabilize, setShouldStabilize] = useState(false);
|
||||
|
||||
// État audio preview
|
||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||
@@ -316,6 +317,7 @@ export default function NewReel() {
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
drawText,
|
||||
stabilize: shouldStabilize,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -335,7 +337,16 @@ export default function NewReel() {
|
||||
{/* Camera Recorder Modal */}
|
||||
{showCamera && (
|
||||
<CameraRecorder
|
||||
onCapture={(file) => {
|
||||
onCapture={(file, options) => {
|
||||
if (options?.stabilize) {
|
||||
setShouldStabilize(true);
|
||||
toast({
|
||||
title: "Stabilisation activée",
|
||||
description: "La vidéo sera stabilisée lors de la création du Reel (ceci peut prendre plus de temps)",
|
||||
});
|
||||
} else {
|
||||
setShouldStabilize(false);
|
||||
}
|
||||
uploadMutation.mutate(file);
|
||||
setShowCamera(false);
|
||||
}}
|
||||
@@ -989,5 +1000,115 @@ export default function NewReel() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/* Overlay de progression */ }
|
||||
<ProcessingOverlay
|
||||
isVisible={createReelMutation.isPending}
|
||||
stabilize={shouldStabilize}
|
||||
ttsEnabled={ttsEnabled}
|
||||
/>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
function ProcessingOverlay({ isVisible, stabilize, ttsEnabled }: { isVisible: boolean; stabilize: boolean; ttsEnabled: boolean }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState("Initialisation...");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(0);
|
||||
setStatus("Préparation des fichiers...");
|
||||
|
||||
const timeouts: NodeJS.Timeout[] = [];
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
// Sequence de simulation
|
||||
// 1. 2s: Téléchargement
|
||||
timeouts.push(setTimeout(() => {
|
||||
setStatus("Téléchargement des médias...");
|
||||
setProgress(10);
|
||||
}, 1500));
|
||||
|
||||
// 2. 4s: Audio/TTS
|
||||
timeouts.push(setTimeout(() => {
|
||||
setStatus(ttsEnabled ? "Génération de la voix IA..." : "Mixage audio...");
|
||||
setProgress(25);
|
||||
}, 3500));
|
||||
|
||||
// 3. 7s: Stabilisation ou Encodage
|
||||
timeouts.push(setTimeout(() => {
|
||||
if (stabilize) {
|
||||
setStatus("Stabilisation vidéo (Traitement long)...");
|
||||
setProgress(35);
|
||||
|
||||
// Progression lente : +1% toutes les 800ms
|
||||
// Ça permet de couvrir ~45 secondes avant d'arriver à 90%
|
||||
let p = 35;
|
||||
interval = setInterval(() => {
|
||||
if (p < 90) {
|
||||
p++;
|
||||
setProgress(p);
|
||||
}
|
||||
}, 800);
|
||||
} else {
|
||||
setStatus("Encodage optimisé...");
|
||||
setProgress(40);
|
||||
|
||||
// Progression pour encodage standard (~10-15s)
|
||||
let p = 40;
|
||||
interval = setInterval(() => {
|
||||
if (p < 90) {
|
||||
p += 2;
|
||||
setProgress(p);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}, 6500));
|
||||
|
||||
// Nettoyage
|
||||
return () => {
|
||||
timeouts.forEach(clearTimeout);
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [isVisible, stabilize, ttsEnabled]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] bg-black/90 flex flex-col items-center justify-center p-4 backdrop-blur-md animate-in fade-in duration-300">
|
||||
<div className="w-full max-w-md space-y-8 text-center">
|
||||
<div className="relative w-24 h-24 mx-auto mb-8">
|
||||
<Loader2 className="w-full h-full text-primary animate-spin" />
|
||||
<div className="absolute inset-0 flex items-center justify-center font-bold text-xl">
|
||||
{progress}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-bold text-white tracking-tight">Création de votre Reel</h3>
|
||||
<p className="text-lg text-muted-foreground animate-pulse">
|
||||
{status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-2 bg-secondary/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{stabilize && (
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-yellow-500/80 bg-yellow-500/10 py-2 px-4 rounded-full mx-auto w-fit">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>Stabilisation activée : traitement prolongé</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user