import { useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Play, Pause, Square, Download, Video, Clock } from "lucide-react"; import { toast } from "sonner"; import { Slider } from "@/components/ui/slider"; interface TimelapseFrame { timestamp: number; canvasData: string; } interface TimelapseRecorderProps { onRequestSnapshot: () => string | null; onLoadSnapshot: (data: string) => void; } export const TimelapseRecorder = ({ onRequestSnapshot, onLoadSnapshot }: TimelapseRecorderProps) => { const [isRecording, setIsRecording] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [frames, setFrames] = useState([]); const [currentFrame, setCurrentFrame] = useState(0); const [recordingDuration, setRecordingDuration] = useState(0); const recordingIntervalRef = useRef(null); const playbackIntervalRef = useRef(null); const recordingStartTimeRef = useRef(0); const mediaRecorderRef = useRef(null); const recordedChunksRef = useRef([]); const startRecording = useCallback(() => { setIsRecording(true); setFrames([]); setCurrentFrame(0); recordingStartTimeRef.current = Date.now(); // Capture frames every 500ms recordingIntervalRef.current = setInterval(() => { const snapshot = onRequestSnapshot(); if (snapshot) { const timestamp = Date.now() - recordingStartTimeRef.current; setFrames(prev => [...prev, { timestamp, canvasData: snapshot }]); setRecordingDuration(timestamp); } }, 500); toast.success("Enregistrement démarré"); }, [onRequestSnapshot]); const stopRecording = useCallback(() => { if (recordingIntervalRef.current) { clearInterval(recordingIntervalRef.current); recordingIntervalRef.current = null; } setIsRecording(false); toast.success(`Enregistrement terminé: ${frames.length} frames capturées`); }, [frames.length]); const playTimelapse = useCallback(() => { if (frames.length === 0) { toast.error("Aucune séquence enregistrée"); return; } setIsPlaying(true); setCurrentFrame(0); let frameIndex = 0; playbackIntervalRef.current = setInterval(() => { if (frameIndex >= frames.length) { setIsPlaying(false); if (playbackIntervalRef.current) { clearInterval(playbackIntervalRef.current); } toast.success("Lecture terminée"); return; } onLoadSnapshot(frames[frameIndex].canvasData); setCurrentFrame(frameIndex); frameIndex++; }, 500); }, [frames, onLoadSnapshot]); const pauseTimelapse = useCallback(() => { if (playbackIntervalRef.current) { clearInterval(playbackIntervalRef.current); playbackIntervalRef.current = null; } setIsPlaying(false); }, []); const seekToFrame = useCallback((frameIndex: number) => { if (frameIndex >= 0 && frameIndex < frames.length) { setCurrentFrame(frameIndex); onLoadSnapshot(frames[frameIndex].canvasData); } }, [frames, onLoadSnapshot]); const exportAsVideo = useCallback(async () => { if (frames.length === 0) { toast.error("Aucune séquence à exporter"); return; } try { toast.loading("Préparation de l'export vidéo..."); // Create a temporary canvas for video export const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600; const ctx = canvas.getContext('2d'); if (!ctx) { toast.error("Erreur lors de la création du canvas"); return; } // Setup MediaRecorder const stream = canvas.captureStream(30); const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9', videoBitsPerSecond: 2500000 }); recordedChunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunksRef.current.push(event.data); } }; mediaRecorder.onstop = () => { const blob = new Blob(recordedChunksRef.current, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `experience_timelapse_${Date.now()}.webm`; a.click(); URL.revokeObjectURL(url); toast.success("Vidéo exportée avec succès"); }; mediaRecorder.start(); // Render each frame for (let i = 0; i < frames.length; i++) { const img = new Image(); await new Promise((resolve) => { img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); resolve(null); }; img.src = frames[i].canvasData; }); // Wait to maintain frame rate await new Promise(resolve => setTimeout(resolve, 500)); } mediaRecorder.stop(); } catch (error) { console.error("Export error:", error); toast.error("Erreur lors de l'export vidéo"); } }, [frames]); const formatDuration = (ms: number) => { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; }; return (
{/* Recording Controls */}
{!isRecording ? ( ) : ( )}
{/* Recording Status */} {isRecording && (
Enregistrement en cours... {formatDuration(recordingDuration)} - {frames.length} frames
)} {/* Playback Controls */} {frames.length > 0 && !isRecording && ( <>
{!isPlaying ? ( ) : ( )}
{/* Timeline Slider */}
seekToFrame(value[0])} disabled={isPlaying} className="w-full" />
Frame {currentFrame + 1} / {frames.length} {formatDuration(frames[currentFrame]?.timestamp || 0)}
)}
); };