openvibers/openvibe.mediahub Commits
openvibe-shared v4.192.0, and the video comes along (the small player) (#375)
Changes
@@@ -553,7 +553,7 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [<!-- versions:start -->- openvibe-contracts: v0.384.0- openvibe-sdk: v0.125.0--- openvibe-shared: v4.186.0++- openvibe-shared: v4.192.0<!-- versions:end -->### Public Pics gallery@@@ -10,7 +10,7 @@"packages": ["openvibe-contracts v0.384.0","openvibe-sdk v0.125.0",-- "openvibe-shared v4.186.0"++ "openvibe-shared v4.192.0"],"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.","works": [@@@ -44,7 +44,7 @@],"contracts": "openvibe-contracts v0.384.0","sdk": "openvibe-sdk v0.125.0",-- "shared": "openvibe-shared v4.186.0",++ "shared": "openvibe-shared v4.192.0","deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.","updated": "2026-11-10"}@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.186.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.192.0","pg": "^8.23.0","sharp": "^0.33.5"},@@@ -1654,9 +1654,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.186.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.186.0",-- "integrity": "sha512-wD4net+UjO0a6OG73W+RR1z5MIWtPgj407xLEWphiVWgLTQ4V1r76+DtLOfSf6RfEda/ALizRu1zmxbvuGGZOw==",++ "version": "4.192.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.192.0",++ "integrity": "sha512-Wecot1Ivp9LXtv9Mss5f69SkqyrlfSwboahZe7rlTa5S0Hqz2YtQWnhPA7GgWrPXu/krYj+BoSTEPJYjHAL9jA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -24,7 +24,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.186.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.192.0","pg": "^8.23.0","sharp": "^0.33.5"},@@@ -72,7 +72,8 @@ ${appIcon.headTags({ site: 'media-hub' })}<link rel="stylesheet" href="${asset('css/video.css')}">${(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`).join('\n')}${(o.sharedScripts || []).map((name) => `<script src="${ovServe.url(name)}" defer></script>`).join('\n')}--${(o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">\n`).join('')}${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${o.path === '/record' ? `<script src="${CAM_KIT_URL}" integrity="${CAM_KIT_SRI}" crossorigin="anonymous" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;++${(o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">\n`).join('')}<script src="${ovServe.url('mini.js')}" defer></script>++${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${o.path === '/record' ? `<script src="${CAM_KIT_URL}" integrity="${CAM_KIT_SRI}" crossorigin="anonymous" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;const account = signedIn? html`Signed in as <strong>${viewer.displayName || viewer.username || 'you'}</strong> · <a href="/auth/logout?next=${encodeURIComponent(pathNow)}">Sign out</a>`: html`<a href="/auth/login?next=${encodeURIComponent(pathNow)}">Sign in with OpenVibe</a>`;@@@ -110,6 +110,14 @@ function posterAttr(url) {* broadcast with no camera, plan R4) shows it in place of a black box. Network's /avatar/:username answers the image* (or a neutral placeholder), so no lookup is needed here.*/++/**++ * A watch page's video may come along (openvibe-shared 4.192 mini.js): the player's picture-in-picture button takes the++ * person to the front page and the video goes on in the small player, on every page, from the same second.++ */++function miniAttr(title, by, page) {++ return html` data-ov-mini data-ov-mini-title="${String(title || 'Video')}" data-ov-mini-by="${String(by || '')}" data-ov-mini-page="${String(page || '')}"`;++}++function avatarAttr(username, networkUrl) {const name = String(username || '');return /^[A-Za-z0-9_]{1,32}$/.test(name) && /^https?:\/\//.test(String(networkUrl || '')) ? html` data-ov-avatar="${String(networkUrl).replace(/\/+$/, '')}/avatar/${name}"` : '';@@@ -1494,7 +1502,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv<div class="ovv-col ovv-col--main"><div class="ovv-stage">${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}--<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(vod), { owner: vod.username, opened: true })}><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} 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>${pipsTag(pips)}</div>++<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(vod), { owner: vod.username, opened: true })}><video class="ovv-player" data-ov-player${miniAttr(vod.title || 'Stream recording', channelName || vod.username, watchPath)} controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} 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>${pipsTag(pips)}</div><div class="ovv-head">${titleLine('Stream recording', vod.title || 'Stream recording')}${labelsUi.chips(labelsUi.inForce(vod))}@@@ -1620,7 +1628,7 @@ ${more || ''}<div class="ovv-stage">${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"${labelsUi.attrs(labelsUi.inForce(clip), { owner: clip.username, opened: true })}><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${avatarAttr(clip.username, config.networkUrl)}${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}++ : html`<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(clip), { owner: clip.username, opened: true })}><video class="ovv-player" data-ov-player${miniAttr(clip.title || 'Clip', clip.source_streamer_display_name || clip.display_name || clip.username, watchPath)} controls preload="metadata" playsinline${avatarAttr(clip.username, config.networkUrl)}${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}<div class="ovv-head">${titleLine(aiClip ? 'AI clip' : 'Clip', clip.title || 'Clip')}${labelsUi.chips(labelsUi.inForce(clip))}@@@ -144,6 +144,9 @@ function hreq(base, host, path, { method = 'GET', cookie, form, body, type, sameawait check('beside the player: more from the channel, never the one playing', async () => {const v = await video('/v/301');const more = (v.text.split('<section class="ovv-panel ovv-also"')[1] || '').split('</section>')[0];++ // The video may come along (the small player): it carries its title, who made it and its own page.++ assert.match(v.text, /<video class="ovv-player" data-ov-player data-ov-mini data-ov-mini-title="[^"]+" data-ov-mini-by="[^"]*" data-ov-mini-page="\/(?:watch\?v=[^"]+|v\/301)"/, 'a recording says it may come along');++ assert.match(v.text, /<script src="[^"]*mini[^"]*\.js[^"]*" defer><\/script>/, 'every Video page loads the small player');assert.ok(!/Kim labelled/.test(more), 'never the recording playing');assert.ok(!/Kim clip/.test(more), 'its own clips are in Clips from this stream, not again here');const c = await video('/c/401');@@@ -14,9 +14,9 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');// Budgets are set from the numbers measured on 2026-10-08 (the real ones, fresh database), rounded up with about 10%// headroom; file counts are the measured counts and external files stay at 0.const BUDGETS = {-- htmlRawKB: 60.4, // 60.4: measured 58.4 with openvibe-shared 4.161.0, plus room for 4.162.0–4.163.0, 2026-11-09++ htmlRawKB: 62.5, // 62.5 (2026-11-10: measured 61.8 with openvibe-shared 4.192.0: the small player's script tag, the footer's online strip, the site name's dropdown); 60.4: measured 58.4 with openvibe-shared 4.161.0, plus room for 4.162.0–4.163.0, 2026-11-09// FAQPage JSON-LD) and next steps on the network; was 29.7 at 27.5-- htmlBrotliKB: 12.6, // 12.3 (2026-11-07: nine features on the home); 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1++ htmlBrotliKB: 13.5, // 13.5 (2026-11-10: measured 13.1 with openvibe-shared 4.192.0, as above); 12.6 before; 12.3 (2026-11-07: nine features on the home); 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1jsFiles: 6, // 6: /shared/ov-glyphs.js in place of Font Awesome's stylesheet (2026-11-01, plan L54 K4); was: 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)jsRawKB: 401, // 400.5 with openvibe-shared 4.80.0: 33 more solid glyphs in ov-glyphs.js (2026-11-01); 393.9 with openvibe-shared 4.77.0: the navbar loads the glyphs when a page first shows an icon (2026-11-01); 393: 393: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4; Font Awesome's CSS and font were external, uncounted, and heavier); 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8jsBrotliKB: 98, // 97.6 with openvibe-shared 4.80.0 (2026-11-01); 96.1 with openvibe-shared 4.77.0 (2026-11-01); 96: 96: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4); 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5@@@ -348,7 +348,7 @@ function countOccurrences(haystack, needle) {assert.ok(r.text.includes('playsinline'));assert.ok(r.text.includes('data-ovv-video'));// The network's player (openvibe-shared player.js/css): marked, and both files load on the page.-- assert.ok(/<video class="ovv-player" data-ov-player controls /.test(r.text), 'the video takes the network player');++ assert.ok(/<video class="ovv-player" data-ov-player data-ov-mini data-ov-mini-title="[^"]+" data-ov-mini-by="[^"]*" data-ov-mini-page="[^"]+" controls /.test(r.text), 'the video takes the network player, and may come along in the small player');// The streamer's face for an audio-only recording (openvibe-shared 4.90 player.js, plan R4): Network's avatar URL.assert.match(r.text, /<video class="ovv-player"[^>]* data-ov-avatar="[^"]+\/avatar\/streamer"/, 'the player knows the streamer\'s face');assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(r.text) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(r.text), 'player.css and player.js load');
@@ -553,7 +553,7 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -553,7 +553,7 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.384.0
- openvibe-contracts: v0.384.0
- openvibe-sdk: v0.125.0
- openvibe-sdk: v0.125.0
-- openvibe-shared: v4.186.0
+- openvibe-shared: v4.192.0
<!-- versions:end -->
<!-- versions:end -->
### Public Pics gallery
### Public Pics gallery
@@ -10,7 +10,7 @@
@@ -10,7 +10,7 @@
"packages": [
"packages": [
"openvibe-contracts v0.384.0",
"openvibe-contracts v0.384.0",
"openvibe-sdk v0.125.0",
"openvibe-sdk v0.125.0",
- "openvibe-shared v4.186.0"
+ "openvibe-shared v4.192.0"
],
],
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
"works": [
"works": [
@@ -44,7 +44,7 @@
@@ -44,7 +44,7 @@
],
],
"contracts": "openvibe-contracts v0.384.0",
"contracts": "openvibe-contracts v0.384.0",
"sdk": "openvibe-sdk v0.125.0",
"sdk": "openvibe-sdk v0.125.0",
- "shared": "openvibe-shared v4.186.0",
+ "shared": "openvibe-shared v4.192.0",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"updated": "2026-11-10"
"updated": "2026-11-10"
}
}
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.186.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.192.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
},
},
@@ -1654,9 +1654,9 @@
@@ -1654,9 +1654,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.186.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.186.0",
- "integrity": "sha512-wD4net+UjO0a6OG73W+RR1z5MIWtPgj407xLEWphiVWgLTQ4V1r76+DtLOfSf6RfEda/ALizRu1zmxbvuGGZOw==",
+ "version": "4.192.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.192.0",
+ "integrity": "sha512-Wecot1Ivp9LXtv9Mss5f69SkqyrlfSwboahZe7rlTa5S0Hqz2YtQWnhPA7GgWrPXu/krYj+BoSTEPJYjHAL9jA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.186.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.192.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
},
},
@@ -72,7 +72,8 @@ ${appIcon.headTags({ site: 'media-hub' })}
@@ -72,7 +72,8 @@ ${appIcon.headTags({ site: 'media-hub' })}
<link rel="stylesheet" href="${asset('css/video.css')}">
<link rel="stylesheet" href="${asset('css/video.css')}">
${(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`).join('\n')}
${(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`).join('\n')}
${(o.sharedScripts || []).map((name) => `<script src="${ovServe.url(name)}" defer></script>`).join('\n')}
${(o.sharedScripts || []).map((name) => `<script src="${ovServe.url(name)}" defer></script>`).join('\n')}
-${(o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">\n`).join('')}${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${o.path === '/record' ? `<script src="${CAM_KIT_URL}" integrity="${CAM_KIT_SRI}" crossorigin="anonymous" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;
+${(o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">\n`).join('')}<script src="${ovServe.url('mini.js')}" defer></script>
+${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${o.path === '/record' ? `<script src="${CAM_KIT_URL}" integrity="${CAM_KIT_SRI}" crossorigin="anonymous" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;
const account = signedIn
const account = signedIn
? html`Signed in as <strong>${viewer.displayName || viewer.username || 'you'}</strong> · <a href="/auth/logout?next=${encodeURIComponent(pathNow)}">Sign out</a>`
? html`Signed in as <strong>${viewer.displayName || viewer.username || 'you'}</strong> · <a href="/auth/logout?next=${encodeURIComponent(pathNow)}">Sign out</a>`
: html`<a href="/auth/login?next=${encodeURIComponent(pathNow)}">Sign in with OpenVibe</a>`;
: html`<a href="/auth/login?next=${encodeURIComponent(pathNow)}">Sign in with OpenVibe</a>`;
@@ -110,6 +110,14 @@ function posterAttr(url) {
@@ -110,6 +110,14 @@ function posterAttr(url) {
* broadcast with no camera, plan R4) shows it in place of a black box. Network's /avatar/:username answers the image
* broadcast with no camera, plan R4) shows it in place of a black box. Network's /avatar/:username answers the image
* (or a neutral placeholder), so no lookup is needed here.
* (or a neutral placeholder), so no lookup is needed here.
*/
*/
+/**
+ * A watch page's video may come along (openvibe-shared 4.192 mini.js): the player's picture-in-picture button takes the
+ * person to the front page and the video goes on in the small player, on every page, from the same second.
+ */
+function miniAttr(title, by, page) {
+ return html` data-ov-mini data-ov-mini-title="${String(title || 'Video')}" data-ov-mini-by="${String(by || '')}" data-ov-mini-page="${String(page || '')}"`;
+}
+
function avatarAttr(username, networkUrl) {
function avatarAttr(username, networkUrl) {
const name = String(username || '');
const name = String(username || '');
return /^[A-Za-z0-9_]{1,32}$/.test(name) && /^https?:\/\//.test(String(networkUrl || '')) ? html` data-ov-avatar="${String(networkUrl).replace(/\/+$/, '')}/avatar/${name}"` : '';
return /^[A-Za-z0-9_]{1,32}$/.test(name) && /^https?:\/\//.test(String(networkUrl || '')) ? html` data-ov-avatar="${String(networkUrl).replace(/\/+$/, '')}/avatar/${name}"` : '';
@@ -1494,7 +1502,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv
@@ -1494,7 +1502,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv
<div class="ovv-col ovv-col--main">
<div class="ovv-col ovv-col--main">
<div class="ovv-stage">
<div class="ovv-stage">
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
-<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(vod), { owner: vod.username, opened: true })}><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} 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>${pipsTag(pips)}</div>
+<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(vod), { owner: vod.username, opened: true })}><video class="ovv-player" data-ov-player${miniAttr(vod.title || 'Stream recording', channelName || vod.username, watchPath)} controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} 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>${pipsTag(pips)}</div>
<div class="ovv-head">
<div class="ovv-head">
${titleLine('Stream recording', vod.title || 'Stream recording')}
${titleLine('Stream recording', vod.title || 'Stream recording')}
${labelsUi.chips(labelsUi.inForce(vod))}
${labelsUi.chips(labelsUi.inForce(vod))}
@@ -1620,7 +1628,7 @@ ${more || ''}
@@ -1620,7 +1628,7 @@ ${more || ''}
<div class="ovv-stage">
<div class="ovv-stage">
${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') : ''}
${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>`
${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"${labelsUi.attrs(labelsUi.inForce(clip), { owner: clip.username, opened: true })}><video class="ovv-player" data-ov-player controls preload="metadata" playsinline${avatarAttr(clip.username, config.networkUrl)}${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}
+ : html`<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(clip), { owner: clip.username, opened: true })}><video class="ovv-player" data-ov-player${miniAttr(clip.title || 'Clip', clip.source_streamer_display_name || clip.display_name || clip.username, watchPath)} controls preload="metadata" playsinline${avatarAttr(clip.username, config.networkUrl)}${posterAttr(httpsUrl(clip.thumbnail_url))} src="${fileOf(clip) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}></video></div>`}
<div class="ovv-head">
<div class="ovv-head">
${titleLine(aiClip ? 'AI clip' : 'Clip', clip.title || 'Clip')}
${titleLine(aiClip ? 'AI clip' : 'Clip', clip.title || 'Clip')}
${labelsUi.chips(labelsUi.inForce(clip))}
${labelsUi.chips(labelsUi.inForce(clip))}
@@ -144,6 +144,9 @@ function hreq(base, host, path, { method = 'GET', cookie, form, body, type, same
@@ -144,6 +144,9 @@ function hreq(base, host, path, { method = 'GET', cookie, form, body, type, same
await check('beside the player: more from the channel, never the one playing', async () => {
await check('beside the player: more from the channel, never the one playing', async () => {
const v = await video('/v/301');
const v = await video('/v/301');
const more = (v.text.split('<section class="ovv-panel ovv-also"')[1] || '').split('</section>')[0];
const more = (v.text.split('<section class="ovv-panel ovv-also"')[1] || '').split('</section>')[0];
+ // The video may come along (the small player): it carries its title, who made it and its own page.
+ assert.match(v.text, /<video class="ovv-player" data-ov-player data-ov-mini data-ov-mini-title="[^"]+" data-ov-mini-by="[^"]*" data-ov-mini-page="\/(?:watch\?v=[^"]+|v\/301)"/, 'a recording says it may come along');
+ assert.match(v.text, /<script src="[^"]*mini[^"]*\.js[^"]*" defer><\/script>/, 'every Video page loads the small player');
assert.ok(!/Kim labelled/.test(more), 'never the recording playing');
assert.ok(!/Kim labelled/.test(more), 'never the recording playing');
assert.ok(!/Kim clip/.test(more), 'its own clips are in Clips from this stream, not again here');
assert.ok(!/Kim clip/.test(more), 'its own clips are in Clips from this stream, not again here');
const c = await video('/c/401');
const c = await video('/c/401');
@@ -14,9 +14,9 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
@@ -14,9 +14,9 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
// Budgets are set from the numbers measured on 2026-10-08 (the real ones, fresh database), rounded up with about 10%
// Budgets are set from the numbers measured on 2026-10-08 (the real ones, fresh database), rounded up with about 10%
// headroom; file counts are the measured counts and external files stay at 0.
// headroom; file counts are the measured counts and external files stay at 0.
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 60.4, // 60.4: measured 58.4 with openvibe-shared 4.161.0, plus room for 4.162.0–4.163.0, 2026-11-09
+ htmlRawKB: 62.5, // 62.5 (2026-11-10: measured 61.8 with openvibe-shared 4.192.0: the small player's script tag, the footer's online strip, the site name's dropdown); 60.4: measured 58.4 with openvibe-shared 4.161.0, plus room for 4.162.0–4.163.0, 2026-11-09
// FAQPage JSON-LD) and next steps on the network; was 29.7 at 27.5
// FAQPage JSON-LD) and next steps on the network; was 29.7 at 27.5
- htmlBrotliKB: 12.6, // 12.3 (2026-11-07: nine features on the home); 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1
+ htmlBrotliKB: 13.5, // 13.5 (2026-11-10: measured 13.1 with openvibe-shared 4.192.0, as above); 12.6 before; 12.3 (2026-11-07: nine features on the home); 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1
jsFiles: 6, // 6: /shared/ov-glyphs.js in place of Font Awesome's stylesheet (2026-11-01, plan L54 K4); was: 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
jsFiles: 6, // 6: /shared/ov-glyphs.js in place of Font Awesome's stylesheet (2026-11-01, plan L54 K4); was: 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
jsRawKB: 401, // 400.5 with openvibe-shared 4.80.0: 33 more solid glyphs in ov-glyphs.js (2026-11-01); 393.9 with openvibe-shared 4.77.0: the navbar loads the glyphs when a page first shows an icon (2026-11-01); 393: 393: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4; Font Awesome's CSS and font were external, uncounted, and heavier); 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8
jsRawKB: 401, // 400.5 with openvibe-shared 4.80.0: 33 more solid glyphs in ov-glyphs.js (2026-11-01); 393.9 with openvibe-shared 4.77.0: the navbar loads the glyphs when a page first shows an icon (2026-11-01); 393: 393: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4; Font Awesome's CSS and font were external, uncounted, and heavier); 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8
jsBrotliKB: 98, // 97.6 with openvibe-shared 4.80.0 (2026-11-01); 96.1 with openvibe-shared 4.77.0 (2026-11-01); 96: 96: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4); 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5
jsBrotliKB: 98, // 97.6 with openvibe-shared 4.80.0 (2026-11-01); 96.1 with openvibe-shared 4.77.0 (2026-11-01); 96: 96: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4); 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5
@@ -348,7 +348,7 @@ function countOccurrences(haystack, needle) {
@@ -348,7 +348,7 @@ function countOccurrences(haystack, needle) {
assert.ok(r.text.includes('playsinline'));
assert.ok(r.text.includes('playsinline'));
assert.ok(r.text.includes('data-ovv-video'));
assert.ok(r.text.includes('data-ovv-video'));
// The network's player (openvibe-shared player.js/css): marked, and both files load on the page.
// The network's player (openvibe-shared player.js/css): marked, and both files load on the page.
- assert.ok(/<video class="ovv-player" data-ov-player controls /.test(r.text), 'the video takes the network player');
+ assert.ok(/<video class="ovv-player" data-ov-player data-ov-mini data-ov-mini-title="[^"]+" data-ov-mini-by="[^"]*" data-ov-mini-page="[^"]+" controls /.test(r.text), 'the video takes the network player, and may come along in the small player');
// The streamer's face for an audio-only recording (openvibe-shared 4.90 player.js, plan R4): Network's avatar URL.
// The streamer's face for an audio-only recording (openvibe-shared 4.90 player.js, plan R4): Network's avatar URL.
assert.match(r.text, /<video class="ovv-player"[^>]* data-ov-avatar="[^"]+\/avatar\/streamer"/, 'the player knows the streamer\'s face');
assert.match(r.text, /<video class="ovv-player"[^>]* data-ov-avatar="[^"]+\/avatar\/streamer"/, 'the player knows the streamer\'s face');
assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(r.text) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(r.text), 'player.css and player.js load');
assert.ok(/<link rel="stylesheet" href="\/shared\/player\.css\?v=[0-9a-f]+">/.test(r.text) && /<script src="\/shared\/player\.js\?v=[0-9a-f]+" defer><\/script>/.test(r.text), 'player.css and player.js load');