diff --git a/public/app.js b/public/app.js
index a0caed5..12b0728 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1702,37 +1702,7 @@
return new Blob([view], { type: 'audio/wav' });
}
- function updateExportEngineBadge() {
- const formatSelect = document.getElementById('exportFormat');
- if (!formatSelect) return;
- const format = formatSelect.value;
- const badge = document.getElementById('exportEngineBadge');
- const title = document.getElementById('exportEngineTitle');
- const desc = document.getElementById('exportEngineDesc');
- const icon = badge ? badge.querySelector('.badge-icon') : null;
- if (!badge || !title || !desc) return;
-
- if (format === 'webm_alpha' || format === 'mp4') {
- badge.classList.remove('badge-server');
- if (icon) icon.textContent = '⚡';
- title.textContent = 'Client-GPU Export (Blitzschnell im Browser)';
- if (format === 'webm_alpha') {
- desc.textContent = 'Rendert direkt im Browser bei 60–120+ FPS mit transparentem Alphakanal für DaVinci Resolve (0 MB Server-Upload).';
- } else {
- desc.textContent = 'Rendert direkt im Browser bei 60–120+ FPS als H.264 MP4 für YouTube / Social Media (0 MB Server-Upload).';
- }
- } else {
- badge.classList.add('badge-server');
- if (icon) icon.textContent = '⚡🖥️';
- title.textContent = 'Studio Master Hybrid-Export (GPU + Server FFmpeg)';
- if (format === 'prores') {
- desc.textContent = 'Rendert mit GPU-Beschleunigung und wandelt auf dem Server via FFmpeg in echtes Apple ProRes 4444 (10-Bit Alpha) um – in unter 2 Minuten!';
- } else {
- desc.textContent = 'Rendert mit GPU-Beschleunigung und wandelt auf dem Server via FFmpeg in verlustfreies QuickTime RLE um.';
- }
- }
- }
/**
* Probes for the best supported WebCodecs Video configuration
@@ -2399,9 +2369,8 @@
} else if (statusJson.status === 'error') {
throw new Error(statusJson.error || 'FFmpeg Encoder-Fehler auf dem Server');
} else {
- const mb = (statusJson.sizeBytes / 1024 / 1024).toFixed(1);
- exportStatusText.textContent = `Server verarbeitet Render-Job (${mb} MB geschrieben)...`;
- if (exportFpsLive) exportFpsLive.textContent = 'Warten auf FFmpeg...';
+ exportStatusText.textContent = `Server verarbeitet Render-Job: ${statusJson.currentStatus}`;
+ if (exportFpsLive) exportFpsLive.textContent = '';
}
}
@@ -2719,10 +2688,7 @@
}
});
- const exportFormatSelect = document.getElementById('exportFormat');
- if (exportFormatSelect) {
- exportFormatSelect.addEventListener('change', updateExportEngineBadge);
- }
+ // Export format listener removed as it is now static
btnStartExport.addEventListener('click', startExport);
headerExportBtn.addEventListener('click', () => {
@@ -2830,7 +2796,7 @@
setAspectRatio('16:9');
applyPreset('trap_nation');
setupEvents();
- updateExportEngineBadge();
+ // updateExportEngineBadge();
fetchRenderHistory();
checkActiveSessions();
state.isPlaying = true;
diff --git a/public/index.html b/public/index.html
index e3486eb..281be06 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1176,24 +1176,12 @@
diff --git a/server.mjs b/server.mjs
index 279be11..9b42092 100644
--- a/server.mjs
+++ b/server.mjs
@@ -144,80 +144,30 @@ app.post('/api/export/transcode', upload.fields([
const ext = (format === 'prores' || format === 'qtrle') ? 'mov' : (format === 'webm_alpha' ? 'webm' : 'mp4');
- // We will render ALL formats simultaneously
const formats = {
- mp4: {
- filename: `${title}_Master.mp4`,
- path: path.join(TEMP_DIR, `${sessionId}_${title}_Master.mp4`),
- formatId: 'mp4'
+ prores: {
+ filename: `${title}_ProRes4444_Alpha.mov`,
+ path: path.join(TEMP_DIR, `${sessionId}_${title}_ProRes4444_Alpha.mov`),
+ formatId: 'prores'
},
webm_alpha: {
filename: `${title}_Alpha_DaVinci.webm`,
path: path.join(TEMP_DIR, `${sessionId}_${title}_Alpha_DaVinci.webm`),
formatId: 'webm_alpha'
},
- prores: {
- filename: `${title}_ProRes4444_Alpha.mov`,
- path: path.join(TEMP_DIR, `${sessionId}_${title}_ProRes4444_Alpha.mov`),
- formatId: 'prores'
- },
qtrle: {
filename: `${title}_Lossless_QTRLE_Alpha.mov`,
path: path.join(TEMP_DIR, `${sessionId}_${title}_Lossless_QTRLE_Alpha.mov`),
formatId: 'qtrle'
+ },
+ mp4: {
+ filename: `${title}_Master.mp4`,
+ path: path.join(TEMP_DIR, `${sessionId}_${title}_Master.mp4`),
+ formatId: 'mp4'
}
};
- console.log(`[${sessionId}] ⚡ High-Speed Transcoding to ALL FORMATS`);
-
- const ffmpegArgs = ['-y', '-i', videoPath];
- if (audioPath) {
- ffmpegArgs.push('-i', audioPath);
- }
-
- // Map video and audio to all outputs
- const mapArgs = audioPath ? ['-map', '0:v:0', '-map', '1:a:0'] : ['-map', '0:v:0'];
-
- // 1. MP4
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'libx264', '-crf', '14', '-preset', 'fast', '-pix_fmt', 'yuv420p', '-threads', '0',
- ...(audioPath ? ['-c:a', 'aac', '-b:a', '320k'] : []),
- '-shortest', formats.mp4.path
- );
-
- // 2. WebM VP9 Alpha
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'copy',
- ...(audioPath ? ['-c:a', 'libopus', '-b:a', '320k'] : []),
- '-shortest', formats.webm_alpha.path
- );
-
- // 3. ProRes 4444
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'prores_ks', '-profile:v', '4', '-qscale:v', '11', '-pix_fmt', 'yuva444p10le', '-vendor', 'apl0', '-threads', '0',
- ...(audioPath ? ['-c:a', 'pcm_s16le'] : []),
- '-shortest', formats.prores.path
- );
-
- // 4. QTRLE
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'qtrle', '-pix_fmt', 'argb', '-threads', '0',
- ...(audioPath ? ['-c:a', 'pcm_s16le'] : []),
- '-shortest', formats.qtrle.path
- );
-
- const ffmpegProc = spawn('ffmpeg', ffmpegArgs);
- let errorLog = '';
- ffmpegProc.stderr.on('data', (d) => {
- errorLog += d.toString();
- if (errorLog.length > 10000) {
- errorLog = errorLog.slice(-8000);
- }
- });
+ console.log(`[${sessionId}] ⚡ High-Speed Transcoding starting (Sequential)`);
const session = {
id: sessionId,
@@ -226,36 +176,19 @@ app.post('/api/export/transcode', upload.fields([
videoPath,
audioPath,
formats,
- ffmpegProc,
+ ffmpegProc: null, // No single proc anymore
errorLog: '',
done: false,
- error: null
+ error: null,
+ currentStatus: 'Startet...'
};
-
- ffmpegProc.on('close', (code, signal) => {
- session.done = true;
- if (videoPath && fs.existsSync(videoPath)) {
- try { fs.unlinkSync(videoPath); } catch (e) {}
- }
- if (audioPath && fs.existsSync(audioPath)) {
- try { fs.unlinkSync(audioPath); } catch (e) {}
- }
- if (code !== 0 || signal) {
- session.error = signal
- ? `FFmpeg Transcode Prozess wurde durch Signal ${signal} abgebrochen: ${errorLog.slice(-400)}`
- : `FFmpeg Transcode Fehler (Code ${code}): ${errorLog.slice(-400)}`;
- console.error(`[${sessionId}] ❌ Transcode failed:`, session.error);
- } else {
- console.log(`[${sessionId}] ✅ Transcode complete! (All Formats)`);
- }
- });
-
- ffmpegProc.on('error', (err) => {
- session.done = true;
- session.error = err.message;
- });
-
activeSessions.set(sessionId, session);
+
+ // Run sequentially in background
+ runSequentialEncodes(session, videoPath, audioPath, ['prores', 'webm_alpha', 'qtrle', 'mp4']).catch(err => {
+ console.error(`[${sessionId}] Queue Error:`, err);
+ });
+
res.json({ sessionId, format });
} catch (err) {
console.error('Error starting transcode:', err);
@@ -284,29 +217,29 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
const ext = (format === 'prores' || format === 'qtrle') ? 'mov' : (format === 'webm_alpha' ? 'webm' : 'mp4');
const formats = {
- mp4: {
- filename: `${title}_Master.mp4`,
- path: path.join(TEMP_DIR, `${sessionId}_${title}_Master.mp4`),
- formatId: 'mp4'
+ prores: {
+ filename: `${title}_ProRes4444_Alpha.mov`,
+ path: path.join(TEMP_DIR, `${sessionId}_${title}_ProRes4444_Alpha.mov`),
+ formatId: 'prores'
},
webm_alpha: {
filename: `${title}_Alpha_DaVinci.webm`,
path: path.join(TEMP_DIR, `${sessionId}_${title}_Alpha_DaVinci.webm`),
formatId: 'webm_alpha'
},
- prores: {
- filename: `${title}_ProRes4444_Alpha.mov`,
- path: path.join(TEMP_DIR, `${sessionId}_${title}_ProRes4444_Alpha.mov`),
- formatId: 'prores'
- },
qtrle: {
filename: `${title}_Lossless_QTRLE_Alpha.mov`,
path: path.join(TEMP_DIR, `${sessionId}_${title}_Lossless_QTRLE_Alpha.mov`),
formatId: 'qtrle'
+ },
+ mp4: {
+ filename: `${title}_Master.mp4`,
+ path: path.join(TEMP_DIR, `${sessionId}_${title}_Master.mp4`),
+ formatId: 'mp4'
}
};
- console.log(`[${sessionId}] 🚀 Starting Render: ALL FORMATS ${width}x${height} @ ${fps}FPS`);
+ console.log(`[${sessionId}] 🚀 Starting Render: Live to ProRes ${width}x${height} @ ${fps}FPS`);
const ffmpegArgs = [
'-y',
@@ -315,42 +248,14 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
'-s', `${width}x${height}`,
'-framerate', fps.toString(),
'-i', '-',
- '-i', audioPath
- ];
-
- const mapArgs = ['-map', '0:v:0', '-map', '1:a:0'];
-
- // 1. MP4 (Hardware accelerated preset if possible, fallback to CPU)
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'libx264', '-crf', '14', '-preset', 'fast', '-pix_fmt', 'yuv420p', '-threads', '0',
- '-c:a', 'aac', '-b:a', '320k', '-shortest', formats.mp4.path
- );
-
- // 2. WebM VP9 Alpha (CPU intensive)
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p', '-b:v', '35M', '-minrate', '20M', '-maxrate', '50M', '-speed', '2', '-threads', '4', '-row-mt', '1', '-auto-alt-ref', '0',
- '-c:a', 'libopus', '-b:a', '320k', '-shortest', formats.webm_alpha.path
- );
-
- // 3. ProRes 4444
- ffmpegArgs.push(
- ...mapArgs,
+ '-i', audioPath,
+ '-map', '0:v:0', '-map', '1:a:0',
'-c:v', 'prores_ks', '-profile:v', '4', '-qscale:v', '11', '-pix_fmt', 'yuva444p10le', '-vendor', 'apl0', '-threads', '0',
'-c:a', 'pcm_s16le', '-shortest', formats.prores.path
- );
-
- // 4. QTRLE
- ffmpegArgs.push(
- ...mapArgs,
- '-c:v', 'qtrle', '-pix_fmt', 'argb', '-threads', '0',
- '-c:a', 'pcm_s16le', '-shortest', formats.qtrle.path
- );
+ ];
const ffmpegProc = spawn('ffmpeg', ffmpegArgs);
ffmpegProc.stdin.on('error', (err) => {
- // Prevent unhandled EPIPE if FFmpeg closes before stdin is flushed
console.warn(`[${sessionId}] Warning on stdin pipe:`, err.message);
});
@@ -366,16 +271,22 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
ffmpegProc,
errorLog: '',
done: false,
- error: null
+ error: null,
+ currentStatus: 'Live Empfang & ProRes Render...'
};
+ activeSessions.set(sessionId, session);
ffmpegProc.on('close', (code) => {
- session.done = true;
if (code !== 0) {
- session.error = `FFmpeg Fehler (Code ${code}): ${errorLog.slice(-400)}`;
- console.error(`[${sessionId}] ❌ Render failed:`, session.error);
+ session.error = `FFmpeg Fehler beim Live-Render (Code ${code}): ${errorLog.slice(-400)}`;
+ console.error(`[${sessionId}] ❌ Live Render failed:`, session.error);
+ session.done = true;
} else {
- console.log(`[${sessionId}] ✅ Render complete! (All Formats)`);
+ console.log(`[${sessionId}] ✅ Live Render (ProRes) complete! Starting background queue...`);
+ // Start remaining encodes from the ProRes master file using the original audioPath
+ runSequentialEncodes(session, formats.prores.path, audioPath, ['webm_alpha', 'qtrle', 'mp4']).catch(err => {
+ console.error(`[${sessionId}] Background Queue Error:`, err);
+ });
}
});
@@ -384,7 +295,6 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
session.error = err.message;
});
- activeSessions.set(sessionId, session);
res.json({ sessionId, format });
} catch (err) {
console.error('Error starting export:', err);
@@ -392,6 +302,88 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
}
});
+async function runSequentialEncodes(session, inputVideoPath, inputAudioPath, targetFormats) {
+ for (let i = 0; i < targetFormats.length; i++) {
+ const formatId = targetFormats[i];
+ const formatInfo = session.formats[formatId];
+ if (!formatInfo) continue;
+
+ session.currentStatus = `Rendere ${i+1}/${targetFormats.length}: ${formatId.toUpperCase()}...`;
+
+ const ffmpegArgs = ['-y', '-i', inputVideoPath];
+ if (inputAudioPath) {
+ ffmpegArgs.push('-i', inputAudioPath);
+ }
+
+ const mapArgs = inputAudioPath ? ['-map', '0:v:0', '-map', '1:a:0'] : ['-map', '0:v:0'];
+
+ if (formatId === 'mp4') {
+ ffmpegArgs.push(
+ ...mapArgs,
+ '-c:v', 'libx264', '-crf', '14', '-preset', 'fast', '-pix_fmt', 'yuv420p', '-threads', '0',
+ ...(inputAudioPath ? ['-c:a', 'aac', '-b:a', '320k'] : []),
+ '-shortest', formatInfo.path
+ );
+ } else if (formatId === 'webm_alpha') {
+ ffmpegArgs.push(
+ ...mapArgs,
+ '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p', '-b:v', '35M', '-minrate', '20M', '-maxrate', '50M', '-speed', '2', '-threads', '4', '-row-mt', '1', '-auto-alt-ref', '0',
+ '-c:a', 'libopus', '-b:a', '320k', '-shortest', formatInfo.path
+ );
+ } else if (formatId === 'prores') {
+ ffmpegArgs.push(
+ ...mapArgs,
+ '-c:v', 'prores_ks', '-profile:v', '4', '-qscale:v', '11', '-pix_fmt', 'yuva444p10le', '-vendor', 'apl0', '-threads', '0',
+ ...(inputAudioPath ? ['-c:a', 'pcm_s16le'] : []),
+ '-shortest', formatInfo.path
+ );
+ } else if (formatId === 'qtrle') {
+ ffmpegArgs.push(
+ ...mapArgs,
+ '-c:v', 'qtrle', '-pix_fmt', 'argb', '-threads', '0',
+ ...(inputAudioPath ? ['-c:a', 'pcm_s16le'] : []),
+ '-shortest', formatInfo.path
+ );
+ }
+
+ console.log(`[${session.id}] ⏳ Starting sequential task: ${formatId}`);
+
+ await new Promise((resolve, reject) => {
+ const proc = spawn('ffmpeg', ffmpegArgs);
+ session.ffmpegProc = proc; // Update current process so cleanupSession can kill it if needed
+ let errorLog = '';
+
+ proc.stderr.on('data', d => {
+ errorLog += d.toString();
+ if (errorLog.length > 5000) errorLog = errorLog.slice(-4000);
+ });
+
+ proc.on('close', code => {
+ if (code !== 0) {
+ reject(new Error(`Code ${code} during ${formatId}: ${errorLog.slice(-300)}`));
+ } else {
+ resolve();
+ }
+ });
+
+ proc.on('error', err => reject(err));
+ });
+
+ console.log(`[${session.id}] ✅ Sequential task finished: ${formatId}`);
+ }
+
+ // Clean up original files if they were temporary
+ if (session.videoPath && fs.existsSync(session.videoPath)) {
+ try { fs.unlinkSync(session.videoPath); } catch (e) {}
+ }
+ if (session.audioPath && fs.existsSync(session.audioPath)) {
+ try { fs.unlinkSync(session.audioPath); } catch (e) {}
+ }
+
+ session.currentStatus = 'Fertig';
+ session.done = true;
+}
+
// Stream raw RGBA frame or multi-frame batch
app.post(['/api/export/frame/:id', '/api/export/frame-batch/:id'], express.raw({ type: 'application/octet-stream', limit: '500mb' }), (req, res) => {
const session = activeSessions.get(req.params.id);
@@ -441,24 +433,9 @@ app.get('/api/export/status/:id', (req, res) => {
return res.status(500).json({ status: 'error', error: session.error });
}
- // We can track the size of the mp4 format as a proxy for progress
- let sizeBytes = 0;
- try {
- if (session.formats && session.formats.mp4 && fs.existsSync(session.formats.mp4.path)) {
- sizeBytes = fs.statSync(session.formats.mp4.path).size;
- }
- } catch (e) {}
-
- if (session.done) {
- return res.json({
- status: 'ready',
- sizeBytes: sizeBytes
- });
- }
-
res.json({
- status: 'finalizing',
- sizeBytes: sizeBytes
+ status: session.done ? 'ready' : 'finalizing',
+ currentStatus: session.currentStatus || 'In Bearbeitung...'
});
});