openvibers/openvibe.live Commits
The phone studio sideways: the rail is two columns under the top bar (it ran 150 px off the screen and over the bar); docs: the OBS monitoring echo, and the chat socket's token_in_url and stream_changed frames for bots (#390)
Changes
@@@ -83,6 +83,17 @@ Authorization: Bearer hbt_YOUR_TOKEN_HEREThe token works everywhere a JWT would — the server auto-detects the `hbt_` prefix and validates accordingly. A token in the WebSocket URL (`?token=`) no longer signs a socket in (removed 2026-10-10): URLs end up in proxy logs and browser history. Use the header or the `join` message.++Two frames a bot should handle (OpenVibe.Chat, 2026-10-26, from Maticus's bot):++++- **`{ "type": "system", "code": "token_in_url", … }`.** You put the token in the address. The socket stays signed out,++ and this frame says how to send it instead. Before, the bot stayed anonymous with no word why.++- **`{ "type": "stream_changed", "stream_id", "previous_stream_id", "channel_user_id" }`.** The channel went live again++ on the same slot, and your socket, still joined to the ended stream, was moved to the new one. You stay in the viewer++ list, and chat voice, sounds and robot buttons keep working, with no re-join. Use the new `stream_id` from here on.++++A `!word` that is not one of the channel's sounds presses the channel robot's button of that name, for a signed-in++viewer. It says nothing when the robot has no such button.++## Managing Tokens### List tokens@@@ -15,6 +15,11 @@ OpenVibe.Live supports four live broadcast methods, each suited to different use- **Latency**: 2-5 seconds- **Setup**: Copy Server URL and Stream Key into OBS- **Features**: Full OBS feature set, live thumbnail preview on broadcast page++- **If viewers hear an echo:** look at OBS's audio monitoring (Edit → Advanced Audio Properties, *Audio Monitoring*).++ A microphone set to *Monitor and Output* plays itself back out of the speakers, the microphone picks that up again,++ and the stream gets a loud echo (Maticus's "920 mic", 2026-10-26). Set microphones to *Monitor Off*, or monitor++ on headphones only. A browser source that plays chat voice or channel sounds can stay on *Monitor and Output* if you++ want to hear it, but only when no microphone can hear your speakers.### WHIP (OBS, FFmpeg, GStreamer — or any web page)- **Best for**: WebRTC-native encoders and custom publishers, including browser-only sites@@@ -2607,6 +2607,21 @@ body.bc-studio-on .toast-container { z-index: 1500; }.bcs-btn[data-act="light"][aria-pressed="true"] .bcs-ico { background: #fde047; color: #111; border-color: transparent; }.bcs-btn.is-busy { opacity: 0.75; }.bcs-lbl { font-size: 0.7rem; font-weight: 700; line-height: 1.1; text-align: center; text-shadow: 0 1px 2px rgba(0,0,0,0.9); }++/* A phone on its side is about 330 px tall: seven buttons in one column ran 150 px off the top of the screen and over++ the top bar (Maticus, 2026-10-26: buttons on top of each other). Sideways the rail is two columns under the top bar++ (three on the shortest screens), Flip nearest the edge, words kept, and the chat stops short of it. */++@media (orientation: landscape) and (max-height: 520px) {++ .bcs-rail { display: grid; grid-template-columns: repeat(2, 56px); gap: 6px 4px; direction: rtl; }++ .bcs-btn { width: 56px; direction: ltr; }++ .bcs-ico { width: 40px; height: 40px; font-size: 1rem; }++ body.bc-studio-on #bc-chat-sidebar { right: 128px; }++ body.bc-studio-on #bc-chat-sidebar::before { right: -128px; }++}++@media (orientation: landscape) and (max-height: 300px) {++ .bcs-rail { grid-template-columns: repeat(3, 56px); }++ body.bc-studio-on #bc-chat-sidebar { right: 188px; }++ body.bc-studio-on #bc-chat-sidebar::before { right: -188px; }++}.bcs-sheet-wrap {position: fixed; inset: 0; z-index: 1300; display: flex; align-items: flex-end; justify-content: center;@@@ -259,6 +259,19 @@ function loadStudio({ stream, cameras = [], screenShare = false }) {assert.ok(/body\.bc-studio-on \.bc-video-container \{[^}]*position: fixed/.test(read('public/css/features/broadcast.css')));});++ await check('sideways the rail is two columns under the top bar (three on the shortest), and the chat stops short of it', async () => {++ // Seven buttons in one column are about 470 px: on a phone on its side (about 330 px) they ran 150 px off the top++ // of the screen and over the top bar (Maticus, 2026-10-26). Measured in Chrome: 844x340 and 740x330 fit in two++ // columns below the bar, 667x290 in three.++ const css = read('public/css/features/broadcast.css');++ const side = /@media \(orientation: landscape\) and \(max-height: 520px\) \{([^@]*)\}\n@media \(orientation: landscape\) and \(max-height: 300px\) \{([^@]*)\}/.exec(css);++ assert.ok(side, 'the sideways rules');++ assert.ok(/\.bcs-rail \{ display: grid; grid-template-columns: repeat\(2, 56px\);[^}]*direction: rtl; \}/.test(side[1]), 'two columns, Flip nearest the edge');++ assert.ok(/\.bcs-btn \{ width: 56px; direction: ltr; \}/.test(side[1]) && /\.bcs-ico \{ width: 40px; height: 40px;/.test(side[1]));++ assert.ok(side[1].includes('body.bc-studio-on #bc-chat-sidebar { right: 128px; }'), 'the chat clears two columns');++ assert.ok(side[2].includes('.bcs-rail { grid-template-columns: repeat(3, 56px); }') && side[2].includes('body.bc-studio-on #bc-chat-sidebar { right: 188px; }'));++ });++if (failures) { console.log(`\n${failures} failed`); process.exit(1); }console.log('\nall passed');})();
@@ -83,6 +83,17 @@ Authorization: Bearer hbt_YOUR_TOKEN_HERE
@@ -83,6 +83,17 @@ Authorization: Bearer hbt_YOUR_TOKEN_HERE
The token works everywhere a JWT would — the server auto-detects the `hbt_` prefix and validates accordingly. A token in the WebSocket URL (`?token=`) no longer signs a socket in (removed 2026-10-10): URLs end up in proxy logs and browser history. Use the header or the `join` message.
The token works everywhere a JWT would — the server auto-detects the `hbt_` prefix and validates accordingly. A token in the WebSocket URL (`?token=`) no longer signs a socket in (removed 2026-10-10): URLs end up in proxy logs and browser history. Use the header or the `join` message.
+Two frames a bot should handle (OpenVibe.Chat, 2026-10-26, from Maticus's bot):
+
+- **`{ "type": "system", "code": "token_in_url", … }`.** You put the token in the address. The socket stays signed out,
+ and this frame says how to send it instead. Before, the bot stayed anonymous with no word why.
+- **`{ "type": "stream_changed", "stream_id", "previous_stream_id", "channel_user_id" }`.** The channel went live again
+ on the same slot, and your socket, still joined to the ended stream, was moved to the new one. You stay in the viewer
+ list, and chat voice, sounds and robot buttons keep working, with no re-join. Use the new `stream_id` from here on.
+
+A `!word` that is not one of the channel's sounds presses the channel robot's button of that name, for a signed-in
+viewer. It says nothing when the robot has no such button.
+
## Managing Tokens
## Managing Tokens
### List tokens
### List tokens
@@ -15,6 +15,11 @@ OpenVibe.Live supports four live broadcast methods, each suited to different use
@@ -15,6 +15,11 @@ OpenVibe.Live supports four live broadcast methods, each suited to different use
- **Latency**: 2-5 seconds
- **Latency**: 2-5 seconds
- **Setup**: Copy Server URL and Stream Key into OBS
- **Setup**: Copy Server URL and Stream Key into OBS
- **Features**: Full OBS feature set, live thumbnail preview on broadcast page
- **Features**: Full OBS feature set, live thumbnail preview on broadcast page
+- **If viewers hear an echo:** look at OBS's audio monitoring (Edit → Advanced Audio Properties, *Audio Monitoring*).
+ A microphone set to *Monitor and Output* plays itself back out of the speakers, the microphone picks that up again,
+ and the stream gets a loud echo (Maticus's "920 mic", 2026-10-26). Set microphones to *Monitor Off*, or monitor
+ on headphones only. A browser source that plays chat voice or channel sounds can stay on *Monitor and Output* if you
+ want to hear it, but only when no microphone can hear your speakers.
### WHIP (OBS, FFmpeg, GStreamer — or any web page)
### WHIP (OBS, FFmpeg, GStreamer — or any web page)
- **Best for**: WebRTC-native encoders and custom publishers, including browser-only sites
- **Best for**: WebRTC-native encoders and custom publishers, including browser-only sites
@@ -2607,6 +2607,21 @@ body.bc-studio-on .toast-container { z-index: 1500; }
@@ -2607,6 +2607,21 @@ body.bc-studio-on .toast-container { z-index: 1500; }
.bcs-btn[data-act="light"][aria-pressed="true"] .bcs-ico { background: #fde047; color: #111; border-color: transparent; }
.bcs-btn[data-act="light"][aria-pressed="true"] .bcs-ico { background: #fde047; color: #111; border-color: transparent; }
.bcs-btn.is-busy { opacity: 0.75; }
.bcs-btn.is-busy { opacity: 0.75; }
.bcs-lbl { font-size: 0.7rem; font-weight: 700; line-height: 1.1; text-align: center; text-shadow: 0 1px 2px rgba(0,0,0,0.9); }
.bcs-lbl { font-size: 0.7rem; font-weight: 700; line-height: 1.1; text-align: center; text-shadow: 0 1px 2px rgba(0,0,0,0.9); }
+/* A phone on its side is about 330 px tall: seven buttons in one column ran 150 px off the top of the screen and over
+ the top bar (Maticus, 2026-10-26: buttons on top of each other). Sideways the rail is two columns under the top bar
+ (three on the shortest screens), Flip nearest the edge, words kept, and the chat stops short of it. */
+@media (orientation: landscape) and (max-height: 520px) {
+ .bcs-rail { display: grid; grid-template-columns: repeat(2, 56px); gap: 6px 4px; direction: rtl; }
+ .bcs-btn { width: 56px; direction: ltr; }
+ .bcs-ico { width: 40px; height: 40px; font-size: 1rem; }
+ body.bc-studio-on #bc-chat-sidebar { right: 128px; }
+ body.bc-studio-on #bc-chat-sidebar::before { right: -128px; }
+}
+@media (orientation: landscape) and (max-height: 300px) {
+ .bcs-rail { grid-template-columns: repeat(3, 56px); }
+ body.bc-studio-on #bc-chat-sidebar { right: 188px; }
+ body.bc-studio-on #bc-chat-sidebar::before { right: -188px; }
+}
.bcs-sheet-wrap {
.bcs-sheet-wrap {
position: fixed; inset: 0; z-index: 1300; display: flex; align-items: flex-end; justify-content: center;
position: fixed; inset: 0; z-index: 1300; display: flex; align-items: flex-end; justify-content: center;
@@ -259,6 +259,19 @@ function loadStudio({ stream, cameras = [], screenShare = false }) {
@@ -259,6 +259,19 @@ function loadStudio({ stream, cameras = [], screenShare = false }) {
assert.ok(/body\.bc-studio-on \.bc-video-container \{[^}]*position: fixed/.test(read('public/css/features/broadcast.css')));
assert.ok(/body\.bc-studio-on \.bc-video-container \{[^}]*position: fixed/.test(read('public/css/features/broadcast.css')));
});
});
+ await check('sideways the rail is two columns under the top bar (three on the shortest), and the chat stops short of it', async () => {
+ // Seven buttons in one column are about 470 px: on a phone on its side (about 330 px) they ran 150 px off the top
+ // of the screen and over the top bar (Maticus, 2026-10-26). Measured in Chrome: 844x340 and 740x330 fit in two
+ // columns below the bar, 667x290 in three.
+ const css = read('public/css/features/broadcast.css');
+ const side = /@media \(orientation: landscape\) and \(max-height: 520px\) \{([^@]*)\}\n@media \(orientation: landscape\) and \(max-height: 300px\) \{([^@]*)\}/.exec(css);
+ assert.ok(side, 'the sideways rules');
+ assert.ok(/\.bcs-rail \{ display: grid; grid-template-columns: repeat\(2, 56px\);[^}]*direction: rtl; \}/.test(side[1]), 'two columns, Flip nearest the edge');
+ assert.ok(/\.bcs-btn \{ width: 56px; direction: ltr; \}/.test(side[1]) && /\.bcs-ico \{ width: 40px; height: 40px;/.test(side[1]));
+ assert.ok(side[1].includes('body.bc-studio-on #bc-chat-sidebar { right: 128px; }'), 'the chat clears two columns');
+ assert.ok(side[2].includes('.bcs-rail { grid-template-columns: repeat(3, 56px); }') && side[2].includes('body.bc-studio-on #bc-chat-sidebar { right: 188px; }'));
+ });
+
if (failures) { console.log(`\n${failures} failed`); process.exit(1); }
if (failures) { console.log(`\n${failures} failed`); process.exit(1); }
console.log('\nall passed');
console.log('\nall passed');
})();
})();