openvibers/openvibe.live Commits
While live, every other page shows "You're live · Back to the studio" (#474)
Changes
@@@ -2788,6 +2788,19 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importanbackground: #e11d48; color: #fff; font-weight: 700; box-shadow: 0 8px 24px rgba(0,0,0,0.35); cursor: pointer;}.bcs-return[hidden] { display: none; }++/* "You're live · Back to the studio" on every other page while the broadcast runs (broadcast-studio.js syncLivePill). */++.bc-live-pill {++ position: fixed; top: calc(var(--navbar-h, 56px) + 8px + env(safe-area-inset-top, 0px)); left: 50%; transform: translateX(-50%); z-index: 1001;++ display: inline-flex; align-items: center; gap: 8px; min-height: 40px; max-width: calc(100vw - 32px); padding: 0 16px; border: 0; border-radius: 999px;++ background: #e11d48; color: #fff; font: inherit; font-size: 0.86rem; white-space: nowrap; box-shadow: 0 8px 24px rgba(0,0,0,0.3); cursor: pointer;++}++.bc-live-pill[hidden] { display: none; }++.bc-live-pill b { font-weight: 800; }++.bc-live-pill span:last-child { opacity: 0.9; }++.bc-live-pill-dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; animation: bcLivePillPulse 1.6s ease-in-out infinite; }++@keyframes bcLivePillPulse { 50% { opacity: 0.35; } }++.bc-live-pill:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }++@media (prefers-reduced-motion: reduce) { .bc-live-pill-dot { animation: none; } }@media (prefers-reduced-motion: reduce) {.bcs-live-dot, .bcs-sheet, .bcs-sheet-wrap { animation: none; }}@@@ -684,6 +684,22 @@syncDesktopButtons();if (target) { const n = document.querySelector(target); if (n) n.scrollIntoView({ behavior: 'smooth', block: 'start' }); }}++ /* "● You're live · Back to the studio" (plan L23 R5, owner 2026-10-31): moving around the site never stops the++ broadcast, so on every other page a pill under the navbar says the stream is still on and takes the streamer++ back in one tap. */++ function syncLivePill() {++ const show = !!localStream() && !onBroadcastPage();++ let pill = $('bc-live-pill');++ if (!show) { if (pill) pill.hidden = true; return; }++ if (!pill) {++ pill = el('button', { type: 'button', class: 'bc-live-pill', id: 'bc-live-pill',++ onclick: () => { if (typeof navigate === 'function') navigate('/broadcast'); else location.href = '/broadcast'; } },++ el('span', { class: 'bc-live-pill-dot', 'aria-hidden': 'true' }), el('b', { text: "You're live" }), el('span', { text: 'Back to the studio' }));++ document.body.append(pill);++ }++ pill.hidden = false;++ }++function ensureReturn() {if ($('bcs-return')) return;document.body.append(el('button', { type: 'button', class: 'bcs-return', id: 'bcs-return', hidden: true,@@@ -861,6 +877,7 @@if (wanted()) { if (!st.root) mount(); refresh(); }else { if (st.root) unmount(); applyTracks(); syncDesktopButtons(); }syncGoLiveDock();++ syncLivePill();}setInterval(tick, 1000);document.addEventListener('visibilitychange', tick);@@@ -32,4 +32,11 @@ const app = read('public/js/app.js');const teardown = app.slice(app.indexOf('function teardownRoute('), app.indexOf('\n}\n', app.indexOf('function teardownRoute(')));assert.ok(!/stopBroadcast|cleanupStream|endStream|stopAllStreams/.test(teardown), 'a page change leaves the broadcast running');--console.log('always live: 5 min grace, viewers wait and say so, the studio reconnects at once, leave asks first');++// On every other page while live: "You're live · Back to the studio", one tap back.++const studio = read('public/js/broadcast-studio.js');++assert.match(studio, /function syncLivePill\(\) \{\s*const show = !!localStream\(\) && !onBroadcastPage\(\);/, 'the pill shows off the studio page while live');++assert.match(studio, /navigate\('\/broadcast'\)/, 'and takes the streamer back');++assert.match(studio, /syncGoLiveDock\(\);\s*syncLivePill\(\);/, 'checked every second with the rest of the studio');++assert.match(read('public/css/features/broadcast.css'), /\.bc-live-pill \{[^}]*position: fixed;[^}]*min-height: 40px;/);++++console.log('always live: 5 min grace, viewers wait and say so, the studio reconnects at once, leave asks first, a pill back to the studio');
@@ -2788,6 +2788,19 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
@@ -2788,6 +2788,19 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
background: #e11d48; color: #fff; font-weight: 700; box-shadow: 0 8px 24px rgba(0,0,0,0.35); cursor: pointer;
background: #e11d48; color: #fff; font-weight: 700; box-shadow: 0 8px 24px rgba(0,0,0,0.35); cursor: pointer;
}
}
.bcs-return[hidden] { display: none; }
.bcs-return[hidden] { display: none; }
+/* "You're live · Back to the studio" on every other page while the broadcast runs (broadcast-studio.js syncLivePill). */
+.bc-live-pill {
+ position: fixed; top: calc(var(--navbar-h, 56px) + 8px + env(safe-area-inset-top, 0px)); left: 50%; transform: translateX(-50%); z-index: 1001;
+ display: inline-flex; align-items: center; gap: 8px; min-height: 40px; max-width: calc(100vw - 32px); padding: 0 16px; border: 0; border-radius: 999px;
+ background: #e11d48; color: #fff; font: inherit; font-size: 0.86rem; white-space: nowrap; box-shadow: 0 8px 24px rgba(0,0,0,0.3); cursor: pointer;
+}
+.bc-live-pill[hidden] { display: none; }
+.bc-live-pill b { font-weight: 800; }
+.bc-live-pill span:last-child { opacity: 0.9; }
+.bc-live-pill-dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; animation: bcLivePillPulse 1.6s ease-in-out infinite; }
+@keyframes bcLivePillPulse { 50% { opacity: 0.35; } }
+.bc-live-pill:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }
+@media (prefers-reduced-motion: reduce) { .bc-live-pill-dot { animation: none; } }
@media (prefers-reduced-motion: reduce) {
@media (prefers-reduced-motion: reduce) {
.bcs-live-dot, .bcs-sheet, .bcs-sheet-wrap { animation: none; }
.bcs-live-dot, .bcs-sheet, .bcs-sheet-wrap { animation: none; }
}
}
@@ -684,6 +684,22 @@
@@ -684,6 +684,22 @@
syncDesktopButtons();
syncDesktopButtons();
if (target) { const n = document.querySelector(target); if (n) n.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
if (target) { const n = document.querySelector(target); if (n) n.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
}
}
+ /* "● You're live · Back to the studio" (plan L23 R5, owner 2026-10-31): moving around the site never stops the
+ broadcast, so on every other page a pill under the navbar says the stream is still on and takes the streamer
+ back in one tap. */
+ function syncLivePill() {
+ const show = !!localStream() && !onBroadcastPage();
+ let pill = $('bc-live-pill');
+ if (!show) { if (pill) pill.hidden = true; return; }
+ if (!pill) {
+ pill = el('button', { type: 'button', class: 'bc-live-pill', id: 'bc-live-pill',
+ onclick: () => { if (typeof navigate === 'function') navigate('/broadcast'); else location.href = '/broadcast'; } },
+ el('span', { class: 'bc-live-pill-dot', 'aria-hidden': 'true' }), el('b', { text: "You're live" }), el('span', { text: 'Back to the studio' }));
+ document.body.append(pill);
+ }
+ pill.hidden = false;
+ }
+
function ensureReturn() {
function ensureReturn() {
if ($('bcs-return')) return;
if ($('bcs-return')) return;
document.body.append(el('button', { type: 'button', class: 'bcs-return', id: 'bcs-return', hidden: true,
document.body.append(el('button', { type: 'button', class: 'bcs-return', id: 'bcs-return', hidden: true,
@@ -861,6 +877,7 @@
@@ -861,6 +877,7 @@
if (wanted()) { if (!st.root) mount(); refresh(); }
if (wanted()) { if (!st.root) mount(); refresh(); }
else { if (st.root) unmount(); applyTracks(); syncDesktopButtons(); }
else { if (st.root) unmount(); applyTracks(); syncDesktopButtons(); }
syncGoLiveDock();
syncGoLiveDock();
+ syncLivePill();
}
}
setInterval(tick, 1000);
setInterval(tick, 1000);
document.addEventListener('visibilitychange', tick);
document.addEventListener('visibilitychange', tick);
@@ -32,4 +32,11 @@ const app = read('public/js/app.js');
@@ -32,4 +32,11 @@ const app = read('public/js/app.js');
const teardown = app.slice(app.indexOf('function teardownRoute('), app.indexOf('\n}\n', app.indexOf('function teardownRoute(')));
const teardown = app.slice(app.indexOf('function teardownRoute('), app.indexOf('\n}\n', app.indexOf('function teardownRoute(')));
assert.ok(!/stopBroadcast|cleanupStream|endStream|stopAllStreams/.test(teardown), 'a page change leaves the broadcast running');
assert.ok(!/stopBroadcast|cleanupStream|endStream|stopAllStreams/.test(teardown), 'a page change leaves the broadcast running');
-console.log('always live: 5 min grace, viewers wait and say so, the studio reconnects at once, leave asks first');
+// On every other page while live: "You're live · Back to the studio", one tap back.
+const studio = read('public/js/broadcast-studio.js');
+assert.match(studio, /function syncLivePill\(\) \{\s*const show = !!localStream\(\) && !onBroadcastPage\(\);/, 'the pill shows off the studio page while live');
+assert.match(studio, /navigate\('\/broadcast'\)/, 'and takes the streamer back');
+assert.match(studio, /syncGoLiveDock\(\);\s*syncLivePill\(\);/, 'checked every second with the rest of the studio');
+assert.match(read('public/css/features/broadcast.css'), /\.bc-live-pill \{[^}]*position: fixed;[^}]*min-height: 40px;/);
+
+console.log('always live: 5 min grace, viewers wait and say so, the studio reconnects at once, leave asks first, a pill back to the studio');