Update server.js
Browse files
server.js
CHANGED
|
@@ -56,28 +56,80 @@ app.post('/render', async (req, res) => {
|
|
| 56 |
webpackOverride: (config) => config,
|
| 57 |
});
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
const composition = await selectComposition({
|
| 60 |
serveUrl: bundleLocation,
|
| 61 |
id: 'Main',
|
| 62 |
-
inputProps: { scenes, settings },
|
| 63 |
chromiumOptions: { executablePath: process.env.CHROME_BIN },
|
| 64 |
});
|
| 65 |
|
| 66 |
-
const tempDir = join(tmpdir(), `remotion-${randomUUID()}`);
|
| 67 |
-
await mkdir(tempDir, { recursive: true });
|
| 68 |
const outputPath = join(tempDir, 'output.mp4');
|
| 69 |
|
| 70 |
const os = await import('os');
|
| 71 |
const cpuCount = os.cpus().length;
|
| 72 |
-
console.log(`[Render] Detected ${cpuCount} CPUs.
|
| 73 |
-
|
| 74 |
-
// Tuning for CPU Only
|
| 75 |
-
// Remotion is CPU-bound. Concurrency = CPU Cores is usually good,
|
| 76 |
-
// but we leave 1 core for OS/Orchestration.
|
| 77 |
-
const safeConcurrency = 1;
|
| 78 |
-
console.log(`[Render] Requesting concurrency: ${safeConcurrency} (Host has ${cpuCount})`);
|
| 79 |
-
|
| 80 |
-
let lastLog = 0;
|
| 81 |
|
| 82 |
await renderMedia({
|
| 83 |
composition,
|
|
@@ -87,24 +139,20 @@ app.post('/render', async (req, res) => {
|
|
| 87 |
outputLocation: outputPath,
|
| 88 |
imageFormat: 'jpeg',
|
| 89 |
jpegQuality: 80,
|
| 90 |
-
inputProps: { scenes, settings },
|
| 91 |
chromiumOptions: {
|
| 92 |
executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
|
| 93 |
enableMultiProcessRendering: true,
|
| 94 |
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-software-rasterizer']
|
| 95 |
},
|
|
|
|
| 96 |
concurrency: 1,
|
| 97 |
disallowParallelEncoding: false,
|
| 98 |
-
timeoutInMilliseconds: 300000, // 5 minutes timeout for slow downloads
|
| 99 |
onProgress: ({ progress }) => {
|
| 100 |
const percent = Math.round(progress * 100);
|
| 101 |
-
// Log
|
| 102 |
-
if (percent
|
| 103 |
-
console.log(`[Render] Progress: ${percent}%`);
|
| 104 |
-
lastLog = percent;
|
| 105 |
-
}
|
| 106 |
},
|
| 107 |
-
// Default software encoder (libx264) is robust and decent speed on high CPU
|
| 108 |
});
|
| 109 |
|
| 110 |
console.log(`[Render] Render complete. Uploading to Supabase...`);
|
|
@@ -115,7 +163,7 @@ app.post('/render', async (req, res) => {
|
|
| 115 |
const fileName = `${projectId}/video-${Date.now()}.mp4`;
|
| 116 |
const { data: uploadData, error: uploadError } = await supabase
|
| 117 |
.storage
|
| 118 |
-
.from('projects')
|
| 119 |
.upload(fileName, videoBuffer, {
|
| 120 |
contentType: 'video/mp4',
|
| 121 |
upsert: true
|
|
|
|
| 56 |
webpackOverride: (config) => config,
|
| 57 |
});
|
| 58 |
|
| 59 |
+
// --- STABILITY: Pre-download Assets ---
|
| 60 |
+
// To prevent "server sent no data" timeouts and reduce RAM usage,
|
| 61 |
+
// we download all assets to disk BEFORE starting the render.
|
| 62 |
+
|
| 63 |
+
const fs = await import('fs');
|
| 64 |
+
const { pipeline } = await import('stream/promises');
|
| 65 |
+
const { createWriteStream } = await import('fs');
|
| 66 |
+
|
| 67 |
+
// Helper: Robust Downloader with Retries
|
| 68 |
+
const downloadAsset = async (url, destPath, retries = 3) => {
|
| 69 |
+
for (let i = 0; i < retries; i++) {
|
| 70 |
+
try {
|
| 71 |
+
const response = await fetch(url);
|
| 72 |
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
| 73 |
+
if (!response.body) throw new Error(`No body for ${url}`);
|
| 74 |
+
|
| 75 |
+
await pipeline(response.body, createWriteStream(destPath));
|
| 76 |
+
return; // Success
|
| 77 |
+
} catch (err) {
|
| 78 |
+
console.warn(`[Download] Attempt ${i + 1} failed for ${url}: ${err.message}`);
|
| 79 |
+
if (i === retries - 1) throw err; // Throw on last failure
|
| 80 |
+
await new Promise(r => setTimeout(r, 2000 * (i + 1))); // Backoff
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const tempDir = join(tmpdir(), `remotion-${randomUUID()}`);
|
| 86 |
+
await mkdir(tempDir, { recursive: true });
|
| 87 |
+
|
| 88 |
+
console.log(`[Render] Pre-downloading assets to ${tempDir}...`);
|
| 89 |
+
|
| 90 |
+
// Iterate scenes and download assets
|
| 91 |
+
const localScenes = await Promise.all(scenes.map(async (scene, idx) => {
|
| 92 |
+
const newScene = { ...scene };
|
| 93 |
+
|
| 94 |
+
// 1. Audio
|
| 95 |
+
if (scene.audio_url) {
|
| 96 |
+
const audioExt = scene.audio_url.split('.').pop().split('?')[0] || 'mp3';
|
| 97 |
+
const localAudioPath = join(tempDir, `audio_${idx}.${audioExt}`);
|
| 98 |
+
await downloadAsset(scene.audio_url, localAudioPath);
|
| 99 |
+
newScene.audio_url = `file://${localAudioPath}`;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// 2. Video / Image
|
| 103 |
+
if (scene.image_url) {
|
| 104 |
+
// Check if it's a video (likely from Pexels/Supabase)
|
| 105 |
+
const isVideo = scene.media_type === 'video' || scene.image_url.includes('.mp4');
|
| 106 |
+
|
| 107 |
+
const ext = isVideo ? 'mp4' : 'jpg'; // Default extension if unknown
|
| 108 |
+
const localMediaPath = join(tempDir, `visual_${idx}.${ext}`);
|
| 109 |
+
|
| 110 |
+
await downloadAsset(scene.image_url, localMediaPath);
|
| 111 |
+
newScene.image_url = `file://${localMediaPath}`;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return newScene;
|
| 115 |
+
}));
|
| 116 |
+
|
| 117 |
+
console.log(`[Render] Assets downloaded. Starting engine...`);
|
| 118 |
+
|
| 119 |
+
// --------------------------------------
|
| 120 |
+
|
| 121 |
const composition = await selectComposition({
|
| 122 |
serveUrl: bundleLocation,
|
| 123 |
id: 'Main',
|
| 124 |
+
inputProps: { scenes: localScenes, settings }, // Use LOCAL scenes
|
| 125 |
chromiumOptions: { executablePath: process.env.CHROME_BIN },
|
| 126 |
});
|
| 127 |
|
|
|
|
|
|
|
| 128 |
const outputPath = join(tempDir, 'output.mp4');
|
| 129 |
|
| 130 |
const os = await import('os');
|
| 131 |
const cpuCount = os.cpus().length;
|
| 132 |
+
console.log(`[Render] Detected ${cpuCount} CPUs.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
await renderMedia({
|
| 135 |
composition,
|
|
|
|
| 139 |
outputLocation: outputPath,
|
| 140 |
imageFormat: 'jpeg',
|
| 141 |
jpegQuality: 80,
|
| 142 |
+
inputProps: { scenes: localScenes, settings }, // Use LOCAL scenes
|
| 143 |
chromiumOptions: {
|
| 144 |
executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
|
| 145 |
enableMultiProcessRendering: true,
|
| 146 |
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-software-rasterizer']
|
| 147 |
},
|
| 148 |
+
// STRICT CONCURRENCY FOR STABILITY
|
| 149 |
concurrency: 1,
|
| 150 |
disallowParallelEncoding: false,
|
|
|
|
| 151 |
onProgress: ({ progress }) => {
|
| 152 |
const percent = Math.round(progress * 100);
|
| 153 |
+
// Log to console (Supabase doesn't need realtime logs, just status)
|
| 154 |
+
if (percent % 10 === 0) console.log(`[Render] Progress: ${percent}%`);
|
|
|
|
|
|
|
|
|
|
| 155 |
},
|
|
|
|
| 156 |
});
|
| 157 |
|
| 158 |
console.log(`[Render] Render complete. Uploading to Supabase...`);
|
|
|
|
| 163 |
const fileName = `${projectId}/video-${Date.now()}.mp4`;
|
| 164 |
const { data: uploadData, error: uploadError } = await supabase
|
| 165 |
.storage
|
| 166 |
+
.from('projects')
|
| 167 |
.upload(fileName, videoBuffer, {
|
| 168 |
contentType: 'video/mp4',
|
| 169 |
upsert: true
|