openvibers/openvibe.media Commits

Serve hover previews from the thumbnail origin (#411)

OP OpenVibers committed 2 days ago · 647e5d25e0 · 4 files, +15 −15

* Thumbnails that survive a bad frame, and 6 s hover previews (355)

* Backfill quarantined public recordings too: their visibility is public

* Serve previews on the thumbnail origin: /p is Community's namespace

* Merge remote-tracking branch 'origin/main' into cx-thumbs-previews-Media

# Conflicts:
#	server/jobs/preview.js
#	server/public/routes.js
#	server/thumbnails/preview-service.js
#	test/preview-hover.test.js

Changes

ddiff --git a/server/jobs/preview.js b/server/jobs/preview.js
iindex 8b01608..b5a0768 100644
---- a/server/jobs/preview.js
++++ b/server/jobs/preview.js
@@@ -177,7 +177,8 @@ async function queuePreview(appId, kind, id, { createdBy = 'system:preview' } =
const row = await rowOf(appId, kind, id);
if (!row || Number(row.audio_only)) return null;
if ((row.visibility || (Number(row.is_public) ? 'public' : 'private')) === 'private') return null;
-- if (row.preview_url) return null;
++ // An older /p/ URL (the first release) does not route in production: make a fresh pair.
++ if (row.preview_url && !String(row.preview_url).startsWith('/p/')) return null;
const { enqueue } = require('./queue');
return await enqueue({ appId, type: TYPE, params: { kind, id }, dedupeActive: true, createdBy });
} catch (err) {
@@@ -226,7 +227,7 @@ async function sweepKind(appId, kind, { now, batch, minAgeMs }) {
const rows = await db.all(`SELECT id, created_at, duration_seconds, thumbnail_url, preview_url, file_path${kind === 'clip' ? ', vod_id' : ''} FROM ${table}
WHERE ${where}
AND audio_only IS NOT TRUE
-- AND preview_url IS NULL AND thumbnail_url IS NOT NULL
++ AND (preview_url IS NULL OR preview_url LIKE '/p/%') AND thumbnail_url IS NOT NULL
AND COALESCE(duration_seconds, 0) >= 2
AND id < ?
ORDER BY id DESC LIMIT ?`, [appId, cursor || '9223372036854775807', CANDIDATE_WINDOW]);
ddiff --git a/server/public/routes.js b/server/public/routes.js
iindex 27b8e77..c3a5b7c 100644
---- a/server/public/routes.js
++++ b/server/public/routes.js
@@@ -496,15 +496,9 @@ router.get('/og-image.png', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'static', 'og-image.png'));
});
--// ── Hover previews (6 s muted MP4/WebM; server/thumbnails/preview-service.js) ──
--router.get('/p/:id', async (req, res, next) => {
-- // Not a preview name: a paste slug, which the legacy /p/:slug route below redirects to Community.
-- if (!require('../thumbnails/preview-service').NAME_RE.test(path.basename(String(req.params.id || '')))) return next();
-- try { await require('../thumbnails/preview-service').servePreview(req, res); }
-- catch (err) { console.error('[Public] preview:', err.message); if (!res.headersSent) res.status(500).end(); }
--});
--
--// ── Thumbnails ───────────────────────────────────────────────
++// ── Thumbnails and their hover previews ─────────────────────
++// A preview lives in the same directory and shares the thumbnail route (its file name is vod|clip-<id>-<time>.mp4|webm):
++// /p/ is Community's legacy paste namespace in production, so it cannot be this route's home.
router.get('/t/:id', sharingLib.attachViewer, async (req, res) => {
if (drill.refuseBytes(res)) return;
// Open to every origin like the other public images: Live's player samples a stream thumbnail's colours on a
@@@ -512,6 +506,10 @@ router.get('/t/:id', sharingLib.attachViewer, async (req, res) => {
// Whoever has the URL can already load the image; this lets a page read its pixels.
res.set('Access-Control-Allow-Origin', '*');
res.set('Cross-Origin-Resource-Policy', 'cross-origin');
++ const previewName = path.basename(String(req.params.id || ''));
++ if (require('../thumbnails/preview-service').NAME_RE.test(previewName)) {
++ return require('../thumbnails/preview-service').servePreview(req, res);
++ }
// A Specific-people recording's picture is part of it: only the owner and the people on its list may fetch
// it, by the same check the watch page and its bytes use. A private thumbnail keeps its old unguessable-URL
// behaviour (changing that is not this step).
ddiff --git a/server/thumbnails/preview-service.js b/server/thumbnails/preview-service.js
iindex 6540c0b..543063e 100644
---- a/server/thumbnails/preview-service.js
++++ b/server/thumbnails/preview-service.js
@@@ -3,7 +3,8 @@
*
* A finished recording or clip gets a short preview beside its thumbnail, for the hover/centred player the design
* puts on cards (plan 355): `vod-<id>-<time>.mp4` (H.264) and `.webm` (VP9, VP8 when this ffmpeg has no VP9), both
-- * in THUMB_DIR, both served at GET /p/<name>. The row's preview_url is the MP4 (every browser plays it) and
++ * in THUMB_DIR, both served at GET /t/<name> (the thumbnail origin; /p/ belongs to Community's paste URLs). The row's
++ * preview_url is the MP4 (every browser plays it) and
* preview_webm_url the WebM of the same moment. Files are made by the preview.make job (server/jobs/preview.js)
* and only for public/unlisted, non-audio-only items.
*
@@@ -41,7 +42,7 @@ async function itemServable(kind, id) {
}
/** The public URL of a written preview file. */
--const previewUrl = (name) => `/p/${name}`;
++const previewUrl = (name) => `/t/${name}`;
/** The file name of a new preview for `kind`/`id` at `stamp` (the same stamp for both formats). */
function previewNames(kind, id, stamp = Date.now()) {
ddiff --git a/test/preview-hover.test.js b/test/preview-hover.test.js
iindex 5b3baac..868e72c 100644
---- a/test/preview-hover.test.js
++++ b/test/preview-hover.test.js
@@@ -84,8 +84,8 @@ const { spawnSync } = require('child_process');
// ── The job makes both formats, 320 px wide, muted, under 400 KB, and points the row at them ──
const out = await previewJob.spec.run({ app_id: 'live', params: { kind: 'vod', id: recent } }, {});
-- assert.match(out.preview_url, /^\/p\/vod-\d+-\d+\.mp4$/);
-- assert.match(out.preview_webm_url, /^\/p\/vod-\d+-\d+\.webm$/);
++ assert.match(out.preview_url, /^\/t\/vod-\d+-\d+\.mp4$/);
++ assert.match(out.preview_webm_url, /^\/t\/vod-\d+-\d+\.webm$/);
assert.ok(out.bytes > 0 && out.bytes <= previewService.MAX_BYTES, `mp4 ${out.bytes} bytes`);
assert.ok(out.webm_bytes > 0 && out.webm_bytes <= previewService.MAX_BYTES, `webm ${out.webm_bytes} bytes`);
assert.ok(out.start_seconds >= 0 && out.start_seconds + out.seconds <= 8.1, 'the window is inside the source');