| |
| 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); |
| }); |
|
|