feat(export): sequential multi-format rendering to save RAM
This commit is contained in:
1 parent
1acafdec0b
commit
6e3f2fba4d
3 files changed
+135
-204
No files matched your search
+4
-38
@@ -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;
|
||||
|
||||
+4
-16
@@ -1176,24 +1176,12 @@
|
||||
<div class="export-engine-badge" id="exportEngineBadge">
|
||||
<span class="badge-icon">⚡</span>
|
||||
<div class="badge-content">
|
||||
<strong id="exportEngineTitle">Client-GPU Export (Empfohlen)</strong>
|
||||
<span id="exportEngineDesc">Rendert direkt im Browser bei 60–120+ FPS mit 0 MB Server-Upload.</span>
|
||||
<strong id="exportEngineTitle">Multi-Format Server Export</strong>
|
||||
<span id="exportEngineDesc">Rendert automatisch ProRes, WebM Alpha, QTRLE und MP4 im Hintergrund.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 12px;">
|
||||
<label for="exportFormat">Format & Render-Engine</label>
|
||||
<select id="exportFormat" class="select-input">
|
||||
<optgroup label="⚡ Client-GPU Export (Blitzschnell im Browser & 0 MB Upload)">
|
||||
<option value="webm_alpha" selected>WebM VP9 (.webm Alpha - DaVinci Resolve Transparenz)</option>
|
||||
<option value="mp4">MP4 Video (.mp4 Full HD / 4K - YouTube & Social Media)</option>
|
||||
</optgroup>
|
||||
<optgroup label="🖥️ Studio Master Server Export (FFmpeg)">
|
||||
<option value="prores">Apple ProRes 4444 (.mov 10-Bit Alpha - DaVinci Studio Master)</option>
|
||||
<option value="qtrle">QuickTime Animation RLE (.mov - 100% Lossless Alpha)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="exportFormat" value="prores">
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
|
||||
+127
-150
@@ -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...'
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user