openvibers/openvibe.media Commits

A recording under 1 KB has no picture to take: its thumbnail job fails for good (404), not as retryable (#282)

OP OpenVibers committed 11 days ago · a75eb3f3c2 · 3 files, +11 −2

* A recording under 1 KB has no picture to take: its thumbnail job fails for good (404), not as retryable

* jobs test: a stand-in recording of a real size (under 1 KB the thumbnail job now fails for good)

Changes

ddiff --git a/server/jobs/thumbnail.js b/server/jobs/thumbnail.js
iindex 27e2c54..e341f40 100644
---- a/server/jobs/thumbnail.js
++++ b/server/jobs/thumbnail.js
@@@ -73,6 +73,9 @@ async function run(job) {
let size = 0;
try { size = require('fs').statSync(source.value).size; } catch { size = 0; }
if (!size) throw new JobError('media_unavailable', 'The media file is empty (0 bytes)', { permanent: true, status: 404 });
++ // A few hundred bytes hold a container header and no frame (VOD 1803: 526 bytes for "51 seconds", health "ok"):
++ // ffmpeg fails every time, and a retryable failure had every page that shows it ask again (2026-11-02).
++ if (size < TOO_SMALL_FOR_A_FRAME) throw new JobError('media_unavailable', `The media file is too small to hold a picture (${size} bytes)`, { permanent: true, status: 404 });
}
// An audio-only source (a browser broadcast with no camera, Media#240) has no frame to take. Its picture is the
// streamer's avatar card, made once here and stored like any thumbnail, and the row is marked audio_only. This used
@@@ -96,6 +99,9 @@ async function run(job) {
return { url, kind, id };
}
++// Under 1 KB is a container header at most, with no frame in it: no picture to take.
++const TOO_SMALL_FOR_A_FRAME = 1024;
++
module.exports = {
spec: { lane: 'light', maxAttempts: 2, timeoutMs: 2 * 60 * 1000, needsObject: false, backoffS: () => 20, validate, run },
targetOf,
ddiff --git a/test/jobs.test.js b/test/jobs.test.js
iindex ee6a032..9a3beee 100644
---- a/test/jobs.test.js
++++ b/test/jobs.test.js
@@@ -246,7 +246,8 @@ const http = require('http');
const thumbs = require('../server/thumbnails/thumbnail-service');
let gens = 0;
thumbs.generateVodThumbnail = async (id) => { gens++; await sleep(150); await db.run('UPDATE vods SET thumbnail_url = ? WHERE id = ?', [`/t/vod-${id}-1.jpg`, id]); return `/t/vod-${id}-1.jpg`; };
-- fs.writeFileSync(path.join(process.env.VOD_PATH, 'rec-1.webm'), 'not really a video');
++ // A stand-in recording of a real size: under 1 KB a file has no picture to take and the job fails for good.
++ fs.writeFileSync(path.join(process.env.VOD_PATH, 'rec-1.webm'), Buffer.alloc(4096, 'not really a video '));
await conn.prepare("INSERT INTO vods (id, app_id, user_id, title, file_path, is_public, duration_seconds) OVERRIDING SYSTEM VALUE VALUES (1, 'live', 5, 'A', ?, 1, 60) RETURNING id").run(path.join(process.env.VOD_PATH, 'rec-1.webm'));
await conn.prepare("INSERT INTO vods (id, app_id, user_id, title, file_path, is_public, duration_seconds) OVERRIDING SYSTEM VALUE VALUES (2, 'live', 5, 'B', ?, 1, 60) RETURNING id").run(path.join(process.env.VOD_PATH, 'gone.webm'));
await require('../server/objects/model').sync('vod', 1);
ddiff --git a/test/thumbnail-unavailable.test.js b/test/thumbnail-unavailable.test.js
iindex e113c7b..fe092ea 100644
---- a/test/thumbnail-unavailable.test.js
++++ b/test/thumbnail-unavailable.test.js
@@@ -31,6 +31,8 @@ const express = require('express');
const marked = await insVod(path.join(V, 'zero-marked.mp4'), 'zero_byte'); // as on production
const unscanned = await insVod(path.join(V, 'zero-unscanned.mp4'), 'unknown'); // empty, not scanned yet
const missing = await insVod(path.join(V, 'gone.mp4'), 'ok'); // no file, not offloaded
++ fs.writeFileSync(path.join(V, 'tiny.mp4'), Buffer.alloc(526, 1));
++ const tiny = await insVod(path.join(V, 'tiny.mp4'), 'ok'); // 526 bytes, health ok (VOD 1803)
const app = express();
app.use(express.json());
@@@ -45,7 +47,7 @@ const express = require('express');
(async () => {
await new Promise((r) => (server.listening ? r() : server.once('listening', r)));
-- for (const id of [marked, unscanned, missing]) {
++ for (const id of [marked, unscanned, missing, tiny]) {
const t0 = Date.now();
const r = await post(`/api/v1/live/thumbnails/vod/${id}`);
assert.strictEqual(r.status, 404, `vod ${id}: ${JSON.stringify(r.body)}`);