mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-09 00:00:00 +01:00
webrtc: add example for browser client
This commit is contained in:
parent
5542dfbba1
commit
f516a76f2c
1 changed files with 525 additions and 0 deletions
525
web/webrtc.html
Normal file
525
web/webrtc.html
Normal file
|
@ -0,0 +1,525 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<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 = 'wss://ws-signal.villas.k8s.eonerc.rwth-aachen.de';
|
||||
var sessionName = 'my-session-name';
|
||||
var iceUsername = 'villas';
|
||||
var icePassword = 'villas';
|
||||
var iceUrls = [
|
||||
'stun:edgy.0l.de:3478',
|
||||
'turn:edgy.0l.de:3478?transport=udp',
|
||||
'turn:edgy.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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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: {
|
||||
urls: iceUrls,
|
||||
username: iceUsername,
|
||||
password: icePassword
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
};
|
||||
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.
|
||||
function handleDataChannelMessage(event) {
|
||||
var dec = new TextDecoder();
|
||||
|
||||
var raw = event.data;
|
||||
var msg = dec.decode(raw);
|
||||
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 since -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>
|
Loading…
Add table
Reference in a new issue