Added audio sender test and disconnect reasons

This commit is contained in:
Manevolent
2018-12-21 11:02:08 -05:00
parent 24da34d23d
commit 9d602a8f98
10 changed files with 994 additions and 22 deletions
+60
View File
@@ -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>
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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();
}
@@ -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);
} }
} }