openvibers/openvibe.media Commits
Serve hover previews from the thumbnail origin (#411)
Changes
@@@ -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 NULLAND COALESCE(duration_seconds, 0) >= 2AND id < ?ORDER BY id DESC LIMIT ?`, [appId, cursor || '9223372036854775807', CANDIDATE_WINDOW]);@@@ -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).@@@ -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()) {@@@ -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');
@@ -177,7 +177,8 @@ async function queuePreview(appId, kind, id, { createdBy = 'system:preview' } =
@@ -177,7 +177,8 @@ async function queuePreview(appId, kind, id, { createdBy = 'system:preview' } =
const row = await rowOf(appId, kind, id);
const row = await rowOf(appId, kind, id);
if (!row || Number(row.audio_only)) return null;
if (!row || Number(row.audio_only)) return null;
if ((row.visibility || (Number(row.is_public) ? 'public' : 'private')) === 'private') 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');
const { enqueue } = require('./queue');
return await enqueue({ appId, type: TYPE, params: { kind, id }, dedupeActive: true, createdBy });
return await enqueue({ appId, type: TYPE, params: { kind, id }, dedupeActive: true, createdBy });
} catch (err) {
} catch (err) {
@@ -226,7 +227,7 @@ async function sweepKind(appId, kind, { now, batch, minAgeMs }) {
@@ -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}
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}
WHERE ${where}
AND audio_only IS NOT TRUE
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 COALESCE(duration_seconds, 0) >= 2
AND id < ?
AND id < ?
ORDER BY id DESC LIMIT ?`, [appId, cursor || '9223372036854775807', CANDIDATE_WINDOW]);
ORDER BY id DESC LIMIT ?`, [appId, cursor || '9223372036854775807', CANDIDATE_WINDOW]);
@@ -496,15 +496,9 @@ router.get('/og-image.png', (req, res) => {
@@ -496,15 +496,9 @@ router.get('/og-image.png', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'static', 'og-image.png'));
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) => {
router.get('/t/:id', sharingLib.attachViewer, async (req, res) => {
if (drill.refuseBytes(res)) return;
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
// 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) => {
@@ -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.
// Whoever has the URL can already load the image; this lets a page read its pixels.
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Origin', '*');
res.set('Cross-Origin-Resource-Policy', 'cross-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
// 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
// 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).
// behaviour (changing that is not this step).
@@ -3,7 +3,8 @@
@@ -3,7 +3,8 @@
*
*
* A finished recording or clip gets a short preview beside its thumbnail, for the hover/centred player the design
* 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
* 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)
* 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.
* and only for public/unlisted, non-audio-only items.
*
*
@@ -41,7 +42,7 @@ async function itemServable(kind, id) {
@@ -41,7 +42,7 @@ async function itemServable(kind, id) {
}
}
/** The public URL of a written preview file. */
/** 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). */
/** 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()) {
function previewNames(kind, id, stamp = Date.now()) {
@@ -84,8 +84,8 @@ const { spawnSync } = require('child_process');
@@ -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 ──
// ── 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 } }, {});
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.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.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');
assert.ok(out.start_seconds >= 0 && out.start_seconds + out.seconds <= 8.1, 'the window is inside the source');