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:
@@ -51,6 +51,7 @@ public final class DesktopVoiceInput implements VoiceInput {
|
||||
|
||||
private final ConcurrentLinkedQueue<byte[]> queue = new ConcurrentLinkedQueue<>();
|
||||
private final AtomicBoolean muted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean localMuted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean transmitting = new AtomicBoolean(false);
|
||||
private final AtomicBoolean pttDown = new AtomicBoolean(false);
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
@@ -239,6 +240,16 @@ public final class DesktopVoiceInput implements VoiceInput {
|
||||
this.muted.set(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocalMuted(boolean m) {
|
||||
this.localMuted.set(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocalMuted() {
|
||||
return localMuted.get();
|
||||
}
|
||||
|
||||
// ---- lifecycle ----
|
||||
|
||||
public synchronized void start() {
|
||||
@@ -405,7 +416,7 @@ public final class DesktopVoiceInput implements VoiceInput {
|
||||
}
|
||||
|
||||
private boolean decideGate(double db, float[] pcm) {
|
||||
if (muted.get()) {
|
||||
if (muted.get() || localMuted.get()) {
|
||||
hangover = 0;
|
||||
detectMutedSpeech(db);
|
||||
return false;
|
||||
|
||||
@@ -10,6 +10,8 @@ import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
@@ -24,6 +26,16 @@ public final class DesktopVoiceOutput implements VoiceOutput {
|
||||
/** Longest Opus frame (120 ms @ 48 kHz) a packet may decode to, per channel. */
|
||||
private static final int MAX_FRAME = 5760;
|
||||
|
||||
/**
|
||||
* A speaker is only meant to stop when the empty voice packet marking the end of a
|
||||
* talk burst arrives — but that packet is UDP too, and a lost one otherwise leaves
|
||||
* the talking indicator stuck until the speaker's next burst. Opus frames are 20 ms
|
||||
* apart while someone is actually talking, so a gap several times that long, with no
|
||||
* packet of either kind, is unambiguous: force the indicator off rather than trust
|
||||
* the one packet that could go missing.
|
||||
*/
|
||||
private static final long TALK_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(200);
|
||||
|
||||
/** One speaker's decode + playback pipeline. */
|
||||
private final class ClientStream {
|
||||
final int clientId;
|
||||
@@ -81,8 +93,32 @@ public final class DesktopVoiceOutput implements VoiceOutput {
|
||||
/** Notified (clientId, talking) on the EDT-agnostic worker thread when a speaker starts/stops. */
|
||||
private volatile BiConsumer<Integer, Boolean> talkListener;
|
||||
|
||||
/** Catches a talk burst whose end packet never arrived; see {@link #TALK_TIMEOUT_NANOS}. */
|
||||
private final ScheduledExecutorService watchdog = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "ts3j-talk-watchdog");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
public DesktopVoiceOutput(String outputDevice) {
|
||||
this.outputDevice = outputDevice;
|
||||
watchdog.scheduleWithFixedDelay(this::checkTalkTimeouts, 50, 50, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void checkTalkTimeouts() {
|
||||
long now = System.nanoTime();
|
||||
for (ClientStream s : streams.values()) {
|
||||
if (s.talking && now - s.lastPacketNanos > TALK_TIMEOUT_NANOS) {
|
||||
s.worker.submit(() -> {
|
||||
try {
|
||||
s.line.drain();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (s.decoder != null) s.decoder.reset();
|
||||
markTalking(s, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTalkListener(BiConsumer<Integer, Boolean> l) {
|
||||
@@ -226,6 +262,7 @@ public final class DesktopVoiceOutput implements VoiceOutput {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
watchdog.shutdownNow();
|
||||
for (ClientStream s : streams.values()) {
|
||||
s.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user