Spaces:
Build error
Build error
/* | |
* Copyright (c) 2024 lax1dude, ayunami2000. All Rights Reserved. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. | |
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, | |
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | |
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | |
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
* POSSIBILITY OF SUCH DAMAGE. | |
* | |
*/ | |
const READYSTATE_INIT_FAILED = -2; | |
const READYSTATE_FAILED = -1; | |
const READYSTATE_DISCONNECTED = 0; | |
const READYSTATE_CONNECTING = 1; | |
const READYSTATE_CONNECTED = 2; | |
const EVENT_WEBRTC_ICE = 0; | |
const EVENT_WEBRTC_DESC = 1; | |
const EVENT_WEBRTC_OPEN = 2; | |
const EVENT_WEBRTC_PACKET = 3; | |
const EVENT_WEBRTC_CLOSE = 4; | |
const platfWebRTCName = "platformWebRTC"; | |
/** | |
* @typedef {{ | |
* peerId:string, | |
* peerConnection:!RTCPeerConnection, | |
* dataChannel:RTCDataChannel, | |
* ipcChannel:(string|null), | |
* pushEvent:function(number,*), | |
* disconnect:function() | |
* }} | |
*/ | |
let LANPeerInternal; | |
function initializePlatfWebRTC(webrtcImports) { | |
const clientLANPacketBuffer = new EaglerLinkedQueue(); | |
let lanClient; | |
lanClient = { | |
iceServers: [], | |
/** @type {RTCPeerConnection|null} */ | |
peerConnection: null, | |
/** @type {RTCDataChannel|null} */ | |
dataChannel: null, | |
readyState: READYSTATE_CONNECTING, | |
/** @type {string|null} */ | |
iceCandidate: null, | |
/** @type {string|null} */ | |
description: null, | |
dataChannelOpen: false, | |
dataChannelClosed: true, | |
disconnect: function(quiet) { | |
if (lanClient.dataChannel) { | |
try { | |
lanClient.dataChannel.close(); | |
} catch (t) { | |
} | |
lanClient.dataChannel = null; | |
} | |
if (lanClient.peerConnection) { | |
try { | |
lanClient.peerConnection.close(); | |
} catch (t) { | |
} | |
lanClient.peerConnection = null; | |
} | |
if (!quiet) lanClient.dataChannelClosed = true; | |
lanClient.readyState = READYSTATE_DISCONNECTED; | |
} | |
}; | |
/** | |
* @return {boolean} | |
*/ | |
webrtcImports["supported"] = function() { | |
return typeof RTCPeerConnection !== "undefined"; | |
}; | |
/** | |
* @return {number} | |
*/ | |
webrtcImports["clientLANReadyState"] = function() { | |
return lanClient.readyState; | |
}; | |
webrtcImports["clientLANCloseConnection"] = function() { | |
lanClient.disconnect(false); | |
}; | |
/** | |
* @param {!Uint8Array} pkt | |
*/ | |
webrtcImports["clientLANSendPacket"] = function(pkt) { | |
if (lanClient.dataChannel !== null && "open" === lanClient.dataChannel.readyState) { | |
try { | |
lanClient.dataChannel.send(pkt); | |
} catch (e) { | |
lanClient.disconnect(false); | |
} | |
}else { | |
lanClient.disconnect(false); | |
} | |
}; | |
/** | |
* @return {Uint8Array} | |
*/ | |
webrtcImports["clientLANReadPacket"] = function() { | |
const ret = clientLANPacketBuffer.shift(); | |
return ret ? new Uint8Array(ret["data"]) : null; | |
}; | |
/** | |
* @return {number} | |
*/ | |
webrtcImports["clientLANAvailable"] = function() { | |
return clientLANPacketBuffer.getLength(); | |
}; | |
/** | |
* @param {!Array<string>} servers | |
*/ | |
webrtcImports["clientLANSetICEServersAndConnect"] = function(servers) { | |
lanClient.iceServers.length = 0; | |
for (let url of servers) { | |
let etr = url.split(";"); | |
if(etr.length === 1) { | |
lanClient.iceServers.push({ | |
urls: etr[0] | |
}); | |
}else if(etr.length === 3) { | |
lanClient.iceServers.push({ | |
urls: etr[0], | |
username: etr[1], | |
credential: etr[2] | |
}); | |
} | |
} | |
if(lanClient.readyState === READYSTATE_CONNECTED || lanClient.readyState === READYSTATE_CONNECTING) { | |
lanClient.disconnect(true); | |
} | |
try { | |
if (lanClient.dataChannel) { | |
try { | |
lanClient.dataChannel.close(); | |
} catch (t) { | |
} | |
lanClient.dataChannel = null; | |
} | |
if (lanClient.peerConnection) { | |
try { | |
lanClient.peerConnection.close(); | |
} catch (t) { | |
} | |
} | |
lanClient.peerConnection = new RTCPeerConnection({ | |
iceServers: lanClient.iceServers, | |
optional: [ | |
{ | |
DtlsSrtpKeyAgreement: true | |
} | |
] | |
}); | |
lanClient.readyState = READYSTATE_CONNECTING; | |
} catch (/** Error */ t) { | |
eagStackTrace(ERROR, "Could not create LAN client RTCPeerConnection!", t); | |
lanClient.readyState = READYSTATE_INIT_FAILED; | |
return; | |
} | |
try { | |
const iceCandidates = []; | |
lanClient.peerConnection.addEventListener("icecandidate", /** @type {function(Event)} */ ((/** !RTCPeerConnectionIceEvent */ evt) => { | |
if(evt.candidate) { | |
if(iceCandidates.length === 0) { | |
const candidateState = [0, 0]; | |
const runnable = () => { | |
if(lanClient.peerConnection !== null && lanClient.peerConnection.connectionState !== "disconnected") { | |
const trial = ++candidateState[1]; | |
if(candidateState[0] !== iceCandidates.length && trial < 3) { | |
candidateState[0] = iceCandidates.length; | |
setTimeout(runnable, 2000); | |
return; | |
} | |
lanClient.iceCandidate = JSON.stringify(iceCandidates); | |
iceCandidates.length = 0; | |
} | |
}; | |
setTimeout(runnable, 2000); | |
} | |
iceCandidates.push({ | |
"sdpMLineIndex": evt.candidate.sdpMLineIndex, | |
"candidate": evt.candidate.candidate | |
}); | |
} | |
})); | |
lanClient.dataChannel = lanClient.peerConnection.createDataChannel("lan"); | |
lanClient.dataChannel.binaryType = "arraybuffer"; | |
let evtHandler; | |
evtHandler = () => { | |
if (iceCandidates.length > 0) { | |
setTimeout(evtHandler, 10); | |
return; | |
} | |
lanClient.dataChannelClosed = false; | |
lanClient.dataChannelOpen = true; | |
}; | |
lanClient.dataChannel.addEventListener("open", evtHandler); | |
lanClient.dataChannel.addEventListener("message", /** @type {function(Event)} */ ((/** MessageEvent */ evt) => { | |
clientLANPacketBuffer.push({ "data": evt.data, "_next": null }); | |
})); | |
lanClient.peerConnection.createOffer().then((/** !RTCSessionDescription */ desc) => { | |
lanClient.peerConnection.setLocalDescription(desc).then(() => { | |
lanClient.description = JSON.stringify(desc); | |
}).catch((err) => { | |
eagError("Failed to set local description! {}", /** @type {string} */ (err.message)); | |
lanClient.readyState = READYSTATE_FAILED; | |
lanClient.disconnect(false); | |
}); | |
}).catch((err) => { | |
eagError("Failed to set create offer! {}", /** @type {string} */ (err.message)); | |
lanClient.readyState = READYSTATE_FAILED; | |
lanClient.disconnect(false); | |
}); | |
lanClient.peerConnection.addEventListener("connectionstatechange", /** @type {function(Event)} */ ((evt) => { | |
var connectionState = lanClient.peerConnection.connectionState; | |
if ("disconnected" === connectionState) { | |
lanClient.disconnect(false); | |
} else if ("connected" === connectionState) { | |
lanClient.readyState = READYSTATE_CONNECTED; | |
} else if ("failed" === connectionState) { | |
lanClient.readyState = READYSTATE_FAILED; | |
lanClient.disconnect(false); | |
} | |
})); | |
} catch (t) { | |
if (lanClient.dataChannel) { | |
try { | |
lanClient.dataChannel.close(); | |
} catch (tt) { | |
} | |
lanClient.dataChannel = null; | |
} | |
if (lanClient.peerConnection) { | |
try { | |
lanClient.peerConnection.close(); | |
} catch (tt) { | |
} | |
lanClient.peerConnection = null; | |
} | |
eagStackTrace(ERROR, "Could not create LAN client RTCDataChannel!", t); | |
lanClient.readyState = READYSTATE_INIT_FAILED; | |
} | |
}; | |
webrtcImports["clearLANClientState"] = function() { | |
lanClient.iceCandidate = lanClient.description = null; | |
lanClient.dataChannelOpen = false; | |
lanClient.dataChannelClosed = true; | |
}; | |
/** | |
* @return {string|null} | |
*/ | |
webrtcImports["clientLANAwaitICECandidate"] = function() { | |
if (lanClient.iceCandidate === null) { | |
return null; | |
} | |
const ret = lanClient.iceCandidate; | |
lanClient.iceCandidate = null; | |
return ret; | |
}; | |
/** | |
* @return {string|null} | |
*/ | |
webrtcImports["clientLANAwaitDescription"] = function() { | |
if (lanClient.description === null) { | |
return null; | |
} | |
const ret = lanClient.description; | |
lanClient.description = null; | |
return ret; | |
}; | |
/** | |
* @return {boolean} | |
*/ | |
webrtcImports["clientLANAwaitChannel"] = function() { | |
if (lanClient.dataChannelOpen) { | |
lanClient.dataChannelOpen = false; | |
return true; | |
} | |
return false; | |
}; | |
/** | |
* @return {boolean} | |
*/ | |
webrtcImports["clientLANClosed"] = function() { | |
return lanClient.dataChannelClosed; | |
}; | |
/** | |
* @param {string} candidate | |
*/ | |
webrtcImports["clientLANSetICECandidate"] = function(candidate) { | |
try { | |
const lst = /** @type {Array<!Object>} */ (JSON.parse(candidate)); | |
for (var i = 0; i < lst.length; ++i) { | |
lanClient.peerConnection.addIceCandidate(new RTCIceCandidate(lst[i])); | |
} | |
}catch(/** Error */ ex) { | |
eagStackTrace(ERROR, "Uncaught exception setting remote ICE candidates", ex); | |
lanClient.readyState = READYSTATE_FAILED; | |
lanClient.disconnect(false); | |
} | |
}; | |
/** | |
* @param {string} description | |
*/ | |
webrtcImports["clientLANSetDescription"] = function(description) { | |
try { | |
lanClient.peerConnection.setRemoteDescription(/** @type {!RTCSessionDescription} */ (JSON.parse(description))); | |
}catch(/** Error */ ex) { | |
eagStackTrace(ERROR, "Uncaught exception setting remote description", ex); | |
lanClient.readyState = READYSTATE_FAILED; | |
lanClient.disconnect(false); | |
} | |
}; | |
let lanServer; | |
lanServer = { | |
/** @type {!Array<Object>} */ | |
iceServers: [], | |
/** @type {!Map<string, !LANPeerInternal>} */ | |
peerList: new Map(), | |
/** @type {!Map<string, !LANPeerInternal>} */ | |
ipcMapList: new Map(), | |
disconnect: function(/** string */ peerId) { | |
const thePeer = lanServer.peerList.get(peerId); | |
if(thePeer) { | |
lanServer.peerList.delete(peerId); | |
if(thePeer.ipcChannel) { | |
lanServer.ipcMapList.delete(thePeer.ipcChannel); | |
} | |
try { | |
thePeer.disconnect(); | |
} catch (ignored) {} | |
thePeer.pushEvent(EVENT_WEBRTC_CLOSE, null); | |
} | |
} | |
}; | |
/** | |
* @param {!Array<string>} servers | |
*/ | |
webrtcImports["serverLANInitializeServer"] = function(servers) { | |
lanServer.iceServers.length = 0; | |
for (let url of servers) { | |
let etr = url.split(";"); | |
if(etr.length === 1) { | |
lanServer.iceServers.push({ | |
"urls": etr[0] | |
}); | |
}else if(etr.length === 3) { | |
lanServer.iceServers.push({ | |
"urls": etr[0], | |
"username": etr[1], | |
"credential": etr[2] | |
}); | |
} | |
} | |
}; | |
webrtcImports["serverLANCloseServer"] = function() { | |
for (let thePeer of Object.values(lanServer.peerList)) { | |
if (thePeer) { | |
try { | |
thePeer.disconnect(); | |
} catch (e) {} | |
thePeer.pushEvent(EVENT_WEBRTC_CLOSE, null); | |
} | |
} | |
lanServer.peerList.clear(); | |
}; | |
/** | |
* @param {string} peer | |
*/ | |
webrtcImports["serverLANCreatePeer"] = function(peer) { | |
try { | |
const events = new EaglerLinkedQueue(); | |
/** @type {!LANPeerInternal} */ | |
let peerInstance; | |
peerInstance = { | |
peerId: peer, | |
peerConnection: new RTCPeerConnection(/** @type {RTCConfiguration} */ ({ | |
"iceServers": lanServer.iceServers, | |
"optional": [ | |
{ | |
"DtlsSrtpKeyAgreement": true | |
} | |
] | |
})), | |
/** @type {RTCDataChannel} */ | |
dataChannel: null, | |
/** @type {string|null} */ | |
ipcChannel: null, | |
pushEvent: function(type, data) { | |
events.push({ | |
"type": type, | |
"data": data, | |
"_next": null | |
}); | |
}, | |
disconnect: function() { | |
if (peerInstance.dataChannel) peerInstance.dataChannel.close(); | |
peerInstance.peerConnection.close(); | |
} | |
}; | |
lanServer.peerList.set(peerInstance.peerId, peerInstance); | |
const iceCandidates = []; | |
peerInstance.peerConnection.addEventListener("icecandidate", /** @type {function(Event)} */ ((/** RTCPeerConnectionIceEvent */ evt) => { | |
if(evt.candidate) { | |
if(iceCandidates.length === 0) { | |
const candidateState = [0, 0]; | |
const runnable = () => { | |
if(peerInstance.peerConnection !== null && peerInstance.peerConnection.connectionState !== "disconnected") { | |
const trial = ++candidateState[1]; | |
if(candidateState[0] !== iceCandidates.length && trial < 3) { | |
candidateState[0] = iceCandidates.length; | |
setTimeout(runnable, 2000); | |
return; | |
} | |
peerInstance.pushEvent(EVENT_WEBRTC_ICE, JSON.stringify(iceCandidates)); | |
iceCandidates.length = 0; | |
} | |
}; | |
setTimeout(runnable, 2000); | |
} | |
iceCandidates.push({ | |
"sdpMLineIndex": evt.candidate.sdpMLineIndex, | |
"candidate": evt.candidate.candidate | |
}); | |
} | |
})); | |
let evtHandler; | |
evtHandler = (/** RTCDataChannelEvent */ evt) => { | |
if (iceCandidates.length > 0) { | |
setTimeout(evtHandler, 10, evt); | |
return; | |
} | |
if (!evt.channel) return; | |
const newDataChannel = evt.channel; | |
if(peerInstance.dataChannel !== null) { | |
newDataChannel.close(); | |
return; | |
} | |
peerInstance.dataChannel = newDataChannel; | |
peerInstance.pushEvent(EVENT_WEBRTC_OPEN, null); | |
peerInstance.dataChannel.addEventListener("message", (evt2) => { | |
const data = evt2.data; | |
if(peerInstance.ipcChannel) { | |
sendIPCPacketFunc(peerInstance.ipcChannel, data); | |
}else { | |
peerInstance.pushEvent(EVENT_WEBRTC_PACKET, new Uint8Array(data)); | |
} | |
}); | |
}; | |
peerInstance.peerConnection.addEventListener("datachannel", /** @type {function(Event)} */ (evtHandler)); | |
peerInstance.peerConnection.addEventListener("connectionstatechange", (evt) => { | |
const connectionState = peerInstance.peerConnection.connectionState; | |
if ("disconnected" === connectionState || "failed" === connectionState) { | |
lanServer.disconnect(peerInstance.peerId); | |
} | |
}); | |
return { | |
"peerId": peerInstance.peerId, | |
/** | |
* @return {number} | |
*/ | |
"countAvailableEvents": function() { | |
return events.getLength(); | |
}, | |
/** | |
* @return {Object} | |
*/ | |
"nextEvent": function() { | |
return events.shift(); | |
}, | |
/** | |
* @param {!Uint8Array} dat | |
*/ | |
"writePacket": function(dat) { | |
let b = false; | |
if (peerInstance.dataChannel !== null && "open" === peerInstance.dataChannel.readyState) { | |
try { | |
peerInstance.dataChannel.send(dat); | |
} catch (e) { | |
b = true; | |
} | |
} else { | |
b = true; | |
} | |
if(b) { | |
lanServer.disconnect(peerInstance.peerId); | |
} | |
}, | |
/** | |
* @param {string} iceCandidates | |
*/ | |
"handleRemoteICECandidates": function(iceCandidates) { | |
try { | |
const candidateList = /** @type {!Array<!RTCIceCandidateInit>} */ (JSON.parse(iceCandidates)); | |
for (let candidate of candidateList) { | |
peerInstance.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); | |
} | |
} catch (err) { | |
eagError("Failed to parse ice candidate for \"{}\"! {}", peerInstance.peerId, err.message); | |
lanServer.disconnect(peerInstance.peerId); | |
} | |
}, | |
/** | |
* @param {string} desc | |
*/ | |
"handleRemoteDescription": function(desc) { | |
try { | |
const remoteDesc = /** @type {!RTCSessionDescription} */ (JSON.parse(desc)); | |
peerInstance.peerConnection.setRemoteDescription(remoteDesc).then(() => { | |
if (remoteDesc.hasOwnProperty("type") && "offer" === remoteDesc["type"]) { | |
peerInstance.peerConnection.createAnswer().then((desc) => { | |
peerInstance.peerConnection.setLocalDescription(desc).then(() => { | |
peerInstance.pushEvent(EVENT_WEBRTC_DESC, JSON.stringify(desc)); | |
}).catch((err) => { | |
eagError("Failed to set local description for \"{}\"! {}", peerInstance.peerId, err.message); | |
lanServer.disconnect(peerInstance.peerId); | |
}); | |
}).catch((err) => { | |
eagError("Failed to create answer for \"{}\"! {}", peerInstance.peerId, err.message); | |
lanServer.disconnect(peerInstance.peerId); | |
}); | |
} | |
}).catch((err) => { | |
eagError("Failed to set remote description for \"{}\"! {}", peerInstance.peerId, err.message); | |
lanServer.disconnect(peerInstance.peerId); | |
}); | |
} catch (err) { | |
eagError("Failed to parse remote description for \"{}\"! {}", peerInstance.peerId, err.message); | |
lanServer.disconnect(peerInstance.peerId); | |
} | |
}, | |
/** | |
* @param {string|null} ipcChannel | |
*/ | |
"mapIPC": function(ipcChannel) { | |
if(!peerInstance.ipcChannel) { | |
if(ipcChannel) { | |
peerInstance.ipcChannel = ipcChannel; | |
lanServer.ipcMapList.set(ipcChannel, peerInstance); | |
} | |
}else { | |
if(!ipcChannel) { | |
lanServer.ipcMapList.delete(peerInstance.ipcChannel); | |
peerInstance.ipcChannel = null; | |
} | |
} | |
}, | |
"disconnect": function() { | |
lanServer.disconnect(peerInstance.peerId); | |
} | |
}; | |
}catch(/** Error */ tt) { | |
eagStackTrace(ERROR, "Failed to create WebRTC LAN peer!", tt); | |
return null; | |
} | |
}; | |
/** | |
* @param {string} channel | |
* @param {!ArrayBuffer} arr | |
*/ | |
serverLANPeerPassIPCFunc = function(channel, arr) { | |
const peer = lanServer.ipcMapList.get(channel); | |
if(peer) { | |
let b = false; | |
if (peer.dataChannel && "open" === peer.dataChannel.readyState) { | |
try { | |
peer.dataChannel.send(arr); | |
} catch (e) { | |
b = true; | |
} | |
} else { | |
b = true; | |
} | |
if(b) { | |
lanServer.disconnect(peer.peerId); | |
} | |
return true; | |
}else { | |
return false; | |
} | |
}; | |
} | |
function initializeNoPlatfWebRTC(webrtcImports) { | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "supported"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANReadyState"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANCloseConnection"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANSendPacket"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANReadPacket"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANAvailable"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANSetICEServersAndConnect"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clearLANClientState"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANAwaitICECandidate"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANAwaitDescription"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANAwaitChannel"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANClosed"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANSetICECandidate"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANSetDescription"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "clientLANClosed"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "serverLANInitializeServer"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "serverLANCloseServer"); | |
setUnsupportedFunc(webrtcImports, platfWebRTCName, "serverLANCreatePeer"); | |
} | |