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:
2026-08-17 08:43:22 +00:00
parent e228dd4ad0
commit 75ae6b89b2
12 changed files with 198 additions and 15 deletions

View File

@@ -170,6 +170,33 @@ public final class Icons {
return themed("OUTPUT_MUTED", size, Icons::paintSpeakerMuted);
}
/** The capture device is unavailable (another tab holds it), as opposed to muted. */
public static ImageIcon micDisabled() {
return micDisabled(SZ);
}
public static ImageIcon micDisabled(int size) {
return themed("HARDWARE_INPUT_MUTED", size, Icons::paintMicDisabled);
}
/** The playback device is unavailable, as opposed to muted/deafened. */
public static ImageIcon speakerDisabled() {
return speakerDisabled(SZ);
}
public static ImageIcon speakerDisabled(int size) {
return themed("HARDWARE_OUTPUT_MUTED", size, Icons::paintSpeakerDisabled);
}
/** TS3's "Local Mic Mute": silenced, but not reported as muted. */
public static ImageIcon micLocalMuted() {
return micLocalMuted(SZ);
}
public static ImageIcon micLocalMuted(int size) {
return themed("INPUT_MUTED_LOCAL", size, Icons::paintMicLocalMuted);
}
/** A channel commander that is not talking. */
public static ImageIcon clientCommander(int size) {
return themed("PLAYER_COMMANDER_OFF", size, g -> paintPerson(g, Theme.IDLE_CLIENT));
@@ -296,6 +323,35 @@ public final class Icons {
g.drawLine(2, 2, 14, 14);
}
/** Same shape as {@link #paintMicMuted}, but grey: unavailable, not muted by choice. */
private static void paintMicDisabled(Graphics2D g) {
g.setColor(Theme.IDLE_CLIENT);
g.fillRoundRect(6, 2, 4, 7, 2, 2);
g.setStroke(new BasicStroke(1.4f));
g.drawArc(4, 6, 8, 6, 200, 140);
g.drawLine(8, 12, 8, 14);
g.setStroke(new BasicStroke(2f));
g.drawLine(2, 2, 14, 14);
}
/** Same shape as {@link #paintSpeakerMuted}, but grey: unavailable, not muted by choice. */
private static void paintSpeakerDisabled(Graphics2D g) {
g.setColor(Theme.IDLE_CLIENT);
g.fillRect(2, 6, 3, 4);
int[] xs = {5, 9, 9, 5};
int[] ys = {6, 3, 13, 10};
g.fillPolygon(xs, ys, 4);
g.setStroke(new BasicStroke(2f));
g.drawLine(2, 2, 14, 14);
}
/** {@link #paintMicMuted}, marked with a small dot: silenced, but not reported as muted. */
private static void paintMicLocalMuted(Graphics2D g) {
paintMicMuted(g);
g.setColor(Theme.ACCENT);
g.fillOval(11, 10, 4, 4);
}
private static void paintConnect(Graphics2D g) {
g.setColor(new Color(0x2E8B57));
g.setStroke(new BasicStroke(2f));