diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody0Voice.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody0Voice.java index 173241f..5524795 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody0Voice.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody0Voice.java @@ -2,6 +2,8 @@ package com.github.manevolent.ts3j.protocol.packet; import com.github.manevolent.ts3j.enums.CodecType; import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.protocol.header.HeaderFlag; +import com.github.manevolent.ts3j.protocol.header.PacketHeader; import com.github.manevolent.ts3j.util.Ts3Debugging; import java.nio.ByteBuffer; @@ -12,6 +14,13 @@ public class PacketBody0Voice extends PacketBody { private CodecType codecType; private byte[] codecData; + // Unknown at this time (have seen: 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07) + // Set for the first 5 packets of a new voice session + // I believe this is related to tracking opening a new decoder + // Not all new voice sessions will change this flag + // Best to ignore it for now, but it's here if you need it or figure it out + private Byte serverFlag0; + public PacketBody0Voice(ProtocolRole role) { super(PacketBodyType.VOICE, role); } @@ -44,6 +53,12 @@ public class PacketBody0Voice extends PacketBody { return clientId; } + public Byte getServerFlag0() { return serverFlag0; } + + public void setServerFlag0(Byte flag) { + this.serverFlag0 = flag; + } + public void setClientId(int clientId) { if (getRole() != ProtocolRole.SERVER) throw new IllegalStateException("cannot set client ID field on non-server packet"); @@ -51,6 +66,32 @@ public class PacketBody0Voice extends PacketBody { this.clientId = clientId; } + @Override + public void setHeaderValues(PacketHeader header) { + if (serverFlag0 != null) // Ensure padding is signaled + header.setPacketFlag(HeaderFlag.COMPRESSED, true); + } + + @Override + public void read(PacketHeader header, ByteBuffer buffer) { + super.read(header, buffer); + + int padding = 0; + boolean compressed = header.getPacketFlag(HeaderFlag.COMPRESSED); + + if (compressed) + padding ++; + + codecData = new byte[buffer.remaining() - padding]; + buffer.get(codecData); + + // Handle trailing data + if (padding > 0) { + if (compressed) + serverFlag0 = buffer.get(); + } + } + @Override public void read(ByteBuffer buffer) { packetId = buffer.getShort() & 0x0000FFFF; @@ -59,10 +100,6 @@ public class PacketBody0Voice extends PacketBody { clientId = buffer.getShort() & 0x0000FFFF; codecType = CodecType.fromId(buffer.get() & 0xFF); - - codecData = new byte[buffer.remaining()]; - - buffer.get(codecData); } @Override @@ -75,10 +112,17 @@ public class PacketBody0Voice extends PacketBody { buffer.put((byte)(codecType.getIndex() & 0xFF)); buffer.put(codecData); + + if (serverFlag0 != null) + buffer.put(serverFlag0); } @Override public int getSize() { - return 2 + (getRole() == ProtocolRole.SERVER ? 2 : 0) + 1 + codecData.length; + return 2 + + (getRole() == ProtocolRole.SERVER ? 2 : 0) + + 1 + + codecData.length + + (serverFlag0 != null ? 1 : 0); } } 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 2ae4cce..5e70989 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 @@ -16,20 +16,42 @@ public class PacketBody1VoiceWhisper extends PacketBody { private WhisperTarget target; private byte[] codecData; + // Unknown at this time (have seen: 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07) + // Set for the first 5 packets of a new voice session + // I believe this is related to tracking opening a new decoder + // Not all new voice sessions will change this flag + // Best to ignore it for now, but it's here if you need it or figure it out + private Byte serverFlag0; + public PacketBody1VoiceWhisper(ProtocolRole role) { super(PacketBodyType.VOICE_WHISPER, role); } + public Byte getServerFlag0() { return serverFlag0; } + + public void setServerFlag0(Byte flag) { + this.serverFlag0 = flag; + } + @Override public void setHeaderValues(PacketHeader header) { if (target instanceof WhisperTargetGroup) header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, true); else header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, false); + + if (serverFlag0 != null) // Ensure padding is signaled + header.setPacketFlag(HeaderFlag.COMPRESSED, true); } @Override public void read(PacketHeader header, ByteBuffer buffer) { + int padding = 0; + boolean compressed = header.getPacketFlag(HeaderFlag.COMPRESSED); + + if (compressed) + padding ++; + packetId = buffer.getShort() & 0xFFFF; if (getRole() == ProtocolRole.SERVER) @@ -37,7 +59,6 @@ public class PacketBody1VoiceWhisper extends PacketBody { codecType = CodecType.fromId((int) (buffer.get() & 0xFF)); - if (getRole() == ProtocolRole.CLIENT) { if (header.getPacketFlag(HeaderFlag.NEW_PROTOCOL)) { setTarget(new WhisperTargetGroup()); @@ -50,14 +71,20 @@ public class PacketBody1VoiceWhisper extends PacketBody { target.read(buffer); - read(buffer); + codecData = new byte[buffer.remaining() - padding]; + buffer.get(codecData); + + // Handle trailing data + if (padding > 0) { + if (compressed) + serverFlag0 = buffer.get(); + } } // We implemented the above so this is unneccessary. @Override public void read(ByteBuffer buffer) { - codecData = new byte[buffer.remaining()]; - buffer.get(codecData); + throw new UnsupportedOperationException(); } @Override @@ -73,15 +100,18 @@ public class PacketBody1VoiceWhisper extends PacketBody { target.write(buffer); buffer.put(codecData); + + if (serverFlag0 != null) + buffer.put(serverFlag0); } @Override public int getSize() { switch (getRole()) { case CLIENT: - return 2 + 1 + target.getSize() + codecData.length; + return 2 + 1 + target.getSize() + codecData.length + (serverFlag0 != null ? 1 : 0); case SERVER: - return 2 + 2 + 1 + codecData.length; + return 2 + 2 + 1 + codecData.length + (serverFlag0 != null ? 1 : 0); default: throw new IllegalArgumentException("unknown role"); } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerFull.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerFull.java index 1833f0d..9e1e2be 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerFull.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerFull.java @@ -54,13 +54,14 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler { } break; + case VOICE: Consumer handler = getClient().getVoiceHandler(); - if (handler != null) handler.accept((PacketBody0Voice) packet.getBody()); + if (handler != null) handler.accept((PacketBody0Voice)packet.getBody()); break; case VOICE_WHISPER: Consumer whisperConsumer = getClient().getWhisperHandler(); - if (whisperConsumer != null) whisperConsumer.accept((PacketBody1VoiceWhisper) packet.getBody()); + if (whisperConsumer != null) whisperConsumer.accept((PacketBody1VoiceWhisper)packet.getBody()); break; } } 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 c5b09e7..455729c 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 @@ -515,6 +515,8 @@ public class LocalTeamspeakClientSocket private class MicrophoneTask implements Runnable { private boolean wasSendingAudio = false; + private byte sessionId = 1; + private int flaggedPackets = 5; @Override public void run() { @@ -531,14 +533,19 @@ public class LocalTeamspeakClientSocket // TODO // Set a state and send a muted event } else if (microphone.isReady()) { + byte[] data = microphone.provide(); + if (data.length <= 0) return; + PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); voice.setCodecType(microphone.getCodec()); - voice.setCodecData(microphone.provide()); + voice.setCodecData(data); + if (flaggedPackets > 0) voice.setServerFlag0(sessionId); try { writePacket(voice); + flaggedPackets --; wasSendingAudio = true; } catch (Exception e) { // All we are really concerned about here is a disconnection event, in which case the loop @@ -554,6 +561,13 @@ public class LocalTeamspeakClientSocket 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 @@ -697,12 +711,11 @@ public class LocalTeamspeakClientSocket switch (getState()) { case RETRIEVING_DATA: case CONNECTED: - Command banCommand = new SingleCommand("clientupdate", ProtocolRole.CLIENT); - - banCommand.add(new CommandSingleParameter("client_nickname", nickname)); + Command command = new SingleCommand("clientupdate", ProtocolRole.CLIENT); + command.add(new CommandSingleParameter("client_nickname", nickname)); try { - executeCommand(banCommand).complete(); + executeCommand(command).complete(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java index 9483108..ffeb1ac 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java @@ -5,10 +5,8 @@ import Punisher.NaCl.Internal.Ed25519Ref10.GroupElementP3; import Punisher.NaCl.Internal.Ed25519Ref10.GroupOperations; import Punisher.NaCl.Internal.Ed25519Ref10.ScalarOperations; import Punisher.NaCl.Internal.Sha512; -import com.github.manevolent.ts3j.api.Message; import com.github.manevolent.ts3j.identity.*; import com.github.manevolent.ts3j.license.License; -import com.github.manevolent.ts3j.util.Pair; import org.bouncycastle.asn1.*; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.params.ECDomainParameters; @@ -25,7 +23,6 @@ import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.*; -import java.util.Base64; import java.util.List; public final class Ts3Crypt { diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 907d5cf..ca51b07 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -5,6 +5,7 @@ import com.github.manevolent.ts3j.audio.Microphone; import com.github.manevolent.ts3j.enums.CodecType; import com.github.manevolent.ts3j.event.*; import com.github.manevolent.ts3j.identity.LocalIdentity; +import com.github.manevolent.ts3j.protocol.Packet; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; @@ -22,14 +23,18 @@ public class ServerConnectionTest { LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket(); - Ts3Debugging.setEnabled(true); + Ts3Debugging.setEnabled(false); - LocalIdentity identity = LocalIdentity.generateNew(9); + LocalIdentity identity = LocalIdentity.generateNew(10); client.setIdentity(identity); client.setNickname("ts3j issue 2"); client.setHWID("TestTestTest"); + client.setVoiceHandler(packet -> + System.out.println(packet.getServerFlag0()) + ); + while (true) { try { client.connect(