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
.settings
.idea
build
bin
eaglercraftbungee/.idea
eaglercraftbungee/bin
eaglercraftbungee/rundir
eaglercraftbungee/test
eaglercraftbungee/minecrafthtml5bungee.iml
epkcompiler/bin
spigot-server/world*
eaglercraftbungee/rundir

View File

@ -39,7 +39,6 @@ import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.io.IOException;
import jline.UnsupportedTerminal;
import java.io.OutputStream;
import net.md_5.bungee.log.LoggingOutputStream;
import java.util.logging.Level;
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.DomainBlacklist;
import net.md_5.bungee.eaglercraft.PluginEaglerSkins;
import net.md_5.bungee.eaglercraft.PluginEaglerVoice;
import net.md_5.bungee.eaglercraft.WebSocketListener;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -233,6 +233,7 @@ public class BungeeCord extends ProxyServer {
this.config.load();
this.pluginManager.detectPlugins(this.pluginsFolder);
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.reconnectHandler == null) {
this.reconnectHandler = new SQLReconnectHandler();

View File

@ -19,7 +19,7 @@ public class Team {
private Set<String> players;
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) {

View File

@ -24,6 +24,7 @@ public class Configuration {
private TMap<String, ServerInfo> servers;
private AuthServiceInfo authInfo;
private boolean onlineMode;
private boolean voiceEnabled;
private int playerLimit;
private String name;
private boolean showBanType;
@ -56,6 +57,7 @@ public class Configuration {
}
this.authInfo = adapter.getAuthSettings();
this.onlineMode = false;
this.voiceEnabled = adapter.getBoolean("voice_enabled", true);
this.playerLimit = adapter.getInt("player_limit", this.playerLimit);
this.name = adapter.getString("server_name", EaglercraftBungee.name + " Server");
this.showBanType = adapter.getBoolean("display_ban_type_on_kick", false);
@ -113,6 +115,10 @@ public class Configuration {
return authInfo;
}
public boolean getVoiceEnabled() {
return voiceEnabled;
}
public String getServerName() {
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.PluginDescription;
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.HashSet;
import java.util.Map;
@ -19,12 +18,23 @@ import java.util.Collections;
public class PluginEaglerVoice extends Plugin implements Listener {
private final boolean voiceEnabled;
private final Map<String, UserConnection> voicePlayers = new HashMap<>();
private final Map<String, ExpiringSet<String>> voiceRequests = new HashMap<>();
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));
this.voiceEnabled = voiceEnabled;
}
public void onLoad() {
@ -41,94 +51,125 @@ public class PluginEaglerVoice extends Plugin implements Listener {
@EventHandler
public void onPluginMessage(PluginMessageEvent event) {
if(event.getSender() instanceof UserConnection && event.getData().length > 0) {
UserConnection connection = (UserConnection) event.getSender();
String user = connection.getName();
byte[] msg = event.getData();
try {
if("EAG|VoiceJoin".equals(event.getTag())) {
if (voicePlayers.containsKey(user)) return; // user is already using voice chat
// send out packet for player joined voice
// notice: everyone on the server can see this packet!! however, it doesn't do anything but let clients know that the player has turned on voice chat
for (UserConnection conn : voicePlayers.values()) conn.sendData("EAG|VoiceJoin", user.getBytes(StandardCharsets.UTF_8));
voicePlayers.put(user, connection);
}else if("EAG|VoiceLeave".equals(event.getTag())) {
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
removeUser(user);
}else if("EAG|VoiceReq".equals(event.getTag())) {
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
String targetUser = new String(msg, StandardCharsets.UTF_8);
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);
synchronized (voicePlayers) {
if (!voiceEnabled) return;
if (event.getSender() instanceof UserConnection && event.getData().length > 0) {
UserConnection connection = (UserConnection) event.getSender();
String user = connection.getName();
byte[] msg = event.getData();
try {
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
// send out packet for player joined voice
// notice: everyone on the server can see this packet!! however, it doesn't do anything but let clients know that the player has turned on voice chat
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(VOICE_SIGNAL_CONNECT);
dos.writeUTF(user);
byte[] out = baos.toByteArray();
for (UserConnection conn : voicePlayers.values()) conn.sendData("EAG|Voice", out);
voicePlayers.put(user, connection);
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
if (voiceRequests.containsKey(targetUser) && voiceRequests.get(targetUser).contains(user)) {
if (voiceRequests.containsKey(targetUser)) {
voiceRequests.get(targetUser).remove(user);
if (voiceRequests.get(targetUser).isEmpty()) voiceRequests.remove(targetUser);
}
if (voiceRequests.containsKey(user)) {
voiceRequests.get(user).remove(targetUser);
if (voiceRequests.get(user).isEmpty()) voiceRequests.remove(user);
}
// send each other add data
voicePairs.add(new String[] { user, targetUser });
JSONObject json = new JSONObject();
json.put("username", user);
json.put("offer", false);
voicePlayers.get(targetUser).sendData("EAG|VoiceAdd", json.toString().getBytes(StandardCharsets.UTF_8));
json.put("username", targetUser);
json.put("offer", true);
connection.sendData("EAG|VoiceAdd", json.toString().getBytes(StandardCharsets.UTF_8));
}
} else if("EAG|VoiceRemove".equals(event.getTag())) {
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
String targetUser = new String(msg, StandardCharsets.UTF_8);
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));
}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));
// check if other has requested earlier
if (voiceRequests.containsKey(targetUser) && voiceRequests.get(targetUser).contains(user)) {
if (voiceRequests.containsKey(targetUser)) {
voiceRequests.get(targetUser).remove(user);
if (voiceRequests.get(targetUser).isEmpty()) voiceRequests.remove(targetUser);
}
if (voiceRequests.containsKey(user)) {
voiceRequests.get(user).remove(targetUser);
if (voiceRequests.get(user).isEmpty()) voiceRequests.remove(user);
}
// send each other add data
voicePairs.add(new String[]{user, targetUser});
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
DataOutputStream dos2 = new DataOutputStream(baos2);
dos2.write(VOICE_SIGNAL_CONNECT);
dos2.writeUTF(user);
dos2.writeBoolean(false);
voicePlayers.get(targetUser).sendData("EAG|Voice", baos2.toByteArray());
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|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));
break;
case VOICE_SIGNAL_ICE:
case VOICE_SIGNAL_DESC:
if (!voicePlayers.containsKey(user)) return; // user is not using voice chat
String targetUser2 = streamIn.readUTF();
if (checkVoicePair(user, targetUser2)) {
String data = streamIn.readUTF();
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
DataOutputStream dos2 = new DataOutputStream(baos2);
dos2.write(sig);
dos2.writeUTF(user);
dos2.writeUTF(data);
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
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
@ -137,18 +178,57 @@ public class PluginEaglerVoice extends Plugin implements Listener {
removeUser(nm);
}
public void removeUser(String name) {
voicePlayers.remove(name);
for (String[] voicePair : voicePairs) {
String target = null;
if (voicePair[0].equals(name)) {
target = voicePair[1];
} else if(voicePair[1].equals(name)) {
target = voicePair[0];
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) {
}
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) {

View File

@ -102,7 +102,11 @@ public class WebSocketProxy extends SimpleChannelInboundHandler<ByteBuf> {
ByteBuffer toSend = ByteBuffer.allocateDirect(buffer.capacity());
toSend.put(buffer.nioBuffer());
toSend.flip();
client.send(toSend);
if (client.isOpen()) {
client.send(toSend);
} else {
killConnection();
}
}
@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
Copyright 2022 Calder Young. All rights reserved.
Copyright 2022 Calder Young & ayunami2000. All rights reserved.
Based on code written by ayunami2000
@ -23,35 +23,46 @@ window.initializeVoiceClient = (() => {
class EaglercraftVoicePeer {
constructor(client, peerId, peerConnection) {
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, 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) => {
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.createOffer((desc) => {
const selfDesc = desc;
self.peerConnection.setLocalDescription(selfDesc, () => {
self.client.descriptionHandler(self.peerId, selfDesc.toJSON().stringify());
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 local description for \"" + self.peerId + "\"! " + err);
console.error("Failed to set create offer 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') {
@ -65,32 +76,46 @@ window.initializeVoiceClient = (() => {
this.peerConnection.close();
}
mute(muted) {
this.rawStream.getAudioTracks()[0].enabled = !muted;
}
setRemoteDescription(descJSON) {
const self = this;
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, selfDesc.toJSON().stringify());
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 set local description for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId);
console.error("Failed to create answer for \"" + self.peerId + "\"! " + err);
self.client.signalDisconnect(self.peerId);
});
}, (err) => {
console.error("Failed to create answer for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(peerId);
});
}
}, (err) => {
console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err);
self.signalDisconnect(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) {
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) {
this.ICEServers.length = 0;
for(var i = 0; i < urls.length; ++i) {
this.ICEServers.push({ urls: urls[i] });
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] });
}
}
}
@ -139,10 +168,10 @@ window.initializeVoiceClient = (() => {
}
activateVoice(tk) {
this.localRawMediaStream.getAudioTracks()[0].enabled = tk;
if(this.hasInit) this.localRawMediaStream.getAudioTracks()[0].enabled = tk;
}
intitializeDevices() {
initializeDevices() {
if(!this.hasInit) {
this.taskState = TASKSTATE_LOADING;
const self = this;
@ -154,11 +183,12 @@ window.initializeVoiceClient = (() => {
var localStreamIn = self.microphoneVolumeAudioContext.createMediaStreamSource(stream);
localStreamIn.connect(self.localMediaStreamGain);
self.localMediaStreamGain.connect(self.localMediaStream);
self.localMediaStreamGain.gain = 1.0;
self.localMediaStreamGain.gain.value = 1.0;
self.readyState = READYSTATE_DEVICE_INITIALIZED;
self.taskState = TASKSTATE_COMPLETE;
this.hasInit = true;
}).catch(() => {
}).catch((err) => {
console.error(err);
self.readyState = READYSTATE_ABORTED;
self.taskState = TASKSTATE_FAILED;
});
@ -169,10 +199,12 @@ window.initializeVoiceClient = (() => {
}
setMicVolume(val) {
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;
self.localMediaStreamGain.gain = val * 2.0;
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() {
@ -183,9 +215,10 @@ window.initializeVoiceClient = (() => {
return this.readyState;
}
signalConnect(peerId) {
signalConnect(peerId, offer) {
if (!this.hasInit) initializeDevices();
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);
}
@ -196,14 +229,21 @@ window.initializeVoiceClient = (() => {
}
}
signalDisconnect(peerId) {
signalDisconnect(peerId, quiet) {
var thePeer = this.peerList.get(peerId);
if((typeof thePeer !== "undefined") && thePeer !== null) {
this.peerList.delete(thePeer);
try {
thePeer.disconnect();
}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> {
private final long expiration;
private final ExpiringEvent<T> event;
private final Map<T, Long> timestamps = new HashMap<>();
public ExpiringSet(long expiration) {
public ExpiringSet(long expiration, ExpiringEvent<T> event) {
this.expiration = expiration;
this.event = event;
}
public interface ExpiringEvent<T> {
void onExpiration(T item);
}
public void checkForExpirations() {
@ -23,6 +29,7 @@ public class ExpiringSet<T> extends HashSet<T> {
T element = iterator.next();
if (super.contains(element)) {
if (this.timestamps.get(element) + this.expiration < now) {
this.event.onExpiration(element);
iterator.remove();
super.remove(element);
}

View File

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

View File

@ -4,7 +4,6 @@ import java.text.DecimalFormat;
import java.util.HashSet;
import java.util.List;
import net.minecraft.src.*;
import net.lax1dude.eaglercraft.DefaultSkinRenderer;
import net.lax1dude.eaglercraft.EaglerAdapter;
import net.lax1dude.eaglercraft.EaglerProfile;
@ -13,6 +12,9 @@ import net.lax1dude.eaglercraft.GuiScreenEditProfile;
import net.lax1dude.eaglercraft.GuiScreenLicense;
import net.lax1dude.eaglercraft.GuiVoiceOverlay;
import net.lax1dude.eaglercraft.LocalStorageManager;
import net.lax1dude.eaglercraft.Voice;
import net.minecraft.src.*;
import net.lax1dude.eaglercraft.adapter.Tessellator;
import net.lax1dude.eaglercraft.glemu.EffectPipeline;
import net.lax1dude.eaglercraft.glemu.FixedFunctionShader;
@ -244,6 +246,11 @@ public class Minecraft implements Runnable {
this.ingameGUI = new GuiIngame(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) {
// this.displayGuiScreen(new GuiConnecting(new GuiMainMenu(), this, this.serverName, this.serverPort));
//} else {
@ -1110,6 +1117,25 @@ public class Minecraft implements Runnable {
GuiMultiplayer.tickRefreshCooldown();
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) {
this.mcProfiler.endStartSection("mouse");

View File

@ -8,11 +8,9 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import net.lax1dude.eaglercraft.DefaultSkinRenderer;
import net.lax1dude.eaglercraft.EaglerAdapter;
import net.lax1dude.eaglercraft.EaglercraftRandom;
import net.lax1dude.eaglercraft.WebsocketNetworkManager;
import net.lax1dude.eaglercraft.*;
import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.RateLimit;
import net.minecraft.client.Minecraft;
@ -52,6 +50,14 @@ public class NetClientHandler extends NetHandler {
public NetClientHandler(Minecraft par1Minecraft, String par2Str, int par3) throws IOException {
this.mc = par1Minecraft;
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 {
@ -1166,6 +1172,8 @@ public class NetClientHandler extends NetHandler {
} catch (IOException var7) {
var7.printStackTrace();
}
}else if("EAG|Voice".equals(par1Packet250CustomPayload.channel)) {
EaglerAdapter.handleVoiceSignal(par1Packet250CustomPayload.data);
}
}

View File

@ -1,14 +1,10 @@
package net.lax1dude.eaglercraft.adapter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -21,6 +17,7 @@ import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import net.lax1dude.eaglercraft.*;
import org.json.JSONObject;
import org.teavm.interop.Async;
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.MouseEvent;
import org.teavm.jso.dom.events.WheelEvent;
import org.teavm.jso.dom.html.HTMLCanvasElement;
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.dom.html.*;
import org.teavm.jso.media.MediaError;
import org.teavm.jso.typedarrays.ArrayBuffer;
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.Uint8Array;
import org.teavm.jso.typedarrays.Uint8ClampedArray;
import org.teavm.jso.webaudio.AudioBuffer;
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.webaudio.*;
import org.teavm.jso.webgl.WebGLBuffer;
import org.teavm.jso.webgl.WebGLFramebuffer;
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.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.WebGLVertexArray;
import net.minecraft.src.MathHelper;
@ -1769,13 +1746,15 @@ public class EaglerAdapterImpl2 {
public static final boolean startConnection(String uri) {
String res = connectWebSocket(uri);
return "fail".equals(res) ? false : true;
return !"fail".equals(res);
}
public static final void endConnection() {
if(sock == null || sock.getReadyState() == 3) {
sockIsConnecting = false;
}
if(sock != null && !sockIsConnecting) sock.close();
enableVoice(Voice.VoiceChannel.NONE);
}
public static final boolean connectionOpen() {
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 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 boolean voiceAvailableStat = 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;
// 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() {
voiceAvailableStat = false;
}
// to ayunami - use this to set returnSignalHandler when a new NetworkManager is created
public static void setVoiceSignalHandler(Consumer<byte[]> signalHandler) {
returnSignalHandler = signalHandler;
}
public static final int VOICE_SIGNAL_ALLOWED_CLIENTBOUND = 0;
public static final int VOICE_SIGNAL_ICE_SERVERBOUND = 1;
public static final int VOICE_SIGNAL_DESC_SERVERBOUND = 2;
public static final int VOICE_SIGNAL_ALLOWED = 0;
public static final int VOICE_SIGNAL_REQUEST = 0;
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) {
try {
DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(data));
int sig = streamIn.read();
switch(sig) {
case VOICE_SIGNAL_ALLOWED_CLIENTBOUND:
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;
default:
System.err.println("Unknown voice signal packet '" + sig + "'!");
break;
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();
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) {
ex.printStackTrace();
}
}
@ -2078,8 +2091,60 @@ public class EaglerAdapterImpl2 {
}
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) {
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;
if(enable == Voice.VoiceChannel.NONE) {
talkStatus = false;
@ -2088,13 +2153,12 @@ public class EaglerAdapterImpl2 {
voiceSignalHandlersInitialized = true;
voiceClient.setICECandidateHandler(new EaglercraftVoiceClient.ICECandidateHandler() {
@Override
public void call(String peerId, String sdpMLineIndex, String candidate) {
public void call(String peerId, String candidate) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dat = new DataOutputStream(bos);
dat.write(VOICE_SIGNAL_ICE_SERVERBOUND);
dat.write(VOICE_SIGNAL_ICE);
dat.writeUTF(peerId);
dat.writeUTF(sdpMLineIndex);
dat.writeUTF(candidate);
returnSignalHandler.accept(bos.toByteArray());
}catch(IOException ex) {
@ -2107,7 +2171,7 @@ public class EaglerAdapterImpl2 {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dat = new DataOutputStream(bos);
dat.write(VOICE_SIGNAL_DESC_SERVERBOUND);
dat.write(VOICE_SIGNAL_DESC);
dat.writeUTF(peerId);
dat.writeUTF(candidate);
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();
}
sendInitialVoice();
}
}
public static final Voice.VoiceChannel getVoiceChannel() {
@ -2128,7 +2258,6 @@ public class EaglerAdapterImpl2 {
Voice.VoiceStatus.CONNECTING : Voice.VoiceStatus.CONNECTED);
}
// to ayunami - push to talk in the JS works afaik
private static boolean talkStatus = false;
public static final void activateVoice(boolean talk) {
if(talkStatus != talk) {
@ -2137,7 +2266,6 @@ public class EaglerAdapterImpl2 {
talkStatus = talk;
}
// to ayunami - not currently used in the javascript but is used by GUI and gameSettings
private static int proximity = 16;
public static final void setVoiceProximity(int prox) {
proximity = prox;
@ -2146,16 +2274,21 @@ public class EaglerAdapterImpl2 {
return proximity;
}
// to ayunami - iterate all AudioNodes from PeerTrackHandler players and adjust their gain here
private static float volumeListen = 0.5f;
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;
}
public static final float getVoiceListenVolume() {
return volumeListen;
}
// to ayunami - this is already implemented
private static float volumeSpeak = 0.5f;
public static final void setVoiceSpeakVolume(float f) {
if(volumeSpeak != f) {
@ -2167,19 +2300,16 @@ public class EaglerAdapterImpl2 {
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> emptySet = new HashSet();
private static final List<String> emptyLst = new ArrayList();
private static final Set<String> speakingSet = new HashSet();
public static final Set<String> getVoiceListening() {
return emptySet;
return voiceGains.keySet();
}
public static final Set<String> getVoiceSpeaking() {
return emptySet;
return speakingSet;
}
public static final void setVoiceMuted(String username, boolean mute) {
voiceClient.mutePeer(username, mute);
if(mute) {
mutedSet.add(username);
}else {
@ -2190,18 +2320,27 @@ public class EaglerAdapterImpl2 {
return mutedSet;
}
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() {
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() {
}

View File

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