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 boolean forwarded = false;
|
||||
|
||||
public static boolean serverCmd = true;
|
||||
|
||||
public static boolean voiceEnabled = true;
|
||||
public static List<String> voiceICE = new ArrayList<>();
|
||||
|
||||
public static boolean useAuth = false;
|
||||
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<>();
|
||||
configAuth.put("enabled", false);
|
||||
configAuth.put("ip_limit", -1);
|
||||
|
|
|
@ -30,7 +30,7 @@ public class PluginMessages {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
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) {
|
||||
Main.printMsg("Player " + selfClient + " left!");
|
||||
Skins.removeSkin(selfClient.username);
|
||||
Voice.onQuit(selfClient.username);
|
||||
if (selfClient.socket.isClosed()) {
|
||||
try {
|
||||
selfClient.socket.close();
|
||||
|
@ -169,7 +170,10 @@ public class WebSocketProxy extends WebSocketServer {
|
|||
if (!selfClient.authed && data[0] == 13) selfClient.positionPacket = data;
|
||||
boolean loginPacket = data[0] == 1;
|
||||
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) {
|
||||
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
|
||||
|
|
|
@ -13,6 +13,18 @@ origin_blacklist: "https://g.eags.us/eaglercraft/origin_blacklist.txt"
|
|||
origins: []
|
||||
# enable /server command?
|
||||
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
|
||||
auth:
|
||||
# use auth
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user