<!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>&nbsp;';
                    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 += '&nbsp;';
                        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>