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);
});