mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-09 00:00:00 +01:00
556 lines
21 KiB
HTML
556 lines
21 KiB
HTML
<!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>
|