diff --git a/javascript/eagswebrtc.js b/javascript/eagswebrtc.js index 8b50551..fc78028 100644 --- a/javascript/eagswebrtc.js +++ b/javascript/eagswebrtc.js @@ -314,7 +314,7 @@ window.startVoiceClient = () => { // %%%%%%%%%%%%%%%%%%%%%%%%%%%%% LAN CLIENT CODE %%%%%%%%%%%%%%%%%%%%%%%%%%%%% window.initializeLANClient = (() => { - + const READYSTATE_INIT_FAILED = -2; const READYSTATE_FAILED = -1; const READYSTATE_DISCONNECTED = 0; @@ -322,61 +322,139 @@ window.initializeLANClient = (() => { 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 = null; + } + 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) { - + this.dataChannel.send(buffer); } signalRemoteConnect() { - + const self = this; + this.peerConnection.addEventListener("icecandidate", (evt) => { + if(evt.candidate) { + self.iceCandidateHandler(JSON.stringify({ sdpMLineIndex: evt.candidate.sdpMLineIndex, candidate: evt.candidate.candidate })); + } + }); + + this.peerConnection.addEventListener("datachannel", (evt) => { + self.channel = evt.channel; + self.remoteDataChannelHandler(self.channel); + evt.channel.addEventListener("message", (evt) => { + self.remotePacketHandler(evt.data); + }); + }); + + 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(); + }); + }, (err) => { + console.error("Failed to set create offer! " + err); + self.readyState = READYSTATE_FAILED; + self.signalRemoteDisconnect(); + }); + + this.peerConnection.addEventListener("connectionstatechange", (evt) => { + if(evt.connectionState === 'disconnected') { + self.signalRemoteDisconnect(); + } else if (evt.connectionState === 'connected') { + self.readyState = READYSTATE_CONNECTED; + } else if (evt.connectionState === 'failed') { + self.readyState = READYSTATE_FAILED; + self.signalRemoteDisconnect(); + } + }); } signalRemoteDescription(descJSON) { - + this.peerConnection.setRemoteDescription(descJSON); } signalRemoteICECandidate(candidate) { - + this.peerConnection.addICECandidate(candidate); + } + + signalRemoteDisconnect() { + if(this.dataChannel != null) { + this.dataChannel.close(); + this.dataChannel = null; + } + this.peerConnection.close(); + this.remoteDisconnectHandler(); + this.readyState = READYSTATE_DISCONNECTED; } }; @@ -396,67 +474,245 @@ window.startLANClient = () => { // %%%%%%%%%%%%%%%%%%%%%%%%%%%%% 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; + this.peerConnection.addEventListener("icecandidate", (evt) => { + if(evt.candidate) { + self.client.iceCandidateHandler(self.peerId, JSON.stringify({ sdpMLineIndex: evt.candidate.sdpMLineIndex, candidate: evt.candidate.candidate })); + } + }); + + this.dataChannel = this.peerConnection.createDataChannel("lan"); + + this.dataChannel.addEventListener("open", (evt) => { + self.client.remoteClientDataChannelHandler(self.peerId, this.dataChannel); + }); + + this.dataChannel.addEventListener("message", (evt) => { + self.client.remoteClientPacketHandler(self.peerId, evt.data); + }); + + this.peerConnection.addEventListener("connectionstatechange", (evt) => { + if(evt.connectionState === 'disconnected') { + self.client.signalRemoteDisconnect(self.peerId); + } else if (evt.connectionState === 'connected') { + if (self.client.peerState != PEERSTATE_SUCCESS) self.client.peerState = PEERSTATE_SUCCESS; + } else if (evt.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(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.signalRemoteDisconnect(this.peerId); + } + } + + } class EaglercraftLANServer { 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.remoteClientDataChannelHandler = null; + this.remoteClientDisconnectHandler = null; + this.remoteClientPacketHandler = null; } LANServerSupported() { - + return typeof window.RTCPeerConnection !== "undefined"; } initializeServer() { - + // nothing to do! } setRecieveCodeHandler(cb) { - + // not yet implemented! } 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) { + thePeer.dataChannel.send(buffer); + } } - + + 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); + } } - - disconnectRemotePeer(peerId) { - + + 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, quiet); + } + } + 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, quiet); + } } }; diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java index ee6e718..d9d4abe 100644 --- a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java +++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java @@ -2932,10 +2932,13 @@ public class EaglerAdapterImpl2 { return !isLittleEndian; } - private static EaglercraftLANClient rtcLANClient = null; //TODO + private static EaglercraftLANClient rtcLANClient = null; @JSBody(params = { }, script = "return window.startLANClient();") private static native EaglercraftLANClient startRTCLANClient(); + + private static boolean clientLANinit = false; + private static final List clientLANPacketBuffer = new ArrayList<>(); public static final int LAN_CLIENT_INIT_FAILED = -2; public static final int LAN_CLIENT_FAILED = -1; @@ -2944,39 +2947,128 @@ public class EaglerAdapterImpl2 { public static final int LAN_CLIENT_CONNECTED = 2; public static final boolean clientLANSupported() { - return true; + return rtcLANClient.LANClientSupported(); } public static final void clientLANSetServer(String relay, String peerId) { - + if(!clientLANinit) { + clientLANinit = true; + rtcLANClient.setDescriptionHandler(new EaglercraftLANClient.DescriptionHandler() { + @Override + public void call(String description) { + + } + }); + rtcLANClient.setICECandidateHandler(new EaglercraftLANClient.ICECandidateHandler() { + @Override + public void call(String candidate) { + + } + }); + rtcLANClient.setRemoteDataChannelHandler(new EaglercraftLANClient.ClientSignalHandler() { + @Override + public void call() { + // basically useless, ignore for now. + } + }); + rtcLANClient.setRemotePacketHandler(new EaglercraftLANClient.RemotePacketHandler() { + @Override + public void call(ArrayBuffer buffer) { + Uint8Array array = Uint8Array.create(buffer); + byte[] ret = new byte[array.getByteLength()]; + for(int i = 0; i < ret.length; ++i) { + ret[i] = (byte) array.get(i); + } + clientLANPacketBuffer.add(ret); + } + }); + rtcLANClient.setRemoteDisconnectHandler(new EaglercraftLANClient.ClientSignalHandler() { + @Override + public void call() { + // disconnected + } + }); + } + // todo: java-side: register with relay and set server } public static final int clientLANReadyState() { - return 0; + return rtcLANClient.getReadyState(); } public static final void clientLANCloseConnection() { - + rtcLANClient.signalRemoteDisconnect(); } public static final void clientLANSendPacket(byte[] pkt) { - + ArrayBuffer arr = ArrayBuffer.create(pkt.length); + Uint8Array.create(arr).set(pkt); + rtcLANClient.sendPacketToServer(arr); } public static final byte[] clientLANReadPacket() { - return null; + return clientLANPacketBuffer.size() > 0 ? clientLANPacketBuffer.remove(0) : null; } private static EaglercraftLANServer rtcLANServer = null; @JSBody(params = { }, script = "return window.startLANServer();") private static native EaglercraftLANServer startRTCLANServer(); + + private static boolean serverLANinit = false; + private static final List serverLANPacketBuffer = new ArrayList<>(); public static final boolean serverLANSupported() { - return true; + return rtcLANServer.LANServerSupported(); } public static final String serverLANInitializeServer(String relay) { + rtcLANServer.initializeServer(); + if(!serverLANinit) { + serverLANinit = true; + rtcLANServer.setDescriptionHandler(new EaglercraftLANServer.DescriptionHandler() { + @Override + public void call(String peerId, String candidate) { + + } + }); + rtcLANServer.setICECandidateHandler(new EaglercraftLANServer.ICECandidateHandler() { + @Override + public void call(String peerId, String candidate) { + + } + }); + rtcLANServer.setRecieveCodeHandler(new EaglercraftLANServer.CodeHandler() { + @Override + public void call(String code) { + + } + }); + rtcLANServer.setRemoteClientDataChannelHandler(new EaglercraftLANServer.ClientSignalHandler() { + @Override + public void call() { + // unused + } + }); + rtcLANServer.setRemoteClientPacketHandler(new EaglercraftLANServer.PeerPacketHandler() { + @Override + public void call(String peerId, ArrayBuffer buffer) { + Uint8Array array = Uint8Array.create(buffer); + byte[] ret = new byte[array.getByteLength()]; + for(int i = 0; i < ret.length; ++i) { + ret[i] = (byte) array.get(i); + } + serverLANPacketBuffer.add(new LANPeerPacket(peerId, ret)); + } + }); + rtcLANServer.setRemoteClientDisconnectHandler(new EaglercraftLANServer.ClientSignalHandler() { + @Override + public void call() { + // disconnected + } + }); + } + // todo: java-side: register in relay & return code!! return null; } @@ -2985,7 +3077,8 @@ public class EaglerAdapterImpl2 { } public static final void serverLANCloseServer() { - + rtcLANServer.signalRemoteDisconnect(""); + // todo: java-side: disconnect from relay server } public static interface LANConnectionEvent { @@ -3020,15 +3113,17 @@ public class EaglerAdapterImpl2 { } public static final LANPeerPacket serverLANReadPacket() { - return null; + return serverLANPacketBuffer.size() > 0 ? serverLANPacketBuffer.remove(0) : null; } public static final void serverLANWritePacket(String peer, byte[] data) { - + ArrayBuffer arr = ArrayBuffer.create(data.length); + Uint8Array.create(arr).set(data); + rtcLANServer.sendPacketToRemoteClient(peer, arr); } public static final void serverLANDisconnectPeer(String peer) { - + rtcLANServer.signalRemoteDisconnect(peer); } } diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java index 577beb7..7783cc2 100644 --- a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java +++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java @@ -5,7 +5,7 @@ import org.teavm.jso.JSObject; import org.teavm.jso.typedarrays.ArrayBuffer; public interface EaglercraftLANClient extends JSObject { - + final int READYSTATE_INIT_FAILED = -2; final int READYSTATE_FAILED = -1; final int READYSTATE_DISCONNECTED = 0; @@ -38,6 +38,8 @@ public interface EaglercraftLANClient extends JSObject { void signalRemoteICECandidate(String candidate); + void signalRemoteDisconnect(); + @JSFunctor public static interface ICECandidateHandler extends JSObject { void call(String candidate); @@ -45,7 +47,7 @@ public interface EaglercraftLANClient extends JSObject { @JSFunctor public static interface DescriptionHandler extends JSObject { - void call(String candidate); + void call(String description); } @JSFunctor diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java index 9f0a5af..624e974 100644 --- a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java +++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java @@ -5,6 +5,10 @@ import org.teavm.jso.JSObject; import org.teavm.jso.typedarrays.ArrayBuffer; public interface EaglercraftLANServer extends JSObject { + + final int PEERSTATE_FAILED = 0; + final int PEERSTATE_SUCCESS = 1; + final int PEERSTATE_LOADING = 2; boolean LANServerSupported(); @@ -24,7 +28,19 @@ public interface EaglercraftLANServer extends JSObject { void setRemoteClientPacketHandler(PeerPacketHandler cb); - void ssendPacketToRemoteClient(String peerId, ArrayBuffer buffer); + void sendPacketToRemoteClient(String peerId, ArrayBuffer buffer); + + void resetPeerStates(); + + int getPeerState(); + + int getPeerStateConnect(); + + int getPeerStateInitial(); + + int getPeerStateDesc(); + + int getPeerStateIce(); void signalRemoteConnect(String peerId); @@ -32,7 +48,7 @@ public interface EaglercraftLANServer extends JSObject { void signalRemoteICECandidate(String peerId, String candidate); - void disconnectRemotePeer(String peerId); + void signalRemoteDisconnect(String peerId); @JSFunctor public static interface ICECandidateHandler extends JSObject {