openvibers/openvibe.shared Commits
1.2.1: release-watch treats playing media and live capture as protected
Changes
@@@ -4,6 +4,11 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).++## 1.2.1 — 2026-09-23++++- `release-watch.js` never reloads a tab that is playing `<video>`/`<audio>` or holds a live++ camera/microphone stream (viewers look idle while watching; broadcasters must never be cut).++## 1.2.0 — 2026-09-23- `navbar.js` loads `release-watch.js` (from the same place it was loaded from) four seconds after@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "1.2.0",++ "version": "1.2.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","exports": {@@@ -9,8 +9,8 @@* - the server requires it (min_client_release is the running release) or the new release is older* than the mixed-version window: reload automatically, but ONLY when it is safe — the tab is* hidden or idle for 2 minutes, no text field has focus, and nothing on the page is protected-- * (a form marked data-dirty="true", an element with data-ov-protected, or window.OVProtected()-- * returning true for uploads, calls, broadcasts and recordings).++ * (a form marked data-dirty="true", an element with data-ov-protected, a playing <video>/<audio>++ * or live camera/mic stream, or window.OVProtected() returning true for uploads and calls).*/(function (root) {if (typeof document === 'undefined' || root.OVRelease) return;@@@ -33,6 +33,14 @@const a = document.activeElement;if (a && (a.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName || ''))) return true;if (document.querySelector('form[data-dirty="true"], [data-ov-protected]')) return true;++ // Someone watching or broadcasting: a playing <video>/<audio>, or a live camera/mic stream.++ try {++ for (const m of document.querySelectorAll('video, audio')) {++ if (!m.paused && !m.ended) return true;++ const so = m.srcObject;++ if (so && typeof so.getTracks === 'function' && so.getTracks().some((t) => t.readyState === 'live')) return true;++ }++ } catch { return true; }try { if (typeof root.OVProtected === 'function' && root.OVProtected()) return true; } catch { return true; }return !document.hidden && Date.now() - lastInput < IDLE_MS;}@@@ -35,6 +35,7 @@ const { createRelease } = require('../release');hidden,activeElement: active,querySelector: (sel) => (sel.startsWith('meta') ? { content: 'aaaaaaa', getAttribute: () => '/release.json' } : null),++ querySelectorAll: () => [],addEventListener: (t, f) => { docListeners[t] = f; },},};@@@ -75,6 +76,17 @@ const { createRelease } = require('../release');await p.ctx.OVRelease.check();assert.strictEqual(p.reloads(), 0, 'never during a protected session (upload, call, broadcast)');++ {++ const playing = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: true });++ playing.ctx.document.querySelectorAll = (sel) => (sel === 'video, audio' ? [{ paused: false, ended: false }] : []);++ await playing.ctx.OVRelease.check();++ assert.strictEqual(playing.reloads(), 0, 'never while a stream is playing');++ const capture = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: true });++ capture.ctx.document.querySelectorAll = () => [{ paused: true, ended: false, srcObject: { getTracks: () => [{ readyState: 'live' }] } }];++ await capture.ctx.OVRelease.check();++ assert.strictEqual(capture.reloads(), 0, 'never while the camera/mic is live');++ }++p = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: false });await p.ctx.OVRelease.check();assert.strictEqual(p.reloads(), 0, 'a visible tab someone just used is not reloaded');@@@ -88,7 +100,7 @@ const { createRelease } = require('../release');location: { reload: () => {} }, addEventListener: (t, f) => { listeners[t] = f; },OpenVibeUI: { toast: (msg, o) => toasts.push({ msg, o }) },fetch: async () => ({ ok: true, json: async () => ({ release: served, released_at: fresh, min_client_release: null, mixed_version_window_hours: 24 }) }),-- document: { hidden: true, activeElement: null, querySelector: () => null, addEventListener: () => {} },++ document: { hidden: true, activeElement: null, querySelector: () => null, querySelectorAll: () => [], addEventListener: () => {} },};ctx.window = ctx; ctx.globalThis = ctx; vm.createContext(ctx);vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'release-watch.js'), 'utf8'), ctx);@@@ -105,7 +117,7 @@ const { createRelease } = require('../release');const ctx = {console, Date, JSON, Promise, setInterval: () => 1, clearInterval: () => {}, location: { reload: () => {} }, addEventListener: () => {},fetch: async () => { calls++; return { ok: false, json: async () => null }; },-- document: { hidden: true, activeElement: null, querySelector: () => null, addEventListener: () => {} },++ document: { hidden: true, activeElement: null, querySelector: () => null, querySelectorAll: () => [], addEventListener: () => {} },};ctx.window = ctx; ctx.globalThis = ctx; vm.createContext(ctx);vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'release-watch.js'), 'utf8'), ctx);
@@ -4,6 +4,11 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,11 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 1.2.1 — 2026-09-23
+
+- `release-watch.js` never reloads a tab that is playing `<video>`/`<audio>` or holds a live
+ camera/microphone stream (viewers look idle while watching; broadcasters must never be cut).
+
## 1.2.0 — 2026-09-23
## 1.2.0 — 2026-09-23
- `navbar.js` loads `release-watch.js` (from the same place it was loaded from) four seconds after
- `navbar.js` loads `release-watch.js` (from the same place it was loaded from) four seconds after
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"exports": {
"exports": {
@@ -9,8 +9,8 @@
@@ -9,8 +9,8 @@
* - the server requires it (min_client_release is the running release) or the new release is older
* - the server requires it (min_client_release is the running release) or the new release is older
* than the mixed-version window: reload automatically, but ONLY when it is safe — the tab is
* than the mixed-version window: reload automatically, but ONLY when it is safe — the tab is
* hidden or idle for 2 minutes, no text field has focus, and nothing on the page is protected
* hidden or idle for 2 minutes, no text field has focus, and nothing on the page is protected
- * (a form marked data-dirty="true", an element with data-ov-protected, or window.OVProtected()
- * returning true for uploads, calls, broadcasts and recordings).
+ * (a form marked data-dirty="true", an element with data-ov-protected, a playing <video>/<audio>
+ * or live camera/mic stream, or window.OVProtected() returning true for uploads and calls).
*/
*/
(function (root) {
(function (root) {
if (typeof document === 'undefined' || root.OVRelease) return;
if (typeof document === 'undefined' || root.OVRelease) return;
@@ -33,6 +33,14 @@
@@ -33,6 +33,14 @@
const a = document.activeElement;
const a = document.activeElement;
if (a && (a.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName || ''))) return true;
if (a && (a.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName || ''))) return true;
if (document.querySelector('form[data-dirty="true"], [data-ov-protected]')) return true;
if (document.querySelector('form[data-dirty="true"], [data-ov-protected]')) return true;
+ // Someone watching or broadcasting: a playing <video>/<audio>, or a live camera/mic stream.
+ try {
+ for (const m of document.querySelectorAll('video, audio')) {
+ if (!m.paused && !m.ended) return true;
+ const so = m.srcObject;
+ if (so && typeof so.getTracks === 'function' && so.getTracks().some((t) => t.readyState === 'live')) return true;
+ }
+ } catch { return true; }
try { if (typeof root.OVProtected === 'function' && root.OVProtected()) return true; } catch { return true; }
try { if (typeof root.OVProtected === 'function' && root.OVProtected()) return true; } catch { return true; }
return !document.hidden && Date.now() - lastInput < IDLE_MS;
return !document.hidden && Date.now() - lastInput < IDLE_MS;
}
}
@@ -35,6 +35,7 @@ const { createRelease } = require('../release');
@@ -35,6 +35,7 @@ const { createRelease } = require('../release');
hidden,
hidden,
activeElement: active,
activeElement: active,
querySelector: (sel) => (sel.startsWith('meta') ? { content: 'aaaaaaa', getAttribute: () => '/release.json' } : null),
querySelector: (sel) => (sel.startsWith('meta') ? { content: 'aaaaaaa', getAttribute: () => '/release.json' } : null),
+ querySelectorAll: () => [],
addEventListener: (t, f) => { docListeners[t] = f; },
addEventListener: (t, f) => { docListeners[t] = f; },
},
},
};
};
@@ -75,6 +76,17 @@ const { createRelease } = require('../release');
@@ -75,6 +76,17 @@ const { createRelease } = require('../release');
await p.ctx.OVRelease.check();
await p.ctx.OVRelease.check();
assert.strictEqual(p.reloads(), 0, 'never during a protected session (upload, call, broadcast)');
assert.strictEqual(p.reloads(), 0, 'never during a protected session (upload, call, broadcast)');
+ {
+ const playing = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: true });
+ playing.ctx.document.querySelectorAll = (sel) => (sel === 'video, audio' ? [{ paused: false, ended: false }] : []);
+ await playing.ctx.OVRelease.check();
+ assert.strictEqual(playing.reloads(), 0, 'never while a stream is playing');
+ const capture = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: true });
+ capture.ctx.document.querySelectorAll = () => [{ paused: true, ended: false, srcObject: { getTracks: () => [{ readyState: 'live' }] } }];
+ await capture.ctx.OVRelease.check();
+ assert.strictEqual(capture.reloads(), 0, 'never while the camera/mic is live');
+ }
+
p = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: false });
p = page({ serverRelease: 'bbbbbbb', releasedAt: stale, hidden: false });
await p.ctx.OVRelease.check();
await p.ctx.OVRelease.check();
assert.strictEqual(p.reloads(), 0, 'a visible tab someone just used is not reloaded');
assert.strictEqual(p.reloads(), 0, 'a visible tab someone just used is not reloaded');
@@ -88,7 +100,7 @@ const { createRelease } = require('../release');
@@ -88,7 +100,7 @@ const { createRelease } = require('../release');
location: { reload: () => {} }, addEventListener: (t, f) => { listeners[t] = f; },
location: { reload: () => {} }, addEventListener: (t, f) => { listeners[t] = f; },
OpenVibeUI: { toast: (msg, o) => toasts.push({ msg, o }) },
OpenVibeUI: { toast: (msg, o) => toasts.push({ msg, o }) },
fetch: async () => ({ ok: true, json: async () => ({ release: served, released_at: fresh, min_client_release: null, mixed_version_window_hours: 24 }) }),
fetch: async () => ({ ok: true, json: async () => ({ release: served, released_at: fresh, min_client_release: null, mixed_version_window_hours: 24 }) }),
- document: { hidden: true, activeElement: null, querySelector: () => null, addEventListener: () => {} },
+ document: { hidden: true, activeElement: null, querySelector: () => null, querySelectorAll: () => [], addEventListener: () => {} },
};
};
ctx.window = ctx; ctx.globalThis = ctx; vm.createContext(ctx);
ctx.window = ctx; ctx.globalThis = ctx; vm.createContext(ctx);
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'release-watch.js'), 'utf8'), ctx);
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'release-watch.js'), 'utf8'), ctx);
@@ -105,7 +117,7 @@ const { createRelease } = require('../release');
@@ -105,7 +117,7 @@ const { createRelease } = require('../release');
const ctx = {
const ctx = {
console, Date, JSON, Promise, setInterval: () => 1, clearInterval: () => {}, location: { reload: () => {} }, addEventListener: () => {},
console, Date, JSON, Promise, setInterval: () => 1, clearInterval: () => {}, location: { reload: () => {} }, addEventListener: () => {},
fetch: async () => { calls++; return { ok: false, json: async () => null }; },
fetch: async () => { calls++; return { ok: false, json: async () => null }; },
- document: { hidden: true, activeElement: null, querySelector: () => null, addEventListener: () => {} },
+ document: { hidden: true, activeElement: null, querySelector: () => null, querySelectorAll: () => [], addEventListener: () => {} },
};
};
ctx.window = ctx; ctx.globalThis = ctx; vm.createContext(ctx);
ctx.window = ctx; ctx.globalThis = ctx; vm.createContext(ctx);
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'release-watch.js'), 'utf8'), ctx);
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'release-watch.js'), 'utf8'), ctx);