Video: async conversion with live progress polling

- /convert now returns immediately, ffmpeg runs in background
- Client polls /progress every second for live progress %
- Progress endpoint returns downloadUrl/outputSize when done
- UI shows real-time percentage during conversion
This commit is contained in:
treamz 2026-03-22 00:20:04 +03:00
parent 7f2a6f208f
commit 6aca7e9e47
2 changed files with 27 additions and 24 deletions

View File

@ -939,15 +939,8 @@
throw new Error(err.error || 'Ошибка запуска');
}
const data = await res.json();
// Server may return immediately (sync) or poll (async)
if (data.status === 'done' || data.downloadUrl) {
setProgressUI(100, 'Готово!');
onConversionDone(data);
} else {
startPolling();
}
// Server starts processing in background, poll for progress
startPolling();
} catch (err) {
showConversionError(err.message || 'Ошибка конвертации');
}

View File

@ -184,20 +184,21 @@ router.post('/convert', express.json(), async (req, res) => {
args.push(outFile);
await runFFmpeg(args, jobId);
job.status = 'done';
job.progress = 100;
// Start ffmpeg in background — respond immediately
res.json({ status: 'processing' });
const outStat = fs.statSync(outFile);
const savings = job.size > 0 ? Math.round((1 - outStat.size / job.size) * 100) : 0;
log.info(`Video ${mode}: ${job.originalName}${format} (${(outStat.size / 1024 / 1024).toFixed(1)}MB, ${savings}% saved)`);
res.json({
status: 'done',
downloadUrl: `/video/download/${jobId}${outExt}`,
outputSize: outStat.size,
savings,
runFFmpeg(args, jobId).then(() => {
job.status = 'done';
job.progress = 100;
const outStat = fs.statSync(outFile);
job.outputSize = outStat.size;
job.savings = job.size > 0 ? Math.round((1 - outStat.size / job.size) * 100) : 0;
job.downloadUrl = `/video/download/${jobId}${outExt}`;
log.info(`Video ${mode}: ${job.originalName}${format} (${(outStat.size / 1024 / 1024).toFixed(1)}MB, ${job.savings}% saved)`);
}).catch(err => {
job.status = 'error';
job.error = err.message.slice(0, 200);
log.error('Video convert error', { error: err.message });
});
} catch (err) {
job.status = 'error';
@ -206,11 +207,20 @@ router.post('/convert', express.json(), async (req, res) => {
}
});
// Progress
// Progress (returns downloadUrl when done)
router.get('/progress/:jobId', (req, res) => {
const job = jobs.get(req.params.jobId);
if (!job) return res.status(404).json({ error: 'Not found' });
res.json({ status: job.status, progress: job.progress });
const result = { status: job.status, progress: job.progress };
if (job.status === 'done') {
result.downloadUrl = job.downloadUrl;
result.outputSize = job.outputSize;
result.savings = job.savings;
}
if (job.status === 'error') {
result.error = job.error || 'Ошибка обработки';
}
res.json(result);
});
// Download result