WT_DECLARE_WT_MEMBER(1, JavaScriptConstructor, "WebRTCClient", function (WT, client, offerId, offerBtn, sendMsg, sendBtn, textBrowser, localId, url) { this.peerConnectionMap = {}; this.dataChannelMap = {}; this.ws = null; this.config = { iceServers: [ { urls: "stun:amass.fun:5439" }, { urls: "turns:amass.fun", username: 'amass', credential: '88888888' }, ], }; this.log = (text) => { textBrowser.value = `${textBrowser.value}\n${text}`; }; function sendLocalDescription(ws, id, pc, type) { (type == 'offer' ? pc.createOffer() : pc.createAnswer()) .then((desc) => pc.setLocalDescription(desc)) .then(() => { const { sdp, type } = pc.localDescription; ws.send(JSON.stringify({ id, type, description: sdp, })); }); }; function sendLocalCandidate(ws, id, cand) { const { candidate, sdpMid } = cand; ws.send(JSON.stringify({ id, type: 'candidate', candidate, mid: sdpMid, })); }; this.setupDataChannel = (dc, id) => { dc.onopen = () => { console.log(`DataChannel from ${id} open`); sendMsg.disabled = false; sendBtn.disabled = false; sendBtn.onclick = () => dc.send(sendMsg.value); }; dc.onclose = () => { console.log(`DataChannel from ${id} closed`); }; dc.onmessage = (e) => { if (typeof (e.data) != 'string') return; console.log(`Message from ${id} received: ${e.data}`); this.log(`${id}: ${e.data}`); }; this.dataChannelMap[id] = dc; return dc; }; this.createPeerConnection = (ws, id) => { const pc = new RTCPeerConnection(this.config); pc.oniceconnectionstatechange = () => console.log(`Connection state: ${pc.iceConnectionState}`); pc.onicegatheringstatechange = () => console.log(`Gathering state: ${pc.iceGatheringState}`); pc.onicecandidate = (e) => { if (e.candidate && e.candidate.candidate) { sendLocalCandidate(ws, id, e.candidate); } }; pc.ondatachannel = (e) => { const dc = e.channel; console.log(`DataChannel from ${id} received with label "${dc.label}"`); this.setupDataChannel(dc, id); dc.send(`Hello from ${localId}`); sendMsg.disabled = false; sendBtn.disabled = false; sendBtn.onclick = () => dc.send(sendMsg.value); }; this.peerConnectionMap[id] = pc; return pc; }; this.offerPeerConnection = function (ws, id) { console.log(`Offering to ${id}`); pc = this.createPeerConnection(ws, id); const label = "test"; console.log(`Creating DataChannel with label "${label}"`); const dc = pc.createDataChannel(label); this.setupDataChannel(dc, id); sendLocalDescription(ws, id, pc, 'offer'); }; this.openSignaling = function (url) { return new Promise((resolve, reject) => { const ws = new WebSocket(url); ws.onopen = () => resolve(ws); ws.onerror = () => { if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = 0; } reject(new Error('WebSocket error')); }; ws.onclose = () => { if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = 0; } console.error('WebSocket disconnected'); }; ws.onmessage = (e) => { if (typeof (e.data) != 'string') return; const message = JSON.parse(e.data); if (message.type == undefined || message.type != "pong") { console.log(message); } const { id, type } = message; let pc = this.peerConnectionMap[id]; if (!pc) { if (type != 'offer') return; console.log(`Answering to ${id}`); pc = this.createPeerConnection(ws, id); } switch (type) { case 'offer': case 'answer': pc.setRemoteDescription({ sdp: message.description, type: message.type, }).then(() => { if (type == 'offer') { sendLocalDescription(ws, id, pc, 'answer'); } }); break; case 'candidate': pc.addIceCandidate({ candidate: message.candidate, sdpMid: message.mid, }); break; } } }); }; client.wtWebRTCClient = this; this.openSignaling(`${url}/${localId}`).then(ws => { this.log('WebSocket connected, signaling ready'); offerId.disabled = false; offerBtn.disabled = false; offerBtn.onclick = () => { this.offerPeerConnection(ws, offerId.value); }; this.ws = ws; this.pingTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: "ping", })); } else { clearInterval(this.pingTimer); this.pingTimer = 0; } }, 3000); }); });