File size: 4,451 Bytes
bc18ad5
 
 
 
23d4307
bc18ad5
 
 
 
 
 
23d4307
bc18ad5
 
 
fcf74ae
bc18ad5
 
 
 
569f90a
 
 
bc18ad5
 
 
 
 
 
 
 
 
 
 
fcf74ae
 
 
 
 
 
bc18ad5
 
 
 
 
 
 
 
fcf74ae
 
 
bc18ad5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcf74ae
 
 
bc18ad5
 
 
 
fcf74ae
 
bc18ad5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcf74ae
 
 
 
bc18ad5
fcf74ae
23d4307
fcf74ae
 
23d4307
fcf74ae
 
 
bc18ad5
 
 
 
23d4307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc18ad5
23d4307
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
/**
 * 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;
}