Report mute/hardware status correctly, add Local Mic Mute, fix stale talking indicator
- Mute/deafen status now goes out through clientupdate instead of clientedit, which silently rejected client_input_muted/output_muted since they are runtime status, not editable client properties. - Publish client_input_hardware so other clients see "Microphone Disabled" instead of silence while another tab holds the capture device; track input/output hardware flags for other clients too, and show a distinct grey "disabled" icon instead of reusing the red "muted" one for both the tree and the info panel. - Implement TS3's Enable/Disable/Toggle Local Mic Mute hotkeys: they silence capture like a real mute, but never touch the published mute status or play a sound. - A speaker's "talking" indicator only ever cleared when its zero-length end-of-burst voice packet arrived; if that one UDP packet was lost, the indicator stuck until their next burst. Add a watchdog that clears it after 200ms of silence from that speaker regardless.
This commit is contained in:
@@ -18,6 +18,15 @@ public interface VoiceInput extends Microphone {
|
||||
|
||||
void setMuted(boolean muted);
|
||||
|
||||
/**
|
||||
* Silences capture the same way {@link #setMuted} does, but locally only: it does
|
||||
* not touch {@link #isMuted}, so the mute status published to the server (and the
|
||||
* mute/unmute sound) is unaffected. TS3's "Local Mic Mute".
|
||||
*/
|
||||
void setLocalMuted(boolean muted);
|
||||
|
||||
boolean isLocalMuted();
|
||||
|
||||
void setMode(Settings.InputMode mode);
|
||||
|
||||
void setVadMode(Settings.VadMode mode);
|
||||
|
||||
@@ -36,13 +36,16 @@ public enum HotkeyAction {
|
||||
"Unmute Microphone", true, Argument.NONE, true),
|
||||
MIC_TOGGLE(Category.SERVER, 0x0030, "Toggle",
|
||||
"Toggle Microphone Mute", false, Argument.NONE, true),
|
||||
/** TS3's "local" mute is the hardware-level mute of the capture device itself. */
|
||||
/**
|
||||
* TS3's "local" mute silences capture without publishing a mute status, or playing
|
||||
* a mute/unmute sound — unlike {@link #MIC_MUTE}.
|
||||
*/
|
||||
MIC_LOCAL_UNMUTE(Category.SERVER, 0x0030, "ActivateLocalMute",
|
||||
"Disable Local Mic Mute", true, Argument.NONE, false),
|
||||
"Disable Local Mic Mute", true, Argument.NONE, true),
|
||||
MIC_LOCAL_MUTE(Category.SERVER, 0x0030, "DeactivateLocalMute",
|
||||
"Enable Local Mic Mute", true, Argument.NONE, false),
|
||||
"Enable Local Mic Mute", true, Argument.NONE, true),
|
||||
MIC_LOCAL_TOGGLE(Category.SERVER, 0x0030, "ToggleLocalMute",
|
||||
"Toggle Local Mic Mute", true, Argument.NONE, false),
|
||||
"Toggle Local Mic Mute", true, Argument.NONE, true),
|
||||
|
||||
SPEAKER_MUTE(Category.SERVER, 0x0040, "Mute",
|
||||
"Mute Speaker", true, Argument.NONE, true),
|
||||
|
||||
@@ -21,6 +21,10 @@ public final class ClientEntry {
|
||||
public boolean talking;
|
||||
public boolean inputMuted; // microphone muted (client_input_muted)
|
||||
public boolean outputMuted; // speakers muted / deafened (client_output_muted)
|
||||
/** False while the capture device is unavailable: another tab holds the microphone. */
|
||||
public boolean inputHardware = true;
|
||||
/** False while the playback device is unavailable. */
|
||||
public boolean outputHardware = true;
|
||||
public boolean away;
|
||||
/** The message published with the away state, empty when there is none. */
|
||||
public String awayMessage = "";
|
||||
|
||||
@@ -153,6 +153,22 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
} catch (Exception e) {
|
||||
ui.onError("Microphone unavailable: " + rootMessage(e));
|
||||
}
|
||||
pushInputHardware();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the capture device is actually available on this connection, so
|
||||
* other clients see "Microphone Disabled" rather than plain silence while another
|
||||
* tab holds it. {@code clientinit} always claims hardware input, so this is what
|
||||
* corrects that once tabs start sharing one microphone.
|
||||
*/
|
||||
private void pushInputHardware() {
|
||||
boolean hardware = microphoneActive;
|
||||
updateSelf(self -> self.inputHardware = hardware);
|
||||
if (client == null || !connected) return;
|
||||
selfUpdate(cmd ->
|
||||
cmd.add(new CommandSingleParameter("client_input_hardware", hardware ? "1" : "0")),
|
||||
"Microphone hardware status update failed");
|
||||
}
|
||||
|
||||
// ---- connection lifecycle ----
|
||||
@@ -403,6 +419,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
e.talkPower = cl.getTalkPower();
|
||||
e.inputMuted = cl.isInputMuted();
|
||||
e.outputMuted = cl.isOutputMuted();
|
||||
e.inputHardware = cl.isInputHardware();
|
||||
e.outputHardware = cl.isOutputHardware();
|
||||
e.away = cl.isAway();
|
||||
e.awayMessage = orEmpty(cl.get("client_away_message"));
|
||||
e.uniqueId = cl.getUniqueIdentifier();
|
||||
@@ -505,6 +523,14 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
pushSelfFlags();
|
||||
}
|
||||
|
||||
/**
|
||||
* TS3's "Local Mic Mute": silences capture without touching the {@code client_input_muted}
|
||||
* status the server (and other clients) see, and without a mute/unmute sound.
|
||||
*/
|
||||
public void setMicLocalMuted(boolean muted) {
|
||||
if (microphone != null) microphone.setLocalMuted(muted);
|
||||
}
|
||||
|
||||
public void setDeafened(boolean deaf) {
|
||||
// Announce muting while we can still be heard, and unmuting once we can again.
|
||||
if (deaf) sound(SoundEvent.SOUND_PLAYBACK_MUTED);
|
||||
@@ -524,15 +550,14 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
self.outputMuted = deaf;
|
||||
});
|
||||
|
||||
// Best-effort: publish input/output muted flags to the server so others see them.
|
||||
// Publish the input/output muted flags to the server so others see them. These
|
||||
// are runtime status, not editable client-database properties, so they go out
|
||||
// through clientupdate (like nickname/away), not clientedit.
|
||||
if (client == null || !connected) return;
|
||||
try {
|
||||
java.util.Map<String, String> props = new java.util.HashMap<>();
|
||||
props.put("client_input_muted", microphone.isMuted() ? "1" : "0");
|
||||
props.put("client_output_muted", playback.isDeafened() ? "1" : "0");
|
||||
client.editClient(selfClientId, props);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
selfUpdate(cmd -> {
|
||||
cmd.add(new CommandSingleParameter("client_input_muted", micMuted ? "1" : "0"));
|
||||
cmd.add(new CommandSingleParameter("client_output_muted", deaf ? "1" : "0"));
|
||||
}, "Mute status update failed");
|
||||
}
|
||||
|
||||
public void joinChannel(int channelId, String password) {
|
||||
@@ -758,6 +783,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
c.talkPower = e.getClientTalkPower();
|
||||
c.inputMuted = e.isClientInputMuted();
|
||||
c.outputMuted = e.isClientOutputMuted();
|
||||
c.inputHardware = e.isClientUsingHardwareInput();
|
||||
c.outputHardware = e.isClientUsingHardwareOutput();
|
||||
c.away = e.isClientAway();
|
||||
c.awayMessage = orEmpty(e.get("client_away_message"));
|
||||
c.uniqueId = orEmpty(e.getUniqueClientIdentifier());
|
||||
@@ -926,6 +953,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (renamed) c.nickname = e.get("client_nickname");
|
||||
if (has(e, "client_input_muted")) c.inputMuted = e.getBoolean("client_input_muted");
|
||||
if (has(e, "client_output_muted")) c.outputMuted = e.getBoolean("client_output_muted");
|
||||
if (has(e, "client_input_hardware")) c.inputHardware = e.getBoolean("client_input_hardware");
|
||||
if (has(e, "client_output_hardware")) c.outputHardware = e.getBoolean("client_output_hardware");
|
||||
if (has(e, "client_away")) {
|
||||
c.away = e.getBoolean("client_away");
|
||||
// Both fields travel together, so an absent message here means "no message"
|
||||
|
||||
Reference in New Issue
Block a user