mirror of
https://github.com/ayunami2000/ayungee.git
synced 2024-12-21 14:24:10 -08:00
Voice chat
This commit is contained in:
parent
b5cf14b204
commit
4f3e6e1d58
71
src/main/java/me/ayunami2000/ayungee/ExpiringSet.java
Normal file
71
src/main/java/me/ayunami2000/ayungee/ExpiringSet.java
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package me.ayunami2000.ayungee;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
// note that there's a few things not implemented, but I don't care.
|
||||||
|
|
||||||
|
public class ExpiringSet<T> extends HashSet<T> {
|
||||||
|
private final long expiration;
|
||||||
|
private final ExpiringEvent<T> event;
|
||||||
|
|
||||||
|
private final Map<T, Long> timestamps = new HashMap<>();
|
||||||
|
|
||||||
|
public ExpiringSet(long expiration) {
|
||||||
|
this.expiration = expiration;
|
||||||
|
this.event = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpiringSet(long expiration, ExpiringEvent<T> event) {
|
||||||
|
this.expiration = expiration;
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ExpiringEvent<T> {
|
||||||
|
void onExpiration(T item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkForExpirations() {
|
||||||
|
Iterator<T> iterator = this.timestamps.keySet().iterator();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
T element = iterator.next();
|
||||||
|
if (super.contains(element)) {
|
||||||
|
if (this.timestamps.get(element) + this.expiration < now) {
|
||||||
|
if (this.event != null) this.event.onExpiration(element);
|
||||||
|
iterator.remove();
|
||||||
|
super.remove(element);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iterator.remove();
|
||||||
|
super.remove(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(T o) {
|
||||||
|
checkForExpirations();
|
||||||
|
boolean success = super.add(o);
|
||||||
|
if (success) timestamps.put(o, System.currentTimeMillis());
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(Object o) {
|
||||||
|
checkForExpirations();
|
||||||
|
boolean success = super.remove(o);
|
||||||
|
if (success) timestamps.remove(o);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
this.timestamps.clear();
|
||||||
|
super.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(Object o) {
|
||||||
|
checkForExpirations();
|
||||||
|
return super.contains(o);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,9 +23,11 @@ public class Main {
|
||||||
public static byte[] serverIcon = null;
|
public static byte[] serverIcon = null;
|
||||||
|
|
||||||
public static boolean forwarded = false;
|
public static boolean forwarded = false;
|
||||||
|
|
||||||
public static boolean serverCmd = true;
|
public static boolean serverCmd = true;
|
||||||
|
|
||||||
|
public static boolean voiceEnabled = true;
|
||||||
|
public static List<String> voiceICE = new ArrayList<>();
|
||||||
|
|
||||||
public static boolean useAuth = false;
|
public static boolean useAuth = false;
|
||||||
public static int authIpLimit = -1;
|
public static int authIpLimit = -1;
|
||||||
|
|
||||||
|
@ -117,6 +119,21 @@ public class Main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
voiceICE.add("stun:openrelay.metered.ca:80");
|
||||||
|
voiceICE.add("turn:openrelay.metered.ca:80;openrelayproject;openrelayproject");
|
||||||
|
voiceICE.add("turn:openrelay.metered.ca:443;openrelayproject;openrelayproject");
|
||||||
|
voiceICE.add("turn:openrelay.metered.ca:443?transport=tcp;openrelayproject;openrelayproject");
|
||||||
|
|
||||||
|
Map<String, Object> configVoice = new LinkedHashMap<>();
|
||||||
|
configVoice.put("enabled", true);
|
||||||
|
configVoice.put("ice", voiceICE);
|
||||||
|
|
||||||
|
configVoice = (LinkedHashMap<String, Object>) config.getOrDefault("voice", configVoice);
|
||||||
|
|
||||||
|
voiceEnabled = (boolean) configVoice.getOrDefault("enabled", true);
|
||||||
|
|
||||||
|
voiceICE = (List<String>) configVoice.getOrDefault("ice", voiceICE);
|
||||||
|
|
||||||
Map<String, Object> configAuth = new LinkedHashMap<>();
|
Map<String, Object> configAuth = new LinkedHashMap<>();
|
||||||
configAuth.put("enabled", false);
|
configAuth.put("enabled", false);
|
||||||
configAuth.put("ip_limit", -1);
|
configAuth.put("ip_limit", -1);
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class PluginMessages {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean fromClient(Client client, String name, byte[] data) {
|
public static boolean fromClient(Client client, String name, byte[] data) {
|
||||||
return Skins.setSkin(client.username, client.conn, name, data);
|
return Skins.setSkin(client.username, client.conn, name, data) || Voice.handleVoice(client.username, client.conn, name, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean serverPluginMessage(Client client, byte[] packet) {
|
public static boolean serverPluginMessage(Client client, byte[] packet) {
|
||||||
|
|
209
src/main/java/me/ayunami2000/ayungee/Voice.java
Normal file
209
src/main/java/me/ayunami2000/ayungee/Voice.java
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
package me.ayunami2000.ayungee;
|
||||||
|
|
||||||
|
import org.java_websocket.WebSocket;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class Voice {
|
||||||
|
private static final Map<String, WebSocket> voicePlayers = new HashMap<>();
|
||||||
|
private static final Map<String, ExpiringSet<String>> voiceRequests = new HashMap<>();
|
||||||
|
private static final Set<String[]> voicePairs = new HashSet<>();
|
||||||
|
|
||||||
|
private static final int VOICE_SIGNAL_ALLOWED = 0;
|
||||||
|
private static final int VOICE_SIGNAL_REQUEST = 0;
|
||||||
|
private static final int VOICE_SIGNAL_CONNECT = 1;
|
||||||
|
private static final int VOICE_SIGNAL_DISCONNECT = 2;
|
||||||
|
private static final int VOICE_SIGNAL_ICE = 3;
|
||||||
|
private static final int VOICE_SIGNAL_DESC = 4;
|
||||||
|
private static final int VOICE_SIGNAL_GLOBAL = 5;
|
||||||
|
|
||||||
|
public static boolean handleVoice(String user, WebSocket connection, String tag, byte[] msg) {
|
||||||
|
synchronized (voicePlayers) {
|
||||||
|
if (!Main.voiceEnabled) return false;
|
||||||
|
if (!("EAG|Voice".equals(tag))) return false;
|
||||||
|
try {
|
||||||
|
if (msg.length == 0) return true;
|
||||||
|
DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(msg));
|
||||||
|
int sig = streamIn.read();
|
||||||
|
switch (sig) {
|
||||||
|
case VOICE_SIGNAL_CONNECT:
|
||||||
|
if (voicePlayers.containsKey(user)) return true; // user is already using voice chat
|
||||||
|
// send out packet for player joined voice
|
||||||
|
// notice: everyone on the server can see this packet!! however, it doesn't do anything but let clients know that the player has turned on voice chat
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(baos);
|
||||||
|
dos.write(VOICE_SIGNAL_CONNECT);
|
||||||
|
dos.writeUTF(user);
|
||||||
|
byte[] out = baos.toByteArray();
|
||||||
|
for (WebSocket conn : voicePlayers.values()) sendVoicePacket(conn, out);
|
||||||
|
voicePlayers.put(user, connection);
|
||||||
|
for (String username : voicePlayers.keySet()) sendVoicePlayers(username);
|
||||||
|
break;
|
||||||
|
case VOICE_SIGNAL_DISCONNECT:
|
||||||
|
if (!voicePlayers.containsKey(user)) return true; // user is not using voice chat
|
||||||
|
try {
|
||||||
|
String user2 = streamIn.readUTF();
|
||||||
|
if (!voicePlayers.containsKey(user2)) return true;
|
||||||
|
if (voicePairs.removeIf(pair -> (pair[0].equals(user) && pair[1].equals(user2)) || (pair[0].equals(user2) && pair[1].equals(user)))) {
|
||||||
|
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos2 = new DataOutputStream(baos2);
|
||||||
|
dos2.write(VOICE_SIGNAL_DISCONNECT);
|
||||||
|
dos2.writeUTF(user);
|
||||||
|
sendVoicePacket(voicePlayers.get(user2), baos2.toByteArray());
|
||||||
|
baos2 = new ByteArrayOutputStream();
|
||||||
|
dos2 = new DataOutputStream(baos2);
|
||||||
|
dos2.write(VOICE_SIGNAL_DISCONNECT);
|
||||||
|
dos2.writeUTF(user2);
|
||||||
|
sendVoicePacket(connection, baos2.toByteArray());
|
||||||
|
}
|
||||||
|
} catch (EOFException e) {
|
||||||
|
removeUser(user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VOICE_SIGNAL_REQUEST:
|
||||||
|
if (!voicePlayers.containsKey(user)) return true; // user is not using voice chat
|
||||||
|
String targetUser = streamIn.readUTF();
|
||||||
|
if (user.equals(targetUser)) return true; // prevent duplicates
|
||||||
|
if (checkVoicePair(user, targetUser)) return true; // already paired
|
||||||
|
if (!voicePlayers.containsKey(targetUser)) return true; // target user is not using voice chat
|
||||||
|
if (!voiceRequests.containsKey(user)) voiceRequests.put(user, new ExpiringSet<>(2000));
|
||||||
|
if (voiceRequests.get(user).contains(targetUser)) return true;
|
||||||
|
voiceRequests.get(user).add(targetUser);
|
||||||
|
|
||||||
|
// check if other has requested earlier
|
||||||
|
if (voiceRequests.containsKey(targetUser) && voiceRequests.get(targetUser).contains(user)) {
|
||||||
|
if (voiceRequests.containsKey(targetUser)) {
|
||||||
|
voiceRequests.get(targetUser).remove(user);
|
||||||
|
if (voiceRequests.get(targetUser).isEmpty()) voiceRequests.remove(targetUser);
|
||||||
|
}
|
||||||
|
if (voiceRequests.containsKey(user)) {
|
||||||
|
voiceRequests.get(user).remove(targetUser);
|
||||||
|
if (voiceRequests.get(user).isEmpty()) voiceRequests.remove(user);
|
||||||
|
}
|
||||||
|
// send each other add data
|
||||||
|
voicePairs.add(new String[]{user, targetUser});
|
||||||
|
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos2 = new DataOutputStream(baos2);
|
||||||
|
dos2.write(VOICE_SIGNAL_CONNECT);
|
||||||
|
dos2.writeUTF(user);
|
||||||
|
dos2.writeBoolean(false);
|
||||||
|
sendVoicePacket(voicePlayers.get(targetUser), baos2.toByteArray());
|
||||||
|
baos2 = new ByteArrayOutputStream();
|
||||||
|
dos2 = new DataOutputStream(baos2);
|
||||||
|
dos2.write(VOICE_SIGNAL_CONNECT);
|
||||||
|
dos2.writeUTF(targetUser);
|
||||||
|
dos2.writeBoolean(true);
|
||||||
|
sendVoicePacket(connection, baos2.toByteArray());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VOICE_SIGNAL_ICE:
|
||||||
|
case VOICE_SIGNAL_DESC:
|
||||||
|
if (!voicePlayers.containsKey(user)) return true; // user is not using voice chat
|
||||||
|
String targetUser2 = streamIn.readUTF();
|
||||||
|
if (checkVoicePair(user, targetUser2)) {
|
||||||
|
String data = streamIn.readUTF();
|
||||||
|
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos2 = new DataOutputStream(baos2);
|
||||||
|
dos2.write(sig);
|
||||||
|
dos2.writeUTF(user);
|
||||||
|
dos2.writeUTF(data);
|
||||||
|
sendVoicePacket(voicePlayers.get(targetUser2), baos2.toByteArray());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// hacker
|
||||||
|
// t.printStackTrace(); // todo: remove in production
|
||||||
|
removeUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onLogin(String username, WebSocket conn) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(baos);
|
||||||
|
dos.write(VOICE_SIGNAL_ALLOWED);
|
||||||
|
dos.writeBoolean(Main.voiceEnabled);
|
||||||
|
dos.write(Main.voiceICE.size());
|
||||||
|
for(String str : Main.voiceICE) {
|
||||||
|
dos.writeUTF(str);
|
||||||
|
}
|
||||||
|
sendVoicePacket(conn, baos.toByteArray());
|
||||||
|
sendVoicePlayers(username);
|
||||||
|
} catch (IOException ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onQuit(String username) {
|
||||||
|
removeUser(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendVoicePlayers(String name) {
|
||||||
|
synchronized (voicePlayers) {
|
||||||
|
if (!Main.voiceEnabled) return;
|
||||||
|
if (!voicePlayers.containsKey(name)) return;
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(baos);
|
||||||
|
dos.write(VOICE_SIGNAL_GLOBAL);
|
||||||
|
Set<String> mostlyGlobalPlayers = new HashSet<>();
|
||||||
|
for (String username : voicePlayers.keySet()) {
|
||||||
|
if (username.equals(name)) continue;
|
||||||
|
if (voicePairs.stream().anyMatch(pair -> (pair[0].equals(name) && pair[1].equals(username)) || (pair[0].equals(username) && pair[1].equals(name))))
|
||||||
|
continue;
|
||||||
|
mostlyGlobalPlayers.add(username);
|
||||||
|
}
|
||||||
|
if (mostlyGlobalPlayers.size() > 0) {
|
||||||
|
dos.writeInt(mostlyGlobalPlayers.size());
|
||||||
|
for (String username : mostlyGlobalPlayers) dos.writeUTF(username);
|
||||||
|
sendVoicePacket(voicePlayers.get(name), baos.toByteArray());
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeUser(String name) {
|
||||||
|
synchronized (voicePlayers) {
|
||||||
|
voicePlayers.remove(name);
|
||||||
|
for (String username : voicePlayers.keySet()) {
|
||||||
|
if (!name.equals(username)) sendVoicePlayers(username);
|
||||||
|
}
|
||||||
|
for (String[] voicePair : voicePairs) {
|
||||||
|
String target = null;
|
||||||
|
if (voicePair[0].equals(name)) {
|
||||||
|
target = voicePair[1];
|
||||||
|
} else if (voicePair[1].equals(name)) {
|
||||||
|
target = voicePair[0];
|
||||||
|
}
|
||||||
|
if (target != null && voicePlayers.containsKey(target)) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(baos);
|
||||||
|
dos.write(VOICE_SIGNAL_DISCONNECT);
|
||||||
|
dos.writeUTF(name);
|
||||||
|
sendVoicePacket(voicePlayers.get(target), baos.toByteArray());
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
voicePairs.removeIf(pair -> pair[0].equals(name) || pair[1].equals(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean checkVoicePair(String user1, String user2) {
|
||||||
|
return voicePairs.stream().anyMatch(pair -> (pair[0].equals(user1) && pair[1].equals(user2)) || (pair[0].equals(user2) && pair[1].equals(user1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendVoicePacket(WebSocket conn, byte[] msg) {
|
||||||
|
byte[] packetPrefix = new byte[] { (byte) 250, 0, 9, 0, 69, 0, 65, 0, 71, 0, 124, 0, 86, 0, 111, 0, 105, 0, 99, 0, 101, (byte) ((msg.length >>> 8) & 0xFF), (byte) (msg.length & 0xFF) };
|
||||||
|
byte[] fullPacket = new byte[packetPrefix.length + msg.length];
|
||||||
|
System.arraycopy(packetPrefix, 0, fullPacket, 0, packetPrefix.length);
|
||||||
|
System.arraycopy(msg, 0, fullPacket, packetPrefix.length, msg.length);
|
||||||
|
conn.send(fullPacket);
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,6 +66,7 @@ public class WebSocketProxy extends WebSocketServer {
|
||||||
if (selfClient != null) {
|
if (selfClient != null) {
|
||||||
Main.printMsg("Player " + selfClient + " left!");
|
Main.printMsg("Player " + selfClient + " left!");
|
||||||
Skins.removeSkin(selfClient.username);
|
Skins.removeSkin(selfClient.username);
|
||||||
|
Voice.onQuit(selfClient.username);
|
||||||
if (selfClient.socket.isClosed()) {
|
if (selfClient.socket.isClosed()) {
|
||||||
try {
|
try {
|
||||||
selfClient.socket.close();
|
selfClient.socket.close();
|
||||||
|
@ -169,7 +170,10 @@ public class WebSocketProxy extends WebSocketServer {
|
||||||
if (!selfClient.authed && data[0] == 13) selfClient.positionPacket = data;
|
if (!selfClient.authed && data[0] == 13) selfClient.positionPacket = data;
|
||||||
boolean loginPacket = data[0] == 1;
|
boolean loginPacket = data[0] == 1;
|
||||||
if (loginPacket && !selfClient.hasLoginHappened) selfClient.hasLoginHappened = true;
|
if (loginPacket && !selfClient.hasLoginHappened) selfClient.hasLoginHappened = true;
|
||||||
if (selfClient.firstTime && loginPacket) selfClient.clientEntityId = selfClient.serverEntityId = EntityMap.readInt(data, 1);
|
if (selfClient.firstTime && loginPacket) {
|
||||||
|
selfClient.clientEntityId = selfClient.serverEntityId = EntityMap.readInt(data, 1);
|
||||||
|
Voice.onLogin(username, conn);
|
||||||
|
}
|
||||||
if (!selfClient.firstTime && loginPacket) {
|
if (!selfClient.firstTime && loginPacket) {
|
||||||
selfClient.serverEntityId = EntityMap.readInt(data, 1);
|
selfClient.serverEntityId = EntityMap.readInt(data, 1);
|
||||||
// assume server is giving valid data; we don't have to validate it because it isn't a potentially malicious client
|
// assume server is giving valid data; we don't have to validate it because it isn't a potentially malicious client
|
||||||
|
|
|
@ -13,6 +13,18 @@ origin_blacklist: "https://g.eags.us/eaglercraft/origin_blacklist.txt"
|
||||||
origins: []
|
origins: []
|
||||||
# enable /server command?
|
# enable /server command?
|
||||||
server_cmd: true
|
server_cmd: true
|
||||||
|
# voice chat
|
||||||
|
voice:
|
||||||
|
# enable voice chat
|
||||||
|
enabled: true
|
||||||
|
# voice chat ICE servers
|
||||||
|
# format is like <server> OR <server>;<username>;<password>
|
||||||
|
# chances are you won't have to change these
|
||||||
|
ice:
|
||||||
|
- "stun:openrelay.metered.ca:80"
|
||||||
|
- "turn:openrelay.metered.ca:80;openrelayproject;openrelayproject"
|
||||||
|
- "turn:openrelay.metered.ca:443;openrelayproject;openrelayproject"
|
||||||
|
- "turn:openrelay.metered.ca:443?transport=tcp;openrelayproject;openrelayproject"
|
||||||
# authentication info
|
# authentication info
|
||||||
auth:
|
auth:
|
||||||
# use auth
|
# use auth
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user