"use strict"; /* This is the backend for voice channels and LAN servers in eaglercraft it links with TeaVM EaglerAdapter at runtime Copyright 2022 ayunami2000 & lax1dude. All rights reserved. */ // %%%%%%%%%%%%%%%%%%%%%%%%%%%%% VOICE CODE %%%%%%%%%%%%%%%%%%%%%%%%%%%%% window.initializeVoiceClient = (() => { const READYSTATE_NONE = 0; const READYSTATE_ABORTED = -1; const READYSTATE_DEVICE_INITIALIZED = 1; const PEERSTATE_FAILED = 0; const PEERSTATE_SUCCESS = 1; const PEERSTATE_LOADING = 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)); if (self.client.peerStateInitial != PEERSTATE_SUCCESS) self.client.peerStateInitial = PEERSTATE_SUCCESS; }, (err) => { console.error("Failed to set local description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateInitial == PEERSTATE_LOADING) self.client.peerStateInitial = PEERSTATE_FAILED; self.client.signalDisconnect(self.peerId); }); }, (err) => { console.error("Failed to set create offer for \"" + self.peerId + "\"! " + err); if (self.client.peerStateInitial == PEERSTATE_LOADING) self.client.peerStateInitial = PEERSTATE_FAILED; self.client.signalDisconnect(self.peerId); }); } this.peerConnection.addEventListener("connectionstatechange", (evt) => { if(self.peerConnection.connectionState === 'disconnected') { self.client.signalDisconnect(self.peerId); } else if (self.peerConnection.connectionState === 'connected') { if (self.client.peerState != PEERSTATE_SUCCESS) self.client.peerState = PEERSTATE_SUCCESS; } else if (self.peerConnection.connectionState === 'failed') { if (self.client.peerState == PEERSTATE_LOADING) self.client.peerState = PEERSTATE_FAILED; 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)); if (self.client.peerStateDesc != PEERSTATE_SUCCESS) self.client.peerStateDesc = PEERSTATE_SUCCESS; }, (err) => { console.error("Failed to set local description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalDisconnect(self.peerId); }); }, (err) => { console.error("Failed to create answer for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalDisconnect(self.peerId); }); } }, (err) => { console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalDisconnect(self.peerId); }); } catch (err) { console.error("Failed to parse remote description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalDisconnect(self.peerId); } } addICECandidate(candidate) { try { this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate))); if (this.client.peerStateIce != PEERSTATE_SUCCESS) this.client.peerStateIce = PEERSTATE_SUCCESS; } catch (err) { console.error("Failed to parse ice candidate for \"" + this.peerId + "\"! " + err); if (this.client.peerStateIce == PEERSTATE_LOADING) this.client.peerStateIce = PEERSTATE_FAILED; this.client.signalDisconnect(this.peerId); } } } class EaglercraftVoiceClient { constructor() { this.ICEServers = []; this.hasInit = false; this.peerList = new Map(); this.readyState = READYSTATE_NONE; this.peerState = PEERSTATE_LOADING; this.peerStateConnect = PEERSTATE_LOADING; this.peerStateInitial = PEERSTATE_LOADING; this.peerStateDesc = PEERSTATE_LOADING; this.peerStateIce = PEERSTATE_LOADING; this.iceCandidateHandler = null; this.descriptionHandler = null; this.peerTrackHandler = null; this.peerDisconnectHandler = null; this.microphoneVolumeAudioContext = null; } voiceClientSupported() { return typeof window.RTCPeerConnection !== "undefined" && typeof navigator.mediaDevices !== "undefined" && typeof navigator.mediaDevices.getUserMedia !== "undefined"; } setICEServers(urls) { this.ICEServers.length = 0; for(var i = 0; i < urls.length; ++i) { var etr = urls[i].split(";"); if(etr.length == 1) { this.ICEServers.push({ urls: etr[0] }); }else if(etr.length == 3) { this.ICEServers.push({ urls: etr[0], username: etr[1], credential: etr[2] }); } } } setICECandidateHandler(cb) { this.iceCandidateHandler = cb; } setDescriptionHandler(cb) { this.descriptionHandler = cb; } setPeerTrackHandler(cb) { this.peerTrackHandler = cb; } setPeerDisconnectHandler(cb) { this.peerDisconnectHandler = cb; } activateVoice(tk) { if(this.hasInit) this.localRawMediaStream.getAudioTracks()[0].enabled = tk; } initializeDevices() { if(!this.hasInit) { const self = this; navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => { self.microphoneVolumeAudioContext = new AudioContext(); 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; this.hasInit = true; }).catch((err) => { self.readyState = READYSTATE_ABORTED; }); }else { this.readyState = READYSTATE_DEVICE_INITIALIZED; } } 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; } } resetPeerStates() { this.peerState = this.peerStateConnect = this.peerStateInitial = this.peerStateDesc = this.peerStateIce = PEERSTATE_LOADING; } getPeerState() { return this.peerState; } getPeerStateConnect() { return this.peerStateConnect; } getPeerStateInitial() { return this.peerStateInitial; } getPeerStateDesc() { return this.peerStateDesc; } getPeerStateIce() { return this.peerStateIce; } getReadyState() { return this.readyState; } signalConnect(peerId, offer) { if (!this.hasInit) initializeDevices(); try { const peerConnection = new RTCPeerConnection({ iceServers: this.ICEServers, optional: [ { DtlsSrtpKeyAgreement: true } ] }); const peerInstance = new EaglercraftVoicePeer(this, peerId, peerConnection, offer); this.peerList.set(peerId, peerInstance); if (this.peerStateConnect != PEERSTATE_SUCCESS) this.peerStateConnect = PEERSTATE_SUCCESS; } catch (e) { if (this.peerStateConnect == PEERSTATE_LOADING) this.peerStateConnect = PEERSTATE_FAILED; } } 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) {} this.peerDisconnectHandler(peerId, quiet); } } 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(); }; // %%%%%%%%%%%%%%%%%%%%%%%%%%%%% LAN CLIENT CODE %%%%%%%%%%%%%%%%%%%%%%%%%%%%% window.initializeLANClient = (() => { const READYSTATE_INIT_FAILED = -2; const READYSTATE_FAILED = -1; const READYSTATE_DISCONNECTED = 0; const READYSTATE_CONNECTING = 1; const READYSTATE_CONNECTED = 2; class EaglercraftLANClient { constructor() { this.ICEServers = []; this.peerConnection = null; this.dataChannel = null; this.readyState = READYSTATE_CONNECTING; this.iceCandidateHandler = null; this.descriptionHandler = null; this.remoteDataChannelHandler = null; this.remoteDisconnectHandler = null; this.remotePacketHandler = null; } LANClientSupported() { return typeof window.RTCPeerConnection !== "undefined"; } initializeClient() { try { if(this.dataChannel != null) { this.dataChannel.close(); this.dataChannel = null; } if(this.peerConnection != null) { this.peerConnection.close(); } this.peerConnection = new RTCPeerConnection({ iceServers: this.ICEServers, optional: [ { DtlsSrtpKeyAgreement: true } ] }); this.readyState = READYSTATE_CONNECTING; } catch (e) { this.readyState = READYSTATE_INIT_FAILED; } } setICEServers(urls) { this.ICEServers.length = 0; for(var i = 0; i < urls.length; ++i) { var etr = urls[i].split(";"); if(etr.length == 1) { this.ICEServers.push({ urls: etr[0] }); }else if(etr.length == 3) { this.ICEServers.push({ urls: etr[0], username: etr[1], credential: etr[2] }); } } } setICECandidateHandler(cb) { this.iceCandidateHandler = cb; } setDescriptionHandler(cb) { this.descriptionHandler = cb; } setRemoteDataChannelHandler(cb) { this.remoteDataChannelHandler = cb; } setRemoteDisconnectHandler(cb) { this.remoteDisconnectHandler = cb; } setRemotePacketHandler(cb) { this.remotePacketHandler = cb; } getReadyState() { return this.readyState; } sendPacketToServer(buffer) { if(this.dataChannel != null && this.dataChannel.readyState == "open") { this.dataChannel.send(buffer); }else { this.signalRemoteDisconnect(false); } } signalRemoteConnect() { const self = this; const iceCandidates = []; this.peerConnection.addEventListener("icecandidate", (evt) => { if(evt.candidate) { if(iceCandidates.length == 0) setTimeout(() => { if(self.peerConnection != null && self.peerConnection.connectionState != "disconnected") { self.iceCandidateHandler(JSON.stringify(iceCandidates)); iceCandidates.length = 0; } }, 1500); iceCandidates.push({ sdpMLineIndex: evt.candidate.sdpMLineIndex, candidate: evt.candidate.candidate }); } }); this.dataChannel = this.peerConnection.createDataChannel("lan"); this.dataChannel.binaryType = "arraybuffer"; this.dataChannel.addEventListener("open", async (evt) => { while(iceCandidates.length > 0) { await new Promise(resolve => setTimeout(resolve, 0)); } self.remoteDataChannelHandler(self.dataChannel); }); this.dataChannel.addEventListener("message", (evt) => { self.remotePacketHandler(evt.data); }, false); this.peerConnection.createOffer((desc) => { const selfDesc = desc; self.peerConnection.setLocalDescription(selfDesc, () => { self.descriptionHandler(JSON.stringify(selfDesc)); }, (err) => { console.error("Failed to set local description! " + err); self.readyState = READYSTATE_FAILED; self.signalRemoteDisconnect(false); }); }, (err) => { console.error("Failed to set create offer! " + err); self.readyState = READYSTATE_FAILED; self.signalRemoteDisconnect(false); }); this.peerConnection.addEventListener("connectionstatechange", (evt) => { if(self.peerConnection.connectionState === 'disconnected') { self.signalRemoteDisconnect(false); } else if (self.peerConnection.connectionState === 'connected') { self.readyState = READYSTATE_CONNECTED; } else if (self.peerConnection.connectionState === 'failed') { self.readyState = READYSTATE_FAILED; self.signalRemoteDisconnect(false); } }); } signalRemoteDescription(descJSON) { try { this.peerConnection.setRemoteDescription(JSON.parse(descJSON)); } catch (e) { console.error(e); this.readyState = READYSTATE_FAILED; this.signalRemoteDisconnect(false); } } signalRemoteICECandidate(candidates) { try { const candidateList = JSON.parse(candidates); for (let candidate of candidateList) { this.peerConnection.addIceCandidate(candidate); } } catch (e) { console.error(e); this.readyState = READYSTATE_FAILED; this.signalRemoteDisconnect(false); } } signalRemoteDisconnect(quiet) { if(this.dataChannel != null) { this.dataChannel.close(); this.dataChannel = null; } if(this.peerConnection != null) { this.peerConnection.close(); } if(!quiet) this.remoteDisconnectHandler(); this.readyState = READYSTATE_DISCONNECTED; } }; window.constructLANClient = () => new EaglercraftLANClient(); }); window.startLANClient = () => { if(typeof window.constructLANClient !== "function") { window.initializeLANClient(); } return window.constructLANClient(); }; // %%%%%%%%%%%%%%%%%%%%%%%%%%%%% LAN SERVER CODE %%%%%%%%%%%%%%%%%%%%%%%%%%%%% window.initializeLANServer = (() => { const PEERSTATE_FAILED = 0; const PEERSTATE_SUCCESS = 1; const PEERSTATE_LOADING = 2; class EaglercraftLANPeer { constructor(client, peerId, peerConnection) { this.client = client; this.peerId = peerId; this.peerConnection = peerConnection; this.dataChannel = null; const self = this; const iceCandidates = []; this.peerConnection.addEventListener("icecandidate", (evt) => { if(evt.candidate) { if(iceCandidates.length == 0) setTimeout(() => { if(self.peerConnection != null && self.peerConnection.connectionState != "disconnected") { self.client.iceCandidateHandler(self.peerId, JSON.stringify(iceCandidates)); iceCandidates.length = 0; } }, 1500); iceCandidates.push({ sdpMLineIndex: evt.candidate.sdpMLineIndex, candidate: evt.candidate.candidate }); } }); this.peerConnection.addEventListener("datachannel", async (evt) => { while(iceCandidates.length > 0) { await new Promise(resolve => setTimeout(resolve, 0)); } self.dataChannel = evt.channel; self.client.remoteClientDataChannelHandler(self.peerId, self.dataChannel); self.dataChannel.addEventListener("message", (evt) => { self.client.remoteClientPacketHandler(self.peerId, evt.data); }, false); }, false); this.peerConnection.addEventListener("connectionstatechange", (evt) => { if(self.peerConnection.connectionState === 'disconnected') { self.client.signalRemoteDisconnect(self.peerId); } else if (self.peerConnection.connectionState === 'connected') { if (self.client.peerState != PEERSTATE_SUCCESS) self.client.peerState = PEERSTATE_SUCCESS; } else if (self.peerConnection.connectionState === 'failed') { if (self.client.peerState == PEERSTATE_LOADING) self.client.peerState = PEERSTATE_FAILED; self.client.signalRemoteDisconnect(self.peerId); } }); } disconnect() { if(this.dataChannel != null) { this.dataChannel.close(); this.dataChannel = null; } this.peerConnection.close(); } 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)); if (self.client.peerStateDesc != PEERSTATE_SUCCESS) self.client.peerStateDesc = PEERSTATE_SUCCESS; }, (err) => { console.error("Failed to set local description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalRemoteDisconnect(self.peerId); }); }, (err) => { console.error("Failed to create answer for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalRemoteDisconnect(self.peerId); }); } }, (err) => { console.error("Failed to set remote description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalRemoteDisconnect(self.peerId); }); } catch (err) { console.error("Failed to parse remote description for \"" + self.peerId + "\"! " + err); if (self.client.peerStateDesc == PEERSTATE_LOADING) self.client.peerStateDesc = PEERSTATE_FAILED; self.client.signalRemoteDisconnect(self.peerId); } } addICECandidate(candidates) { try { const candidateList = JSON.parse(candidates); for (let candidate of candidateList) { this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); } if (this.client.peerStateIce != PEERSTATE_SUCCESS) this.client.peerStateIce = PEERSTATE_SUCCESS; } catch (err) { console.error("Failed to parse ice candidate for \"" + this.peerId + "\"! " + err); if (this.client.peerStateIce == PEERSTATE_LOADING) this.client.peerStateIce = PEERSTATE_FAILED; this.client.signalRemoteDisconnect(this.peerId); } } } class EaglercraftLANServer { constructor() { this.ICEServers = []; this.hasInit = false; this.peerList = new Map(); this.peerState = PEERSTATE_LOADING; this.peerStateConnect = PEERSTATE_LOADING; this.peerStateInitial = PEERSTATE_LOADING; this.peerStateDesc = PEERSTATE_LOADING; this.peerStateIce = PEERSTATE_LOADING; this.iceCandidateHandler = null; this.descriptionHandler = null; this.remoteClientDataChannelHandler = null; this.remoteClientDisconnectHandler = null; this.remoteClientPacketHandler = null; } LANServerSupported() { return typeof window.RTCPeerConnection !== "undefined"; } initializeServer() { // nothing to do! } setICEServers(urls) { this.ICEServers.length = 0; for(var i = 0; i < urls.length; ++i) { var etr = urls[i].split(";"); if(etr.length == 1) { this.ICEServers.push({ urls: etr[0] }); }else if(etr.length == 3) { this.ICEServers.push({ urls: etr[0], username: etr[1], credential: etr[2] }); } } } setICECandidateHandler(cb) { this.iceCandidateHandler = cb; } setDescriptionHandler(cb) { this.descriptionHandler = cb; } setRemoteClientDataChannelHandler(cb) { this.remoteClientDataChannelHandler = cb; } setRemoteClientDisconnectHandler(cb) { this.remoteClientDisconnectHandler = cb; } setRemoteClientPacketHandler(cb) { this.remoteClientPacketHandler = cb; } sendPacketToRemoteClient(peerId, buffer) { var thePeer = this.peerList.get(peerId); if((typeof thePeer !== "undefined") && thePeer !== null) { if(thePeer.dataChannel != null && thePeer.dataChannel.readyState == "open") { thePeer.dataChannel.send(buffer); }else { this.signalRemoteDisconnect(peerId); } } } resetPeerStates() { this.peerState = this.peerStateConnect = this.peerStateInitial = this.peerStateDesc = this.peerStateIce = PEERSTATE_LOADING; } getPeerState() { return this.peerState; } getPeerStateConnect() { return this.peerStateConnect; } getPeerStateInitial() { return this.peerStateInitial; } getPeerStateDesc() { return this.peerStateDesc; } getPeerStateIce() { return this.peerStateIce; } signalRemoteConnect(peerId) { try { const peerConnection = new RTCPeerConnection({ iceServers: this.ICEServers, optional: [ { DtlsSrtpKeyAgreement: true } ] }); const peerInstance = new EaglercraftLANPeer(this, peerId, peerConnection); this.peerList.set(peerId, peerInstance); if (this.peerStateConnect != PEERSTATE_SUCCESS) this.peerStateConnect = PEERSTATE_SUCCESS; } catch (e) { if (this.peerStateConnect == PEERSTATE_LOADING) this.peerStateConnect = PEERSTATE_FAILED; } } signalRemoteDescription(peerId, descJSON) { var thePeer = this.peerList.get(peerId); if((typeof thePeer !== "undefined") && thePeer !== null) { thePeer.setRemoteDescription(descJSON); } } signalRemoteICECandidate(peerId, candidate) { var thePeer = this.peerList.get(peerId); if((typeof thePeer !== "undefined") && thePeer !== null) { thePeer.addICECandidate(candidate); } } signalRemoteDisconnect(peerId) { if(peerId.length == 0) { for(const thePeer of this.peerList.values()) { if((typeof thePeer !== "undefined") && thePeer !== null) { this.peerList.delete(thePeer); try { thePeer.disconnect(); }catch(e) {} this.remoteClientDisconnectHandler(peerId); } } this.peerList.clear(); return; } var thePeer = this.peerList.get(peerId); if((typeof thePeer !== "undefined") && thePeer !== null) { this.peerList.delete(thePeer); try { thePeer.disconnect(); }catch(e) {} this.remoteClientDisconnectHandler(peerId); } } }; window.constructLANServer = () => new EaglercraftLANServer(); }); window.startLANServer = () => { if(typeof window.constructLANServer !== "function") { window.initializeLANServer(); } return window.constructLANServer(); };