Skip to content
Snippets Groups Projects
Commit cb61c429 authored by Simon Döring's avatar Simon Döring
Browse files

Finish sender UI and implement name change

parent d025f9c0
No related branches found
No related tags found
No related merge requests found
......@@ -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,17 +58,14 @@ const handleSetFeedId = (
throw new ValidationError('No feed id was provided');
}
const unescapedCustomName = data.customName;
let unescapedCustomName = data.customName;
if (unescapedCustomName != null) {
unescapedCustomName = unescapedCustomName.trim();
if (unescapedCustomName.length > 0) {
console.log(
`Got custom name from slot ${slot}: ${unescapedCustomName}`
);
const customName = unescapedCustomName
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
const customName = escapeHTML(unescapedCustomName);
if (unescapedCustomName !== customName) {
console.log(
`Warning: Escaped custom name (${customName}) does not equal the unescaped custom name` +
......@@ -75,6 +73,11 @@ const handleSetFeedId = (
);
}
notifyCustomName(slot, customName);
} else {
console.log(
'Error: Got a name that is either empty or consists only of whitespaces'
);
}
}
console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
......@@ -96,29 +99,83 @@ const handleSetFeedId = (
fn({ success, message });
};
const handleChangeName = (
socket: SenderSocket,
data: null | { newName?: string }
) => {
const slot = socket.cameraSlot;
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(
`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`
);
}
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;
if (slot != null) {
const currentSlotState = cameraSlotState[slot];
if (
currentSlotState.feedActive &&
socket.id === currentSlotState.senderSocketId
) {
console.log(
'Sender on slot ' + slot + ' disconnected - Clearing slot'
);
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);
......
export const escapeHTML = (raw: string) => {
return raw
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
......@@ -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;
}
}
......@@ -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>
<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>
<div id="preview-container">Preview</div>
<div id="preview-container" class="hidden"></div>
<section class="main__options">
<div class="options__toggle-wrapper">
<div id="options-toggle" class="options__toggle">Advanced options</div>
</div>
<div id="options" class="options__body options__body--hidden">
<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>
......
......@@ -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').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,7 +229,6 @@ 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,
......@@ -220,17 +246,12 @@ document.addEventListener('DOMContentLoaded', function() {
},
error: function(error) {
Janus.error('WebRTC error:', error);
alert('WebRTC error: ' + error.message);
setStatusMessage(`Janus WebRTC error: ${error.message} - Reload to try again`, STATUS_CODE.error);
}
});
//} else {
// alert('There is already somebody who is sharing his camera in this room!');
// window.location.reload();
//}
}
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);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment