openvibers/openvibe.live Commits
Thumbnails: an audio-only recording (Media's media.no_video) is asked about once a day, not hourly (three VODs cost Media a job an hour each) (#198)
Changes
@@@ -61,6 +61,9 @@ router.post('/live/:streamId', requireAuth, upload.single('thumbnail'), async (r// 429 used to reach every visitor's console (the browser check of release 977747d7, 2026-10-12).const NO_PICTURE_MS = 60 * 60 * 1000;const MEDIA_BUSY_MS = 5 * 60 * 1000;++// An audio-only recording (Media's 404 media.no_video) will never have a picture: asked again once a day, not hourly++// (VODs 5703, 5725 and 5770 cost Media a job an hour each on 2026-10-17).++const NO_VIDEO_MS = 24 * 60 * 60 * 1000;const noPicture = new Map(); // `${kind}:${id}` → until (ms), oldest firstfunction noPictureKnown(key) {const until = noPicture.get(key);@@@ -92,7 +95,8 @@ async function generateFor(kind, id, req, res) {const failed = err && err.name === 'MediaApiError' && err.status >= 500;if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429 || failed))) throw err;if (failed) console.warn(`[Thumbnails] Media's thumbnail job failed for ${key}: ${err.status}${err.body && err.body.job_id ? ` (${err.body.job_id})` : ''}`);-- rememberNoPicture(key, err.status === 404 ? NO_PICTURE_MS : MEDIA_BUSY_MS);++ const noVideo = err.status === 404 && err.body && err.body.code === 'media.no_video';++ rememberNoPicture(key, noVideo ? NO_VIDEO_MS : err.status === 404 ? NO_PICTURE_MS : MEDIA_BUSY_MS);return res.json({ thumbnail_url: null });}const url = media.publicUrl(out?.url || meta.thumbnail_url);@@@ -49,6 +49,28 @@ console.warn = () => {};await post('/api/thumbnails/generate/clip/456');assert.strictEqual(asked, 2, 'a failed job is remembered too');++ // An audio-only recording (404 media.no_video) is remembered for a day; another 404 for an hour.++ const realNow = Date.now;++ const notFound = (code) => async () => {++ asked++;++ const err = new Error('no picture');++ Object.assign(err, { name: 'MediaApiError', status: 404, body: { error: 'no picture', code } });++ throw err;++ };++ media.generateThumbnail = notFound('media.no_video');++ await post('/api/thumbnails/generate/clip/457');++ media.generateThumbnail = notFound('media.not_found');++ await post('/api/thumbnails/generate/clip/458');++ assert.strictEqual(asked, 4);++ Date.now = () => realNow() + 2 * 60 * 60 * 1000; // two hours later++ media.generateThumbnail = notFound('media.no_video');++ await post('/api/thumbnails/generate/clip/457');++ assert.strictEqual(asked, 4, 'audio only: still remembered after two hours');++ media.generateThumbnail = notFound('media.not_found');++ await post('/api/thumbnails/generate/clip/458');++ assert.strictEqual(asked, 5, 'another 404: asked again after an hour');++ Date.now = realNow;++server.close();console.log('thumbnail no-picture: all checks passed');process.exit(0);
@@ -61,6 +61,9 @@ router.post('/live/:streamId', requireAuth, upload.single('thumbnail'), async (r
@@ -61,6 +61,9 @@ router.post('/live/:streamId', requireAuth, upload.single('thumbnail'), async (r
// 429 used to reach every visitor's console (the browser check of release 977747d7, 2026-10-12).
// 429 used to reach every visitor's console (the browser check of release 977747d7, 2026-10-12).
const NO_PICTURE_MS = 60 * 60 * 1000;
const NO_PICTURE_MS = 60 * 60 * 1000;
const MEDIA_BUSY_MS = 5 * 60 * 1000;
const MEDIA_BUSY_MS = 5 * 60 * 1000;
+// An audio-only recording (Media's 404 media.no_video) will never have a picture: asked again once a day, not hourly
+// (VODs 5703, 5725 and 5770 cost Media a job an hour each on 2026-10-17).
+const NO_VIDEO_MS = 24 * 60 * 60 * 1000;
const noPicture = new Map(); // `${kind}:${id}` → until (ms), oldest first
const noPicture = new Map(); // `${kind}:${id}` → until (ms), oldest first
function noPictureKnown(key) {
function noPictureKnown(key) {
const until = noPicture.get(key);
const until = noPicture.get(key);
@@ -92,7 +95,8 @@ async function generateFor(kind, id, req, res) {
@@ -92,7 +95,8 @@ async function generateFor(kind, id, req, res) {
const failed = err && err.name === 'MediaApiError' && err.status >= 500;
const failed = err && err.name === 'MediaApiError' && err.status >= 500;
if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429 || failed))) throw err;
if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429 || failed))) throw err;
if (failed) console.warn(`[Thumbnails] Media's thumbnail job failed for ${key}: ${err.status}${err.body && err.body.job_id ? ` (${err.body.job_id})` : ''}`);
if (failed) console.warn(`[Thumbnails] Media's thumbnail job failed for ${key}: ${err.status}${err.body && err.body.job_id ? ` (${err.body.job_id})` : ''}`);
- rememberNoPicture(key, err.status === 404 ? NO_PICTURE_MS : MEDIA_BUSY_MS);
+ const noVideo = err.status === 404 && err.body && err.body.code === 'media.no_video';
+ rememberNoPicture(key, noVideo ? NO_VIDEO_MS : err.status === 404 ? NO_PICTURE_MS : MEDIA_BUSY_MS);
return res.json({ thumbnail_url: null });
return res.json({ thumbnail_url: null });
}
}
const url = media.publicUrl(out?.url || meta.thumbnail_url);
const url = media.publicUrl(out?.url || meta.thumbnail_url);
@@ -49,6 +49,28 @@ console.warn = () => {};
@@ -49,6 +49,28 @@ console.warn = () => {};
await post('/api/thumbnails/generate/clip/456');
await post('/api/thumbnails/generate/clip/456');
assert.strictEqual(asked, 2, 'a failed job is remembered too');
assert.strictEqual(asked, 2, 'a failed job is remembered too');
+ // An audio-only recording (404 media.no_video) is remembered for a day; another 404 for an hour.
+ const realNow = Date.now;
+ const notFound = (code) => async () => {
+ asked++;
+ const err = new Error('no picture');
+ Object.assign(err, { name: 'MediaApiError', status: 404, body: { error: 'no picture', code } });
+ throw err;
+ };
+ media.generateThumbnail = notFound('media.no_video');
+ await post('/api/thumbnails/generate/clip/457');
+ media.generateThumbnail = notFound('media.not_found');
+ await post('/api/thumbnails/generate/clip/458');
+ assert.strictEqual(asked, 4);
+ Date.now = () => realNow() + 2 * 60 * 60 * 1000; // two hours later
+ media.generateThumbnail = notFound('media.no_video');
+ await post('/api/thumbnails/generate/clip/457');
+ assert.strictEqual(asked, 4, 'audio only: still remembered after two hours');
+ media.generateThumbnail = notFound('media.not_found');
+ await post('/api/thumbnails/generate/clip/458');
+ assert.strictEqual(asked, 5, 'another 404: asked again after an hour');
+ Date.now = realNow;
+
server.close();
server.close();
console.log('thumbnail no-picture: all checks passed');
console.log('thumbnail no-picture: all checks passed');
process.exit(0);
process.exit(0);