diff --git a/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java b/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java index 2653023..bb6635a 100644 --- a/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java +++ b/src/main/java/com/github/manevolent/ts3j/audio/AudioEncoding.java @@ -3,5 +3,6 @@ package com.github.manevolent.ts3j.audio; public enum AudioEncoding { OPUS, SPEEX, - PCM + PCM, + CELT } diff --git a/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java b/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java index e702718..58bb6c9 100644 --- a/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java +++ b/src/main/java/com/github/manevolent/ts3j/audio/Microphone.java @@ -1,5 +1,7 @@ package com.github.manevolent.ts3j.audio; +import com.github.manevolent.ts3j.enums.CodecType; + public interface Microphone { default boolean isMuted() { return false; @@ -7,9 +9,7 @@ public interface Microphone { boolean isReady(); - default AudioEncoding getEncoding() { - return AudioEncoding.PCM; - } + CodecType getCodec(); byte[] provide(); } 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 85ec51b..12452d9 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 @@ -57,7 +57,7 @@ public class PacketBody0Voice extends PacketBody { if (getRole() == ProtocolRole.SERVER) clientId = buffer.getShort() & 0x0000FFFF; - codecType = CodecType.fromId((int) (buffer.get() & 0xFF)); + codecType = CodecType.fromId(buffer.get() & 0xFF); codecData = new byte[buffer.remaining()]; 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 94387ac..ea76bae 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 @@ -30,6 +30,11 @@ import java.nio.ByteOrder; import java.util.*; import java.util.concurrent.*; +/** + * Server/client common abstract client socket, used to interact with the UDP connection. Provides most of the protocol + * such as fragmentation, compression, encryption, and sliding windows. Handles packet counters and automatically + * handles ACK and PING types for the higher layer. + */ public abstract class AbstractTeamspeakClientSocket extends AbstractTeamspeakSocket implements TeamspeakClientSocket { @@ -257,6 +262,11 @@ public abstract class AbstractTeamspeakClientSocket case COMMAND_LOW: header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, true); break; + case VOICE: + // > 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)body).setPacketId(header.getPacketId()); + break; } packet.setHeader(header); 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 fed05e7..1885ecc 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 @@ -1,10 +1,12 @@ package com.github.manevolent.ts3j.protocol.socket.client; import com.github.manevolent.ts3j.api.Channel; +import com.github.manevolent.ts3j.audio.Microphone; import com.github.manevolent.ts3j.command.*; import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; import com.github.manevolent.ts3j.command.response.AbstractCommandResponse; import com.github.manevolent.ts3j.command.response.CommandResponse; +import com.github.manevolent.ts3j.enums.CodecType; import com.github.manevolent.ts3j.event.TS3Event; import com.github.manevolent.ts3j.event.TS3Listener; import com.github.manevolent.ts3j.identity.LocalIdentity; @@ -12,7 +14,9 @@ import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.header.PacketHeader; +import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice; import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command; +import com.github.manevolent.ts3j.util.HighPrecisionRecurrentTask; import com.github.manevolent.ts3j.util.Ts3Debugging; import java.io.IOException; @@ -41,6 +45,10 @@ public class LocalTeamspeakClientSocket private final Map namedProcessors = new HashMap<>(); private final Object commandSendLock = new Object(); + + private Microphone microphone; + private Thread microphoneThread; + private List listeners = new LinkedList<>(); private int acceptingReturnCode = 0; @@ -147,6 +155,14 @@ public class LocalTeamspeakClientSocket setState(ClientConnectionState.CONNECTING); waitForState(ClientConnectionState.CONNECTED, timeout); + + microphoneThread = new HighPrecisionRecurrentTask( + 1000, + 0.020F, + new MicrophoneTask() + ); + + microphoneThread.start(); } catch (TimeoutException e) { setState(ClientConnectionState.DISCONNECTED); @@ -271,6 +287,52 @@ public class LocalTeamspeakClientSocket return response; } + public Microphone getMicrophone() { + return microphone; + } + + public void setMicrophone(Microphone microphone) { + if (this.microphone != microphone) { + this.microphone = microphone; + + if (microphone == null && microphoneThread != null) { + microphoneThread.interrupt(); + microphoneThread = null; + } + } + } + + private class MicrophoneTask implements Runnable { + @Override + public void run() { + PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); + Microphone microphone; + while (isConnected()) { + // Attempt to get the current microphone instance, saving it in a variable to prevent + // any race conditions by accessing the parent class' this.microphone. + if ((microphone = LocalTeamspeakClientSocket.this.microphone) == null) + continue; + + if (microphone.isMuted()) { + // TODO + // Set a state and send a muted event + } else if (microphone.isReady()) { + voice.setCodecType(microphone.getCodec()); + voice.setCodecData(microphone.provide()); + + try { + writePacket(voice); + } catch (IOException e) { + // All we are really concerned about here is a disconnection event, in which case the loop + // will exit. + } catch (TimeoutException e) { + // Not really possible, as we might expect, voice doesn't have ACKs. + } + } + } + } + } + private class ClientCommandResponse extends AbstractCommandResponse implements CommandProcessor { diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/TeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/TeamspeakClientSocket.java index fbe2e0f..ad142a8 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/TeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/TeamspeakClientSocket.java @@ -47,10 +47,44 @@ public interface TeamspeakClientSocket extends TeamspeakSocket { PacketHandler getHandler(); + /** + * Reads a packet from the remote endpoint. + * + * Note that this call may fight with any other reading threads; it is advisede to use the PacketHandler instead. + * + * This method does not return ACKs, such as: ACK, ACK_LOW, PING, or PONG. These are protocol-level packets that + * are handled automatically by the protocol code. + * + * This method does return INIT1, COMMAND, COMMAND_LOW, and possibly VOICE, and VOICE_WHISPER. + * + * This method does not return fragments or encrypted bodies. This is handled by the underlying protocol + * implementation. + * + * @return Packet. + * @throws IOException + * @throws TimeoutException + */ Packet readPacket() throws IOException, TimeoutException; + /** + * Sends a packet body to the remote endpoint, generating header information as necessary. + * + * @param body Body to send. + * @throws IOException if there is an issue sending the packet (i.e. socket closed) + * @throws TimeoutException if the packet is not acknowledged in time + */ void writePacket(PacketBody body) throws IOException, TimeoutException; + /** + * Writes a packet to the remote endpoint, not generating header information. This must be done by the caller. + * The packet will be checked, split, and encrypted as needed. + * + * This includes packet ID, client ID, type, and flags. + * + * @param packet raw packet information to send. + * @throws IOException + * @throws TimeoutException + */ void writePacket(Packet packet) throws IOException, TimeoutException; } diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 5b2563b..c06f519 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -1,6 +1,8 @@ package com.github.manevolent.ts3j; +import com.github.manevolent.ts3j.audio.Microphone; import com.github.manevolent.ts3j.command.SingleCommand; +import com.github.manevolent.ts3j.enums.CodecType; import com.github.manevolent.ts3j.identity.LocalIdentity; import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; @@ -37,6 +39,23 @@ public class ServerConnectionTest { 10000L ); + client.setMicrophone(new Microphone() { + @Override + public boolean isReady() { + return true; + } + + @Override + public CodecType getCodec() { + return CodecType.OPUS_MUSIC; + } + + @Override + public byte[] provide() { + return new byte[20]; + } + }); + assertEquals(client.getState(), ClientConnectionState.CONNECTED); while (true) {