openvibers/openvibe.live Commits
Test privately: ten seconds of the preview, recorded on this device and played back (#545)
Changes
@@@ -3304,3 +3304,26 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + var(--ovna.gl-pv-off[aria-pressed="true"] i::before { content: "\f06e"; }@media (pointer: coarse) { .gl-pv-off { width: 44px; height: 44px; } }@media (prefers-reduced-motion: reduce) { .gl-preview video, .gl-pv-lvl i { transition: none; } }++++/* Test privately: the quiet button under Go live; while recording, a red dot and the seconds left; then the test plays++ back over the preview with Done. */++.gl-test-btn { all: unset; box-sizing: border-box; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 4px 8px; width: 100%; min-height: 44px; margin-top: 8px;++ padding: 6px 14px; border-radius: 14px; cursor: pointer; font-size: 0.9rem; font-weight: 700; color: var(--text-primary);++ border: 1px solid var(--border); background: color-mix(in srgb, var(--text-primary) 4%, transparent); transition: background-color var(--ov-dur-2, .15s), border-color var(--ov-dur-2, .15s); }++.gl-test-btn i { color: var(--text-muted); }++.gl-test-btn small { font-size: 0.74rem; font-weight: 500; color: var(--text-muted); }++.gl-test-btn:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }++.gl-test-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }++.gl-test-btn.is-rec { border-color: color-mix(in srgb, var(--live-red, #e11d48) 60%, var(--border)); background: color-mix(in srgb, var(--live-red, #e11d48) 8%, transparent); }++.gl-test-btn.is-rec i { color: var(--live-red, #e11d48); animation: gl-breathe 1.2s var(--ov-ease-in-out, ease-in-out) infinite; }++.gl-test-btn.is-rec small { display: none; }++.gl-test-play { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; background: var(--bg-primary); }++.gl-preview .gl-test-play video { position: static; inset: auto; opacity: 1; transform: none; flex: 1 1 auto; min-height: 0; width: 100%; height: auto; object-fit: contain; background: #000; }++.gl-test-play audio { width: calc(100% - 24px); margin: auto 12px 8px; }++.gl-test-bar { display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px 12px; font-size: 0.78rem; line-height: 1.35; color: var(--text-secondary); }++.gl-test-bar span { flex: 1 1 auto; min-width: 0; }++.gl-test-done { all: unset; box-sizing: border-box; flex: none; min-height: 36px; padding: 0 14px; border-radius: 999px; cursor: pointer; font-weight: 700; font-size: 0.84rem;++ background: var(--accent-strong, var(--accent)); color: var(--on-accent-strong, var(--on-accent, #fff)); }++.gl-test-done:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }++@media (pointer: coarse) { .gl-test-done { min-height: 44px; } }++@media (prefers-reduced-motion: reduce) { .gl-test-btn.is-rec i { animation: none; } }@@@ -392,6 +392,9 @@ function _wsRenderPanel() {onclick="goLiveFromWorkspace()" id="bc-ws-golive-btn"><i class="fa-solid fa-tower-broadcast"></i> Go Live</button>++ ${browserMode === 'screen' ? '' : `<button type="button" class="gl-test-btn" id="gl-test-btn" onclick="_glTestPrivately()">++ <i class="fa-solid fa-circle-dot" aria-hidden="true"></i><span id="gl-test-label">Test privately</span><small>10 seconds, only on this device</small>++ </button>`}<p class="bc-create-reassurance" style="margin-top:6px;font-size:0.82rem"><i class="fa-solid fa-circle-info"></i>Live at <strong>openvibe.live${channelPath(currentUser?.username || 'your-channel', ms.slug || ms.id)}</strong>@@@ -1053,6 +1056,7 @@ function _glPreviewWant() {}function _glPreviewStop() {++ if (_glT.rec) _glTestStop(true);cancelAnimationFrame(_glPv.raf);_glPv.raf = 0;if (_glPv.ctx) { try { _glPv.ctx.close(); } catch { /* */ } _glPv.ctx = null; }@@@ -1181,6 +1185,101 @@ function _glPreviewWatch() {window.addEventListener('offline', _glPreviewSync);}++/* ── Test privately (golive-setup redesign, part 3): ten seconds of what the preview holds, recorded in this browser++ and played back in the card, so the camera and the microphone are heard and seen before anyone else does. Nothing++ is uploaded and nobody is told: the recording lives in this page's memory until Done. ── */++const _glT = { rec: null, chunks: [], timer: 0, left: 0, url: '' };++++function _glTestType(audioOnly) {++ const list = audioOnly ? ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4']++ : ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm', 'video/mp4'];++ if (typeof MediaRecorder === 'undefined') return null;++ return list.find((t) => { try { return MediaRecorder.isTypeSupported(t); } catch { return false; } }) || '';++}++++function _glTestLabel(text) { const l = document.getElementById('gl-test-label'); if (l) l.textContent = text; }++++function _glTestPrivately() {++ if (_glT.rec) { _glTestStop(); return; }++ const stream = _glPv.stream;++ const note = document.getElementById('gl-pv-note');++ if (!stream || !stream.getTracks().some((t) => t.readyState === 'live')) {++ if (note) note.textContent = _glPreviewOff() ? 'Turn the preview on (the eye) to record a test.' : 'Allow the camera and microphone below, then record a test.';++ return;++ }++ const audioOnly = !stream.getVideoTracks().length;++ const type = _glTestType(audioOnly);++ if (type === null) { if (note) note.textContent = 'This browser cannot record a test. Going live still works.'; return; }++ _glTestClear();++ let rec;++ try { rec = new MediaRecorder(stream, type ? { mimeType: type } : undefined); } catch { if (note) note.textContent = 'This browser cannot record a test. Going live still works.'; return; }++ _glT.rec = rec;++ _glT.chunks = [];++ rec.ondataavailable = (e) => { if (e.data && e.data.size) _glT.chunks.push(e.data); };++ rec.onstop = () => _glTestShow(rec.mimeType || type || (audioOnly ? 'audio/webm' : 'video/webm'), audioOnly);++ rec.start(1000);++ const btn = document.getElementById('gl-test-btn');++ if (btn) btn.classList.add('is-rec');++ _glT.left = 10;++ _glTestLabel(`Recording · ${_glT.left} s · tap to stop`);++ _glT.timer = setInterval(() => {++ _glT.left -= 1;++ if (_glT.left <= 0) { _glTestStop(); return; }++ _glTestLabel(`Recording · ${_glT.left} s · tap to stop`);++ }, 1000);++}++++/** Stop recording. discard: drop it (going live, or the page changing) rather than play it back. */++function _glTestStop(discard = false) {++ clearInterval(_glT.timer);++ _glT.timer = 0;++ const rec = _glT.rec;++ _glT.rec = null;++ const btn = document.getElementById('gl-test-btn');++ if (btn) btn.classList.remove('is-rec');++ _glTestLabel('Test privately');++ if (rec && rec.state !== 'inactive') {++ if (discard) rec.onstop = null;++ try { rec.stop(); } catch { /* */ }++ }++ if (discard) _glTestClear();++}++++function _glTestShow(type, audioOnly) {++ const box = document.getElementById('gl-preview');++ if (!box || !_glT.chunks.length) return;++ _glTestClear();++ _glT.url = URL.createObjectURL(new Blob(_glT.chunks, { type }));++ _glT.chunks = [];++ const wrap = document.createElement('div');++ wrap.className = 'gl-test-play';++ wrap.id = 'gl-test-play';++ const media = document.createElement(audioOnly ? 'audio' : 'video');++ media.src = _glT.url;++ media.controls = true;++ media.playsInline = true;++ media.setAttribute('aria-label', 'Your private test');++ const bar = document.createElement('div');++ bar.className = 'gl-test-bar';++ const say = document.createElement('span');++ say.textContent = 'Your test, only on this device. Check the picture and that you can hear yourself.';++ const done = document.createElement('button');++ done.type = 'button';++ done.className = 'gl-test-done';++ done.textContent = 'Done';++ done.onclick = _glTestClear;++ bar.append(say, done);++ wrap.append(media, bar);++ box.append(wrap);++ media.play().catch(() => { /* the controls are there: a tap plays it */ });++}++++function _glTestClear() {++ const w = document.getElementById('gl-test-play');++ if (w) w.remove();++ if (_glT.url) { URL.revokeObjectURL(_glT.url); _glT.url = ''; }++}++/* ── Tab switching ───────────────────────────────────────────── */function _wsTabSwitch(tabId) {@@@ -2525,6 +2624,7 @@ async function goLiveFromWorkspace() {await _wsFlushSave();// The studio opens the camera itself: the preview lets go of it first (a phone cannot open one camera twice)._glPv.hold = Date.now() + 20000;++ _glTestStop(true);_glPreviewStop();const p = _wsState.profile;nnew file mode 100644@@@ -0,0 +1,33 @@++'use strict';++++/**++ * Test privately (golive-setup redesign, part 3): ten seconds of the preview, recorded in the browser and played back in++ * the card. Nothing leaves the device: the test code calls no API and no fetch, the recording is an object URL revoked on++ * Done, and going live (or the preview letting go of the camera) ends a test that is running.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const ws = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'broadcast-workspace.js'), 'utf8');++const start = ws.indexOf('/* ── Test privately');++const block = ws.slice(start, ws.indexOf('\n/* ── Tab switching', start));++assert.ok(start > 0 && block.length > 500, 'the Test privately block');++++assert.ok(!/\bapi\(|fetch\(|XMLHttpRequest|sendBeacon|WebSocket/.test(block), 'nothing is sent anywhere');++assert.ok(block.includes('new MediaRecorder(stream'), 'it records the preview the card already holds (no second camera)');++assert.ok(/_glT\.left = 10;/.test(block), 'ten seconds');++assert.ok(block.includes('URL.revokeObjectURL(_glT.url)'), 'Done frees the recording');++assert.ok(block.includes("rec.onstop = null"), 'a discarded test is never played back');++++const golive = ws.slice(ws.indexOf('async function goLiveFromWorkspace('), ws.indexOf('function updateWorkspaceLiveStatus('));++assert.ok(golive.indexOf('_glTestStop(true);') > 0 && golive.indexOf('_glTestStop(true);') < golive.indexOf('_glPreviewStop();'), 'going live ends a running test first');++const stop = ws.slice(ws.indexOf('function _glPreviewStop() {'), ws.indexOf('\n}\n', ws.indexOf('function _glPreviewStop() {')));++assert.ok(stop.includes('if (_glT.rec) _glTestStop(true);'), 'the preview letting go ends a running test');++++// The button: under Go live, not for a screen share (the browser picks the screen only when going live).++const panel = ws.slice(ws.indexOf('function _wsRenderPanel()'), ws.indexOf('\n}\n', ws.indexOf('function _wsRenderPanel()')));++assert.ok(/\$\{browserMode === 'screen' \? '' : `<button type="button" class="gl-test-btn" id="gl-test-btn" onclick="_glTestPrivately\(\)">/.test(panel), 'the button, except for a screen share');++assert.ok(panel.indexOf('id="gl-test-btn"') > panel.indexOf('id="bc-ws-golive-btn"'), 'under Go live');++++console.log('golive-test-privately: all passed');
@@ -3304,3 +3304,26 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + var(--ovna
@@ -3304,3 +3304,26 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + var(--ovna
.gl-pv-off[aria-pressed="true"] i::before { content: "\f06e"; }
.gl-pv-off[aria-pressed="true"] i::before { content: "\f06e"; }
@media (pointer: coarse) { .gl-pv-off { width: 44px; height: 44px; } }
@media (pointer: coarse) { .gl-pv-off { width: 44px; height: 44px; } }
@media (prefers-reduced-motion: reduce) { .gl-preview video, .gl-pv-lvl i { transition: none; } }
@media (prefers-reduced-motion: reduce) { .gl-preview video, .gl-pv-lvl i { transition: none; } }
+
+/* Test privately: the quiet button under Go live; while recording, a red dot and the seconds left; then the test plays
+ back over the preview with Done. */
+.gl-test-btn { all: unset; box-sizing: border-box; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 4px 8px; width: 100%; min-height: 44px; margin-top: 8px;
+ padding: 6px 14px; border-radius: 14px; cursor: pointer; font-size: 0.9rem; font-weight: 700; color: var(--text-primary);
+ border: 1px solid var(--border); background: color-mix(in srgb, var(--text-primary) 4%, transparent); transition: background-color var(--ov-dur-2, .15s), border-color var(--ov-dur-2, .15s); }
+.gl-test-btn i { color: var(--text-muted); }
+.gl-test-btn small { font-size: 0.74rem; font-weight: 500; color: var(--text-muted); }
+.gl-test-btn:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
+.gl-test-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
+.gl-test-btn.is-rec { border-color: color-mix(in srgb, var(--live-red, #e11d48) 60%, var(--border)); background: color-mix(in srgb, var(--live-red, #e11d48) 8%, transparent); }
+.gl-test-btn.is-rec i { color: var(--live-red, #e11d48); animation: gl-breathe 1.2s var(--ov-ease-in-out, ease-in-out) infinite; }
+.gl-test-btn.is-rec small { display: none; }
+.gl-test-play { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; background: var(--bg-primary); }
+.gl-preview .gl-test-play video { position: static; inset: auto; opacity: 1; transform: none; flex: 1 1 auto; min-height: 0; width: 100%; height: auto; object-fit: contain; background: #000; }
+.gl-test-play audio { width: calc(100% - 24px); margin: auto 12px 8px; }
+.gl-test-bar { display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px 12px; font-size: 0.78rem; line-height: 1.35; color: var(--text-secondary); }
+.gl-test-bar span { flex: 1 1 auto; min-width: 0; }
+.gl-test-done { all: unset; box-sizing: border-box; flex: none; min-height: 36px; padding: 0 14px; border-radius: 999px; cursor: pointer; font-weight: 700; font-size: 0.84rem;
+ background: var(--accent-strong, var(--accent)); color: var(--on-accent-strong, var(--on-accent, #fff)); }
+.gl-test-done:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
+@media (pointer: coarse) { .gl-test-done { min-height: 44px; } }
+@media (prefers-reduced-motion: reduce) { .gl-test-btn.is-rec i { animation: none; } }
@@ -392,6 +392,9 @@ function _wsRenderPanel() {
@@ -392,6 +392,9 @@ function _wsRenderPanel() {
onclick="goLiveFromWorkspace()" id="bc-ws-golive-btn">
onclick="goLiveFromWorkspace()" id="bc-ws-golive-btn">
<i class="fa-solid fa-tower-broadcast"></i> Go Live
<i class="fa-solid fa-tower-broadcast"></i> Go Live
</button>
</button>
+ ${browserMode === 'screen' ? '' : `<button type="button" class="gl-test-btn" id="gl-test-btn" onclick="_glTestPrivately()">
+ <i class="fa-solid fa-circle-dot" aria-hidden="true"></i><span id="gl-test-label">Test privately</span><small>10 seconds, only on this device</small>
+ </button>`}
<p class="bc-create-reassurance" style="margin-top:6px;font-size:0.82rem">
<p class="bc-create-reassurance" style="margin-top:6px;font-size:0.82rem">
<i class="fa-solid fa-circle-info"></i>
<i class="fa-solid fa-circle-info"></i>
Live at <strong>openvibe.live${channelPath(currentUser?.username || 'your-channel', ms.slug || ms.id)}</strong>
Live at <strong>openvibe.live${channelPath(currentUser?.username || 'your-channel', ms.slug || ms.id)}</strong>
@@ -1053,6 +1056,7 @@ function _glPreviewWant() {
@@ -1053,6 +1056,7 @@ function _glPreviewWant() {
}
}
function _glPreviewStop() {
function _glPreviewStop() {
+ if (_glT.rec) _glTestStop(true);
cancelAnimationFrame(_glPv.raf);
cancelAnimationFrame(_glPv.raf);
_glPv.raf = 0;
_glPv.raf = 0;
if (_glPv.ctx) { try { _glPv.ctx.close(); } catch { /* */ } _glPv.ctx = null; }
if (_glPv.ctx) { try { _glPv.ctx.close(); } catch { /* */ } _glPv.ctx = null; }
@@ -1181,6 +1185,101 @@ function _glPreviewWatch() {
@@ -1181,6 +1185,101 @@ function _glPreviewWatch() {
window.addEventListener('offline', _glPreviewSync);
window.addEventListener('offline', _glPreviewSync);
}
}
+/* ── Test privately (golive-setup redesign, part 3): ten seconds of what the preview holds, recorded in this browser
+ and played back in the card, so the camera and the microphone are heard and seen before anyone else does. Nothing
+ is uploaded and nobody is told: the recording lives in this page's memory until Done. ── */
+const _glT = { rec: null, chunks: [], timer: 0, left: 0, url: '' };
+
+function _glTestType(audioOnly) {
+ const list = audioOnly ? ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4']
+ : ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm', 'video/mp4'];
+ if (typeof MediaRecorder === 'undefined') return null;
+ return list.find((t) => { try { return MediaRecorder.isTypeSupported(t); } catch { return false; } }) || '';
+}
+
+function _glTestLabel(text) { const l = document.getElementById('gl-test-label'); if (l) l.textContent = text; }
+
+function _glTestPrivately() {
+ if (_glT.rec) { _glTestStop(); return; }
+ const stream = _glPv.stream;
+ const note = document.getElementById('gl-pv-note');
+ if (!stream || !stream.getTracks().some((t) => t.readyState === 'live')) {
+ if (note) note.textContent = _glPreviewOff() ? 'Turn the preview on (the eye) to record a test.' : 'Allow the camera and microphone below, then record a test.';
+ return;
+ }
+ const audioOnly = !stream.getVideoTracks().length;
+ const type = _glTestType(audioOnly);
+ if (type === null) { if (note) note.textContent = 'This browser cannot record a test. Going live still works.'; return; }
+ _glTestClear();
+ let rec;
+ try { rec = new MediaRecorder(stream, type ? { mimeType: type } : undefined); } catch { if (note) note.textContent = 'This browser cannot record a test. Going live still works.'; return; }
+ _glT.rec = rec;
+ _glT.chunks = [];
+ rec.ondataavailable = (e) => { if (e.data && e.data.size) _glT.chunks.push(e.data); };
+ rec.onstop = () => _glTestShow(rec.mimeType || type || (audioOnly ? 'audio/webm' : 'video/webm'), audioOnly);
+ rec.start(1000);
+ const btn = document.getElementById('gl-test-btn');
+ if (btn) btn.classList.add('is-rec');
+ _glT.left = 10;
+ _glTestLabel(`Recording · ${_glT.left} s · tap to stop`);
+ _glT.timer = setInterval(() => {
+ _glT.left -= 1;
+ if (_glT.left <= 0) { _glTestStop(); return; }
+ _glTestLabel(`Recording · ${_glT.left} s · tap to stop`);
+ }, 1000);
+}
+
+/** Stop recording. discard: drop it (going live, or the page changing) rather than play it back. */
+function _glTestStop(discard = false) {
+ clearInterval(_glT.timer);
+ _glT.timer = 0;
+ const rec = _glT.rec;
+ _glT.rec = null;
+ const btn = document.getElementById('gl-test-btn');
+ if (btn) btn.classList.remove('is-rec');
+ _glTestLabel('Test privately');
+ if (rec && rec.state !== 'inactive') {
+ if (discard) rec.onstop = null;
+ try { rec.stop(); } catch { /* */ }
+ }
+ if (discard) _glTestClear();
+}
+
+function _glTestShow(type, audioOnly) {
+ const box = document.getElementById('gl-preview');
+ if (!box || !_glT.chunks.length) return;
+ _glTestClear();
+ _glT.url = URL.createObjectURL(new Blob(_glT.chunks, { type }));
+ _glT.chunks = [];
+ const wrap = document.createElement('div');
+ wrap.className = 'gl-test-play';
+ wrap.id = 'gl-test-play';
+ const media = document.createElement(audioOnly ? 'audio' : 'video');
+ media.src = _glT.url;
+ media.controls = true;
+ media.playsInline = true;
+ media.setAttribute('aria-label', 'Your private test');
+ const bar = document.createElement('div');
+ bar.className = 'gl-test-bar';
+ const say = document.createElement('span');
+ say.textContent = 'Your test, only on this device. Check the picture and that you can hear yourself.';
+ const done = document.createElement('button');
+ done.type = 'button';
+ done.className = 'gl-test-done';
+ done.textContent = 'Done';
+ done.onclick = _glTestClear;
+ bar.append(say, done);
+ wrap.append(media, bar);
+ box.append(wrap);
+ media.play().catch(() => { /* the controls are there: a tap plays it */ });
+}
+
+function _glTestClear() {
+ const w = document.getElementById('gl-test-play');
+ if (w) w.remove();
+ if (_glT.url) { URL.revokeObjectURL(_glT.url); _glT.url = ''; }
+}
+
/* ── Tab switching ───────────────────────────────────────────── */
/* ── Tab switching ───────────────────────────────────────────── */
function _wsTabSwitch(tabId) {
function _wsTabSwitch(tabId) {
@@ -2525,6 +2624,7 @@ async function goLiveFromWorkspace() {
@@ -2525,6 +2624,7 @@ async function goLiveFromWorkspace() {
await _wsFlushSave();
await _wsFlushSave();
// The studio opens the camera itself: the preview lets go of it first (a phone cannot open one camera twice).
// The studio opens the camera itself: the preview lets go of it first (a phone cannot open one camera twice).
_glPv.hold = Date.now() + 20000;
_glPv.hold = Date.now() + 20000;
+ _glTestStop(true);
_glPreviewStop();
_glPreviewStop();
const p = _wsState.profile;
const p = _wsState.profile;
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+'use strict';
+
+/**
+ * Test privately (golive-setup redesign, part 3): ten seconds of the preview, recorded in the browser and played back in
+ * the card. Nothing leaves the device: the test code calls no API and no fetch, the recording is an object URL revoked on
+ * Done, and going live (or the preview letting go of the camera) ends a test that is running.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const ws = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'broadcast-workspace.js'), 'utf8');
+const start = ws.indexOf('/* ── Test privately');
+const block = ws.slice(start, ws.indexOf('\n/* ── Tab switching', start));
+assert.ok(start > 0 && block.length > 500, 'the Test privately block');
+
+assert.ok(!/\bapi\(|fetch\(|XMLHttpRequest|sendBeacon|WebSocket/.test(block), 'nothing is sent anywhere');
+assert.ok(block.includes('new MediaRecorder(stream'), 'it records the preview the card already holds (no second camera)');
+assert.ok(/_glT\.left = 10;/.test(block), 'ten seconds');
+assert.ok(block.includes('URL.revokeObjectURL(_glT.url)'), 'Done frees the recording');
+assert.ok(block.includes("rec.onstop = null"), 'a discarded test is never played back');
+
+const golive = ws.slice(ws.indexOf('async function goLiveFromWorkspace('), ws.indexOf('function updateWorkspaceLiveStatus('));
+assert.ok(golive.indexOf('_glTestStop(true);') > 0 && golive.indexOf('_glTestStop(true);') < golive.indexOf('_glPreviewStop();'), 'going live ends a running test first');
+const stop = ws.slice(ws.indexOf('function _glPreviewStop() {'), ws.indexOf('\n}\n', ws.indexOf('function _glPreviewStop() {')));
+assert.ok(stop.includes('if (_glT.rec) _glTestStop(true);'), 'the preview letting go ends a running test');
+
+// The button: under Go live, not for a screen share (the browser picks the screen only when going live).
+const panel = ws.slice(ws.indexOf('function _wsRenderPanel()'), ws.indexOf('\n}\n', ws.indexOf('function _wsRenderPanel()')));
+assert.ok(/\$\{browserMode === 'screen' \? '' : `<button type="button" class="gl-test-btn" id="gl-test-btn" onclick="_glTestPrivately\(\)">/.test(panel), 'the button, except for a screen share');
+assert.ok(panel.indexOf('id="gl-test-btn"') > panel.indexOf('id="bc-ws-golive-btn"'), 'under Go live');
+
+console.log('golive-test-privately: all passed');