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

Add multiple camera slot logic for sender side

parent 3be8192e
No related branches found
No related tags found
No related merge requests found
# Conecpt
This file is used to document the conecpts of the cvh-camera project.
# Socket Traffic
This section describes the socket traffic and the socket.io events that are used.
## Sender
When the sender web page expects an answer of the server, a callback can be passed to the emit function on. The server can then take that function as a parameter of the handler and call it with the response.
Server responses to request will always include the following fields:
* `success`: A boolean that indicates, whether the request was successful or not.
* `message`: A string that holds a user-friendly text to display. Holds the error in case one occurred.
In order to authenticate itself, the sender has to provide a slot and a token by emitting a `sender_init` event. These values are provided through the query string of the sender web page. When the server receives the `sender_init` event it validates the slot and the token and sends a response.
After a successful initialisation the connectin to janus is established. When the camera is shared, the feed id is received by janus and then transmitted to the server using the `set_feed_id` event. On the server the corresponding slot will save that feed id. This will then be used to tell all receivers which feed id to attach to.
#preview-container { .visually-hidden {
margin: 8px; visibility: hidden;
height: 500px;
} }
...@@ -3,17 +3,86 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -3,17 +3,86 @@ document.addEventListener('DOMContentLoaded', function() {
var janus = null; var janus = null;
var videoroomHandle = null; var videoroomHandle = null;
var room = 1006;
var sendResolution = 'stdres'; var sendResolution = 'stdres';
const socketNumber = room + 4000;
const socket = io('https://' + window.location.hostname, { path: '/socket.io/' + socketNumber });
var startButton = document.getElementById('start'); var startButton = document.getElementById('start');
var stopButton = document.getElementById('stop'); var stopButton = document.getElementById('stop');
var roomIndicator = document.getElementById('room-indicator');
var gotSocketInitResponse = false;
var transmitting = false;
var room = 1006;
var slot = 0;
var token = '';
var feedId = null;
parseRoomFromURL(); parseRoomFromURL();
parseSlotFromURL();
parseTokenFromURL();
roomIndicator.innerText = `VNC ${room - 1000} (Room ${room}) - Slot ${slot} - Token: ${token || '*none*'}`;
const socketNumber = room + 4000;
const socket = io('https://' + window.location.hostname, {
path: '/socket.io/' + socketNumber
});
registerSocketHandlers();
function handleSenderInitResponse(data) {
if (!gotSocketInitResponse) {
gotSocketInitResponse = true;
console.log('sender_init response data:', data);
if (data.success) {
initJanus();
} else {
alert('Socket connection error:\n' + data.message);
}
}
}
function handleSetFeedIdResponse(data) {
console.log('set_feed_id response data:', data);
if (data.success) {
transmitting = true;
var bandwidthForm = document.getElementById('bandwidth-form');
var bandwidthSubmit = document.getElementById('bandwidth-submit');
bandwidthForm.onsubmit = handleBandwidthFormSubmit;
bandwidthSubmit.removeAttribute('disabled', '');
stopButton.removeAttribute('disabled');
stopButton.onclick = function() {
janus.destroy();
};
var video = document.getElementById('camera-preview');
if (video != null) {
video.classList.remove('visually-hidden');
}
alert('Sharing camera');
} else {
alert('Error: ' + data.message);
window.location.reload();
}
}
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() {
Janus.init({ debug: 'all', callback: function() { Janus.init({ debug: 'all', callback: function() {
if (!Janus.isWebrtcSupported()) { if (!Janus.isWebrtcSupported()) {
alert('No WebRTC support... '); alert('No WebRTC support... ');
...@@ -34,10 +103,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -34,10 +103,6 @@ document.addEventListener('DOMContentLoaded', function() {
var pinInput = document.getElementById('pin-input'); var pinInput = document.getElementById('pin-input');
startButton.setAttribute('disabled', ''); startButton.setAttribute('disabled', '');
resSelect.setAttribute('disabled', ''); resSelect.setAttribute('disabled', '');
stopButton.removeAttribute('disabled');
stopButton.onclick = function() {
janus.destroy();
};
sendResolution = resSelect.value; sendResolution = resSelect.value;
Janus.log('sendResolution:', sendResolution); Janus.log('sendResolution:', sendResolution);
shareCamera(pinInput.value); shareCamera(pinInput.value);
...@@ -51,11 +116,8 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -51,11 +116,8 @@ document.addEventListener('DOMContentLoaded', function() {
}, },
webrtcState: function(on) { webrtcState: function(on) {
if (on) { if (on) {
var bandwidthForm = document.getElementById('bandwidth-form'); socket.emit('set_feed_id', { feedId }, handleSetFeedIdResponse);
var bandwidthSubmit = document.getElementById('bandwidth-submit'); // Sharing camera successful, when the set_feed_id request is successful
bandwidthForm.onsubmit = handleBandwidthFormSubmit;
bandwidthSubmit.removeAttribute('disabled', '');
alert('Sharing camera');
} else { } else {
janus.destroy(); janus.destroy();
} }
...@@ -68,6 +130,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -68,6 +130,9 @@ document.addEventListener('DOMContentLoaded', function() {
video.setAttribute('autoplay', ''); video.setAttribute('autoplay', '');
video.setAttribute('playsinline', ''); video.setAttribute('playsinline', '');
video.setAttribute('muted', 'muted'); video.setAttribute('muted', 'muted');
if (!transmitting) {
video.classList.add('visually-hidden');
}
document.getElementById('preview-container').appendChild(video); document.getElementById('preview-container').appendChild(video);
// var noVncLink = 'https://simon-doering.com/novnc/vnc.html?room=' + room; // var noVncLink = 'https://simon-doering.com/novnc/vnc.html?room=' + room;
...@@ -90,6 +155,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -90,6 +155,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
}}); }});
};
function shareCamera(pin) { function shareCamera(pin) {
var register = { var register = {
...@@ -123,8 +189,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -123,8 +189,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (event) { if (event) {
if (event === 'joined') { if (event === 'joined') {
Janus.log('Joined event:', msg); Janus.log('Joined event:', msg);
Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + msg['id']); feedId = msg.id;
if (msg['publishers'].length === 0) { Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + feedId);
//if (msg['publishers'].length === 0) {
videoroomHandle.createOffer({ videoroomHandle.createOffer({
media: { media: {
videoSend: true, videoSend: true,
...@@ -145,10 +212,10 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -145,10 +212,10 @@ document.addEventListener('DOMContentLoaded', function() {
alert('WebRTC error: ' + error.message); alert('WebRTC error: ' + error.message);
} }
}); });
} else { //} else {
alert('There is already somebody who is sharing his camera in this room!'); // alert('There is already somebody who is sharing his camera in this room!');
window.location.reload(); // window.location.reload();
} //}
} }
if (event === 'event' && msg['error']) { if (event === 'event' && msg['error']) {
alert('Error message: ' + msg['error'] + '.\nError object: ' + JSON.stringify(msg, null, 2)); alert('Error message: ' + msg['error'] + '.\nError object: ' + JSON.stringify(msg, null, 2));
...@@ -163,15 +230,32 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -163,15 +230,32 @@ document.addEventListener('DOMContentLoaded', function() {
function parseRoomFromURL() { function parseRoomFromURL() {
var urlParams = new URLSearchParams(window.location.search); var urlParams = new URLSearchParams(window.location.search);
var roomParam = urlParams.get('room'); var roomParam = urlParams.get('room');
var roomIndicator = document.getElementById('room-indicator');
var appendix = '';
if (roomParam != null && !isNaN(roomParam)) { if (roomParam != null && !isNaN(roomParam)) {
room = parseInt(roomParam); room = parseInt(roomParam);
} else { } else {
console.log('Got no valid room in URL search params, using default room ' + room); console.log('Got no valid room in URL search params, using default room ' + room);
appendix = ' (Default value)';
} }
roomIndicator.innerText = `VNC ${room - 1000} - Room ${room}` + appendix; }
function parseSlotFromURL() {
var urlParams = new URLSearchParams(window.location.search);
var slotParam = urlParams.get('slot');
if (slotParam != null && !isNaN(slotParam)) {
slot = parseInt(slotParam);
} else {
console.log('Got no valid slot in URL search params, using default slot ' + slot);
}
}
function parseTokenFromURL() {
var urlParams = new URLSearchParams(window.location.search);
var tokenParam = urlParams.get('token');
if (tokenParam != null) {
token = tokenParam;
} else {
console.log('Got no valid token in URL search params, using default token ' + token);
}
} }
}, false); }, false);
class ValidationError extends Error {
constructor(message) {
super(message);
}
}
module.exports = ValidationError;
const ValidationError = require('./models/validation-error');
const readline = require('readline'); const readline = require('readline');
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
...@@ -41,6 +43,7 @@ for (let i = 0; i < cameraSlots; i++) { ...@@ -41,6 +43,7 @@ for (let i = 0; i < cameraSlots; i++) {
token: null, token: null,
feedActive: false, feedActive: false,
feedId: null, feedId: null,
senderSocketId: null,
visibility: { visibility: {
command: 'show', command: 'show',
params: [] params: []
...@@ -52,11 +55,129 @@ for (let i = 0; i < cameraSlots; i++) { ...@@ -52,11 +55,129 @@ for (let i = 0; i < cameraSlots; i++) {
}); });
} }
const handleSetFeedId = (socket, data, fn) => {
let success = true;
let message = '';
try {
const slot = socket.cameraSlot;
const currentCameraState = cameraStates[slot];
if (currentCameraState.token !== socket.cameraSlotToken) {
console.log('Error: Got set_feed_id event for slot ' + slot + ' with an old token');
throw new ValidationError('The provided token is not valid anymore - the feed is not transmitted');
}
if (currentCameraState.feedActive) {
console.log('Error: Got set_feed_id event for slot ' + slot + ' which already has an active feed');
throw new ValidationError('There is already somebody using this slot');
}
if (data == null) {
console.log('Error: Got set_feed_id event for slot ' + slot + ' without data');
throw new ValidationError('Could not get feed id because no data was provided');
}
const feedId = data.feedId;
if (feedId == null) {
console.log('Error: Got set_feed_id event without a feed id on slot ' + slot);
throw new ValidationError('No feed id was provided');
}
console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
message = 'Successfully set feed id - you are now using this slot';
currentCameraState.feedActive = true;
currentCameraState.feedId = data.id;
currentCameraState.senderSocketId = socket.id;
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
// TODO: Emit some kind of 'new feed to attach to on slot x' to receivers
};
const handleSenderDisconnect = (socket, reason) => {
const slot = socket.cameraSlot;
if (slot != null) {
const currentCameraState = cameraStates[slot];
if (currentCameraState.feedActive && socket.id === currentCameraState.senderSocketId) {
console.log('Sender on slot ' + slot + ' disconnected - Clearing slot');
currentCameraState.feedActive = false;
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
// TODO: Emit some kind of 'feed x not available anymore' to receivers
}
}
};
const registerSenderHandlers = (socket) => {
socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
};
const handleSenderInit = (socket, data, fn) => {
let success = true;
let message = '';
try {
const slotStr = data.slot;
if (isNaN(slotStr)) {
console.log('Error: Got socket connection with slot ' + slotStr + ' that cannot be parsed to a number');
throw new ValidationError('Slot ' + slotStr + ' cannot be parsed to number');
}
const slot = parseInt(slotStr);
if (slot < 0 || slot > cameraStates.length - 1) {
console.log('Error: Got socket connection with slot ' + slot + ' which is not in the list of slots');
throw new ValidationError('Slot ' + slot + ' is not in the list of slots');
}
const currentCameraState = cameraStates[slot];
if (!currentCameraState.slotActive) {
console.log('Error: Got socket connection for inactive slot ' + slot);
throw new ValidationError('Slot ' + slot + ' is not active');
}
const token = data.token;
if (currentCameraState.token !== token) {
console.log('Error: Got socket connecion with wrong token ' + token + ' for slot ' + slot);
throw new ValidationError('Invalid token');
}
console.log('Got sender socket connection on slot ' + slot);
message = 'Socket authenticated';
socket.cameraSlot = slot;
socket.cameraSlotToken = data.token;
registerSenderHandlers(socket);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
};
io.on('connection', (socket) => { io.on('connection', (socket) => {
socket.on('query_state', () => { socket.on('query_state', () => {
console.log('Got state query from socket'); console.log('Got state query from socket');
socket.emit('init', cameraStates); socket.emit('init', cameraStates);
}); });
socket.on('sender_init', handleSenderInit.bind(null, socket));
}); });
const handleInternalCommand = (command, slot, params) => { const handleInternalCommand = (command, slot, params) => {
...@@ -80,8 +201,12 @@ const handleInternalCommand = (command, slot, params) => { ...@@ -80,8 +201,12 @@ const handleInternalCommand = (command, slot, params) => {
return; return;
} }
console.log('Deactivating slot ' + slot); console.log('Deactivating slot ' + slot);
currentCameraState.token = null; // TODO: Emit 'feed x is not available anymore' to receivers
currentCameraState.slotActive = false; currentCameraState.slotActive = false;
currentCameraState.token = null;
currentCameraState.feedActive = false;
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
break; break;
case 'refresh_token': case 'refresh_token':
if (!currentCameraState.slotActive) { if (!currentCameraState.slotActive) {
...@@ -112,7 +237,12 @@ const handleCommand = (line) => { ...@@ -112,7 +237,12 @@ const handleCommand = (line) => {
console.log('Error: Got no slot to apply the command on'); console.log('Error: Got no slot to apply the command on');
return; return;
} }
const slot = +params.shift(); const slotStr = params.shift();
if (isNaN(slotStr)) {
console.log('Error: Could not parse slot ' + slotStr + ' to an integer');
return;
}
const slot = parseInt(slotStr);
console.log('command:', command); console.log('command:', command);
console.log('slot:', slot); console.log('slot:', slot);
console.log('params:', params); console.log('params:', params);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment