openvibers/openvibe.live Commits

Go Live for a new streamer: one card, going live from the browser first (#708)

OP OpenVibers committed 4 days ago · 5d8fe846eb · 4 files, +130 −8

With no streams yet, Go Live said "Check the card, press Go live" with no card on the page, offered two buttons
that did the same thing (New slot and Create stream slot), and explained the word "slot" instead of going live.
Now one card leads with "Go live from this browser" (no OBS, no download, no follower minimum): one press makes a
browser stream and opens its card, where Go live is. "Use OBS or another app" opens the full form with RTMP
picked. The slot row stays empty until a stream exists, and the line under Go Live says what to do first.

Changes

ddiff --git a/public/css/features/broadcast.css b/public/css/features/broadcast.css
iindex f4785504..b37759f7 100644
---- a/public/css/features/broadcast.css
++++ b/public/css/features/broadcast.css
@@@ -216,6 +216,31 @@
padding: 48px 32px;
text-align: center;
}
++/* First run: one card, the browser first (the headline: no OBS, no download, no follower minimum), apps second. */
++.bc-first {
++ max-width: 520px; width: 100%;
++ padding: 28px 20px;
++ border: 1px solid var(--border);
++ border-radius: var(--radius-lg, 12px);
++ background: var(--bg-secondary);
++}
++.bc-first-mark {
++ display: inline-flex; align-items: center; justify-content: center;
++ width: 52px; height: 52px; margin-bottom: 12px; border-radius: 50%;
++ font-size: 1.35rem; color: var(--live-red, var(--accent));
++ background: color-mix(in srgb, var(--live-red, var(--accent)) 14%, transparent);
++}
++.bc-first-title { margin: 0 0 8px; font-size: 1.25rem; }
++.bc-first-lede { margin: 0 0 18px; color: var(--text-secondary); line-height: 1.5; }
++.bc-first-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
++.bc-first-actions .btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
++.bc-first-go { flex: 1 1 220px; }
++.bc-first-app { flex: 0 1 auto; }
++.bc-first-fine { margin: 14px 0 0; font-size: 0.8rem; color: var(--text-muted); }
++@media (max-width: 480px) {
++ .bc-ws-empty { padding: 20px 0; }
++ .bc-first-actions .btn { flex: 1 1 100%; }
++}
.bc-ws-panel-hd {
display: flex;
align-items: flex-start;
ddiff --git a/public/fragments/broadcast.html b/public/fragments/broadcast.html
iindex df3b0c2d..08f3d4ed 100644
---- a/public/fragments/broadcast.html
++++ b/public/fragments/broadcast.html
@@@ -18,7 +18,7 @@
<!-- ═══ STEP 0: Stream Manager (existing streams + create new) ═══ -->
<div id="bc-stream-manager" class="broadcast-tab-content active" data-tab="go-live">
<h2><i class="fa-solid fa-tower-broadcast"></i> Go Live <button type="button" class="btn btn-outline btn-sm glw-open-btn" onclick="openSetupHub()" title="Everything you can set up, step by step"><i class="fa-solid fa-list-check"></i> Streamer setup</button></h2>
-- <p class="bc-page-subtitle">Check the card, press Go live. Everything else is in the rows under it.</p>
++ <p class="bc-page-subtitle" id="bc-page-subtitle">Check the card, press Go live. Everything else is in the rows under it.</p>
<!-- Guests: going live needs an account. broadcast.js shows this in place of the slot
workspace and loads nothing else (_broadcastSignInGate). -->
@@@ -59,13 +59,19 @@
<!-- Right main panel -->
<div class="bc-ws-main">
-- <!-- Empty state: no stream selected -->
++ <!-- First run (no streams yet): one press makes a browser stream and opens its card, where
++ Go live is; OBS and other apps get the full form. broadcast-workspace.js _wsShowEmpty. -->
<div class="bc-ws-empty" id="bc-ws-empty">
-- <i class="fa-solid fa-tower-broadcast" style="font-size:2.5rem;opacity:0.2;margin-bottom:16px"></i>
-- <p class="muted" style="margin:0 0 16px">A slot is one stream of yours: its title, its link, how it goes out and where else it is sent. Make one to go live.</p>
-- <button class="btn btn-outline" onclick="showCreateManagedStreamModal()">
-- <i class="fa-solid fa-plus"></i> Create stream slot
-- </button>
++ <div class="bc-first" id="bc-first">
++ <span class="bc-first-mark" aria-hidden="true"><i class="fa-solid fa-tower-broadcast"></i></span>
++ <h3 class="bc-first-title">Your first stream</h3>
++ <p class="bc-first-lede">Go live from this browser with your camera, your screen or both: no OBS, no download, no follower minimum.</p>
++ <div class="bc-first-actions">
++ <button type="button" class="btn btn-primary bc-first-go" id="bc-first-go" onclick="wsFirstBrowserStream(this)"><i class="fa-solid fa-video" aria-hidden="true"></i> Go live from this browser</button>
++ <button type="button" class="btn btn-outline bc-first-app" onclick="wsFirstAppStream()"><i class="fa-solid fa-server" aria-hidden="true"></i> Use OBS or another app</button>
++ </div>
++ <p class="bc-first-fine">Each stream you make keeps its own title, link and key. Make more any time.</p>
++ </div>
</div>
<!-- Profile editor + Go Live + History (populated by broadcast-workspace.js) -->
<div class="bc-ws-panel" id="bc-ws-panel" style="display:none"></div>
ddiff --git a/public/js/broadcast-workspace.js b/public/js/broadcast-workspace.js
iindex 70310720..c6a41478 100644
---- a/public/js/broadcast-workspace.js
++++ b/public/js/broadcast-workspace.js
@@@ -80,8 +80,10 @@ function _wsRenderSidebar() {
const list = document.getElementById('bc-ws-list');
if (!list) return;
const add = '<button type="button" class="gl-slot gl-slot--add" onclick="showCreateManagedStreamModal()"><i class="fa-solid fa-plus" aria-hidden="true"></i><span>New slot</span></button>';
++ // No streams yet: the first-run card below has the ways to start, so the row stays empty (two "make one" buttons
++ // that did the same thing confused new streamers).
if (_wsState.managedStreams.length === 0) {
-- list.innerHTML = add;
++ list.innerHTML = '';
return;
}
list.innerHTML = _wsState.managedStreams.map(ms => {
@@@ -182,6 +184,40 @@ function _wsShowEmpty() {
const panel = document.getElementById('bc-ws-panel');
if (empty) empty.style.display = '';
if (panel) panel.style.display = 'none';
++ // "Check the card, press Go live" points at a card that does not exist yet.
++ _wsSubtitle(_wsState.managedStreams.length ? null : 'Make your first stream, then press Go live on its card.');
++}
++
++/** The page's line under Go Live: `text`, or (null) the line for when a stream's card is shown. */
++function _wsSubtitle(text) {
++ const sub = document.getElementById('bc-page-subtitle');
++ if (!sub) return;
++ if (sub.dataset.withCard === undefined) sub.dataset.withCard = sub.textContent;
++ sub.textContent = text || sub.dataset.withCard;
++}
++
++/** First run: make a browser stream in one press and open its card, where Go live is. */
++async function wsFirstBrowserStream(btn) {
++ if (!currentUser) { if (typeof _broadcastSignInGate === 'function') _broadcastSignInGate(false); return; }
++ if (btn) btn.disabled = true;
++ try {
++ const name = String(currentUser.display_name || currentUser.username || '').trim();
++ const data = await api('/streams/managed', {
++ method: 'POST',
++ body: { title: name ? `${name}'s stream`.slice(0, 140) : 'My stream', category: '', protocol: 'webrtc', streaming_method: 'browser' },
++ });
++ if (data && data.managed_stream) await onManagedStreamCreated(data.managed_stream.id);
++ } catch (err) {
++ toast('Your stream could not be made: ' + (err?.message || 'the server did not answer'), 'error');
++ } finally {
++ if (btn) btn.disabled = false;
++ }
++}
++
++/** First run with OBS, an encoder or FFmpeg: the full form, with RTMP picked. */
++function wsFirstAppStream() {
++ showCreateManagedStreamModal();
++ if (typeof _cmsSelectMethod === 'function') _cmsSelectMethod('rtmp');
}
/** A Tours link may fill the studio. The streamer still presses Go live and can edit the title. */
@@@ -204,6 +240,7 @@ function _wsRenderPanel() {
if (!ms) { _wsShowEmpty(); return; }
if (empty) empty.style.display = 'none';
++ _wsSubtitle(null);
panel.style.display = '';
const p = _wsState.profile;
ddiff --git a/test/golive-first-run.test.js b/test/golive-first-run.test.js
nnew file mode 100644
iindex 00000000..327ac03e
---- /dev/null
++++ b/test/golive-first-run.test.js
@@@ -0,0 +1,54 @@
++'use strict';
++/**
++ * Go Live with no streams yet (a new streamer's first visit). It used to say "Check the card, press Go live" with no
++ * card on the page, offered two buttons that did the same thing ("New slot" and "Create stream slot") and explained the
++ * word "slot" instead of going live (signed-in sweep, 2026-11-09). Now one card leads with going live from the browser
++ * (one press makes a browser stream and opens its card), OBS and other apps get the full form with RTMP picked, the
++ * slot row stays empty, and the line under Go Live says what to do first.
++ *
++ * node test/golive-first-run.test.js
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const root = path.join(__dirname, '..');
++const html = fs.readFileSync(path.join(root, 'public/fragments/broadcast.html'), 'utf8');
++const ws = fs.readFileSync(path.join(root, 'public/js/broadcast-workspace.js'), 'utf8');
++const css = fs.readFileSync(path.join(root, 'public/css/features/broadcast.css'), 'utf8');
++
++// The card: going live from the browser first, apps second; no "slot" lecture, no second create button.
++const empty = html.slice(html.indexOf('id="bc-ws-empty"'), html.indexOf('id="bc-ws-panel"'));
++assert.ok(empty.includes('Your first stream'), 'the first-run card names what it is');
++assert.ok(/onclick="wsFirstBrowserStream\(this\)"[^>]*>[\s\S]*?Go live from this browser/.test(empty), 'the browser is the main action');
++assert.ok(/onclick="wsFirstAppStream\(\)"[^>]*>[\s\S]*?Use OBS or another app/.test(empty), 'apps are the second action');
++assert.ok(empty.indexOf('wsFirstBrowserStream') < empty.indexOf('wsFirstAppStream'), 'the browser comes first');
++assert.ok(!/Create stream slot|A slot is one stream/.test(empty), 'no second create button and no slot lecture');
++assert.ok(/no OBS, no download, no follower minimum/.test(empty), 'the headline promise is on the card');
++assert.ok(html.includes('id="bc-page-subtitle"'), 'the line under Go Live can change with the state');
++
++// One press: a browser stream (WebRTC), then its card opens (onManagedStreamCreated selects it).
++const first = ws.slice(ws.indexOf('async function wsFirstBrowserStream'), ws.indexOf('function wsFirstAppStream'));
++assert.ok(/streaming_method: 'browser'/.test(first) && /protocol: 'webrtc'/.test(first), 'a browser stream is made');
++assert.ok(/onManagedStreamCreated\(data\.managed_stream\.id\)/.test(first), 'and its card opens');
++assert.ok(/if \(!currentUser\)/.test(first), 'a guest gets the sign-in gate, not a 401');
++assert.ok(/btn\.disabled = true/.test(first) && /btn\.disabled = false/.test(first), 'no double press makes two streams');
++const app = ws.slice(ws.indexOf('function wsFirstAppStream'), ws.indexOf('function wsFirstAppStream') + 300);
++assert.ok(/showCreateManagedStreamModal\(\)/.test(app) && /_cmsSelectMethod\('rtmp'\)/.test(app), 'apps open the form with RTMP picked');
++
++// No streams: the slot row is empty (the card has the ways to start); with streams, New slot is at the end of the row.
++const side = ws.slice(ws.indexOf('function _wsRenderSidebar'), ws.indexOf('function _wsRenderSidebar') + 1800);
++assert.ok(/managedStreams\.length === 0\) \{\s*list\.innerHTML = '';/.test(side), 'no New slot pill beside the first-run card');
++assert.ok(/\}\)\.join\(''\) \+ add;/.test(side), 'New slot still ends the row once streams exist');
++
++// The subtitle: what to do first while there is no card, the card line back once a card shows.
++const showEmpty = ws.slice(ws.indexOf('function _wsShowEmpty'), ws.indexOf('function _wsSubtitle'));
++assert.ok(/Make your first stream, then press Go live on its card\./.test(showEmpty), 'the first-run line');
++const renderPanel = ws.slice(ws.indexOf('function _wsRenderPanel'), ws.indexOf('function _wsRenderPanel') + 600);
++assert.ok(/_wsSubtitle\(null\)/.test(renderPanel), 'a shown card restores the card line');
++
++// Phone: both buttons full width and 44 px tall.
++assert.ok(/\.bc-first-actions \.btn \{ min-height: 44px;/.test(css), '44 px buttons');
++assert.ok(/@media \(max-width: 480px\) \{[^}]*\.bc-ws-empty[^}]*\}[^}]*\.bc-first-actions \.btn \{ flex: 1 1 100%; \}/.test(css), 'full width on a phone');
++
++console.log('go live first run: one card, the browser first, apps second, no duplicate create button');