Skip to content
Snippets Groups Projects
Commit 40ea0ae7 authored by Peter Gerwinski's avatar Peter Gerwinski
Browse files

more than one CVH Camera via Janus

parent 50c242f8
Branches
No related tags found
No related merge requests found
Showing
with 6954 additions and 466 deletions
......@@ -362,6 +362,11 @@ Installation
------------
Detailed installation instructions ar being written *right now*.
We are also planning to provide a ready-to-run PULT installation
as a Docker container. That way you can install it in just a few
minutes.
This section gives a brief overview.
The PULT server was desiged to run on GNU/Linux. However it should
......
......@@ -9,12 +9,18 @@ bbb_url="https://bbb.cvh-server.de/b/pet-jxk-dch"
bbb_invitation="follow <a target=\"_blank\" href=\"$bbb_url\">this link</a> to $bbb_url"
jm_url="https://meet.cvh-server.de/VNC6"
jm_invitation="follow <a target=\"_blank\" href=\"$jm_url\">this link</a> to $jm_url"
janus_receiver_url="https://streaming.cvh-server.de/cvh-camera/camera-receiver.html?room=1006"
janus_receiver_url_redundant="https://streaming.cvh-server.de/cvh-camera/camera-receiver.html?room=1005"
janus_sender_url="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1006"
janus_sender_url_redundant="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1005"
janus_invitation="follow <a target=\"_blank\" href=\"$janus_sender_url\">this link</a> to $janus_sender_url"
janus_invitation_redundant="follow <a target=\"_blank\" href=\"$janus_sender_url_redundant\">this link</a> to $janus_sender_url_redundant"
#janus_receiver_url="https://streaming.cvh-server.de/cvh-camera/camera-receiver.html?room=1006"
#janus_receiver_url_redundant="https://streaming.cvh-server.de/cvh-camera/camera-receiver.html?room=1005"
#janus_sender_url="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1006"
#janus_sender_url_redundant="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1005"
janus_sender_url_1="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1006&slot=0&token=@INFO@"
janus_sender_url_2="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1006&slot=1&token=@INFO@"
janus_sender_url_3="https://streaming.cvh-server.de/cvh-camera/camera-sender.html?room=1006&slot=2&token=@INFO@"
#janus_invitation="follow <a target=\"_blank\" href=\"$janus_sender_url\">this link</a> to $janus_sender_url"
#janus_invitation_redundant="follow <a target=\"_blank\" href=\"$janus_sender_url_redundant\">this link</a> to $janus_sender_url_redundant"
janus_invitation_1="follow <a target=\"_blank\" href=\"$janus_sender_url_1\">this link</a> to $janus_sender_url_1"
janus_invitation_2="follow <a target=\"_blank\" href=\"$janus_sender_url_2\">this link</a> to $janus_sender_url_2"
janus_invitation_3="follow <a target=\"_blank\" href=\"$janus_sender_url_3\">this link</a> to $janus_sender_url_3"
om_url="https://openmeetings.cvh-server.de:5443/openmeetings/#room/24"
om_invitation="follow <a target=\"_blank\" href=\"$om_url\">this link</a> to $om_url"
upload_sender_url="https://streaming.cvh-server.de/cgi-bin/pult-upload-6.cgi"
......@@ -25,9 +31,12 @@ module vnc-gast vnc module_name="VNC Gast" invitation="connect to streaming.cvh-
module bbb bbb-camera module_name="BBB-Kamera" url="$bbb_url" invitation="$bbb_invitation"
module jm jm-camera module_name="JM-Kamera" url="$jm_url" invitation="$jm_invitation"
camera_default_max_resolution="1024x768"
module janus cvh-camera module_name="CVH-Kamera 6" url="$janus_receiver_url" persistent_invitation="$janus_invitation"
module janus-r cvh-camera module_name="CVH-Kamera 5" camera_room="1005" url="$janus_receiver_url_redundant" persistent_invitation="$janus_invitation_redundant" min_geometry="480x360-0-360"
module janus-d cvh-camera module_name="CVH-Kamera D" browser="false" direct="true" persistent_invitation="$janus_invitation"
#module janus cvh-camera module_name="CVH-Kamera 6" url="$janus_receiver_url" persistent_invitation="$janus_invitation"
#module janus-r cvh-camera module_name="CVH-Kamera 5" camera_room="1005" url="$janus_receiver_url_redundant" persistent_invitation="$janus_invitation_redundant" min_geometry="480x360-0-360"
camera_default_max_resolution="1440x1080"
module janus-1 cvh-camera module_name="CVH-Kamera 1" browser="false" direct="true" slot="0" persistent_invitation="$janus_invitation_1"
module janus-2 cvh-camera module_name="CVH-Kamera 2" browser="false" direct="true" slot="1" persistent_invitation="$janus_invitation_2" min_geometry="480x360-0-360"
module janus-3 cvh-camera module_name="CVH-Kamera 3" browser="false" direct="true" slot="2" persistent_invitation="$janus_invitation_3" min_geometry="480x360-0-720"
module om om-board module_name="OM-Tafel" url="$om_url" persistent_invitation="$om_invitation"
module bbbb bbb-board module_name="BBB-Tafel" url="$bbb_url" persistent_invitation="$bbb_invitation" min_geometry="1440x810+0+0"
module upload upload module_name="Upload" persistent_invitation="$upload_invitation"
......@@ -27,11 +27,11 @@ module ()
fi
}
. "/usr/local/etc/pult/pult-$channel.conf"
max_width=$(echo "$camera_default_max_resolution" | cut -d "x" -f 1)
max_height=$(echo "$camera_default_max_resolution" | cut -d "x" -f 2)
. "/usr/local/etc/pult/pult-$channel.conf"
$debug && echo "$0[$channel]: url = $url" 1>&2
cleanup ()
......
......@@ -5,6 +5,7 @@ debug=true
module_name="$1"
channel="$2"
camera_room="$((1000 + channel))"
camera_server_port="$((5010 + channel))"
browser=true
direct=false
......@@ -28,22 +29,28 @@ module ()
"camera_room="*)
camera_room="$(echo $opt | sed -e 's/^camera_room=//')"
;;
"browser="*)
browser="$(echo $opt | sed -e 's/^browser=//')"
;;
"direct="*)
direct="$(echo $opt | sed -e 's/^direct=//')"
;;
"browser="*)
browser="$(echo $opt | sed -e 's/^browser=//')"
"camera_server_port="*)
camera_server_port="$(echo $opt | sed -e 's/^camera_server_port=//')"
;;
"slot="*)
slot="$(echo $opt | sed -e 's/^slot=//')"
;;
esac
done
fi
}
. "/usr/local/etc/pult/pult-$channel.conf"
max_width=$(echo "$camera_default_max_resolution" | cut -d "x" -f 1)
max_height=$(echo "$camera_default_max_resolution" | cut -d "x" -f 2)
. "/usr/local/etc/pult/pult-$channel.conf"
$debug && echo "$0[$channel]: url = $url" 1>&2
cleanup ()
......@@ -55,6 +62,14 @@ cleanup ()
trap cleanup exit;
if $direct; then
token="$(dd if=/dev/urandom bs=1 count=32 2>/dev/null | shasum | cut -d " " -f 1)"
$debug && echo "$0[$channel]: sending command to camera server: activate_slot" 1>&2
echo "activate_slot $slot $token" | nc -q 0 localhost "$camera_server_port"
else
token=""
fi
if $browser; then
if [ -z "$url" ]; then
......@@ -76,14 +91,14 @@ if $browser; then
rm -rf "$pid_file"
echo "$chromium_pid" > "$pid_file"
if wait "$selenium_pid"; then
report_status "running" "$windowid" "$max_width" "$max_height"
report_status "running" "$windowid" "$max_width" "$max_height" "$token"
else
$debug && echo "start of $module_name interrupted" 1>&2
fi
elif $direct; then
report_status "running" "NULL" "$max_width" "$max_height"
report_status "running" "NULL" "$max_width" "$max_height" "$token"
else
......
......@@ -24,11 +24,15 @@ module ()
"camera_room="*)
camera_room="$(echo $opt | sed -e 's/^camera_room=//')"
;;
"direct="*)
direct="$(echo $opt | sed -e 's/^direct=//')"
;;
"camera_server_port="*)
camera_server_port="$(echo $opt | sed -e 's/^camera_server_port=//')"
;;
"direct="*)
direct="$(echo $opt | sed -e 's/^direct=//')"
"slot="*)
slot="$(echo $opt | sed -e 's/^slot=//')"
;;
esac
done
fi
......@@ -48,8 +52,8 @@ case "$cmd" in
;;
stop)
if $direct; then
$debug && echo "$0[$channel]: sending command to camera server: hide" 1>&2
echo "hide" | nc -q 0 localhost "$camera_server_port"
$debug && echo "$0[$channel]: sending command to camera server: deactivate_slot" 1>&2
echo "deactivate_slot $slot" | nc -q 0 localhost "$camera_server_port"
fi
if [ -f "$pid_file" ]; then
pid=$(cat "$pid_file")
......@@ -64,16 +68,17 @@ case "$cmd" in
echo "min=true browser=true direct=false"
;;
geometry)
$debug && echo "new geometry: ${2}x${3}+$4+$5" 1>&2
$debug && echo "new geometry: ${2}x${3}+$4+$5, z = $6" 1>&2
if $direct; then
if [ "$2" = "hidden" ]; then
$debug && echo "$0[$channel]: sending command to camera server: hide" 1>&2
echo "hide" | nc -q 0 localhost "$camera_server_port"
$debug && echo "$0[$channel]: sending command to camera server: hide $slot" 1>&2
echo "hide $slot" | nc -q 0 localhost "$camera_server_port"
else
$debug && echo "$0[$channel]: sending command to camera server: show" 1>&2
echo "show" | nc -q 0 localhost "$camera_server_port"
$debug && echo "$0[$channel]: sending command to camera server: set_geometry_relative_to_canvas lt $4 $5 $2 $3" 1>&2
echo "set_geometry_relative_to_canvas lt $4 $5 $2 $3" | nc -q 0 localhost "$camera_server_port"
$debug && echo "$0[$channel]: sending command to camera server: show $slot" 1>&2
echo "show $slot" | nc -q 0 localhost "$camera_server_port"
sleep 0.1
$debug && echo "$0[$channel]: sending command to camera server: set_geometry_relative_to_canvas $slot lt $4 $5 $2 $3 $6" 1>&2
echo "set_geometry_relative_to_canvas $slot lt $4 $5 $2 $3 $6" | nc -q 0 localhost "$camera_server_port"
fi
fi
;;
......
......@@ -27,11 +27,11 @@ module ()
fi
}
. "/usr/local/etc/pult/pult-$channel.conf"
max_width=$(echo "$camera_default_max_resolution" | cut -d "x" -f 1)
max_height=$(echo "$camera_default_max_resolution" | cut -d "x" -f 2)
. "/usr/local/etc/pult/pult-$channel.conf"
$debug && echo "$0[$channel]: url = $url" 1>&2
cleanup ()
......
......@@ -23,6 +23,7 @@ declare -A module_min_geometry
declare -A module_wid
declare -A module_w
declare -A module_h
declare -A module_info
invoke_module ()
{
......@@ -133,18 +134,20 @@ switch_to_geometry ()
local h="$4"
local x="$5"
local y="$6"
local z="$7"
if [ "$wid" != "NULL" ]; then
$debug && echo xdotool windowmove "$wid" "$x" "$y" windowsize "$wid" "$w" "$h" windowraise "$wid" 1>&2
xdotool windowmove "$wid" "$x" "$y" windowsize "$wid" "$w" "$h" windowraise "$wid"
else
$debug && echo "module $mm: no window id" 2>&2
fi
invoke_module "$mm" "geometry" "$w" "$h" "$x" "$y"
invoke_module "$mm" "geometry" "$w" "$h" "$x" "$y" "$z"
}
position_min_module ()
{
local mm="$1"
local z="$2"
local wid=${module_wid["$mm"]}
local geometry=${module_min_geometry["$mm"]}
if [ -z "$geometry" ]; then
......@@ -167,7 +170,7 @@ position_min_module ()
if [ "$new_pos_y_sign" = "-" ]; then
new_pos_y=$((screen_height - min_height - new_pos_y))
fi
switch_to_geometry "$mm" "$wid" "$min_width" "$min_height" "$new_pos_x" "$new_pos_y"
switch_to_geometry "$mm" "$wid" "$min_width" "$min_height" "$new_pos_x" "$new_pos_y" "$z"
fi
}
......@@ -175,11 +178,13 @@ position_all_min_modules_except ()
{
local exception="$1"
local mm
local z=1
for mm in "${!module_min[@]}"; do
$debug && echo -e "$0: running 1: mm = $mm, min = ${module_min[$mm]}, status = ${module_status[$mm]}" 1>&2
if [ "$mm" != "$exception" ] && [ "$mm" != "$primary_module" ] && ${module_min["$mm"]} && [ "${module_status[$mm]}" = "running" ]; then
position_min_module "$mm"
position_min_module "$mm" "$z"
fi
z=$((z + 1))
done
}
......@@ -196,7 +201,7 @@ position_primary_module ()
local h=${module_h["$primary_module"]}
$debug && echo -e "$0: wid = $wid, w = $w, h = $h" 1>&2
switch_to_resolution "$w" "$h"
switch_to_geometry "$primary_module" "$wid" "$screen_width" "$screen_height" "0" "0"
switch_to_geometry "$primary_module" "$wid" "$screen_width" "$screen_height" "0" "0" "0"
fi
}
......@@ -235,6 +240,9 @@ update_pult_status ()
for mm in "${!module_status[@]}"; do
echo -e "module_status[$mm]=\"${module_status[$mm]}\"" >> "$status_file"
done
for mm in "${!module_status[@]}"; do
echo -e "module_info[$mm]=\"${module_info[$mm]}\"" >> "$status_file"
done
}
shopt -s lastpipe
......@@ -272,10 +280,12 @@ while true; do
wid="${cmd[3]}"
w="${cmd[4]}"
h="${cmd[5]}"
info="${cmd[6]}"
$debug && echo -e "$0: wid = $wid, w = $w, h = $h" 1>&2
module_wid["$m"]="$wid"
module_w["$m"]="$w"
module_h["$m"]="$h"
module_info["$m"]="$info"
if ! ${module_min["$m"]} || [ -z "$primary_module" ] || ${module_min["$primary_module"]}; then
$debug && echo -n "min[m] = ${module_min["$m"]}, p = $primary_module" 1>&2
if [ -n "$primary_module" ]; then
......@@ -314,7 +324,7 @@ while true; do
fi
position_all_min_modules_except "$m"
$debug && echo -e "$0: minimising 3: m = $m, min = ${module_min[$m]}, status = ${module_status[$m]}" 1>&2
position_min_module "$m"
position_min_module "$m" "999"
else
echo "$0: trying to minimise module $m, which is not minimiseable"
fi
......
......@@ -50,6 +50,7 @@ do
done
$install -o root -g root -m 644 sbin/*.{py,js} "$sbin_directory/"
$install -o root -g root -m 644 sbin/models/*.js "$sbin_directory/models/"
$install -o root -g root -m 755 lib/pult/*.{module,daemon} "$lib_directory/pult/"
$install -o root -g root -m 644 lib/pult/*.{py,functions} "$lib_directory/pult/"
install_symlink "$lib_directory/pult/pult.daemon" "$sbin_directory/pultd"
......
const ValidationError = require('./models/validation-error');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
......@@ -5,14 +7,20 @@ const rl = readline.createInterface({
terminal: false
});
const defaultPort = 5005;
let port;
let port = 5000;
if (process.env.PORT) {
port = process.env.PORT;
port = +process.env.PORT;
console.log('Using port ' + port + ' from PORT environment variable');
} else {
port = defaultPort;
console.log('Got no PORT environment variable - using default port ' + defaultPort);
console.log('Got no PORT environment variable - using default port ' + port);
}
let cameraSlots = 4;
if (process.env.CAMERA_SLOTS) {
cameraSlots = +process.env.CAMERA_SLOTS;
console.log('Using camera count ' + cameraSlots + ' from CAMERA_SLOTS environment variable');
} else {
console.log('Got no CAMERA_SLOTS environment variable - using default count of ' + cameraSlots);
}
const io = require('socket.io')(port);
......@@ -22,8 +30,20 @@ const geometryCommands = [
'set_geometry_relative_to_window',
'set_geometry_relative_to_canvas'
];
const internalCommands = [
'activate_slot',
'deactivate_slot',
'refresh_token'
];
const cameraState = {
let cameraStates = [];
for (let i = 0; i < cameraSlots; i++) {
cameraStates.push({
slotActive: false,
token: null,
feedActive: false,
feedId: null,
senderSocketId: null,
visibility: {
command: 'show',
params: []
......@@ -32,51 +52,275 @@ const cameraState = {
command: 'set_geometry_relative_to_canvas',
params: ['rb', '0', '0', '200', '200']
}
});
}
const emitNewFeed = (slot) => {
const cameraState = cameraStates[slot];
io.emit('new_feed', {
slot,
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
});
};
io.on('connection', (socket) => {
socket.on('query_state', () => {
const emitRemoveFeed = (slot) => {
io.emit('remove_feed', { slot });
};
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 = feedId;
currentCameraState.senderSocketId = socket.id;
emitNewFeed(slot);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
};
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;
emitRemoveFeed(slot);
}
}
};
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 });
};
const handleQueryState = (fn) => {
console.log('Got state query from socket');
socket.emit('init', cameraState);
});
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));
});
const handleInternalCommand = (command, slot, params) => {
const currentCameraState = cameraStates[slot];
switch (command) {
case 'activate_slot':
if (currentCameraState.slotActive) {
console.log('Error: Tried to activate active slot ' + slot);
return;
}
if (params.length === 0) {
console.log('Error while activating slot ' + slot + ' - Got no token parameter');
return;
}
currentCameraState.token = params[0];
currentCameraState.slotActive = true;
break;
case 'deactivate_slot':
if (!currentCameraState.slotActive) {
console.log('Error: Tried to deactivate inactive slot ' + slot );
return;
}
console.log('Deactivating slot ' + slot);
emitRemoveFeed(slot);
currentCameraState.slotActive = false;
currentCameraState.token = null;
currentCameraState.feedActive = false;
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
break;
case 'refresh_token':
if (!currentCameraState.slotActive) {
console.log('Error: Tried to refresh token for inactive slot ' + slot);
return;
}
if (params.length === 0) {
console.log('Error while refreshing token for slot ' + slot + ' - Got no token parameter');
console.log('Keeping old token');
return;
}
console.log('Refreshing token for slot ' + slot);
currentCameraState.token = params[0];
break;
default:
console.log('Error: handleInternalCommand got unknown command ' + command);
break;
}
};
const handleCommand = (line) => {
let emitCommand = false;
console.log('Got command from stdin:', line);
const params = line.split(' ');
const command = params.shift();
if (params.length === 0) {
console.log('Error: Got no slot to apply the command on');
return;
}
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('slot:', slot);
console.log('params:', params);
if (slot < 0 || slot > cameraStates.length - 1) {
console.log(`Error: Got invalid slot number ${slot}. There are ${cameraStates.length} camera slots.`);
return;
}
const currentCameraState = cameraStates[slot];
if (visibilityCommands.includes(command)) {
cameraState.visibility = {
currentCameraState.visibility = {
command,
params
};
emitCommand = true;
} else if (geometryCommands.includes(command)) {
cameraState.geometry = {
currentCameraState.geometry = {
command,
params
};
emitCommand = true;
} else if (internalCommands.includes(command)) {
handleInternalCommand(command, slot, params);
} else {
console.log('Command "' + command + '" is not a valid command');
return;
}
console.log('new cameraState:', cameraState);
console.log('new cameraState:', currentCameraState);
if (currentCameraState.feedActive && emitCommand) {
io.emit('command', {
slot,
command,
params
});
}
}
rl.on('line', handleCommand);
const cleanup = () => {
console.log('cleanup');
handleCommand('hide');
io.emit('remove_all_feeds');
};
function exitHandler(options, exitCode) {
cleanup();
const exitHandler = (options, exitCode) => {
if (options.cleanup) cleanup();
if (exitCode || exitCode === 0) console.log(exitCode);
if (options.exit) process.exit();
}
......
class ValidationError extends Error {
constructor(message) {
super(message);
}
}
module.exports = ValidationError;
......@@ -20,6 +20,7 @@ max_text="max"
lock_file="/usr/local/var/run/pult/pultd-$channel.lock"
status_file="/usr/local/var/run/pult/pult-status-$channel"
declare -A module_status
declare -A module_info
. "/usr/local/etc/pult/pult.conf"
......@@ -90,14 +91,14 @@ module ()
fi
;;
"invitation="*)
local invitation="$(echo $opt | sed -e 's/^invitation=//')"
local invitation=$(echo "$opt" | sed -e 's/^invitation=//' -e "s/@INFO@/${module_info[$m]}/g")
local status="${module_status[$m]}"
if [ -n "$status" -a "$status" != "running" ]; then
print_invitation="$print_invitation<center><strong>$module_name:</strong> $invitation</center>"
fi
;;
"persistent_invitation="*)
local invitation="$(echo $opt | sed -e 's/^persistent_invitation=//')"
local invitation=$(echo "$opt" | sed -e 's/^persistent_invitation=//' -e "s/@INFO@/${module_info[$m]}/g")
local status="${module_status[$m]}"
if [ -n "$status" ]; then
print_invitation="$print_invitation<center><strong>$module_name:</strong> $invitation</center>"
......
var server = 'https://' + window.location.hostname + ':5509/janus';
var server = 'https://' + window.location.hostname + ':8089/janus';
var janus = null;
var videoroomHandle = null;
......@@ -10,7 +10,6 @@ var source = null;
document.addEventListener('DOMContentLoaded', function() {
parseRoomFromURL();
document.title += ' ' + room.toString();
Janus.init({ debug: 'all', callback: function() {
if (!Janus.isWebrtcSupported()) {
......
#preview-container {
margin: 8px;
height: 500px;
}
button,select,option,input {
font-size: small;
}
body {
font-size: medium;
.visually-hidden {
visibility: hidden;
}
......@@ -3,23 +3,15 @@
<title>Camera Sender</title>
<script type="text/javascript" src="adapter.min.js"></script>
<script type="text/javascript" src="janus.js"></script>
<script type="text/javascript" src="socket.io.js"></script>
<script type="text/javascript" src="camera-sender.js"></script>
<link rel="stylesheet" href="camera-sender.css">
</head>
<body>
<h3 id="room-indicator"></h3>
<div>
Leave the pin empty if the rooms has none set.
</div>
<!--
<select id="room-select">
<option value="1001">VNC 1</option>
<option value="1002">VNC 2</option>
<option value="1003">VNC 3</option>
<option value="1004">VNC 4</option>
<option value="1005">VNC 5</option>
<option value="1006">VNC 6</option>
</select>
-->
<select id="res-select">
<option value="lowres">320x240</option>
<option value="lowres-16:9">320x180</option>
......
document.addEventListener('DOMContentLoaded', function() {
// var server = 'https://' + window.location.hostname + ':8089/janus';
var server = 'https://' + window.location.hostname + ':5509/janus';
var janus = null;
var videoroomHandle = null;
var room = 1006;
var sendResolution = 'stdres';
var startButton = null;
var stopButton = null;
var startButton = document.getElementById('start');
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;
document.addEventListener('DOMContentLoaded', function() {
parseRoomFromURL();
startButton = document.getElementById('start');
stopButton = document.getElementById('stop');
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() {
if (!Janus.isWebrtcSupported()) {
alert('No WebRTC support... ');
......@@ -30,17 +101,10 @@ document.addEventListener('DOMContentLoaded', function() {
Janus.log('Plugin attached! (' + videoroomHandle.getPlugin() + ', id=' + videoroomHandle.getId() + ')');
startButton.onclick = function() {
// var roomSelect = document.getElementById('room-select');
var resSelect = document.getElementById('res-select');
var pinInput = document.getElementById('pin-input');
startButton.setAttribute('disabled', '');
// roomSelect.setAttribute('disabled', '');
resSelect.setAttribute('disabled', '');
stopButton.removeAttribute('disabled');
stopButton.onclick = function() {
janus.destroy();
};
// room = parseInt(roomSelect.value);
sendResolution = resSelect.value;
Janus.log('sendResolution:', sendResolution);
shareCamera(pinInput.value);
......@@ -54,11 +118,8 @@ document.addEventListener('DOMContentLoaded', function() {
},
webrtcState: function(on) {
if (on) {
var bandwidthForm = document.getElementById('bandwidth-form');
var bandwidthSubmit = document.getElementById('bandwidth-submit');
bandwidthForm.onsubmit = handleBandwidthFormSubmit;
bandwidthSubmit.removeAttribute('disabled', '');
alert('Sharing camera');
socket.emit('set_feed_id', { feedId }, handleSetFeedIdResponse);
// Sharing camera successful, when the set_feed_id request is successful
} else {
janus.destroy();
}
......@@ -68,11 +129,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('camera-preview') == null) {
var video = document.createElement('video');
video.setAttribute('id', 'camera-preview');
video.setAttribute('width', '100%');
video.setAttribute('height', '100%');
video.setAttribute('autoplay', '');
video.setAttribute('playsinline', '');
video.setAttribute('muted', 'muted');
if (!transmitting) {
video.classList.add('visually-hidden');
}
document.getElementById('preview-container').appendChild(video);
// var noVncLink = 'https://simon-doering.com/novnc/vnc.html?room=' + room;
......@@ -95,7 +157,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}});
}, false);
};
function shareCamera(pin) {
var register = {
......@@ -129,8 +191,9 @@ function handleMessage(msg, jsep) {
if (event) {
if (event === 'joined') {
Janus.log('Joined event:', msg);
Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + msg['id']);
if (msg['publishers'].length === 0) {
feedId = msg.id;
Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + feedId);
//if (msg['publishers'].length === 0) {
videoroomHandle.createOffer({
media: {
videoSend: true,
......@@ -151,10 +214,10 @@ function handleMessage(msg, jsep) {
alert('WebRTC error: ' + error.message);
}
});
} else {
alert('There is already somebody who is sharing his camera in this room!');
window.location.reload();
}
//} 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));
......@@ -171,8 +234,30 @@ function parseRoomFromURL() {
var roomParam = urlParams.get('room');
if (roomParam != null && !isNaN(roomParam)) {
room = parseInt(roomParam);
console.log('Using room ' + room + ' from URL query string');
} else {
console.log('Got no valid room in URL query string, using default room ' + room);
console.log('Got no valid room in URL search params, using default room ' + room);
}
}
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);
This diff is collapsed.
......@@ -4,12 +4,12 @@ document.addEventListener('DOMContentLoaded', function() {
var server = 'https://' + window.location.hostname + ':5509/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;
......@@ -33,9 +33,9 @@ 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 = '';
var socketLogicMounted = false;
// Every property (slot) holds a string that represents the active geometry for that camera slot
var videoActiveGeometry = {};
var previousCanvasGeometryState = {
vncHeight: 0,
vncWidth: 0,
......@@ -44,13 +44,16 @@ 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,
// z: 0
// }
var videoPrescale = 1;
parseVideoPrescaleFromURL();
......@@ -61,20 +64,35 @@ 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');
// Should only be triggered once, but the connect event
// can be triggered multiple time for example when reconnecting
if (!socketLogicMounted) {
socketLogicMounted = true;
console.log('mount socket logic');
registerSocketHandlers();
setInterval(adjustVideoGeometry, 500);
}
socket.emit('query_state', handleQueryStateResponse);
}
}, 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... ');
......@@ -85,29 +103,8 @@ document.addEventListener('DOMContentLoaded', function() {
server,
iceServers: [{urls: "turn:195.37.15.39:3478", username: "@TURN_USER@", credential: "@TURN_PASSWORD@"}],
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);
......@@ -120,105 +117,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) {
......@@ -241,16 +216,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');
......@@ -283,23 +248,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('init', function (cameraState) {
handleCommand(cameraState.geometry.command, cameraState.geometry.params);
handleCommand(cameraState.visibility.command, cameraState.visibility.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('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,10 +288,14 @@ document.addEventListener('DOMContentLoaded', function() {
var y = params[2];
var w = params[3];
var h = params[4];
var z = 100;
if (params.length >= 6) {
z += parseInt(params[5]);
}
setFixedPosition(video, origin, x, y, w, h);
videoGeometryParams = { origin, x, y, w, h };
videoActiveGeometry = command;
setFixedPosition(video, origin, x, y, w, h, z);
videoGeometryParams[slot] = { origin, x, y, w, h, z };
videoActiveGeometry[slot] = command;
break;
case 'set_geometry_relative_to_canvas':
......@@ -319,8 +304,12 @@ document.addEventListener('DOMContentLoaded', function() {
var y = parseInt(params[2]);
var w = parseInt(params[3]);
var h = parseInt(params[4]);
var z = 100;
if (params.length >= 6) {
z += parseInt(params[5]);
}
handleSetGeometryRelativeToCanvas(origin, x, y, w, h);
handleSetGeometryRelativeToCanvas(video, slot, origin, x, y, w, h, z);
break;
case 'show':
......@@ -335,11 +324,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, z) {
// 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, z };
var vncWidth = parseInt(canvas.width);
var vncHeight = parseInt(canvas.height);
......@@ -379,8 +367,8 @@ document.addEventListener('DOMContentLoaded', function() {
y += (canvasRect.bottom - canvasRect.height);
}
setFixedPosition(video, origin, x, y, w, h);
videoActiveGeometry = 'set_geometry_relative_to_canvas';
setFixedPosition(video, origin, x, y, w, h, z);
videoActiveGeometry[slot] = 'set_geometry_relative_to_canvas';
previousCanvasGeometryState = {
vncWidth,
vncHeight,
......@@ -391,11 +379,12 @@ document.addEventListener('DOMContentLoaded', function() {
};
}
function setFixedPosition(element, origin, x, y, w, h) {
function setFixedPosition(video, origin, x, y, w, h, z) {
var style = (
'position: fixed;' +
`width: ${w}px;` +
`height: ${h}px;`
`height: ${h}px;` +
`z-index: ${z};`
);
var xOrigin = origin.charAt(0);
......@@ -417,11 +406,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;
......@@ -435,17 +423,24 @@ 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,
params.z
);
}
});
}
}
});
......
#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%;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment