/* * libdatachannel example web client * Copyright (C) 2020 Lara Mackey * Copyright (C) 2020 Paul-Louis Ageneau * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; If not, see . */ window.addEventListener('load', () => { const config = { iceServers: [{ urls: 'stun:stun.l.google.com:19302', // change to your STUN server }], }; const localId = randomId(8); const url = `ws://localhost:8000/${localId}`; const peerConnectionMap = {}; const dataChannelMap = {}; const offerId = document.getElementById('offerId'); const offerBtn = document.getElementById('offerBtn'); const sendMsg = document.getElementById('sendMsg'); const sendBtn = document.getElementById('sendBtn'); const _localId = document.getElementById('localId'); _localId.textContent = localId; console.log('Connecting to signaling...'); openSignaling(url) .then((ws) => { console.log('WebSocket connected, signaling ready'); offerId.disabled = false; offerBtn.disabled = false; offerBtn.onclick = () => offerPeerConnection(ws, offerId.value); }) .catch((err) => console.error(err)); function openSignaling(url) { return new Promise((resolve, reject) => { const ws = new WebSocket(url); ws.onopen = () => resolve(ws); ws.onerror = () => reject(new Error('WebSocket error')); ws.onclosed = () => console.error('WebSocket disconnected'); ws.onmessage = (e) => { if(typeof(e.data) != 'string') return; const message = JSON.parse(e.data); const { id, type } = message; let pc = peerConnectionMap[id]; if(!pc) { if(type != 'offer') return; // Create PeerConnection for answer console.log(`Answering to ${id}`); pc = createPeerConnection(ws, id); } switch(type) { case 'offer': case 'answer': pc.setRemoteDescription({ sdp: message.description, type: message.type, }) .then(() => { if(type == 'offer') { // Send answer sendLocalDescription(ws, id, pc, 'answer'); } }); break; case 'candidate': pc.addIceCandidate({ candidate: message.candidate, sdpMid: message.mid, }); break; } } }); } function offerPeerConnection(ws, id) { // Create PeerConnection console.log(`Offering to ${id}`); pc = createPeerConnection(ws, id); // Create DataChannel const label = "test"; console.log(`Creating DataChannel with label "${label}"`); const dc = pc.createDataChannel(label); setupDataChannel(dc, id); // Send offer sendLocalDescription(ws, id, pc, 'offer'); } // Create and setup a PeerConnection function createPeerConnection(ws, id) { const pc = new RTCPeerConnection(config); pc.onconnectionstatechange = () => console.log(`Connection state: ${pc.connectionState}`); pc.onicegatheringstatechange = () => console.log(`Gathering state: ${pc.iceGatheringState}`); pc.onicecandidate = (e) => { if (e.candidate) { // Send candidate sendLocalCandidate(ws, id, e.candidate); } }; pc.ondatachannel = (e) => { const dc = e.channel; console.log(`"DataChannel from ${id} received with label "${dc.label}"`); setupDataChannel(dc, id); dc.send(`Hello from ${localId}`); sendMsg.disabled = false; sendBtn.disabled = false; sendBtn.onclick = () => dc.send(sendMsg.value); }; peerConnectionMap[id] = pc; return pc; } // Setup a DataChannel function 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}`); document.body.appendChild(document.createTextNode(e.data)); }; dataChannelMap[id] = dc; return dc; } 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, })); } // Helper function to generate a random ID function randomId(length) { const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const pickRandom = () => characters.charAt(Math.floor(Math.random() * characters.length)); return [...Array(length)].map(pickRandom).join(''); } });