feat: Multi-Format Rendering und Render-Verlauf

This commit is contained in:
Jannik committed 2026-08-09 10:23:38 +02:00
1 parent bae998f6c9
commit ced7588919
4 files changed
+314 -206

No files matched your search

+101 -54
View File
@@ -2243,46 +2243,10 @@
currentExportSessionId = transcodeStartRes.sessionId;
// Step 3: Monitor Server FFmpeg Transcode
exportStatusText.textContent = `Server transcodiert ${format === 'prores' ? 'Apple ProRes 4444 (10-Bit Alpha)' : (format === 'qtrle' ? 'Lossless QuickTime RLE' : 'Master Video')}...`;
exportStatusText.textContent = `Server transcodiert Master Video...`;
if (exportFpsLive) exportFpsLive.textContent = 'FFmpeg Multi-Thread Encoding...';
let finishData = null;
while (!finishData && !isExportCancelled) {
await new Promise(r => setTimeout(r, 800));
const statusRes = await fetch(`/api/export/status/${currentExportSessionId}`);
if (!statusRes.ok) {
const errData = await statusRes.json().catch(() => ({}));
throw new Error(errData.error || 'Fehler beim Abrufen des Video-Status');
}
const statusJson = await statusRes.json();
if (statusJson.status === 'ready') {
finishData = statusJson;
break;
} else if (statusJson.status === 'error') {
throw new Error(statusJson.error || 'FFmpeg Transcoder-Fehler auf dem Server');
} else {
const mb = (statusJson.sizeBytes / 1024 / 1024).toFixed(1);
exportStatusText.textContent = `Schreibe Master-Datei auf Server (${mb} MB)...`;
}
}
if (isExportCancelled) throw new Error('Export durch Benutzer abgebrochen');
// Trigger automatic download
const downloadLink = document.createElement('a');
downloadLink.style.display = 'none';
downloadLink.href = finishData.downloadUrl;
downloadLink.download = finishData.filename;
document.body.appendChild(downloadLink);
downloadLink.click();
setTimeout(() => {
document.body.removeChild(downloadLink);
}, 5000);
const finalMb = (finishData.sizeBytes / 1024 / 1024).toFixed(1);
alert(`🎉 Fertig! Dein Studio-Master "${finishData.filename}" (${finalMb} MB) wurde erfolgreich transcodiert und heruntergeladen.`);
await monitorServerSession(currentExportSessionId, format);
return;
}
@@ -2414,11 +2378,15 @@
}
// Poll status until ready
await monitorServerSession(currentExportSessionId, format);
}
async function monitorServerSession(sessionId, format) {
let finishData = null;
while (!finishData && !isExportCancelled) {
await new Promise(r => setTimeout(r, 600));
await new Promise(r => setTimeout(r, 1000));
const statusRes = await fetch(`/api/export/status/${currentExportSessionId}`);
const statusRes = await fetch(`/api/export/status/${sessionId}`);
if (!statusRes.ok) {
const errData = await statusRes.json().catch(() => ({}));
throw new Error(errData.error || 'Fehler beim Abrufen des Video-Status');
@@ -2432,25 +2400,15 @@
throw new Error(statusJson.error || 'FFmpeg Encoder-Fehler auf dem Server');
} else {
const mb = (statusJson.sizeBytes / 1024 / 1024).toFixed(1);
exportStatusText.textContent = `Finalisiere Video (${mb} MB geschrieben)...`;
if (exportFpsLive) exportFpsLive.textContent = 'Schreibe Datei auf Festplatte...';
exportStatusText.textContent = `Server verarbeitet Render-Job (${mb} MB geschrieben)...`;
if (exportFpsLive) exportFpsLive.textContent = 'Warten auf FFmpeg...';
}
}
if (isExportCancelled) throw new Error('Export durch Benutzer abgebrochen');
// Automatic Download Trigger
const downloadLink = document.createElement('a');
downloadLink.style.display = 'none';
downloadLink.href = finishData.downloadUrl;
downloadLink.download = finishData.filename;
document.body.appendChild(downloadLink);
downloadLink.click();
setTimeout(() => {
document.body.removeChild(downloadLink);
}, 1000);
alert(`🎉 Fertig! Dein Video "${finishData.filename}" wurde in makelloser Studio-Qualität gerendert.`);
fetchRenderHistory(); // Refresh the UI list
alert(`🎉 Fertig! Das Rendering ist abgeschlossen. Wähle nun im Render-Verlauf dein gewünschtes Format zum Herunterladen aus.`);
}
async function startExport() {
@@ -2772,6 +2730,11 @@
});
btnCancelExport.addEventListener('click', cancelExport);
const btnRefreshHistory = document.getElementById('btnRefreshHistory');
if (btnRefreshHistory) {
btnRefreshHistory.addEventListener('click', fetchRenderHistory);
}
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' && e.target.tagName !== 'INPUT') {
e.preventDefault();
@@ -2780,12 +2743,96 @@
});
}
async function fetchRenderHistory() {
try {
const res = await fetch('/api/export/history');
const history = await res.json();
const listContainer = document.getElementById('exportHistoryList');
if (!listContainer) return;
if (!history || history.length === 0) {
listContainer.innerHTML = '<div class="empty-state">Noch keine Renderings im Verlauf.</div>';
return;
}
listContainer.innerHTML = '';
history.forEach(sess => {
const item = document.createElement('div');
item.className = 'export-history-item';
let statusHtml = sess.error
? `<span style="color: #ff3333">❌ Fehler</span>`
: (sess.done ? `<span style="color: #00ff88">✅ Fertig</span>` : `<span style="color: #ffaa00">🔄 Rendert...</span>`);
const d = new Date(sess.createdAt);
const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
let downloadsHtml = '';
if (sess.done && !sess.error && sess.formats) {
downloadsHtml = `
<div class="export-history-actions">
<a href="/api/export/download/${sess.id}/mp4" class="btn btn-primary" download>⬇️ MP4</a>
<a href="/api/export/download/${sess.id}/webm_alpha" class="btn btn-secondary" download>⬇️ WebM (Alpha)</a>
<a href="/api/export/download/${sess.id}/prores" class="btn btn-secondary" download>⬇️ ProRes</a>
<a href="/api/export/download/${sess.id}/qtrle" class="btn btn-secondary" download>⬇️ QTRLE</a>
</div>
`;
} else if (!sess.done && !sess.error) {
downloadsHtml = `<div class="export-history-meta">Dieses Rendering läuft gerade...</div>`;
}
item.innerHTML = `
<div class="export-history-header">
<div class="export-history-title">${sess.title}</div>
<div class="export-history-meta">${statusHtml}</div>
</div>
<div class="export-history-meta">${dateStr} | ID: ${sess.id.substring(0, 15)}...</div>
${downloadsHtml}
`;
listContainer.appendChild(item);
});
} catch (err) {
console.error('Failed to fetch render history:', err);
}
}
async function checkActiveSessions() {
try {
const res = await fetch('/api/export/history');
const history = await res.json();
const runningSession = history.find(s => !s.done && !s.error);
if (runningSession) {
currentExportSessionId = runningSession.id;
state.isExporting = true;
btnStartExport.classList.add('hidden');
exportProgressCard.classList.remove('hidden');
exportStatusText.textContent = 'Stelle Verbindung zum Server-Render wieder her...';
exportPercent.textContent = '...';
monitorServerSession(runningSession.id, 'all').catch(err => {
console.error(err);
alert('Fehler beim Überwachen des wiederhergestellten Render-Jobs: ' + err.message);
}).finally(() => {
state.isExporting = false;
currentExportSessionId = null;
exportProgressCard.classList.add('hidden');
btnStartExport.classList.remove('hidden');
});
}
} catch (e) {
console.warn('Could not check active sessions on load', e);
}
}
// --- Initialize App ---
function init() {
setAspectRatio('16:9');
applyPreset('trap_nation');
setupEvents();
updateExportEngineBadge();
fetchRenderHistory();
checkActiveSessions();
state.isPlaying = true;
iconPlay.classList.add('hidden');
iconPause.classList.remove('hidden');
+13
View File
@@ -1236,6 +1236,19 @@
<button id="btnCancelExport" class="btn btn-secondary btn-sm" style="margin-top: 8px;">Abbrechen</button>
</div>
</div>
<!-- Render History Section -->
<div class="panel-section">
<div class="section-header-with-toggle">
<h3 class="section-title">Render Verlauf</h3>
<button id="btnRefreshHistory" class="btn btn-secondary btn-sm" title="Verlauf aktualisieren">
🔄
</button>
</div>
<div class="export-history-list" id="exportHistoryList">
<div class="empty-state">Noch keine Renderings im Verlauf.</div>
</div>
</div>
</div>
</div>
</aside>
+42
View File
@@ -1367,3 +1367,45 @@ body {
to { transform: rotate(360deg); }
}
/* Export History */
.export-history-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
max-height: 400px;
overflow-y: auto;
}
.export-history-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.export-history-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.export-history-title {
font-weight: 600;
color: var(--text-main);
font-size: 0.9rem;
}
.export-history-meta {
font-size: 0.75rem;
color: var(--text-muted);
}
.export-history-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.export-history-actions .btn {
font-size: 0.75rem;
padding: 4px 8px;
}
+158 -152
View File
@@ -98,10 +98,30 @@ function cleanupSession(id) {
if (sess.videoPath && fs.existsSync(sess.videoPath)) {
try { fs.unlinkSync(sess.videoPath); } catch (e) {}
}
activeSessions.delete(id);
// We intentionally DO NOT delete the output files here,
// so they remain available in the history until server restart.
// activeSessions.delete(id); // wir behalten die Session im Speicher für die Historie
}
}
// GET history endpoint
app.get('/api/export/history', (req, res) => {
const history = [];
for (const [id, sess] of activeSessions.entries()) {
history.push({
id: sess.id,
title: sess.title || 'Visualizer',
createdAt: sess.createdAt,
done: sess.done,
error: sess.error,
formats: sess.formats
});
}
// Neueste zuerst
history.sort((a, b) => b.createdAt - a.createdAt);
res.json(history);
});
/**
* ⚡ Ultra-Fast Mezzanine Transcoding Route (ProRes 4444 / QTRLE / MP4)
* Receives compact GPU-rendered video (WebM VP9 Alpha / MP4) and original audio,
@@ -123,66 +143,72 @@ app.post('/api/export/transcode', upload.fields([
const title = (req.body.title || 'Visualizer').replace(/[^a-zA-Z0-9_-]/g, '_');
const ext = (format === 'prores' || format === 'qtrle') ? 'mov' : (format === 'webm_alpha' ? 'webm' : 'mp4');
let formatSuffix = 'Visualizer';
if (format === 'prores') formatSuffix = 'ProRes4444_Alpha';
else if (format === 'qtrle') formatSuffix = 'Lossless_QTRLE_Alpha';
else if (format === 'webm_alpha') formatSuffix = 'Alpha_DaVinci';
// We will render ALL formats simultaneously
const formats = {
mp4: {
filename: `${title}_Master.mp4`,
path: path.join(TEMP_DIR, `${sessionId}_${title}_Master.mp4`),
formatId: 'mp4'
},
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'
}
};
const outputFilename = `${title}_${formatSuffix}.${ext}`;
const outputPath = path.join(TEMP_DIR, `${sessionId}_${outputFilename}`);
console.log(`[${sessionId}] ⚡ High-Speed Transcoding to ${format.toUpperCase()} -> ${outputFilename}`);
console.log(`[${sessionId}] ⚡ High-Speed Transcoding to ALL FORMATS`);
const ffmpegArgs = ['-y', '-i', videoPath];
if (audioPath) {
ffmpegArgs.push('-i', audioPath);
ffmpegArgs.push('-map', '0:v:0', '-map', '1:a:0');
}
if (format === 'prores') {
// Apple ProRes 4444 with True 10-bit Alpha for DaVinci Resolve / Premiere
// MUST use 'prores_ks' because native 'prores' ignores/drops the alpha channel (producing black background)
ffmpegArgs.push(
'-c:v', 'prores_ks',
'-profile:v', '4',
'-qscale:v', '11',
'-pix_fmt', 'yuva444p10le',
'-vendor', 'apl0',
'-threads', '0',
'-c:a', 'pcm_s16le',
'-shortest',
outputPath
);
} else if (format === 'qtrle') {
ffmpegArgs.push(
'-c:v', 'qtrle',
'-pix_fmt', 'argb',
'-threads', '0',
'-c:a', 'pcm_s16le',
'-shortest',
outputPath
);
} else if (format === 'webm_alpha') {
ffmpegArgs.push(
'-c:v', 'copy',
'-c:a', 'libopus',
'-b:a', '320k',
'-shortest',
outputPath
);
} else {
ffmpegArgs.push(
'-c:v', 'libx264',
'-crf', '14',
'-preset', 'fast',
'-pix_fmt', 'yuv420p',
'-threads', '0',
'-c:a', 'aac',
'-b:a', '320k',
'-shortest',
outputPath
);
}
// 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 = '';
@@ -195,11 +221,11 @@ app.post('/api/export/transcode', upload.fields([
const session = {
id: sessionId,
title: title,
createdAt: Date.now(),
videoPath,
audioPath,
outputPath,
outputFilename,
format,
formats,
ffmpegProc,
errorLog: '',
done: false,
@@ -220,8 +246,7 @@ app.post('/api/export/transcode', upload.fields([
: `FFmpeg Transcode Fehler (Code ${code}): ${errorLog.slice(-400)}`;
console.error(`[${sessionId}] ❌ Transcode failed:`, session.error);
} else {
const sizeMb = fs.existsSync(outputPath) ? (fs.statSync(outputPath).size / 1024 / 1024).toFixed(1) : '0';
console.log(`[${sessionId}] ✅ Transcode complete! Size: ${sizeMb} MB`);
console.log(`[${sessionId}] ✅ Transcode complete! (All Formats)`);
}
});
@@ -231,7 +256,7 @@ app.post('/api/export/transcode', upload.fields([
});
activeSessions.set(sessionId, session);
res.json({ sessionId, outputFilename });
res.json({ sessionId, format });
} catch (err) {
console.error('Error starting transcode:', err);
res.status(500).json({ error: err.message });
@@ -257,15 +282,31 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
const title = (req.body.title || 'Visualizer').replace(/[^a-zA-Z0-9_-]/g, '_');
const ext = (format === 'prores' || format === 'qtrle') ? 'mov' : (format === 'webm_alpha' ? 'webm' : 'mp4');
let formatSuffix = 'Visualizer';
if (format === 'prores') formatSuffix = 'ProRes4444_Alpha';
else if (format === 'qtrle') formatSuffix = 'Lossless_QTRLE_Alpha';
else if (format === 'webm_alpha') formatSuffix = 'Alpha_DaVinci';
const outputFilename = `${title}_${formatSuffix}.${ext}`;
const outputPath = path.join(TEMP_DIR, `${sessionId}_${outputFilename}`);
const formats = {
mp4: {
filename: `${title}_Master.mp4`,
path: path.join(TEMP_DIR, `${sessionId}_${title}_Master.mp4`),
formatId: 'mp4'
},
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'
}
};
console.log(`[${sessionId}] 🚀 Starting Render: ${format.toUpperCase()} ${width}x${height} @ ${fps}FPS -> ${outputFilename}`);
console.log(`[${sessionId}] 🚀 Starting Render: ALL FORMATS ${width}x${height} @ ${fps}FPS`);
const ffmpegArgs = [
'-y',
@@ -277,61 +318,35 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
'-i', audioPath
];
if (format === 'prores') {
// Apple ProRes 4444 with True 10-bit Alpha for DaVinci Resolve / Premiere
// MUST use 'prores_ks' because native 'prores' ignores/drops the alpha channel (producing black background)
ffmpegArgs.push(
'-c:v', 'prores_ks',
'-profile:v', '4',
'-qscale:v', '11',
'-pix_fmt', 'yuva444p10le',
'-vendor', 'apl0',
'-threads', '0',
'-c:a', 'pcm_s16le',
'-shortest',
outputPath
);
} else if (format === 'qtrle') {
// QuickTime Animation RLE - 100% Lossless RGBA (Direct DaVinci Resolve native)
ffmpegArgs.push(
'-c:v', 'qtrle',
'-pix_fmt', 'argb',
'-threads', '0',
'-c:a', 'pcm_s16le',
'-shortest',
outputPath
);
} else if (format === 'webm_alpha') {
// WebM VP9 with True Alpha (Lossless/High-bitrate studio quality)
ffmpegArgs.push(
'-c:v', 'libvpx-vp9',
'-pix_fmt', 'yuva420p',
'-b:v', '35M',
'-minrate', '20M',
'-maxrate', '50M',
'-speed', '2',
'-threads', '8',
'-row-mt', '1',
'-auto-alt-ref', '0',
'-c:a', 'libopus',
'-b:a', '320k',
'-shortest',
outputPath
);
} else {
// MP4 H.264 (Broadcast quality CRF 14, high profile, yuv420p)
ffmpegArgs.push(
'-c:v', 'libx264',
'-crf', '14',
'-preset', 'fast',
'-pix_fmt', 'yuv420p',
'-threads', '0',
'-c:a', 'aac',
'-b:a', '320k',
'-shortest',
outputPath
);
}
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', '8', '-row-mt', '1', '-auto-alt-ref', '0',
'-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',
'-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) => {
@@ -344,10 +359,10 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
const session = {
id: sessionId,
title: title,
createdAt: Date.now(),
audioPath,
outputPath,
outputFilename,
format,
formats,
ffmpegProc,
errorLog: '',
done: false,
@@ -360,8 +375,7 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
session.error = `FFmpeg Fehler (Code ${code}): ${errorLog.slice(-400)}`;
console.error(`[${sessionId}] ❌ Render failed:`, session.error);
} else {
const sizeMb = fs.existsSync(outputPath) ? (fs.statSync(outputPath).size / 1024 / 1024).toFixed(1) : '0';
console.log(`[${sessionId}] ✅ Render complete! Output size: ${sizeMb} MB`);
console.log(`[${sessionId}] ✅ Render complete! (All Formats)`);
}
});
@@ -371,7 +385,7 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => {
});
activeSessions.set(sessionId, session);
res.json({ sessionId, outputFilename });
res.json({ sessionId, format });
} catch (err) {
console.error('Error starting export:', err);
res.status(500).json({ error: err.message });
@@ -427,21 +441,17 @@ 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 (fs.existsSync(session.outputPath)) {
sizeBytes = fs.statSync(session.outputPath).size;
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) {
if (!fs.existsSync(session.outputPath)) {
return res.status(500).json({ status: 'error', error: 'Ausgabedatei wurde nicht gefunden.' });
}
return res.json({
status: 'ready',
downloadUrl: `/api/export/download/${session.id}`,
filename: session.outputFilename,
sizeBytes: sizeBytes
});
}
@@ -452,33 +462,29 @@ app.get('/api/export/status/:id', (req, res) => {
});
});
// Download exported video file
app.get('/api/export/download/:id', (req, res) => {
// Download exported video file for a specific format
app.get('/api/export/download/:id/:formatId', (req, res) => {
const session = activeSessions.get(req.params.id);
if (!session || !fs.existsSync(session.outputPath)) {
const formatId = req.params.formatId;
if (!session || !session.formats || !session.formats[formatId] || !fs.existsSync(session.formats[formatId].path)) {
return res.status(404).send('Datei nicht gefunden oder abgelaufen.');
}
const stat = fs.statSync(session.outputPath);
const mime = session.format.startsWith('prores') || session.format === 'qtrle'
const formatObj = session.formats[formatId];
const stat = fs.statSync(formatObj.path);
const mime = formatId === 'prores' || formatId === 'qtrle'
? 'video/quicktime'
: (session.format === 'webm_alpha' ? 'video/webm' : 'video/mp4');
: (formatId === 'webm_alpha' ? 'video/webm' : 'video/mp4');
res.setHeader('Content-Type', mime);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Disposition', `attachment; filename="${session.outputFilename}"`);
res.setHeader('Content-Disposition', `attachment; filename="${formatObj.filename}"`);
const stream = fs.createReadStream(session.outputPath);
const stream = fs.createReadStream(formatObj.path);
stream.pipe(res);
stream.on('end', () => {
setTimeout(() => {
try {
if (fs.existsSync(session.outputPath)) fs.unlinkSync(session.outputPath);
cleanupSession(session.id);
} catch (e) {}
}, 300000);
});
// Notice: We don't delete the file anymore so it's available in history
});
// Cancel export session