// Try loading .env for local development, ignore if missing (production) try { process.loadEnvFile?.(); } catch { } // Node 20+ native support import express from 'express'; import cors from 'cors'; import { bundle } from '@remotion/bundler'; import { renderMedia, selectComposition } from '@remotion/renderer'; import { readFile, rm } from 'fs/promises'; import { tmpdir } from 'os'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import os from 'os'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 7860; app.use(cors()); app.use(express.json({ limit: '50mb' })); // Health check app.get('/', (req, res) => { res.json({ status: 'ok', service: 'FacelessFlowAI Video Renderer (Simple)' }); }); // Render endpoint app.post('/render', async (req, res) => { const { projectId, scenes, settings } = req.body; if (!projectId || !scenes || !settings) { return res.status(400).json({ error: 'Missing projectId, scenes, or settings' }); } // Initialize Supabase Admin Client const SUPABASE_URL = process.env.SUPABASE_URL; const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) { console.error('[Config] Missing Supabase credentials'); return res.status(500).json({ error: 'Renderer configuration error' }); } const { createClient } = await import('@supabase/supabase-js'); const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); // 1. Respond immediately res.json({ success: true, message: 'Rendering started in background' }); // 2. Start Background Process (async () => { try { console.log(`[Render] Starting background render for project ${projectId}`); // Available resources const cpuCount = os.cpus().length; const freeMem = (os.freemem() / 1024 / 1024).toFixed(0); const totalMem = (os.totalmem() / 1024 / 1024).toFixed(0); console.log(`[Resources] vCPUs: ${cpuCount} | RAM: ${freeMem}/${totalMem} MB Free`); const bundleLocation = await bundle({ entryPoint: join(__dirname, 'remotion', 'index.tsx'), webpackOverride: (config) => config, }); // Clean Composition Params const composition = await selectComposition({ serveUrl: bundleLocation, id: 'Main', inputProps: { scenes, settings }, // Direct Cloud URLs chromiumOptions: { executablePath: process.env.CHROME_BIN }, }); const outputLocation = join(tmpdir(), `out-${projectId}.mp4`); let lastLoggedPercent = -1; // Simple Render await renderMedia({ composition, serveUrl: bundleLocation, codec: 'h264', pixelFormat: 'yuv420p', outputLocation: outputLocation, imageFormat: 'jpeg', jpegQuality: 80, inputProps: { scenes, settings }, chromiumOptions: { executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable', enableMultiProcessRendering: true, // Requested: Enable Multi-process args: [ '--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--mute-audio', ] }, // Use all available cores (or set specific number) concurrency: 1, disallowParallelEncoding: false, onProgress: ({ progress }) => { const percent = Math.round(progress * 100); if (percent !== lastLoggedPercent && percent % 5 === 0) { console.log(`[Render] Progress: ${percent}%`); lastLoggedPercent = percent; } }, }); console.log(`[Render] Render complete. Uploading to Supabase...`); const videoBuffer = await readFile(outputLocation); // Upload to Supabase Storage const fileName = `${projectId}/video-${Date.now()}.mp4`; const { data: uploadData, error: uploadError } = await supabase .storage .from('projects') .upload(fileName, videoBuffer, { contentType: 'video/mp4', upsert: true }); if (uploadError) throw new Error(`Upload failed: ${uploadError.message}`); // Get Public URL const { data: { publicUrl } } = supabase .storage .from('projects') .getPublicUrl(fileName); console.log(`[Render] Uploaded to ${publicUrl}`); // Update Project in DB const { error: dbError } = await supabase .from('projects') .update({ status: 'done', video_url: publicUrl }) .eq('id', projectId); if (dbError) throw new Error(`DB Update failed: ${dbError.message}`); console.log(`[Render] Project ${projectId} updated successfully`); // Cleanup await rm(outputLocation, { force: true }); } catch (error) { console.error(`[Render] Background Error for ${projectId}:`, error); await supabase .from('projects') .update({ status: 'error', }) .eq('id', projectId); } })(); }); app.listen(PORT, () => { console.log(`🎬 FacelessFlowAI Renderer running on port ${PORT}`); // Debug: Check for Secrets on Startup console.log('--- Environment Check ---'); console.log('SUPABASE_URL exists:', !!process.env.SUPABASE_URL); console.log('SUPABASE_SERVICE_ROLE_KEY exists:', !!process.env.SUPABASE_SERVICE_ROLE_KEY); });