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

Add multiple camera slot logic for receiver side

parent 65857693
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.
# CVH-Camera
The CVH-Camera project provides a way to share a camera view using the Janus WebRTC gateway. It is designed to be used with the PULT project available at [ https://gitlab.cvh-server.de/pgerwinski/pult ](https://gitlab.cvh-server.de/pgerwinski/pult).
Note that the project and its documentation are still in development.
# 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.
When the sender side expects an answer to a request, a callback can be passed to the emit function. The server can then take that function as a parameter of the handler and call it with the response. Below you can see an abstract example of the described technique.
```javascript
// Client
socket.emit('sender_init', 'Some data', function(responseData) {
console.log(responseData); // Should log 'Some answer'
});
// Server
socket.on('sender_init', function(data, fn) {
console.log(data); // Should log 'Some data'
fn('Some answer');
});
```
Server responses to request will always include the following fields:
* `success`: A boolean that indicates, whether the request was successful or not.
......@@ -15,3 +30,4 @@ In order to authenticate itself, the sender has to provide a slot and a token by
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.
When the sender socket disconnects, an event will be emitted to the receivers telling them to remove the feed.
document.addEventListener('DOMContentLoaded', function() {
var server = 'https://' + window.location.hostname + ':8089/janus';
var janus = null;
var videoroomHandle = null;
var remoteFeedHandle = null;
var janusPluginHandles = {};
var janusInitialised = false;
var opaqueId = 'camera-receiver-' + Janus.randomString(12);
var room = 1000;
var source = null;
var source = {};
var passwordSubmitClicked = false;
......@@ -32,9 +31,8 @@ document.addEventListener('DOMContentLoaded', function() {
var socketNumber = room + 4000;
var socket = io('https://' + window.location.hostname, { path: '/socket.io/' + socketNumber.toString() });
var socketEventListenersRegistered = false;
var videoMounted = false;
var videoActiveGeometry = '';
// Every property (slot) holds a string that represents the active geometry for that camera slot
var videoActiveGeometry = {};
var previousCanvasGeometryState = {
vncHeight: 0,
vncWidth: 0,
......@@ -43,13 +41,15 @@ document.addEventListener('DOMContentLoaded', function() {
canvasX: 0,
canvasY: 0
};
var videoGeometryParams = {
origin: 'lt',
x: 0,
y: 0,
w: 0,
h: 0
};
var videoGeometryParams = {};
// Every video slot has the following structure
// {
// origin: 'lt',
// x: 0,
// y: 0,
// w: 0,
// h: 0
// }
var videoPrescale = 1;
parseVideoPrescaleFromURL();
......@@ -60,20 +60,30 @@ document.addEventListener('DOMContentLoaded', function() {
var socketMountCheckInterval = setInterval(function () {
// Video element and vnc canvas must be mounted
if (
videoMounted &&
janusInitialised &&
passwordSubmitClicked &&
document.querySelector('canvas') != null
) {
console.log('mount socket logic');
clearInterval(socketMountCheckInterval);
if (!socketEventListenersRegistered) {
registerSocketEventListeners();
}
socket.emit('query_state');
registerSocketHandlers();
socket.emit('query_state', handleQueryStateResponse);
setInterval(adjustVideoGeometry, 500);
}
}, 500);
});
function handleQueryStateResponse(cameraStates) {
console.log('handleQueryStateResponse:', cameraStates);
Object.keys(cameraStates).forEach(function(slot) {
var state = cameraStates[slot];
newRemoteFeed(slot, state.feedId, {
geometry: state.geometry,
visibility: state.visibility
});
});
}
Janus.init({ debug: true, callback: function() {
if (!Janus.isWebrtcSupported()) {
alert('No WebRTC support... ');
......@@ -83,29 +93,8 @@ document.addEventListener('DOMContentLoaded', function() {
janus = new Janus({
server,
success: function() {
janus.attach({
plugin: 'janus.plugin.videoroom',
opaqueId,
success: function(pluginHandle) {
videoroomHandle = pluginHandle;
Janus.log('Plugin attached! (' + videoroomHandle.getPlugin() + ', id=' + videoroomHandle.getId() + ')');
if (passwordSubmitClicked) {
joinRoom();
} else {
passwordButton.onclick = function() {
pin = currentPassword;
joinRoom();
};
}
},
error: function(error) {
var formattedError = JSON.stringify(error, null, 2);
Janus.error('Error attaching plugin: ', formattedError);
alert(formattedError);
},
onmessage: handleMessagePublisher
});
console.log('Janus initialised');
janusInitialised = true;
},
error: function(error) {
var formattedError = JSON.stringify(error, null, 2);
......@@ -118,105 +107,83 @@ document.addEventListener('DOMContentLoaded', function() {
});
}});
function handleMessagePublisher(msg, jsep) {
var event = msg['videoroom'];
if (event) {
if (event === 'joined') {
Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + msg['id']);
passwordButton.onclick = null;
var publishers = msg['publishers'];
if (publishers && publishers.length !== 0) {
newRemoteFeed(publishers[0]['id']);
}
} else if (event === 'event') {
var publishers = msg['publishers'];
if (publishers && publishers.length !== 0) {
newRemoteFeed(publishers[0]['id']);
} else if (msg['leaving'] && msg['leaving'] === source) {
Janus.log('Publisher left');
var video = document.getElementById('camera-feed');
if (video != null) {
video.classList.add('hidden');
}
} else if (msg['error']) {
if (msg['error_code'] === 433) {
console.error('Janus: wrong pin "' + pin + '" for room ' + room);
return;
function removeRemoteFeed(slot) {
delete videoActiveGeometry[slot];
delete videoGeometryParams[slot];
delete source[slot];
var video = document.getElementById('camera-feed-' + slot);
video.remove();
janusPluginHandles[slot].detach();
delete janusPluginHandles[slot];
}
alert('Error message: ' + msg['error'] + '.\nError object: ' + JSON.stringify(msg, null, 2));
}
}
}
if (jsep) {
videoRoomHandle.handleRemoteJsep({ jsep });
function newRemoteFeed(slot, feedId, initialState) {
source[slot] = feedId;
var cameraElementId = 'camera-feed-' + slot;
var video = document.getElementById(cameraElementId);
if (video == null) {
video = document.createElement('video');
video.setAttribute('id', cameraElementId);
video.setAttribute('muted', '');
video.setAttribute('autoplay', '');
video.setAttribute('playsinline', '');
// Necessary for autoplay without user interaction
video.oncanplaythrough = function() {
video.muted = true;
video.play();
}
video.classList.add('camera-feed');
document.body.appendChild(video);
}
function newRemoteFeed(id) {
source = id;
var remoteFeedHandle = null;
janus.attach({
plugin: 'janus.plugin.videoroom',
opaqueId,
success: function(pluginHandle) {
remoteFeedHandle = pluginHandle;
Janus.log('Plugin attached (subscriber)! (' + remoteFeedHandle.getPlugin() + ', id=' + remoteFeedHandle.getId() + ')');
janusPluginHandles[slot] = pluginHandle;
Janus.log('Plugin attached (subscriber slot ' + slot + ')! (' + remoteFeedHandle.getPlugin() + ', id=' + remoteFeedHandle.getId() + ')');
var listen = {
request: 'join',
room,
ptype: 'subscriber',
feed: id,
feed: feedId,
pin
};
remoteFeedHandle.send({ message: listen });
},
error: function(error) {
var formattedError = JSON.stringify(error, null, 2);
Janus.error('Error attaching plugin (subscriber): ', formattedError);
Janus.error('Error attaching plugin (subscriber slot ' + slot + '): ', formattedError);
alert(formattedError);
},
onmessage: handleMessageListener,
onmessage: handleMessageSubscriber.bind(null, slot),
onremotestream: function(stream) {
var video = document.getElementById('camera-feed');
if (video == null) {
video = document.createElement('video');
video.setAttribute('id', 'camera-feed');
video.setAttribute('muted', '');
video.setAttribute('autoplay', '');
video.setAttribute('playsinline', '');
video.setAttribute(
'style',
'position: fixed;' +
'bottom: 0;' +
'right: 0;' +
'max-width: calc(150px + 10%);' +
'max-height: calc(150px + 20%);'
);
// Hide until the init socket event is received which will overwrite this
video.classList.add('visually-hidden');
video.oncanplaythrough = function() {
video.muted = true;
video.play();
}
document.body.appendChild(video);
// video.onclick = function(event) {
// event.target.classList.toggle('fullscreen');
// }
videoMounted = true;
}
video.classList.remove('hidden');
Janus.attachMediaStream(video, stream);
},
oncleanup: function() {
Janus.log('Got a cleanup notification (remote feed ' + source + ')');
Janus.log('Got a cleanup notification');
}
});
handleCommand(slot, initialState.geometry.command, initialState.geometry.params);
handleCommand(slot, initialState.visibility.command, initialState.visibility.params);
}
function handleMessageListener(msg, jsep) {
function handleMessageSubscriber(slot, msg, jsep) {
var remoteFeedHandle = janusPluginHandles[slot];
var event = msg['videoroom'];
if (event) {
if (event === 'attached') {
Janus.log('Successfully attached to feed ' + source + ' in room ' + msg['room']);
Janus.log('Successfully attached to feed on slot ' + slot + ' in room ' + msg['room']);
} else if (event === 'event') {
if (msg['error']) {
console.error('handleMessageSubscriber', msg['error']);
}
}
}
if (jsep) {
......@@ -239,16 +206,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
function joinRoom() {
var register = {
request: 'join',
room,
ptype: 'publisher',
pin
};
videoroomHandle.send({ message: register });
}
function parseRoomFromURL() {
var urlParams = new URLSearchParams(window.location.search);
var roomParam = urlParams.get('room');
......@@ -281,23 +238,39 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
function registerSocketEventListeners() {
function registerSocketHandlers() {
socket.on('command', function (data) {
handleCommand(data.command, data.params);
handleCommand(data.slot, data.command, data.params);
});
socket.on('new_feed', function(data) {
console.log('new_feed', data);
newRemoteFeed(data.slot, data.feedId, {
geometry: data.geometry,
visibility: data.visibility
});
});
socket.on('init', function (cameraState) {
handleCommand(cameraState.geometry.command, cameraState.geometry.params);
handleCommand(cameraState.visibility.command, cameraState.visibility.params);
socket.on('remove_feed', function(data) {
console.log('remove_feed', data);
removeRemoteFeed(data.slot);
});
socketEventListenersRegistered = true;
socket.on('remove_all_feeds', function() {
Object.keys(videoGeometryParams).forEach(function(slot) {
removeRemoteFeed(slot);
});
});
}
function handleCommand(command, params) {
var video = document.getElementById('camera-feed');
function handleCommand(slot, command, params) {
console.log('Got command:', command);
console.log('For slot:', slot);
console.log('With params:', params);
var video = document.getElementById('camera-feed-' + slot);
if (video == null) {
console.log('handleCommand video element is null');
}
switch(command) {
case 'set_geometry_relative_to_window':
var origin = params[0];
......@@ -307,8 +280,8 @@ document.addEventListener('DOMContentLoaded', function() {
var h = params[4];
setFixedPosition(video, origin, x, y, w, h);
videoGeometryParams = { origin, x, y, w, h };
videoActiveGeometry = command;
videoGeometryParams[slot] = { origin, x, y, w, h };
videoActiveGeometry[slot] = command;
break;
case 'set_geometry_relative_to_canvas':
......@@ -318,7 +291,7 @@ document.addEventListener('DOMContentLoaded', function() {
var w = parseInt(params[3]);
var h = parseInt(params[4]);
handleSetGeometryRelativeToCanvas(origin, x, y, w, h);
handleSetGeometryRelativeToCanvas(video, slot, origin, x, y, w, h);
break;
case 'show':
......@@ -333,11 +306,10 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
function handleSetGeometryRelativeToCanvas(origin, x, y, w, h) {
var video = document.getElementById('camera-feed');
function handleSetGeometryRelativeToCanvas(video, slot, origin, x, y, w, h) {
// Site contains only one canvas - the vnc viewer
var canvas = document.querySelector('canvas');
videoGeometryParams = { origin, x, y, w, h };
videoGeometryParams[slot] = { origin, x, y, w, h };
var vncWidth = parseInt(canvas.width);
var vncHeight = parseInt(canvas.height);
......@@ -378,7 +350,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
setFixedPosition(video, origin, x, y, w, h);
videoActiveGeometry = 'set_geometry_relative_to_canvas';
videoActiveGeometry[slot] = 'set_geometry_relative_to_canvas';
previousCanvasGeometryState = {
vncWidth,
vncHeight,
......@@ -389,7 +361,7 @@ document.addEventListener('DOMContentLoaded', function() {
};
}
function setFixedPosition(element, origin, x, y, w, h) {
function setFixedPosition(video, origin, x, y, w, h) {
var style = (
'position: fixed;' +
`width: ${w}px;` +
......@@ -415,11 +387,10 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
element.setAttribute('style', style);
video.setAttribute('style', style);
}
function adjustVideoGeometry() {
if (videoActiveGeometry === 'set_geometry_relative_to_canvas') {
var canvas = document.querySelector('canvas');
var canvasRect = canvas.getBoundingClientRect();
var vncWidth = canvas.width;
......@@ -433,17 +404,23 @@ document.addEventListener('DOMContentLoaded', function() {
vncHeight !== previousCanvasGeometryState.vncHeight ||
canvasWidth !== previousCanvasGeometryState.canvasWidth ||
canvasHeight !== previousCanvasGeometryState.canvasHeight ||
canvasX !== previousCanvasGeometryState.x ||
canvasY !== previousCanvasGeometryState.y
canvasX !== previousCanvasGeometryState.canvasX ||
canvasY !== previousCanvasGeometryState.canvasY
) {
Object.keys(videoGeometryParams).forEach(function(slot) {
if (videoActiveGeometry[slot] === 'set_geometry_relative_to_canvas') {
var params = videoGeometryParams[slot];
handleSetGeometryRelativeToCanvas(
videoGeometryParams.origin,
videoGeometryParams.x,
videoGeometryParams.y,
videoGeometryParams.w,
videoGeometryParams.h
document.getElementById('camera-feed-' + slot),
slot,
params.origin,
params.x,
params.y,
params.w,
params.h
);
}
});
}
}
});
......
#camera-feed {
.camera-feed {
z-index: 100;
}
#camera-feed.hidden {
.camera-feed.hidden {
display: none;
}
#camera-feed.visually-hidden {
.camera-feed.visually-hidden {
visibility: hidden;
}
#camera-feed.fullscreen {
.camera-feed.fullscreen {
max-width: none;
max-height: none;
width: 100%;
......
......@@ -15,7 +15,7 @@ if (process.env.PORT) {
console.log('Got no PORT environment variable - using default port ' + port);
}
let cameraSlots = 2;
let cameraSlots = 4;
if (process.env.CAMERA_SLOTS) {
cameraSlots = +process.env.CAMERA_SLOTS;
console.log('Using camera count ' + cameraSlots + ' from CAMERA_SLOTS environment variable');
......@@ -55,6 +55,20 @@ for (let i = 0; i < cameraSlots; i++) {
});
}
const emitNewFeed = (slot) => {
const cameraState = cameraStates[slot];
io.emit('new_feed', {
slot,
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
});
};
const emitRemoveFeed = (slot) => {
io.emit('remove_feed', { slot });
};
const handleSetFeedId = (socket, data, fn) => {
let success = true;
let message = '';
......@@ -88,8 +102,10 @@ const handleSetFeedId = (socket, data, fn) => {
message = 'Successfully set feed id - you are now using this slot';
currentCameraState.feedActive = true;
currentCameraState.feedId = data.id;
currentCameraState.feedId = feedId;
currentCameraState.senderSocketId = socket.id;
emitNewFeed(slot);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
......@@ -100,8 +116,6 @@ const handleSetFeedId = (socket, data, fn) => {
}
fn({ success, message });
// TODO: Emit some kind of 'new feed to attach to on slot x' to receivers
};
const handleSenderDisconnect = (socket, reason) => {
......@@ -114,7 +128,7 @@ const handleSenderDisconnect = (socket, reason) => {
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
// TODO: Emit some kind of 'feed x not available anymore' to receivers
emitRemoveFeed(slot);
}
}
};
......@@ -171,11 +185,24 @@ const handleSenderInit = (socket, data, fn) => {
fn({ success, message });
};
io.on('connection', (socket) => {
socket.on('query_state', () => {
const handleQueryState = (fn) => {
console.log('Got state query from socket');
socket.emit('init', cameraStates);
});
let response = {};
for (let i = 0; i < cameraStates.length; i++) {
const cameraState = cameraStates[i];
if (cameraState.feedActive) {
response[i] = {
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
};
}
}
fn(response);
}
io.on('connection', (socket) => {
socket.on('query_state', handleQueryState);
socket.on('sender_init', handleSenderInit.bind(null, socket));
});
......@@ -201,7 +228,8 @@ const handleInternalCommand = (command, slot, params) => {
return;
}
console.log('Deactivating slot ' + slot);
// TODO: Emit 'feed x is not available anymore' to receivers
emitRemoveFeed(slot);
currentCameraState.slotActive = false;
currentCameraState.token = null;
currentCameraState.feedActive = false;
......@@ -228,7 +256,7 @@ const handleInternalCommand = (command, slot, params) => {
};
const handleCommand = (line) => {
const emitCommand = false;
let emitCommand = false;
console.log('Got command from stdin:', line);
const params = line.split(' ');
......@@ -275,8 +303,9 @@ const handleCommand = (line) => {
console.log('new cameraState:', currentCameraState);
if (emitCommand) {
if (currentCameraState.feedActive && emitCommand) {
io.emit('command', {
slot,
command,
params
});
......@@ -287,7 +316,7 @@ rl.on('line', handleCommand);
const cleanup = () => {
console.log('cleanup');
io.emit('hide_all');
io.emit('remove_all_feeds');
};
const exitHandler = (options, exitCode) => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment