openvibers/openvibe.live Commits

Viewers no longer hear chat TTS read in their own browser on channels they watch, unless they turn on "Read TTS aloud on channels I watch" (#382)

OP OpenVibers committed 18 days ago · aa4a6e88bc · 2 files, +49 −3

The speaker button under the chat input is the sender's switch (are my messages read) and defaults on; it also
gated local playback on other channels, so every viewer's browser read the stream's TTS over the stream's own audio
(owner and Maticus, 2026-10-26). Local playback on a watched channel is now its own setting, off by default.

Changes

ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 925f966f..28712be8 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -136,6 +136,10 @@ const CHAT_SETTINGS_DEFAULTS = {
// Defaults ON so messages aren't dot-prefixed/muted-styled by default.
streamingTtsEnabled: true, // Separate TTS toggle while broadcasting live
channelTtsAlways: false, // Hear your own channel's chat TTS even while NOT live
++ ttsHearWatching: false, // Also read TTS aloud in this browser on channels you WATCH. Off by default: the stream's
++ // own audio already carries its chat TTS (the streamer plays it), and a second copy in
++ // every viewer's browser was read over the stream (owner and Maticus, 2026-10-26).
++ // This browser only: not part of the synced chat.tts_defaults record.
ttsVolume: 80, // TTS message volume (0–100)
soundVolume: 80, // Chat !sound / soundboard clip volume (0–100), independent of TTS
// Chat sounds (soundboard + channel !sound commands) — independent of TTS
@@@ -2298,7 +2302,7 @@ function handleChatMessage(msg) {
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) {
speakTTS(msg.message || msg.text, msg.voiceFX, msg.username);
}
-- } else if (isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {
++ } else if (chatSettings.ttsHearWatching && isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {
speakTTS(msg.message || msg.text, msg.voiceFX, msg.username);
}
break;
@@@ -2310,7 +2314,7 @@ function handleChatMessage(msg) {
&& !_ttsRecentlyPlayed(msg)) playBroadcastTTSAudio(msg);
} else if (_isOwnChannelChat()) {
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) playTTSAudio(msg);
-- } else if (isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {
++ } else if (chatSettings.ttsHearWatching && isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {
playTTSAudio(msg);
}
break;
@@@ -5824,7 +5828,7 @@ function buildSettingsPanelHTML() {
<div class="csp-title"><i class="fa-solid fa-volume-high"></i> Text-to-Speech</div>
<!-- No "Enable TTS" row here — the speaker button under the chat input IS
that toggle; a second switch for the same thing just confused people. -->
-- <label class="csp-row" title="Controls TTS in your own channel chat while you are live (plays in one tab only, synced across your tabs). TTS on other channels uses the speaker button under the chat input.">
++ <label class="csp-row" title="Controls TTS in your own channel chat while you are live (plays in one tab only, synced across your tabs). Whether your own messages are read aloud is the speaker button under the chat input.">
<span>TTS On My Channel When Live</span>
<input type="checkbox" data-setting="streamingTtsEnabled" onchange="onChatSettingChange(this)">
</label>
@@@ -5832,6 +5836,10 @@ function buildSettingsPanelHTML() {
<span>TTS On My Channel When Offline</span>
<input type="checkbox" data-setting="channelTtsAlways" onchange="onChatSettingChange(this)">
</label>
++ <label class="csp-row" title="The stream's audio already has its chat read aloud (the streamer plays it). Turn this on to also hear it read in this browser.">
++ <span>Read TTS Aloud On Channels I Watch</span>
++ <input type="checkbox" data-setting="ttsHearWatching" onchange="onChatSettingChange(this)">
++ </label>
<label class="csp-row">
<span>TTS Volume</span>
<input type="range" min="0" max="100" step="5" data-setting="ttsVolume" onchange="onChatSettingChange(this)" oninput="onChatSettingChange(this)">
@@@ -5883,6 +5891,7 @@ function onChatSettingChange(el) {
else chatSettings[key] = el.value;
saveChatSettings();
if (key === 'ttsEnabled' || key === 'streamingTtsEnabled') syncTTSToggleButtons();
++ if (key === 'ttsHearWatching' && !chatSettings.ttsHearWatching && typeof cancelAllTTS === 'function') cancelAllTTS();
if (key === 'showFloatingChat') _fcwUpdateVisibility();
if (key === 'showGlobalInStream' || key === 'showAllStreamsInStream') _syncGlobalFeed();
if (key === 'friendlyGlobal') { _friendlyEnsureRules().then(() => { if (getChatEl().isGlobal) hydrateActiveChatHistory(chatStreamId).catch(() => { }); }); }
ddiff --git a/test/tts-watching-optin.test.js b/test/tts-watching-optin.test.js
nnew file mode 100644
iindex 00000000..a14f9340
---- /dev/null
++++ b/test/tts-watching-optin.test.js
@@@ -0,0 +1,37 @@
++/**
++ * A viewer's browser does not read chat TTS aloud on channels they watch unless they turn it on (owner and Maticus,
++ * 2026-10-26: "viewers hear tts through their own browser now"; another developer opening Maticus's stream heard every
++ * TTS line read in their own browser over the stream, which already carries it).
++ *
++ * The speaker button under the chat input (ttsEnabled) is the sender's switch: whether your own messages are read.
++ * It defaults on, and it also gated local playback on other channels, so every viewer heard TTS. Local playback on a
++ * watched channel is now its own setting, ttsHearWatching, off by default; the streamer's own-channel paths are
++ * unchanged.
++ *
++ * node test/tts-watching-optin.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const chat = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'chat.js'), 'utf8');
++
++assert.match(chat, /\n {4}ttsHearWatching: false, /, 'off by default');
++assert.match(chat, /\n {4}ttsEnabled: true, /, 'the sender switch keeps its default');
++
++// Both viewer branches (browser speech and server audio) need the new setting.
++const viewerSpeak = chat.match(/\} else if \(([^)]*?)isChatTTSEnabled\(\) && !_isOwnTtsMessage\(msg\) && _isTTSEnabledForSource\(msg\.source_platform\)\) \{\n\s*(speakTTS|playTTSAudio)\(/g) || [];
++assert.strictEqual(viewerSpeak.length, 2, 'the tts and tts-audio cases each have one viewer branch');
++for (const b of viewerSpeak) assert.ok(b.includes('chatSettings.ttsHearWatching && isChatTTSEnabled()'), `gated: ${b.slice(0, 80)}`);
++assert.ok(!/\} else if \(isChatTTSEnabled\(\) && !_isOwnTtsMessage/.test(chat), 'no viewer branch speaks on ttsEnabled alone');
++
++// The setting is in the panel, and turning it off stops what is being read.
++assert.ok(chat.includes('<span>Read TTS Aloud On Channels I Watch</span>') && chat.includes('data-setting="ttsHearWatching"'));
++assert.ok(chat.includes("if (key === 'ttsHearWatching' && !chatSettings.ttsHearWatching && typeof cancelAllTTS === 'function') cancelAllTTS();"));
++
++// Not part of the synced record (Chat's chat.tts_defaults schema has no such field).
++const ttsModule = chat.slice(chat.indexOf('function _ttsModuleFromSettings'), chat.indexOf('const CHAT_MODULES'));
++assert.ok(!ttsModule.includes('ttsHearWatching'), 'local to this browser');
++
++console.log('tts watching opt-in: viewers hear TTS from the stream only, unless they turn on reading it here');