<!doctype html> <html> <!-- -- WebRTC demo -- -- Author: Steffen Vogel <post@steffenvogel.de> -- SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University -- SPDX-License-Identifier: Apache-2.0 --> <head> <title>VILLASnode: Simple WebRTC node example</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.27.0/themes/prism.css"> <style> pre, .filename { font-size: small !important; } #log { display: inline; } #received-container { overflow: auto; height: 300px; border: 1px solid darkgray; } #log-container { overflow: auto; height: 300px; border: 1px solid darkgray; } .log-warn { color: orange } .log-error { color: red } .log-info { color: darkblue; } .log-log { color: black; } .log-warn, .log-error { font-weight: bold; } .filename { background-color: #adadad; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; margin-bottom: -8px; padding: 5px; } h3, h4, h5 { margin-top: 15px; } </style> <script> (function() { var btnConnect = null; var btnDisconnect = null; var btnSend = null; var inpMessage = null; var inpSessionName = null; var inpServer = null; var inpUsername = null; var inpPassword = null; var received = null; var receivedContainer = null; var villasConfig = null; var first = false; var polite = false; var ignoreOffer = false; var makingOffer = false; var pc = null; // RTCPeerConnection for our 'local' connection var dc = null; // RTCDataChannel for the local (sender) var sc = null; // Signaling Client var server = 'https://villas.k8s.eonerc.rwth-aachen.de/ws/signaling'; var sessionName = 'my-session-name'; var iceUsername = 'villas'; var icePassword = 'villas'; var iceUrls = [ 'stun:stun.0l.de:3478', 'turn:turn.0l.de:3478?transport=udp', 'turn:turn.0l.de:3478?transport=tcp' ]; // Functions // Set things up, connect event listeners, etc. function startup() { log = document.getElementById('log'); logContainer = document.getElementById('log-container'); rewireLoggingToElement(); btnConnect = document.getElementById('connect'); btnDisconnect = document.getElementById('disconnect'); btnSend = document.getElementById('send'); inpMessage = document.getElementById('message'); inpSessionName = document.getElementById('session-name'); inpServer = document.getElementById('server'); inpIceUsername = document.getElementById('ice-username'); inpIcePassword = document.getElementById('ice-password'); inpIceUrls = document.getElementById('ice-urls'); received = document.getElementById('received'); receivedContainer = document.getElementById('received-container'); villasConfig = document.getElementById('villas-config'); btnConnect.addEventListener('click', connectPeers, false); btnDisconnect.addEventListener('click', disconnectPeers, false); btnSend.addEventListener('click', sendMessage, false); let queryParams = new URLSearchParams(window.location.search) let sessionNameQuery = queryParams.get('session_name'); let autoConnect = queryParams.get('auto_connect'); if (sessionNameQuery !== null) { sessionName = sessionNameQuery; } inpServer.onkeyup = (e) => { server = e.target.value; updateConfig(); }; inpSessionName.onkeyup = (e) => { sessionName = e.target.value; updateConfig(); }; inpIceUsername.onkeyup = (e) => { iceUsername = e.target.value; updateConfig(); }; inpIcePassword.onkeyup = (e) => { icePassword = e.target.value; updateConfig(); }; inpIceUrls.onkeyup = (e) => { iceUrls = e.target.value.split(','); updateConfig(); }; inpSessionName.value = sessionName; inpServer.value = server; inpIcePassword.value = icePassword; inpIceUsername.value = iceUsername; inpIceUrls.value = iceUrls; updateConfig(); if (autoConnect !== null) { connectPeers(); } } function rewireLoggingToElement() { function produceOutput(name, args) { let now = new Date(); let el = document.createElement('span'); el.classList.add('log-' + name); el.innerHTML += '<span>' + now.toLocaleTimeString() + '</span> '; el.innerHTML += '<span>' + (name == 'log' ? 'info' : name) + '</span>'; for (let arg of args) { let a = document.createElement('code'); a.classList.add('log-' + (typeof arg)); if (typeof arg !== 'string' && (JSON || {}).stringify) { a.classList.add('lang-json'); a.innerHTML = JSON.stringify(arg); Prism.highlightElement(a); } else { a.innerHTML = arg; } el.innerHTML += ' '; el.appendChild(a) } return el; } function fixLoggingFunc(name) { console['old' + name] = console[name]; console[name] = function(...arguments) { const output = produceOutput(name, arguments); const isScrolledToBottom = logContainer.scrollHeight - logContainer.clientHeight <= logContainer.scrollTop + 1; log.appendChild(output); log.innerHTML += '<br>'; if (isScrolledToBottom) { logContainer.scrollTop = logContainer.scrollHeight - logContainer.clientHeight; } console['old' + name].apply(undefined, arguments); }; } for (logger of['log', 'debug', 'warn', 'error', 'info']) { fixLoggingFunc(logger); } } function updateConfig() { let cfg = { nodes: { webrtc_1: { type: 'webrtc', session: sessionName, server: server, ice: { servers: [ { urls: iceUrls, username: iceUsername, password: icePassword } ] } }, siggen_1: { type: 'signal', values: 5, signal: 'random' } }, paths: [{ in: ['siggen_1'], out: ['webrtc_1'] }] } villasConfig.innerHTML = JSON.stringify(cfg, null, 4); Prism.highlightAll(); } // Connect the two peers. Normally you look for and connect to a remote // machine here, but we're just connecting two local objects, so we can // bypass that step. function connectPeers() { // Create the local connection and its event listeners pc = new RTCPeerConnection({ iceServers: [{ username: iceUsername, credential: icePassword, urls: iceUrls }] }); sc = new WebSocket(server + '/' + inpSessionName.value); sc.onmessage = handleSignalingMessage; pc.onicecandidate = handleIceCandidate; pc.onnegotiationneeded = handleNegotationNeeded; pc.ondatachannel = handleNewDataChannel // Some more logging sc.onopen = (e) => console.info('Connected to signaling channel', e); sc.onerror = (e) => console.error('Failed to establish signaling connection', e); pc.onconnectionstatechange = () => console.info('Connection state changed:', pc.connectionState); pc.onsignalingstatechange = () => console.info('Signaling state changed:', pc.signalingState); pc.oniceconnectionstatechange = () => console.info('ICE connection state changed:', pc.iceConnectionState); pc.onicegatheringstatechange = () => console.info('ICE gathering state changed:', pc.iceGatheringState); } async function handleNegotationNeeded() { console.info('Negotation needed!'); try { makingOffer = true; await pc.setLocalDescription(); let msg = { description: pc.localDescription.toJSON() }; console.info('Sending signaling message', msg); sc.send(JSON.stringify(msg)); } catch (err) { console.error(err); } finally { makingOffer = false; } } function handleIceCandidate(event) { if (event.candidate == null) { console.info('Candidate gathering completed'); return; } console.info('New local ICE Candidate', event.candidate); let msg = { candidate: event.candidate.toJSON() }; console.info('Sending signaling message', msg); sc.send(JSON.stringify(msg)); } async function handleSignalingMessage(event) { let msg = JSON.parse(event.data); console.info('Received signaling message', msg); try { if (msg.control !== undefined) { first = true; for (connection of msg.control.connections) { if (connection.id < msg.control.connection_id) first = false; } polite = first; console.info('Role', { polite: polite, first: first }) if (!first) { // Create the data channel and establish its event listeners ch = pc.createDataChannel('villas'); handleDataChannel(ch); } } else if (msg.description !== undefined) { const offerCollision = (msg.description.type == 'offer') && (makingOffer || pc.signalingState != 'stable'); ignoreOffer = !polite && offerCollision; if (ignoreOffer) { return; } await pc.setRemoteDescription(msg.description); console.info(msg.description); if (msg.description.type == 'offer') { await pc.setLocalDescription(); let msg = { description: pc.localDescription.toJSON() } sc.send(JSON.stringify(msg)) } } else if (msg.candidate !== undefined) { try { console.info('New remote ICE candidate', msg.candidate); await pc.addIceCandidate(msg.candidate); } catch (err) { if (!ignoreOffer) { throw err; } } } } catch (err) { console.error(err); } } // Handles clicks on the 'Send' button by transmitting // a message to the remote peer. function sendMessage() { var msg = inpMessage.value; console.info('Sending message', msg); dc.send(msg); // Clear the input box and re-focus it, so that we're // ready for the next message. inpMessage.value = ''; inpMessage.focus(); } function handleNewDataChannel(e) { console.info('New datachannel', e.channel) handleDataChannel(e.channel); } function handleDataChannel(ch) { dc = ch; dc.onopen = () => console.info('Datachannel opened'); dc.onclose = () => console.info('Datachannel closed'); dc.onmessage = handleDataChannelMessage; } // Handle onmessage events for the receiving channel. // These are the data messages sent by the sending channel. async function handleDataChannelMessage(event) { var dec = new TextDecoder(); var raw = event.data; var msg = dec.decode(await raw.arrayBuffer()); var msgJson = JSON.parse(msg); console.info('Received message', msgJson); var el = document.createElement('span'); el.innerHTML = msg; Prism.highlightAllUnder(el); const isScrolledToBottom = receivedContainer.scrollHeight - receivedContainer.clientHeight <= receivedContainer.scrollTop + 1; received.appendChild(el) received.innerHTML += '<br>'; if (isScrolledToBottom) receivedContainer.scrollTop = receivedContainer.scrollHeight - receivedContainer.clientHeight; } // Close the connection, including data channels if they're open. // Also update the UI to reflect the disconnected status. function disconnectPeers() { sc.close() dc.close(); pc.close(); dc = null; pc = null; sc = null; } // Set up an event listener which will run the startup // function once the page is done loading. window.addEventListener('load', startup, false); })(); </script> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="#"> <img src="https://git.rwth-aachen.de/acs/public/villas/node/-/raw/master/doc/pictures/villas_node.svg" alt="VILLASnode logo" width="30" height="24" class="d-inline-block align-text-top"> VILLASnode: Simple WebRTC node example </a> <div class="btn-group" role="group" aria-label="Basic example"> <button class="btn btn-outline-success" id="connect">Connect</button> <button class="btn btn-outline-danger" id="disconnect">Disconnect</button> </div> </div> </nav> <h1></h1> <div class="container"> <div class="row"> <h3>Configuration</h3> <div class="col"> <div class="mb-3"> <label class="form-label" for="message">Session name:</label> <input class="form-control" type="text" id="session-name"> </div> <div class="mb-3"> <label class="form-label" for="server">Signaling Server:</label> <input class="form-control" type="text" id="server"> </div> </div> <div class="col"> <div class="row"> <div class="col mb-3"> <label class="form-label" for="ice-username">ICE Username:</label> <input class="form-control" type="text" id="ice-username"> </div> <div class="col mb-3"> <label class="form-label" for="ice-password">ICE Password:</label> <input class="form-control" type="text" id="ice-password"> </div> </div> <div class="mb-3"> <label class="form-label" for="ice-urls">ICE URLs:</label> <input class="form-control" type="text" id="ice-urls"> </div> </div> </div> <h3>VILLASnode setup <button class="btn-sm btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">Show</button></h3> <div class="collapse" id="collapseExample"> <div class="card card-body"> <h4>Config</h4> <p>Copy the contents of the following file into a file named <code>webrtc.conf</code>:</p> <div> <div class="filename">webrtc.conf</div> <pre><code class="language-json" id="villas-config"></code></pre> </div> <h4>Invocation</h4> <p> Make sure <code>webrtc.conf</code> is in your current working directory and then run the following command:</p> <pre><code> villas signal sine -r 5 | villas pipe webrtc.conf webrtc_1 </code></pre> <h5>With Docker</h5> You can also start VILLASnode via <a href="https://docs.docker.com/get-docker/">Docker</a> by running the following command before the commands in the previous section. <pre><code> alias villas='docker run -v $(pwd):/mount -w /mount registry.git.rwth-aachen.de/acs/public/villas/node' </code></pre> </div> </div> <h3>Send message</h3> <div class="input-group"> <input type="text" class="form-control" id="message" placeholder="Message text"> <button id="send" class="btn-sm btn-primary">Send</button> </div> <h3>Log</h3> <div id="log-container"> <pre id="log">Press "Connect" in the upper right corner to start<br></pre> </div> <h3>Received messages</h3> <div id="received-container"> <pre id="received"></pre> </div> <footer class="bg-light text-center text-lg-start fixed-bottom"> <div class="text-center p-3" style="background-color: rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important;"> © 2022 Copyright: <a class="text-dark" href="https://www.acs.eonerc.rwth-aachen.de/">Institute for Automation of Complex Power Systems, RWTH Aachen University</a> </div> </footer> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/prismjs@1.27.0/prism.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/prismjs@1.27.0/plugins/autoloader/prism-autoloader.min.js"></script> </body> </html>