| | |
| | try { process.loadEnvFile?.(); } catch { } |
| | 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' })); |
| |
|
| | |
| | app.get('/', (req, res) => { |
| | res.json({ status: 'ok', service: 'FacelessFlowAI Video Renderer (Simple)' }); |
| | }); |
| |
|
| | |
| | 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' }); |
| | } |
| |
|
| | |
| | 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); |
| |
|
| | |
| | res.json({ success: true, message: 'Rendering started in background' }); |
| |
|
| | |
| | (async () => { |
| | try { |
| | console.log(`[Render] Starting background render for project ${projectId}`); |
| |
|
| | |
| | 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, |
| | }); |
| |
|
| | |
| | const composition = await selectComposition({ |
| | serveUrl: bundleLocation, |
| | id: 'Main', |
| | inputProps: { scenes, settings }, |
| | chromiumOptions: { executablePath: process.env.CHROME_BIN }, |
| | }); |
| |
|
| | const outputLocation = join(tmpdir(), `out-${projectId}.mp4`); |
| |
|
| | let lastLoggedPercent = -1; |
| |
|
| | |
| | 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, |
| | args: [ |
| | '--no-sandbox', |
| | '--disable-dev-shm-usage', |
| | '--disable-gpu', |
| | '--mute-audio', |
| | ] |
| | }, |
| | |
| | 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); |
| |
|
| | |
| | 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}`); |
| |
|
| | |
| | const { data: { publicUrl } } = supabase |
| | .storage |
| | .from('projects') |
| | .getPublicUrl(fileName); |
| |
|
| | console.log(`[Render] Uploaded to ${publicUrl}`); |
| |
|
| | |
| | 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`); |
| |
|
| | |
| | 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}`); |
| | |
| | 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); |
| | }); |
| |
|