openvibers/openvibe.media Commits

Read old /p preview URLs as /t (#413)

OP OpenVibers committed 2 days ago · 09477aedff · 6 files, +22 −10

* 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

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

* An old /p preview URL reads as /t: no re-encode for the first release's rows

* A failed clip with a file still has frames: give it its picture

* WebM previews get a bitrate cap: pure CRF reached 600 KB on busy footage

Changes

ddiff --git a/server/jobs/preview.js b/server/jobs/preview.js
iindex b5a0768..1e13df3 100644
---- a/server/jobs/preview.js
++++ b/server/jobs/preview.js
@@@ -105,12 +105,17 @@ async function encode(input, remote, startAbs, seconds, out, codec, ctx, attempt
'-pix_fmt', 'yuv420p', '-profile:v', 'main', '-movflags', '+faststart', '-f', 'mp4', out);
} else {
// VP9 where this ffmpeg has it, VP8 otherwise (both are WebM; a hover preview needs only the codec a browser plays).
-- args.push('-c:v', 'libvpx-vp9', '-crf', String(40 + attempt * 6), '-b:v', '0', '-deadline', 'good', '-cpu-used', '4', '-row-mt', '1', '-f', 'webm', out);
++ // `-b:v 0` (pure CRF) let a high-motion 6 s clip reach 600 KB in production: a constrained bitrate keeps the
++ // 400 KB aim while the CRF still drops quiet scenes. attempt 1 and 2 lower it further.
++ args.push('-c:v', 'libvpx-vp9', '-crf', String(40 + attempt * 6),
++ '-b:v', `${Math.max(120, 300 - attempt * 60)}k`, '-maxrate', `${Math.max(180, 420 - attempt * 80)}k`,
++ '-bufsize', '840k', '-deadline', 'good', '-cpu-used', '4', '-row-mt', '1', '-f', 'webm', out);
}
let r = await d.ffmpeg(args, { signal: ctx.signal, timeoutMs: 5 * 60 * 1000, niceness: 5 });
if (!r.ok && !mp4 && /Unknown encoder|libvpx-vp9/i.test(r.stderr)) {
args.splice(args.indexOf('libvpx-vp9'), 1, 'libvpx');
args.splice(args.indexOf('-row-mt'), 2);
++ const maxrate = args.indexOf('-maxrate'); if (maxrate >= 0) args.splice(maxrate, 4);
r = await d.ffmpeg(args, { signal: ctx.signal, timeoutMs: 5 * 60 * 1000, niceness: 5 });
}
if (!r.ok || !d.existing(out) || !fs.statSync(out).size) return null;
@@@ -177,8 +182,7 @@ 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;
-- // 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;
++ if (row.preview_url) return null;
const { enqueue } = require('./queue');
return await enqueue({ appId, type: TYPE, params: { kind, id }, dedupeActive: true, createdBy });
} catch (err) {
@@@ -227,7 +231,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 OR preview_url LIKE '/p/%') AND thumbnail_url IS NOT NULL
++ AND preview_url IS NULL 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/jobs/thumbnail.js b/server/jobs/thumbnail.js
iindex 6ffdd05..50df57c 100644
---- a/server/jobs/thumbnail.js
++++ b/server/jobs/thumbnail.js
@@@ -214,7 +214,8 @@ async function sweepKind(app, kind, { now, batch, minAgeMs }) {
AND id < ?
ORDER BY id DESC LIMIT ?`, [app, cursor || '9223372036854775807', CANDIDATE_WINDOW])
: await db.all(`SELECT id, created_at, thumbnail_url FROM clips
-- WHERE app_id = ? AND (is_public = 1 OR visibility IN ('public', 'unlisted')) AND status = 'ready'
++ WHERE app_id = ? AND (is_public = 1 OR visibility IN ('public', 'unlisted'))
++ AND (status = 'ready' OR (status = 'failed' AND file_path IS NOT NULL))
AND (file_path IS NOT NULL OR vod_id IS NOT NULL)
AND id < ?
ORDER BY id DESC LIMIT ?`, [app, cursor || '9223372036854775807', CANDIDATE_WINDOW]);
ddiff --git a/server/thumbnails/preview-service.js b/server/thumbnails/preview-service.js
iindex 543063e..1183ea4 100644
---- a/server/thumbnails/preview-service.js
++++ b/server/thumbnails/preview-service.js
@@@ -44,6 +44,10 @@ async function itemServable(kind, id) {
/** The public URL of a written preview file. */
const previewUrl = (name) => `/t/${name}`;
++/** A stored preview URL, on the route that serves it today. The first release stored /p/, which production sends to
++ * Community's paste pages: the same file name is served at /t/, so old rows need no re-encode, only this rewrite. */
++const normalizePreviewUrl = (url) => (typeof url === 'string' && url.startsWith('/p/') ? `/t/${url.slice(3)}` : url);
++
/** 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()) {
return { mp4: `${kind}-${id}-${stamp}.mp4`, webm: `${kind}-${id}-${stamp}.webm` };
@@@ -114,5 +118,5 @@ async function servePreview(req, res) {
module.exports = {
PREVIEW_DIR, PREVIEW_WIDTH, PREVIEW_SECONDS, MAX_BYTES, NAME_RE,
-- previewUrl, previewNames, removePreviewFiles, parseRange, servePreview, itemServable,
++ previewUrl, normalizePreviewUrl, previewNames, removePreviewFiles, parseRange, servePreview, itemServable,
};
ddiff --git a/server/vod/clips-routes.js b/server/vod/clips-routes.js
iindex 4a764a6..9084d89 100644
---- a/server/vod/clips-routes.js
++++ b/server/vod/clips-routes.js
@@@ -98,8 +98,8 @@ async function clipPublic(clip, { readiness = false, hls } = {}) {
// Absolute URLs — cross-origin consumers render these directly.
playback_url: `${config.publicUrl}/c/${clip.id}`,
thumbnail_url: require('./finalize')._absUrl(clip.thumbnail_url),
-- preview_url: require('./finalize')._absUrl(clip.preview_url),
-- preview_webm_url: require('./finalize')._absUrl(clip.preview_webm_url),
++ preview_url: require('./finalize')._absUrl(require('../thumbnails/preview-service').normalizePreviewUrl(clip.preview_url)),
++ preview_webm_url: require('./finalize')._absUrl(require('../thumbnails/preview-service').normalizePreviewUrl(clip.preview_webm_url)),
// No picture in the source (an audio-only recording's clip): the player shows the avatar card, not a black box.
audio_only: !!clip.audio_only,
visibility: clip.visibility || 'public',
ddiff --git a/server/vod/finalize.js b/server/vod/finalize.js
iindex c6dcff6..c34e75c 100644
---- a/server/vod/finalize.js
++++ b/server/vod/finalize.js
@@@ -70,8 +70,8 @@ async function vodPublic(vod, { readiness = false, hls } = {}) {
playback_url: `${config.publicUrl}/v/${vod.id}`,
thumbnail_url: _absUrl(vod.thumbnail_url),
// The hover preview (preview.make): 6 s muted MP4, with the WebM of the same moment beside it. Null until made.
-- preview_url: _absUrl(vod.preview_url),
-- preview_webm_url: _absUrl(vod.preview_webm_url),
++ preview_url: _absUrl(require('../thumbnails/preview-service').normalizePreviewUrl(vod.preview_url)),
++ preview_webm_url: _absUrl(require('../thumbnails/preview-service').normalizePreviewUrl(vod.preview_webm_url)),
// No picture in the source (an audio-only browser broadcast, Media#240): one probe at finalize says so once.
audio_only: !!vod.audio_only,
storage_provider: vod.storage_provider || 'local',
ddiff --git a/test/preview-hover.test.js b/test/preview-hover.test.js
iindex 868e72c..6ad959b 100644
---- a/test/preview-hover.test.js
++++ b/test/preview-hover.test.js
@@@ -35,6 +35,9 @@ const { spawnSync } = require('child_process');
assert.strictEqual(previewService.parseRange('bytes=1000-', 1000), null);
assert.deepStrictEqual(previewJob.pickWindow(60), { len: 6, anchor: 6 });
assert.deepStrictEqual(previewJob.pickWindow(3), { len: 3, anchor: 0 });
++ assert.strictEqual(previewService.normalizePreviewUrl('/p/vod-1-2.mp4'), '/t/vod-1-2.mp4', 'an old /p URL is served at /t');
++ assert.strictEqual(previewService.normalizePreviewUrl('/t/vod-1-2.mp4'), '/t/vod-1-2.mp4');
++ assert.strictEqual(previewService.normalizePreviewUrl(null), null);
await db.upsertApp({ app_id: 'live', api_key: 'live-key' });
const VIDEO = path.join(process.env.VOD_PATH, 'bright.mp4');