openvibers/openvibe.live Commits

Broadcast: before going live on a phone, a Go live bar stays in reach while the slot's own button is two screens down (#159)

OP OpenVibers committed 28 days ago · 50dc892fdf · 3 files, +60 −1

Changes

ddiff --git a/public/css/features/broadcast.css b/public/css/features/broadcast.css
iindex a55111e9..ee47921f 100644
---- a/public/css/features/broadcast.css
++++ b/public/css/features/broadcast.css
@@@ -2646,3 +2646,21 @@ body.bc-studio-on .toast-container { z-index: 1500; }
@media (prefers-reduced-motion: reduce) {
.bcs-live-dot, .bcs-sheet, .bcs-sheet-wrap { animation: none; }
}
++
++/* "Go live" in reach on a phone before going live (public/js/broadcast-studio.js): pinned while the slot's own Go Live
++ button is below the fold. */
++.bcs-golive {
++ position: fixed; left: 0; right: 0; bottom: 0; z-index: 1050; display: grid; gap: 4px; justify-items: center;
++ padding: 10px max(14px, env(safe-area-inset-right, 0px)) calc(10px + env(safe-area-inset-bottom, 0px)) max(14px, env(safe-area-inset-left, 0px));
++ background: linear-gradient(to bottom, transparent, var(--bg-primary, #0b0b10) 34%);
++}
++.bcs-golive[hidden] { display: none; }
++.bcs-golive-btn {
++ width: 100%; max-width: 520px; min-height: 52px; border: 0; border-radius: 14px; cursor: pointer;
++ background: var(--accent-strong, #2563eb); color: var(--on-accent-strong, #fff); font: inherit; font-size: 1.05rem; font-weight: 800;
++ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
++}
++.bcs-golive-btn:disabled { opacity: 0.6; cursor: default; }
++.bcs-golive-btn:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }
++.bcs-golive-where { color: var(--text-secondary, #a1a1aa); font-size: 0.76rem; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++body.bc-golive-dock-on .messenger-toggle { display: none !important; }
ddiff --git a/public/js/broadcast-studio.js b/public/js/broadcast-studio.js
iindex df44152e..157b74fb 100644
---- a/public/js/broadcast-studio.js
++++ b/public/js/broadcast-studio.js
@@@ -13,7 +13,8 @@
MediaStreamTrack.enabled = false: viewers get silence or black while the stream and its recording go on. The
choice is applied again every second, so a replaced track (a camera switch, a recovery) keeps it.
-- window.bcStudio (toggleMic, toggleCamera, flip, open) also serves the desktop control row. */
++ Before going live, a phone also keeps "Go live" in reach (a bar at the bottom while the slot's own button is off
++ screen). window.bcStudio (toggleMic, toggleCamera, flip, open) also serves the desktop control row. */
(function () {
'use strict';
// A phone held upright, or turned sideways (wide but short, with a touch screen).
@@@ -381,6 +382,35 @@
else if (st.sheet) closeSheet();
});
++ // ── Before going live on a phone: "Go live" stays in reach ──
++ // The slot workspace puts the real Go Live button below the title, method, capture mode, quality and devices,
++ // about two screens down on a phone. While it is out of view, a bar at the bottom presses it.
++ function goLiveButton() { const b = $('bc-ws-golive-btn'); return b && b.offsetParent ? b : null; }
++ function syncGoLiveDock() {
++ const real = NARROW.matches && onBroadcastPage() && !localStream() ? goLiveButton() : null;
++ let inView = false;
++ if (real) { const r = real.getBoundingClientRect(); inView = r.top < window.innerHeight - 8 && r.bottom > 0; }
++ const show = !!real && !inView;
++ let dock = $('bcs-golive');
++ if (show && !dock) {
++ dock = el('div', { class: 'bcs-golive', id: 'bcs-golive' },
++ el('button', { type: 'button', class: 'bcs-golive-btn', onclick: () => { const b = goLiveButton(); if (b && !b.disabled) b.click(); } },
++ icon('fa-tower-broadcast'), ' Go live'),
++ el('small', { class: 'bcs-golive-where' }));
++ document.body.append(dock);
++ }
++ if (dock) {
++ dock.hidden = !show;
++ if (show) {
++ const where = real.parentElement && real.parentElement.querySelector('.bc-create-reassurance');
++ dock.querySelector('.bcs-golive-where').textContent = where ? where.textContent.trim() : '';
++ dock.querySelector('.bcs-golive-btn').disabled = !!real.disabled;
++ }
++ }
++ document.body.classList.toggle('bc-golive-dock-on', show);
++ }
++ document.addEventListener('scroll', () => { if ($('bcs-golive') || goLiveButton()) syncGoLiveDock(); }, { capture: true, passive: true });
++
// ── Once a second: mount, update or leave ──
function tick() {
const has = !!localStream();
@@@ -393,6 +423,7 @@
}
if (wanted()) { if (!st.root) mount(); refresh(); }
else { if (st.root) unmount(); applyTracks(); syncDesktopButtons(); }
++ syncGoLiveDock();
}
setInterval(tick, 1000);
if (NARROW.addEventListener) NARROW.addEventListener('change', tick);
ddiff --git a/test/broadcast-studio.test.js b/test/broadcast-studio.test.js
iindex dac0aa13..360fec97 100644
---- a/test/broadcast-studio.test.js
++++ b/test/broadcast-studio.test.js
@@@ -6,6 +6,8 @@
* first, then ultra-wide and others), else to the next camera; one camera says so; screen sharing says to stop it.
* - Mute and camera off are track.enabled = false, and they hold when the track is replaced (a camera switch, a
* recovery) because the studio applies them again every second.
++ * - Before going live on a phone, a "Go live" bar presses the slot's own Go Live button while that one is two screens
++ * down, under the title, method, capture mode, quality and devices.
* - The camera buttons are visible once the camera runs: the live view used to show before capture started, which
* hid "Switch Cam" for the whole stream.
* - The live controls say what they do: "watching" (not "total viewers"), no "WEBRTC" badge, RobotStreamer's line
@@@ -164,6 +166,14 @@ function loadStudio({ stream, cameras = [], screenShare = false }) {
assert.ok(!/passthrough`|to RS`/.test(rs) && /ms delay/.test(rs) && /reconnected once/.test(rs), 'RobotStreamer in words');
});
++ await check('before going live on a phone, a "Go live" bar presses the slot\'s own button while it is off screen', async () => {
++ const dock = extract(studioSrc, 'syncGoLiveDock');
++ assert.ok(/NARROW\.matches && onBroadcastPage\(\) && !localStream\(\)/.test(dock), 'phones, on /broadcast, not live yet');
++ assert.ok(/getBoundingClientRect\(\)/.test(dock) && /const show = !!real && !inView/.test(dock), 'only while the real button is out of view');
++ assert.ok(/b && !b\.disabled\) b\.click\(\)/.test(dock), 'it clicks the real Go Live button (same checks, same settings)');
++ assert.ok(/id="bc-ws-golive-btn"/.test(read('public/js/broadcast-workspace.js')), 'the button it presses exists');
++ });
++
await check('the studio loads with the broadcast feature, after broadcast.js', async () => {
const js = features.broadcast.js;
assert.ok(js.indexOf('/js/broadcast-studio.js') > js.indexOf('/js/broadcast.js'));