File size: 6,258 Bytes
c06e6dc 8431e01 7f69117 aa606c4 8431e01 aa606c4 8431e01 aa606c4 8431e01 368b96d 8431e01 368b96d 8431e01 368b96d 8431e01 368b96d 8431e01 368b96d 8431e01 368b96d 8431e01 368b96d 0243f01 8431e01 368b96d 8431e01 368b96d aa606c4 368b96d aa606c4 415afc3 056e30b aa606c4 03145d4 aa606c4 e6ed96b aa606c4 885352b aa606c4 885352b aa606c4 368b96d 056e30b 368b96d 8431e01 368b96d aa606c4 368b96d 8431e01 368b96d 8431e01 aa606c4 8431e01 368b96d aa606c4 368b96d aa606c4 368b96d 8431e01 368b96d aa606c4 368b96d 8431e01 368b96d e6ed96b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | // 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);
});
|