openvibers/openvibe.mediahub Commits

Video pages: the player loads colour → 32 px preview → poster → frame, and glows around itself (openvibe-shared 4.13 data-ov-thumb, data-ov-ambient) (#128)

OP OpenVibers committed 19 days ago · edd6b708d0 · 4 files, +11 −4

Changes

ddiff --git a/public/css/video.css b/public/css/video.css
iindex 026fbb6..05fef23 100644
---- a/public/css/video.css
++++ b/public/css/video.css
@@@ -95,7 +95,10 @@
.ovv-watch--clip { grid-template-columns: minmax(0, 960px); justify-content: center; }
.ovv-main, .ovv-side { min-width: 0; }
.ovv-side { display: grid; gap: 18px; }
--.ovv-player-wrap { border-radius: 14px; overflow: hidden; background: #000; border: 1px solid var(--ovv-line); box-shadow: 0 18px 40px rgba(0, 0, 0, .35); }
++.ovv-player-wrap { position: relative; border-radius: 14px; background: #000; border: 1px solid var(--ovv-line); box-shadow: 0 18px 40px rgba(0, 0, 0, .35); }
++/* The player clips itself to the corners, so the wrapper can let the ambient glow (shared player.js) spill around it. */
++.ovv-player-wrap > .ov-player, .ovv-player-wrap > video { border-radius: inherit; overflow: hidden; }
++.ovv-player-wrap.ov-player-host { box-shadow: none; border-color: transparent; }
.ovv-player { display: block; width: 100%; aspect-ratio: 16 / 9; background: #000; }
.ovv-title { margin: 1rem 0 .6rem; font-size: clamp(1.3rem, 2.4vw, 1.75rem); line-height: 1.25; }
.ovv-byline { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 14px; padding-bottom: 16px; border-bottom: 1px solid var(--ovv-line); }
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 16d8fa4..309b6e6 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -75,8 +75,10 @@ const CARD_SIZES = '(max-width: 520px) calc(100vw - 32px), (max-width: 820px) 45
// 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}"` : '';
++ // A Media thumbnail comes in sizes: the poster at 1280 px, and a 32 px preview the player shows blurred (and takes its
++ // background colour from) before the poster arrives. data-ov-ambient: the picture's glow around the player.
++ if (/^https:\/\/openvibe\.media\/t\/[^?#\s]+$/.test(url || '')) return html` poster="${`${url}?w=1280&fmt=webp`}" data-ov-thumb="${`${url}?w=32&fmt=webp`}" data-ov-ambient`;
++ return url ? html` poster="${url}" data-ov-ambient` : '';
}
/**
ddiff --git a/server/video/upload-routes.js b/server/video/upload-routes.js
iindex 92bfec5..70a8e15 100644
---- a/server/video/upload-routes.js
++++ b/server/video/upload-routes.js
@@@ -274,7 +274,7 @@ ${req.query.saved === '1' ? notice('Saved.', 'ok') : ''}${req.query.reported ===
${row.state === 'hidden' ? notice('Hidden after reports: only you and OpenVibe staff can see it until staff decide.', 'warn') : ''}
${row.visibility === 'private' && ready ? notice('Private: only you (and OpenVibe staff) can see this video.', 'info') : ''}
${original && original.state !== 'removed' ? html`<p class="ovv-fine ovv-edited-from">An edited copy of <a href="/u/${original.id}">${original.title}</a>, made by the AI cut.</p>` : ''}
--${ready && vod ? html`<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${thumb ? html` poster="${`${thumb}?w=1280&fmt=webp`}"` : ''} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${hlsAttrs(vod)}>${tracks}</video></div>`
++${ready && vod ? html`<div class="ovv-player-wrap"><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${thumb ? html` poster="${`${thumb}?w=1280&fmt=webp`}" data-ov-thumb="${`${thumb}?w=32&fmt=webp`}" data-ov-ambient` : ''} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${hlsAttrs(vod)}>${tracks}</video></div>`
: html`<div class="ovv-player-wrap ovv-processing" role="status">${row.status === 'processing' ? html`<p><i class="fa-solid fa-gear fa-spin" aria-hidden="true"></i> Processing: OpenVibe.Media is making it playable. This page reloads by itself.</p>`
: row.status === 'failed' ? html`<p>This upload could not be processed. ${row.error || ''}</p>`
: html`<p>The upload stopped before the end (${row.parts_received} of ${row.parts_expected} parts). Upload the file again from <a href="/upload">Upload</a>.</p>`}</div>`}
ddiff --git a/test/video-pages.test.js b/test/video-pages.test.js
iindex c3dec75..c80d536 100644
---- a/test/video-pages.test.js
++++ b/test/video-pages.test.js
@@@ -275,6 +275,8 @@ function countOccurrences(haystack, needle) {
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]);
++ // The player's layered loading and glow (openvibe-shared 4.13): a 32 px preview and data-ov-ambient.
++ assert.ok(/<video class="ovv-player"[^>]* data-ov-thumb="https:\/\/openvibe\.media\/t\/101\.jpg\?w=32&amp;fmt=webp" data-ov-ambient/.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');
});