Added audio sender test and disconnect reasons
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.github.manevolent.ts3j</groupId>
|
||||||
|
<artifactId>audio-example</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>tomp2p.net</id>
|
||||||
|
<name>tomp2p</name>
|
||||||
|
<url>http://tomp2p.net/dev/mvn/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Opus playback -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.tomp2p</groupId>
|
||||||
|
<artifactId>opus-wrapper</artifactId>
|
||||||
|
<version>1.4</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Careful since this dependency may conflict -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.sun.jna</groupId>
|
||||||
|
<artifactId>jna</artifactId>
|
||||||
|
<version>3.0.9</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- if you've loaded the entire repo into your IDE, and ran the maven "install" step this should work -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.manevolent</groupId>
|
||||||
|
<artifactId>ts3j</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.identity.LocalIdentity;
|
||||||
|
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
|
||||||
|
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
|
||||||
|
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||||
|
|
||||||
|
import static com.github.manevolent.ts3j.examples.audio.TeamspeakFastMixerSink.AUDIO_FORMAT;
|
||||||
|
|
||||||
|
public class AudioSender {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Ts3Debugging.setEnabled(false);
|
||||||
|
|
||||||
|
LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
|
||||||
|
|
||||||
|
LocalIdentity identity = LocalIdentity.generateNew(10);
|
||||||
|
|
||||||
|
client.setIdentity(identity);
|
||||||
|
client.setNickname(AudioSender.class.getSimpleName());
|
||||||
|
client.setHWID("TestTestTest");
|
||||||
|
|
||||||
|
TeamspeakFastMixerSink sink = new TeamspeakFastMixerSink(
|
||||||
|
AUDIO_FORMAT,
|
||||||
|
(int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels() * 4 /*2=16bit float*/,
|
||||||
|
new OpusParameters(
|
||||||
|
20,
|
||||||
|
96000, // 96kbps
|
||||||
|
10, // max complexity
|
||||||
|
0, // 0 expected packet loss
|
||||||
|
false, // no VBR
|
||||||
|
false, // no FEC
|
||||||
|
true // OPUS MUSIC - channel doesn't have to be Opus Music ;)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
client.setMicrophone(sink);
|
||||||
|
|
||||||
|
// -1 to 1 (signed) positions of samples
|
||||||
|
float[] thirtySecondsOfAudio = new float[(int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels()];
|
||||||
|
|
||||||
|
// Generate a sine wave
|
||||||
|
for (int i = 0; i < thirtySecondsOfAudio.length; i ++) {
|
||||||
|
thirtySecondsOfAudio[i] = (float) Math.sin(i / 60f);
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.start();
|
||||||
|
|
||||||
|
// Write into the sink
|
||||||
|
sink.write(thirtySecondsOfAudio, thirtySecondsOfAudio.length);
|
||||||
|
|
||||||
|
client.connect(
|
||||||
|
"your.teamspeak.server",
|
||||||
|
10000L
|
||||||
|
);
|
||||||
|
|
||||||
|
sink.drain(); // this will block until the sink is stopped
|
||||||
|
|
||||||
|
System.err.println("Encoder time taken = " + sink.getNanotime() + " ns");
|
||||||
|
System.err.println("Audio position = " + sink.getPosition() + " samples sank");
|
||||||
|
System.err.println("Encoder position = " + sink.getEncoderPosition() + " samples encoded");
|
||||||
|
System.err.println("Network position = " + sink.getNetworkPosition() + " bytes sent");
|
||||||
|
System.err.println("Packets encoded = " + sink.getPacketsEncoded() + " packets encoded");
|
||||||
|
System.err.println("Packets sent = " + sink.getPacketsSent() + " packets sent");
|
||||||
|
System.err.println("Overflow events = " + sink.getOverflows());
|
||||||
|
|
||||||
|
// (resource starvation; can cause lost packets)
|
||||||
|
System.err.println("Underflow events = " + sink.getUnderflows());
|
||||||
|
|
||||||
|
client.disconnect("BYE");
|
||||||
|
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
import com.sun.jna.ptr.PointerByReference;
|
||||||
|
import net.tomp2p.opuswrapper.Opus;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
|
||||||
|
public class OpusDecoder implements AutoCloseable {
|
||||||
|
private final PointerByReference decoder;
|
||||||
|
private final int sampleRate, frameSize, channels;
|
||||||
|
|
||||||
|
public OpusDecoder(int sampleRate, int frameSize, int channels) {
|
||||||
|
this.channels = channels;
|
||||||
|
this.sampleRate = sampleRate;
|
||||||
|
this.frameSize = frameSize;
|
||||||
|
|
||||||
|
IntBuffer errorBuffer = IntBuffer.allocate(1);
|
||||||
|
|
||||||
|
decoder = Opus.INSTANCE.opus_decoder_create(
|
||||||
|
sampleRate,
|
||||||
|
channels,
|
||||||
|
errorBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
OpusUtil.checkError("opus_decoder_create", errorBuffer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int reset() {
|
||||||
|
return OpusUtil.checkError(
|
||||||
|
"opus_decoder_ctl/OPUS_RESET_STATE",
|
||||||
|
Opus.INSTANCE.opus_decoder_ctl(decoder, Opus.OPUS_RESET_STATE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int setDecoderValue(int field, Object... values) {
|
||||||
|
return OpusUtil.checkError(
|
||||||
|
"opus_decoder_ctl/" + field,
|
||||||
|
Opus.INSTANCE.opus_decoder_ctl(decoder, field, values)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int decodePLC(float[] floats) {
|
||||||
|
return decode(null, floats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int decode(byte[] packet, float[] floats) {
|
||||||
|
if (floats == null)
|
||||||
|
throw new NullPointerException("floats");
|
||||||
|
else if (floats.length < frameSize * channels)
|
||||||
|
throw new IllegalArgumentException(floats.length + " < " + frameSize * channels);
|
||||||
|
|
||||||
|
int result = Opus.INSTANCE.opus_decode_float(
|
||||||
|
decoder,
|
||||||
|
packet,
|
||||||
|
packet == null ? 0 : packet.length,
|
||||||
|
FloatBuffer.wrap(floats),
|
||||||
|
frameSize,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
OpusUtil.checkError("opus_decode_float", result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSampleRate() {
|
||||||
|
return sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChannels() {
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
Opus.INSTANCE.opus_decoder_destroy(decoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getVersion() {
|
||||||
|
return Opus.INSTANCE.opus_get_version_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
import com.sun.jna.ptr.PointerByReference;
|
||||||
|
import net.tomp2p.opuswrapper.Opus;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread-safe opus encoder
|
||||||
|
*/
|
||||||
|
public class OpusEncoder implements AutoCloseable {
|
||||||
|
private static final int[] OPUS_PERMITTED_SAMPLE_RATES =
|
||||||
|
{ 8000, 12000, 16000, 24000, 48000 };
|
||||||
|
|
||||||
|
private static final int[] OPUS_PERMITTED_FRAME_SIZES =
|
||||||
|
{ 120, 240, 480, 960, 1920, 2880 };
|
||||||
|
|
||||||
|
private static final int[] OPUS_PERMITTED_CHANNEL_COUNTS =
|
||||||
|
{ 1, 2 };
|
||||||
|
|
||||||
|
private final PointerByReference encoder;
|
||||||
|
private final int sampleRate, frameSize, channels;
|
||||||
|
private final int expectedByteSize;
|
||||||
|
private final boolean bigEndian;
|
||||||
|
|
||||||
|
private final ShortBuffer sourceShortBuffer;
|
||||||
|
private final ByteBuffer sourceByteBuffer;
|
||||||
|
private final ByteBuffer targetBuffer;
|
||||||
|
|
||||||
|
private final Object encoderLock = new Object();
|
||||||
|
|
||||||
|
private boolean closed = false;
|
||||||
|
|
||||||
|
public OpusEncoder(int sampleRate, int frameSize, int channels, boolean bigEndian) {
|
||||||
|
this(sampleRate, frameSize, channels, bigEndian, 4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpusEncoder(int sampleRate, int frameSize,
|
||||||
|
int channels, boolean bigEndian,
|
||||||
|
int maxPacketLength) {
|
||||||
|
this.channels = channels;
|
||||||
|
this.sampleRate = sampleRate;
|
||||||
|
this.frameSize = frameSize;
|
||||||
|
this.bigEndian = bigEndian;
|
||||||
|
this.expectedByteSize = frameSize * channels * 2;
|
||||||
|
|
||||||
|
if (!ArrayUtils.contains(OPUS_PERMITTED_FRAME_SIZES, frameSize))
|
||||||
|
throw new IllegalArgumentException("Invalid Opus frame size: " + frameSize);
|
||||||
|
|
||||||
|
if (!ArrayUtils.contains(OPUS_PERMITTED_SAMPLE_RATES, sampleRate))
|
||||||
|
throw new IllegalArgumentException("Invalid Opus sample rate: " + sampleRate);
|
||||||
|
|
||||||
|
if (!ArrayUtils.contains(OPUS_PERMITTED_CHANNEL_COUNTS, channels))
|
||||||
|
throw new IllegalArgumentException("Invalid Opus channel count: " + channels);
|
||||||
|
|
||||||
|
this.sourceShortBuffer = ShortBuffer.allocate(frameSize * channels);
|
||||||
|
this.sourceByteBuffer = ByteBuffer.allocate(frameSize * channels * 2);
|
||||||
|
this.sourceByteBuffer.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
this.targetBuffer = ByteBuffer.allocate(maxPacketLength);
|
||||||
|
|
||||||
|
IntBuffer errorBuffer = IntBuffer.allocate(1);
|
||||||
|
|
||||||
|
encoder = Opus.INSTANCE.opus_encoder_create(
|
||||||
|
sampleRate,
|
||||||
|
channels,
|
||||||
|
Opus.OPUS_APPLICATION_AUDIO,
|
||||||
|
errorBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
if (encoder == null) throw new NullPointerException("encoder");
|
||||||
|
|
||||||
|
OpusUtil.checkError(
|
||||||
|
"opus_encoder_create",
|
||||||
|
errorBuffer.get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
synchronized (encoderLock) {
|
||||||
|
OpusUtil.checkError(
|
||||||
|
"opus_encoder_ctl/OPUS_RESET_STATE",
|
||||||
|
Opus.INSTANCE.opus_encoder_ctl(encoder, Opus.OPUS_RESET_STATE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int setEncoderValue(int field, Object... values) {
|
||||||
|
synchronized (encoderLock) {
|
||||||
|
if (closed) throw new IllegalStateException("encoder closed");
|
||||||
|
|
||||||
|
return OpusUtil.checkError(
|
||||||
|
"opus_encoder_ctl/" + field,
|
||||||
|
Opus.INSTANCE.opus_encoder_ctl(encoder, field, values)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encode(float[] floats, int len) {
|
||||||
|
if (len != getFrameSize() * getChannels())
|
||||||
|
throw new IllegalArgumentException(len + " != " + getFrameSize() * getChannels());
|
||||||
|
|
||||||
|
targetBuffer.clear();
|
||||||
|
|
||||||
|
int result;
|
||||||
|
synchronized (encoderLock) {
|
||||||
|
if (closed) throw new IllegalStateException("encoder closed");
|
||||||
|
|
||||||
|
if (encoder == null) throw new IllegalStateException("encoder is null");
|
||||||
|
|
||||||
|
result = Opus.INSTANCE.opus_encode_float(
|
||||||
|
encoder,
|
||||||
|
floats,
|
||||||
|
frameSize, // this is STATIC.
|
||||||
|
targetBuffer,
|
||||||
|
targetBuffer.capacity()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusUtil.checkError(
|
||||||
|
"opus_encode_float," +
|
||||||
|
" ch=" + channels +
|
||||||
|
" smprate=" + sampleRate +
|
||||||
|
" rate=" + sampleRate +
|
||||||
|
" len=" + len + "/" + floats.length + ", frame_size=" + frameSize +
|
||||||
|
", max_data_bytes=" + targetBuffer.capacity(),
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
byte[] encoded = new byte[result];
|
||||||
|
targetBuffer.get(encoded);
|
||||||
|
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encode(float[] floats) {
|
||||||
|
return encode(floats, floats.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encode(byte[] pcm) {
|
||||||
|
if (pcm.length != expectedByteSize)
|
||||||
|
throw new IllegalArgumentException(pcm.length + " != " + expectedByteSize);
|
||||||
|
|
||||||
|
// Clear source and input the PCM samples
|
||||||
|
sourceByteBuffer.clear();
|
||||||
|
sourceByteBuffer.put(pcm);
|
||||||
|
sourceByteBuffer.flip();
|
||||||
|
|
||||||
|
sourceShortBuffer.clear();
|
||||||
|
while (sourceByteBuffer.position() < sourceByteBuffer.limit())
|
||||||
|
sourceShortBuffer.put(sourceByteBuffer.getShort());
|
||||||
|
|
||||||
|
// Clear target
|
||||||
|
sourceShortBuffer.flip();
|
||||||
|
targetBuffer.clear();
|
||||||
|
|
||||||
|
int result;
|
||||||
|
|
||||||
|
synchronized (encoderLock) {
|
||||||
|
if (closed) throw new IllegalStateException("encoder closed");
|
||||||
|
|
||||||
|
result = Opus.INSTANCE.opus_encode(
|
||||||
|
encoder,
|
||||||
|
sourceShortBuffer,
|
||||||
|
frameSize, // this is STATIC.
|
||||||
|
targetBuffer,
|
||||||
|
targetBuffer.capacity()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusUtil.checkError("opus_encode", result);
|
||||||
|
|
||||||
|
byte[] encoded = new byte[result];
|
||||||
|
targetBuffer.get(encoded);
|
||||||
|
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFrameSize() { return frameSize; }
|
||||||
|
|
||||||
|
public int getSampleRate() {
|
||||||
|
return sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChannels() {
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
synchronized (encoderLock) {
|
||||||
|
if (closed) throw new IllegalStateException("already closed");
|
||||||
|
Opus.INSTANCE.opus_encoder_destroy(encoder);
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
public class OpusParameters {
|
||||||
|
private final int opusFrameRate;
|
||||||
|
private final int opusBitrate;
|
||||||
|
private final int opusComplexity;
|
||||||
|
private final int opusPacketLoss;
|
||||||
|
private final boolean opusVbr;
|
||||||
|
private final boolean opusFec;
|
||||||
|
private final boolean opusMusic;
|
||||||
|
|
||||||
|
public OpusParameters(int opusFrameRate, int opusBitrate,
|
||||||
|
int opusComplexity, int opusPacketLoss,
|
||||||
|
boolean opusVbr, boolean opusFec, boolean opusMusic) {
|
||||||
|
this.opusFrameRate = opusFrameRate;
|
||||||
|
this.opusBitrate = opusBitrate;
|
||||||
|
this.opusComplexity = opusComplexity;
|
||||||
|
this.opusPacketLoss = opusPacketLoss;
|
||||||
|
this.opusVbr = opusVbr;
|
||||||
|
this.opusFec = opusFec;
|
||||||
|
this.opusMusic = opusMusic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOpusFrameTime() {
|
||||||
|
return opusFrameRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOpusBitrate() {
|
||||||
|
return opusBitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOpusComplexity() {
|
||||||
|
return opusComplexity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOpusPacketLoss() {
|
||||||
|
return opusPacketLoss;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpusVbr() {
|
||||||
|
return opusVbr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpusFec() {
|
||||||
|
return opusFec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpusMusic() {
|
||||||
|
return opusMusic;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
import net.tomp2p.opuswrapper.Opus;
|
||||||
|
|
||||||
|
public class OpusUtil {
|
||||||
|
public static int checkError(String description, int returnCode) throws RuntimeException {
|
||||||
|
if (returnCode < Opus.OPUS_OK)
|
||||||
|
throw new RuntimeException("Opus error [" + description + "] (" + returnCode + "): " +
|
||||||
|
Opus.INSTANCE.opus_strerror(returnCode).trim()
|
||||||
|
);
|
||||||
|
else return returnCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int checkError(int returnCode) throws RuntimeException {
|
||||||
|
if (returnCode < Opus.OPUS_OK)
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Opus error (" + returnCode + "): " +
|
||||||
|
Opus.INSTANCE.opus_strerror(returnCode).trim()
|
||||||
|
);
|
||||||
|
else return returnCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getVersion() {
|
||||||
|
return Opus.INSTANCE.opus_get_version_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
+410
@@ -0,0 +1,410 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.enums.CodecType;
|
||||||
|
import net.tomp2p.opuswrapper.Opus;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class TeamspeakFastMixerSink implements TeamspeakMixerSink {
|
||||||
|
public static final AudioFormat AUDIO_FORMAT =
|
||||||
|
new AudioFormat(48000, 32, 2, true, false);
|
||||||
|
|
||||||
|
private final Object stateLock = new Object();
|
||||||
|
private final Object drainLock = new Object();
|
||||||
|
|
||||||
|
// Audio format
|
||||||
|
private final AudioFormat audioFormat; // Audio output format (mostly just ensured to match certain parameters)
|
||||||
|
|
||||||
|
// OPUS variables
|
||||||
|
private final OpusParameters opusParameters; // Opus parameters set by configurations
|
||||||
|
private volatile OpusEncoder encoder = null; // Opus encoder instance
|
||||||
|
private final int opusFrameSize; // Opus frame size (PER CHANNEL!) (also the ASIO chunk size)
|
||||||
|
private long opusPacketsEncoded = 0, opusPacketsSent = 0;
|
||||||
|
private long opusTime = 0;
|
||||||
|
private long networkTime = 0;
|
||||||
|
private long opusPosition = 0;
|
||||||
|
private long opusBytePosition = 0;
|
||||||
|
|
||||||
|
private long waitTime = 0;
|
||||||
|
|
||||||
|
// Statistics variables
|
||||||
|
private long underflowed = 0; // Underflowed samples (ASIO has to return 0 samples to the encoder)
|
||||||
|
private long overflowed = 0; // Overflowed samples (write returns 0)
|
||||||
|
private long clipped = 0; // Clipped samples (outside -1F,1F bounds)
|
||||||
|
private long position = 0; // Position in samples
|
||||||
|
|
||||||
|
// I/O variables
|
||||||
|
private final Queue<OpusPacket> packetQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
private final int bufferSize; // Buffer size, in samples
|
||||||
|
private volatile int availableInput;
|
||||||
|
private volatile int samplePosition; // Position, in samples, of the ASIO buffer (available samples)
|
||||||
|
private final float[] sampleBuffer; // ASIO buffer
|
||||||
|
|
||||||
|
// Mixer sink state variables
|
||||||
|
private volatile boolean running = false, opening = false;
|
||||||
|
|
||||||
|
public TeamspeakFastMixerSink(AudioFormat audioFormat,
|
||||||
|
int bufferSizeInBytes,
|
||||||
|
OpusParameters opusParameters) {
|
||||||
|
|
||||||
|
this.audioFormat = audioFormat;
|
||||||
|
|
||||||
|
if (audioFormat.getSampleRate() != 48000)
|
||||||
|
throw new IllegalArgumentException("Invalid audio sample rate: " +
|
||||||
|
audioFormat.getSampleRate() + " != 48000: OPUS requires 48KHz audio");
|
||||||
|
else if (audioFormat.getChannels() != 2)
|
||||||
|
throw new IllegalArgumentException("Invalid audio channel count: " +
|
||||||
|
audioFormat.getChannels() + " != 2: OPUS requires stereo audio");
|
||||||
|
|
||||||
|
this.bufferSize = bufferSizeInBytes / (audioFormat.getSampleSizeInBits() / 8);
|
||||||
|
if (bufferSize <= 0 || bufferSize % audioFormat.getChannels() != 0)
|
||||||
|
throw new IllegalArgumentException("invalid bufferSize: " + bufferSize);
|
||||||
|
|
||||||
|
this.sampleBuffer = new float[bufferSize];
|
||||||
|
this.availableInput = 0;
|
||||||
|
|
||||||
|
this.opusParameters = opusParameters;
|
||||||
|
this.opusFrameSize = (int) getAudioFormat().getSampleRate() / (1000 / opusParameters.getOpusFrameTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioFormat getAudioFormat() {
|
||||||
|
return audioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(float[] buffer, int len) {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (!running) throw new IllegalStateException("not running");
|
||||||
|
|
||||||
|
if (len > availableInput()) { // This should never happen
|
||||||
|
overflowed++; // Probably never will run
|
||||||
|
throw new IllegalArgumentException(len + " > " + availableInput());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len > buffer.length) {
|
||||||
|
overflowed++;
|
||||||
|
throw new IllegalArgumentException(len + " > " + buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len <= 0)
|
||||||
|
throw new IllegalArgumentException(len + " <= 0");
|
||||||
|
|
||||||
|
if (len % getChannels() != 0)
|
||||||
|
throw new IllegalArgumentException("not a full frame");
|
||||||
|
|
||||||
|
// Write to the buffer
|
||||||
|
System.arraycopy(buffer, 0, sampleBuffer, samplePosition, len);
|
||||||
|
samplePosition += len;
|
||||||
|
availableInput -= len;
|
||||||
|
|
||||||
|
// Read samples from the buffer and encode them into packets
|
||||||
|
position += encode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the buffer to packets for the packet queue
|
||||||
|
* @return Packets encoded
|
||||||
|
*/
|
||||||
|
private int encode(boolean flush) {
|
||||||
|
if (flush) Logger.getGlobal().log(Level.FINE, "Flushing TeamspeakFastMixerSink...");
|
||||||
|
|
||||||
|
int written = 0;
|
||||||
|
int frameSize = opusFrameSize * getChannels();
|
||||||
|
byte[] encoded;
|
||||||
|
long now;
|
||||||
|
int copy;
|
||||||
|
|
||||||
|
while (samplePosition >= (flush ? 1 : frameSize)) {
|
||||||
|
copy = Math.min(samplePosition, frameSize);
|
||||||
|
if (flush && copy < frameSize) {
|
||||||
|
for (int i = copy; i < frameSize; i++)
|
||||||
|
sampleBuffer[i] = 0f;
|
||||||
|
|
||||||
|
samplePosition = frameSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoder == null) openOpusEncoder();
|
||||||
|
|
||||||
|
now = System.nanoTime();
|
||||||
|
encoded = encoder.encode(sampleBuffer, frameSize);
|
||||||
|
opusTime += (System.nanoTime() - now);
|
||||||
|
opusPacketsEncoded ++;
|
||||||
|
opusPosition += frameSize;
|
||||||
|
|
||||||
|
System.arraycopy(sampleBuffer, frameSize, sampleBuffer, 0, samplePosition - frameSize);
|
||||||
|
samplePosition -= frameSize;
|
||||||
|
if (packetQueue.add(new OpusPacket(frameSize, encoded)))
|
||||||
|
written+=frameSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (written > 0) opening = false;
|
||||||
|
|
||||||
|
if (flush) Logger.getGlobal().log(Level.FINE, "Flushed TeamspeakFastMixerSink.");
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getNanotime() {
|
||||||
|
return opusTime;
|
||||||
|
}
|
||||||
|
public long getFrameSize() {
|
||||||
|
return opusFrameSize;
|
||||||
|
}
|
||||||
|
public long getEncoderPosition() {
|
||||||
|
return opusPosition;
|
||||||
|
}
|
||||||
|
public long getNetworkPosition() {
|
||||||
|
return opusBytePosition;
|
||||||
|
}
|
||||||
|
public OpusParameters getEncoderParameters() {
|
||||||
|
return opusParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPacketsEncoded() {
|
||||||
|
return opusPacketsEncoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPacketsSent() {
|
||||||
|
return opusPacketsSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the count of available samples to be written to write() in the len param
|
||||||
|
* @return Available sample count
|
||||||
|
*/
|
||||||
|
public int availableInput() {
|
||||||
|
return availableInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the Opus encoder
|
||||||
|
*/
|
||||||
|
private void openOpusEncoder() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (encoder != null) return;
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Opening TeamspeakFastMixerSink encoder...");
|
||||||
|
|
||||||
|
encoder = new OpusEncoder(
|
||||||
|
(int) getAudioFormat().getSampleRate(), // smp rate (always 48kHz)
|
||||||
|
opusFrameSize,
|
||||||
|
getChannels(),
|
||||||
|
audioFormat.isBigEndian()
|
||||||
|
//,MAXIMUM_OPUS_PACKET_SIZE // Prevent IP fragmentation
|
||||||
|
);
|
||||||
|
|
||||||
|
encoder.setEncoderValue(
|
||||||
|
Opus.OPUS_SET_SIGNAL_REQUEST,
|
||||||
|
opusParameters.isOpusMusic() ? Opus.OPUS_SIGNAL_MUSIC : Opus.OPUS_SIGNAL_VOICE
|
||||||
|
);
|
||||||
|
|
||||||
|
encoder.setEncoderValue(Opus.OPUS_SET_BITRATE_REQUEST, opusParameters.getOpusBitrate());
|
||||||
|
encoder.setEncoderValue(Opus.OPUS_SET_COMPLEXITY_REQUEST, opusParameters.getOpusComplexity());
|
||||||
|
encoder.setEncoderValue(Opus.OPUS_SET_PACKET_LOSS_PERC_REQUEST, opusParameters.getOpusPacketLoss());
|
||||||
|
encoder.setEncoderValue(Opus.OPUS_SET_VBR_REQUEST, opusParameters.isOpusVbr() ? 1 : 0);
|
||||||
|
encoder.setEncoderValue(Opus.OPUS_SET_INBAND_FEC_REQUEST, opusParameters.isOpusFec() ? 1 : 0);
|
||||||
|
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Opened TeamspeakFastMixerSink encoder.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeOpusEncoder() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (encoder != null) {
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Closing TeamspeakFastMixerSink encoder...");
|
||||||
|
encoder.close();
|
||||||
|
encoder = null;
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Closed TeamspeakFastMixerSink encoder.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (isRunning()) stop();
|
||||||
|
|
||||||
|
closeOpusEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean start() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (running) return false;
|
||||||
|
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Starting TeamspeakFastMixerSink...");
|
||||||
|
|
||||||
|
// Flush buffers, clear outgoing packet queues.
|
||||||
|
for (int i = 0; i < sampleBuffer.length; i ++) sampleBuffer[i] = 0f;
|
||||||
|
samplePosition = 0;
|
||||||
|
availableInput = bufferSize;
|
||||||
|
packetQueue.clear();
|
||||||
|
|
||||||
|
// Open (or re-open) Opus encoder.
|
||||||
|
openOpusEncoder();
|
||||||
|
|
||||||
|
// Reset the encoder
|
||||||
|
if (encoder != null) {
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Resetting TeamspeakFastMixerSink...");
|
||||||
|
encoder.reset(); // Reset encoder output
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Reset TeamspeakFastMixerSink.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as running.
|
||||||
|
opening = true;
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Started TeamspeakFastMixerSink.");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drain() throws InterruptedException {
|
||||||
|
synchronized (drainLock) {
|
||||||
|
while (running && packetQueue.peek() != null) {
|
||||||
|
drainLock.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes mixer sink
|
||||||
|
*
|
||||||
|
* Note that we don't close the Opus encoder - just in case there are more samples to write/flush out.
|
||||||
|
*/
|
||||||
|
public boolean stop() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (!running) return false;
|
||||||
|
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Stopping TeamspeakFastMixerSink...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
encode(true);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Logger.getGlobal().log(Level.WARNING, "Problem flushing audio buffer upon close", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
Logger.getGlobal().log(Level.FINE, "Stopped TeamspeakFastMixerSink.");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBufferSize() {
|
||||||
|
return bufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getPosition() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getClippedSamples() {
|
||||||
|
return clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUnderflows() {
|
||||||
|
return underflowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOverflows() {
|
||||||
|
return overflowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the TS3J encoder thread to determine if the sink is ready to send samples.
|
||||||
|
* @return true if the sink has samples to write, or if samples will become available.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return (running && !opening) || packetQueue.size() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CodecType getCodec() {
|
||||||
|
return CodecType.OPUS_MUSIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides OPUS encoded audio (as a packet) to the TS3J UDP audio network thread.
|
||||||
|
* @return Encoded OPUS audio packet (zero-length packet if there is an underflow).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] provide() {
|
||||||
|
long start = System.nanoTime();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (packetQueue.peek() == null) {
|
||||||
|
underflowed ++;
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusPacket packet = packetQueue.remove();
|
||||||
|
|
||||||
|
if (packet == null) {
|
||||||
|
underflowed ++;
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
availableInput += packet.getSamples();
|
||||||
|
opusBytePosition += packet.getBytes().length;
|
||||||
|
opusPacketsSent ++;
|
||||||
|
|
||||||
|
return packet.getBytes();
|
||||||
|
} catch (NoSuchElementException ex) {
|
||||||
|
underflowed ++;
|
||||||
|
return new byte[0];
|
||||||
|
} finally {
|
||||||
|
long networkTime = System.nanoTime() - start;
|
||||||
|
|
||||||
|
if (networkTime >= (opusParameters.getOpusFrameTime() * 1000000L)) {
|
||||||
|
underflowed++;
|
||||||
|
|
||||||
|
Logger.getGlobal().warning("[TeamspeakFastMixerSink] provide() took longer than the expected " +
|
||||||
|
opusParameters.getOpusFrameTime() + "ms: " + ((double)networkTime / 1_000_000D) + "ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.networkTime += networkTime;
|
||||||
|
|
||||||
|
if (packetQueue.peek() == null) {
|
||||||
|
synchronized (drainLock) {
|
||||||
|
drainLock.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChannels() {
|
||||||
|
return getAudioFormat().getChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getWaitTime() {
|
||||||
|
return waitTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class OpusPacket {
|
||||||
|
private final int samples;
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
protected OpusPacket(int samples, byte[] bytes) {
|
||||||
|
this.samples = samples;
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSamples() {
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package com.github.manevolent.ts3j.examples.audio;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.audio.Microphone;
|
||||||
|
|
||||||
|
public interface TeamspeakMixerSink extends Microphone {
|
||||||
|
/**
|
||||||
|
* Gets the Encoder parameters
|
||||||
|
* @return Encoder parameters
|
||||||
|
*/
|
||||||
|
OpusParameters getEncoderParameters();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the count of encoded Encoder packets
|
||||||
|
* @return Encoder packets
|
||||||
|
*/
|
||||||
|
long getPacketsEncoded();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the count of written encoded Encoder packets
|
||||||
|
* @return Encoder packets
|
||||||
|
*/
|
||||||
|
long getPacketsSent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the time spent encoding Encoder packets, in nanoseconds
|
||||||
|
* @return Encoder time
|
||||||
|
*/
|
||||||
|
long getNanotime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Encoder frame size for packets
|
||||||
|
* @return Encoder frame size
|
||||||
|
*/
|
||||||
|
long getFrameSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Encoder position in samples
|
||||||
|
* @return Encoder position
|
||||||
|
*/
|
||||||
|
long getEncoderPosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Encoder network position in bytes
|
||||||
|
* @return Encoder network position
|
||||||
|
*/
|
||||||
|
long getNetworkPosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the channel count
|
||||||
|
* @return Channel count
|
||||||
|
*/
|
||||||
|
int getChannels();
|
||||||
|
|
||||||
|
}
|
||||||
+22
-4
@@ -1088,16 +1088,34 @@ public class LocalTeamspeakClientSocket
|
|||||||
throws
|
throws
|
||||||
IOException, TimeoutException,
|
IOException, TimeoutException,
|
||||||
ExecutionException, InterruptedException {
|
ExecutionException, InterruptedException {
|
||||||
|
disconnect(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnect(String reason)
|
||||||
|
throws
|
||||||
|
IOException, TimeoutException,
|
||||||
|
ExecutionException, InterruptedException {
|
||||||
try {
|
try {
|
||||||
switch (getState()) {
|
switch (getState()) {
|
||||||
case CONNECTING:
|
case CONNECTING:
|
||||||
case RETRIEVING_DATA:
|
case RETRIEVING_DATA:
|
||||||
waitForState(ClientConnectionState.CONNECTED, 10000L);
|
waitForState(ClientConnectionState.CONNECTED, 10000L);
|
||||||
case CONNECTED:
|
case CONNECTED:
|
||||||
writePacket(new PacketBody2Command(
|
if (reason == null) {
|
||||||
ProtocolRole.CLIENT,
|
writePacket(new PacketBody2Command(
|
||||||
new SingleCommand("clientdisconnect", ProtocolRole.CLIENT))
|
ProtocolRole.CLIENT,
|
||||||
);
|
new SingleCommand("clientdisconnect", ProtocolRole.CLIENT))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
writePacket(new PacketBody2Command(
|
||||||
|
ProtocolRole.CLIENT,
|
||||||
|
new SingleCommand(
|
||||||
|
"clientdisconnect", ProtocolRole.CLIENT,
|
||||||
|
new CommandSingleParameter("reasonid", "8"),
|
||||||
|
new CommandSingleParameter("reasonmsg", reason)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
waitForState(ClientConnectionState.DISCONNECTED, 30000L);
|
waitForState(ClientConnectionState.DISCONNECTED, 30000L);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
|
|||||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice;
|
import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice;
|
||||||
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;
|
||||||
|
import com.sun.security.ntlm.Server;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
@@ -20,34 +21,29 @@ import static org.junit.Assert.assertEquals;
|
|||||||
|
|
||||||
public class ServerConnectionTest {
|
public class ServerConnectionTest {
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
|
Ts3Debugging.setEnabled(true);
|
||||||
|
|
||||||
LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
|
LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket();
|
||||||
|
|
||||||
Ts3Debugging.setEnabled(false);
|
|
||||||
|
|
||||||
LocalIdentity identity = LocalIdentity.generateNew(10);
|
LocalIdentity identity = LocalIdentity.generateNew(10);
|
||||||
|
|
||||||
client.setIdentity(identity);
|
client.setIdentity(identity);
|
||||||
client.setNickname("ts3j issue 2");
|
client.setNickname(ServerConnectionTest.class.getSimpleName());
|
||||||
client.setHWID("TestTestTest");
|
client.setHWID("TestTestTest");
|
||||||
|
|
||||||
client.setVoiceHandler(packet ->
|
try {
|
||||||
System.out.println(packet.getServerFlag0())
|
client.connect(
|
||||||
);
|
"teamlixo.net",
|
||||||
|
10000L
|
||||||
|
);
|
||||||
|
|
||||||
while (true) {
|
assertEquals(client.getState(), ClientConnectionState.CONNECTED);
|
||||||
try {
|
|
||||||
client.connect(
|
|
||||||
"teamlixo.net",
|
|
||||||
10000L
|
|
||||||
);
|
|
||||||
|
|
||||||
assertEquals(client.getState(), ClientConnectionState.CONNECTED);
|
client.disconnect("BYE");
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(30000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Thread.sleep(3000L);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user