3v324v23's picture
upload firebase
23d4307
/**
* 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;
}