diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody1VoiceWhisper.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody1VoiceWhisper.java index 5e70989..4c5a5d1 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody1VoiceWhisper.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody1VoiceWhisper.java @@ -239,13 +239,13 @@ public class PacketBody1VoiceWhisper extends PacketBody { channelIds[n] = (int) (buffer.getLong()); clientIds = new int[clientIdCount]; - for (int n = 0; n < channelIdCount; n ++) + for (int n = 0; n < clientIdCount; n ++) clientIds[n] = (buffer.getShort() & 0xFFFF); } @Override public int getSize() { - return 2 + 2 + + return 1 + 1 + (channelIds == null ? 0 : channelIds.length * 8) + (clientIds == null ? 0 : clientIds.length * 2); } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java index 35d9fb7..68ff69d 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java @@ -478,6 +478,8 @@ public abstract class AbstractTeamspeakClientSocket // > X is a ushort in H2N order of an own audio packet counter // it seems it can be the same as the packet counter so we will let the packethandler do it. ((PacketBody0Voice)packet.getBody()).setPacketId(header.getPacketId()); + } else if (header.getType() == PacketBodyType.VOICE_WHISPER) { + ((PacketBody1VoiceWhisper)packet.getBody()).setPacketId(header.getPacketId()); } // Flush to a buffer diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java index 00656c7..4de4811 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java @@ -16,7 +16,10 @@ import com.github.manevolent.ts3j.identity.Uid; import com.github.manevolent.ts3j.protocol.*; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.header.PacketHeader; +import com.github.manevolent.ts3j.enums.CodecType; +import com.github.manevolent.ts3j.protocol.packet.PacketBody; import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice; +import com.github.manevolent.ts3j.protocol.packet.PacketBody1VoiceWhisper; import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command; import com.github.manevolent.ts3j.protocol.packet.PacketBodyType; import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler; @@ -54,6 +57,7 @@ public class LocalTeamspeakClientSocket private volatile InetSocketAddress remote; private Microphone microphone; + private volatile PacketBody1VoiceWhisper.WhisperTarget whisperTarget; private HighPrecisionTimer microphoneThread; /** Voice packets still to be sent again, soonest first; see {@link Microphone#getRedundancyDelay}. */ private final Queue voiceCopies = @@ -286,7 +290,8 @@ public class LocalTeamspeakClientSocket socket.send(packet.getDatagram()); long copyDelay = voiceCopyDelayNanos; - if (copyDelay > 0 && packet.getHeader().getType() == PacketBodyType.VOICE) { + PacketBodyType type = packet.getHeader().getType(); + if (copyDelay > 0 && (type == PacketBodyType.VOICE || type == PacketBodyType.VOICE_WHISPER)) { // Aim half a tick early, so each copy goes out on the tick nearest its time. long sent = System.nanoTime() - TICK_MS * 1_000_000L / 2; for (int i = 1, n = voiceCopyCount; i <= n; i++) @@ -585,6 +590,19 @@ public class LocalTeamspeakClientSocket return microphone; } + /** Where the microphone's voice goes: null for our channel, else whisper packets to the target. */ + public PacketBody1VoiceWhisper.WhisperTarget getWhisperTarget() { + return whisperTarget; + } + + /** + * Sends the microphone's voice to {@code target} as whispers, or to our channel again with null. + * A voice session under way ends on its old route first. + */ + public void setWhisperTarget(PacketBody1VoiceWhisper.WhisperTarget target) { + this.whisperTarget = target; + } + public void setMicrophone(Microphone microphone) { if (this.microphone != microphone) { this.microphone = microphone; @@ -664,6 +682,8 @@ public class LocalTeamspeakClientSocket private class MicrophoneTask implements Runnable { private boolean wasSendingAudio = false; + /** Where the voice session under way goes; null for the channel. */ + private PacketBody1VoiceWhisper.WhisperTarget sendingTarget; private byte sessionId = 1; private int flaggedPackets = 5; @@ -690,6 +710,12 @@ public class LocalTeamspeakClientSocket voiceCopyCount = copies; voiceCopyDelayNanos = copyDelay > 0 && copies > 0 ? copyDelay * 1_000_000L : 0; + PacketBody1VoiceWhisper.WhisperTarget target = whisperTarget; + if (wasSendingAudio && target != sendingTarget) { + // The listeners of the old route hear the end of what they were hearing. + if (!sendEnd(microphone)) return; + } + if (microphone.isMuted()) { // TODO // Set a state and send a muted event @@ -700,17 +726,13 @@ public class LocalTeamspeakClientSocket data = microphone.provide(); if (data.length <= 0) break; - PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); - - voice.setCodecType(microphone.getCodec()); - voice.setCodecData(data); - if (flaggedPackets > 0) voice.setServerFlag0(sessionId); - try { - writePacket(voice); + writePacket(voicePacket(target, microphone.getCodec(), data, + flaggedPackets > 0 && target == null ? sessionId : null)); flaggedPackets--; wasSendingAudio = true; + sendingTarget = target; } catch (Exception e) { // All we are really concerned about here is a disconnection event, in which case the loop // will exit. @@ -719,29 +741,47 @@ public class LocalTeamspeakClientSocket } } while (live && microphone.isReady()); } else if (wasSendingAudio) { - PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); - - voice.setCodecType(microphone.getCodec()); - voice.setCodecData(new byte[0]); - - try { - writePacket(voice); - - // reset decoder flag - flaggedPackets = 5; - sessionId++; - - if (sessionId >= 8) - sessionId = 1; - - wasSendingAudio = false; - } catch (Exception e) { - // All we are really concerned about here is a disconnection event, in which case the loop - // will exit. - getExceptionHandler().accept(e); - } + sendEnd(microphone); } } + + /** Ends the voice session on the route it went, which an empty packet does; false once disconnected. */ + private boolean sendEnd(Microphone microphone) { + try { + writePacket(voicePacket(sendingTarget, microphone.getCodec(), new byte[0], null)); + + // reset decoder flag + flaggedPackets = 5; + sessionId++; + + if (sessionId >= 8) + sessionId = 1; + + wasSendingAudio = false; + return true; + } catch (Exception e) { + // All we are really concerned about here is a disconnection event, in which case the loop + // will exit. + getExceptionHandler().accept(e); + return false; + } + } + + private PacketBody voicePacket(PacketBody1VoiceWhisper.WhisperTarget target, CodecType codec, byte[] data, + Byte flag) { + if (target == null) { + PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); + voice.setCodecType(codec); + voice.setCodecData(data); + if (flag != null) voice.setServerFlag0(flag); + return voice; + } + PacketBody1VoiceWhisper whisper = new PacketBody1VoiceWhisper(getRole().getOut()); + whisper.setCodecType(codec); + whisper.setTarget(target); + whisper.setCodecData(data); + return whisper; + } } private class ClientCommandResponse diff --git a/src/test/java/com/github/manevolent/ts3j/WhisperPacketTest.java b/src/test/java/com/github/manevolent/ts3j/WhisperPacketTest.java new file mode 100644 index 0000000..53f6e0e --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/WhisperPacketTest.java @@ -0,0 +1,41 @@ +package com.github.manevolent.ts3j; + +import com.github.manevolent.ts3j.enums.CodecType; +import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader; +import com.github.manevolent.ts3j.protocol.packet.PacketBody1VoiceWhisper; +import junit.framework.TestCase; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** A whisper a client sends: codec, then the channel and client counts and ids, then the voice. */ +public class WhisperPacketTest extends TestCase { + + public void testClientWhisperRoundTrip() { + PacketBody1VoiceWhisper.WhisperTargetMultiple target = new PacketBody1VoiceWhisper.WhisperTargetMultiple(); + target.setChannelIds(new int[]{7, 9}); + target.setClientIds(new int[]{3}); + PacketBody1VoiceWhisper sent = new PacketBody1VoiceWhisper(ProtocolRole.CLIENT); + sent.setPacketId(42); + sent.setCodecType(CodecType.OPUS_VOICE); + sent.setTarget(target); + sent.setCodecData(new byte[]{1, 2, 3}); + + ByteBuffer buffer = ByteBuffer.allocate(sent.getSize()); + sent.write(buffer); + assertEquals("the size covers exactly what is written", buffer.capacity(), buffer.position()); + assertEquals(2 + 1 + 1 + 1 + 2 * 8 + 2 + 3, buffer.capacity()); + + buffer.flip(); + PacketBody1VoiceWhisper read = new PacketBody1VoiceWhisper(ProtocolRole.CLIENT); + read.read(new ClientPacketHeader(), buffer); + PacketBody1VoiceWhisper.WhisperTargetMultiple readTarget = + (PacketBody1VoiceWhisper.WhisperTargetMultiple) read.getTarget(); + assertEquals(42, read.getPacketId()); + assertEquals(CodecType.OPUS_VOICE, read.getCodecType()); + assertTrue(Arrays.equals(new int[]{7, 9}, readTarget.getChannelIds())); + assertTrue(Arrays.equals(new int[]{3}, readTarget.getClientIds())); + assertTrue(Arrays.equals(new byte[]{1, 2, 3}, read.getCodecData())); + } +}