openvibers/openvibe.live Commits
Go Live page reorganized, and the popout chat fixed.
Changes
@@@ -1067,8 +1067,9 @@.bc-method-card.selected,.bc-method-card-sm.selected {border-color: var(--accent);-- background: rgba(241, 196, 15, 0.08);-- box-shadow: 0 0 0 1px var(--accent);++ /* The theme's accent (a fixed yellow tint read brown on dark themes). */++ background: color-mix(in srgb, var(--accent) 12%, var(--bg-tertiary));++ box-shadow: 0 0 0 1px var(--accent), 0 6px 18px color-mix(in srgb, var(--accent) 18%, transparent);}.bc-method-card i {@@@ -2373,3 +2373,82 @@font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;color: var(--text-secondary); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;}++++/* ═══ Go Live workspace layout (2026-09-28) ═══════════════════════════════════++ No box around the page: while the slot manager shows, the main column is transparent and is the one scroll++ (a thin themed scrollbar at the window edge, not one in the middle of the page). The slot list and the panel++ are two cards side by side: the list is as tall as its slots and stays in view while the panel scrolls. */++.broadcast-main:has(> #bc-stream-manager:not([style*="display: none"])) {++ background: transparent; border: 0; border-radius: 0; box-shadow: none;++}++.broadcast-main { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--accent) 38%, transparent) transparent; }++.broadcast-main::-webkit-scrollbar { width: 8px; }++.broadcast-main::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--accent) 38%, transparent); border-radius: 8px; }++.broadcast-main::-webkit-scrollbar-track { background: transparent; }++#bc-stream-manager { max-width: 1480px; padding: 4px clamp(0px, 1vw, 12px) 28px; flex: none; min-height: auto; overflow: visible; }++#bc-stream-manager > h2 { margin: 0 0 2px; font-size: 1.22rem; flex-wrap: wrap; row-gap: 6px; }++#bc-stream-manager > h2 .glw-open-btn { margin-left: 6px; }++#bc-stream-manager > .bc-page-subtitle { margin: 0 0 10px; font-size: 0.86rem; }++.bc-workspace { flex: none; border: 0; border-radius: 0; background: transparent; overflow: visible; gap: 14px; align-items: flex-start; margin-top: 6px; }++.bc-ws-sidebar, .bc-ws-main {++ border: 1px solid var(--border); border-radius: 14px;++ background: color-mix(in srgb, var(--bg-card, var(--surface)) 92%, transparent);++ box-shadow: 0 1px 0 color-mix(in srgb, var(--text-primary) 4%, transparent) inset;++}++.bc-ws-sidebar { width: clamp(200px, 17vw, 264px); border-right: 1px solid var(--border); overflow: hidden; }++.bc-ws-sidebar-hd { padding: 10px 10px 10px 14px; }++.bc-ws-list { padding: 4px; flex: none; }++.bc-ws-item { border-radius: 10px; padding: 9px 10px; }++.bc-ws-item.selected { box-shadow: inset 3px 0 0 var(--accent); }++.bc-ws-main { min-height: 0; }++.bc-ws-panel { overflow: visible; }++@media (min-width: 681px) {++ .bc-ws-sidebar { position: sticky; top: 0; align-self: flex-start; max-height: calc(100dvh - var(--navbar-h, 56px) - 40px); }++ .bc-ws-list { overflow-y: auto; flex: 1 1 auto; }++}++@media (max-width: 680px) {++ .bc-workspace { gap: 10px; }++ .bc-ws-sidebar { width: 100%; border-bottom: 1px solid var(--border); }++ .bc-ws-main { width: 100%; }++}++++/* NSFW: a themed switch with what it does, instead of a bare checkbox floating beside a select. */++.bc-switch { display: flex; align-items: flex-start; gap: 12px; margin: 2px 0 16px; padding: 10px 12px; cursor: pointer;++ border: 1px solid var(--border); border-radius: 12px; background: color-mix(in srgb, var(--bg-input, var(--bg)) 70%, transparent);++ transition: border-color .18s, background-color .18s; }++.bc-switch:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }++.bc-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }++.bc-switch-track { flex: none; position: relative; width: 38px; height: 22px; margin-top: 1px; border-radius: 999px;++ background: color-mix(in srgb, var(--text-muted) 35%, transparent); border: 1px solid var(--border); transition: background-color .2s, border-color .2s; }++.bc-switch-thumb { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-primary);++ box-shadow: 0 1px 3px rgba(0,0,0,.35); transition: transform .22s cubic-bezier(.22,1,.36,1), background-color .2s; }++.bc-switch input:checked + .bc-switch-track { background: var(--danger, #ef4444); border-color: var(--danger, #ef4444); }++.bc-switch input:checked + .bc-switch-track .bc-switch-thumb { transform: translateX(16px); background: #fff; }++.bc-switch input:focus-visible + .bc-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }++.bc-switch:has(input:checked) { border-color: color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border)); background: color-mix(in srgb, var(--danger, #ef4444) 7%, transparent); }++.bc-switch-text { display: grid; gap: 2px; min-width: 0; }++.bc-switch-text b { font-size: 0.88rem; font-weight: 700; color: var(--text-primary); }++.bc-switch-text b i { color: var(--danger, #ef4444); margin-right: 3px; }++.bc-switch-text small { font-size: 0.76rem; color: var(--text-muted); line-height: 1.4; }++++/* Video quality: reads as a row that opens (chevron, current values), not a static label. */++.bc-ws-quality { padding: 0; border-radius: 12px; overflow: hidden; }++.bc-ws-quality > summary { display: flex; align-items: center; gap: 8px; padding: 11px 12px; cursor: pointer; list-style: none; user-select: none;++ font-size: 0.86rem; font-weight: 650; color: var(--text-primary); transition: background-color .15s; }++.bc-ws-quality > summary::-webkit-details-marker { display: none; }++.bc-ws-quality > summary:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }++.bc-ws-quality > summary > i:first-child { color: var(--accent); margin: 0; }++.bc-sum-val { margin-left: auto; font-size: 0.76rem; font-weight: 500; color: var(--text-muted); white-space: nowrap; }++.bc-sum-chev { color: var(--text-muted); font-size: 0.75rem; transition: transform .22s ease; }++.bc-ws-quality[open] .bc-sum-chev { transform: rotate(180deg); color: var(--accent); }++.bc-ws-quality[open] > summary { border-bottom: 1px solid var(--border); }++.bc-ws-quality .bc-ws-quality-inner { margin: 0; padding: 12px; }++@media (prefers-reduced-motion: reduce) { .bc-switch-thumb, .bc-sum-chev { transition: none; } }++++/* Wide panels: details on the left, how you stream on the right, so fields are not a thousand pixels wide. */++.bc-ws-main { container-type: inline-size; container-name: bcws; }++@container bcws (min-width: 960px) {++ .bc-ws-profile-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 8px 28px; align-items: start; }++ .bc-ws-profile-split > .bc-ws-col--method .bc-method-picker-sm { grid-template-columns: repeat(2, minmax(0, 1fr)); }++ .bc-ws-profile-split > .bc-ws-col--details .bc-switch { margin-bottom: 14px; }++}@@@ -403,7 +403,8 @@ function _wsRenderPanel() {<!-- ═══ TAB: Stream Profile ═══ --><div class="bc-ws-tab-panel active" data-wstabpanel="profile">-- <div class="bc-ws-profile-section">++ <div class="bc-ws-profile-section bc-ws-profile-split">++ <div class="bc-ws-col bc-ws-col--details"><div class="form-group"><label>Title</label>@@@ -420,22 +421,12 @@ function _wsRenderPanel() {oninput="_wsMarkDirty()">${esc(ms.description || '')}</textarea></div>-- <div class="bc-ws-row">-- <div class="form-group" style="flex:2">-- <label>Category</label>-- <select id="bc-category" class="form-input" onchange="_wsMarkDirty()">-- ${_wsRenderCategoryOptions(ms.category || '')}-- </select>-- </div>-- <div class="form-group" style="flex:1">-- <label> </label>-- <label class="bc-toggle-label">-- <input type="checkbox" id="bc-nsfw" ${ms.is_nsfw ? 'checked' : ''}-- onchange="_wsMarkDirty()">-- <i class="fa-solid fa-triangle-exclamation" style="color:var(--danger)"></i> NSFW-- </label>-- </div>-- </div>++ <!-- No category picker: the AI decides it from the stream itself (streams.ai_category wins everywhere). -->++ <label class="bc-switch">++ <input type="checkbox" id="bc-nsfw" ${ms.is_nsfw ? 'checked' : ''} onchange="_wsMarkDirty()">++ <span class="bc-switch-track" aria-hidden="true"><span class="bc-switch-thumb"></span></span>++ <span class="bc-switch-text"><b><i class="fa-solid fa-triangle-exclamation"></i> Mature content (NSFW)</b><small>Viewers confirm their age before the stream plays. The category is set automatically from what is on stream.</small></span>++ </label><!-- URL Slug --><div class="form-group">@@@ -448,6 +439,8 @@ function _wsRenderPanel() {</div></div>++ </div>++ <div class="bc-ws-col bc-ws-col--method"><!-- ═══ Streaming Method ═══ --><div class="form-group"><label>Streaming Method</label>@@@ -604,7 +597,7 @@ function _wsRenderPanel() {<!-- Video quality defaults (Class A blob, browser method only) --><div id="bc-ws-quality-wrap" style="${method === 'browser' ? '' : 'display:none'}"><details class="bc-ws-quality">-- <summary><i class="fa-solid fa-film"></i> Video Quality Defaults</summary>++ <summary><i class="fa-solid fa-film"></i><span class="bc-sum-title">Video quality</span><small class="bc-sum-val">${esc(p.resolution || '720')}p · ${esc(p.fps || '30')} fps · ${esc(p.bitrate || 2500)} kbps</small><i class="fa-solid fa-chevron-down bc-sum-chev" aria-hidden="true"></i></summary><div class="bc-ws-quality-inner bc-ws-row"><div class="form-group" style="margin:0;flex:1"><label style="font-size:0.82rem">Resolution</label>@@@ -630,6 +623,8 @@ function _wsRenderPanel() {</details></div>++ </div>++<!-- Control profile lives under Settings; createNewStream() and the savepayload both read #bc-control-config, which is rendered there. -->@@@ -1339,21 +1334,13 @@ async function _wsEndStream() {function _wsOpenPopoutChat() {const ms = _wsState.selectedMs;if (!ms) return;---- // Use the managed stream's live session ID if live, otherwise open global chat for the channel-- const liveSessionId = _wsGetLiveSessionId(ms.id);-- if (liveSessionId && typeof popoutChat === 'function') {-- popoutChat('stream', liveSessionId, (typeof currentUser !== 'undefined' && currentUser && currentUser.username) || null);-- } else {-- // Even when not live, open a popout chat scoped to the channel-- if (typeof popoutChat === 'function') {-- popoutChat('global');-- } else {-- const params = new URLSearchParams({ popout: '1', mode: 'global' });-- window.open(`/popout-chat.html?${params.toString()}`, `openvibe-chat-global`,-- 'width=400,height=600,menubar=no,toolbar=no,resizable=yes');-- }-- }++ // The channel's own chat: /popout/<username>[/<live session>] follows the channel's live state, so it is the++ // right window before going live too (never global chat).++ const liveSessionId = _wsGetLiveSessionId(ms.id) || null;++ const username = (typeof currentUser !== 'undefined' && currentUser && currentUser.username) || null;++ if (typeof popoutChat === 'function' && (username || liveSessionId)) { popoutChat('stream', liveSessionId, username); return; }++ if (!username) return;++ window.open(`/popout/${encodeURIComponent(username)}`, `openvibe_chat_chan-${username}`, 'width=400,height=600,menubar=no,toolbar=no,resizable=yes');}/* ── Method cards ────────────────────────────────────────────── */@@@ -1390,20 +1377,6 @@ function _wsRenderBrowserModeCards(selected) {</div>`).join('');}--/* ── Category options ────────────────────────────────────────── */----function _wsRenderCategoryOptions(selected) {-- const cats = [-- ['', 'Auto — the AI decides from the stream'],-- ['outdoors', 'Outdoors'], ['travel', 'Travel'], ['building', 'Building/Craft'],-- ['music', 'Music'], ['gaming', 'Gaming'], ['robot', 'Robot'],-- ['desktop', 'Desktop'], ['irl', 'IRL'], ['other', 'Other'],-- ];-- return cats.map(([v, l]) =>-- `<option value="${v}"${v === selected ? ' selected' : ''}>${l}</option>`-- ).join('');--}--/* ── Method-specific endpoint info ───────────────────────────── */function _wsRenderMethodEndpoint(method, streamKey, managedStreamId) {@@@ -9,6 +9,7 @@inline next to the username as "broken badges" until their cleanup timer). --><link rel="stylesheet" href="/css/style.css"><link rel="stylesheet" href="/css/chat-shared.css">++ <link rel="stylesheet" href="/css/i18n-star.css"><link rel="stylesheet" href="/css/cosmetics.css"><link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><style>
@@ -1067,8 +1067,9 @@
@@ -1067,8 +1067,9 @@
.bc-method-card.selected,
.bc-method-card.selected,
.bc-method-card-sm.selected {
.bc-method-card-sm.selected {
border-color: var(--accent);
border-color: var(--accent);
- background: rgba(241, 196, 15, 0.08);
- box-shadow: 0 0 0 1px var(--accent);
+ /* The theme's accent (a fixed yellow tint read brown on dark themes). */
+ background: color-mix(in srgb, var(--accent) 12%, var(--bg-tertiary));
+ box-shadow: 0 0 0 1px var(--accent), 0 6px 18px color-mix(in srgb, var(--accent) 18%, transparent);
}
}
.bc-method-card i {
.bc-method-card i {
@@ -2373,3 +2373,82 @@
@@ -2373,3 +2373,82 @@
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
color: var(--text-secondary); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;
color: var(--text-secondary); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;
}
}
+
+/* ═══ Go Live workspace layout (2026-09-28) ═══════════════════════════════════
+ No box around the page: while the slot manager shows, the main column is transparent and is the one scroll
+ (a thin themed scrollbar at the window edge, not one in the middle of the page). The slot list and the panel
+ are two cards side by side: the list is as tall as its slots and stays in view while the panel scrolls. */
+.broadcast-main:has(> #bc-stream-manager:not([style*="display: none"])) {
+ background: transparent; border: 0; border-radius: 0; box-shadow: none;
+}
+.broadcast-main { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--accent) 38%, transparent) transparent; }
+.broadcast-main::-webkit-scrollbar { width: 8px; }
+.broadcast-main::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--accent) 38%, transparent); border-radius: 8px; }
+.broadcast-main::-webkit-scrollbar-track { background: transparent; }
+#bc-stream-manager { max-width: 1480px; padding: 4px clamp(0px, 1vw, 12px) 28px; flex: none; min-height: auto; overflow: visible; }
+#bc-stream-manager > h2 { margin: 0 0 2px; font-size: 1.22rem; flex-wrap: wrap; row-gap: 6px; }
+#bc-stream-manager > h2 .glw-open-btn { margin-left: 6px; }
+#bc-stream-manager > .bc-page-subtitle { margin: 0 0 10px; font-size: 0.86rem; }
+.bc-workspace { flex: none; border: 0; border-radius: 0; background: transparent; overflow: visible; gap: 14px; align-items: flex-start; margin-top: 6px; }
+.bc-ws-sidebar, .bc-ws-main {
+ border: 1px solid var(--border); border-radius: 14px;
+ background: color-mix(in srgb, var(--bg-card, var(--surface)) 92%, transparent);
+ box-shadow: 0 1px 0 color-mix(in srgb, var(--text-primary) 4%, transparent) inset;
+}
+.bc-ws-sidebar { width: clamp(200px, 17vw, 264px); border-right: 1px solid var(--border); overflow: hidden; }
+.bc-ws-sidebar-hd { padding: 10px 10px 10px 14px; }
+.bc-ws-list { padding: 4px; flex: none; }
+.bc-ws-item { border-radius: 10px; padding: 9px 10px; }
+.bc-ws-item.selected { box-shadow: inset 3px 0 0 var(--accent); }
+.bc-ws-main { min-height: 0; }
+.bc-ws-panel { overflow: visible; }
+@media (min-width: 681px) {
+ .bc-ws-sidebar { position: sticky; top: 0; align-self: flex-start; max-height: calc(100dvh - var(--navbar-h, 56px) - 40px); }
+ .bc-ws-list { overflow-y: auto; flex: 1 1 auto; }
+}
+@media (max-width: 680px) {
+ .bc-workspace { gap: 10px; }
+ .bc-ws-sidebar { width: 100%; border-bottom: 1px solid var(--border); }
+ .bc-ws-main { width: 100%; }
+}
+
+/* NSFW: a themed switch with what it does, instead of a bare checkbox floating beside a select. */
+.bc-switch { display: flex; align-items: flex-start; gap: 12px; margin: 2px 0 16px; padding: 10px 12px; cursor: pointer;
+ border: 1px solid var(--border); border-radius: 12px; background: color-mix(in srgb, var(--bg-input, var(--bg)) 70%, transparent);
+ transition: border-color .18s, background-color .18s; }
+.bc-switch:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
+.bc-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
+.bc-switch-track { flex: none; position: relative; width: 38px; height: 22px; margin-top: 1px; border-radius: 999px;
+ background: color-mix(in srgb, var(--text-muted) 35%, transparent); border: 1px solid var(--border); transition: background-color .2s, border-color .2s; }
+.bc-switch-thumb { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-primary);
+ box-shadow: 0 1px 3px rgba(0,0,0,.35); transition: transform .22s cubic-bezier(.22,1,.36,1), background-color .2s; }
+.bc-switch input:checked + .bc-switch-track { background: var(--danger, #ef4444); border-color: var(--danger, #ef4444); }
+.bc-switch input:checked + .bc-switch-track .bc-switch-thumb { transform: translateX(16px); background: #fff; }
+.bc-switch input:focus-visible + .bc-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }
+.bc-switch:has(input:checked) { border-color: color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border)); background: color-mix(in srgb, var(--danger, #ef4444) 7%, transparent); }
+.bc-switch-text { display: grid; gap: 2px; min-width: 0; }
+.bc-switch-text b { font-size: 0.88rem; font-weight: 700; color: var(--text-primary); }
+.bc-switch-text b i { color: var(--danger, #ef4444); margin-right: 3px; }
+.bc-switch-text small { font-size: 0.76rem; color: var(--text-muted); line-height: 1.4; }
+
+/* Video quality: reads as a row that opens (chevron, current values), not a static label. */
+.bc-ws-quality { padding: 0; border-radius: 12px; overflow: hidden; }
+.bc-ws-quality > summary { display: flex; align-items: center; gap: 8px; padding: 11px 12px; cursor: pointer; list-style: none; user-select: none;
+ font-size: 0.86rem; font-weight: 650; color: var(--text-primary); transition: background-color .15s; }
+.bc-ws-quality > summary::-webkit-details-marker { display: none; }
+.bc-ws-quality > summary:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
+.bc-ws-quality > summary > i:first-child { color: var(--accent); margin: 0; }
+.bc-sum-val { margin-left: auto; font-size: 0.76rem; font-weight: 500; color: var(--text-muted); white-space: nowrap; }
+.bc-sum-chev { color: var(--text-muted); font-size: 0.75rem; transition: transform .22s ease; }
+.bc-ws-quality[open] .bc-sum-chev { transform: rotate(180deg); color: var(--accent); }
+.bc-ws-quality[open] > summary { border-bottom: 1px solid var(--border); }
+.bc-ws-quality .bc-ws-quality-inner { margin: 0; padding: 12px; }
+@media (prefers-reduced-motion: reduce) { .bc-switch-thumb, .bc-sum-chev { transition: none; } }
+
+/* Wide panels: details on the left, how you stream on the right, so fields are not a thousand pixels wide. */
+.bc-ws-main { container-type: inline-size; container-name: bcws; }
+@container bcws (min-width: 960px) {
+ .bc-ws-profile-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 8px 28px; align-items: start; }
+ .bc-ws-profile-split > .bc-ws-col--method .bc-method-picker-sm { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .bc-ws-profile-split > .bc-ws-col--details .bc-switch { margin-bottom: 14px; }
+}
@@ -403,7 +403,8 @@ function _wsRenderPanel() {
@@ -403,7 +403,8 @@ function _wsRenderPanel() {
<!-- ═══ TAB: Stream Profile ═══ -->
<!-- ═══ TAB: Stream Profile ═══ -->
<div class="bc-ws-tab-panel active" data-wstabpanel="profile">
<div class="bc-ws-tab-panel active" data-wstabpanel="profile">
- <div class="bc-ws-profile-section">
+ <div class="bc-ws-profile-section bc-ws-profile-split">
+ <div class="bc-ws-col bc-ws-col--details">
<div class="form-group">
<div class="form-group">
<label>Title</label>
<label>Title</label>
@@ -420,22 +421,12 @@ function _wsRenderPanel() {
@@ -420,22 +421,12 @@ function _wsRenderPanel() {
oninput="_wsMarkDirty()">${esc(ms.description || '')}</textarea>
oninput="_wsMarkDirty()">${esc(ms.description || '')}</textarea>
</div>
</div>
- <div class="bc-ws-row">
- <div class="form-group" style="flex:2">
- <label>Category</label>
- <select id="bc-category" class="form-input" onchange="_wsMarkDirty()">
- ${_wsRenderCategoryOptions(ms.category || '')}
- </select>
- </div>
- <div class="form-group" style="flex:1">
- <label> </label>
- <label class="bc-toggle-label">
- <input type="checkbox" id="bc-nsfw" ${ms.is_nsfw ? 'checked' : ''}
- onchange="_wsMarkDirty()">
- <i class="fa-solid fa-triangle-exclamation" style="color:var(--danger)"></i> NSFW
- </label>
- </div>
- </div>
+ <!-- No category picker: the AI decides it from the stream itself (streams.ai_category wins everywhere). -->
+ <label class="bc-switch">
+ <input type="checkbox" id="bc-nsfw" ${ms.is_nsfw ? 'checked' : ''} onchange="_wsMarkDirty()">
+ <span class="bc-switch-track" aria-hidden="true"><span class="bc-switch-thumb"></span></span>
+ <span class="bc-switch-text"><b><i class="fa-solid fa-triangle-exclamation"></i> Mature content (NSFW)</b><small>Viewers confirm their age before the stream plays. The category is set automatically from what is on stream.</small></span>
+ </label>
<!-- URL Slug -->
<!-- URL Slug -->
<div class="form-group">
<div class="form-group">
@@ -448,6 +439,8 @@ function _wsRenderPanel() {
@@ -448,6 +439,8 @@ function _wsRenderPanel() {
</div>
</div>
</div>
</div>
+ </div>
+ <div class="bc-ws-col bc-ws-col--method">
<!-- ═══ Streaming Method ═══ -->
<!-- ═══ Streaming Method ═══ -->
<div class="form-group">
<div class="form-group">
<label>Streaming Method</label>
<label>Streaming Method</label>
@@ -604,7 +597,7 @@ function _wsRenderPanel() {
@@ -604,7 +597,7 @@ function _wsRenderPanel() {
<!-- Video quality defaults (Class A blob, browser method only) -->
<!-- Video quality defaults (Class A blob, browser method only) -->
<div id="bc-ws-quality-wrap" style="${method === 'browser' ? '' : 'display:none'}">
<div id="bc-ws-quality-wrap" style="${method === 'browser' ? '' : 'display:none'}">
<details class="bc-ws-quality">
<details class="bc-ws-quality">
- <summary><i class="fa-solid fa-film"></i> Video Quality Defaults</summary>
+ <summary><i class="fa-solid fa-film"></i><span class="bc-sum-title">Video quality</span><small class="bc-sum-val">${esc(p.resolution || '720')}p · ${esc(p.fps || '30')} fps · ${esc(p.bitrate || 2500)} kbps</small><i class="fa-solid fa-chevron-down bc-sum-chev" aria-hidden="true"></i></summary>
<div class="bc-ws-quality-inner bc-ws-row">
<div class="bc-ws-quality-inner bc-ws-row">
<div class="form-group" style="margin:0;flex:1">
<div class="form-group" style="margin:0;flex:1">
<label style="font-size:0.82rem">Resolution</label>
<label style="font-size:0.82rem">Resolution</label>
@@ -630,6 +623,8 @@ function _wsRenderPanel() {
@@ -630,6 +623,8 @@ function _wsRenderPanel() {
</details>
</details>
</div>
</div>
+ </div>
+
<!-- Control profile lives under Settings; createNewStream() and the save
<!-- Control profile lives under Settings; createNewStream() and the save
payload both read #bc-control-config, which is rendered there. -->
payload both read #bc-control-config, which is rendered there. -->
@@ -1339,21 +1334,13 @@ async function _wsEndStream() {
@@ -1339,21 +1334,13 @@ async function _wsEndStream() {
function _wsOpenPopoutChat() {
function _wsOpenPopoutChat() {
const ms = _wsState.selectedMs;
const ms = _wsState.selectedMs;
if (!ms) return;
if (!ms) return;
-
- // Use the managed stream's live session ID if live, otherwise open global chat for the channel
- const liveSessionId = _wsGetLiveSessionId(ms.id);
- if (liveSessionId && typeof popoutChat === 'function') {
- popoutChat('stream', liveSessionId, (typeof currentUser !== 'undefined' && currentUser && currentUser.username) || null);
- } else {
- // Even when not live, open a popout chat scoped to the channel
- if (typeof popoutChat === 'function') {
- popoutChat('global');
- } else {
- const params = new URLSearchParams({ popout: '1', mode: 'global' });
- window.open(`/popout-chat.html?${params.toString()}`, `openvibe-chat-global`,
- 'width=400,height=600,menubar=no,toolbar=no,resizable=yes');
- }
- }
+ // The channel's own chat: /popout/<username>[/<live session>] follows the channel's live state, so it is the
+ // right window before going live too (never global chat).
+ const liveSessionId = _wsGetLiveSessionId(ms.id) || null;
+ const username = (typeof currentUser !== 'undefined' && currentUser && currentUser.username) || null;
+ if (typeof popoutChat === 'function' && (username || liveSessionId)) { popoutChat('stream', liveSessionId, username); return; }
+ if (!username) return;
+ window.open(`/popout/${encodeURIComponent(username)}`, `openvibe_chat_chan-${username}`, 'width=400,height=600,menubar=no,toolbar=no,resizable=yes');
}
}
/* ── Method cards ────────────────────────────────────────────── */
/* ── Method cards ────────────────────────────────────────────── */
@@ -1390,20 +1377,6 @@ function _wsRenderBrowserModeCards(selected) {
@@ -1390,20 +1377,6 @@ function _wsRenderBrowserModeCards(selected) {
</div>`).join('');
</div>`).join('');
}
}
-/* ── Category options ────────────────────────────────────────── */
-
-function _wsRenderCategoryOptions(selected) {
- const cats = [
- ['', 'Auto — the AI decides from the stream'],
- ['outdoors', 'Outdoors'], ['travel', 'Travel'], ['building', 'Building/Craft'],
- ['music', 'Music'], ['gaming', 'Gaming'], ['robot', 'Robot'],
- ['desktop', 'Desktop'], ['irl', 'IRL'], ['other', 'Other'],
- ];
- return cats.map(([v, l]) =>
- `<option value="${v}"${v === selected ? ' selected' : ''}>${l}</option>`
- ).join('');
-}
-
/* ── Method-specific endpoint info ───────────────────────────── */
/* ── Method-specific endpoint info ───────────────────────────── */
function _wsRenderMethodEndpoint(method, streamKey, managedStreamId) {
function _wsRenderMethodEndpoint(method, streamKey, managedStreamId) {
@@ -9,6 +9,7 @@
@@ -9,6 +9,7 @@
inline next to the username as "broken badges" until their cleanup timer). -->
inline next to the username as "broken badges" until their cleanup timer). -->
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/chat-shared.css">
<link rel="stylesheet" href="/css/chat-shared.css">
+ <link rel="stylesheet" href="/css/i18n-star.css">
<link rel="stylesheet" href="/css/cosmetics.css">
<link rel="stylesheet" href="/css/cosmetics.css">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>