fix: video size error handling — validation, timeouts, failure surfacing
Backend: - Increase ffmpeg/ffprobe timeouts from 15s to 60s for large videos - Classify processing errors (timeout, OOM, corrupt) with user-facing messages - Emit failure details via media:job:update socket event (error field) - Bump refboard container memory 512M → 1G Frontend: - Client-side file size validation (200MB) before upload starts - Oversized files show immediate error in upload manager, skip upload - Handle media:job:update status='failed' — surface error in upload panel - Add processingFailed() to UploadManager for video processing errors
This commit is contained in:
@@ -85,14 +85,14 @@ async function processJob(job) {
|
||||
const image = getImage(imageId);
|
||||
if (!image) {
|
||||
updateMediaJob(jobId, { status: 'failed', error: 'Image record not found' });
|
||||
emitJobUpdate(boardId, jobId, imageId, 'failed');
|
||||
emitJobFailed(boardId, jobId, imageId, 'Image record not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoBuffer = await fetchFromMinio(image.minio_path);
|
||||
if (!videoBuffer) {
|
||||
updateMediaJob(jobId, { status: 'failed', error: 'Failed to fetch video from storage' });
|
||||
emitJobUpdate(boardId, jobId, imageId, 'failed');
|
||||
emitJobFailed(boardId, jobId, imageId, 'Failed to fetch video from storage');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,13 +131,23 @@ async function processJob(job) {
|
||||
} catch (err) {
|
||||
console.error('[media-worker] Job %s failed:', jobId, err.message);
|
||||
|
||||
// Classify error for user-facing message
|
||||
let userError = 'Video processing failed';
|
||||
if (err.killed || err.signal === 'SIGTERM') {
|
||||
userError = 'Video processing timed out — file may be too large or corrupt';
|
||||
} else if (err.message?.includes('ENOMEM') || err.message?.includes('Cannot allocate')) {
|
||||
userError = 'Out of memory — video file is too large to process';
|
||||
} else if (err.message?.includes('Invalid data')) {
|
||||
userError = 'Invalid or corrupt video file';
|
||||
}
|
||||
|
||||
const attempts = (job.attempts || 0) + 1;
|
||||
if (attempts < 3) {
|
||||
updateMediaJob(jobId, { status: 'retry', attempts, error: err.message });
|
||||
emitJobUpdate(boardId, jobId, imageId, 'retry');
|
||||
} else {
|
||||
updateMediaJob(jobId, { status: 'failed', attempts, error: err.message });
|
||||
emitJobUpdate(boardId, jobId, imageId, 'failed');
|
||||
emitJobFailed(boardId, jobId, imageId, userError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,4 +183,11 @@ function emitJobUpdate(boardId, jobId, imageId, status, result) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a job failure with error details to the board room.
|
||||
*/
|
||||
function emitJobFailed(boardId, jobId, imageId, error) {
|
||||
emitJobUpdate(boardId, jobId, imageId, 'failed', { error });
|
||||
}
|
||||
|
||||
module.exports = { startMediaWorker, stopMediaWorker };
|
||||
|
||||
@@ -34,7 +34,7 @@ function probeVideo(buffer) {
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
tmpFile,
|
||||
], { timeout: 15000 }, (err, stdout) => {
|
||||
], { timeout: 60000 }, (err, stdout) => {
|
||||
cleanup(tmpFile, tmpDir);
|
||||
if (err) {
|
||||
console.warn('[video-utils] ffprobe failed:', err.message);
|
||||
@@ -91,7 +91,7 @@ function extractPoster(buffer) {
|
||||
'-q:v', '3', // JPEG quality (2=best, 31=worst)
|
||||
'-y', // overwrite
|
||||
tmpOutput,
|
||||
], { timeout: 15000 }, (err) => {
|
||||
], { timeout: 60000 }, (err) => {
|
||||
if (err) {
|
||||
console.warn('[video-utils] ffmpeg poster extraction failed:', err.message);
|
||||
cleanup(tmpInput, tmpDir);
|
||||
|
||||
Reference in New Issue
Block a user