openvibers/openvibe.mediahub Commits

Watch pages: the player shows the recording's or clip's picture until it plays (poster; LCP waited for the first video frame, 3.8 s on a phone-speed CPU) (#94)

OP OpenVibers committed 27 days ago · 0b039219d0 · 2 files, +15 −2

Changes

ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 5ac8bb3..fa53fdf 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -62,6 +62,12 @@ function thumbImg(url, sizes) {
return html`<img src="${`${url}?w=480&fmt=webp`}" srcset="${srcset}" sizes="${sizes}" alt="" loading="lazy">`;
}
const CARD_SIZES = '(max-width: 520px) calc(100vw - 32px), (max-width: 820px) 45vw, 240px';
++// The player shows the item's picture until it plays: without a poster the page's largest paint waited for the first
++// video frame (3.8 s on a phone-speed CPU, 2026-10-17). A Media thumbnail is asked for as a 1280 px WebP, as uploads are.
++function posterAttr(url) {
++ if (/^https:\/\/openvibe\.media\/t\/[^?#\s]+$/.test(url || '')) return html` poster="${`${url}?w=1280&fmt=webp`}"`;
++ return url ? html` poster="${url}"` : '';
++}
/**
* An item's playable file. Live's `file_url` is always the file; its `playback_url` is the HLS playlist when Media has
@@@ -801,7 +807,7 @@ ${uploaded.length ? html`<section class="ovv-section" aria-labelledby="h-uploads
body: html`<div class="ovv-watch">
<div class="ovv-main">
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
--<div class="ovv-player-wrap"><video class="ovv-player" controls preload="metadata" playsinline src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video></div>
++<div class="ovv-player-wrap"><video class="ovv-player" controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video></div>
<h1 class="ovv-title">${vod.title || 'Stream recording'}</h1>
<div class="ovv-byline">
${channelName ? html`<a class="ovv-avatar" href="${localChannel || liveChannel}" aria-hidden="true" tabindex="-1">${String(channelName).slice(0, 1).toUpperCase()}${live.isUsername(String(vod.username || '')) ? html`<img src="${config.networkUrl}/avatar/${vod.username}" alt="" loading="lazy">` : ''}</a>` : ''}
@@@ -879,7 +885,7 @@ ${clips.length ? html`<section class="ovv-panel" aria-labelledby="h-clips"><h2 i
<div class="ovv-main">
${savedNotice(req)}${req.query.made === '1' ? notice('Your clip was made. It is credited to you on OpenVibe.Live and here.', 'ok') : ''}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this clip.', 'info') : ''}
${cutting ? html`<div class="ovv-player-wrap ovv-cutting" role="status" data-ovv-reload="8"><p><i class="fa-solid fa-scissors" aria-hidden="true"></i> This clip is being cut. It plays here in a moment; the page checks again by itself.</p></div>`
-- : html`<div class="ovv-player-wrap"><video class="ovv-player" controls preload="metadata" playsinline src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}
++ : html`<div class="ovv-player-wrap"><video class="ovv-player" controls preload="metadata" playsinline${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}
<h1 class="ovv-title">${clip.title || 'Clip'}</h1>
<div class="ovv-byline">
<div class="ovv-byline-text"><span class="ovv-channel-name">${clip.auto_generated ? html`<span class="ovv-tag ovv-tag--inline">AI clip</span> ` : ''}${attribution}</span>
ddiff --git a/test/video-pages.test.js b/test/video-pages.test.js
iindex 789971d..1ebd5e6 100644
---- a/test/video-pages.test.js
++++ b/test/video-pages.test.js
@@@ -221,6 +221,13 @@ function countOccurrences(haystack, needle) {
live.setChatDown(false);
});
++ await check('the player shows the item\'s picture until it plays (a 1280 px WebP of a Media thumbnail)', async () => {
++ const vod = await getVideo('/v/101');
++ assert.ok(/<video class="ovv-player"[^>]* poster="https:\/\/openvibe\.media\/t\/101\.jpg\?w=1280&amp;fmt=webp"/.test(vod.text), vod.text.match(/<video[^>]*>/)?.[0]);
++ const clip = await getVideo('/c/201');
++ assert.ok(clip.status !== 200 || !/<video class="ovv-player"[^>]* poster=/.test(clip.text), 'no thumbnail, no poster');
++ });
++
await check('the video host answers /release.json too (the shared release check reads its own origin)', async () => {
const r = await getVideo('/release.json');
assert.strictEqual(r.status, 200);