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

6
.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
@ -13,4 +17,4 @@ stable-download/java/spigot_command/world_the_end/*
stable-download/java/spigot_command/server.log stable-download/java/spigot_command/server.log
stable-download/java/bungee_command/proxy* stable-download/java/bungee_command/proxy*
stable-download/web_ stable-download/web_
lwjgl-rundir/_eagstorage* lwjgl-rundir/_eagstorage*

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,94 +51,125 @@ 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) {
UserConnection connection = (UserConnection) event.getSender(); if (!voiceEnabled) return;
String user = connection.getName(); if (event.getSender() instanceof UserConnection && event.getData().length > 0) {
byte[] msg = event.getData(); UserConnection connection = (UserConnection) event.getSender();
try { String user = connection.getName();
if("EAG|VoiceJoin".equals(event.getTag())) { byte[] msg = event.getData();
if (voicePlayers.containsKey(user)) return; // user is already using voice chat try {
// send out packet for player joined voice if (!("EAG|Voice".equals(event.getTag()))) return;
// 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 DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(msg));
for (UserConnection conn : voicePlayers.values()) conn.sendData("EAG|VoiceJoin", user.getBytes(StandardCharsets.UTF_8)); int sig = streamIn.read();
voicePlayers.put(user, connection); switch (sig) {
}else if("EAG|VoiceLeave".equals(event.getTag())) { case VOICE_SIGNAL_CONNECT:
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat if (voicePlayers.containsKey(user)) return; // user is already using voice chat
removeUser(user); // send out packet for player joined voice
}else if("EAG|VoiceReq".equals(event.getTag())) { // 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
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat ByteArrayOutputStream baos = new ByteArrayOutputStream();
String targetUser = new String(msg, StandardCharsets.UTF_8); DataOutputStream dos = new DataOutputStream(baos);
if (user.equals(targetUser)) return; // prevent duplicates dos.write(VOICE_SIGNAL_CONNECT);
if (checkVoicePair(user, targetUser)) return; // already paired dos.writeUTF(user);
if (!voicePlayers.containsKey(targetUser)) return; // target user is not using voice chat byte[] out = baos.toByteArray();
if (!voiceRequests.containsKey(user)) voiceRequests.put(user, new ExpiringSet<>(2000)); for (UserConnection conn : voicePlayers.values()) conn.sendData("EAG|Voice", out);
if (voiceRequests.get(user).contains(targetUser)) return; voicePlayers.put(user, connection);
voiceRequests.get(user).add(targetUser); for (String username : voicePlayers.keySet()) sendVoicePlayers(username);
break;
case VOICE_SIGNAL_DISCONNECT:
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);
}
break;
case VOICE_SIGNAL_REQUEST:
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
String targetUser = streamIn.readUTF();
if (user.equals(targetUser)) return; // prevent duplicates
if (checkVoicePair(user, targetUser)) return; // already paired
if (!voicePlayers.containsKey(targetUser)) return; // target user is not using voice chat
if (!voiceRequests.containsKey(user)) voiceRequests.put(user, new ExpiringSet<>(2000));
if (voiceRequests.get(user).contains(targetUser)) return;
voiceRequests.get(user).add(targetUser);
// check if other has requested earlier // check if other has requested earlier
if (voiceRequests.containsKey(targetUser) && voiceRequests.get(targetUser).contains(user)) { if (voiceRequests.containsKey(targetUser) && voiceRequests.get(targetUser).contains(user)) {
if (voiceRequests.containsKey(targetUser)) { if (voiceRequests.containsKey(targetUser)) {
voiceRequests.get(targetUser).remove(user); voiceRequests.get(targetUser).remove(user);
if (voiceRequests.get(targetUser).isEmpty()) voiceRequests.remove(targetUser); if (voiceRequests.get(targetUser).isEmpty()) voiceRequests.remove(targetUser);
} }
if (voiceRequests.containsKey(user)) { if (voiceRequests.containsKey(user)) {
voiceRequests.get(user).remove(targetUser); voiceRequests.get(user).remove(targetUser);
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);
} else if("EAG|VoiceRemove".equals(event.getTag())) { dos2.write(VOICE_SIGNAL_CONNECT);
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat dos2.writeUTF(targetUser);
String targetUser = new String(msg, StandardCharsets.UTF_8); dos2.writeBoolean(true);
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)); connection.sendData("EAG|Voice", baos2.toByteArray());
}else if("EAG|VoiceIce".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("ice_candidate")) {
// 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;
} case VOICE_SIGNAL_ICE:
}else if("EAG|VoiceDesc".equals(event.getTag())) { 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
JSONObject json = new JSONObject(new String(msg)); String targetUser2 = streamIn.readUTF();
if (json.has("username") && json.get("username") instanceof String) { if (checkVoicePair(user, targetUser2)) {
String targetUser = json.getString("username"); String data = streamIn.readUTF();
if (checkVoicePair(user, targetUser)) { ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
if (json.has("session_description")) { DataOutputStream dos2 = new DataOutputStream(baos2);
// todo: limit session_description data length or sanitize it fully dos2.write(sig);
json.keySet().removeIf(s -> !s.equals("session_description")); dos2.writeUTF(user);
json.put("username", user); dos2.writeUTF(data);
voicePlayers.get(targetUser).sendData("EAG|VoiceDesc", json.toString().getBytes(StandardCharsets.UTF_8)); voicePlayers.get(targetUser2).sendData("EAG|Voice", baos2.toByteArray());
} }
} break;
default:
break;
} }
} catch (Throwable t) {
// hacker
// t.printStackTrace(); // todo: remove in production
removeUser(user);
} }
}catch(Throwable t) {
// hacker
t.printStackTrace(); // todo: remove in production
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,18 +178,57 @@ public class PluginEaglerVoice extends Plugin implements Listener {
removeUser(nm); removeUser(nm);
} }
public void removeUser(String name) { public void sendVoicePlayers(String name) {
voicePlayers.remove(name); synchronized (voicePlayers) {
for (String[] voicePair : voicePairs) { if (!voiceEnabled) return;
String target = null; if (!voicePlayers.containsKey(name)) return;
if (voicePair[0].equals(name)) { try {
target = voicePair[1]; ByteArrayOutputStream baos = new ByteArrayOutputStream();
} else if(voicePair[1].equals(name)) { DataOutputStream dos = new DataOutputStream(baos);
target = voicePair[0]; 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) {
} }
if (target != null && voicePlayers.containsKey(target)) voicePlayers.get(target).sendData("EAG|VoiceRemove", name.getBytes(StandardCharsets.UTF_8));
} }
voicePairs.removeIf(pair -> pair[0].equals(name) || pair[1].equals(name)); }
public 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);
voicePlayers.get(target).sendData("EAG|Voice", baos.toByteArray());
} catch (IOException ignored) {
}
}
}
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) {

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();
client.send(toSend); if (client.isOpen()) {
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,35 +23,46 @@ 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);
this.peerConnection.createOffer((desc) => { if (offer) {
const selfDesc = desc; this.peerConnection.createOffer((desc) => {
self.peerConnection.setLocalDescription(selfDesc, () => { const selfDesc = desc;
self.client.descriptionHandler(self.peerId, selfDesc.toJSON().stringify()); 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) => { }, (err) => {
console.error("Failed to set local description 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);
}); });
}, (err) => { }
console.error("Failed to set create offer for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
this.peerConnection.addEventListener("connectionstatechange", (evt) => { this.peerConnection.addEventListener("connectionstatechange", (evt) => {
if(evt.connectionState === 'disconnected') { if(evt.connectionState === 'disconnected') {
@ -64,33 +75,47 @@ window.initializeVoiceClient = (() => {
disconnect() { disconnect() {
this.peerConnection.close(); this.peerConnection.close();
} }
mute(muted) {
this.rawStream.getAudioTracks()[0].enabled = !muted;
}
setRemoteDescription(descJSON) { setRemoteDescription(descJSON) {
const self = this; const self = this;
const remoteDesc = JSON.parse(descJSON); try {
this.peerConnection.setRemoteDescription(remoteDesc, () => { const remoteDesc = JSON.parse(descJSON);
if(remoteDesc.type == 'offer') { this.peerConnection.setRemoteDescription(remoteDesc, () => {
self.peerConnection.createAnswer((desc) => { if(remoteDesc.type == 'offer') {
const selfDesc = desc; self.peerConnection.createAnswer((desc) => {
self.peerConnection.setLocalDescription(selfDesc, () => { const selfDesc = desc;
self.client.descriptionHandler(self.peerId, selfDesc.toJSON().stringify()); 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) => { }, (err) => {
console.error("Failed to set local description for \"" + self.peerId + "\"! " + err); console.error("Failed to create answer for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId); self.client.signalDisconnect(self.peerId);
}); });
}, (err) => { }
console.error("Failed to create answer for \"" + self.peerId + "\"! " + err); }, (err) => {
self.signalDisconnect(peerId); console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err);
}); self.client.signalDisconnect(self.peerId);
} });
}, (err) => { } catch (err) {
console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err); console.error("Failed to parse remote description for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId); 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,8 +142,12 @@ window.initializeVoiceClient = (() => {
setICEServers(urls) { setICEServers(urls) {
this.ICEServers.length = 0; this.ICEServers.length = 0;
for(var i = 0; i < urls.length; ++i) { if (urls.length == 0) {
this.ICEServers.push({ urls: urls[i] }); 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] });
}
} }
} }
@ -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(val > 0.5) val = 0.5 + (val - 0.5) * 2.0; if(this.hasInit) {
if(val > 1.5) val = 1.5; if(val > 0.5) val = 0.5 + (val - 0.5) * 2.0;
if(val < 0.0) val = 0.0; if(val > 1.5) val = 1.5;
self.localMediaStreamGain.gain = val * 2.0; if(val < 0.0) val = 0.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

@ -30,7 +30,7 @@ public class WebsocketNetworkManager implements INetworkManager {
public void setNetHandler(NetHandler netHandler) { public void setNetHandler(NetHandler netHandler) {
this.netHandler = netHandler; this.netHandler = netHandler;
} }
private ByteArrayOutputStream sendBuffer = new ByteArrayOutputStream(); private ByteArrayOutputStream sendBuffer = new ByteArrayOutputStream();
public void addToSendQueue(Packet var1) { public void addToSendQueue(Packet var1) {
@ -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) {
@ -2010,60 +1989,94 @@ public class EaglerAdapterImpl2 {
public static final void openConsole() { public static final void openConsole() {
} }
//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;
// to ayunami - use this to pass voice signal packets public static final int VOICE_SIGNAL_ICE = 3;
public static final int VOICE_SIGNAL_DESC = 4;
public static final int VOICE_SIGNAL_GLOBAL = 5;
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:
voiceAvailableStat = streamIn.readBoolean(); if (enabledChannel != Voice.VoiceChannel.GLOBAL) return;
String[] servs = new String[streamIn.read()]; String[] voicePlayers = new String[streamIn.readInt()];
for(int i = 0; i < servs.length; ++i) { for(int i = 0; i < voicePlayers.length; i++) voicePlayers[i] = streamIn.readUTF();
servs[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.
voiceClient.setICEServers(servs); if (!voiceGains.containsKey(username)) addNearbyPlayer(username);
break; }
default: break;
System.err.println("Unknown voice signal packet '" + sig + "'!"); case VOICE_SIGNAL_ALLOWED:
break; voiceAvailableStat = streamIn.readBoolean();
String[] servs = new String[streamIn.read()];
for(int i = 0; i < servs.length; i++) {
servs[i] = streamIn.readUTF();
}
voiceClient.setICEServers(servs);
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:
System.err.println("Unknown voice signal packet '" + sig + "'!");
break;
} }
}catch(IOException ex) { }catch(IOException ex) {
ex.printStackTrace();
} }
} }
@ -2077,24 +2090,75 @@ public class EaglerAdapterImpl2 {
return false; return false;
} }
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;
}else { }else {
if(!voiceSignalHandlersInitialized) { if(!voiceSignalHandlersInitialized) {
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() {
@ -2127,8 +2257,7 @@ public class EaglerAdapterImpl2 {
(voiceClient.getReadyState() != EaglercraftVoiceClient.READYSTATE_DEVICE_INITIALIZED ? (voiceClient.getReadyState() != EaglercraftVoiceClient.READYSTATE_DEVICE_INITIALIZED ?
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) {
@ -2136,8 +2265,7 @@ 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;
@ -2145,17 +2273,22 @@ public class EaglerAdapterImpl2 {
public static final int getVoiceProximity() { public static final int getVoiceProximity() {
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) {
@ -2166,20 +2299,17 @@ public class EaglerAdapterImpl2 {
public static final float getVoiceSpeakVolume() { public static final float getVoiceSpeakVolume() {
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,18 +2320,27 @@ 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);
} }
} }