diff --git a/client/src/pages/mobile/remotion-video.tsx b/client/src/pages/mobile/remotion-video.tsx index c450b4a..9c43165 100644 --- a/client/src/pages/mobile/remotion-video.tsx +++ b/client/src/pages/mobile/remotion-video.tsx @@ -147,13 +147,43 @@ export default function MobileRemotionVideoPage() { const response = await fetch("/api/remotion/render", { method: "POST", body: formData }); if (!response.ok) { const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); } const data = await response.json(); - setVideoUrl(data.url); - // Pre-fill description with overlay text so it appears in the Facebook post - if (overlayText && !publishDescription) setPublishDescription(overlayText); - toast({ title: "Vidéo générée !" }); + + if (data.jobId) { + toast({ title: "Rendu démarré", description: "Veuillez patienter (~1-2 min)..." }); + + const checkStatus = async () => { + try { + const res = await fetch(`/api/remotion/render/status/${data.jobId}`); + if (!res.ok) throw new Error("Erreur de suivi du rendu"); + const job = await res.json(); + + if (job.status === 'done') { + setVideoUrl(job.url); + if (overlayText && !publishDescription) setPublishDescription(overlayText); + toast({ title: "Vidéo générée !" }); + setIsRendering(false); + } else if (job.status === 'error') { + toast({ title: "Erreur de rendu", description: job.error || "Échec du rendu", variant: "destructive" }); + setIsRendering(false); + } else { + setTimeout(checkStatus, 3000); + } + } catch (e: any) { + toast({ title: "Erreur de connexion", description: e.message, variant: "destructive" }); + setIsRendering(false); + } + }; + setTimeout(checkStatus, 3000); + } else { + setVideoUrl(data.url); + if (overlayText && !publishDescription) setPublishDescription(overlayText); + toast({ title: "Vidéo générée !" }); + setIsRendering(false); + } } catch (err: any) { toast({ title: "Erreur", description: err.message, variant: "destructive" }); - } finally { setIsRendering(false); } + setIsRendering(false); + } }; const handlePublish = async () => { diff --git a/client/src/pages/remotion-video.tsx b/client/src/pages/remotion-video.tsx index 46504c9..4a93596 100644 --- a/client/src/pages/remotion-video.tsx +++ b/client/src/pages/remotion-video.tsx @@ -114,13 +114,45 @@ export default function RemotionVideoPage() { const response = await fetch("/api/remotion/render", { method: "POST", body: formData }); if (!response.ok) { const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); } const data = await response.json(); - setVideoUrl(data.url); - // Pre-fill description with overlay text so it appears in the Facebook post - if (overlayText && !publishDescription) setPublishDescription(overlayText); - toast({ title: "Vidéo générée !", description: "Votre vidéo est prête à publier." }); + + if (data.jobId) { + toast({ title: "Rendu démarré", description: "Veuillez patienter (~1-2 min)..." }); + + const checkStatus = async () => { + try { + const res = await fetch(`/api/remotion/render/status/${data.jobId}`); + if (!res.ok) throw new Error("Erreur de suivi du rendu"); + const job = await res.json(); + + if (job.status === 'done') { + setVideoUrl(job.url); + if (overlayText && !publishDescription) setPublishDescription(overlayText); + toast({ title: "Vidéo générée !", description: "Votre vidéo est prête à publier." }); + setIsRendering(false); + } else if (job.status === 'error') { + toast({ title: "Erreur de rendu", description: job.error || "Échec du rendu", variant: "destructive" }); + setIsRendering(false); + } else { + setTimeout(checkStatus, 3000); + } + } catch (e: any) { + toast({ title: "Erreur de connexion", description: e.message, variant: "destructive" }); + setIsRendering(false); + } + }; + setTimeout(checkStatus, 3000); + } else { + // Fallback for older sync backend + setVideoUrl(data.url); + if (overlayText && !publishDescription) setPublishDescription(overlayText); + toast({ title: "Vidéo générée !", description: "Votre vidéo est prête à publier." }); + setIsRendering(false); + } } catch (err: any) { toast({ title: "Erreur", description: err.message || "Impossible de créer la vidéo.", variant: "destructive" }); - } finally { setIsRendering(false); } + setIsRendering(false); + } + // Note: removed finally block so isRendering stays true during polling }; const handlePublish = async () => { diff --git a/server/routes/remotion.ts b/server/routes/remotion.ts index 4a1496f..75042fe 100644 --- a/server/routes/remotion.ts +++ b/server/routes/remotion.ts @@ -33,6 +33,20 @@ async function generateVideoThumbnail(videoPath: string, outputPath: string, see export const remotionRouter = Router(); +interface RenderJob { + status: 'processing' | 'done' | 'error'; + url?: string; + thumbnailUrl?: string | null; + error?: string; +} +const renderJobs = new Map(); + +remotionRouter.get("/render/status/:jobId", (req, res) => { + const job = renderJobs.get(req.params.jobId); + if (!job) return res.status(404).json({ error: "Job introuvable" }); + res.json(job); +}); + const uploadDir = path.join(process.cwd(), "uploads", "temp"); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); @@ -294,45 +308,58 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, { inputProps, }); - const outputFilename = `out-${Date.now()}.mp4`; - const outputLocation = path.join(uploadDir, outputFilename); + const jobId = Date.now().toString(); + renderJobs.set(jobId, { status: 'processing' }); + res.json({ jobId, message: "Rendu démarré en arrière-plan" }); - console.log("🎬 Rendering media...", totalFrames, "frames"); + // Run async to prevent 504 Gateway Timeout + (async () => { + try { + const outputFilename = `out-${Date.now()}.mp4`; + const outputLocation = path.join(uploadDir, outputFilename); - await renderMedia({ - composition: { ...composition, durationInFrames: totalFrames }, - serveUrl: bundleLocation, - codec: "h264", - outputLocation, - inputProps, - chromiumOptions: { - disableWebSecurity: true, - ignoreCertificateErrors: true, - args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"], - }, - concurrency: 1, // Limit concurrency to prevent Docker memory exhaustion - }); + console.log("🎬 Rendering media...", totalFrames, "frames (Job:", jobId, ")"); - console.log("✅ Render completed:", outputFilename); + await renderMedia({ + composition: { ...composition, durationInFrames: totalFrames }, + serveUrl: bundleLocation, + codec: "h264", + outputLocation, + inputProps, + chromiumOptions: { + disableWebSecurity: true, + ignoreCertificateErrors: true, + args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"], + }, + concurrency: 1, // Limit concurrency to prevent Docker memory exhaustion + }); - // Generate thumbnail for the video - const thumbnailFilename = outputFilename.replace('.mp4', '-thumb.jpg'); - const thumbnailPath = path.join(uploadDir, thumbnailFilename); - const thumbnailGenerated = await generateVideoThumbnail(outputLocation, thumbnailPath, 2); + console.log("✅ Render completed:", outputFilename); - if (thumbnailGenerated) { - console.log("🖼️ Thumbnail generated:", thumbnailFilename); - } + // Generate thumbnail for the video + const thumbnailFilename = outputFilename.replace('.mp4', '-thumb.jpg'); + const thumbnailPath = path.join(uploadDir, thumbnailFilename); + const thumbnailGenerated = await generateVideoThumbnail(outputLocation, thumbnailPath, 2); - // Return relative URL so any client (mobile, desktop, Docker) can access it - res.json({ - url: `/uploads/temp/${outputFilename}`, - thumbnailUrl: thumbnailGenerated ? `/uploads/temp/${thumbnailFilename}` : null, - }); + if (thumbnailGenerated) { + console.log("🖼️ Thumbnail generated:", thumbnailFilename); + } + + renderJobs.set(jobId, { + status: 'done', + url: `/uploads/temp/${outputFilename}`, + thumbnailUrl: thumbnailGenerated ? `/uploads/temp/${thumbnailFilename}` : null, + }); + + } catch (err: any) { + console.error("❌ Remotion render error for Job", jobId, ":", err); + renderJobs.set(jobId, { status: 'error', error: "Erreur lors du rendu: " + err.message }); + } + })(); } catch (err: any) { - console.error("❌ Remotion render error:", err); - res.status(500).json({ error: "Erreur lors du rendu: " + err.message }); + console.error("❌ Remotion pre-render error:", err); + res.status(500).json({ error: "Erreur lors du lancement du rendu: " + err.message }); } }); diff --git a/task.md b/task.md index 563f27e..ef2d704 100644 --- a/task.md +++ b/task.md @@ -6,7 +6,7 @@ Ajout d'une fonctionnalité complète de création de Reels Facebook permettant ## Current Focus -**Phase: EXECUTION** - Fix Remotion Render Target Closed error. +**Phase: EXECUTION** - Fix 504 Gateway Timeout on Remotion Render (Async Polling). ## Master Plan @@ -269,3 +269,11 @@ Ajout d'une fonctionnalité complète de création de Reels Facebook permettant - [x] Ajouter les arguments `--disable-dev-shm-usage`, `--no-sandbox` aux options de Chromium. - [x] Limiter la concurrence `concurrency: 1` dans `renderMedia` pour économiser la mémoire en Docker. +### Phase 26: Fix 504 Gateway Timeout (Async Remotion Render) 🚀 + +- [x] Backend: Modifier `POST /api/remotion/render` pour retourner un `jobId` immédiatement. +- [x] Backend: Créer la route `GET /api/remotion/render/status/:jobId` pour suivre l'état (in-memory map). +- [x] Frontend: Modifier `remotion-video.tsx` pour faire du polling sur le `jobId` après la soumission. +- [x] Frontend: Appliquer la même modification de polling à `mobile/remotion-video.tsx`. + +