Voice-Bridge: Plugin, Bridge-Dienst und Protokoll
This commit is contained in:
commit
c0c290d3dc
35 files changed
+2497
No files matched your search
@@ -0,0 +1,9 @@
|
||||
dependencies {
|
||||
testImplementation platform('org.junit:junit-bom:5.13.4')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package eu.mhsl.voicebridge.protocol;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Nachrichten zwischen Paper-Plugin und Bridge-Dienst (UDP, ein Datagramm pro Nachricht).
|
||||
*/
|
||||
public sealed interface Message {
|
||||
|
||||
/** Plugin → Bridge, jede Sekunde: Herzschlag und verbundene Spieler pro Raum. */
|
||||
record Hello(int version, Map<String, Integer> members) implements Message {}
|
||||
|
||||
/** Bridge → Plugin, Antwort auf jedes Hello: die Räume und wer auf den anderen Seiten ist. */
|
||||
record Rooms(List<RoomInfo> rooms) implements Message {}
|
||||
|
||||
record RoomInfo(String key, String name, UUID groupId, int teamspeak, int discord, boolean active) {}
|
||||
|
||||
/**
|
||||
* Ein Opus-Frame (20 ms, 48 kHz mono) in beide Richtungen. {@code source} ist beim Plugin → Bridge
|
||||
* die Spieler-UUID, beim Bridge → Plugin eine feste UUID pro externem Sprecher.
|
||||
*/
|
||||
record Audio(String room, UUID source, byte[] opus) implements Message {}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package eu.mhsl.voicebridge.protocol;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class Protocol {
|
||||
/** Bei inkompatiblen Änderungen am Format hochzählen. */
|
||||
public static final int VERSION = 1;
|
||||
public static final int DEFAULT_PORT = 24470;
|
||||
public static final int MAX_DATAGRAM = 2048;
|
||||
|
||||
private static final byte HELLO = 1;
|
||||
private static final byte ROOMS = 2;
|
||||
private static final byte AUDIO = 3;
|
||||
|
||||
private Protocol() {}
|
||||
|
||||
public static byte[] encode(Message message) {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
switch (message) {
|
||||
case Message.Hello hello -> {
|
||||
out.writeByte(HELLO);
|
||||
out.writeInt(hello.version());
|
||||
out.writeShort(hello.members().size());
|
||||
for (Map.Entry<String, Integer> e : hello.members().entrySet()) {
|
||||
out.writeUTF(e.getKey());
|
||||
out.writeShort(e.getValue());
|
||||
}
|
||||
}
|
||||
case Message.Rooms rooms -> {
|
||||
out.writeByte(ROOMS);
|
||||
out.writeShort(rooms.rooms().size());
|
||||
for (Message.RoomInfo room : rooms.rooms()) {
|
||||
out.writeUTF(room.key());
|
||||
out.writeUTF(room.name());
|
||||
writeUuid(out, room.groupId());
|
||||
out.writeShort(room.teamspeak());
|
||||
out.writeShort(room.discord());
|
||||
out.writeBoolean(room.active());
|
||||
}
|
||||
}
|
||||
case Message.Audio audio -> {
|
||||
out.writeByte(AUDIO);
|
||||
out.writeUTF(audio.room());
|
||||
writeUuid(out, audio.source());
|
||||
out.write(audio.opus());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
public static Message decode(byte[] data, int offset, int length) throws IOException {
|
||||
DataInputStream in = new DataInputStream(new ByteArrayInputStream(data, offset, length));
|
||||
byte type = in.readByte();
|
||||
return switch (type) {
|
||||
case HELLO -> {
|
||||
int version = in.readInt();
|
||||
int count = in.readUnsignedShort();
|
||||
Map<String, Integer> members = new LinkedHashMap<>();
|
||||
for (int i = 0; i < count; i++) members.put(in.readUTF(), in.readUnsignedShort());
|
||||
yield new Message.Hello(version, members);
|
||||
}
|
||||
case ROOMS -> {
|
||||
int count = in.readUnsignedShort();
|
||||
List<Message.RoomInfo> rooms = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
rooms.add(new Message.RoomInfo(in.readUTF(), in.readUTF(), readUuid(in),
|
||||
in.readUnsignedShort(), in.readUnsignedShort(), in.readBoolean()));
|
||||
}
|
||||
yield new Message.Rooms(rooms);
|
||||
}
|
||||
case AUDIO -> new Message.Audio(in.readUTF(), readUuid(in), in.readAllBytes());
|
||||
default -> throw new IOException("Unbekannter Nachrichtentyp " + type);
|
||||
};
|
||||
}
|
||||
|
||||
private static void writeUuid(DataOutputStream out, UUID uuid) throws IOException {
|
||||
out.writeLong(uuid.getMostSignificantBits());
|
||||
out.writeLong(uuid.getLeastSignificantBits());
|
||||
}
|
||||
|
||||
private static UUID readUuid(DataInputStream in) throws IOException {
|
||||
return new UUID(in.readLong(), in.readLong());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.mhsl.voicebridge.protocol;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ProtocolTest {
|
||||
|
||||
private static Message roundTrip(Message message) throws IOException {
|
||||
byte[] data = Protocol.encode(message);
|
||||
return Protocol.decode(data, 0, data.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hello() throws IOException {
|
||||
Message.Hello hello = new Message.Hello(Protocol.VERSION, Map.of("crosstalk-1", 3));
|
||||
assertEquals(hello, roundTrip(hello));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rooms() throws IOException {
|
||||
Message.Rooms rooms = new Message.Rooms(List.of(
|
||||
new Message.RoomInfo("crosstalk-1", "Crosstalk 1", UUID.randomUUID(), 2, 0, true)));
|
||||
assertEquals(rooms, roundTrip(rooms));
|
||||
}
|
||||
|
||||
@Test
|
||||
void audio() throws IOException {
|
||||
byte[] opus = {1, 2, 3, (byte) 0xF8, (byte) 0xFF};
|
||||
UUID source = UUID.randomUUID();
|
||||
Message.Audio audio = (Message.Audio) roundTrip(new Message.Audio("crosstalk-2", source, opus));
|
||||
assertEquals("crosstalk-2", audio.room());
|
||||
assertEquals(source, audio.source());
|
||||
assertArrayEquals(opus, audio.opus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownType() {
|
||||
assertThrows(IOException.class, () -> Protocol.decode(new byte[]{42}, 0, 1));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user