fix(voice): clamp transmit when speakers muted

This commit is contained in:
Edison Jwa
2026-05-20 16:37:48 +09:00
parent 9a2c882ab0
commit 171baf6e41
2 changed files with 33 additions and 10 deletions
+8 -1
View File
@@ -718,8 +718,14 @@ class _BetaHomeState extends State<_BetaHome> {
final next = !_outputMuted;
try {
await rust.setOutputMuted(muted: next);
await rust.setHardMute(
muted: next || _inputMuted || _hardMuteByPermission,
);
if (!mounted) return;
setState(() => _outputMuted = next);
setState(() {
_outputMuted = next;
_hardMute = next || _inputMuted || _hardMuteByPermission;
});
} catch (e) {
if (!mounted) return;
setState(() => _error = e.toString());
@@ -887,6 +893,7 @@ class _BetaHomeState extends State<_BetaHome> {
if (!mounted) return;
setState(() {
_inputMuted = next;
_hardMute = next;
_hardMuteByPermission = false;
});
} catch (e) {
+25 -9
View File
@@ -300,6 +300,13 @@ struct ConnectedState {
/// for that channel. Kept in memory only; durable secret storage is
/// a separate storage-schema decision.
channel_passwords: HashMap<u64, String>,
/// Local microphone mute state used to resolve the single
/// transmit hard-mute clamp from independent UI mute buttons.
local_input_muted: bool,
/// Local speaker mute state. Product rule for P0: speaker muted
/// also means mic disabled, so this participates in the local
/// transmit hard-mute clamp without sending server-side deafen.
local_output_muted: bool,
}
fn normalize_channel_password(password: Option<String>) -> Option<String> {
@@ -610,6 +617,8 @@ impl ChanoraSession {
sup_inner,
join_state,
channel_passwords: HashMap::new(),
local_input_muted: false,
local_output_muted: false,
});
// TS3 servers auto-place a newly-connected client into the
@@ -996,15 +1005,23 @@ impl ChanoraSession {
input: Option<bool>,
output: Option<bool>,
) -> Result<(), CoreError> {
let guard = self.inner.lock().await;
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
let mut guard = self.inner.lock().await;
let state = guard.as_mut().ok_or(CoreError::NotConnected)?;
// Server-side output mute/deafen makes TeamSpeak/tsclientlib
// consider the client unable to send audio. That is correct
// for a server-visible "deafened" state, but our P0 speaker
// button is a local playback mute. Keep output mute local-only
// so Android can still transmit while the user silences remote
// playback on this device.
state.protocol.set_muted(input, None).await?;
// button is a local playback mute. Keep output mute off the
// server-side output/deafen flag, but fold it into the mic
// disabled state below because P0 product semantics are:
// speaker disabled also means microphone disabled.
if let Some(muted) = input {
state.local_input_muted = muted;
}
if let Some(muted) = output {
state.local_output_muted = muted;
}
let mic_disabled = state.local_input_muted || state.local_output_muted;
state.protocol.set_muted(Some(mic_disabled), None).await?;
if let Some(muted) = output {
if let Some(audio) = state.audio.as_ref() {
audio.set_output_muted(muted);
@@ -1019,9 +1036,8 @@ impl ChanoraSession {
// flooded the log to 200 MB on the Korean test host.
// Clamp the transmit-mode selector's hard_mute input so
// the gate goes false too.
if let Some(muted) = input {
self.voice_selector.set_hard_mute(muted);
}
self.voice_selector
.set_hard_mute(mic_disabled);
Ok(())
}