openvibers/openvibe.live Commits
Home live grid on Events realtime (WS-F task 1): public live.stream.started/ended from events.openvibe.network/realtime/stream refresh the grid the moment a stream starts or ends; the 12 s poll stays as the fallback and, while realtime is connected, only runs once a minute; a native EventSource (the SDK's browser transport) instead of the SDK's 128 KB bundle; CSP connect-src allows events.openvibe.network
Changes
@@@ -9,6 +9,7 @@],"js": ["/js/ov-rail.js",++ "/js/ov-live-realtime.js","/js/app-home.js","/js/ov-odometer.js","/js/ov-density.js",@@@ -1910,15 +1910,31 @@ function refreshHomeSections() {if (typeof loadHomeLeaderboards === 'function') loadHomeLeaderboards();}++// The live grid follows OpenVibe.Events realtime (public/js/ov-live-realtime.js): a stream that++// starts or ends refreshes it at once. The 12 s poll stays as the fallback, and while realtime is++// connected it only runs once a minute as a safety net.++let _homeRtOff = null, _homeRtDebounce = null, _homeLastLiveRefresh = 0;++function _homeRealtimeRefresh() {++ clearTimeout(_homeRtDebounce);++ _homeRtDebounce = setTimeout(() => { _homeLastLiveRefresh = Date.now(); refreshHomeLive(); }, 400);++}++function _homeLiveTick() {++ if (window.OVLiveRealtime && OVLiveRealtime.connected && Date.now() - _homeLastLiveRefresh < 60000) return;++ _homeLastLiveRefresh = Date.now();++ refreshHomeLive();++}function startHomeRefresh() {stopHomeRefresh();_initUptimeTooltip();-- _homeLiveTimer = setInterval(refreshHomeLive, 12000);++ _homeLiveTimer = setInterval(_homeLiveTick, 12000);_homeSectionsTimer = setInterval(refreshHomeSections, 60000);++ if (window.OVLiveRealtime) _homeRtOff = OVLiveRealtime.on((kind) => { if (kind !== 'error') _homeRealtimeRefresh(); });}function stopHomeRefresh() {if (_homeLiveTimer) { clearInterval(_homeLiveTimer); _homeLiveTimer = null; }if (_homeSectionsTimer) { clearInterval(_homeSectionsTimer); _homeSectionsTimer = null; }++ if (_homeRtOff) { _homeRtOff(); _homeRtOff = null; }++ clearTimeout(_homeRtDebounce);} // populated on channel load, used for filter bar// Homepage pagination state// The home page used to open with 46 cards across four list sections, which made it enormous andnnew file mode 100644@@@ -0,0 +1,45 @@++/**++ * Live's stream lifecycle over OpenVibe.Events realtime (roadmap WS-F task 1): the public++ * live.stream.started / live.stream.ended events on events.openvibe.network/realtime/stream (SSE).++ * The home page's live grid refreshes the moment a stream starts or ends, and polls only as a++ * safety net while this is connected (public/js/app-home.js startHomeRefresh).++ *++ * A native EventSource, the same transport openvibe-sdk/realtime uses in a browser, without the SDK's++ * 128 KB browser bundle on the home page. Resume needs no bookkeeping here: every event, reconnect++ * or gap just refetches the grid.++ *++ * const off = OVLiveRealtime.on((kind, event) => { … }); // kind: 'open' | 'event' | 'gap' | 'error'++ * OVLiveRealtime.connected // true while the stream is open++ */++(function () {++ 'use strict';++ const URL_ = 'https://events.openvibe.network/realtime/stream?topics=live.stream.started,live.stream.ended';++ const listeners = new Set();++ let es = null;++ let connected = false;++++ const emit = (kind, event) => { for (const fn of listeners) { try { fn(kind, event); } catch { /* a listener's problem */ } } };++ function start() {++ if (es || typeof EventSource === 'undefined') return;++ es = new EventSource(URL_);++ es.onopen = () => { connected = true; emit('open'); };++ // EventSource reconnects by itself (with Last-Event-ID); until then the page polls.++ es.onerror = () => { connected = false; emit('error'); };++ es.onmessage = (m) => {++ let d = null;++ try { d = JSON.parse(m.data); } catch { return; }++ if (d && d.event && /^live\.stream\.(started|ended)$/.test(d.event.event_type)) emit('event', d.event);++ };++ es.addEventListener('gap', () => emit('gap'));++ }++ function stop() { if (es) { es.close(); es = null; } connected = false; }++++ window.OVLiveRealtime = {++ on(fn) {++ listeners.add(fn);++ start();++ return () => { listeners.delete(fn); if (!listeners.size) stop(); };++ },++ get connected() { return connected; },++ };++})();@@@ -290,7 +290,7 @@ app.use(helmet({// every queued item renders as a broken image, which is what the media// request tab was doing for every YouTube link.imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com"],-- connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", whipConnectSrc],++ connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://events.openvibe.network", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", whipConnectSrc],// VODs/clips play from openvibe.media (the /api proxies 302 there), which may// itself redirect to presigned B2/R2 object-store URLs — all must be allowed// or the browser blocks the media element.
@@ -9,6 +9,7 @@
@@ -9,6 +9,7 @@
],
],
"js": [
"js": [
"/js/ov-rail.js",
"/js/ov-rail.js",
+ "/js/ov-live-realtime.js",
"/js/app-home.js",
"/js/app-home.js",
"/js/ov-odometer.js",
"/js/ov-odometer.js",
"/js/ov-density.js",
"/js/ov-density.js",
@@ -1910,15 +1910,31 @@ function refreshHomeSections() {
@@ -1910,15 +1910,31 @@ function refreshHomeSections() {
if (typeof loadHomeLeaderboards === 'function') loadHomeLeaderboards();
if (typeof loadHomeLeaderboards === 'function') loadHomeLeaderboards();
}
}
+// The live grid follows OpenVibe.Events realtime (public/js/ov-live-realtime.js): a stream that
+// starts or ends refreshes it at once. The 12 s poll stays as the fallback, and while realtime is
+// connected it only runs once a minute as a safety net.
+let _homeRtOff = null, _homeRtDebounce = null, _homeLastLiveRefresh = 0;
+function _homeRealtimeRefresh() {
+ clearTimeout(_homeRtDebounce);
+ _homeRtDebounce = setTimeout(() => { _homeLastLiveRefresh = Date.now(); refreshHomeLive(); }, 400);
+}
+function _homeLiveTick() {
+ if (window.OVLiveRealtime && OVLiveRealtime.connected && Date.now() - _homeLastLiveRefresh < 60000) return;
+ _homeLastLiveRefresh = Date.now();
+ refreshHomeLive();
+}
function startHomeRefresh() {
function startHomeRefresh() {
stopHomeRefresh();
stopHomeRefresh();
_initUptimeTooltip();
_initUptimeTooltip();
- _homeLiveTimer = setInterval(refreshHomeLive, 12000);
+ _homeLiveTimer = setInterval(_homeLiveTick, 12000);
_homeSectionsTimer = setInterval(refreshHomeSections, 60000);
_homeSectionsTimer = setInterval(refreshHomeSections, 60000);
+ if (window.OVLiveRealtime) _homeRtOff = OVLiveRealtime.on((kind) => { if (kind !== 'error') _homeRealtimeRefresh(); });
}
}
function stopHomeRefresh() {
function stopHomeRefresh() {
if (_homeLiveTimer) { clearInterval(_homeLiveTimer); _homeLiveTimer = null; }
if (_homeLiveTimer) { clearInterval(_homeLiveTimer); _homeLiveTimer = null; }
if (_homeSectionsTimer) { clearInterval(_homeSectionsTimer); _homeSectionsTimer = null; }
if (_homeSectionsTimer) { clearInterval(_homeSectionsTimer); _homeSectionsTimer = null; }
+ if (_homeRtOff) { _homeRtOff(); _homeRtOff = null; }
+ clearTimeout(_homeRtDebounce);
} // populated on channel load, used for filter bar
} // populated on channel load, used for filter bar
// Homepage pagination state
// Homepage pagination state
// The home page used to open with 46 cards across four list sections, which made it enormous and
// The home page used to open with 46 cards across four list sections, which made it enormous and
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+/**
+ * Live's stream lifecycle over OpenVibe.Events realtime (roadmap WS-F task 1): the public
+ * live.stream.started / live.stream.ended events on events.openvibe.network/realtime/stream (SSE).
+ * The home page's live grid refreshes the moment a stream starts or ends, and polls only as a
+ * safety net while this is connected (public/js/app-home.js startHomeRefresh).
+ *
+ * A native EventSource, the same transport openvibe-sdk/realtime uses in a browser, without the SDK's
+ * 128 KB browser bundle on the home page. Resume needs no bookkeeping here: every event, reconnect
+ * or gap just refetches the grid.
+ *
+ * const off = OVLiveRealtime.on((kind, event) => { … }); // kind: 'open' | 'event' | 'gap' | 'error'
+ * OVLiveRealtime.connected // true while the stream is open
+ */
+(function () {
+ 'use strict';
+ const URL_ = 'https://events.openvibe.network/realtime/stream?topics=live.stream.started,live.stream.ended';
+ const listeners = new Set();
+ let es = null;
+ let connected = false;
+
+ const emit = (kind, event) => { for (const fn of listeners) { try { fn(kind, event); } catch { /* a listener's problem */ } } };
+ function start() {
+ if (es || typeof EventSource === 'undefined') return;
+ es = new EventSource(URL_);
+ es.onopen = () => { connected = true; emit('open'); };
+ // EventSource reconnects by itself (with Last-Event-ID); until then the page polls.
+ es.onerror = () => { connected = false; emit('error'); };
+ es.onmessage = (m) => {
+ let d = null;
+ try { d = JSON.parse(m.data); } catch { return; }
+ if (d && d.event && /^live\.stream\.(started|ended)$/.test(d.event.event_type)) emit('event', d.event);
+ };
+ es.addEventListener('gap', () => emit('gap'));
+ }
+ function stop() { if (es) { es.close(); es = null; } connected = false; }
+
+ window.OVLiveRealtime = {
+ on(fn) {
+ listeners.add(fn);
+ start();
+ return () => { listeners.delete(fn); if (!listeners.size) stop(); };
+ },
+ get connected() { return connected; },
+ };
+})();
@@ -290,7 +290,7 @@ app.use(helmet({
@@ -290,7 +290,7 @@ app.use(helmet({
// every queued item renders as a broken image, which is what the media
// every queued item renders as a broken image, which is what the media
// request tab was doing for every YouTube link.
// request tab was doing for every YouTube link.
imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com"],
imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com"],
- connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", whipConnectSrc],
+ connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://events.openvibe.network", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", whipConnectSrc],
// VODs/clips play from openvibe.media (the /api proxies 302 there), which may
// VODs/clips play from openvibe.media (the /api proxies 302 there), which may
// itself redirect to presigned B2/R2 object-store URLs — all must be allowed
// itself redirect to presigned B2/R2 object-store URLs — all must be allowed
// or the browser blocks the media element.
// or the browser blocks the media element.