Merge remote-tracking branch 'origin/master'

This commit is contained in:
Manevolent
2019-02-05 08:51:54 -05:00
4 changed files with 84 additions and 32 deletions
+2
View File
@@ -4,6 +4,8 @@ Discord: https://discord.gg/4q9jpCa
TS3J is an open-source implementation of the reverse-engineered Teamspeak3 full server/client protocol, as an adaptation of Splamy's C# TS3Client source code. You can find that here: https://github.com/Splamy/TS3AudioBot/. TS3J is an open-source implementation of the reverse-engineered Teamspeak3 full server/client protocol, as an adaptation of Splamy's C# TS3Client source code. You can find that here: https://github.com/Splamy/TS3AudioBot/.
A standalone proof-of-concept was created to wrap ts3j: https://github.com/Manevolent/ts3j-musicbot
(Shameless plug) If you want to deliver a great music bot, check out ffmpeg4j, a wrapper around the native C library FFmpeg, to get insanely low CPU usage on your YouTube video downloads (or streams if you don't want to hit disk), and audio playback: (Shameless plug) If you want to deliver a great music bot, check out ffmpeg4j, a wrapper around the native C library FFmpeg, to get insanely low CPU usage on your YouTube video downloads (or streams if you don't want to hit disk), and audio playback:
https://github.com/Manevolent/ffmpeg4j https://github.com/Manevolent/ffmpeg4j
@@ -4,6 +4,9 @@ import com.github.manevolent.ffmpeg4j.*;
import com.github.manevolent.ffmpeg4j.filter.audio.FFmpegAudioResampleFilter; import com.github.manevolent.ffmpeg4j.filter.audio.FFmpegAudioResampleFilter;
import com.github.manevolent.ffmpeg4j.source.FFmpegAudioSourceSubstream; import com.github.manevolent.ffmpeg4j.source.FFmpegAudioSourceSubstream;
import com.github.manevolent.ffmpeg4j.stream.source.FFmpegSourceStream; import com.github.manevolent.ffmpeg4j.stream.source.FFmpegSourceStream;
import com.github.manevolent.ts3j.event.ClientUpdatedEvent;
import com.github.manevolent.ts3j.event.DisconnectedEvent;
import com.github.manevolent.ts3j.event.TS3Listener;
import com.github.manevolent.ts3j.identity.LocalIdentity; import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
import com.github.manevolent.ts3j.util.Ts3Debugging; import com.github.manevolent.ts3j.util.Ts3Debugging;
@@ -51,6 +54,13 @@ public class MusicPlayer {
client.setNickname(MusicPlayer.class.getSimpleName()); client.setNickname(MusicPlayer.class.getSimpleName());
client.setHWID("TestTestTest"); client.setHWID("TestTestTest");
client.addListener(new TS3Listener() {
@Override
public void onDisconnected(DisconnectedEvent e) {
System.exit(-1);
}
});
// Create a sink // Create a sink
TeamspeakFastMixerSink sink = new TeamspeakFastMixerSink( TeamspeakFastMixerSink sink = new TeamspeakFastMixerSink(
AUDIO_FORMAT, AUDIO_FORMAT,
@@ -149,10 +149,12 @@ public abstract class AbstractTeamspeakClientSocket
protected void start() { protected void start() {
Thread networkThread = new Thread(networkReader); Thread networkThread = new Thread(networkReader);
networkThread.setName("TS3J/NetworkReader/" + networkThread.getId());
networkThread.setDaemon(true); networkThread.setDaemon(true);
networkThread.start(); networkThread.start();
Thread handlerThread = new Thread(networkHandler); Thread handlerThread = new Thread(networkHandler);
handlerThread.setName("TS3J/NetworkHandler/" + handlerThread.getId());
handlerThread.setDaemon(true); handlerThread.setDaemon(true);
handlerThread.start(); handlerThread.start();
} }
@@ -922,19 +924,21 @@ public abstract class AbstractTeamspeakClientSocket
private class NetworkHandler implements Runnable { private class NetworkHandler implements Runnable {
@Override @Override
public void run() { public void run() {
while (isReading()) { try {
try { while (isReading()) {
Packet packet = readQueue.take(); try {
Packet packet = readQueue.take();
// separate these so we getHandler at proper runtime instant rather than get it and let it change // separate these so we getHandler at proper runtime instant rather than get it and let it change
// on us // on us
getHandler().handlePacket(packet); getHandler().handlePacket(packet);
} catch (Throwable e) { } catch (InterruptedException e) {
if (e instanceof InterruptedException) continue; break;
}
getExceptionHandler().accept(e);
} }
} catch (Throwable e) {
getExceptionHandler().accept(e);
} }
} }
} }
@@ -55,10 +55,15 @@ public class LocalTeamspeakClientSocket
private List<TS3Listener> listeners = new LinkedList<>(); private List<TS3Listener> listeners = new LinkedList<>();
private int acceptingReturnCode = Integer.MAX_VALUE;
private Integer serverId = null; private Integer serverId = null;
/**
* The internal command buffer
*
* This buffer ascends through specific command codes, which are calculated using:
*
* get
*/
private Map<Integer, ClientCommandResponse> awaitingCommands = new ConcurrentHashMap<>(); private Map<Integer, ClientCommandResponse> awaitingCommands = new ConcurrentHashMap<>();
public LocalTeamspeakClientSocket() { public LocalTeamspeakClientSocket() {
@@ -292,8 +297,6 @@ public class LocalTeamspeakClientSocket
} }
awaitingCommands.clear(); awaitingCommands.clear();
calculateAcceptingReturnCode();
} }
serverId = null; serverId = null;
@@ -372,10 +375,10 @@ public class LocalTeamspeakClientSocket
synchronized (commandSendLock) { synchronized (commandSendLock) {
for (Integer id : new ArrayList<>(awaitingCommands.keySet())) { for (Integer id : new ArrayList<>(awaitingCommands.keySet())) {
ClientCommandResponse response = awaitingCommands.get(id); ClientCommandResponse response = awaitingCommands.get(id);
response.completeFailure(new IOException("client disconnected")); response.completeFailure(new IOException("client reconnecting"));
} }
awaitingCommands.clear(); awaitingCommands.clear();
calculateAcceptingReturnCode();
} }
setOption("client.hostname", remote.getHostString()); setOption("client.hostname", remote.getHostString());
@@ -436,7 +439,7 @@ public class LocalTeamspeakClientSocket
CommandProcessor awaitingCommandProcessor; CommandProcessor awaitingCommandProcessor;
synchronized (commandSendLock) { synchronized (commandSendLock) {
awaitingCommandProcessor = awaitingCommands.get(acceptingReturnCode); awaitingCommandProcessor = awaitingCommands.get(getAwaitingCommandCode());
} }
if (awaitingCommandProcessor != null) if (awaitingCommandProcessor != null)
@@ -468,14 +471,14 @@ public class LocalTeamspeakClientSocket
else if (getState() == ClientConnectionState.CONNECTING) else if (getState() == ClientConnectionState.CONNECTING)
throw new IOException("connecting"); throw new IOException("connecting");
int maxReturnCode = calculateAcceptingReturnCode(); // Calculate the next command send code we should use
if (maxReturnCode == Integer.MAX_VALUE) maxReturnCode = 0; // This is so we can sequentially send commands and expect responses in order from the server
else maxReturnCode = maxReturnCode + 1; int sendCode = getNextCommandCode();
int returnCode = maxReturnCode; if (sendCode >= Integer.MAX_VALUE) throw new ArrayIndexOutOfBoundsException("sendCode");
command.add(new CommandSingleParameter("return_code", Integer.toString(returnCode))); command.add(new CommandSingleParameter("return_code", Integer.toString(sendCode)));
awaitingCommands.put(returnCode, response = new ClientCommandResponse<>(returnCode, multiCommands -> { awaitingCommands.put(sendCode, response = new ClientCommandResponse<>(sendCode, multiCommands -> {
List<SingleCommand> commands = new LinkedList<>(); List<SingleCommand> commands = new LinkedList<>();
for (MultiCommand multiCommand : multiCommands) commands.addAll(multiCommand.simplify()); for (MultiCommand multiCommand : multiCommands) commands.addAll(multiCommand.simplify());
@@ -484,10 +487,14 @@ public class LocalTeamspeakClientSocket
.collect(Collectors.toCollection(LinkedList::new)); .collect(Collectors.toCollection(LinkedList::new));
}, command)); }, command));
acceptingReturnCode = calculateAcceptingReturnCode(); // Ensure that the next command we expect to receive a response for is sent to the network
// but, only send that command if the accepting receive code matches the code we picked above
if (acceptingReturnCode == returnCode) // this ensures we don't double-send a command
sendNextCommand(); int nextCommandCode = getAwaitingCommandCode();
if (nextCommandCode < 0) // This should never happen
throw new IllegalStateException("nextCommandCode <= 0: " + nextCommandCode);
else if (nextCommandCode == sendCode)
sendCommand(nextCommandCode);
} }
return response; return response;
@@ -508,17 +515,42 @@ public class LocalTeamspeakClientSocket
} }
} }
private int calculateAcceptingReturnCode() { /**
* Gets the next command return_code we will create and buffer for sending.
*
* @return Next command send code
*/
private int getNextCommandCode() {
int highest = 0;
for (Integer integer : awaitingCommands.keySet()) highest = Math.max(highest, integer);
return highest + 1;
}
/**
* Gets the next return_code we expect to receive from the server.
*
* @return command receive code
*/
private int getAwaitingCommandCode() {
if (awaitingCommands.size() <= 0) return -1;
int lowest = Integer.MAX_VALUE; int lowest = Integer.MAX_VALUE;
for (Integer integer : awaitingCommands.keySet()) lowest = Math.min(lowest, integer); for (Integer integer : awaitingCommands.keySet()) lowest = Math.min(lowest, integer);
return lowest; return lowest;
} }
private void sendCommand(int code)
throws IOException, TimeoutException {
synchronized (commandSendLock) {
ClientCommandResponse currentResponse = awaitingCommands.get(code);
if (currentResponse != null) currentResponse.ensureSent();
}
}
private void sendNextCommand() private void sendNextCommand()
throws IOException, TimeoutException { throws IOException, TimeoutException {
synchronized (commandSendLock) { synchronized (commandSendLock) {
ClientCommandResponse currentResponse = awaitingCommands.get(acceptingReturnCode); ClientCommandResponse currentResponse = awaitingCommands.get(getAwaitingCommandCode());
if (currentResponse != null) currentResponse.ensureSent(); if (currentResponse != null) currentResponse.ensureSent();
} }
} }
@@ -649,9 +681,13 @@ public class LocalTeamspeakClientSocket
} catch (Throwable e) { } catch (Throwable e) {
completeFailure(e); completeFailure(e);
} finally { } finally {
synchronized (commandSendLock) { synchronized (commandSendLock) { // (this is likely already locked)
// Remove the current "awaiting" command. This is removing this instance, effectively.
awaitingCommands.remove(getReturnCode()); awaitingCommands.remove(getReturnCode());
acceptingReturnCode = calculateAcceptingReturnCode();
// Ensure we send the next command
// This is essential to keep the command chain flowing when there are commands buffered in the
// command send (awaitingCommands) queue
sendNextCommand(); sendNextCommand();
} }
} }