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

Migrate camera server to Typescript

parent 86d32b48
Branches
No related tags found
No related merge requests found
node_modules/
dist/
class ValidationError extends Error {
constructor(message) {
super(message);
}
}
module.exports = ValidationError;
......@@ -19,11 +19,40 @@
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz",
"integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg=="
},
"@types/engine.io": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.4.tgz",
"integrity": "sha512-98rXVukLD6/ozrQ2O80NAlWDGA4INg+tqsEReWJldqyi2fulC9V7Use/n28SWgROXKm6003ycWV4gZHoF8GA6w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "14.14.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.12.tgz",
"integrity": "sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g=="
},
"@types/socket.io": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.12.tgz",
"integrity": "sha512-oStc5VFkpb0AsjOxQUj9ztX5Iziatyla/rjZTYbFGoVrrKwd+JU2mtxk7iSl5RGYx9WunLo6UXW1fBzQok/ZyA==",
"dev": true,
"requires": {
"@types/engine.io": "*",
"@types/node": "*",
"@types/socket.io-parser": "*"
}
},
"@types/socket.io-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/socket.io-parser/-/socket.io-parser-2.2.1.tgz",
"integrity": "sha512-+JNb+7N7tSINyXPxAJb62+NcpC1x/fPn7z818W4xeNCdPTp6VsO/X8fCsg6+ug4a56m1v9sEiTIIUKVupcHOFQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
......
......@@ -7,5 +7,8 @@
},
"dependencies": {
"socket.io": "^3.0.4"
},
"devDependencies": {
"@types/socket.io": "^2.1.12"
}
}
export interface CommandDescriptor {
command: string;
params: string[];
}
type NullableString = string | null;
export class CameraSlotState {
slotActive = false;
token: NullableString = null;
feedActive = false;
feedId: NullableString = null;
senderSocketId: NullableString = null;
visibility: CommandDescriptor = {
command: 'show',
params: []
};
geometry: CommandDescriptor = {
command: 'set_geometry_relative_to_canvas',
params: ['rb', '0', '0', '200', '200']
};
}
\ No newline at end of file
import { Socket } from 'socket.io';
export interface SenderSocket extends Socket {
cameraSlot: number;
cameraSlotToken: string;
};
export class ValidationError extends Error {
constructor(message: string) {
super(message);
}
}
const ValidationError = require('./models/validation-error');
import { Server as SocketIOServer, Socket } from 'socket.io';
import { mountCleanupLogic } from './util/cleanup';
import { ValidationError } from './models/validation-error';
import { CameraSlotState, CommandDescriptor } from './models/camera-slot-state';
import { SenderSocket } from './models/sender-socket';
const readline = require('readline');
const rl = readline.createInterface({
......@@ -12,51 +17,42 @@ if (process.env.PORT) {
port = +process.env.PORT;
console.log('Using port ' + port + ' from PORT environment variable');
} else {
console.log('Got no PORT environment variable - using default port ' + port);
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');
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);
console.log(
'Got no CAMERA_SLOTS environment variable - using default count of ' +
cameraSlots
);
}
const io = require('socket.io')(port);
const io = new SocketIOServer(port);
const visibilityCommands = ['hide', 'show'];
const geometryCommands = [
'set_geometry_relative_to_window',
'set_geometry_relative_to_canvas'
];
const internalCommands = [
'activate_slot',
'deactivate_slot',
'refresh_token'
];
const internalCommands = ['activate_slot', 'deactivate_slot', 'refresh_token'];
let cameraStates = [];
let cameraSlotState: CameraSlotState[] = [];
for (let i = 0; i < cameraSlots; i++) {
cameraStates.push({
slotActive: false,
token: null,
feedActive: false,
feedId: null,
senderSocketId: null,
visibility: {
command: 'show',
params: []
},
geometry: {
command: 'set_geometry_relative_to_canvas',
params: ['rb', '0', '0', '200', '200']
}
});
cameraSlotState.push(new CameraSlotState());
}
const emitNewFeed = (slot) => {
const cameraState = cameraStates[slot];
const emitNewFeed = (slot: number) => {
const cameraState = cameraSlotState[slot];
io.emit('new_feed', {
slot,
feedId: cameraState.feedId,
......@@ -65,36 +61,60 @@ const emitNewFeed = (slot) => {
});
};
const emitRemoveFeed = (slot) => {
const emitRemoveFeed = (slot: number) => {
io.emit('remove_feed', { slot });
};
const handleSetFeedId = (socket, data, fn) => {
const handleSetFeedId = (
socket: SenderSocket,
data: null | { feedId?: string },
fn: Function
) => {
let success = true;
let message = '';
try {
const slot = socket.cameraSlot;
const currentCameraState = cameraStates[slot];
const currentCameraState = cameraSlotState[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');
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');
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');
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);
console.log(
'Error: Got set_feed_id event without a feed id on slot ' + slot
);
throw new ValidationError('No feed id was provided');
}
......@@ -118,12 +138,17 @@ const handleSetFeedId = (socket, data, fn) => {
fn({ success, message });
};
const handleSenderDisconnect = (socket, reason) => {
const handleSenderDisconnect = (socket: SenderSocket, _: string) => {
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');
const currentCameraState = cameraSlotState[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;
......@@ -133,36 +158,72 @@ const handleSenderDisconnect = (socket, reason) => {
}
};
const registerSenderHandlers = (socket) => {
const registerSenderHandlers = (socket: SenderSocket) => {
socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
};
const handleSenderInit = (socket, data, fn) => {
const handleSenderInit = (
socket: SenderSocket,
data: null | { slot?: string; token?: string },
fn: Function
) => {
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');
if (data == null) {
console.log('Error: Got socket connection without data');
throw new ValidationError('No data provided');
}
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 slotStr = data.slot;
if (slotStr == null) {
console.log('Error: Got socket connection without a slot');
throw new ValidationError('No slot provided');
}
const currentCameraState = cameraStates[slot];
if (!currentCameraState.slotActive) {
console.log('Error: Got socket connection for inactive slot ' + slot);
const slot = parseInt(slotStr);
if (isNaN(slot)) {
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'
);
}
if (slot < 0 || slot > cameraSlotState.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 slotState = cameraSlotState[slot];
if (!slotState.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);
if (token == null) {
console.log('Error: Got socket connection without token');
throw new ValidationError('No token provided');
}
if (slotState.token !== token) {
console.log(
'Error: Got socket connecion with wrong token ' +
token +
' for slot ' +
slot
);
throw new ValidationError('Invalid token');
}
......@@ -170,7 +231,7 @@ const handleSenderInit = (socket, data, fn) => {
message = 'Socket authenticated';
socket.cameraSlot = slot;
socket.cameraSlotToken = data.token;
socket.cameraSlotToken = token;
registerSenderHandlers(socket);
} catch (e) {
......@@ -185,11 +246,17 @@ const handleSenderInit = (socket, data, fn) => {
fn({ success, message });
};
const handleQueryState = (fn) => {
const handleQueryState = (fn: Function) => {
console.log('Got state query from socket');
let response = {};
for (let i = 0; i < cameraStates.length; i++) {
const cameraState = cameraStates[i];
let response: {
[index: number]: {
feedId: string | null;
visibility: CommandDescriptor;
geometry: CommandDescriptor;
};
} = {};
for (let i = 0; i < cameraSlotState.length; i++) {
const cameraState = cameraSlotState[i];
if (cameraState.feedActive) {
response[i] = {
feedId: cameraState.feedId,
......@@ -199,16 +266,20 @@ const handleQueryState = (fn) => {
}
}
fn(response);
}
};
io.on('connection', (socket) => {
io.on('connection', (socket: Socket) => {
socket.on('query_state', handleQueryState);
socket.on('sender_init', handleSenderInit.bind(null, socket));
});
const handleInternalCommand = (command, slot, params) => {
const currentCameraState = cameraStates[slot];
const handleInternalCommand = (
command: string,
slot: number,
params: string[]
) => {
const currentCameraState = cameraSlotState[slot];
switch (command) {
case 'activate_slot':
if (currentCameraState.slotActive) {
......@@ -216,7 +287,11 @@ const handleInternalCommand = (command, slot, params) => {
return;
}
if (params.length === 0) {
console.log('Error while activating slot ' + slot + ' - Got no token parameter');
console.log(
'Error while activating slot ' +
slot +
' - Got no token parameter'
);
return;
}
currentCameraState.token = params[0];
......@@ -238,11 +313,17 @@ const handleInternalCommand = (command, slot, params) => {
break;
case 'refresh_token':
if (!currentCameraState.slotActive) {
console.log('Error: Tried to refresh token for inactive slot ' + slot);
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(
'Error while refreshing token for slot ' +
slot +
' - Got no token parameter'
);
console.log('Keeping old token');
return;
}
......@@ -250,37 +331,50 @@ const handleInternalCommand = (command, slot, params) => {
currentCameraState.token = params[0];
break;
default:
console.log('Error: handleInternalCommand got unknown command ' + command);
console.log(
'Error: handleInternalCommand got unknown command ' + command
);
break;
}
};
const handleCommand = (line) => {
const handleCommand = (line: string) => {
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');
if (command == null) {
console.log('Error: Got malformed line with no command');
return;
}
const slotStr = params.shift();
if (isNaN(slotStr)) {
console.log('Error: Could not parse slot ' + slotStr + ' to an integer');
if (slotStr == null) {
console.log('Error: Got no slot to apply the command on');
return;
}
const slot = parseInt(slotStr);
if (isNaN(slot)) {
console.log(
'Error: Could not parse slot ' + slotStr + ' to an integer'
);
return;
}
if (slot < 0 || slot > cameraSlotState.length - 1) {
console.log(
`Error: Got invalid slot number ${slot}. There are ${cameraSlotState.length} camera slots.`
);
return;
}
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];
const currentCameraState = cameraSlotState[slot];
if (visibilityCommands.includes(command)) {
currentCameraState.visibility = {
......@@ -310,33 +404,8 @@ const handleCommand = (line) => {
params
});
}
}
rl.on('line', handleCommand);
const cleanup = () => {
console.log('cleanup');
io.emit('remove_all_feeds');
};
const exitHandler = (options, exitCode) => {
if (options.cleanup) cleanup();
if (exitCode || exitCode === 0) console.log(exitCode);
if (options.exit) process.exit();
}
// do something when app is closing
process.on('exit', exitHandler.bind(null, { cleanup:true }));
// catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, { exit:true }));
// catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR1', exitHandler.bind(null, { exit:true }));
process.on('SIGUSR2', exitHandler.bind(null, { exit:true }));
// catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, { exit:true }));
rl.on('line', handleCommand);
// catches termination
process.on('SIGTERM', exitHandler.bind(null, { exit:true }));
mountCleanupLogic(io);
import { Server as SocketIOServer } from 'socket.io';
interface ExitHandlerOptions {
cleanup?: boolean;
exit?: boolean;
}
export const mountCleanupLogic = (io: SocketIOServer) => {
const cleanup = () => {
console.log('cleanup');
io.emit('remove_all_feeds');
};
const exitHandler = (options: ExitHandlerOptions, exitCode: number) => {
if (options.cleanup) cleanup();
if (exitCode || exitCode === 0) console.log(exitCode);
if (options.exit) process.exit();
}
// do something when app is closing
process.on('exit', exitHandler.bind(null, { cleanup:true }));
// catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, { exit:true }));
// catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR1', exitHandler.bind(null, { exit:true }));
process.on('SIGUSR2', exitHandler.bind(null, { exit:true }));
// catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, { exit:true }));
// catches termination
process.on('SIGTERM', exitHandler.bind(null, { exit:true }));
}
\ No newline at end of file
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
const io = require('socket.io')(5005);
io.on('connection', (socket) => {
socket.emit('command', { message: 'Hello, world!' });
socket.on('query', (param1) => {
console.log('query:', param1);
});
rl.on('line', function(line) {
console.log('Got command from stdin:', line);
const params = line.split(' ');
const command = params.shift();
console.log('command:', command);
console.log('params:', params);
socket.emit('command', {
command,
params
});
});
});
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": [
"dom",
"es6",
"es2017",
"esnext.asynciterable"
],
"sourceMap": true,
"outDir": "./dist",
"moduleResolution": "node",
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"baseUrl": "."
},
"exclude": [
"node_modules"
],
"include": [
"./src/**/*.ts"
]
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment