Voice-Bridge: Plugin, Bridge-Dienst und Protokoll

This commit is contained in:
MineTec committed 2026-10-18 12:46:59 +02:00
commit c0c290d3dc
35 files changed
+2497

No files matched your search

+9
View File
@@ -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));
}
}