2022-03-14 15:33:14 -04:00
<!doctype html>
< html >
2022-03-15 09:25:02 -04:00
<!--
-- WebRTC demo
2024-02-29 22:02:08 +01:00
--
2022-03-15 09:25:02 -04:00
-- Author: Steffen Vogel < post @ steffenvogel . de >
-- SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
2022-07-04 18:20:03 +02:00
-- SPDX-License-Identifier: Apache-2.0
2022-03-15 09:25:02 -04:00
-->
2022-03-14 15:33:14 -04:00
< 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;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
#log {
display: inline;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
#received-container {
overflow: auto;
height: 300px;
border: 1px solid darkgray;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
#log-container {
overflow: auto;
height: 300px;
border: 1px solid darkgray;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
.log-warn {
color: orange
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
.log-error {
color: red
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
.log-info {
color: darkblue;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
.log-log {
color: black;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
.log-warn,
.log-error {
font-weight: bold;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
.filename {
background-color: #adadad;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
margin-bottom: -8px;
padding: 5px;
}
2024-02-29 22:02:08 +01:00
2022-03-14 15:33:14 -04:00
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
2023-06-21 15:02:08 +02:00
var server = 'https://villas.k8s.eonerc.rwth-aachen.de/ws/signaling';
2022-03-14 15:33:14 -04:00
var sessionName = 'my-session-name';
var iceUsername = 'villas';
var icePassword = 'villas';
var iceUrls = [
2022-03-29 13:34:45 +02:00
'stun:stun.0l.de:3478',
'turn:turn.0l.de:3478?transport=udp',
'turn:turn.0l.de:3478?transport=tcp'
2022-03-14 15:33:14 -04:00
];
// Functions
// Set things up, connect event listeners, etc.
function startup() {
2022-03-29 13:34:45 +02:00
log = document.getElementById('log');
logContainer = document.getElementById('log-container');
2022-03-14 15:33:14 -04:00
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);
2022-03-29 13:34:45 +02:00
let queryParams = new URLSearchParams(window.location.search)
let sessionNameQuery = queryParams.get('session_name');
let autoConnect = queryParams.get('auto_connect');
if (sessionNameQuery !== null) {
sessionName = sessionNameQuery;
}
2022-03-14 15:33:14 -04:00
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();
2022-03-29 13:34:45 +02:00
if (autoConnect !== null) {
connectPeers();
}
2022-03-14 15:33:14 -04:00
}
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 > ';
2022-03-29 13:34:45 +02:00
if (isScrolledToBottom) {
2022-03-14 15:33:14 -04:00
logContainer.scrollTop = logContainer.scrollHeight - logContainer.clientHeight;
2022-03-29 13:34:45 +02:00
}
2022-03-14 15:33:14 -04:00
console['old' + name].apply(undefined, arguments);
};
}
2022-03-29 13:34:45 +02:00
for (logger of['log', 'debug', 'warn', 'error', 'info']) {
2022-03-14 15:33:14 -04:00
fixLoggingFunc(logger);
2022-03-29 13:34:45 +02:00
}
2022-03-14 15:33:14 -04:00
}
function updateConfig() {
let cfg = {
nodes: {
webrtc_1: {
type: 'webrtc',
session: sessionName,
server: server,
ice: {
2022-03-14 19:39:34 -04:00
servers: [
{
urls: iceUrls,
username: iceUsername,
password: icePassword
}
]
2022-03-14 15:33:14 -04:00
}
2022-03-14 17:23:15 -04:00
},
siggen_1: {
type: 'signal',
values: 5,
signal: 'random'
2022-03-14 15:33:14 -04:00
}
},
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) {
2022-03-29 13:34:45 +02:00
console.info('Candidate gathering completed');
return;
2022-03-14 15:33:14 -04:00
}
console.info('New local ICE Candidate', event.candidate);
let msg = {
2022-03-29 13:34:45 +02:00
candidate: event.candidate.toJSON()
2022-03-14 15:33:14 -04:00
};
2022-03-29 13:34:45 +02:00
console.info('Sending signaling message', msg);
2022-03-14 15:33:14 -04:00
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.
2022-04-07 13:16:18 +02:00
async function handleDataChannelMessage(event) {
2022-03-14 15:33:14 -04:00
var dec = new TextDecoder();
var raw = event.data;
2022-04-07 13:16:18 +02:00
var msg = dec.decode(await raw.arrayBuffer());
2022-03-14 15:33:14 -04:00
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 >
2022-03-14 19:39:34 -04:00
villas signal sine -r 5 | villas pipe webrtc.conf webrtc_1
2022-03-14 15:33:14 -04:00
< / 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 >