Give back a speaker's line after 30 s of silence

Every client that ever spoke kept an open playback line and a thread
until it left the server. A speaker silent for 30 s now has both
released, and its next talk burst opens them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 10:47:09 +00:00
parent 0083dc8ad6
commit 6234ec6d97

View File

@@ -20,6 +20,8 @@ final class PerSpeakerPlayout implements Playout {
private static final long FRAME_NANOS = private static final long FRAME_NANOS =
TimeUnit.SECONDS.toNanos(VoiceFormat.FRAME_SIZE) / VoiceFormat.SAMPLE_RATE; TimeUnit.SECONDS.toNanos(VoiceFormat.FRAME_SIZE) / VoiceFormat.SAMPLE_RATE;
/** Silence after which a speaker's line and thread are given back; the next burst reopens them. */
private static final long IDLE_RELEASE_NANOS = TimeUnit.SECONDS.toNanos(30);
private final class Speaker { private final class Speaker {
final VoiceStream stream; final VoiceStream stream;
@@ -32,6 +34,8 @@ final class PerSpeakerPlayout implements Playout {
final byte[] out; final byte[] out;
/** Guarded by this speaker. */ /** Guarded by this speaker. */
ScheduledFuture<?> ticks; ScheduledFuture<?> ticks;
/** Guarded by this speaker; a closed speaker is never ticked again. */
boolean closed;
Speaker(VoiceStream stream) throws Exception { Speaker(VoiceStream stream) throws Exception {
this.stream = stream; this.stream = stream;
@@ -49,6 +53,7 @@ final class PerSpeakerPlayout implements Playout {
void close() { void close() {
synchronized (this) { synchronized (this) {
closed = true;
if (ticks != null) ticks.cancel(false); if (ticks != null) ticks.cancel(false);
} }
worker.shutdownNow(); worker.shutdownNow();
@@ -68,10 +73,12 @@ final class PerSpeakerPlayout implements Playout {
this.outputDevice = outputDevice; this.outputDevice = outputDevice;
this.gain = gain; this.gain = gain;
this.clock = clock; this.clock = clock;
clock.scheduleWithFixedDelay(this::releaseIdle, 1, 1, TimeUnit.SECONDS);
} }
@Override @Override
public void started(VoiceStream stream) { public void started(VoiceStream stream) {
while (true) {
Speaker speaker = speakers.get(stream); Speaker speaker = speakers.get(stream);
if (speaker == null) { if (speaker == null) {
try { try {
@@ -87,11 +94,31 @@ final class PerSpeakerPlayout implements Playout {
} }
} }
synchronized (speaker) { synchronized (speaker) {
if (speaker.closed) {
// Released a moment ago; this burst gets a fresh line.
speakers.remove(stream, speaker);
continue;
}
if (speaker.ticks == null) { if (speaker.ticks == null) {
Speaker s = speaker; Speaker s = speaker;
speaker.ticks = clock.scheduleAtFixedRate( speaker.ticks = clock.scheduleAtFixedRate(
() -> queueTick(s), FRAME_NANOS, FRAME_NANOS, TimeUnit.NANOSECONDS); () -> queueTick(s), FRAME_NANOS, FRAME_NANOS, TimeUnit.NANOSECONDS);
} }
return;
}
}
}
/** Gives back the line and thread of every speaker that has been silent for a while. */
private void releaseIdle() {
long now = System.nanoTime();
for (Speaker speaker : speakers.values()) {
synchronized (speaker) {
if (speaker.ticks != null || now - speaker.stream.lastPacketNanos() < IDLE_RELEASE_NANOS) continue;
speaker.closed = true;
}
speakers.remove(speaker.stream, speaker);
speaker.close();
} }
} }