openvibers/openvibe.live Commits

openvibe-shared v4.15.0 (was v4.14.0), and a channel's footer is compact only while it is live (#387)

OP OpenVibers committed 17 days ago · b3ff40fa1d · 8 files, +75 −13

* openvibe-shared v4.15.0 (was v4.14.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v4.15.0 release.

* A channel's footer is compact while its stream plays and full while it is offline (the shared footer's claim)

* Budget: jsBrotliKB 266.5 (measured 265.3 with openvibe-shared 4.15.0's footer)

Changes

ddiff --git a/README.md b/README.md
iindex e763dff1..82827d84 100644
---- a/README.md
++++ b/README.md
@@@ -177,5 +177,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
<!-- versions:start -->
- openvibe-contracts: v0.214.0
- openvibe-sdk: v0.65.0
--- openvibe-shared: v4.14.0
++- openvibe-shared: v4.15.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 2f9df393..a7722013 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,7 +7,7 @@
"domain": "openvibe.live (www, ingest, whip)",
"contracts": "openvibe-contracts v0.214.0",
"sdk": "openvibe-sdk v0.65.0",
-- "shared": "openvibe-shared v4.14.0",
++ "shared": "openvibe-shared v4.15.0",
"capabilities": [
"live.chat_context.read",
"live.chat_effects.write",
@@@ -32,5 +32,5 @@
"per-actor limits on API writes; account export and deletion contributions; follows on OpenVibe.Network",
"channel ↔ OpenVibe.Bot robot binding, its dashboard field and the Bot panel iframe on the channel page behind LIVE_BOT_EMBED (off by default; on in production)"
],
-- "updated": "2026-10-26"
++ "updated": "2026-10-27"
}
ddiff --git a/package-lock.json b/package-lock.json
iindex efcc88d6..f5272e3a 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -25,7 +25,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.14.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.15.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
@@@ -2688,9 +2688,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.14.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.14.0",
-- "integrity": "sha512-ZDGfX/fDOhpAfEvT+TEmj4Uz5spyE+gjqdEfY+iE623MLFQq6SFCj+qhdDxWsDS3X0UESAcf445OxSevbPOV9g==",
++ "version": "4.15.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.15.0",
++ "integrity": "sha512-v5iz3ZlLSuT3uYwHjBkR4ryeAxVA2ejmrrYSDAkaaXeYfTb4mIPjwoiM7V3tT4jA+Q2SJu1B+XUL5FkOt34ghg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex ab37f9c3..cf16776d 100644
---- a/package.json
++++ b/package.json
@@@ -44,7 +44,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.14.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.15.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
ddiff --git a/public/js/app-channel.js b/public/js/app-channel.js
iindex dfb6956e..ad3e8e66 100644
---- a/public/js/app-channel.js
++++ b/public/js/app-channel.js
@@@ -797,6 +797,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
// ── LIVE STATE ──
document.getElementById('ch-live-area').style.display = '';
document.getElementById('ch-offline-area').style.display = 'none';
++ _channelFooter(true);
_placeBelowFold();
// Populate streamer info bar (below video). Avatar image (letter fallback)
@@@ -877,6 +878,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
}
document.getElementById('ch-live-area').style.display = 'none';
document.getElementById('ch-offline-area').style.display = '';
++ _channelFooter(false);
_placeBelowFold();
// Populate offline header
@@@ -2302,6 +2304,22 @@ async function loadChannelAiTimeline(username) {
}
}
++// The footer while the stream plays is the quiet one-line footer; an offline channel gets the whole footer, where someone
++// looks for where to go next (owner, 2026-10-27). The shared footer's claim (openvibe-shared 4.15): it holds while the
++// stream is live and is released when the stream ends or the page is left (public/js/footer-live.js keeps channels full).
++let _footerRelease = null;
++function _channelFooter(live) {
++ const F = window.OpenVibeFooter;
++ if (!F || typeof F.claim !== 'function') return;
++ if (live && !_footerRelease) {
++ _footerRelease = F.claim('compact', 'channel-live');
++ if (window.ov) ov.scope().onDispose(() => _channelFooter(false));
++ } else if (!live && _footerRelease) {
++ _footerRelease();
++ _footerRelease = null;
++ }
++}
++
// The channel's OpenVibe.Bot robot panel (roadmap T15 R9). `bot_embed` is only in the channel JSON with
// LIVE_BOT_EMBED on, and public/js/bot-embed.js (feature "botEmbed") only loads for a bound channel, so with
// the flag off this does nothing. The frame goes when the route ends, closing its socket to Bot.
ddiff --git a/public/js/footer-live.js b/public/js/footer-live.js
iindex d8014cb9..a86061ac 100644
---- a/public/js/footer-live.js
++++ b/public/js/footer-live.js
@@@ -5,9 +5,11 @@
* openvibe-shared/footer.js, which every OpenVibe property loads. This file supplies only the
* links that belong to Live, and decides which variant a page should use.
*
-- * Variant policy: a streamer's channel, a player and the broadcast desk are places people are
-- * *doing* something, so they get the one-line compact footer. Browse and landing pages, where
-- * someone may be looking for where to go next, get the full one.
++ * Variant policy: a player and the broadcast desk are places people are *doing* something, so they
++ * get the one-line compact footer. Browse and landing pages, where someone may be looking for where
++ * to go next, get the full one. A streamer's channel is full while offline and compact while its
++ * stream plays: the channel page claims compact for as long as the stream is live (app-channel.js
++ * _channelFooter, openvibe-shared 4.15 claim()); with an older footer it stays compact.
*/
(function () {
'use strict';
@@@ -39,7 +41,7 @@
function variantForPage() {
const p = location.pathname;
if (p === '/' || p === '') return 'full';
-- if (/^\/@/.test(p)) return 'compact'; // streamer channels
++ if (/^\/@/.test(p)) return window.OpenVibeFooter && typeof OpenVibeFooter.claim === 'function' ? 'full' : 'compact'; // channels: full, compact while live
const seg = p.split('/').filter(Boolean)[0] || '';
if (COMPACT_PAGES.includes(seg)) return 'compact';
if (seg === 'vod' || seg === 'clip' || seg === 'recap') return 'compact';
ddiff --git a/scripts/perf/check-budgets.js b/scripts/perf/check-budgets.js
iindex e4ccaf90..b33af251 100644
---- a/scripts/perf/check-budgets.js
++++ b/scripts/perf/check-budgets.js
@@@ -23,7 +23,7 @@ const BUDGETS = {
htmlBrotliKB: 30,
jsFiles: 26, // measured 23 (incl. route scripts for "/")
jsRawKB: 1150, // measured ~1000
-- jsBrotliKB: 265, // measured 263.3 (2026-10-23: openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
++ jsBrotliKB: 266.5, // measured 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)
cssBrotliKB: 55, // measured ~47.5 (was ~130)
};
ddiff --git a/test/footer-channel-claim.test.js b/test/footer-channel-claim.test.js
nnew file mode 100644
iindex 00000000..8dbcb411
---- /dev/null
++++ b/test/footer-channel-claim.test.js
@@@ -0,0 +1,42 @@
++/**
++ * A channel's footer: compact while its stream plays, full while it is offline (owner, 2026-10-27: "you can use compact
++ * footer on the live streamer channel when they are live only like openvibe.live/@goosely but it should be full mode
++ * when not live"). The channel page claims compact from the shared footer (openvibe-shared 4.15 claim()) when it shows the
++ * live state and releases it when it shows the offline state or the page is left; the page's own variant for a channel
++ * is full. With an older shared footer (no claim) a channel stays compact, as before.
++ *
++ * node test/footer-channel-claim.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
++const app = read('public/js/app-channel.js');
++const fn = app.slice(app.indexOf('function _channelFooter('), app.indexOf('\n}\n', app.indexOf('function _channelFooter(')) + 3);
++
++// The claim, run against a fake shared footer.
++const calls = [];
++const disposers = [];
++const ctx = vm.createContext({
++ window: { OpenVibeFooter: { claim: (v, k) => { calls.push(['claim', v, k]); return () => calls.push(['release', k]); } }, ov: true },
++ ov: { scope: () => ({ onDispose: (f) => disposers.push(f) }) },
++});
++vm.runInContext(`let _footerRelease = null;\n${fn}\nthis._channelFooter = _channelFooter;`, ctx);
++ctx._channelFooter(true);
++ctx._channelFooter(true);
++assert.deepStrictEqual(calls, [['claim', 'compact', 'channel-live']], 'one claim while live, however often the live state is drawn');
++ctx._channelFooter(false);
++assert.deepStrictEqual(calls.at(-1), ['release', 'channel-live'], 'released when the channel goes offline');
++ctx._channelFooter(true);
++disposers.forEach((d) => d());
++assert.deepStrictEqual(calls.at(-1), ['release', 'channel-live'], 'released when the page is left');
++
++// Both states draw it; the channel's own variant is full only when the footer can be claimed.
++assert.ok(/\/\/ ── LIVE STATE ──[\s\S]{0,300}_channelFooter\(true\);/.test(app), 'the live state claims');
++assert.ok(/getElementById\('ch-offline-area'\)\.style\.display = '';\n\s*_channelFooter\(false\);/.test(app), 'the offline state releases');
++assert.ok(read('public/js/footer-live.js').includes("if (/^\\/@/.test(p)) return window.OpenVibeFooter && typeof OpenVibeFooter.claim === 'function' ? 'full' : 'compact';"));
++
++console.log('channel footer: compact while live, full while offline');