This commit is contained in:
ayunami2000 2022-07-17 20:57:05 -04:00
parent 1637fc2998
commit 0a58308b80
17 changed files with 59041 additions and 55755 deletions

4
.gitignore vendored
View File

@ -1,9 +1,13 @@
.gradle .gradle
.settings .settings
.idea
build build
bin bin
eaglercraftbungee/.idea
eaglercraftbungee/bin eaglercraftbungee/bin
eaglercraftbungee/rundir eaglercraftbungee/rundir
eaglercraftbungee/test
eaglercraftbungee/minecrafthtml5bungee.iml
epkcompiler/bin epkcompiler/bin
spigot-server/world* spigot-server/world*
eaglercraftbungee/rundir eaglercraftbungee/rundir

View File

@ -39,7 +39,6 @@ import java.util.concurrent.TimeUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.io.IOException; import java.io.IOException;
import jline.UnsupportedTerminal; import jline.UnsupportedTerminal;
import java.io.OutputStream;
import net.md_5.bungee.log.LoggingOutputStream; import net.md_5.bungee.log.LoggingOutputStream;
import java.util.logging.Level; import java.util.logging.Level;
import net.md_5.bungee.log.BungeeLogger; import net.md_5.bungee.log.BungeeLogger;
@ -78,6 +77,7 @@ import net.md_5.bungee.config.YamlConfig;
import net.md_5.bungee.eaglercraft.BanList; import net.md_5.bungee.eaglercraft.BanList;
import net.md_5.bungee.eaglercraft.DomainBlacklist; import net.md_5.bungee.eaglercraft.DomainBlacklist;
import net.md_5.bungee.eaglercraft.PluginEaglerSkins; import net.md_5.bungee.eaglercraft.PluginEaglerSkins;
import net.md_5.bungee.eaglercraft.PluginEaglerVoice;
import net.md_5.bungee.eaglercraft.WebSocketListener; import net.md_5.bungee.eaglercraft.WebSocketListener;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -233,6 +233,7 @@ public class BungeeCord extends ProxyServer {
this.config.load(); this.config.load();
this.pluginManager.detectPlugins(this.pluginsFolder); this.pluginManager.detectPlugins(this.pluginsFolder);
this.pluginManager.addInternalPlugin(new PluginEaglerSkins()); this.pluginManager.addInternalPlugin(new PluginEaglerSkins());
this.pluginManager.addInternalPlugin(new PluginEaglerVoice(this.config.getVoiceEnabled()));
//if(this.config.getAuthInfo().isEnabled()) this.pluginManager.addInternalPlugin(new PluginEaglerAuth()); //if(this.config.getAuthInfo().isEnabled()) this.pluginManager.addInternalPlugin(new PluginEaglerAuth());
if (this.reconnectHandler == null) { if (this.reconnectHandler == null) {
this.reconnectHandler = new SQLReconnectHandler(); this.reconnectHandler = new SQLReconnectHandler();

View File

@ -19,7 +19,7 @@ public class Team {
private Set<String> players; private Set<String> players;
public Collection<String> getPlayers() { public Collection<String> getPlayers() {
return (Collection<String>) Collections.unmodifiableSet((Set<?>) this.players); return (Collection<String>) (Collection) Collections.unmodifiableSet((Set<?>) this.players);
} }
public void addPlayer(final String name) { public void addPlayer(final String name) {

View File

@ -24,6 +24,7 @@ public class Configuration {
private TMap<String, ServerInfo> servers; private TMap<String, ServerInfo> servers;
private AuthServiceInfo authInfo; private AuthServiceInfo authInfo;
private boolean onlineMode; private boolean onlineMode;
private boolean voiceEnabled;
private int playerLimit; private int playerLimit;
private String name; private String name;
private boolean showBanType; private boolean showBanType;
@ -56,6 +57,7 @@ public class Configuration {
} }
this.authInfo = adapter.getAuthSettings(); this.authInfo = adapter.getAuthSettings();
this.onlineMode = false; this.onlineMode = false;
this.voiceEnabled = adapter.getBoolean("voice_enabled", true);
this.playerLimit = adapter.getInt("player_limit", this.playerLimit); this.playerLimit = adapter.getInt("player_limit", this.playerLimit);
this.name = adapter.getString("server_name", EaglercraftBungee.name + " Server"); this.name = adapter.getString("server_name", EaglercraftBungee.name + " Server");
this.showBanType = adapter.getBoolean("display_ban_type_on_kick", false); this.showBanType = adapter.getBoolean("display_ban_type_on_kick", false);
@ -113,6 +115,10 @@ public class Configuration {
return authInfo; return authInfo;
} }
public boolean getVoiceEnabled() {
return voiceEnabled;
}
public String getServerName() { public String getServerName() {
return name; return name;
} }

View File

@ -8,9 +8,8 @@ import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginDescription; import net.md_5.bungee.api.plugin.PluginDescription;
import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventHandler;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets; import java.io.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
@ -19,12 +18,23 @@ import java.util.Collections;
public class PluginEaglerVoice extends Plugin implements Listener { public class PluginEaglerVoice extends Plugin implements Listener {
private final boolean voiceEnabled;
private final Map<String, UserConnection> voicePlayers = new HashMap<>(); private final Map<String, UserConnection> voicePlayers = new HashMap<>();
private final Map<String, ExpiringSet<String>> voiceRequests = new HashMap<>(); private final Map<String, ExpiringSet<String>> voiceRequests = new HashMap<>();
private final Set<String[]> voicePairs = new HashSet<>(); private final Set<String[]> voicePairs = new HashSet<>();
public PluginEaglerVoice() { 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 PluginEaglerVoice(boolean voiceEnabled) {
super(new PluginDescription("EaglerVoice", PluginEaglerVoice.class.getName(), "1.0.0", "ayunami2000", Collections.emptySet(), null)); super(new PluginDescription("EaglerVoice", PluginEaglerVoice.class.getName(), "1.0.0", "ayunami2000", Collections.emptySet(), null));
this.voiceEnabled = voiceEnabled;
} }
public void onLoad() { public void onLoad() {
@ -41,23 +51,54 @@ public class PluginEaglerVoice extends Plugin implements Listener {
@EventHandler @EventHandler
public void onPluginMessage(PluginMessageEvent event) { public void onPluginMessage(PluginMessageEvent event) {
if(event.getSender() instanceof UserConnection && event.getData().length > 0) { synchronized (voicePlayers) {
if (!voiceEnabled) return;
if (event.getSender() instanceof UserConnection && event.getData().length > 0) {
UserConnection connection = (UserConnection) event.getSender(); UserConnection connection = (UserConnection) event.getSender();
String user = connection.getName(); String user = connection.getName();
byte[] msg = event.getData(); byte[] msg = event.getData();
try { try {
if("EAG|VoiceJoin".equals(event.getTag())) { if (!("EAG|Voice".equals(event.getTag()))) return;
DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(msg));
int sig = streamIn.read();
switch (sig) {
case VOICE_SIGNAL_CONNECT:
if (voicePlayers.containsKey(user)) return; // user is already using voice chat if (voicePlayers.containsKey(user)) return; // user is already using voice chat
// send out packet for player joined voice // 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 // 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
for (UserConnection conn : voicePlayers.values()) conn.sendData("EAG|VoiceJoin", user.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_CONNECT);
dos.writeUTF(user);
byte[] out = baos.toByteArray();
for (UserConnection conn : voicePlayers.values()) conn.sendData("EAG|Voice", out);
voicePlayers.put(user, connection); voicePlayers.put(user, connection);
}else if("EAG|VoiceLeave".equals(event.getTag())) { for (String username : voicePlayers.keySet()) sendVoicePlayers(username);
break;
case VOICE_SIGNAL_DISCONNECT:
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
try {
String user2 = streamIn.readUTF();
if (!voicePlayers.containsKey(user2)) return;
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);
voicePlayers.get(user2).sendData("EAG|Voice", baos2.toByteArray());
baos2 = new ByteArrayOutputStream();
dos2 = new DataOutputStream(baos2);
dos2.write(VOICE_SIGNAL_DISCONNECT);
dos2.writeUTF(user2);
connection.sendData("EAG|Voice", baos2.toByteArray());
}
} catch (EOFException e) {
removeUser(user); removeUser(user);
}else if("EAG|VoiceReq".equals(event.getTag())) { }
break;
case VOICE_SIGNAL_REQUEST:
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
String targetUser = new String(msg, StandardCharsets.UTF_8); String targetUser = streamIn.readUTF();
if (user.equals(targetUser)) return; // prevent duplicates if (user.equals(targetUser)) return; // prevent duplicates
if (checkVoicePair(user, targetUser)) return; // already paired if (checkVoicePair(user, targetUser)) return; // already paired
if (!voicePlayers.containsKey(targetUser)) return; // target user is not using voice chat if (!voicePlayers.containsKey(targetUser)) return; // target user is not using voice chat
@ -76,59 +117,59 @@ public class PluginEaglerVoice extends Plugin implements Listener {
if (voiceRequests.get(user).isEmpty()) voiceRequests.remove(user); if (voiceRequests.get(user).isEmpty()) voiceRequests.remove(user);
} }
// send each other add data // send each other add data
voicePairs.add(new String[] { user, targetUser }); voicePairs.add(new String[]{user, targetUser});
JSONObject json = new JSONObject(); ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
json.put("username", user); DataOutputStream dos2 = new DataOutputStream(baos2);
json.put("offer", false); dos2.write(VOICE_SIGNAL_CONNECT);
voicePlayers.get(targetUser).sendData("EAG|VoiceAdd", json.toString().getBytes(StandardCharsets.UTF_8)); dos2.writeUTF(user);
json.put("username", targetUser); dos2.writeBoolean(false);
json.put("offer", true); voicePlayers.get(targetUser).sendData("EAG|Voice", baos2.toByteArray());
connection.sendData("EAG|VoiceAdd", json.toString().getBytes(StandardCharsets.UTF_8)); baos2 = new ByteArrayOutputStream();
dos2 = new DataOutputStream(baos2);
dos2.write(VOICE_SIGNAL_CONNECT);
dos2.writeUTF(targetUser);
dos2.writeBoolean(true);
connection.sendData("EAG|Voice", baos2.toByteArray());
} }
} else if("EAG|VoiceRemove".equals(event.getTag())) { break;
case VOICE_SIGNAL_ICE:
case VOICE_SIGNAL_DESC:
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
String targetUser = new String(msg, StandardCharsets.UTF_8); String targetUser2 = streamIn.readUTF();
if (voicePairs.removeIf(pair -> (pair[0].equals(user) && pair[1].equals(targetUser)) || (pair[0].equals(targetUser) && pair[1].equals(user)))) voicePlayers.get(targetUser).sendData("EAG|VoiceRemove", user.getBytes(StandardCharsets.UTF_8)); if (checkVoicePair(user, targetUser2)) {
}else if("EAG|VoiceIce".equals(event.getTag())) { String data = streamIn.readUTF();
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
JSONObject json = new JSONObject(new String(msg)); DataOutputStream dos2 = new DataOutputStream(baos2);
if (json.has("username") && json.get("username") instanceof String) { dos2.write(sig);
String targetUser = json.getString("username"); dos2.writeUTF(user);
if (checkVoicePair(user, targetUser)) { dos2.writeUTF(data);
if (json.has("ice_candidate")) { voicePlayers.get(targetUser2).sendData("EAG|Voice", baos2.toByteArray());
// todo: limit ice_candidate data length or sanitize it fully
json.keySet().removeIf(s -> !s.equals("ice_candidate"));
json.put("username", user);
voicePlayers.get(targetUser).sendData("EAG|VoiceIce", json.toString().getBytes(StandardCharsets.UTF_8));
} }
break;
default:
break;
} }
} } catch (Throwable t) {
}else if("EAG|VoiceDesc".equals(event.getTag())) {
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
JSONObject json = new JSONObject(new String(msg));
if (json.has("username") && json.get("username") instanceof String) {
String targetUser = json.getString("username");
if (checkVoicePair(user, targetUser)) {
if (json.has("session_description")) {
// todo: limit session_description data length or sanitize it fully
json.keySet().removeIf(s -> !s.equals("session_description"));
json.put("username", user);
voicePlayers.get(targetUser).sendData("EAG|VoiceDesc", json.toString().getBytes(StandardCharsets.UTF_8));
}
}
}
}
}catch(Throwable t) {
// hacker // hacker
t.printStackTrace(); // todo: remove in production // t.printStackTrace(); // todo: remove in production
removeUser(user); removeUser(user);
} }
} }
} }
}
@EventHandler @EventHandler
public void onPostLogin(PostLoginEvent event) { public void onPostLogin(PostLoginEvent event) {
event.getPlayer().sendData("EAG|Voice", new byte[] { }); try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_ALLOWED);
dos.writeBoolean(voiceEnabled);
dos.write(0);
//dos.writeUTF("\"stun:stun.l.google.com:19302\""); // todo: add config controls for ICE servers!
event.getPlayer().sendData("EAG|Voice", baos.toByteArray());
sendVoicePlayers(event.getPlayer().getName());
} catch (IOException ignored) { }
} }
@EventHandler @EventHandler
@ -137,19 +178,58 @@ public class PluginEaglerVoice extends Plugin implements Listener {
removeUser(nm); removeUser(nm);
} }
public void sendVoicePlayers(String name) {
synchronized (voicePlayers) {
if (!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);
voicePlayers.get(name).sendData("EAG|Voice", baos.toByteArray());
}
} catch (IOException ignored) {
}
}
}
public void removeUser(String name) { public void removeUser(String name) {
synchronized (voicePlayers) {
voicePlayers.remove(name); voicePlayers.remove(name);
for (String username : voicePlayers.keySet()) {
if (!name.equals(username)) sendVoicePlayers(username);
}
for (String[] voicePair : voicePairs) { for (String[] voicePair : voicePairs) {
String target = null; String target = null;
if (voicePair[0].equals(name)) { if (voicePair[0].equals(name)) {
target = voicePair[1]; target = voicePair[1];
} else if(voicePair[1].equals(name)) { } else if (voicePair[1].equals(name)) {
target = voicePair[0]; target = voicePair[0];
} }
if (target != null && voicePlayers.containsKey(target)) voicePlayers.get(target).sendData("EAG|VoiceRemove", name.getBytes(StandardCharsets.UTF_8)); if (target != null && voicePlayers.containsKey(target)) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_DISCONNECT);
dos.writeUTF(name);
voicePlayers.get(target).sendData("EAG|Voice", baos.toByteArray());
} catch (IOException ignored) {
}
}
} }
voicePairs.removeIf(pair -> pair[0].equals(name) || pair[1].equals(name)); voicePairs.removeIf(pair -> pair[0].equals(name) || pair[1].equals(name));
} }
}
private boolean checkVoicePair(String user1, String user2) { private 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))); return voicePairs.stream().anyMatch(pair -> (pair[0].equals(user1) && pair[1].equals(user2)) || (pair[0].equals(user2) && pair[1].equals(user1)));

View File

@ -102,7 +102,11 @@ public class WebSocketProxy extends SimpleChannelInboundHandler<ByteBuf> {
ByteBuffer toSend = ByteBuffer.allocateDirect(buffer.capacity()); ByteBuffer toSend = ByteBuffer.allocateDirect(buffer.capacity());
toSend.put(buffer.nioBuffer()); toSend.put(buffer.nioBuffer());
toSend.flip(); toSend.flip();
if (client.isOpen()) {
client.send(toSend); client.send(toSend);
} else {
killConnection();
}
} }
@Override @Override

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
This is the backend for voice channels in eaglercraft, it links with TeaVM EaglerAdapter at runtime This is the backend for voice channels in eaglercraft, it links with TeaVM EaglerAdapter at runtime
Copyright 2022 Calder Young. All rights reserved. Copyright 2022 Calder Young & ayunami2000. All rights reserved.
Based on code written by ayunami2000 Based on code written by ayunami2000
@ -23,27 +23,37 @@ window.initializeVoiceClient = (() => {
class EaglercraftVoicePeer { class EaglercraftVoicePeer {
constructor(client, peerId, peerConnection) { constructor(client, peerId, peerConnection, offer) {
this.client = client; this.client = client;
this.peerId = peerId; this.peerId = peerId;
this.peerConnection = peerConnection; this.peerConnection = peerConnection;
this.stream = null;
const self = this; const self = this;
this.peerConnection.addEventListener("icecandidate", (evt) => { this.peerConnection.addEventListener("icecandidate", (evt) => {
if(evt.candidate) { if(evt.candidate) {
self.client.iceCandidateHandler(self.peerId, evt.candidate.sdpMLineIndex, evt.candidate.candidate.toJSON().stringify()); self.client.iceCandidateHandler(self.peerId, JSON.stringify({ sdpMLineIndex: evt.candidate.sdpMLineIndex, candidate: evt.candidate.candidate }));
} }
}); });
this.peerConnection.addEventListener("track", (evt) => { this.peerConnection.addEventListener("track", (evt) => {
self.client.peerTrackHandler(self.peerId, evt.streams[0]); self.rawStream = evt.streams[0];
const aud = new Audio();
aud.autoplay = true;
aud.muted = true;
aud.onended = function() {
aud.remove();
};
aud.srcObject = self.rawStream;
self.client.peerTrackHandler(self.peerId, self.rawStream);
}); });
this.peerConnection.addStream(this.client.localMediaStream); this.peerConnection.addStream(this.client.localMediaStream.stream);
if (offer) {
this.peerConnection.createOffer((desc) => { this.peerConnection.createOffer((desc) => {
const selfDesc = desc; const selfDesc = desc;
self.peerConnection.setLocalDescription(selfDesc, () => { self.peerConnection.setLocalDescription(selfDesc, () => {
self.client.descriptionHandler(self.peerId, selfDesc.toJSON().stringify()); self.client.descriptionHandler(self.peerId, JSON.stringify(selfDesc));
}, (err) => { }, (err) => {
console.error("Failed to set local description for \"" + self.peerId + "\"! " + err); console.error("Failed to set local description for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId); self.client.signalDisconnect(self.peerId);
@ -52,6 +62,7 @@ window.initializeVoiceClient = (() => {
console.error("Failed to set create offer for \"" + self.peerId + "\"! " + err); console.error("Failed to set create offer for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId); self.client.signalDisconnect(self.peerId);
}); });
}
this.peerConnection.addEventListener("connectionstatechange", (evt) => { this.peerConnection.addEventListener("connectionstatechange", (evt) => {
if(evt.connectionState === 'disconnected') { if(evt.connectionState === 'disconnected') {
@ -65,32 +76,46 @@ window.initializeVoiceClient = (() => {
this.peerConnection.close(); this.peerConnection.close();
} }
mute(muted) {
this.rawStream.getAudioTracks()[0].enabled = !muted;
}
setRemoteDescription(descJSON) { setRemoteDescription(descJSON) {
const self = this; const self = this;
try {
const remoteDesc = JSON.parse(descJSON); const remoteDesc = JSON.parse(descJSON);
this.peerConnection.setRemoteDescription(remoteDesc, () => { this.peerConnection.setRemoteDescription(remoteDesc, () => {
if(remoteDesc.type == 'offer') { if(remoteDesc.type == 'offer') {
self.peerConnection.createAnswer((desc) => { self.peerConnection.createAnswer((desc) => {
const selfDesc = desc; const selfDesc = desc;
self.peerConnection.setLocalDescription(selfDesc, () => { self.peerConnection.setLocalDescription(selfDesc, () => {
self.client.descriptionHandler(self.peerId, selfDesc.toJSON().stringify()); self.client.descriptionHandler(self.peerId, JSON.stringify(selfDesc));
}, (err) => { }, (err) => {
console.error("Failed to set local description for \"" + self.peerId + "\"! " + err); console.error("Failed to set local description for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId); self.client.signalDisconnect(self.peerId);
}); });
}, (err) => { }, (err) => {
console.error("Failed to create answer for \"" + self.peerId + "\"! " + err); console.error("Failed to create answer for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId); self.client.signalDisconnect(self.peerId);
}); });
} }
}, (err) => { }, (err) => {
console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err); console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId); self.client.signalDisconnect(self.peerId);
}); });
} catch (err) {
console.error("Failed to parse remote description for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
}
} }
addICECandidate(candidate) { addICECandidate(candidate) {
this.peerConnection.addICECandidate(new RTCIceCandidate(JSON.parse(candidate))); try {
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
} catch (err) {
console.error("Failed to parse ice candidate for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
}
} }
} }
@ -117,10 +142,14 @@ window.initializeVoiceClient = (() => {
setICEServers(urls) { setICEServers(urls) {
this.ICEServers.length = 0; this.ICEServers.length = 0;
if (urls.length == 0) {
this.ICEServers = [ { urls: "stun:openrelay.metered.ca:80" }, { urls: "turn:openrelay.metered.ca:80", username: "openrelayproject", credential: "openrelayproject" }, { urls: "turn:openrelay.metered.ca:443", username: "openrelayproject", credential: "openrelayproject", }, { urls: "turn:openrelay.metered.ca:443?transport=tcp", username: "openrelayproject", credential: "openrelayproject" } ];
} else {
for(var i = 0; i < urls.length; ++i) { for(var i = 0; i < urls.length; ++i) {
this.ICEServers.push({ urls: urls[i] }); this.ICEServers.push({ urls: urls[i] });
} }
} }
}
setICECandidateHandler(cb) { setICECandidateHandler(cb) {
this.iceCandidateHandler = cb; this.iceCandidateHandler = cb;
@ -139,10 +168,10 @@ window.initializeVoiceClient = (() => {
} }
activateVoice(tk) { activateVoice(tk) {
this.localRawMediaStream.getAudioTracks()[0].enabled = tk; if(this.hasInit) this.localRawMediaStream.getAudioTracks()[0].enabled = tk;
} }
intitializeDevices() { initializeDevices() {
if(!this.hasInit) { if(!this.hasInit) {
this.taskState = TASKSTATE_LOADING; this.taskState = TASKSTATE_LOADING;
const self = this; const self = this;
@ -154,11 +183,12 @@ window.initializeVoiceClient = (() => {
var localStreamIn = self.microphoneVolumeAudioContext.createMediaStreamSource(stream); var localStreamIn = self.microphoneVolumeAudioContext.createMediaStreamSource(stream);
localStreamIn.connect(self.localMediaStreamGain); localStreamIn.connect(self.localMediaStreamGain);
self.localMediaStreamGain.connect(self.localMediaStream); self.localMediaStreamGain.connect(self.localMediaStream);
self.localMediaStreamGain.gain = 1.0; self.localMediaStreamGain.gain.value = 1.0;
self.readyState = READYSTATE_DEVICE_INITIALIZED; self.readyState = READYSTATE_DEVICE_INITIALIZED;
self.taskState = TASKSTATE_COMPLETE; self.taskState = TASKSTATE_COMPLETE;
this.hasInit = true; this.hasInit = true;
}).catch(() => { }).catch((err) => {
console.error(err);
self.readyState = READYSTATE_ABORTED; self.readyState = READYSTATE_ABORTED;
self.taskState = TASKSTATE_FAILED; self.taskState = TASKSTATE_FAILED;
}); });
@ -169,10 +199,12 @@ window.initializeVoiceClient = (() => {
} }
setMicVolume(val) { setMicVolume(val) {
if(this.hasInit) {
if(val > 0.5) val = 0.5 + (val - 0.5) * 2.0; if(val > 0.5) val = 0.5 + (val - 0.5) * 2.0;
if(val > 1.5) val = 1.5; if(val > 1.5) val = 1.5;
if(val < 0.0) val = 0.0; if(val < 0.0) val = 0.0;
self.localMediaStreamGain.gain = val * 2.0; this.localMediaStreamGain.gain.value = val * 2.0;
}
} }
getTaskState() { getTaskState() {
@ -183,9 +215,10 @@ window.initializeVoiceClient = (() => {
return this.readyState; return this.readyState;
} }
signalConnect(peerId) { signalConnect(peerId, offer) {
if (!this.hasInit) initializeDevices();
const peerConnection = new RTCPeerConnection({ iceServers: this.ICEServers, optional: [ { DtlsSrtpKeyAgreement: true } ] }); const peerConnection = new RTCPeerConnection({ iceServers: this.ICEServers, optional: [ { DtlsSrtpKeyAgreement: true } ] });
const peerInstance = new EaglercraftVoicePeer(this, peerId, peerConnection); const peerInstance = new EaglercraftVoicePeer(this, peerId, peerConnection, offer);
this.peerList.set(peerId, peerInstance); this.peerList.set(peerId, peerInstance);
} }
@ -196,14 +229,21 @@ window.initializeVoiceClient = (() => {
} }
} }
signalDisconnect(peerId) { signalDisconnect(peerId, quiet) {
var thePeer = this.peerList.get(peerId); var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) { if((typeof thePeer !== "undefined") && thePeer !== null) {
this.peerList.delete(thePeer); this.peerList.delete(thePeer);
try { try {
thePeer.disconnect(); thePeer.disconnect();
}catch(e) {} }catch(e) {}
this.peerDisconnectHandler(peerId); this.peerDisconnectHandler(peerId, quiet);
}
}
mutePeer(peerId, muted) {
var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) {
thePeer.mute(muted);
} }
} }

View File

@ -0,0 +1,276 @@
"use strict";
/*
This is the backend for voice channels in eaglercraft, it links with TeaVM EaglerAdapter at runtime
Copyright 2022 Calder Young & ayunami2000. All rights reserved.
Based on code written by ayunami2000
*/
window.initializeVoiceClient = (() => {
const READYSTATE_NONE = 0;
const READYSTATE_ABORTED = -1;
const READYSTATE_DEVICE_INITIALIZED = 1;
const TASKSTATE_NONE = -1;
const TASKSTATE_LOADING = 0;
const TASKSTATE_COMPLETE = 1;
const TASKSTATE_FAILED = 2;
class EaglercraftVoicePeer {
constructor(client, peerId, peerConnection, offer) {
this.client = client;
this.peerId = peerId;
this.peerConnection = peerConnection;
this.stream = null;
const self = this;
this.peerConnection.addEventListener("icecandidate", (evt) => {
if(evt.candidate) {
self.client.iceCandidateHandler(self.peerId, JSON.stringify({ sdpMLineIndex: evt.candidate.sdpMLineIndex, candidate: evt.candidate.candidate }));
}
});
this.peerConnection.addEventListener("track", (evt) => {
self.rawStream = evt.streams[0];
const aud = new Audio();
aud.autoplay = true;
aud.muted = true;
aud.onended = function() {
aud.remove();
};
aud.srcObject = self.rawStream;
self.client.peerTrackHandler(self.peerId, self.rawStream);
});
this.peerConnection.addStream(this.client.localMediaStream.stream);
if (offer) {
this.peerConnection.createOffer((desc) => {
const selfDesc = desc;
self.peerConnection.setLocalDescription(selfDesc, () => {
self.client.descriptionHandler(self.peerId, JSON.stringify(selfDesc));
}, (err) => {
console.error("Failed to set local description for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
}, (err) => {
console.error("Failed to set create offer for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
}
this.peerConnection.addEventListener("connectionstatechange", (evt) => {
if(evt.connectionState === 'disconnected') {
self.client.signalDisconnect(self.peerId);
}
});
}
disconnect() {
this.peerConnection.close();
}
mute(muted) {
this.rawStream.getAudioTracks()[0].enabled = !muted;
}
setRemoteDescription(descJSON) {
const self = this;
try {
const remoteDesc = JSON.parse(descJSON);
this.peerConnection.setRemoteDescription(remoteDesc, () => {
if(remoteDesc.type == 'offer') {
self.peerConnection.createAnswer((desc) => {
const selfDesc = desc;
self.peerConnection.setLocalDescription(selfDesc, () => {
self.client.descriptionHandler(self.peerId, JSON.stringify(selfDesc));
}, (err) => {
console.error("Failed to set local description for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
}, (err) => {
console.error("Failed to create answer for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
}
}, (err) => {
console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
} catch (err) {
console.error("Failed to parse remote description for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
}
}
addICECandidate(candidate) {
try {
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
} catch (err) {
console.error("Failed to parse ice candidate for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
}
}
}
class EaglercraftVoiceClient {
constructor() {
this.ICEServers = [];
this.hasInit = false;
this.peerList = new Map();
this.readyState = READYSTATE_NONE;
this.taskState = TASKSTATE_NONE;
this.iceCandidateHandler = null;
this.descriptionHandler = null;
this.peerTrackHandler = null;
this.peerDisconnectHandler = null;
this.peerDisconnectHandlerQuiet = null;
this.microphoneVolumeAudioContext = new AudioContext();
}
voiceClientSupported() {
return typeof window.RTCPeerConnection !== "undefined" && typeof navigator.mediaDevices !== "undefined" &&
typeof navigator.mediaDevices.getUserMedia !== "undefined";
}
setICEServers(urls) {
this.ICEServers.length = 0;
if (urls.length == 0) {
this.ICEServers = [ { urls: "stun:openrelay.metered.ca:80" }, { urls: "turn:openrelay.metered.ca:80", username: "openrelayproject", credential: "openrelayproject" }, { urls: "turn:openrelay.metered.ca:443", username: "openrelayproject", credential: "openrelayproject", }, { urls: "turn:openrelay.metered.ca:443?transport=tcp", username: "openrelayproject", credential: "openrelayproject" } ];
} else {
for(var i = 0; i < urls.length; ++i) {
this.ICEServers.push({ urls: urls[i] });
}
}
}
setICECandidateHandler(cb) {
this.iceCandidateHandler = cb;
}
setDescriptionHandler(cb) {
this.descriptionHandler = cb;
}
setPeerTrackHandler(cb) {
this.peerTrackHandler = cb;
}
setPeerDisconnectHandler(cb) {
this.peerDisconnectHandler = cb;
}
setPeerDisconnectHandlerQuiet(cb) {
this.peerDisconnectHandlerQuiet = cb;
}
activateVoice(tk) {
if(this.hasInit) this.localRawMediaStream.getAudioTracks()[0].enabled = tk;
}
initializeDevices() {
if(!this.hasInit) {
this.taskState = TASKSTATE_LOADING;
const self = this;
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => {
self.localRawMediaStream = stream;
self.localRawMediaStream.getAudioTracks()[0].enabled = false;
self.localMediaStream = self.microphoneVolumeAudioContext.createMediaStreamDestination();
self.localMediaStreamGain = self.microphoneVolumeAudioContext.createGain();
var localStreamIn = self.microphoneVolumeAudioContext.createMediaStreamSource(stream);
localStreamIn.connect(self.localMediaStreamGain);
self.localMediaStreamGain.connect(self.localMediaStream);
self.localMediaStreamGain.gain.value = 1.0;
self.readyState = READYSTATE_DEVICE_INITIALIZED;
self.taskState = TASKSTATE_COMPLETE;
this.hasInit = true;
}).catch((err) => {
console.error(err);
self.readyState = READYSTATE_ABORTED;
self.taskState = TASKSTATE_FAILED;
});
}else {
self.readyState = READYSTATE_DEVICE_INITIALIZED;
self.taskState = TASKSTATE_COMPLETE;
}
}
setMicVolume(val) {
if(this.hasInit) {
if(val > 0.5) val = 0.5 + (val - 0.5) * 2.0;
if(val > 1.5) val = 1.5;
if(val < 0.0) val = 0.0;
this.localMediaStreamGain.gain.value = val * 2.0;
}
}
getTaskState() {
return this.taskState;
}
getReadyState() {
return this.readyState;
}
signalConnect(peerId, offer) {
if (!this.hasInit) initializeDevices();
const peerConnection = new RTCPeerConnection({ iceServers: this.ICEServers, optional: [ { DtlsSrtpKeyAgreement: true } ] });
const peerInstance = new EaglercraftVoicePeer(this, peerId, peerConnection, offer);
this.peerList.set(peerId, peerInstance);
}
signalDescription(peerId, descJSON) {
var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) {
thePeer.setRemoteDescription(descJSON);
}
}
signalDisconnect(peerId, quiet) {
var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) {
this.peerList.delete(thePeer);
try {
thePeer.disconnect();
}catch(e) {}
if (quoet) {
this.peerDisconnectHandlerQuiet(peerId);
} else {
this.peerDisconnectHandler(peerId);
}
}
}
mutePeer(peerId, muted) {
var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) {
thePeer.mute(muted);
}
}
signalICECandidate(peerId, candidate) {
var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) {
thePeer.addICECandidate(candidate);
}
}
}
window.constructVoiceClient = () => new EaglercraftVoiceClient();
});
window.startVoiceClient = () => {
if(typeof window.constructVoiceClient !== "function") {
window.initializeVoiceClient();
}
return window.constructVoiceClient();
};

View File

@ -9,11 +9,17 @@ import java.util.Map;
public class ExpiringSet<T> extends HashSet<T> { public class ExpiringSet<T> extends HashSet<T> {
private final long expiration; private final long expiration;
private final ExpiringEvent<T> event;
private final Map<T, Long> timestamps = new HashMap<>(); private final Map<T, Long> timestamps = new HashMap<>();
public ExpiringSet(long expiration) { public ExpiringSet(long expiration, ExpiringEvent<T> event) {
this.expiration = expiration; this.expiration = expiration;
this.event = event;
}
public interface ExpiringEvent<T> {
void onExpiration(T item);
} }
public void checkForExpirations() { public void checkForExpirations() {
@ -23,6 +29,7 @@ public class ExpiringSet<T> extends HashSet<T> {
T element = iterator.next(); T element = iterator.next();
if (super.contains(element)) { if (super.contains(element)) {
if (this.timestamps.get(element) + this.expiration < now) { if (this.timestamps.get(element) + this.expiration < now) {
this.event.onExpiration(element);
iterator.remove(); iterator.remove();
super.remove(element); super.remove(element);
} }

View File

@ -95,7 +95,6 @@ public class WebsocketNetworkManager implements INetworkManager {
stream.mark(); stream.mark();
try { try {
Packet pkt = Packet.readPacket(packetStream, false); Packet pkt = Packet.readPacket(packetStream, false);
//System.out.println(pkt.toString());
pkt.processPacket(this.netHandler); pkt.processPacket(this.netHandler);
} catch (EOFException e) { } catch (EOFException e) {
stream.reset(); stream.reset();
@ -117,7 +116,6 @@ public class WebsocketNetworkManager implements INetworkManager {
} }
public void serverShutdown() { public void serverShutdown() {
EaglerAdapter.setVoiceStatus(Voice.VoiceStatus.DISCONNECTED);
if(EaglerAdapter.connectionOpen()) { if(EaglerAdapter.connectionOpen()) {
EaglerAdapter.endConnection(); EaglerAdapter.endConnection();
EaglerAdapter.setDebugVar("minecraftServer", "null"); EaglerAdapter.setDebugVar("minecraftServer", "null");
@ -133,7 +131,6 @@ public class WebsocketNetworkManager implements INetworkManager {
} }
public void closeConnections() { public void closeConnections() {
EaglerAdapter.setVoiceStatus(Voice.VoiceStatus.DISCONNECTED);
if(EaglerAdapter.connectionOpen()) { if(EaglerAdapter.connectionOpen()) {
EaglerAdapter.endConnection(); EaglerAdapter.endConnection();
EaglerAdapter.setDebugVar("minecraftServer", "null"); EaglerAdapter.setDebugVar("minecraftServer", "null");

View File

@ -4,7 +4,6 @@ import java.text.DecimalFormat;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import net.minecraft.src.*;
import net.lax1dude.eaglercraft.DefaultSkinRenderer; import net.lax1dude.eaglercraft.DefaultSkinRenderer;
import net.lax1dude.eaglercraft.EaglerAdapter; import net.lax1dude.eaglercraft.EaglerAdapter;
import net.lax1dude.eaglercraft.EaglerProfile; import net.lax1dude.eaglercraft.EaglerProfile;
@ -13,6 +12,9 @@ import net.lax1dude.eaglercraft.GuiScreenEditProfile;
import net.lax1dude.eaglercraft.GuiScreenLicense; import net.lax1dude.eaglercraft.GuiScreenLicense;
import net.lax1dude.eaglercraft.GuiVoiceOverlay; import net.lax1dude.eaglercraft.GuiVoiceOverlay;
import net.lax1dude.eaglercraft.LocalStorageManager; import net.lax1dude.eaglercraft.LocalStorageManager;
import net.lax1dude.eaglercraft.Voice;
import net.minecraft.src.*;
import net.lax1dude.eaglercraft.adapter.Tessellator; import net.lax1dude.eaglercraft.adapter.Tessellator;
import net.lax1dude.eaglercraft.glemu.EffectPipeline; import net.lax1dude.eaglercraft.glemu.EffectPipeline;
import net.lax1dude.eaglercraft.glemu.FixedFunctionShader; import net.lax1dude.eaglercraft.glemu.FixedFunctionShader;
@ -244,6 +246,11 @@ public class Minecraft implements Runnable {
this.ingameGUI = new GuiIngame(this); this.ingameGUI = new GuiIngame(this);
this.voiceOverlay = new GuiVoiceOverlay(this); this.voiceOverlay = new GuiVoiceOverlay(this);
ScaledResolution var2 = new ScaledResolution(this.gameSettings, this.displayWidth, this.displayHeight);
int var3 = var2.getScaledWidth();
int var4 = var2.getScaledHeight();
this.voiceOverlay.setResolution(var3, var4);
//if (this.serverName != null) { //if (this.serverName != null) {
// this.displayGuiScreen(new GuiConnecting(new GuiMainMenu(), this, this.serverName, this.serverPort)); // this.displayGuiScreen(new GuiConnecting(new GuiMainMenu(), this, this.serverName, this.serverPort));
//} else { //} else {
@ -1110,6 +1117,25 @@ public class Minecraft implements Runnable {
GuiMultiplayer.tickRefreshCooldown(); GuiMultiplayer.tickRefreshCooldown();
EaglerAdapter.tickVoice(); EaglerAdapter.tickVoice();
EaglerAdapter.activateVoice(EaglerAdapter.isKeyDown(gameSettings.voicePTTKey));
if (EaglerAdapter.getVoiceStatus() == Voice.VoiceStatus.CONNECTING || EaglerAdapter.getVoiceStatus() == Voice.VoiceStatus.CONNECTED) {
if (EaglerAdapter.getVoiceChannel() == Voice.VoiceChannel.PROXIMITY) {
if (this.theWorld != null && this.thePlayer != null) {
for (Object playerObject : this.theWorld.playerEntities) {
EntityPlayer player = (EntityPlayer) playerObject;
if (player == this.thePlayer) continue;
EaglerAdapter.updateVoicePosition(player.username, player.posX, player.posY + player.getEyeHeight(), player.posZ);
int prox = EaglerAdapter.getVoiceProximity();
// cube
if (Math.abs(thePlayer.posX - player.posX) < prox && Math.abs(thePlayer.posY - player.posY) < prox && Math.abs(thePlayer.posZ - player.posZ) < prox) {
EaglerAdapter.addNearbyPlayer(player.username);
} else {
EaglerAdapter.removeNearbyPlayer(player.username);
}
}
}
}
}
if (this.currentScreen == null || this.currentScreen.allowUserInput) { if (this.currentScreen == null || this.currentScreen.allowUserInput) {
this.mcProfiler.endStartSection("mouse"); this.mcProfiler.endStartSection("mouse");

View File

@ -8,11 +8,9 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import net.lax1dude.eaglercraft.DefaultSkinRenderer; import net.lax1dude.eaglercraft.*;
import net.lax1dude.eaglercraft.EaglerAdapter;
import net.lax1dude.eaglercraft.EaglercraftRandom;
import net.lax1dude.eaglercraft.WebsocketNetworkManager;
import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.RateLimit; import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.RateLimit;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
@ -52,6 +50,14 @@ public class NetClientHandler extends NetHandler {
public NetClientHandler(Minecraft par1Minecraft, String par2Str, int par3) throws IOException { public NetClientHandler(Minecraft par1Minecraft, String par2Str, int par3) throws IOException {
this.mc = par1Minecraft; this.mc = par1Minecraft;
this.netManager = new WebsocketNetworkManager(par2Str, null, this); this.netManager = new WebsocketNetworkManager(par2Str, null, this);
EaglerAdapter.clearVoiceAvailableStatus();
EaglerAdapter.setVoiceSignalHandler(new Consumer<byte[]>() {
@Override
public void accept(byte[] bytes) {
NetClientHandler.this.addToSendQueue(new Packet250CustomPayload("EAG|Voice", bytes));
}
});
if (EaglerAdapter.getVoiceChannel() != Voice.VoiceChannel.NONE) EaglerAdapter.sendInitialVoice();
} }
//public NetClientHandler(Minecraft par1Minecraft, String par2Str, int par3, GuiScreen par4GuiScreen) throws IOException { //public NetClientHandler(Minecraft par1Minecraft, String par2Str, int par3, GuiScreen par4GuiScreen) throws IOException {
@ -1166,6 +1172,8 @@ public class NetClientHandler extends NetHandler {
} catch (IOException var7) { } catch (IOException var7) {
var7.printStackTrace(); var7.printStackTrace();
} }
}else if("EAG|Voice".equals(par1Packet250CustomPayload.channel)) {
EaglerAdapter.handleVoiceSignal(par1Packet250CustomPayload.data);
} }
} }

View File

@ -1,14 +1,10 @@
package net.lax1dude.eaglercraft.adapter; package net.lax1dude.eaglercraft.adapter;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,6 +17,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import net.lax1dude.eaglercraft.*;
import org.json.JSONObject; import org.json.JSONObject;
import org.teavm.interop.Async; import org.teavm.interop.Async;
import org.teavm.interop.AsyncCallback; import org.teavm.interop.AsyncCallback;
@ -40,11 +37,7 @@ import org.teavm.jso.dom.events.KeyboardEvent;
import org.teavm.jso.dom.events.MessageEvent; import org.teavm.jso.dom.events.MessageEvent;
import org.teavm.jso.dom.events.MouseEvent; import org.teavm.jso.dom.events.MouseEvent;
import org.teavm.jso.dom.events.WheelEvent; import org.teavm.jso.dom.events.WheelEvent;
import org.teavm.jso.dom.html.HTMLCanvasElement; import org.teavm.jso.dom.html.*;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
import org.teavm.jso.dom.html.HTMLVideoElement;
import org.teavm.jso.dom.html.HTMLImageElement;
import org.teavm.jso.media.MediaError; import org.teavm.jso.media.MediaError;
import org.teavm.jso.typedarrays.ArrayBuffer; import org.teavm.jso.typedarrays.ArrayBuffer;
import org.teavm.jso.typedarrays.DataView; import org.teavm.jso.typedarrays.DataView;
@ -52,16 +45,7 @@ import org.teavm.jso.typedarrays.Float32Array;
import org.teavm.jso.typedarrays.Int32Array; import org.teavm.jso.typedarrays.Int32Array;
import org.teavm.jso.typedarrays.Uint8Array; import org.teavm.jso.typedarrays.Uint8Array;
import org.teavm.jso.typedarrays.Uint8ClampedArray; import org.teavm.jso.typedarrays.Uint8ClampedArray;
import org.teavm.jso.webaudio.AudioBuffer; import org.teavm.jso.webaudio.*;
import org.teavm.jso.webaudio.AudioBufferSourceNode;
import org.teavm.jso.webaudio.AudioContext;
import org.teavm.jso.webaudio.AudioListener;
import org.teavm.jso.webaudio.DecodeErrorCallback;
import org.teavm.jso.webaudio.DecodeSuccessCallback;
import org.teavm.jso.webaudio.GainNode;
import org.teavm.jso.webaudio.MediaElementAudioSourceNode;
import org.teavm.jso.webaudio.MediaEvent;
import org.teavm.jso.webaudio.PannerNode;
import org.teavm.jso.webgl.WebGLBuffer; import org.teavm.jso.webgl.WebGLBuffer;
import org.teavm.jso.webgl.WebGLFramebuffer; import org.teavm.jso.webgl.WebGLFramebuffer;
import org.teavm.jso.webgl.WebGLProgram; import org.teavm.jso.webgl.WebGLProgram;
@ -72,13 +56,6 @@ import org.teavm.jso.webgl.WebGLUniformLocation;
import org.teavm.jso.websocket.CloseEvent; import org.teavm.jso.websocket.CloseEvent;
import org.teavm.jso.websocket.WebSocket; import org.teavm.jso.websocket.WebSocket;
import net.lax1dude.eaglercraft.AssetRepository;
import net.lax1dude.eaglercraft.Base64;
import net.lax1dude.eaglercraft.EaglerImage;
import net.lax1dude.eaglercraft.EarlyLoadScreen;
import net.lax1dude.eaglercraft.LocalStorageManager;
import net.lax1dude.eaglercraft.ServerQuery;
import net.lax1dude.eaglercraft.Voice;
import net.lax1dude.eaglercraft.adapter.teavm.WebGLQuery; import net.lax1dude.eaglercraft.adapter.teavm.WebGLQuery;
import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray; import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray;
import net.minecraft.src.MathHelper; import net.minecraft.src.MathHelper;
@ -1769,13 +1746,15 @@ public class EaglerAdapterImpl2 {
public static final boolean startConnection(String uri) { public static final boolean startConnection(String uri) {
String res = connectWebSocket(uri); String res = connectWebSocket(uri);
return "fail".equals(res) ? false : true; return !"fail".equals(res);
} }
public static final void endConnection() { public static final void endConnection() {
if(sock == null || sock.getReadyState() == 3) { if(sock == null || sock.getReadyState() == 3) {
sockIsConnecting = false; sockIsConnecting = false;
} }
if(sock != null && !sockIsConnecting) sock.close(); if(sock != null && !sockIsConnecting) sock.close();
enableVoice(Voice.VoiceChannel.NONE);
} }
public static final boolean connectionOpen() { public static final boolean connectionOpen() {
if(sock == null || sock.getReadyState() == 3) { if(sock == null || sock.getReadyState() == 3) {
@ -2011,59 +1990,93 @@ public class EaglerAdapterImpl2 {
} }
//TODO: voice start =======================================================================
// implementation notes - DO NOT access any net.minecraft.* classes from EaglerAdapterImpl2 this time
// implementation notes - Tick all the "for (Object playerObject : Minecraft.getMinecraft().theWorld.playerEntities)" in net.minecraft.client.Minecraft.runTick() or similar
// implementation notes - try to only connect to client in GLOBAL or LOCAL not both // implementation notes - try to only connect to client in GLOBAL or LOCAL not both
// implementation notes - try to only connect to nearby clients, and disconnect once they've been out of range for more then 5-10 seconds
// implementation notes - AGAIN, don't access net.minecraft.* classes from this file
// to ayunami - this is initialized at startup, right before downloadAssetPack
private static EaglercraftVoiceClient voiceClient = null; private static EaglercraftVoiceClient voiceClient = null;
private static boolean voiceAvailableStat = false; private static boolean voiceAvailableStat = false;
private static boolean voiceSignalHandlersInitialized = false; private static boolean voiceSignalHandlersInitialized = false;
// to ayunami - use this as a callback to send packets on the voice signal channel
private static Consumer<byte[]> returnSignalHandler = null; private static Consumer<byte[]> returnSignalHandler = null;
// to ayunami - call this before joining a new server private static final HashMap<String, AnalyserNode> voiceAnalysers = new HashMap<>();
private static final HashMap<String, GainNode> voiceGains = new HashMap<>();
private static final HashMap<String, PannerNode> voicePanners = new HashMap<>();
private static final HashSet<String> nearbyPlayers = new HashSet<>();
public static void clearVoiceAvailableStatus() { public static void clearVoiceAvailableStatus() {
voiceAvailableStat = false; voiceAvailableStat = false;
} }
// to ayunami - use this to set returnSignalHandler when a new NetworkManager is created
public static void setVoiceSignalHandler(Consumer<byte[]> signalHandler) { public static void setVoiceSignalHandler(Consumer<byte[]> signalHandler) {
returnSignalHandler = signalHandler; returnSignalHandler = signalHandler;
} }
public static final int VOICE_SIGNAL_ALLOWED_CLIENTBOUND = 0; public static final int VOICE_SIGNAL_ALLOWED = 0;
public static final int VOICE_SIGNAL_ICE_SERVERBOUND = 1; public static final int VOICE_SIGNAL_REQUEST = 0;
public static final int VOICE_SIGNAL_DESC_SERVERBOUND = 2; public static final int VOICE_SIGNAL_CONNECT = 1;
public static final int VOICE_SIGNAL_DISCONNECT = 2;
public static final int VOICE_SIGNAL_ICE = 3;
public static final int VOICE_SIGNAL_DESC = 4;
public static final int VOICE_SIGNAL_GLOBAL = 5;
// to ayunami - use this to pass voice signal packets
public static void handleVoiceSignal(byte[] data) { public static void handleVoiceSignal(byte[] data) {
try { try {
DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(data)); DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(data));
int sig = streamIn.read(); int sig = streamIn.read();
switch(sig) { switch(sig) {
case VOICE_SIGNAL_ALLOWED_CLIENTBOUND: case VOICE_SIGNAL_GLOBAL:
if (enabledChannel != Voice.VoiceChannel.GLOBAL) return;
String[] voicePlayers = new String[streamIn.readInt()];
for(int i = 0; i < voicePlayers.length; i++) voicePlayers[i] = streamIn.readUTF();
for (String username : voicePlayers) {
// notice that literally everyone except for those already connected using voice chat will receive the request; however, ones using proximity will simply ignore it.
if (!voiceGains.containsKey(username)) addNearbyPlayer(username);
}
break;
case VOICE_SIGNAL_ALLOWED:
voiceAvailableStat = streamIn.readBoolean(); voiceAvailableStat = streamIn.readBoolean();
String[] servs = new String[streamIn.read()]; String[] servs = new String[streamIn.read()];
for(int i = 0; i < servs.length; ++i) { for(int i = 0; i < servs.length; i++) {
servs[i] = streamIn.readUTF(); servs[i] = streamIn.readUTF();
} }
voiceClient.setICEServers(servs); voiceClient.setICEServers(servs);
break; break;
case VOICE_SIGNAL_CONNECT:
String peerId = streamIn.readUTF();
try {
boolean offer = streamIn.readBoolean();
voiceClient.signalConnect(peerId, offer);
} catch (EOFException e) { // this is actually a connect ANNOUNCE, not an absolute "yes please connect" situation
if (enabledChannel == Voice.VoiceChannel.PROXIMITY && !nearbyPlayers.contains(peerId)) return;
// send request to peerId
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_REQUEST);
dos.writeUTF(peerId);
returnSignalHandler.accept(baos.toByteArray());
}
break;
case VOICE_SIGNAL_DISCONNECT:
String peerId2 = streamIn.readUTF();
voiceClient.signalDisconnect(peerId2, true);
break;
case VOICE_SIGNAL_ICE:
String peerId3 = streamIn.readUTF();
String candidate = streamIn.readUTF();
voiceClient.signalICECandidate(peerId3, candidate);
break;
case VOICE_SIGNAL_DESC:
String peerId4 = streamIn.readUTF();
String descJSON = streamIn.readUTF();
voiceClient.signalDescription(peerId4, descJSON);
break;
default: default:
System.err.println("Unknown voice signal packet '" + sig + "'!"); System.err.println("Unknown voice signal packet '" + sig + "'!");
break; break;
} }
}catch(IOException ex) { }catch(IOException ex) {
ex.printStackTrace();
} }
} }
@ -2078,8 +2091,60 @@ public class EaglerAdapterImpl2 {
} }
private static Voice.VoiceChannel enabledChannel = Voice.VoiceChannel.NONE; private static Voice.VoiceChannel enabledChannel = Voice.VoiceChannel.NONE;
// to ayunami - use this to switch channel modes or disable voice public static final void addNearbyPlayer(String username) {
recentlyNearbyPlayers.remove(username);
if (nearbyPlayers.add(username)) {
if (getVoiceStatus() == Voice.VoiceStatus.DISCONNECTED || getVoiceStatus() == Voice.VoiceStatus.UNAVAILABLE) return;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_REQUEST);
dos.writeUTF(username);
returnSignalHandler.accept(baos.toByteArray());
} catch (IOException ignored) { }
}
}
private static final ExpiringSet<String> recentlyNearbyPlayers = new ExpiringSet<>(5000, new ExpiringSet.ExpiringEvent<String>() {
@Override
public void onExpiration(String username) {
if (!nearbyPlayers.contains(username)) voiceClient.signalDisconnect(username, false);
}
});
public static final void removeNearbyPlayer(String username) {
// todo: add 5-10s disconnect delay
if (nearbyPlayers.remove(username)) {
if (getVoiceStatus() == Voice.VoiceStatus.DISCONNECTED || getVoiceStatus() == Voice.VoiceStatus.UNAVAILABLE) return;
recentlyNearbyPlayers.add(username);
}
}
public static final void updateVoicePosition(String username, double x, double y, double z) {
if (voicePanners.containsKey(username)) voicePanners.get(username).setPosition((float) x, (float) y, (float) z);
}
public static final void sendInitialVoice() {
returnSignalHandler.accept(new byte[] { VOICE_SIGNAL_CONNECT });
for (String username : nearbyPlayers) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_REQUEST);
dos.writeUTF(username);
returnSignalHandler.accept(baos.toByteArray());
} catch (IOException ignored) { }
}
}
public static final void enableVoice(Voice.VoiceChannel enable) { public static final void enableVoice(Voice.VoiceChannel enable) {
if (enabledChannel == enable) return;
if (enabledChannel != Voice.VoiceChannel.NONE) {
for (String username : nearbyPlayers) voiceClient.signalDisconnect(username, false);
for (String username : recentlyNearbyPlayers) voiceClient.signalDisconnect(username, false);
nearbyPlayers.clear();
returnSignalHandler.accept(new byte[] { VOICE_SIGNAL_DISCONNECT });
}
enabledChannel = enable; enabledChannel = enable;
if(enable == Voice.VoiceChannel.NONE) { if(enable == Voice.VoiceChannel.NONE) {
talkStatus = false; talkStatus = false;
@ -2088,13 +2153,12 @@ public class EaglerAdapterImpl2 {
voiceSignalHandlersInitialized = true; voiceSignalHandlersInitialized = true;
voiceClient.setICECandidateHandler(new EaglercraftVoiceClient.ICECandidateHandler() { voiceClient.setICECandidateHandler(new EaglercraftVoiceClient.ICECandidateHandler() {
@Override @Override
public void call(String peerId, String sdpMLineIndex, String candidate) { public void call(String peerId, String candidate) {
try { try {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dat = new DataOutputStream(bos); DataOutputStream dat = new DataOutputStream(bos);
dat.write(VOICE_SIGNAL_ICE_SERVERBOUND); dat.write(VOICE_SIGNAL_ICE);
dat.writeUTF(peerId); dat.writeUTF(peerId);
dat.writeUTF(sdpMLineIndex);
dat.writeUTF(candidate); dat.writeUTF(candidate);
returnSignalHandler.accept(bos.toByteArray()); returnSignalHandler.accept(bos.toByteArray());
}catch(IOException ex) { }catch(IOException ex) {
@ -2107,7 +2171,7 @@ public class EaglerAdapterImpl2 {
try { try {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dat = new DataOutputStream(bos); DataOutputStream dat = new DataOutputStream(bos);
dat.write(VOICE_SIGNAL_DESC_SERVERBOUND); dat.write(VOICE_SIGNAL_DESC);
dat.writeUTF(peerId); dat.writeUTF(peerId);
dat.writeUTF(candidate); dat.writeUTF(candidate);
returnSignalHandler.accept(bos.toByteArray()); returnSignalHandler.accept(bos.toByteArray());
@ -2115,8 +2179,74 @@ public class EaglerAdapterImpl2 {
} }
} }
}); });
voiceClient.setPeerTrackHandler(new EaglercraftVoiceClient.PeerTrackHandler() {
@Override
public void call(String peerId, MediaStream audioStream) {
if (enabledChannel == Voice.VoiceChannel.NONE) return;
MediaStreamAudioSourceNode audioNode = audioctx.createMediaStreamSource(audioStream);
AnalyserNode analyser = audioctx.createAnalyser();
analyser.setSmoothingTimeConstant(0f);
analyser.setFftSize(32);
audioNode.connect(analyser);
voiceAnalysers.put(peerId, analyser);
if (enabledChannel == Voice.VoiceChannel.GLOBAL) {
GainNode gain = audioctx.createGain();
gain.getGain().setValue(getVoiceListenVolume());
analyser.connect(gain);
gain.connect(audioctx.getDestination());
voiceGains.put(peerId, gain);
} else if (enabledChannel == Voice.VoiceChannel.PROXIMITY) {
PannerNode panner = audioctx.createPanner();
panner.setRolloffFactor(1f);
panner.setDistanceModel("linear");
panner.setPanningModel("HRTF");
panner.setConeInnerAngle(360f);
panner.setConeOuterAngle(0f);
panner.setConeOuterGain(0f);
panner.setOrientation(0f, 1f, 0f);
panner.setPosition(0, 0, 0);
float vol = getVoiceListenVolume();
panner.setMaxDistance(vol * getVoiceProximity() + 0.1f);
GainNode gain = audioctx.createGain();
gain.getGain().setValue(vol);
analyser.connect(gain);
gain.connect(panner);
panner.connect(audioctx.getDestination());
voiceGains.put(peerId, gain);
voicePanners.put(peerId, panner);
}
}
});
voiceClient.setPeerDisconnectHandler(new EaglercraftVoiceClient.PeerDisconnectHandler() {
@Override
public void call(String peerId, boolean quiet) {
if (voiceAnalysers.containsKey(peerId)) {
voiceAnalysers.get(peerId).disconnect();
voiceAnalysers.remove(peerId);
}
if (voiceGains.containsKey(peerId)) {
voiceGains.get(peerId).disconnect();
voiceGains.remove(peerId);
}
if (voicePanners.containsKey(peerId)) {
voicePanners.get(peerId).disconnect();
voicePanners.remove(peerId);
}
if (!quiet) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dat = new DataOutputStream(bos);
dat.write(VOICE_SIGNAL_DISCONNECT);
dat.writeUTF(peerId);
returnSignalHandler.accept(bos.toByteArray());
} catch (IOException ex) {
}
}
}
});
voiceClient.initializeDevices(); voiceClient.initializeDevices();
} }
sendInitialVoice();
} }
} }
public static final Voice.VoiceChannel getVoiceChannel() { public static final Voice.VoiceChannel getVoiceChannel() {
@ -2128,7 +2258,6 @@ public class EaglerAdapterImpl2 {
Voice.VoiceStatus.CONNECTING : Voice.VoiceStatus.CONNECTED); Voice.VoiceStatus.CONNECTING : Voice.VoiceStatus.CONNECTED);
} }
// to ayunami - push to talk in the JS works afaik
private static boolean talkStatus = false; private static boolean talkStatus = false;
public static final void activateVoice(boolean talk) { public static final void activateVoice(boolean talk) {
if(talkStatus != talk) { if(talkStatus != talk) {
@ -2137,7 +2266,6 @@ public class EaglerAdapterImpl2 {
talkStatus = talk; talkStatus = talk;
} }
// to ayunami - not currently used in the javascript but is used by GUI and gameSettings
private static int proximity = 16; private static int proximity = 16;
public static final void setVoiceProximity(int prox) { public static final void setVoiceProximity(int prox) {
proximity = prox; proximity = prox;
@ -2146,16 +2274,21 @@ public class EaglerAdapterImpl2 {
return proximity; return proximity;
} }
// to ayunami - iterate all AudioNodes from PeerTrackHandler players and adjust their gain here
private static float volumeListen = 0.5f; private static float volumeListen = 0.5f;
public static final void setVoiceListenVolume(float f) { public static final void setVoiceListenVolume(float f) {
for (GainNode gain : voiceGains.values()) {
float val = f;
if(val > 0.5) val = 0.5f + (val - 0.5f) * 2.0f;
if(val > 1.5) val = 1.5f;
if(val < 0.0) val = 0.0f;
gain.getGain().setValue(val * 3.0f);
}
volumeListen = f; volumeListen = f;
} }
public static final float getVoiceListenVolume() { public static final float getVoiceListenVolume() {
return volumeListen; return volumeListen;
} }
// to ayunami - this is already implemented
private static float volumeSpeak = 0.5f; private static float volumeSpeak = 0.5f;
public static final void setVoiceSpeakVolume(float f) { public static final void setVoiceSpeakVolume(float f) {
if(volumeSpeak != f) { if(volumeSpeak != f) {
@ -2167,19 +2300,16 @@ public class EaglerAdapterImpl2 {
return volumeSpeak; return volumeSpeak;
} }
// to ayunami - this is used to make the ingame GUI display who is speaking
// I also already programmed a speaker icon above player name tags of players in "getVoiceSpeaking()"
private static final Set<String> mutedSet = new HashSet(); private static final Set<String> mutedSet = new HashSet();
private static final Set<String> emptySet = new HashSet(); private static final Set<String> speakingSet = new HashSet();
private static final List<String> emptyLst = new ArrayList();
public static final Set<String> getVoiceListening() { public static final Set<String> getVoiceListening() {
return emptySet; return voiceGains.keySet();
} }
public static final Set<String> getVoiceSpeaking() { public static final Set<String> getVoiceSpeaking() {
return emptySet; return speakingSet;
} }
public static final void setVoiceMuted(String username, boolean mute) { public static final void setVoiceMuted(String username, boolean mute) {
voiceClient.mutePeer(username, mute);
if(mute) { if(mute) {
mutedSet.add(username); mutedSet.add(username);
}else { }else {
@ -2190,16 +2320,25 @@ public class EaglerAdapterImpl2 {
return mutedSet; return mutedSet;
} }
public static final List<String> getVoiceRecent() { public static final List<String> getVoiceRecent() {
return emptyLst; return new ArrayList<>(voiceGains.keySet());
} }
// to ayunami - use this to clean up that ExpiringSet class you made
public static final void tickVoice() { public static final void tickVoice() {
recentlyNearbyPlayers.checkForExpirations();
for (String username : voiceAnalysers.keySet()) {
AnalyserNode analyser = voiceAnalysers.get(username);
Uint8Array array = Uint8Array.create(analyser.getFrequencyBinCount());
analyser.getByteFrequencyData(array);
int len = array.getLength();
speakingSet.remove(username);
for (int i = 0; i < len; i++) {
if (array.get(i) >= 0.1f) {
speakingSet.add(username);
break;
}
}
}
} }
//TODO: voice end ========================================================
public static final void doJavascriptCoroutines() { public static final void doJavascriptCoroutines() {

View File

@ -2,7 +2,7 @@ package net.lax1dude.eaglercraft.adapter.teavm;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.webaudio.MediaStreamAudioSourceNode; import org.teavm.jso.webaudio.MediaStream;
public interface EaglercraftVoiceClient extends JSObject { public interface EaglercraftVoiceClient extends JSObject {
@ -19,40 +19,37 @@ public interface EaglercraftVoiceClient extends JSObject {
void initializeDevices(); void initializeDevices();
// to ayunami - allow the server to tell the client what to put here
void setICEServers(String[] urls); void setICEServers(String[] urls);
// to ayunami - this is the equivalent of your "EAG|VoiceIce" callback
void setICECandidateHandler(ICECandidateHandler callback); void setICECandidateHandler(ICECandidateHandler callback);
// to ayunami - this is the equivalent of your "EAG|VoiceDesc" callback
void setDescriptionHandler(DescriptionHandler callback); void setDescriptionHandler(DescriptionHandler callback);
// to ayunami - this returns a "MediaStreamAudioSourceNode" for new peers
void setPeerTrackHandler(PeerTrackHandler callback); void setPeerTrackHandler(PeerTrackHandler callback);
// to ayunami - this is called when a peer disconnects (so you can remove their MediaStreamAudioSourceNode and stuff)
void setPeerDisconnectHandler(PeerDisconnectHandler callback); void setPeerDisconnectHandler(PeerDisconnectHandler callback);
void activateVoice(boolean active); void activateVoice(boolean active);
void setMicVolume(float volume); void setMicVolume(float volume);
void mutePeer(String peerId, boolean muted);
int getTaskState(); int getTaskState();
int getReadyState(); int getReadyState();
int signalConnect(String peerId); int signalConnect(String peerId, boolean offer);
int signalDescription(String peerId, String description); int signalDescription(String peerId, String description);
int signalDisconnect(String peerId); int signalDisconnect(String peerId, boolean quiet);
int signalICECandidate(String peerId, String candidate); int signalICECandidate(String peerId, String candidate);
@JSFunctor @JSFunctor
public static interface ICECandidateHandler extends JSObject { public static interface ICECandidateHandler extends JSObject {
void call(String peerId, String sdpMLineIndex, String candidate); void call(String peerId, String candidate);
} }
@JSFunctor @JSFunctor
@ -62,12 +59,12 @@ public interface EaglercraftVoiceClient extends JSObject {
@JSFunctor @JSFunctor
public static interface PeerTrackHandler extends JSObject { public static interface PeerTrackHandler extends JSObject {
void call(String peerId, MediaStreamAudioSourceNode candidate); void call(String peerId, MediaStream audioNode);
} }
@JSFunctor @JSFunctor
public static interface PeerDisconnectHandler extends JSObject { public static interface PeerDisconnectHandler extends JSObject {
void call(String peerId); void call(String peerId, boolean quiet);
} }
} }