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)
Changes
@@@ -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,@@@ -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);@@@ -31,6 +31,8 @@ const express = require('express');const marked = await insVod(path.join(V, 'zero-marked.mp4'), 'zero_byte'); // as on productionconst unscanned = await insVod(path.join(V, 'zero-unscanned.mp4'), 'unknown'); // empty, not scanned yetconst 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)}`);
@@ -73,6 +73,9 @@ async function run(job) {
@@ -73,6 +73,9 @@ async function run(job) {
let size = 0;
let size = 0;
try { size = require('fs').statSync(source.value).size; } catch { 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 });
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
// 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
// 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) {
@@ -96,6 +99,9 @@ async function run(job) {
return { url, kind, id };
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 = {
module.exports = {
spec: { lane: 'light', maxAttempts: 2, timeoutMs: 2 * 60 * 1000, needsObject: false, backoffS: () => 20, validate, run },
spec: { lane: 'light', maxAttempts: 2, timeoutMs: 2 * 60 * 1000, needsObject: false, backoffS: () => 20, validate, run },
targetOf,
targetOf,
@@ -246,7 +246,8 @@ const http = require('http');
@@ -246,7 +246,8 @@ const http = require('http');
const thumbs = require('../server/thumbnails/thumbnail-service');
const thumbs = require('../server/thumbnails/thumbnail-service');
let gens = 0;
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`; };
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 (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 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);
await require('../server/objects/model').sync('vod', 1);
@@ -31,6 +31,8 @@ const express = require('express');
@@ -31,6 +31,8 @@ const express = require('express');
const marked = await insVod(path.join(V, 'zero-marked.mp4'), 'zero_byte'); // as on production
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 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
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();
const app = express();
app.use(express.json());
app.use(express.json());
@@ -45,7 +47,7 @@ const express = require('express');
@@ -45,7 +47,7 @@ const express = require('express');
(async () => {
(async () => {
await new Promise((r) => (server.listening ? r() : server.once('listening', r)));
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 t0 = Date.now();
const r = await post(`/api/v1/live/thumbnails/vod/${id}`);
const r = await post(`/api/v1/live/thumbnails/vod/${id}`);
assert.strictEqual(r.status, 404, `vod ${id}: ${JSON.stringify(r.body)}`);
assert.strictEqual(r.status, 404, `vod ${id}: ${JSON.stringify(r.body)}`);