openvibers/openvibe.media Commits
Media: an audio-only recording's thumbnail answers no_video at once (no new job each time) (#127)
Changes
@@@ -83,6 +83,13 @@ router.post('/:kind/:id', tenantAuth({ verb: 'write', allowUser: true }), uploadreturn res.json({ url });}++ // A recording already found to have no video (audio only) has no frame to take, and its file never changes after++ // it is finished: answer that at once instead of running the same failing job again (every visitor's page asked,++ // 56 failed jobs in two days, 2026-10-15). Uses idx_media_jobs_object.++ if (row.object_id && !row.is_recording && await db.get("SELECT 1 AS x FROM media_jobs WHERE object_id = ? AND job_type = 'thumbnail.regenerate' AND status = 'failed' AND error_code = 'no_video' LIMIT 1", [row.object_id])) {++ return res.status(404).json({ error: 'The recording has no video', code: 'media.no_video' });++ }++// Generate from the media file (local or presigned remote) as a thumbnail.regenerate job.const queue = require('../jobs/queue');const worker = require('../jobs/worker');@@@ -263,6 +263,17 @@ const http = require('http');r = await call('POST', '/api/v1/live/thumbnails/vod/2');assert.deepStrictEqual([r.status, r.body.error], [404, 'Media file unavailable'], 'no media is still a 404');assert.strictEqual((await queue.get(r.body.job_id)).error_code, 'media_unavailable');++ // An audio-only recording failed once with no_video: later asks answer at once, with no new job (every visitor's++ // page asked again before, 2026-10-15).++ await conn.prepare("INSERT INTO vods (id, app_id, user_id, title, file_path, is_public, duration_seconds) OVERRIDING SYSTEM VALUE VALUES (3, 'live', 5, 'C', ?, 1, 60) RETURNING id").run(path.join(process.env.VOD_PATH, 'rec-1.webm'));++ await require('../server/objects/model').sync('vod', 3);++ const audioObj = (await db.get('SELECT object_id FROM vods WHERE id = 3')).object_id;++ const failed = await queue.enqueue({ appId: 'live', type: 'thumbnail.regenerate', objectId: audioObj, params: { kind: 'vod', id: 3 }, maxAttempts: 1, createdBy: 'app:live' });++ await db.run("UPDATE media_jobs SET status = 'failed', error_code = 'no_video', error = 'The recording has no video (audio only)' WHERE id = ?", [failed.job.id]);++ const jobsBefore = (await db.get("SELECT COUNT(*) AS n FROM media_jobs WHERE object_id = ?", [audioObj])).n;++ r = await call('POST', '/api/v1/live/thumbnails/vod/3');++ assert.deepStrictEqual([r.status, r.body.code], [404, 'media.no_video'], 'a known audio-only recording answers no_video');++ assert.strictEqual((await db.get("SELECT COUNT(*) AS n FROM media_jobs WHERE object_id = ?", [audioObj])).n, jobsBefore, 'and runs no new job');r = await call('POST', '/api/v1/live/thumbnails/vod/1?async=1');assert.strictEqual(r.status, 202);assert.ok(await waitFor(() => false, 50) || true);
@@ -83,6 +83,13 @@ router.post('/:kind/:id', tenantAuth({ verb: 'write', allowUser: true }), upload
@@ -83,6 +83,13 @@ router.post('/:kind/:id', tenantAuth({ verb: 'write', allowUser: true }), upload
return res.json({ url });
return res.json({ url });
}
}
+ // A recording already found to have no video (audio only) has no frame to take, and its file never changes after
+ // it is finished: answer that at once instead of running the same failing job again (every visitor's page asked,
+ // 56 failed jobs in two days, 2026-10-15). Uses idx_media_jobs_object.
+ if (row.object_id && !row.is_recording && await db.get("SELECT 1 AS x FROM media_jobs WHERE object_id = ? AND job_type = 'thumbnail.regenerate' AND status = 'failed' AND error_code = 'no_video' LIMIT 1", [row.object_id])) {
+ return res.status(404).json({ error: 'The recording has no video', code: 'media.no_video' });
+ }
+
// Generate from the media file (local or presigned remote) as a thumbnail.regenerate job.
// Generate from the media file (local or presigned remote) as a thumbnail.regenerate job.
const queue = require('../jobs/queue');
const queue = require('../jobs/queue');
const worker = require('../jobs/worker');
const worker = require('../jobs/worker');
@@ -263,6 +263,17 @@ const http = require('http');
@@ -263,6 +263,17 @@ const http = require('http');
r = await call('POST', '/api/v1/live/thumbnails/vod/2');
r = await call('POST', '/api/v1/live/thumbnails/vod/2');
assert.deepStrictEqual([r.status, r.body.error], [404, 'Media file unavailable'], 'no media is still a 404');
assert.deepStrictEqual([r.status, r.body.error], [404, 'Media file unavailable'], 'no media is still a 404');
assert.strictEqual((await queue.get(r.body.job_id)).error_code, 'media_unavailable');
assert.strictEqual((await queue.get(r.body.job_id)).error_code, 'media_unavailable');
+ // An audio-only recording failed once with no_video: later asks answer at once, with no new job (every visitor's
+ // page asked again before, 2026-10-15).
+ await conn.prepare("INSERT INTO vods (id, app_id, user_id, title, file_path, is_public, duration_seconds) OVERRIDING SYSTEM VALUE VALUES (3, 'live', 5, 'C', ?, 1, 60) RETURNING id").run(path.join(process.env.VOD_PATH, 'rec-1.webm'));
+ await require('../server/objects/model').sync('vod', 3);
+ const audioObj = (await db.get('SELECT object_id FROM vods WHERE id = 3')).object_id;
+ const failed = await queue.enqueue({ appId: 'live', type: 'thumbnail.regenerate', objectId: audioObj, params: { kind: 'vod', id: 3 }, maxAttempts: 1, createdBy: 'app:live' });
+ await db.run("UPDATE media_jobs SET status = 'failed', error_code = 'no_video', error = 'The recording has no video (audio only)' WHERE id = ?", [failed.job.id]);
+ const jobsBefore = (await db.get("SELECT COUNT(*) AS n FROM media_jobs WHERE object_id = ?", [audioObj])).n;
+ r = await call('POST', '/api/v1/live/thumbnails/vod/3');
+ assert.deepStrictEqual([r.status, r.body.code], [404, 'media.no_video'], 'a known audio-only recording answers no_video');
+ assert.strictEqual((await db.get("SELECT COUNT(*) AS n FROM media_jobs WHERE object_id = ?", [audioObj])).n, jobsBefore, 'and runs no new job');
r = await call('POST', '/api/v1/live/thumbnails/vod/1?async=1');
r = await call('POST', '/api/v1/live/thumbnails/vod/1?async=1');
assert.strictEqual(r.status, 202);
assert.strictEqual(r.status, 202);
assert.ok(await waitFor(() => false, 50) || true);
assert.ok(await waitFor(() => false, 50) || true);