Spaces:
Sleeping
Sleeping
| /** | |
| * Render Module | |
| * | |
| * Handles Remotion video rendering with optimized settings. | |
| * After render completes, uploads to Firebase Storage. | |
| */ | |
| import { bundle } from '@remotion/bundler'; | |
| import { renderMedia, selectComposition, getCompositions } from '@remotion/renderer'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| import { uploadToFirebase } from './firebase-admin'; | |
| const REMOTION_ENTRY = path.resolve(process.cwd(), 'remotion/index.tsx'); | |
| const VIDEOS_DIR = path.join(process.cwd(), 'videos'); | |
| const JOBS_DIR = path.join(process.cwd(), 'jobs'); | |
| const COMPOSITION_ID = 'VEditor'; | |
| // Configuration | |
| const CONFIG = { | |
| // Set concurrency to 1 to ensure stability in container environments | |
| // os.cpus() can report host cores which are not accessible | |
| concurrency: 1, | |
| verbose: false, | |
| timeoutMs: 300000, // 5 minutes | |
| }; | |
| interface RenderOptions { | |
| fps?: number; | |
| width?: number; | |
| height?: number; | |
| format?: 'mp4' | 'webm'; | |
| } | |
| // Helper to update job status file | |
| function updateJobStatus(jobId: string, status: object): void { | |
| const statusFile = path.join(JOBS_DIR, `${jobId}.status.json`); | |
| fs.writeFileSync(statusFile, JSON.stringify(status)); | |
| } | |
| export async function renderVideo( | |
| jobId: string, | |
| design: unknown, | |
| options: RenderOptions | |
| ): Promise<string> { | |
| console.log(`[Render] Job ${jobId}: Starting`); | |
| console.log(`[Render] Concurrency: ${CONFIG.concurrency}`); | |
| // Update status to bundling | |
| updateJobStatus(jobId, { status: 'bundling', progress: 0 }); | |
| // Ensure output directory exists | |
| if (!fs.existsSync(VIDEOS_DIR)) { | |
| fs.mkdirSync(VIDEOS_DIR, { recursive: true }); | |
| } | |
| // Bundle | |
| console.log(`[Render] Bundling from: ${REMOTION_ENTRY}`); | |
| const bundlePath = await bundle({ | |
| entryPoint: REMOTION_ENTRY, | |
| }); | |
| console.log(`[Render] Bundle created: ${bundlePath}`); | |
| // Select composition | |
| const inputProps = { design }; | |
| const compositions = await getCompositions(bundlePath, { inputProps }); | |
| console.log(`[Render] Compositions: ${compositions.map(c => c.id).join(', ')}`); | |
| const composition = await selectComposition({ | |
| serveUrl: bundlePath, | |
| id: COMPOSITION_ID, | |
| inputProps, | |
| }); | |
| console.log(`[Render] Selected: ${composition.id} - ${composition.durationInFrames} frames`); | |
| // Update status to rendering | |
| updateJobStatus(jobId, { status: 'rendering', progress: 5 }); | |
| // Render | |
| const format = options.format || 'mp4'; | |
| const outputPath = path.join(VIDEOS_DIR, `${jobId}.${format}`); | |
| let lastReportedProgress = 0; | |
| await renderMedia({ | |
| composition, | |
| serveUrl: bundlePath, | |
| codec: format === 'webm' ? 'vp8' : 'h264', | |
| outputLocation: outputPath, | |
| inputProps, | |
| verbose: CONFIG.verbose, | |
| concurrency: CONFIG.concurrency, | |
| timeoutInMilliseconds: CONFIG.timeoutMs, | |
| chromiumOptions: { | |
| disableWebSecurity: true, | |
| gl: 'angle', | |
| }, | |
| onProgress: ({ progress, renderedFrames }) => { | |
| const pct = Math.round(progress * 100); | |
| // Only update if progress changed by at least 5% | |
| if (pct >= lastReportedProgress + 5 || pct === 100) { | |
| lastReportedProgress = pct; | |
| console.log(`[Render] Progress: ${pct}% (${renderedFrames}/${composition.durationInFrames})`); | |
| // Update status file with current progress (max 90% for render, 10% for upload) | |
| updateJobStatus(jobId, { | |
| status: 'rendering', | |
| progress: Math.min(pct * 0.9, 90), | |
| renderedFrames, | |
| totalFrames: composition.durationInFrames | |
| }); | |
| } | |
| }, | |
| }); | |
| console.log(`[Render] Render completed: ${outputPath}`); | |
| // Upload to Firebase Storage | |
| updateJobStatus(jobId, { status: 'uploading', progress: 92 }); | |
| console.log(`[Render] Uploading to Firebase...`); | |
| const firebaseUrl = await uploadToFirebase(outputPath, `exports/${jobId}.${format}`); | |
| console.log(`[Render] Upload completed: ${firebaseUrl}`); | |
| // Clean up local file to save space | |
| try { | |
| fs.unlinkSync(outputPath); | |
| console.log(`[Render] Cleaned up local file: ${outputPath}`); | |
| } catch (e) { | |
| console.warn(`[Render] Failed to clean up local file: ${outputPath}`); | |
| } | |
| return firebaseUrl; | |
| } | |