diff --git a/public/app.js b/public/app.js index 192d10c..a0caed5 100644 --- a/public/app.js +++ b/public/app.js @@ -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 = '
Noch keine Renderings im Verlauf.
'; + return; + } + + listContainer.innerHTML = ''; + history.forEach(sess => { + const item = document.createElement('div'); + item.className = 'export-history-item'; + + let statusHtml = sess.error + ? `❌ Fehler` + : (sess.done ? `✅ Fertig` : `🔄 Rendert...`); + + const d = new Date(sess.createdAt); + const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); + + let downloadsHtml = ''; + if (sess.done && !sess.error && sess.formats) { + downloadsHtml = ` +
+ ⬇️ MP4 + ⬇️ WebM (Alpha) + ⬇️ ProRes + ⬇️ QTRLE +
+ `; + } else if (!sess.done && !sess.error) { + downloadsHtml = `
Dieses Rendering läuft gerade...
`; + } + + item.innerHTML = ` +
+
${sess.title}
+
${statusHtml}
+
+
${dateStr} | ID: ${sess.id.substring(0, 15)}...
+ ${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'); diff --git a/public/index.html b/public/index.html index 5e97cbb..e3486eb 100644 --- a/public/index.html +++ b/public/index.html @@ -1236,6 +1236,19 @@ + + +
+
+

Render Verlauf

+ +
+
+
Noch keine Renderings im Verlauf.
+
+
diff --git a/public/style.css b/public/style.css index 6e8ce5b..694090e 100644 --- a/public/style.css +++ b/public/style.css @@ -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; +} diff --git a/server.mjs b/server.mjs index 9dc34dd..cc06477 100644 --- a/server.mjs +++ b/server.mjs @@ -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