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)
Changes
@@@ -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 livechannelTtsAlways: 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 ISthat 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(() => { }); }); }nnew file mode 100644@@@ -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');
@@ -136,6 +136,10 @@ const CHAT_SETTINGS_DEFAULTS = {
@@ -136,6 +136,10 @@ const CHAT_SETTINGS_DEFAULTS = {
// Defaults ON so messages aren't dot-prefixed/muted-styled by default.
// Defaults ON so messages aren't dot-prefixed/muted-styled by default.
streamingTtsEnabled: true, // Separate TTS toggle while broadcasting live
streamingTtsEnabled: true, // Separate TTS toggle while broadcasting live
channelTtsAlways: false, // Hear your own channel's chat TTS even while NOT 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)
ttsVolume: 80, // TTS message volume (0–100)
soundVolume: 80, // Chat !sound / soundboard clip volume (0–100), independent of TTS
soundVolume: 80, // Chat !sound / soundboard clip volume (0–100), independent of TTS
// Chat sounds (soundboard + channel !sound commands) — independent of TTS
// Chat sounds (soundboard + channel !sound commands) — independent of TTS
@@ -2298,7 +2302,7 @@ function handleChatMessage(msg) {
@@ -2298,7 +2302,7 @@ function handleChatMessage(msg) {
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) {
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) {
speakTTS(msg.message || msg.text, msg.voiceFX, msg.username);
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);
speakTTS(msg.message || msg.text, msg.voiceFX, msg.username);
}
}
break;
break;
@@ -2310,7 +2314,7 @@ function handleChatMessage(msg) {
@@ -2310,7 +2314,7 @@ function handleChatMessage(msg) {
&& !_ttsRecentlyPlayed(msg)) playBroadcastTTSAudio(msg);
&& !_ttsRecentlyPlayed(msg)) playBroadcastTTSAudio(msg);
} else if (_isOwnChannelChat()) {
} else if (_isOwnChannelChat()) {
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) playTTSAudio(msg);
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);
playTTSAudio(msg);
}
}
break;
break;
@@ -5824,7 +5828,7 @@ function buildSettingsPanelHTML() {
@@ -5824,7 +5828,7 @@ function buildSettingsPanelHTML() {
<div class="csp-title"><i class="fa-solid fa-volume-high"></i> Text-to-Speech</div>
<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
<!-- 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. -->
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>
<span>TTS On My Channel When Live</span>
<input type="checkbox" data-setting="streamingTtsEnabled" onchange="onChatSettingChange(this)">
<input type="checkbox" data-setting="streamingTtsEnabled" onchange="onChatSettingChange(this)">
</label>
</label>
@@ -5832,6 +5836,10 @@ function buildSettingsPanelHTML() {
@@ -5832,6 +5836,10 @@ function buildSettingsPanelHTML() {
<span>TTS On My Channel When Offline</span>
<span>TTS On My Channel When Offline</span>
<input type="checkbox" data-setting="channelTtsAlways" onchange="onChatSettingChange(this)">
<input type="checkbox" data-setting="channelTtsAlways" onchange="onChatSettingChange(this)">
</label>
</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">
<label class="csp-row">
<span>TTS Volume</span>
<span>TTS Volume</span>
<input type="range" min="0" max="100" step="5" data-setting="ttsVolume" onchange="onChatSettingChange(this)" oninput="onChatSettingChange(this)">
<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) {
@@ -5883,6 +5891,7 @@ function onChatSettingChange(el) {
else chatSettings[key] = el.value;
else chatSettings[key] = el.value;
saveChatSettings();
saveChatSettings();
if (key === 'ttsEnabled' || key === 'streamingTtsEnabled') syncTTSToggleButtons();
if (key === 'ttsEnabled' || key === 'streamingTtsEnabled') syncTTSToggleButtons();
+ if (key === 'ttsHearWatching' && !chatSettings.ttsHearWatching && typeof cancelAllTTS === 'function') cancelAllTTS();
if (key === 'showFloatingChat') _fcwUpdateVisibility();
if (key === 'showFloatingChat') _fcwUpdateVisibility();
if (key === 'showGlobalInStream' || key === 'showAllStreamsInStream') _syncGlobalFeed();
if (key === 'showGlobalInStream' || key === 'showAllStreamsInStream') _syncGlobalFeed();
if (key === 'friendlyGlobal') { _friendlyEnsureRules().then(() => { if (getChatEl().isGlobal) hydrateActiveChatHistory(chatStreamId).catch(() => { }); }); }
if (key === 'friendlyGlobal') { _friendlyEnsureRules().then(() => { if (getChatEl().isGlobal) hydrateActiveChatHistory(chatStreamId).catch(() => { }); }); }
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -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');