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)

OP OpenVibers committed 17 days ago · 7d000fc327 · 4 files, +44 −0

Changes

ddiff --git a/docs/api-tokens.md b/docs/api-tokens.md
iindex 2b5d5cc8..5c9f93b1 100644
---- a/docs/api-tokens.md
++++ b/docs/api-tokens.md
@@@ -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.
++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
ddiff --git a/docs/broadcasting.md b/docs/broadcasting.md
iindex ca5b9790..57a85160 100644
---- a/docs/broadcasting.md
++++ b/docs/broadcasting.md
@@@ -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
ddiff --git a/public/css/features/broadcast.css b/public/css/features/broadcast.css
iindex 1d1f19d9..789b0a30 100644
---- a/public/css/features/broadcast.css
++++ b/public/css/features/broadcast.css
@@@ -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;
ddiff --git a/test/broadcast-studio.test.js b/test/broadcast-studio.test.js
iindex ed5a3e55..dc039075 100644
---- a/test/broadcast-studio.test.js
++++ b/test/broadcast-studio.test.js
@@@ -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');
})();