openvibers/openvibe.media Commits
Read old /p preview URLs as /t (#413)
Changes
@@@ -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 NULLAND COALESCE(duration_seconds, 0) >= 2AND id < ?ORDER BY id DESC LIMIT ?`, [appId, cursor || '9223372036854775807', CANDIDATE_WINDOW]);@@@ -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]);@@@ -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,};@@@ -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',@@@ -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',@@@ -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');
@@ -105,12 +105,17 @@ async function encode(input, remote, startAbs, seconds, out, codec, ctx, attempt
@@ -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);
'-pix_fmt', 'yuv420p', '-profile:v', 'main', '-movflags', '+faststart', '-f', 'mp4', out);
} else {
} else {
// VP9 where this ffmpeg has it, VP8 otherwise (both are WebM; a hover preview needs only the codec a browser plays).
// 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 });
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)) {
if (!r.ok && !mp4 && /Unknown encoder|libvpx-vp9/i.test(r.stderr)) {
args.splice(args.indexOf('libvpx-vp9'), 1, 'libvpx');
args.splice(args.indexOf('libvpx-vp9'), 1, 'libvpx');
args.splice(args.indexOf('-row-mt'), 2);
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 });
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;
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' } =
@@ -177,8 +182,7 @@ 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;
- // 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');
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) {
@@ -227,7 +231,7 @@ async function sweepKind(appId, kind, { now, batch, minAgeMs }) {
@@ -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}
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 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 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]);
@@ -214,7 +214,8 @@ async function sweepKind(app, kind, { now, batch, minAgeMs }) {
@@ -214,7 +214,8 @@ async function sweepKind(app, kind, { now, batch, minAgeMs }) {
AND id < ?
AND id < ?
ORDER BY id DESC LIMIT ?`, [app, cursor || '9223372036854775807', CANDIDATE_WINDOW])
ORDER BY id DESC LIMIT ?`, [app, cursor || '9223372036854775807', CANDIDATE_WINDOW])
: await db.all(`SELECT id, created_at, thumbnail_url FROM clips
: 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 (file_path IS NOT NULL OR vod_id IS NOT NULL)
AND id < ?
AND id < ?
ORDER BY id DESC LIMIT ?`, [app, cursor || '9223372036854775807', CANDIDATE_WINDOW]);
ORDER BY id DESC LIMIT ?`, [app, cursor || '9223372036854775807', CANDIDATE_WINDOW]);
@@ -44,6 +44,10 @@ async function itemServable(kind, id) {
@@ -44,6 +44,10 @@ async function itemServable(kind, id) {
/** The public URL of a written preview file. */
/** The public URL of a written preview file. */
const previewUrl = (name) => `/t/${name}`;
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). */
/** 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()) {
return { mp4: `${kind}-${id}-${stamp}.mp4`, webm: `${kind}-${id}-${stamp}.webm` };
return { mp4: `${kind}-${id}-${stamp}.mp4`, webm: `${kind}-${id}-${stamp}.webm` };
@@ -114,5 +118,5 @@ async function servePreview(req, res) {
@@ -114,5 +118,5 @@ async function servePreview(req, res) {
module.exports = {
module.exports = {
PREVIEW_DIR, PREVIEW_WIDTH, PREVIEW_SECONDS, MAX_BYTES, NAME_RE,
PREVIEW_DIR, PREVIEW_WIDTH, PREVIEW_SECONDS, MAX_BYTES, NAME_RE,
- previewUrl, previewNames, removePreviewFiles, parseRange, servePreview, itemServable,
+ previewUrl, normalizePreviewUrl, previewNames, removePreviewFiles, parseRange, servePreview, itemServable,
};
};
@@ -98,8 +98,8 @@ async function clipPublic(clip, { readiness = false, hls } = {}) {
@@ -98,8 +98,8 @@ async function clipPublic(clip, { readiness = false, hls } = {}) {
// Absolute URLs — cross-origin consumers render these directly.
// Absolute URLs — cross-origin consumers render these directly.
playback_url: `${config.publicUrl}/c/${clip.id}`,
playback_url: `${config.publicUrl}/c/${clip.id}`,
thumbnail_url: require('./finalize')._absUrl(clip.thumbnail_url),
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.
// 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,
audio_only: !!clip.audio_only,
visibility: clip.visibility || 'public',
visibility: clip.visibility || 'public',
@@ -70,8 +70,8 @@ async function vodPublic(vod, { readiness = false, hls } = {}) {
@@ -70,8 +70,8 @@ async function vodPublic(vod, { readiness = false, hls } = {}) {
playback_url: `${config.publicUrl}/v/${vod.id}`,
playback_url: `${config.publicUrl}/v/${vod.id}`,
thumbnail_url: _absUrl(vod.thumbnail_url),
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.
// 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.
// No picture in the source (an audio-only browser broadcast, Media#240): one probe at finalize says so once.
audio_only: !!vod.audio_only,
audio_only: !!vod.audio_only,
storage_provider: vod.storage_provider || 'local',
storage_provider: vod.storage_provider || 'local',
@@ -35,6 +35,9 @@ const { spawnSync } = require('child_process');
@@ -35,6 +35,9 @@ const { spawnSync } = require('child_process');
assert.strictEqual(previewService.parseRange('bytes=1000-', 1000), null);
assert.strictEqual(previewService.parseRange('bytes=1000-', 1000), null);
assert.deepStrictEqual(previewJob.pickWindow(60), { len: 6, anchor: 6 });
assert.deepStrictEqual(previewJob.pickWindow(60), { len: 6, anchor: 6 });
assert.deepStrictEqual(previewJob.pickWindow(3), { len: 3, anchor: 0 });
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' });
await db.upsertApp({ app_id: 'live', api_key: 'live-key' });
const VIDEO = path.join(process.env.VOD_PATH, 'bright.mp4');
const VIDEO = path.join(process.env.VOD_PATH, 'bright.mp4');