Browse Source

first commit

merge-requests/1/head
Paul Tirk 1 year ago
commit
1a47ee71d3
  1. 281
      .eslintrc.json
  2. 3
      .gitignore
  3. 8
      config.json
  4. 6433
      package-lock.json
  5. 36
      package.json
  6. 68
      src/backend.ts
  7. 40
      src/classes/AbstractWebRtcServerHelper.ts
  8. 105
      src/classes/BackendApiRequestHelper.ts
  9. 220
      src/classes/JanusWebRtcServerHelper.ts
  10. 322
      src/classes/RoomApiHandler.ts
  11. 102
      src/classes/SessionManager.ts
  12. 512
      src/classes/SignalingMessageHelper.ts
  13. 282
      src/janus-videoroom-client.d.ts
  14. 24
      tsconfig.json
  15. 38
      webpack.config.js

281
.eslintrc.json

@ -0,0 +1,281 @@
//these rules are based on the aurelia-tools standard and modified for recordIt use
{
"root": true,
"parser": "@typescript-eslint/parser",
// https://github.com/babel/babel-eslint
"parserOptions": {
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": false,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": false,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"spread": true,
"superInFunctions": true,
"templateStrings": true,
"jsx": false,
"legacyDecorators": true
},
"sourceType": "module",
"ecmaVersion": 6
},
"env": {
// http://eslint.org/docs/user-guide/configuring.html#specifying-environments
"browser": true,
"node": true,
"es6": true
},
"rules": {
"no-tabs": "error",
// // babel inserts "use strict"; for us
// "strict": [
// "error",
// "never"
// ],
// "no-var": "error",
// "prefer-const": "warn",
// "no-shadow": "error",
// "no-shadow-restricted-names": "error",
// "no-unused-vars": [
// "error",
// {
// "vars": "local",
// "args": "none"
// }
// ],
// "no-use-before-define": "off",
// "comma-dangle": [
// "error",
// "never"
// ],
// "no-cond-assign": [
// "error",
// "except-parens"
// ],
// "no-console": "off",
// "no-debugger": "warn",
// "no-alert": "warn",
// "no-constant-condition": "warn",
// "no-dupe-keys": "error",
// "no-dupe-class-members": "error",
// "no-duplicate-case": "error",
// "no-empty": "error",
// "no-ex-assign": "error",
// "no-extra-boolean-cast": "off",
// "no-extra-semi": "error",
// "no-func-assign": "error",
// "no-inner-declarations": "error",
// "no-invalid-regexp": "error",
// "no-irregular-whitespace": "error",
// "no-obj-calls": "error",
// "no-sparse-arrays": "error",
// "no-unreachable": "error",
// "use-isnan": "error",
// "block-scoped-var": "off",
// /**
// * Best practices
// */
// "consistent-return": "off",
// "curly": [
// "error",
// "multi-line"
// ],
"default-case": "error",
// "dot-notation": "off",
// "eqeqeq": [
// "warn",
// "smart"
// ],
// "guard-for-in": "off",
// "no-caller": "error",
// "no-else-return": "off",
// "no-eq-null": "off",
// "no-eval": "error",
// "no-extend-native": "error",
// "no-extra-bind": "error",
// "no-fallthrough": "error",
// "no-floating-decimal": "error",
// "no-implied-eval": "error",
// "no-lone-blocks": "error",
// "no-loop-func": "error",
// "no-multi-str": "error",
// "no-native-reassign": "error",
// "no-new": "error",
// "no-new-func": "error",
// "no-new-wrappers": "error",
// "no-octal": "error",
// "no-octal-escape": "error",
// "no-param-reassign": "off",
// "no-proto": "error",
// "no-redeclare": "error",
// "no-return-assign": "error",
// "no-script-url": "error",
// "no-self-compare": "error",
// "no-sequences": "error",
// "no-throw-literal": "error",
// "no-with": "error",
// "radix": [
// "warn",
// "as-needed"
// ],
// "vars-on-top": "error",
// "wrap-iife": [
// "error",
// "any"
// ],
// "yoda": "error",
// /**
// * Style
// */
"indent": [
"error",
2,
{
"SwitchCase": 1
// "MemberExpression": 1,
// "FunctionDeclaration": {
// "parameters": "first"
// },
// "FunctionExpression": {
// "parameters": "first"
// }
}
],
// "brace-style": [
// "error",
// "1tbs",
// {
// "allowSingleLine": true
// }
// ],
"quotes": [
"error",
"single",
"avoid-escape"
],
// "camelcase": "off",
"comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
// "comma-style": [
// "error",
// "last"
// ],
// "eol-last": "error",
// "func-names": "off",
// "key-spacing": [
// "error",
// {
// "beforeColon": false,
// "afterColon": true
// }
// ],
// "new-cap": [
// "error",
// {
// "newIsCap": true
// }
// ],
// "no-multiple-empty-lines": [
// "error",
// {
// "max": 2
// }
// ],
// "no-nested-ternary": "error",
// "no-new-object": "error",
// "no-spaced-func": "error",
"no-trailing-spaces": "error",
// "no-extra-parens": [
// "error",
// "functions"
// ],
// "no-underscore-dangle": "off",
// "one-var": [
// "error",
// "never"
// ],
"padded-blocks": [
"error",
{
"classes": "always"
}
],
"semi": [
"error",
"always"
],
// "semi-spacing": [
// "error",
// {
// "before": false,
// "after": true
// }
// ],
"keyword-spacing": [
"error",
{
"overrides": {
"if": {
"after": false
},
"for": {
"after": false
},
"catch": {
"after": false
},
"while": {
"after": false
},
"switch": {
"after": false
}
}
}
],
// "space-before-blocks": "error",
// "space-before-function-paren": ["error", {
// "anonymous": "never",
// "named": "never",
// "asyncArrow": "always"
// }],
// "space-infix-ops": "error",
"spaced-comment": [
"error",
"always",
{
// "exceptions": [
// "*"
// ],
// "markers": [
// "*"
// ]
}
],
"linebreak-style": [
"error",
"unix"
],
// "no-undef": "error",
"lines-between-class-members": ["error", "always", {
"exceptAfterSingleLine": true
}]
}
}

3
.gitignore

@ -0,0 +1,3 @@
node_modules
dist
.vscode

8
config.json

@ -0,0 +1,8 @@
{
"port": 3000,
"sharedSecret": "MySecretValue",
"janusClient": {
"url": "ws://localhost:8188",
"apiSecret": "MyJanusApiSecret"
}
}

6433
package-lock.json
File diff suppressed because it is too large
View File

36
package.json

@ -0,0 +1,36 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "backend.js",
"scripts": {
"default": "npx webpack",
"build": "npx webpack",
"build-dev-watch": "npx webpack --watch --progress --colors"
},
"repository": {},
"author": "Paul Tirk",
"license": "GPL",
"dependencies": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"janus-videoroom-client": "^4.1.6",
"randomstring": "^1.1.5",
"ws": "^7.2.3"
},
"devDependencies": {
"@types/express": "^4.17.3",
"@types/node": "^13.9.5",
"@types/randomstring": "^1.1.6",
"@types/ws": "^7.2.3",
"@typescript-eslint/parser": "^2.26.0",
"clean-webpack-plugin": "^3.0.0",
"del": "^5.1.0",
"eslint": "^6.8.0",
"ts-loader": "^6.2.2",
"typescript": "^3.8.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
}
}

68
src/backend.ts

@ -0,0 +1,68 @@
import express from 'express';
import http from 'http';
import bodyParser from 'body-parser';
import crypto from 'crypto';
import WebSocket from 'ws';
import * as config from '../config.json';
import {SessionManager} from './classes/SessionManager';
import {RoomApiHandler} from './classes/RoomApiHandler';
import {SignalingMessageHelper} from './classes/SignalingMessageHelper';
import {BackendApiRequestHelper} from './classes/BackendApiRequestHelper';
import {JanusWebRtcServerHelper} from './classes/JanusWebRtcServerHelper';
let webRtcServerHelper = null;
if(config.janusClient) {
webRtcServerHelper = new JanusWebRtcServerHelper({
url: config.janusClient.url,
apiSecret: config.janusClient.apiSecret
});
}
const backendApiRequestHelper = new BackendApiRequestHelper();
const sessionManager = new SessionManager(webRtcServerHelper);
const app = express();
app.use(bodyParser.text({ type: 'application/json' }));
app.use((request, response, next) => {
const randomString = request.headers['spreed-signaling-random'];
const checksum = request.headers['spreed-signaling-checksum'];
if(!randomString) {
return response.status(401).send();
}
const hmac = crypto.createHmac('sha256', config.sharedSecret);
hmac.update(randomString.toString());
hmac.update(request.body);
const newChecksum = hmac.digest('hex');
if(checksum === newChecksum) {
request.body = JSON.parse(request.body);
next();
} else {
response.status(401).send();
}
});
const server = http.createServer(app);
const webSocketServer = new WebSocket.Server({
server: server,
path: '/spreed'
});
const signalingMessageHelper = new SignalingMessageHelper(
webSocketServer,
sessionManager,
backendApiRequestHelper,
webRtcServerHelper
);
const roomApiHandler = new RoomApiHandler(sessionManager, signalingMessageHelper);
app.use(roomApiHandler.getRouter());
server.listen(config.port, () => { console.log('server started'); });

40
src/classes/AbstractWebRtcServerHelper.ts

@ -0,0 +1,40 @@
abstract class AbstractWebRtcServerHelper {
abstract async createSession(): Promise<string>;
abstract async destroySession(sessionId: string): Promise<void>;
abstract createRoom(sessionId: string, roomId: string): RoomInfo;
abstract deleteRoom(sessionId: string, roomId: string): void;
abstract async joinRoom(sessionId: string, roomId: string): Promise<void>;
abstract async leaveRoom(sessionId: string, roomId: string): Promise<void>;
abstract async publishInRoom(sessionId: string, roomId: string, offer: Offer): Promise<Answer>;
abstract async subscribeToFeedOfRoom(sessionId: string, roomId: string, sessionIdToSubscribe: string): Promise<Offer>;
abstract async setAnswerToSubscription(sessionId: string, sessionIdToSubscribe: string, answer: Answer): Promise<void>
abstract async unpublish(sessionId: string): Promise<void>;
abstract async unsubscribe(sessionId: string, sessionIdToSubscribe: string): Promise<void>;
abstract async trickleCandidate(sessionId: string, candidate: Candidate): Promise<void>;
async trickleCandidates(sessionId: string, candidates: Array<Candidate>): Promise<void> {
for(const candidate of candidates) {
await this.trickleCandidate(sessionId, candidate);
}
}
abstract async trickleCompleted(sessionId: string): Promise<void>
}
interface RoomInfo {
// TODO
}
type Offer = string
type Answer = string
interface Candidate {
// TODO
}

105
src/classes/BackendApiRequestHelper.ts

@ -0,0 +1,105 @@
import * as randomstring from 'randomstring';
import * as crypto from 'crypto';
import Axios from 'axios';
import * as config from '../../config.json';
export class BackendApiRequestHelper {
_url: string = '';
async sendAuthRequest(data) {
const requestBody = {
type: 'auth',
auth: {
version: '1.0',
params: data.params
}
};
this._url = data.url;
const backendResponse = await this._sendBackendAPIRequest(data.url, requestBody);
return backendResponse.auth;
}
async sendRoomRequest(data: RoomRequestData): Promise<RoomResponseData> {
const requestBody = {
type: 'room',
room: {
version: '1.0',
roomid: data.roomid,
userid: data.userid,
sessionid: data.sessionid,
action: data.action
}
};
const backendResponse = await this._sendBackendAPIRequest(this._url, requestBody);
return backendResponse.room;
}
async sendPingRequest(data): Promise<PingResponseData> {
// TODO - find out what and why?
const requestBody = {
type: 'ping',
ping: {
// roomid: data.roomid,
// entries: [
// {
// userid?,
// sessionid
// }
// ]
}
};
const backendResponse = await this._sendBackendAPIRequest(this._url, requestBody);
return backendResponse.ping;
}
async _sendBackendAPIRequest(url: string, data) {
const randomString = randomstring.generate(32);
const hmac = crypto.createHmac('sha256', config.sharedSecret);
hmac.update(randomString);
hmac.update(JSON.stringify(data));
const checksum = hmac.digest('hex');
const response = await Axios.post(url, data, {
headers: {
'Spreed-Signaling-Random': randomString,
'Spreed-Signaling-Checksum': checksum,
'OCS-APIRequest': true
}
});
const responseData = response.data.ocs.data;
return new Promise((res, rej) => {
if(responseData.error) {
rej(responseData.error);
} else {
res(responseData);
}
});
}
}
export interface RoomRequestData {
version: '1.0',
roomid: string,
userid: string,
sessionid: string,
action: 'join'|'leave'
}
interface RoomResponseData {
version: '1.0',
roomid: string,
properties: {} // TODO
}

220
src/classes/JanusWebRtcServerHelper.ts

@ -0,0 +1,220 @@
// const JanusVideoroomClient = require('janus-videoroom-client').Janus;
import {Janus, Session as JanusSession, VideoRoomPluginListenerHandle, VideoRoomPluginPublisherHandle} from 'janus-videoroom-client';
export class JanusWebRtcServerHelper implements AbstractWebRtcServerHelper {
private janusClient: Janus;
private sessions: Map<number, Session> = new Map();
constructor(options: JanusOptions) {
this.janusClient = new Janus(options);
this.janusClient.onConnected(this.onConnectedHandler.bind(this));
this.janusClient.onDisconnected(this.onDisconnectedHandler.bind(this));
this.janusClient.onError(this.onErrorHandler.bind(this));
this.janusClient.onEvent(this.onEventHandler.bind(this));
this.connect();
}
private onConnectedHandler() {
console.log('Janus: connected');
}
private onDisconnectedHandler() {
console.log('Janus: disconnected, reconnecting...');
this.connect();
}
private onErrorHandler() {
throw new Error('Janus: could not connect!');
}
private onEventHandler(event) {
console.log('JANUS EVENT:', event);
}
private connect() {
this.janusClient.connect();
}
async createSession(): Promise<string> {
const session = await this.janusClient.createSession();
this.sessions.set(session.getId(), {
janusSession: session,
publisherHandle: null,
listenerHandles: new Map()
});
return session.getId().toFixed();
}
async destroySession(sessionId: string): Promise<void> {
return await this.janusClient.destroySession(Number(sessionId));
}
async getRoomInfos(sessionId: string) {
const defaultHandle = await this.getDefaultHandle(sessionId);
const roomList = await defaultHandle.list();
return roomList.list;
}
private async getRoomInfo(sessionId: string, roomId: string) {
const roomInfos = await this.getRoomInfos(sessionId);
return roomInfos.find(roomInfo => roomInfo.description === roomId);
}
async createRoom(sessionId: string, roomId: string) {
const defaultHandle = await this.getDefaultHandle(sessionId);
const room = await defaultHandle.create({
description: roomId,
is_private: false,
publishers: 100
});
console.log('JANUS: room created', roomId);
return room.room;
}
async getOrCreateRoom(sessionId: string, roomId: string) {
const roomList = await this.getRoomInfos(sessionId);
const room = roomList.find(roomInfo => roomInfo.description === roomId);
if(!room) {
return await this.createRoom(sessionId, roomId);
} else {
return room;
}
}
async joinRoom(sessionId: string, roomId: string) {
const room = await this.getOrCreateRoom(sessionId, roomId);
}
async leaveRoom(sessionId: string, roomId: string) {
}
async deleteRoom(sessionId: string, roomId: string) {
const roomInfo = this.getRoomInfo(sessionId, roomId);
const defaultHandle = await this.getDefaultHandle(sessionId);
defaultHandle.destroy({ room: roomInfo.room });
}
async publishInRoom(sessionId: string, roomId: string, offerSdp: string) {
const session = this.getSession(sessionId);
const room = await this.getRoomInfo(sessionId, roomId);
if(!room) throw new RoomError(roomId);
const publisherHandle = await session.janusSession.videoRoom().publishFeed(room.room, offerSdp);
session.publisherHandle = publisherHandle;
return publisherHandle.getAnswer();
}
async getFeedsOfRoom(sessionId: string, roomId: string) {
const session = this.getSession(sessionId);
const room = await this.getRoomInfo(sessionId, roomId);
if(!room) throw new RoomError(roomId);
return await session.janusSession.videoRoom().getFeeds(room.room);
}
async subscribeToFeedOfRoom(sessionId: string, roomId: string, sessionIdToSubscribe: string) {
const session = this.getSession(sessionId);
const room = await this.getRoomInfo(sessionId, roomId);
if(!room) throw new RoomError(roomId);
const feedId = this.getFeedIdFromSessionId(sessionIdToSubscribe);
const listenerHandle = await session.janusSession.videoRoom().listenFeed(room.room, feedId);
session.listenerHandles.set(feedId, listenerHandle);
return listenerHandle.getOffer();
}
async setAnswerToSubscription(sessionId: string, sessionIdToSubscribe: string, answerSdp: string) {
const session = this.getSession(sessionId);
const feedId = this.getFeedIdFromSessionId(sessionIdToSubscribe);
const listenerHandle = session.listenerHandles.get(feedId);
await listenerHandle.setRemoteAnswer(answerSdp);
}
async unpublish(sessionId: string) {
}
async unsubscribe(sessionId: string, sessionIdToSubscribe: string) {
}
async trickleCandidate(sessionId: string, candidate: Candidate) {
const handle = await this.getDefaultHandle(sessionId);
handle.trickle(candidate);
}
async trickleCandidates(sessionId: string, candidates: Array<Candidate>) {
const handle = await this.getDefaultHandle(sessionId);
handle.trickles(candidates);
}
async trickleCompleted(sessionId: string) {
const handle = await this.getDefaultHandle(sessionId);
handle.trickleCompleted();
}
private getSession(sessionId: string): Session {
const session = this.sessions.get(Number(sessionId));
if(!session) throw new SessionError();
return session;
}
private async getDefaultHandle(sessionId: string) {
const session = this.getSession(sessionId);
return await session.janusSession.videoRoom().defaultHandle();
}
private getFeedIdFromSessionId(sessionId: string): number {
const session = this.getSession(sessionId);
return session.publisherHandle?.getPublisherId();
}
}
interface Session {
janusSession: JanusSession;
publisherHandle: VideoRoomPluginPublisherHandle|null;
listenerHandles: Map<number, VideoRoomPluginListenerHandle>
}
interface JanusOptions {
url: string,
apiSecret: string|null
}
class SessionError extends Error {
// TODO
}
class RoomError extends Error {
private _roomId: string
constructor(roomId: string) {
super();
this.message = 'Room does not exist!';
this._roomId = roomId;
}
getRoomId() {
return this._roomId;
}
}

322
src/classes/RoomApiHandler.ts

@ -0,0 +1,322 @@
import {SessionManager} from './SessionManager';
import {SignalingMessageHelper, RoomListEventType} from './SignalingMessageHelper';
import {Router, Response, Request} from 'express';
import express from 'express';
export class RoomApiHandler {
private sessionManager: SessionManager;
private signalingMessageHelper: SignalingMessageHelper;
private router: Router;
constructor(sessionManager: SessionManager, signalingMessageHelper: SignalingMessageHelper) {
this.sessionManager = sessionManager;
this.signalingMessageHelper = signalingMessageHelper;
this.router = express.Router();
this.setUpRoutes();
}
getRouter(): Router {
return this.router;
}
private setUpRoutes() {
this.router.post('/api/v1/room/:roomId', this.handleRoomApiRequest.bind(this));
}
private handleRoomApiRequest(request: Request, response: Response) {
const roomId = request.params.roomId;
const body: RoomApiRequestBody = request.body;
switch(body.type) {
case RoomApiRequestType.INVITE:
this.handleInviteRequest(roomId, body.invite);
break;
case RoomApiRequestType.DISINVITE:
this.handleDisinviteRequest(roomId, body.disinvite);
break;
case RoomApiRequestType.UPDATE:
this.handleUpdateRequest(roomId, body.update);
break;
case RoomApiRequestType.DELETE:
this.handleDeleteRequest(roomId, body.delete);
break;
case RoomApiRequestType.PARTICIPANTS:
this.handleParticipantsRequest(roomId, body.participants);
break;
case RoomApiRequestType.INCALL:
this.handleIncallRequest(roomId, body.incall);
break;
case RoomApiRequestType.MESSAGE:
this.handleMessageRequest(roomId, body.message);
break;
default:
console.warn('API REQ BODY:', request.body);
// TODO
break;
}
response.status(200).send();
}
// Room::EVENT_AFTER_USERS_ADD
private handleInviteRequest(roomId: string, data: RoomApiInviteRequestData) {
console.log('API INVITE:', data);
const sessions = this.sessionManager.getSessionsForUserIds(data.userids);
sessions.forEach(session => {
const sendData = {
roomid: roomId,
properties: data.properties
};
this.signalingMessageHelper.sendRoomListEvent(session.socket, RoomListEventType.INVITE, sendData);
});
}
// Room::EVENT_AFTER_USER_REMOVE
// Room::EVENT_AFTER_PARTICIPANT_REMOVE -> sessions
private handleDisinviteRequest(roomId: string, data: RoomApiDisinviteRequestData) {
console.log('API DISINVITE:', data);
const sessions = data.userids ? this.sessionManager.getSessionsForUserIds(data.userids) : this.sessionManager.getSessionsByIds(data.sessionids);
sessions.forEach(session => {
const sendData = {
roomid: roomId,
properties: data.properties
};
this.signalingMessageHelper.sendRoomListEvent(session.socket, RoomListEventType.DISINVITE, sendData);
});
}
// Room::EVENT_AFTER_NAME_SET
// Room::EVENT_AFTER_PASSWORD_SET
// Room::EVENT_AFTER_TYPE_SET
// Room::EVENT_AFTER_READONLY_SET
// Room::EVENT_AFTER_LOBBY_STATE_SET
// TODO remove handler with "roomModified" in favour of handler with
// "participantsModified" once the clients no longer expect a
// "roomModified" message for participant type changes.
// Room::EVENT_AFTER_PARTICIPANT_TYPE_SET
private handleUpdateRequest(roomId: string, data: RoomApiUpdateRequestData) {
console.log('API UPDATE:', data);
const sessions = this.sessionManager.getSessionsForUserIds(data.userids);
sessions.forEach(session => {
const sendData = {
roomid: roomId,
properties: data.properties
};
this.signalingMessageHelper.sendRoomListEvent(session.socket, RoomListEventType.UPDATE, sendData);
});
}
// Room::EVENT_BEFORE_ROOM_DELETE
private handleDeleteRequest(roomId: string, data: RoomApiDeleteRequestData) {
console.log('API DELETE:', data);
// TODO
const sessions = this.sessionManager.getSessionsForUserIds(data.userids);
sessions.forEach(session => {
const sendData = {
roomid: roomId,
properties: []
};
this.signalingMessageHelper.sendRoomListEvent(session.socket, RoomListEventType.DISINVITE, sendData);
});
}
// Room::EVENT_AFTER_PARTICIPANT_TYPE_SET
// Room::EVENT_AFTER_GUESTS_CLEAN
// GuestManager::EVENT_AFTER_NAME_UPDATE
private handleParticipantsRequest(roomId: string, data: RoomApiParticipantsRequestData) {
console.log('API PARTICIPANTS:', data);
const userIds = data.users.map(user => user.userId);
const sessions = this.sessionManager.getSessionsForUserIds(userIds);
sessions.forEach(session => {
this.signalingMessageHelper.sendParticipantsListEvent(session.socket, {
roomid: roomId,
users: this.remapParticipantUserIds(data.changed)
});
});
}
// Room::EVENT_AFTER_SESSION_JOIN_CALL
// Room::EVENT_AFTER_SESSION_LEAVE_CALL
private handleIncallRequest(roomId: string, data: RoomApiIncallRequestData) {
console.log('API INCALL:', data);
const sessions = this.sessionManager.getSessionsForRoomId(roomId);
sessions.forEach(session => {
this.signalingMessageHelper.sendParticipantsListEvent(session.socket, {
roomid: roomId,
users: this.remapParticipantUserIds(data.changed)
});
});
}
// ChatManager::EVENT_AFTER_MESSAGE_SEND
// ChatManager::EVENT_AFTER_SYSTEM_MESSAGE_SEND
private handleMessageRequest(roomId: string, data: RoomApiMessageRequestData) {
console.log('API MESSAGE:', data);
if(data.data && data.data.chat) {
const sessions = this.sessionManager.getSessionsForRoomId(roomId);
sessions.forEach(session => {
const socket = session.socket;
socket.send(JSON.stringify(data.data));
});
}
}
private remapParticipantUserIds(userIds: Array<Participant>): Array<Participant> {
return userIds.map(changedItem => {
const session = this.sessionManager.getSessionByNcId(changedItem.sessionId);
if(!session) return changedItem;
const sessionId = session.id;
return {
...changedItem,
sessionId: sessionId
};
});
}
}
// export namespace RoomApiHandler {
interface RoomApiRequestBody<T, U> {
type: RoomApiRequestType
[T]: U
}
type RoomApiRequestData =
RoomApiInviteRequestData |
RoomApiDisinviteRequestData |
RoomApiUpdateRequestData |
RoomApiDeleteRequestData |
RoomApiParticipantsRequestData |
RoomApiIncallRequestData |
RoomApiMessageRequestData
interface RoomApiInviteRequestData {
userids: Array<string>;
alluserids: Array<string>;
properties: RoomApiRequestDataProperties;
}
type RoomApiDisinviteRequestData = RoomApiDisinviteUserIdsRequestData | RoomApiDisinviteSessionIdsRequestData;
interface RoomApiDisinviteRequestDataCommon {
alluserids: Array<string>;
properties: RoomApiRequestDataProperties;
}
type RoomApiDisinviteUserIdsRequestData = RoomApiDisinviteRequestDataCommon & {
userids: Array<string>;
}
// TODO variant with sessionids?
type RoomApiDisinviteSessionIdsRequestData = RoomApiDisinviteRequestDataCommon & {
sessionids: Array<string>;
}
interface RoomApiUpdateRequestData {
userids: Array<string>;
properties: RoomApiRequestDataProperties;
}
interface RoomApiDeleteRequestData {
userids: Array<string>;
}
interface RoomApiParticipantsRequestData {
changed: Array<Participant>;
users: Array<Participant>;
}
interface RoomApiIncallRequestData {
incall: number; // TODO Flags
changed: Array<Participant>;
users: Array<Participant>;
}
interface RoomApiMessageRequestData {
data: {
chat: { refresh: true };
}
}
interface RoomApiRequestDataProperties {
name: string;
type: RoomType;
'lobby-state': LobbyState;
'lobby-timer': RoomApiDateObject|null;
'read-only': ReadOnly;
'active-since': RoomApiDateObject|null;
}
enum RoomApiRequestType {
INVITE = 'invite',
DISINVITE = 'disinvite',
UPDATE = 'update',
DELETE = 'delete',
PARTICIPANTS = 'participants',
INCALL = 'incall',
MESSAGE = 'message'
}
enum RoomType {
UNKNOWN_CALL = -1,
ONE_TO_ONE_CALL = 1,
GROUP_CALL,
PUBLIC_CALL,
CHANGELOG_CONVERSATION
}
enum LobbyState {
NO = 0,
YES
}
enum ReadOnly {
READ_WRITE = 0,
READ_ONLY
}
export interface Participant {
inCall: number; // Combination of ParticipantFlags
lastPing: number;
sessionId: string;
participantType: ParticipantType;
userId: string | undefined; // for guests
}
enum ParticipantType {
OWNER = 1,
MODERATOR,
USER,
GUEST,
USER_SELF_JOINED,
GUEST_MODERATOR
}
enum ParticipantFlags {
DISCONNECTED = 0,
IN_CALL = 1,
WITH_AUDIO = 2,
WITH_VIDEO = 4
}
interface RoomApiDateObject {
date: string,
timezone: string,
timezone_type: number
}
// }

102
src/classes/SessionManager.ts

@ -0,0 +1,102 @@
import WebSocket from 'ws';
export class SessionManager {
private sessions: Array<Session>
private webRtcServerHelper: AbstractWebRtcServerHelper|null;
constructor(webRtcServerHelper: AbstractWebRtcServerHelper|null) {
this.sessions = [];
this.webRtcServerHelper = webRtcServerHelper;
}
getSessionById(sessionId: string): Session|null {
return this.sessions.find(session => session.id === sessionId) || null;
}
getSessionsByIds(sessionIds: Array<string>): Array<Session> {
return this.sessions.filter(session => sessionIds.includes(session.id));
}
getSessionForSocket(socket: WebSocket): Session|null {
return this.sessions.find(session => session.socket === socket) || null;
}
getSessionByNcId(ncSessionId: string): Session|null {
return this.sessions.find(session => session.ncSessionId === ncSessionId) || null;
}
getSessionsForRoomId(roomId: string): Array<Session> {
return this.sessions.filter(session => session.ncRoomId === roomId);
}
getSessionsForUserId(userId: string): Array<Session> {
return this.sessions.filter(session => session.ncUserId === userId);
}
getSessionsForUserIds(userIds: Array<string>): Array<Session> {
return this.sessions.filter(session => userIds.includes(session.ncUserId));
}
async createSession(sessionCreationData: SessionCreationData) {
const newSession: Session = {
...sessionCreationData,
id: await this.getNewSessionId(),
subscriberIds: new Map()
};
this.sessions.push(newSession);
return newSession;
}
private async getNewSessionId(): Promise<string> {
if(this.webRtcServerHelper) {
return this.webRtcServerHelper.createSession();
} else {
return Date.now().toFixed();
}
}
deleteSession(session: Session) {
this.deleteSessionById(session.id);
}
deleteSessionById(sessionId: string) {
const index = this.sessions.findIndex(session => session.id === sessionId);
if(index >= 0) {
const sessionToDelete = this.sessions[index];
this.cleanupSession(sessionToDelete);
this.sessions.splice(index, 1);
}
}
deleteSessionBySocket(socket: WebSocket) {
const session = this.sessions.find(session => session.socket === socket);
if(session) this.deleteSessionById(session.id);
}
private cleanupSession(session: Session) {
this.webRtcServerHelper?.unpublish(session.id);
session.subscriberIds.forEach(id => {
this.webRtcServerHelper?.unsubscribe(session.id, id);
});
}
}
export interface Session {
id: string;
socket: WebSocket;
ncUserId: string;
ncRoomId?: string;
ncSessionId?: string;
subscriberIds: Map<string, any>;
}
interface SessionCreationData {
ncUserId: string;
socket: WebSocket;
}

512
src/classes/SignalingMessageHelper.ts

@ -0,0 +1,512 @@
import WebSocket from 'ws';
import {SessionManager, Session} from './SessionManager';
import {BackendApiRequestHelper, RoomRequestData} from './BackendApiRequestHelper';
import {Participant} from './RoomApiHandler';
export class SignalingMessageHelper {
private webRtcServerHelper: AbstractWebRtcServerHelper|null;
private sessionManager: SessionManager;
private backendApiRequestHelper: BackendApiRequestHelper;
constructor(webSocketServer: WebSocket.Server, sessionManager: SessionManager, backendApiRequestHelper: BackendApiRequestHelper, webRtCServerHelper: AbstractWebRtcServerHelper|null) {
this.sessionManager = sessionManager;
this.backendApiRequestHelper = backendApiRequestHelper;
this.webRtcServerHelper = webRtCServerHelper;
webSocketServer.on('connection', this.onConnection.bind(this));
webSocketServer.on('close', this.onDisconnect.bind(this));
}
private onConnection(socket: WebSocket) {
console.log('user connected...');
socket.on('message', (message: string) => {
this.handleMessage(socket, message);
});
socket.on('close', () => {
console.log('user disconnected (client).');
this.sessionManager.deleteSessionBySocket(socket);
});
}
private onDisconnect() {
console.log('user disconnected (server).');
}
private handleMessage(socket: WebSocket, message: string) {
// console.log('message:', message);
const data: HelloMessage|ByeMessage|RoomMessage|ControlMessage|MessageMessage = JSON.parse(message);
// console.log('data:', data);
switch(data.type) {
case MessageType.HELLO:
this.handleHelloMessage(socket, data);
break;
case MessageType.BYE:
this.handleByeMessage(socket, data);
break;
case MessageType.ROOM:
this.handleRoomMessage(socket, data.id, data.room);
break;
case MessageType.CONTROL:
this.handleControlMessage(socket, data.control);
break;
case MessageType.MESSAGE:
this.handleMessageMessage(socket, data.message);
break;
default:
throw new Error('message not understood!');
}
}
private async handleHelloMessage(socket: WebSocket, data: HelloMessage) {
console.log('HELLO:', data.hello);
const backendResponse = await this.backendApiRequestHelper.sendAuthRequest(data.hello.auth);
const userId = backendResponse.userid;
const session = await this.sessionManager.createSession({ socket: socket, ncUserId: userId });
const features = [];
if(this.webRtcServerHelper) features.push('mcu');
const response: HelloMessageResponse = {
id: data.id,
type: MessageType.HELLO,
hello: {
sessionid: session.id,
resumeid: '',
userid: userId,
version: '1.0',
server: {
features: features
}
}
};
this.sendSocketMessage(socket, response);
}
private handleByeMessage(socket: WebSocket, data: ByeMessage) {
console.log('BYE:', data.bye);
this.sessionManager.deleteSessionBySocket(socket);
const response: ByeMessageResponse = {
id: data.id,
type: MessageType.BYE,
bye: {}
};
this.sendSocketMessage(socket, response);
// TODO
}
private async handleRoomMessage(socket: WebSocket, id: string, data: RoomMessageData) {
console.log('ROOM:', data);
const session = this.sessionManager.getSessionForSocket(socket);
if(!session) throw new Error('session for socket not found!');
const action = data.roomid ? 'join' : 'leave';
const sendData: RoomRequestData = {
version: '1.0',
userid: session.ncUserId,
sessionid: data.sessionid || session.ncSessionId,
roomid: data.roomid || session.ncRoomId,
action: action
};
try {
const backendResponse = await this.backendApiRequestHelper.sendRoomRequest(sendData);
const response = {
id: id,
type: 'room',
room: backendResponse
};
session.ncRoomId = data.roomid || undefined;
session.ncSessionId = action === 'join' ? data.sessionid : undefined;
this.sendSocketMessage(socket, response);
if(sendData.action === 'join') {
this.webRtcServerHelper?.joinRoom(session.id, session.ncRoomId);
this.sendRoomEvent(sendData.roomid, RoomEventType.JOIN, [ { sessionid: session.id } ]); // TODO session objects
} else {
this.webRtcServerHelper?.leaveRoom(session.id, data.roomid);
this.sendRoomEvent(data.roomid, RoomEventType.LEAVE, [ session.id ]);
}
} catch(error) {
console.error(error);
}
}
private handleControlMessage(socket: WebSocket, data: ControlMessageData) {
console.log('CONTROL:', data);
}
private async handleMessageMessage(socket: WebSocket, data: MessageMessageData) {
console.log('MESSAGE:', data);
const senderSessionId = this.sessionManager.getSessionForSocket(socket)?.id;
const recipient = data.recipient;
let response = null;
switch(recipient.type) {
case MessageMessageRecipientType.SESSION:
response = await this.handleMessageToSession(senderSessionId, recipient.sessionid, data.data);
break;
case MessageMessageRecipientType.USER:
this.handleMessageToUser(recipient.userid, data.data);
break;
case MessageMessageRecipientType.ROOM:
const session = this.sessionManager.getSessionForSocket(socket);
this.handleMessageToRoom(session.ncRoomId, data.data);
break;
default:
console.warn('Message not understood!', data);
}
if(response) {
const messageResponse: MessageMessageResponse = {
type: MessageType.MESSAGE,
message: {
sender: {
type: MessageMessageRecipientType.SESSION,
sessionid: recipient.sessionid,
userid: recipient.userid
},
data: response
}
};
this.sendSocketMessage(socket, messageResponse);
}
}
private async handleMessageToSession(senderSessionId: string, sessionId: string, data) {
console.log('SESSION MESSAGE:', data);
const session = this.sessionManager.getSessionById(sessionId);
if(!session) return; // TODO Error handling?
const payload = data.payload;
switch(data.type) {
case 'offer':
return {
type: 'answer',
roomType: data.roomType,
payload: {
type: 'answer',
sdp: await this.handleOffer(session, payload),
nick: payload.nick
}
};
case 'candidate':
await this.handleCandidate(session, payload.candidate);
break;
case 'requestoffer':
return {
type: 'offer',
from: data.to,
roomType: 'video',
payload: {
type: 'offer',
sdp: await this.handleOfferRequest(session, senderSessionId),
// nick: payload.nick
}
};
case 'answer':
await this.handleAnswer(session, senderSessionId, payload);
break;
default:
console.warn('Type of session message not understood!', data);
}
// if(session) this._sendSocketMessage(session.socket, data);
}
private async handleOffer(session: Session, payload) {
if(this.webRtcServerHelper) {
return this.webRtcServerHelper.publishInRoom(session.id, session.ncRoomId, payload.sdp);
} else {
}
}
private async handleCandidate(session: Session, candidate) {
if(this.webRtcServerHelper) {
return await this.webRtcServerHelper.trickleCandidate(session.id, candidate);
} else {
}
}
private async handleOfferRequest(session: Session, senderSessionId: string) {
if(this.webRtcServerHelper) {
session.subscriberIds.set(senderSessionId, true);
return await this.webRtcServerHelper.subscribeToFeedOfRoom(senderSessionId, session.ncRoomId, session.id);
} else {
}
}
private async handleAnswer(session: Session, senderSessionId: string, payload) {
if(this.webRtcServerHelper) {
await this.webRtcServerHelper.setAnswerToSubscription(senderSessionId, session.id, payload.sdp);
} else {
}
}
private handleMessageToUser(userId: string, data) {
const sessions = this.sessionManager.getSessionsForUserId(userId);
sessions.forEach((session) => {
this.sendSocketMessage(session.socket, data);
});
}
private handleMessageToRoom(roomId: string, data) {
const sessions = this.sessionManager.getSessionsForRoomId(roomId);
sessions.forEach((session) => {
this.sendSocketMessage(session.socket, data);
});
}
// ********** MESSAGE ANSWER **********
private sendAnswerMessage(sender, data) {
// this._sendSocketMessage(socket, {
// type: 'message',
// message: {
// sender: sender,
// data: data
// }
// });
}
// ********** EVENTS **********
sendRoomEvent(roomId: string, type: RoomEventType, eventData: RoomEventData) {
const sendData = {
target: 'room',
type: type,
[type]: eventData
};
const roomSessions = this.sessionManager.getSessionsForRoomId(roomId);
roomSessions.forEach((session) => {
this.sendEventMessage(session.socket, sendData);
});
}
sendRoomListEvent(socket: WebSocket, type: RoomListEventType, eventData: RoomListEventData) {
const sendData = {
target: 'roomlist',
type: type,
[type]: eventData
};
this.sendEventMessage(socket, sendData);
}
sendParticipantsListEvent(socket: WebSocket, eventData: ParticipantsListEventData) {
const sendData = {
target: 'participants',
type: 'update',
update: eventData
};
this.sendEventMessage(socket, sendData);
}
private sendEventMessage(socket: WebSocket, eventData: MessageEventData) {
const sendData = {
type: 'event',
event: eventData
};
this.sendSocketMessage(socket, sendData);
}
private sendSocketMessage(socket: WebSocket, data: Object) {
socket.send(JSON.stringify(data));
}
}
interface Message {
id: string
type: MessageType
}
interface HelloMessage extends Message {
type: MessageType.HELLO
hello: HelloMessageData
}
interface HelloMessageData {
version: '1.0'
auth: {
url: string
params: {
userid: string,
ticket;
}
}
}
interface HelloMessageResponse {
id: string
type: MessageType.HELLO
hello: HelloMessageResponseData
}
interface HelloMessageResponseData {
sessionid: string
resumeid: string
userid: string
version: '1.0'
server: {
features: Array<string>
}
}
interface ByeMessage extends Message {
type: MessageType.BYE,
bye: {}
}
interface ByeMessageResponse {
id: string;
type: MessageType.BYE;
bye: {}
}
interface RoomMessage extends Message {
type: MessageType.ROOM
room: RoomMessageData
}
type RoomMessageData = JoinRoomMessageData | LeaveRoomMessageData;
interface JoinRoomMessageData {
roomid: string
// Pass the Nextcloud session id to the signaling server. The
// session id will be passed through to Nextcloud to check if
// the (Nextcloud) user is allowed to join the room.
sessionid: string
}
interface LeaveRoomMessageData {
roomid: ''
}
interface ControlMessage extends Message {
type: MessageType.CONTROL
control: ControlMessageData
}
interface ControlMessageData {
recipient: MessageRecipientData; // currently only to sessions?
data;
}
interface MessageMessage extends Message {
type: MessageType.MESSAGE
message: MessageMessageData
}
interface MessageMessageData {
recipient: MessageRecipientData
data: