When the COMPRESSED flag is set by the official Teamspeak Gbmh client on a VOICE or VOICE_WHISPER packet, which is done for the first 5 packets of any decoder session, the packet is always 1 byte longer. More importantly, the final byte is static for those 5 packets.

You have to trim that last byte off, otherwise your audio will be corrupted.  TS3J now does this automatically and stores it in ServerFlag0 for you, as a nullable Byte object. This way you can handle it if you need to. This has also been factored into the output/write stage.

Digging into theory, I believe this ID has something to do with the opening of a new decoder to ensure that the decoder opens cleanly each time. You do NOT have to do anything to your decoder to reset states, etc., from what I can tell. In other words, COMPRESSED being on shouldn't change the behavior of your decoding.
This commit is contained in:
Manevolent
2018-10-13 16:03:51 -04:00
parent d9ce7f21a3
commit d97b95f021
3 changed files with 91 additions and 12 deletions
@@ -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);
}
}
@@ -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");
}
@@ -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,7 +23,7 @@ public class ServerConnectionTest {
LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
Ts3Debugging.setEnabled(true);
Ts3Debugging.setEnabled(false);
LocalIdentity identity = LocalIdentity.generateNew(10);
@@ -30,6 +31,10 @@ public class ServerConnectionTest {
client.setNickname("ts3j issue 2");
client.setHWID("TestTestTest");
client.setVoiceHandler(packet ->
System.out.println(((PacketBody0Voice)packet.getBody()).getServerFlag0())
);
while (true) {
try {
client.connect(