From cb61c429625c6e63e420c9ec3308c3d17786af0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simon=20D=C3=B6ring?= <simon.doering@stud.hs-bochum.de>
Date: Fri, 15 Jan 2021 22:28:59 +0100
Subject: [PATCH] Finish sender UI and implement name change

---
 .../src/socket-io/handlers/sender-handlers.ts | 125 +++++++---
 camera-server/src/util/escape-html.ts         |   8 +
 sender/camera-sender.css                      | 181 +++++++++++---
 sender/camera-sender.html                     |  47 ++--
 sender/camera-sender.js                       | 228 ++++++++++++------
 5 files changed, 428 insertions(+), 161 deletions(-)
 create mode 100644 camera-server/src/util/escape-html.ts

diff --git a/camera-server/src/socket-io/handlers/sender-handlers.ts b/camera-server/src/socket-io/handlers/sender-handlers.ts
index 5fbcc69..2c18aba 100644
--- a/camera-server/src/socket-io/handlers/sender-handlers.ts
+++ b/camera-server/src/socket-io/handlers/sender-handlers.ts
@@ -3,6 +3,7 @@ import { emitNewFeed, emitRemoveFeed } from './common-handlers';
 import { SenderSocket } from '../../models/sender-socket';
 import { ValidationError } from '../../models/validation-error';
 import { notifyCustomName } from '../../io-interface/handlers/output-handlers';
+import { escapeHTML } from '../../util/escape-html';
 
 const handleSetFeedId = (
     socket: SenderSocket,
@@ -57,24 +58,26 @@ const handleSetFeedId = (
             throw new ValidationError('No feed id was provided');
         }
 
-        const unescapedCustomName = data.customName;
+        let unescapedCustomName = data.customName;
         if (unescapedCustomName != null) {
-            console.log(
-                `Got custom name from slot ${slot}: ${unescapedCustomName}`
-            );
-            const customName = unescapedCustomName
-                .replace(/&/g, '&amp;')
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/"/g, '&quot;')
-                .replace(/'/g, '&#039;');
-            if (unescapedCustomName !== customName) {
+            unescapedCustomName = unescapedCustomName.trim();
+            if (unescapedCustomName.length > 0) {
+                console.log(
+                    `Got custom name from slot ${slot}: ${unescapedCustomName}`
+                );
+                const customName = escapeHTML(unescapedCustomName);
+                if (unescapedCustomName !== customName) {
+                    console.log(
+                        `Warning: Escaped custom name (${customName}) does not equal the unescaped custom name` +
+                            `(${unescapedCustomName}) on slot ${slot} - this could mean that the user tried a Cross-Site-Scripting (XSS) attack`
+                    );
+                }
+                notifyCustomName(slot, customName);
+            } else {
                 console.log(
-                    `Warning: Escaped custom name (${customName}) does not equal the unescaped custom name` +
-                        `(${unescapedCustomName}) on slot ${slot} - this could mean that the user tried a Cross-Site-Scripting (XSS) attack`
+                    'Error: Got a name that is either empty or consists only of whitespaces'
                 );
             }
-            notifyCustomName(slot, customName);
         }
 
         console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
@@ -96,29 +99,83 @@ const handleSetFeedId = (
 
     fn({ success, message });
 };
-
-const handleSenderDisconnect = (socket: SenderSocket, _: string) => {
+const handleChangeName = (
+    socket: SenderSocket,
+    data: null | { newName?: string }
+) => {
     const slot = socket.cameraSlot;
-    if (slot != null) {
-        const currentSlotState = cameraSlotState[slot];
-        if (
-            currentSlotState.feedActive &&
-            socket.id === currentSlotState.senderSocketId
-        ) {
+    const currentSlotState = cameraSlotState[slot];
+
+    if (!currentSlotState.feedActive) {
+        console.log(
+            'Error: Got change_name event on slot ' +
+                slot +
+                ' which has no active feed'
+        );
+        return;
+    }
+
+    if (socket.id !== currentSlotState.senderSocketId) {
+        console.log(
+            'Error: Got change_name event on slot ' +
+                slot +
+                ' from somebody who is not the sender'
+        );
+        return;
+    }
+
+    if (data == null) {
+        console.log(
+            'Error: Got change_name event with no data on slot ' + slot
+        );
+        return;
+    }
+
+    let unescapedNewName = data.newName;
+    if (unescapedNewName == null) {
+        console.log(
+            'Error: Got change_name event with no new name on slot' + slot
+        );
+        return;
+    }
+
+    unescapedNewName = unescapedNewName.trim();
+    if (unescapedNewName.length > 0) {
+        console.log(`Got new name for slot ${slot}: ${unescapedNewName}`);
+        const newName = escapeHTML(unescapedNewName);
+        if (unescapedNewName !== newName) {
             console.log(
-                'Sender on slot ' + slot + ' disconnected - Clearing slot'
+                `Warning: Escaped new name (${newName}) does not equal the unescaped new name` +
+                    `(${unescapedNewName}) on slot ${slot} - this could mean that the user tried a Cross-Site-Scripting (XSS) attack`
             );
-            currentSlotState.feedActive = false;
-            currentSlotState.feedId = null;
-            currentSlotState.senderSocketId = null;
-
-            emitRemoveFeed(slot);
         }
+        notifyCustomName(slot, newName);
+    } else {
+        console.log(
+            'Error: Got a name that is either empty or consists only of whitespaces'
+        );
+    }
+};
+
+const handleSenderDisconnect = (socket: SenderSocket, _: string) => {
+    const slot = socket.cameraSlot;
+    const currentSlotState = cameraSlotState[slot];
+    if (
+        currentSlotState.feedActive &&
+        socket.id === currentSlotState.senderSocketId
+    ) {
+        console.log('Sender on slot ' + slot + ' disconnected - Clearing slot');
+        currentSlotState.feedActive = false;
+        currentSlotState.feedId = null;
+        currentSlotState.senderSocketId = null;
+
+        emitRemoveFeed(slot);
     }
 };
 
 const registerSenderHandlers = (socket: SenderSocket) => {
     socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
+    socket.on('change_name', handleChangeName.bind(null, socket));
     socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
 };
 
@@ -149,7 +206,7 @@ export const handleSenderInit = (
                     ' that cannot be parsed to a number'
             );
             throw new ValidationError(
-                'Slot ' + slotStr + ' cannot be parsed to number'
+                'An invalid camera slot was provided (' + slotStr + ')'
             );
         }
         if (slot < 0 || slot > cameraSlotState.length - 1) {
@@ -158,9 +215,7 @@ export const handleSenderInit = (
                     slot +
                     ' which is not in the list of slots'
             );
-            throw new ValidationError(
-                'Slot ' + slot + ' is not in the list of slots'
-            );
+            throw new ValidationError('This camera slot does not exist');
         }
 
         const slotState = cameraSlotState[slot];
@@ -168,7 +223,9 @@ export const handleSenderInit = (
             console.log(
                 'Error: Got socket connection for inactive slot ' + slot
             );
-            throw new ValidationError('Slot ' + slot + ' is not active');
+            throw new ValidationError(
+                'This camera slot is not activated, contact your moderator'
+            );
         }
 
         const token = data.token;
@@ -183,7 +240,7 @@ export const handleSenderInit = (
                     ' for slot ' +
                     slot
             );
-            throw new ValidationError('Invalid token');
+            throw new ValidationError('Invalid token, contact your moderator');
         }
 
         console.log('Got sender socket connection on slot ' + slot);
diff --git a/camera-server/src/util/escape-html.ts b/camera-server/src/util/escape-html.ts
new file mode 100644
index 0000000..5905bce
--- /dev/null
+++ b/camera-server/src/util/escape-html.ts
@@ -0,0 +1,8 @@
+export const escapeHTML = (raw: string) => {
+    return raw
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#039;');
+};
diff --git a/sender/camera-sender.css b/sender/camera-sender.css
index f69b6f2..04653ed 100644
--- a/sender/camera-sender.css
+++ b/sender/camera-sender.css
@@ -4,6 +4,19 @@
     box-sizing: border-box;
 }
 
+:root {
+    --green-1: #23fb64;
+    --green-2: #05f04b;
+    --green-3: #04b439;
+    --red-1: #e75f68;
+    --red-2: #e23c47;
+    --yellow-1: #e47e11;
+    --light-blue-1: #cde0e6;
+    --light-blue-2: #91d9f1;
+    --light-blue-3: #75bfd9;
+    --background-color: #eee;
+}
+
 body {
     font-size: 16px;
     margin: 0;
@@ -18,10 +31,6 @@ body {
     }
 }
 
-.visually-hidden {
-    visibility: hidden;
-}
-
 .main__seperator {
     margin: 16px 0;
 }
@@ -33,6 +42,7 @@ body {
     padding: 16px;
     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
     border-radius: 8px;
+    background: var(--background-color);
 }
 
 .main__header {
@@ -40,13 +50,26 @@ body {
 }
 
 .main__header-title {
+    margin: 0 0 8px;
+}
+
+.main__header-subtitle {
     margin: 0;
 }
 
-#connection-status {
-    margin-bottom: 8px;
+#status {
+    margin: 0 auto 8px;
+    max-width: 80%;
     text-align: center;
-    color: green;
+    color: var(--green-3);
+}
+
+#status[data-status-code="1"] {
+    color: var(--yellow-1);
+}
+
+#status[data-status-code="2"] {
+    color: var(--red-2);
 }
 
 .spinner-wrapper {
@@ -62,15 +85,11 @@ body {
     height: 30px;
     border-radius: 50%;
     border: solid transparent 2px;
-    --border-color: #00bfff;
+    --border-color: var(--light-blue-3);
     border-top-color: var(--border-color);
     border-bottom-color: var(--border-color);
 }
 
-.main__room-form {
-    margin: 0;
-}
-
 .form-control {
     margin-bottom: 8px;
     display: flex;
@@ -80,49 +99,90 @@ body {
 }
 
 .form-control__label {
-    width: 120px;
+    width: 150px;
     padding-right: 4px;
 }
 
 .form-control__input {
-    min-width: 100px;
-    width: 100px;
-    max-width: 200px;
-    flex-grow: 1;
+    min-width: 150px;
+    width: 150px;
+    max-width: 250px;
+    flex: 1;
 }
 
-#preview-container {
-    height: 100px;
-    background: orange;
+.name-control__form {
     display: flex;
-    align-items: center;
-    justify-content: center;
-    margin-bottom: 16px;
+    margin: 0;
 }
 
-.main__options {
-    margin-bottom: 8px;
+.name-control__input {
+    flex: 1 0 0;
+    min-width: 0;
 }
 
-.options__toggle-wrapper {
-    display: flex;
-    justify-content: center;
-    margin-bottom: 6px;
+.name-control__change {
+    margin-left: 4px;
+}
+
+#preview-container {
+    border-radius: 8px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
+    margin: 12px 0;
+    overflow: hidden;
+}
+
+#camera-preview {
+    width: 100%;
+}
+
+.main__options {
+    margin-bottom: 8px;
 }
 
 .options__toggle {
+    border: none;
+    outline: none;
+    width: 100%;
+    font-size: 14px;
+    position: relative;
     text-align: center;
-    padding: 4px 8px;
-    border-radius: 4px;
-    background: #eee;
+    z-index: 2;
+    background: var(--light-blue-2);
+    border-radius: 8px;
+    padding: 8px;
 }
 
 .options__toggle:hover {
-    background: #ccc;
+    background: var(--light-blue-3);
 }
 
-.options__body--hidden {
-    display: none;
+.options__body {
+    border-radius: 0 0 8px 8px;
+    background: var(--light-blue-1);
+    margin-top: -8px;
+    padding: 16px 8px 8px;
+}
+
+.options__hint {
+    margin: 0 0 4px;
+}
+
+#bandwidth-form {
+    margin-bottom: 0;
+}
+
+.bandwidth-form__control {
+    display: flex;
+}
+
+.bandwidth-form__input {
+    flex: 2 0 50px;
+    min-width: 0;
+    margin-right: 4px;
+}
+
+.bandwidth-form__button {
+    flex: 1 0 0;
 }
 
 .main__controls {
@@ -133,7 +193,54 @@ body {
 
 .main__control-button {
     width: 100%;
-    max-width: 200px;
     margin: 0 4px;
-    
+    outline: none;
+    border: none;
+    border-radius: 4px;
+    padding: 8px 0;
+}
+
+.main__control-button:disabled {
+    filter: grayscale(0.6);
+}
+
+#start {
+    background: var(--green-1);
+}
+
+#start:not(:disabled):hover {
+    background: var(--green-2);
+}
+
+#stop {
+    background: var(--red-1);
+}
+
+#stop:not(:disabled):hover {
+    background: var(--red-2);
+}
+
+#reload {
+    background: var(--light-blue-2);
+}
+
+#reload:hover {
+    background: var(--light-blue-3);
+}
+
+.hidden {
+    display: none;
+}
+
+@media (max-width: 640px) {
+    body {
+        background: var(--background-color);
+    }
+
+    .main {
+        box-shadow: none;
+        border-radius: 0;
+        width: 100%;
+        margin: 8px auto;
+    }
 }
diff --git a/sender/camera-sender.html b/sender/camera-sender.html
index 28cb555..54f4af9 100644
--- a/sender/camera-sender.html
+++ b/sender/camera-sender.html
@@ -2,6 +2,7 @@
     <head>
         <title>Camera Sender</title>
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta charset="utf-8">
         <script type="text/javascript" src="adapter.min.js"></script>
         <script type="text/javascript" src="janus.js"></script>
         <script type="text/javascript" src="socket.io.min.js"></script>
@@ -11,20 +12,21 @@
     <body>
         <main class="main">
             <header class="main__header">
-                <h3 id="room-indicator" class="main__header-title">
+                <h2 class="main__header-title">CVH-Camera</h2>
+                <h3 id="room-indicator" class="main__header-subtitle">
                     Channel&nbsp;<span id="room"></span> &bull; Camera&nbsp;<span id="slot"></span>
                 </h3>
             </header>
 
             <hr class="main__seperator" />
 
-            <div id="connection-status">Connected</div>
+            <div id="status">Connected</div>
 
             <div id="spinner" class="spinner-wrapper">
                 <div class="spinner"></div>
             </div>
 
-            <form id="room-form" class="main__room-form">
+            <div id="inputs-container" class="main__inputs-container hidden">
                 <div class="form-control">
                     <label class="form-control__label" for="res-select">Resolution</label>
                     <select id="res-select" class="form-control__input">
@@ -37,29 +39,31 @@
                     </select>
                 </div>
 
-                <div id="name-control" class="form-control">
-                    <label class="form-control__label" for="name-input">Display name</label>
-                    <input id="name-input" class="form-control__input" required type="text" />
-                </div>
-
-                <div id="pin-control" class="form-control">
+                <div id="pin-control" class="form-control hidden">
                     <label class="form-control__label" for="pin-input">Room PIN</label>
                     <input id="pin-input" class="form-control__input" type="password" placeholder="Leave empty if none" />
                 </div>
-            </form>
 
-            <div id="preview-container">Preview</div>
-
-            <section class="main__options">
-                <div class="options__toggle-wrapper">
-                    <div id="options-toggle" class="options__toggle">Advanced options</div>
+                <div id="name-control" class="form-control hidden">
+                    <label class="form-control__label" for="name-input">Display name</label>
+                    <form id="name-form" class="form-control__input name-control__form">
+                        <input id="name-input" class="name-control__input" required type="text" />
+                        <button disabled title="Change name" id="name-change" class="name-control__change hidden">&gt;</button>
+                    </form>
                 </div>
-                <div id="options" class="options__body options__body--hidden">
+            </div>
+
+            <div id="preview-container" class="hidden"></div>
+
+            <section id="options-container" class="main__options hidden">
+                <button id="options-toggle" class="options__toggle">Advanced options</button>
+                <div id="options" class="options__body hidden">
                     <form id="bandwidth-form">
-                        <label>While the camera is running, a bandwidth cap can be set here. 0 or negative means no cap.</label>
-                        <br />
-                        <input type="text" id="bandwidth-input" placeholder="New bitrate [in kbit/s]" />
-                        <input type="submit" id="bandwidth-submit" value="Change" disabled />
+                        <p class="options__hint">While the camera is running, a bandwidth cap can be set here. 0 or negative means no cap.</p>
+                        <div class="bandwidth-form__control">
+                            <input type="text" placeholder="Bitrate [in kbit/s]" id="bandwidth-input" class="bandwidth-form__input" />
+                            <button type="submit" id="bandwidth-submit" class="bandwidth-form__button" disabled>Change</button>
+                        </div>
                     </form>
                 </div>
             </section>
@@ -67,8 +71,9 @@
             <hr class="main__seperator" />
 
             <div class="main__controls">
-                <button id="start" class="main__control-button" type="submit" disabled>Start</button>
+                <button id="start" class="main__control-button" disabled>Start</button>
                 <button id="stop" class="main__control-button" disabled>Stop</button>
+                <button id="reload" class="main__control-button hidden">Reload</button>
             </div>
         </main>
     </body>
diff --git a/sender/camera-sender.js b/sender/camera-sender.js
index ec29de9..f74a911 100644
--- a/sender/camera-sender.js
+++ b/sender/camera-sender.js
@@ -5,7 +5,6 @@ document.addEventListener('DOMContentLoaded', function() {
     var videoroomHandle = null;
     var sendResolution = 'stdres';
 
-    var roomForm = document.getElementById('room-form');
     var startButton = document.getElementById('start');
     var stopButton = document.getElementById('stop');
 
@@ -19,6 +18,12 @@ document.addEventListener('DOMContentLoaded', function() {
     var customNameAllowed = false;
     var feedId = null;
 
+    var STATUS_CODE = {
+        success: 0,
+        warning: 1,
+        error: 2
+    };
+
     parseRoomFromURL();
     parseSlotFromURL();
     parsePinFromURL();
@@ -32,13 +37,38 @@ document.addEventListener('DOMContentLoaded', function() {
     const socket = io('https://' + window.location.hostname, {
         path: '/socket.io/' + socketNumber
     });
+    setStatusMessage('Connecting to camera server...');
 
-    document.getElementById('options-toggle').onclick = function () {
-        document.getElementById('options').classList.toggle('options__body--hidden');
+    document.getElementById('options-toggle').onclick = function() {
+        document.getElementById('options').classList.toggle('hidden');
     };
 
+    document.getElementById('reload').onclick = function() {
+        window.location.reload();
+    };
+
+    var timeoutTime = 10000;
+    var cameraServerTimeout = setTimeout(function() {
+        setStatusMessage('Camera server connection timeout... Please try again later', STATUS_CODE.error);
+    }, timeoutTime);
+
     registerSocketHandlers();
 
+    function registerSocketHandlers() {
+        socket.on('connect', function() {
+            clearTimeout(cameraServerTimeout);
+            // This event will be triggered on every connect including reconnects
+            // That's why the check is necessary to ensure that the event is only emitted once
+            if (!gotSocketInitResponse) {
+                socket.emit(
+                    'sender_init',
+                    { slot, token },
+                    handleSenderInitResponse
+                );
+            }
+        });
+    };
+
     function handleSenderInitResponse(data) {
         if (!gotSocketInitResponse) {
             gotSocketInitResponse = true;
@@ -46,7 +76,7 @@ document.addEventListener('DOMContentLoaded', function() {
             if (data.success) {
                 initJanus();
             } else {
-                alert('Socket connection error:\n' + data.message);
+                setStatusMessage(`Socket connection error: ${data.message} - Reload to try again`, STATUS_CODE.error);
             }
         }
     }
@@ -62,40 +92,36 @@ document.addEventListener('DOMContentLoaded', function() {
             bandwidthSubmit.removeAttribute('disabled', '');
             stopButton.removeAttribute('disabled');
             stopButton.onclick = function() {
-                janus.destroy();
+                setStatusMessage('Transmission stopped', STATUS_CODE.warning);
+                cleanup();
             };
 
-            var video = document.getElementById('camera-preview');
-            if (video != null) {
-                video.classList.remove('visually-hidden');
+            showVideo();
+            showOptions();
+            if (customNameAllowed) {
+                var nameForm = document.getElementById('name-form');
+                nameForm.onsubmit = function(event) {
+                    event.preventDefault();
+                    const newName = document.getElementById('name-input').value;
+                    socket.emit('change_name', { newName });
+                    setStatusMessage(`Requested name change to: ${newName}`);
+                };
+                var nameChangeButton = document.getElementById('name-change');
+                nameChangeButton.removeAttribute('disabled');
+                showElement(nameChangeButton);
             }
 
-            alert('Sharing camera');
+            setStatusMessage('Sharing camera');
         } else {
-            alert('Error: ' + data.message);
-            window.location.reload();
+            setStatusMessage(`Error: ${data.message} - Reload to try again`, STATUS_CODE.error);
         }
     }
 
-    function registerSocketHandlers() {
-        socket.on('connect', function() {
-            // This event will be triggered on every connect including reconnects
-            // That's why the check is necessary to ensure that the event is only emitted once
-            console.log('socket on connect handler');
-            if (!gotSocketInitResponse) {
-                socket.emit(
-                    'sender_init',
-                    { slot, token },
-                    handleSenderInitResponse
-                );
-            }
-        });
-    };
-
     function initJanus() {
+        setStatusMessage('Initializing Janus...')
         Janus.init({ debug: 'all', callback: function() {
             if (!Janus.isWebrtcSupported()) {
-                alert('No WebRTC support... ');
+                setStatusMessage('Your browser does not support camera transmission', STATUS_CODE.error);
                 return;
             }
 
@@ -108,23 +134,29 @@ document.addEventListener('DOMContentLoaded', function() {
                             videoroomHandle = pluginHandle;
                             Janus.log('Plugin attached! (' + videoroomHandle.getPlugin() + ', id=' + videoroomHandle.getId() + ')');    
 
-                            roomForm.onsubmit = function(event) {
-                                event.preventDefault();
+                            hideSpinner();
+                            showInputs();
+
+                            startButton.onclick = function() {
+                                setStatusMessage('Connecting...');
                                 var resSelect = document.getElementById('res-select');
                                 startButton.setAttribute('disabled', '');
                                 resSelect.setAttribute('disabled', '');
                                 sendResolution = resSelect.value;
                                 Janus.log('sendResolution:', sendResolution);
                                 if (useUserPin) {
-                                    pin = document.getElementById('pin-input').value;
+                                    var pinInputEl = document.getElementById('pin-input');
+                                    pin = pinInputEl.value;
+                                    pinInputEl.setAttribute('disabled', '');
                                 }
                                 shareCamera(pin);
                             };
                             startButton.removeAttribute('disabled');
+                            setStatusMessage('Connected - Click Start to transmit your camera feed');
                         },
                         error: function(error) {
                             Janus.error('Error attaching plugin: ', error);
-                            alert(error);
+                            setStatusMessage(`Janus attach error: ${error} - Reload to try again`, STATUS_CODE.error);
                         },
                         webrtcState: function(on) {
                             if (on) {
@@ -146,9 +178,6 @@ document.addEventListener('DOMContentLoaded', function() {
                                 video.setAttribute('autoplay', '');
                                 video.setAttribute('playsinline', '');
                                 video.setAttribute('muted', 'muted');
-                                if (!transmitting) {
-                                    video.classList.add('visually-hidden');
-                                }
                                 document.getElementById('preview-container').appendChild(video);
                             }
                             Janus.attachMediaStream(document.getElementById('camera-preview'), stream);
@@ -157,12 +186,10 @@ document.addEventListener('DOMContentLoaded', function() {
                 },
                 error: function(error) {
                     Janus.error(error);
-                    alert(error);
-                    window.location.reload();
+                    setStatusMessage(`Janus error: ${error} - Reload to try again`, STATUS_CODE.error);
                 },
                 destroyed: function() {
-                    alert('Stopped');
-                    window.location.reload();
+                    console.log('Janus destroyed!');
                 }
             });
         }});
@@ -202,35 +229,29 @@ document.addEventListener('DOMContentLoaded', function() {
                 Janus.log('Joined event:', msg);
                 feedId = msg.id;
                 Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + feedId);
-                //if (msg['publishers'].length === 0) {
-                    videoroomHandle.createOffer({
-                        media: {
-                            videoSend: true,
-                            video: sendResolution,
-                            audioSend: false,
-                            videoRecv: false
-                        },
-                        success: function(jsep) {
-                            var publish = {
-                                request: 'configure',
-                                audio: false,
-                                video: true
-                            };
-                            videoroomHandle.send({ message: publish, jsep });
-                        },
-                        error: function(error) {
-                            Janus.error('WebRTC error:', error);
-                            alert('WebRTC error: ' + error.message);
-                        }
-                    });
-                //} else {
-                //    alert('There is already somebody who is sharing his camera in this room!');
-                //    window.location.reload();
-                //}
+                videoroomHandle.createOffer({
+                    media: {
+                        videoSend: true,
+                        video: sendResolution,
+                        audioSend: false,
+                        videoRecv: false
+                    },
+                    success: function(jsep) {
+                        var publish = {
+                            request: 'configure',
+                            audio: false,
+                            video: true
+                        };
+                        videoroomHandle.send({ message: publish, jsep });
+                    },
+                    error: function(error) {
+                        Janus.error('WebRTC error:', error);
+                        setStatusMessage(`Janus WebRTC error: ${error.message} - Reload to try again`, STATUS_CODE.error);
+                    }
+                });
             }
             if (event === 'event' && msg['error']) {
-                alert('Error message: ' + msg['error'] + '.\nError object: ' + JSON.stringify(msg, null, 2));
-                window.location.reload();
+                setStatusMessage(`Janus error: ${msg['error']} - Reload to try again`, STATUS_CODE.error);
             }
         }
         if (jsep) {
@@ -238,6 +259,76 @@ document.addEventListener('DOMContentLoaded', function() {
         }
     };
 
+    function setStatusMessage(message, statusCode) {
+        // For error status messages a cleanup is performed automatically
+        // If this is not desired, use warnings
+        if (statusCode == null) {
+            statusCode = STATUS_CODE.success;
+        }
+        var statusEl = document.getElementById('status');
+        statusEl.setAttribute('data-status-code', statusCode);
+        statusEl.innerText = message;
+        if (statusCode === STATUS_CODE.error) {
+            cleanup();
+        }
+    }
+
+    function cleanup() {
+        hideVideo();
+        hideOptions();
+        hideInputs();
+        hideSpinner();
+        showReload();
+        if (videoroomHandle) {
+            videoroomHandle.detach();
+        }
+        if (socket) {
+            socket.disconnect();
+        }
+    }
+
+    function hideElement(el) {
+        el.classList.add('hidden');
+    }
+
+    function showElement(el) {
+        el.classList.remove('hidden');
+    }
+
+    function hideSpinner() {
+        hideElement(document.getElementById('spinner'));
+    }
+
+    function showInputs() {
+        showElement(document.getElementById('inputs-container'));
+    }
+
+    function hideInputs() {
+        hideElement(document.getElementById('inputs-container'));
+    }
+
+    function showOptions() {
+        showElement(document.getElementById('options-container'));
+    }
+
+    function hideOptions() {
+        hideElement(document.getElementById('options-container'));
+    }
+
+    function showVideo() {
+        showElement(document.getElementById('preview-container'));
+    }
+
+    function hideVideo() {
+        hideElement(document.getElementById('preview-container'));
+    }
+
+    function showReload() {
+        showElement(document.getElementById('reload'));
+        hideElement(startButton);
+        hideElement(stopButton);
+    }
+
     function parseRoomFromURL() {
         var urlParams = new URLSearchParams(window.location.search);
         var roomParam = urlParams.get('room');
@@ -268,10 +359,9 @@ document.addEventListener('DOMContentLoaded', function() {
             if (pin === 'none') {
                 pin = '';
             }
-            document.getElementById('pin-container').remove();
-            document.getElementById('pin-hint').remove();
         } else {
             console.log('Got no valid pin in URL search params');
+            showElement(document.getElementById('pin-control'));
         }
     }
 
@@ -289,8 +379,8 @@ document.addEventListener('DOMContentLoaded', function() {
         var urlParams = new URLSearchParams(window.location.search);
         var param = urlParams.get('customNameAllowed');
         customNameAllowed = param != null;
-        if (!customNameAllowed) {
-            document.getElementById('name-container').remove();
+        if (customNameAllowed) {
+            showElement(document.getElementById('name-control'));
         }
     }
 }, false);
-- 
GitLab