merge from develop
This commit is contained in:
+111
-14
@@ -1,5 +1,7 @@
|
||||
const os = require('os');
|
||||
const userRoles = require('../userRoles');
|
||||
// const AwaitQueue = require('awaitqueue');
|
||||
// const axios = require('axios');
|
||||
|
||||
module.exports =
|
||||
{
|
||||
@@ -34,8 +36,15 @@ module.exports =
|
||||
},
|
||||
*/
|
||||
// URI and key for requesting geoip-based TURN server closest to the client
|
||||
turnAPIKey : 'examplekey',
|
||||
turnAPIURI : 'https://example.com/api/turn',
|
||||
turnAPIKey : 'examplekey',
|
||||
turnAPIURI : 'https://example.com/api/turn',
|
||||
turnAPIparams : {
|
||||
'uri_schema' : 'turn',
|
||||
'transport' : 'tcp',
|
||||
'ip_ver' : 'ipv4',
|
||||
'servercount' : '2'
|
||||
},
|
||||
|
||||
// Backup turnservers if REST fails or is not configured
|
||||
backupTurnServers : [
|
||||
{
|
||||
@@ -46,24 +55,68 @@ module.exports =
|
||||
credential : 'example'
|
||||
}
|
||||
],
|
||||
fileTracker : 'wss://tracker.lab.vvc.niif.hu:443',
|
||||
redisOptions : {},
|
||||
// session cookie secret
|
||||
cookieSecret : 'T0P-S3cR3t_cook!e',
|
||||
cookieName : 'multiparty-meeting.sid',
|
||||
// if you use encrypted private key the set the passphrase
|
||||
tls :
|
||||
{
|
||||
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
|
||||
// passphrase: 'key_password'
|
||||
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
|
||||
},
|
||||
// listening Host or IP
|
||||
// If omitted listens on every IP. ("0.0.0.0" and "::")
|
||||
// listeningHost: 'localhost',
|
||||
// Listening port for https server.
|
||||
listeningPort : 443,
|
||||
// Any http request is redirected to https.
|
||||
// Listening port for http server.
|
||||
// Listening port for http server.
|
||||
listeningRedirectPort : 80,
|
||||
// Listens only on http, only on listeningPort
|
||||
// listeningRedirectPort disabled
|
||||
// use case: loadbalancer backend
|
||||
httpOnly : false,
|
||||
// WebServer/Express trust proxy config for httpOnly mode
|
||||
// You can find more info:
|
||||
// - https://expressjs.com/en/guide/behind-proxies.html
|
||||
// - https://www.npmjs.com/package/proxy-addr
|
||||
// use case: loadbalancer backend
|
||||
trustProxy : '',
|
||||
// This logger class will have the log function
|
||||
// called every time there is a room created or destroyed,
|
||||
// or peer created or destroyed. This would then be able
|
||||
// to log to a file or external service.
|
||||
/* StatusLogger : class
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._queue = new AwaitQueue();
|
||||
}
|
||||
|
||||
// rooms: rooms object
|
||||
// peers: peers object
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async log({ rooms, peers })
|
||||
{
|
||||
this._queue.push(async () =>
|
||||
{
|
||||
// Do your logging in here, use queue to keep correct order
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Number of rooms: ', rooms.size);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Number of peers: ', peers.size);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('error in log', error);
|
||||
});
|
||||
}
|
||||
}, */
|
||||
// This function will be called on successful login through oidc.
|
||||
// Use this function to map your oidc userinfo to the Peer object.
|
||||
// The roomId is equal to the room name.
|
||||
@@ -124,6 +177,7 @@ module.exports =
|
||||
peer.addRole(userRoles.AUTHENTICATED);
|
||||
},
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
userMapping : async ({ peer, roomId, userinfo }) =>
|
||||
{
|
||||
if (userinfo.picture != null)
|
||||
@@ -153,20 +207,53 @@ module.exports =
|
||||
peer.email = userinfo.email;
|
||||
}
|
||||
},
|
||||
// Required roles for Access. All users have the role "ALL" by default.
|
||||
// Other roles need to be added in the "userMapping" function. This
|
||||
// is an Array of roles. userRoles.ADMIN have all priveleges and access
|
||||
// always.
|
||||
// All users have the role "NORMAL" by default. Other roles need to be
|
||||
// added in the "userMapping" function. The following accesses and
|
||||
// permissions are arrays of roles. Roles can be changed in userRoles.js
|
||||
//
|
||||
// Example:
|
||||
// [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
|
||||
// This will allow all MODERATOR and AUTHENTICATED users access.
|
||||
requiredRolesForAccess : [ userRoles.ALL ],
|
||||
accessFromRoles : {
|
||||
// The role(s) will gain access to the room
|
||||
// even if it is locked (!)
|
||||
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
|
||||
// The role(s) will gain access to the room without
|
||||
// going into the lobby. If you want to restrict access to your
|
||||
// server to only directly allow authenticated users, you could
|
||||
// add the userRoles.AUTHENTICATED to the user in the userMapping
|
||||
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
|
||||
BYPASS_LOBBY : [ userRoles.NORMAL ]
|
||||
},
|
||||
permissionsFromRoles : {
|
||||
// The role(s) have permission to lock/unlock a room
|
||||
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
|
||||
// The role(s) have permission to promote a peer from the lobby
|
||||
PROMOTE_PEER : [ userRoles.NORMAL ],
|
||||
// The role(s) have permission to send chat messages
|
||||
SEND_CHAT : [ userRoles.NORMAL ],
|
||||
// The role(s) have permission to moderate chat
|
||||
MODERATE_CHAT : [ userRoles.MODERATOR ],
|
||||
// The role(s) have permission to share screen
|
||||
SHARE_SCREEN : [ userRoles.NORMAL ],
|
||||
// The role(s) have permission to produce extra video
|
||||
EXTRA_VIDEO : [ userRoles.NORMAL ],
|
||||
// The role(s) have permission to share files
|
||||
SHARE_FILE : [ userRoles.NORMAL ],
|
||||
// The role(s) have permission to moderate files
|
||||
MODERATE_FILES : [ userRoles.MODERATOR ],
|
||||
// The role(s) have permission to moderate room (e.g. kick user)
|
||||
MODERATE_ROOM : [ userRoles.MODERATOR ]
|
||||
},
|
||||
// When truthy, the room will be open to all users when as long as there
|
||||
// are allready users in the room
|
||||
activateOnHostJoin : true,
|
||||
activateOnHostJoin : true,
|
||||
// When set, maxUsersPerRoom defines how many users can join
|
||||
// a single room. If not set, there is no limit.
|
||||
// maxUsersPerRoom : 20,
|
||||
// Room size before spreading to new router
|
||||
routerScaleSize : 40,
|
||||
// Mediasoup settings
|
||||
mediasoup :
|
||||
mediasoup :
|
||||
{
|
||||
numWorkers : Object.keys(os.cpus()).length,
|
||||
// mediasoup Worker settings.
|
||||
@@ -247,11 +334,12 @@ module.exports =
|
||||
{
|
||||
listenIps :
|
||||
[
|
||||
// change ip to your servers IP address!
|
||||
{ ip: '0.0.0.0', announcedIp: null }
|
||||
// change 192.0.2.1 IPv4 to your server's IPv4 address!!
|
||||
{ ip: '192.0.2.1', announcedIp: null }
|
||||
|
||||
// Can have multiple listening interfaces
|
||||
// { ip: '::/0', announcedIp: null }
|
||||
// change 2001:DB8::1 IPv6 to your server's IPv6 address!!
|
||||
// { ip: '2001:DB8::1', announcedIp: null }
|
||||
],
|
||||
initialAvailableOutgoingBitrate : 1000000,
|
||||
minimumAvailableOutgoingBitrate : 600000,
|
||||
@@ -259,4 +347,13 @@ module.exports =
|
||||
maxIncomingBitrate : 1500000
|
||||
}
|
||||
}
|
||||
// Prometheus exporter
|
||||
/*
|
||||
prometheus: {
|
||||
deidentify: false, // deidentify IP addresses
|
||||
numeric: false, // show numeric IP addresses
|
||||
port: 8889, // allocated port
|
||||
quiet: false // include fewer labels
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
+4
-3
@@ -47,7 +47,8 @@ class Lobby extends EventEmitter
|
||||
return Object.values(this._peers).map((peer) =>
|
||||
({
|
||||
peerId : peer.id,
|
||||
displayName : peer.displayName
|
||||
displayName : peer.displayName,
|
||||
picture : peer.picture
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -62,8 +63,8 @@ class Lobby extends EventEmitter
|
||||
|
||||
for (const peer in this._peers)
|
||||
{
|
||||
if (peer.socket)
|
||||
this.promotePeer(peer.id);
|
||||
if (!this._peers[peer].closed)
|
||||
this.promotePeer(peer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
-5
@@ -23,9 +23,15 @@ class Peer extends EventEmitter
|
||||
|
||||
this._joined = false;
|
||||
|
||||
this._joinedTimestamp = null;
|
||||
|
||||
this._inLobby = false;
|
||||
|
||||
this._roles = [ userRoles.ALL ];
|
||||
this._authenticated = false;
|
||||
|
||||
this._authenticatedTimestamp = null;
|
||||
|
||||
this._roles = [ userRoles.NORMAL ];
|
||||
|
||||
this._displayName = false;
|
||||
|
||||
@@ -33,10 +39,14 @@ class Peer extends EventEmitter
|
||||
|
||||
this._email = null;
|
||||
|
||||
this._routerId = null;
|
||||
|
||||
this._rtpCapabilities = null;
|
||||
|
||||
this._raisedHand = false;
|
||||
|
||||
this._raisedHandTimestamp = null;
|
||||
|
||||
this._transports = new Map();
|
||||
|
||||
this._producers = new Map();
|
||||
@@ -133,9 +143,18 @@ class Peer extends EventEmitter
|
||||
|
||||
set joined(joined)
|
||||
{
|
||||
joined ?
|
||||
this._joinedTimestamp = Date.now() :
|
||||
this._joinedTimestamp = null;
|
||||
|
||||
this._joined = joined;
|
||||
}
|
||||
|
||||
get joinedTimestamp()
|
||||
{
|
||||
return this._joinedTimestamp;
|
||||
}
|
||||
|
||||
get inLobby()
|
||||
{
|
||||
return this._inLobby;
|
||||
@@ -146,6 +165,32 @@ class Peer extends EventEmitter
|
||||
this._inLobby = inLobby;
|
||||
}
|
||||
|
||||
get authenticated()
|
||||
{
|
||||
return this._authenticated;
|
||||
}
|
||||
|
||||
set authenticated(authenticated)
|
||||
{
|
||||
if (authenticated !== this._authenticated)
|
||||
{
|
||||
authenticated ?
|
||||
this._authenticatedTimestamp = Date.now() :
|
||||
this._authenticatedTimestamp = null;
|
||||
|
||||
const oldAuthenticated = this._authenticated;
|
||||
|
||||
this._authenticated = authenticated;
|
||||
|
||||
this.emit('authenticationChanged', { oldAuthenticated });
|
||||
}
|
||||
}
|
||||
|
||||
get authenticatedTimestamp()
|
||||
{
|
||||
return this._authenticatedTimestamp;
|
||||
}
|
||||
|
||||
get roles()
|
||||
{
|
||||
return this._roles;
|
||||
@@ -195,6 +240,16 @@ class Peer extends EventEmitter
|
||||
this._email = email;
|
||||
}
|
||||
|
||||
get routerId()
|
||||
{
|
||||
return this._routerId;
|
||||
}
|
||||
|
||||
set routerId(routerId)
|
||||
{
|
||||
this._routerId = routerId;
|
||||
}
|
||||
|
||||
get rtpCapabilities()
|
||||
{
|
||||
return this._rtpCapabilities;
|
||||
@@ -212,9 +267,18 @@ class Peer extends EventEmitter
|
||||
|
||||
set raisedHand(raisedHand)
|
||||
{
|
||||
raisedHand ?
|
||||
this._raisedHandTimestamp = Date.now() :
|
||||
this._raisedHandTimestamp = null;
|
||||
|
||||
this._raisedHand = raisedHand;
|
||||
}
|
||||
|
||||
get raisedHandTimestamp()
|
||||
{
|
||||
return this._raisedHandTimestamp;
|
||||
}
|
||||
|
||||
get transports()
|
||||
{
|
||||
return this._transports;
|
||||
@@ -314,10 +378,12 @@ class Peer extends EventEmitter
|
||||
{
|
||||
const peerInfo =
|
||||
{
|
||||
id : this.id,
|
||||
displayName : this.displayName,
|
||||
picture : this.picture,
|
||||
roles : this.roles
|
||||
id : this.id,
|
||||
displayName : this.displayName,
|
||||
picture : this.picture,
|
||||
roles : this.roles,
|
||||
raisedHand : this.raisedHand,
|
||||
raisedHandTimestamp : this.raisedHandTimestamp
|
||||
};
|
||||
|
||||
return peerInfo;
|
||||
|
||||
+456
-134
@@ -2,11 +2,37 @@ const EventEmitter = require('events').EventEmitter;
|
||||
const axios = require('axios');
|
||||
const Logger = require('./Logger');
|
||||
const Lobby = require('./Lobby');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const userRoles = require('../userRoles');
|
||||
const config = require('../config/config');
|
||||
|
||||
const logger = new Logger('Room');
|
||||
|
||||
// In case they are not configured properly
|
||||
const accessFromRoles =
|
||||
{
|
||||
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
|
||||
BYPASS_LOBBY : [ userRoles.NORMAL ],
|
||||
...config.accessFromRoles
|
||||
};
|
||||
|
||||
const permissionsFromRoles =
|
||||
{
|
||||
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
|
||||
PROMOTE_PEER : [ userRoles.NORMAL ],
|
||||
SEND_CHAT : [ userRoles.NORMAL ],
|
||||
MODERATE_CHAT : [ userRoles.MODERATOR ],
|
||||
SHARE_SCREEN : [ userRoles.NORMAL ],
|
||||
EXTRA_VIDEO : [ userRoles.NORMAL ],
|
||||
SHARE_FILE : [ userRoles.NORMAL ],
|
||||
MODERATE_FILES : [ userRoles.MODERATOR ],
|
||||
MODERATE_ROOM : [ userRoles.MODERATOR ],
|
||||
...config.permissionsFromRoles
|
||||
};
|
||||
|
||||
const ROUTER_SCALE_SIZE = config.routerScaleSize || 40;
|
||||
|
||||
class Room extends EventEmitter
|
||||
{
|
||||
/**
|
||||
@@ -14,38 +40,57 @@ class Room extends EventEmitter
|
||||
*
|
||||
* @async
|
||||
*
|
||||
* @param {mediasoup.Worker} mediasoupWorker - The mediasoup Worker in which a new
|
||||
* @param {mediasoup.Worker} mediasoupWorkers - The mediasoup Worker in which a new
|
||||
* mediasoup Router must be created.
|
||||
* @param {String} roomId - Id of the Room instance.
|
||||
*/
|
||||
static async create({ mediasoupWorker, roomId })
|
||||
static async create({ mediasoupWorkers, roomId })
|
||||
{
|
||||
logger.info('create() [roomId:"%s"]', roomId);
|
||||
|
||||
// Shuffle workers to get random cores
|
||||
let shuffledWorkers = mediasoupWorkers.sort(() => Math.random() - 0.5);
|
||||
|
||||
// Router media codecs.
|
||||
const mediaCodecs = config.mediasoup.router.mediaCodecs;
|
||||
|
||||
// Create a mediasoup Router.
|
||||
const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });
|
||||
const mediasoupRouters = new Map();
|
||||
|
||||
// Create a mediasoup AudioLevelObserver.
|
||||
const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(
|
||||
let firstRouter = null;
|
||||
|
||||
for (const worker of shuffledWorkers)
|
||||
{
|
||||
const router = await worker.createRouter({ mediaCodecs });
|
||||
|
||||
if (!firstRouter)
|
||||
firstRouter = router;
|
||||
|
||||
mediasoupRouters.set(router.id, router);
|
||||
}
|
||||
|
||||
// Create a mediasoup AudioLevelObserver on first router
|
||||
const audioLevelObserver = await firstRouter.createAudioLevelObserver(
|
||||
{
|
||||
maxEntries : 1,
|
||||
threshold : -80,
|
||||
interval : 800
|
||||
});
|
||||
|
||||
return new Room({ roomId, mediasoupRouter, audioLevelObserver });
|
||||
firstRouter = null;
|
||||
shuffledWorkers = null;
|
||||
|
||||
return new Room({ roomId, mediasoupRouters, audioLevelObserver });
|
||||
}
|
||||
|
||||
constructor({ roomId, mediasoupRouter, audioLevelObserver })
|
||||
constructor({ roomId, mediasoupRouters, audioLevelObserver })
|
||||
{
|
||||
logger.info('constructor() [roomId:"%s"]', roomId);
|
||||
|
||||
super();
|
||||
this.setMaxListeners(Infinity);
|
||||
|
||||
this._uuid = uuidv4();
|
||||
|
||||
// Room ID.
|
||||
this._roomId = roomId;
|
||||
|
||||
@@ -55,12 +100,6 @@ class Room extends EventEmitter
|
||||
// Locked flag.
|
||||
this._locked = false;
|
||||
|
||||
// Required roles to access
|
||||
this._requiredRoles = [ userRoles.ALL ];
|
||||
|
||||
if ('requiredRolesForAccess' in config)
|
||||
this._requiredRoles = config.requiredRolesForAccess;
|
||||
|
||||
// if true: accessCode is a possibility to open the room
|
||||
this._joinByAccesCode = true;
|
||||
|
||||
@@ -78,8 +117,13 @@ class Room extends EventEmitter
|
||||
|
||||
this._peers = {};
|
||||
|
||||
// mediasoup Router instance.
|
||||
this._mediasoupRouter = mediasoupRouter;
|
||||
// Array of mediasoup Router instances.
|
||||
this._mediasoupRouters = mediasoupRouters;
|
||||
|
||||
// The router we are currently putting peers in
|
||||
this._routerIterator = this._mediasoupRouters.values();
|
||||
|
||||
this._currentRouter = this._routerIterator.next().value;
|
||||
|
||||
// mediasoup AudioLevelObserver.
|
||||
this._audioLevelObserver = audioLevelObserver;
|
||||
@@ -102,8 +146,14 @@ class Room extends EventEmitter
|
||||
|
||||
this._closed = true;
|
||||
|
||||
this._chatHistory = null;
|
||||
|
||||
this._fileHistory = null;
|
||||
|
||||
this._lobby.close();
|
||||
|
||||
this._lobby = null;
|
||||
|
||||
// Close the peers.
|
||||
for (const peer in this._peers)
|
||||
{
|
||||
@@ -113,41 +163,86 @@ class Room extends EventEmitter
|
||||
|
||||
this._peers = null;
|
||||
|
||||
// Close the mediasoup Router.
|
||||
this._mediasoupRouter.close();
|
||||
// Close the mediasoup Routers.
|
||||
for (const router of this._mediasoupRouters.values())
|
||||
{
|
||||
router.close();
|
||||
}
|
||||
|
||||
this._routerIterator = null;
|
||||
|
||||
this._currentRouter = null;
|
||||
|
||||
this._mediasoupRouters.clear();
|
||||
|
||||
this._audioLevelObserver = null;
|
||||
|
||||
// Emit 'close' event.
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
handlePeer(peer)
|
||||
verifyPeer({ id, token })
|
||||
{
|
||||
logger.info('handlePeer() [peer:"%s", roles:"%s"]', peer.id, peer.roles);
|
||||
try
|
||||
{
|
||||
const decoded = jwt.verify(token, this._uuid);
|
||||
|
||||
// Allow reconnections, remove old peer
|
||||
logger.info('verifyPeer() [decoded:"%o"]', decoded);
|
||||
|
||||
return decoded.id === id;
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
logger.warn('verifyPeer() | invalid token');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handlePeer({ peer, returning })
|
||||
{
|
||||
logger.info('handlePeer() [peer:"%s", roles:"%s", returning:"%s"]', peer.id, peer.roles, returning);
|
||||
|
||||
// Should not happen
|
||||
if (this._peers[peer.id])
|
||||
{
|
||||
logger.warn(
|
||||
'handleConnection() | there is already a peer with same peerId [peer:"%s"]',
|
||||
peer.id);
|
||||
|
||||
this._peers[peer.id].close();
|
||||
}
|
||||
|
||||
// Always let ADMIN in, even if locked
|
||||
if (peer.roles.includes(userRoles.ADMIN))
|
||||
// Returning user
|
||||
if (returning)
|
||||
this._peerJoining(peer, true);
|
||||
else if ( // Has a role that is allowed to bypass room lock
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
|
||||
)
|
||||
this._peerJoining(peer);
|
||||
else if (
|
||||
'maxUsersPerRoom' in config &&
|
||||
(
|
||||
Object.keys(this._peers).length +
|
||||
this._lobby.peerList().length
|
||||
) >= config.maxUsersPerRoom)
|
||||
{
|
||||
this._handleOverRoomLimit(peer);
|
||||
}
|
||||
else if (this._locked)
|
||||
this._parkPeer(peer);
|
||||
else
|
||||
{
|
||||
// If the user has a role in config.requiredRolesForAccess, let them in
|
||||
peer.roles.some((role) => this._requiredRoles.includes(role)) ?
|
||||
// Has a role that is allowed to bypass lobby
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ?
|
||||
this._peerJoining(peer) :
|
||||
this._handleGuest(peer);
|
||||
}
|
||||
}
|
||||
|
||||
_handleOverRoomLimit(peer)
|
||||
{
|
||||
this._notification(peer.socket, 'overRoomLimit');
|
||||
}
|
||||
|
||||
_handleGuest(peer)
|
||||
{
|
||||
if (config.activateOnHostJoin && !this.checkEmpty())
|
||||
@@ -169,7 +264,11 @@ class Room extends EventEmitter
|
||||
|
||||
this._peerJoining(promotedPeer);
|
||||
|
||||
for (const peer of this._getJoinedPeers())
|
||||
for (
|
||||
const peer of this._getPeersWithPermission({
|
||||
permission : permissionsFromRoles.PROMOTE_PEER
|
||||
})
|
||||
)
|
||||
{
|
||||
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
|
||||
}
|
||||
@@ -177,18 +276,18 @@ class Room extends EventEmitter
|
||||
|
||||
this._lobby.on('peerRolesChanged', (peer) =>
|
||||
{
|
||||
// Always let admin in, even if locked
|
||||
if (peer.roles.includes(userRoles.ADMIN))
|
||||
if ( // Has a role that is allowed to bypass room lock
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
|
||||
)
|
||||
{
|
||||
this._lobby.promotePeer(peer.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has a role in config.requiredRolesForAccess, let them in
|
||||
if (
|
||||
if ( // Has a role that is allowed to bypass lobby
|
||||
!this._locked &&
|
||||
peer.roles.some((role) => this._requiredRoles.includes(role))
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role))
|
||||
)
|
||||
{
|
||||
this._lobby.promotePeer(peer.id);
|
||||
@@ -201,7 +300,11 @@ class Room extends EventEmitter
|
||||
{
|
||||
const { id, displayName } = changedPeer;
|
||||
|
||||
for (const peer of this._getJoinedPeers())
|
||||
for (
|
||||
const peer of this._getPeersWithPermission({
|
||||
permission : permissionsFromRoles.PROMOTE_PEER
|
||||
})
|
||||
)
|
||||
{
|
||||
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
|
||||
}
|
||||
@@ -211,7 +314,11 @@ class Room extends EventEmitter
|
||||
{
|
||||
const { id, picture } = changedPeer;
|
||||
|
||||
for (const peer of this._getJoinedPeers())
|
||||
for (
|
||||
const peer of this._getPeersWithPermission({
|
||||
permission : permissionsFromRoles.PROMOTE_PEER
|
||||
})
|
||||
)
|
||||
{
|
||||
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
|
||||
}
|
||||
@@ -223,7 +330,11 @@ class Room extends EventEmitter
|
||||
|
||||
const { id } = closedPeer;
|
||||
|
||||
for (const peer of this._getJoinedPeers())
|
||||
for (
|
||||
const peer of this._getPeersWithPermission({
|
||||
permission : permissionsFromRoles.PROMOTE_PEER
|
||||
})
|
||||
)
|
||||
{
|
||||
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
|
||||
}
|
||||
@@ -326,13 +437,17 @@ class Room extends EventEmitter
|
||||
{
|
||||
this._lobby.parkPeer(parkPeer);
|
||||
|
||||
for (const peer of this._getJoinedPeers())
|
||||
for (
|
||||
const peer of this._getPeersWithPermission({
|
||||
permission : permissionsFromRoles.PROMOTE_PEER
|
||||
})
|
||||
)
|
||||
{
|
||||
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
|
||||
}
|
||||
}
|
||||
|
||||
async _peerJoining(peer)
|
||||
async _peerJoining(peer, returning = false)
|
||||
{
|
||||
peer.socket.join(this._roomId);
|
||||
|
||||
@@ -341,47 +456,60 @@ class Room extends EventEmitter
|
||||
|
||||
this._peers[peer.id] = peer;
|
||||
|
||||
// Assign routerId
|
||||
peer.routerId = await this._getRouterId();
|
||||
|
||||
this._handlePeer(peer);
|
||||
|
||||
let turnServers;
|
||||
|
||||
if ('turnAPIURI' in config)
|
||||
if (returning)
|
||||
{
|
||||
try
|
||||
{
|
||||
const { data } = await axios.get(
|
||||
config.turnAPIURI,
|
||||
{
|
||||
params : {
|
||||
'uri_schema' : 'turn',
|
||||
'transport' : 'tcp',
|
||||
'ip_ver' : 'ipv4',
|
||||
'servercount' : '2',
|
||||
'api_key' : config.turnAPIKey,
|
||||
'ip' : peer.socket.request.connection.remoteAddress
|
||||
}
|
||||
});
|
||||
|
||||
turnServers = [ {
|
||||
urls : data.uris,
|
||||
username : data.username,
|
||||
credential : data.password
|
||||
} ];
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
if ('backupTurnServers' in config)
|
||||
turnServers = config.backupTurnServers;
|
||||
|
||||
logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
|
||||
}
|
||||
this._notification(peer.socket, 'roomBack');
|
||||
}
|
||||
else if ('backupTurnServers' in config)
|
||||
else
|
||||
{
|
||||
turnServers = config.backupTurnServers;
|
||||
}
|
||||
const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true });
|
||||
|
||||
this._notification(peer.socket, 'roomReady', { turnServers });
|
||||
peer.socket.handshake.session.token = token;
|
||||
|
||||
peer.socket.handshake.session.save();
|
||||
|
||||
let turnServers;
|
||||
|
||||
if ('turnAPIURI' in config)
|
||||
{
|
||||
try
|
||||
{
|
||||
const { data } = await axios.get(
|
||||
config.turnAPIURI,
|
||||
{
|
||||
params : {
|
||||
...config.turnAPIparams,
|
||||
'api_key' : config.turnAPIKey,
|
||||
'ip' : peer.socket.request.connection.remoteAddress
|
||||
}
|
||||
});
|
||||
|
||||
turnServers = [ {
|
||||
urls : data.uris,
|
||||
username : data.username,
|
||||
credential : data.password
|
||||
} ];
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
if ('backupTurnServers' in config)
|
||||
turnServers = config.backupTurnServers;
|
||||
|
||||
logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
|
||||
}
|
||||
}
|
||||
else if ('backupTurnServers' in config)
|
||||
{
|
||||
turnServers = config.backupTurnServers;
|
||||
}
|
||||
|
||||
this._notification(peer.socket, 'roomReady', { turnServers });
|
||||
}
|
||||
}
|
||||
|
||||
_handlePeer(peer)
|
||||
@@ -461,6 +589,17 @@ class Room extends EventEmitter
|
||||
peerId : peer.id,
|
||||
role : newRole
|
||||
}, true, true);
|
||||
|
||||
// Got permission to promote peers, notify peer of
|
||||
// peers in lobby
|
||||
if (permissionsFromRoles.PROMOTE_PEER.includes(newRole))
|
||||
{
|
||||
const lobbyPeers = this._lobby.peerList();
|
||||
|
||||
lobbyPeers.length > 0 && this._notification(peer.socket, 'parkedPeers', {
|
||||
lobbyPeers
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('lostRole', ({ oldRole }) =>
|
||||
@@ -479,11 +618,14 @@ class Room extends EventEmitter
|
||||
|
||||
async _handleSocketRequest(peer, request, cb)
|
||||
{
|
||||
const router =
|
||||
this._mediasoupRouters.get(peer.routerId);
|
||||
|
||||
switch (request.method)
|
||||
{
|
||||
case 'getRouterRtpCapabilities':
|
||||
{
|
||||
cb(null, this._mediasoupRouter.rtpCapabilities);
|
||||
cb(null, router.rtpCapabilities);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -517,9 +659,21 @@ class Room extends EventEmitter
|
||||
.filter((joinedPeer) => joinedPeer.id !== peer.id)
|
||||
.map((joinedPeer) => (joinedPeer.peerInfo));
|
||||
|
||||
const lobbyPeers = this._lobby.peerList();
|
||||
|
||||
cb(null, {
|
||||
roles : peer.roles,
|
||||
peers : peerInfos
|
||||
roles : peer.roles,
|
||||
peers : peerInfos,
|
||||
tracker : config.fileTracker,
|
||||
authenticated : peer.authenticated,
|
||||
permissionsFromRoles : permissionsFromRoles,
|
||||
userRoles : userRoles,
|
||||
chatHistory : this._chatHistory,
|
||||
fileHistory : this._fileHistory,
|
||||
lastNHistory : this._lastN,
|
||||
locked : this._locked,
|
||||
lobbyPeers : lobbyPeers,
|
||||
accessCode : this._accessCode
|
||||
});
|
||||
|
||||
// Mark the new Peer as joined.
|
||||
@@ -580,7 +734,7 @@ class Room extends EventEmitter
|
||||
webRtcTransportOptions.enableTcp = true;
|
||||
}
|
||||
|
||||
const transport = await this._mediasoupRouter.createWebRtcTransport(
|
||||
const transport = await router.createWebRtcTransport(
|
||||
webRtcTransportOptions
|
||||
);
|
||||
|
||||
@@ -646,12 +800,27 @@ class Room extends EventEmitter
|
||||
|
||||
case 'produce':
|
||||
{
|
||||
let { appData } = request.data;
|
||||
|
||||
if (
|
||||
appData.source === 'screen' &&
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.SHARE_SCREEN.includes(role))
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
if (
|
||||
appData.source === 'extravideo' &&
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.EXTRA_VIDEO.includes(role))
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
// Ensure the Peer is joined.
|
||||
if (!peer.joined)
|
||||
throw new Error('Peer not yet joined');
|
||||
|
||||
const { transportId, kind, rtpParameters } = request.data;
|
||||
let { appData } = request.data;
|
||||
const transport = peer.getTransport(transportId);
|
||||
|
||||
if (!transport)
|
||||
@@ -664,6 +833,19 @@ class Room extends EventEmitter
|
||||
const producer =
|
||||
await transport.produce({ kind, rtpParameters, appData });
|
||||
|
||||
const pipeRouters = this._getRoutersToPipeTo(peer.routerId);
|
||||
|
||||
for (const [ routerId, destinationRouter ] of this._mediasoupRouters)
|
||||
{
|
||||
if (pipeRouters.includes(routerId))
|
||||
{
|
||||
await router.pipeToRouter({
|
||||
producerId : producer.id,
|
||||
router : destinationRouter
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Store the Producer into the Peer data Object.
|
||||
peer.addProducer(producer.id, producer);
|
||||
|
||||
@@ -910,16 +1092,10 @@ class Room extends EventEmitter
|
||||
throw new Error('Peer not yet joined');
|
||||
|
||||
const { displayName } = request.data;
|
||||
const oldDisplayName = peer.displayName;
|
||||
|
||||
peer.displayName = displayName;
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'changeDisplayName', {
|
||||
peerId : peer.id,
|
||||
displayName : displayName,
|
||||
oldDisplayName : oldDisplayName
|
||||
}, true);
|
||||
// This will be spread through events from the peer object
|
||||
|
||||
// Return no error
|
||||
cb();
|
||||
@@ -927,7 +1103,7 @@ class Room extends EventEmitter
|
||||
break;
|
||||
}
|
||||
|
||||
case 'changePicture':
|
||||
/* case 'changePicture':
|
||||
{
|
||||
// Ensure the Peer is joined.
|
||||
if (!peer.joined)
|
||||
@@ -947,10 +1123,15 @@ class Room extends EventEmitter
|
||||
cb();
|
||||
|
||||
break;
|
||||
}
|
||||
} */
|
||||
|
||||
case 'chatMessage':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role))
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
const { chatMessage } = request.data;
|
||||
|
||||
this._chatHistory.push(chatMessage);
|
||||
@@ -967,28 +1148,35 @@ class Room extends EventEmitter
|
||||
break;
|
||||
}
|
||||
|
||||
case 'serverHistory':
|
||||
case 'moderator:clearChat':
|
||||
{
|
||||
// Return to sender
|
||||
const lobbyPeers = this._lobby.peerList();
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_CHAT.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._chatHistory = [];
|
||||
|
||||
cb(
|
||||
null,
|
||||
{
|
||||
chatHistory : this._chatHistory,
|
||||
fileHistory : this._fileHistory,
|
||||
lastNHistory : this._lastN,
|
||||
locked : this._locked,
|
||||
lobbyPeers : lobbyPeers,
|
||||
accessCode : this._accessCode
|
||||
}
|
||||
);
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:clearChat', null, true);
|
||||
|
||||
// Return no error
|
||||
cb();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'lockRoom':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._locked = true;
|
||||
|
||||
// Spread to others
|
||||
@@ -1004,6 +1192,13 @@ class Room extends EventEmitter
|
||||
|
||||
case 'unlockRoom':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._locked = false;
|
||||
|
||||
// Spread to others
|
||||
@@ -1059,6 +1254,13 @@ class Room extends EventEmitter
|
||||
|
||||
case 'promotePeer':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
const { peerId } = request.data;
|
||||
|
||||
this._lobby.promotePeer(peerId);
|
||||
@@ -1071,6 +1273,13 @@ class Room extends EventEmitter
|
||||
|
||||
case 'promoteAllPeers':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._lobby.promoteAllPeers();
|
||||
|
||||
// Return no error
|
||||
@@ -1081,6 +1290,13 @@ class Room extends EventEmitter
|
||||
|
||||
case 'sendFile':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.SHARE_FILE.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
const { magnetUri } = request.data;
|
||||
|
||||
this._fileHistory.push({ peerId: peer.id, magnetUri: magnetUri });
|
||||
@@ -1097,16 +1313,37 @@ class Room extends EventEmitter
|
||||
break;
|
||||
}
|
||||
|
||||
case 'raiseHand':
|
||||
case 'moderator:clearFileSharing':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_FILES.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._fileHistory = [];
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:clearFileSharing', null, true);
|
||||
|
||||
// Return no error
|
||||
cb();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'raisedHand':
|
||||
{
|
||||
const { raisedHand } = request.data;
|
||||
|
||||
peer.raisedHand = raisedHand;
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'raiseHand', {
|
||||
peerId : peer.id,
|
||||
raisedHand : raisedHand
|
||||
this._notification(peer.socket, 'raisedHand', {
|
||||
peerId : peer.id,
|
||||
raisedHand : raisedHand,
|
||||
raisedHandTimestamp : peer.raisedHandTimestamp
|
||||
}, true);
|
||||
|
||||
// Return no error
|
||||
@@ -1118,15 +1355,14 @@ class Room extends EventEmitter
|
||||
case 'moderator:muteAll':
|
||||
{
|
||||
if (
|
||||
!peer.hasRole(userRoles.MODERATOR) &&
|
||||
!peer.hasRole(userRoles.ADMIN)
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer does not have moderator priveleges');
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:mute', {
|
||||
peerId : peer.id
|
||||
}, true);
|
||||
this._notification(peer.socket, 'moderator:mute', null, true);
|
||||
|
||||
cb();
|
||||
|
||||
@@ -1136,15 +1372,14 @@ class Room extends EventEmitter
|
||||
case 'moderator:stopAllVideo':
|
||||
{
|
||||
if (
|
||||
!peer.hasRole(userRoles.MODERATOR) &&
|
||||
!peer.hasRole(userRoles.ADMIN)
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer does not have moderator priveleges');
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:stopVideo', {
|
||||
peerId : peer.id
|
||||
}, true);
|
||||
this._notification(peer.socket, 'moderator:stopVideo', null, true);
|
||||
|
||||
cb();
|
||||
|
||||
@@ -1154,17 +1389,13 @@ class Room extends EventEmitter
|
||||
case 'moderator:closeMeeting':
|
||||
{
|
||||
if (
|
||||
!peer.hasRole(userRoles.MODERATOR) &&
|
||||
!peer.hasRole(userRoles.ADMIN)
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer does not have moderator priveleges');
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._notification(
|
||||
peer.socket,
|
||||
'moderator:kick',
|
||||
null,
|
||||
true
|
||||
);
|
||||
this._notification(peer.socket, 'moderator:kick', null, true);
|
||||
|
||||
cb();
|
||||
|
||||
@@ -1177,10 +1408,11 @@ class Room extends EventEmitter
|
||||
case 'moderator:kickPeer':
|
||||
{
|
||||
if (
|
||||
!peer.hasRole(userRoles.MODERATOR) &&
|
||||
!peer.hasRole(userRoles.ADMIN)
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer does not have moderator priveleges');
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
const { peerId } = request.data;
|
||||
|
||||
@@ -1189,10 +1421,7 @@ class Room extends EventEmitter
|
||||
if (!kickPeer)
|
||||
throw new Error(`peer with id "${peerId}" not found`);
|
||||
|
||||
this._notification(
|
||||
kickPeer.socket,
|
||||
'moderator:kick'
|
||||
);
|
||||
this._notification(kickPeer.socket, 'moderator:kick');
|
||||
|
||||
kickPeer.close();
|
||||
|
||||
@@ -1224,6 +1453,8 @@ class Room extends EventEmitter
|
||||
producer.id
|
||||
);
|
||||
|
||||
const router = this._mediasoupRouters.get(producerPeer.routerId);
|
||||
|
||||
// Optimization:
|
||||
// - Create the server-side Consumer. If video, do it paused.
|
||||
// - Tell its Peer about it and wait for its response.
|
||||
@@ -1234,7 +1465,7 @@ class Room extends EventEmitter
|
||||
// NOTE: Don't create the Consumer if the remote Peer cannot consume it.
|
||||
if (
|
||||
!consumerPeer.rtpCapabilities ||
|
||||
!this._mediasoupRouter.canConsume(
|
||||
!router.canConsume(
|
||||
{
|
||||
producerId : producer.id,
|
||||
rtpCapabilities : consumerPeer.rtpCapabilities
|
||||
@@ -1369,6 +1600,19 @@ class Room extends EventEmitter
|
||||
.filter((peer) => peer.joined && peer !== excludePeer);
|
||||
}
|
||||
|
||||
_getPeersWithPermission({ permission = null, excludePeer = undefined, joined = true })
|
||||
{
|
||||
return Object.values(this._peers)
|
||||
.filter(
|
||||
(peer) =>
|
||||
peer.joined === joined &&
|
||||
peer !== excludePeer &&
|
||||
peer.roles.some(
|
||||
(role) => permission.includes(role)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_timeoutCallback(callback)
|
||||
{
|
||||
let called = false;
|
||||
@@ -1433,6 +1677,84 @@ class Room extends EventEmitter
|
||||
socket.emit('notification', { method, data });
|
||||
}
|
||||
}
|
||||
|
||||
async _pipeProducersToNewRouter()
|
||||
{
|
||||
const peersToPipe =
|
||||
Object.values(this._peers)
|
||||
.filter((peer) => peer.routerId !== this._currentRouter.id);
|
||||
|
||||
for (const peer of peersToPipe)
|
||||
{
|
||||
const srcRouter = this._mediasoupRouters.get(peer.routerId);
|
||||
|
||||
for (const producerId of peer.producers.keys())
|
||||
{
|
||||
await srcRouter.pipeToRouter({
|
||||
producerId,
|
||||
router : this._currentRouter
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _getRouterId()
|
||||
{
|
||||
if (this._currentRouter)
|
||||
{
|
||||
const routerLoad =
|
||||
Object.values(this._peers)
|
||||
.filter((peer) => peer.routerId === this._currentRouter.id).length;
|
||||
|
||||
if (routerLoad >= ROUTER_SCALE_SIZE)
|
||||
{
|
||||
this._currentRouter = this._routerIterator.next().value;
|
||||
|
||||
if (this._currentRouter)
|
||||
{
|
||||
await this._pipeProducersToNewRouter();
|
||||
|
||||
return this._currentRouter.id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return this._currentRouter.id;
|
||||
}
|
||||
}
|
||||
|
||||
return this._getLeastLoadedRouter();
|
||||
}
|
||||
|
||||
// Returns an array of router ids we need to pipe to
|
||||
_getRoutersToPipeTo(originRouterId)
|
||||
{
|
||||
return Object.values(this._peers)
|
||||
.map((peer) => peer.routerId)
|
||||
.filter((routerId, index, self) =>
|
||||
routerId !== originRouterId && self.indexOf(routerId) === index
|
||||
);
|
||||
}
|
||||
|
||||
_getLeastLoadedRouter()
|
||||
{
|
||||
let load = Infinity;
|
||||
let id;
|
||||
|
||||
for (const routerId of this._mediasoupRouters.keys())
|
||||
{
|
||||
const routerLoad =
|
||||
Object.values(this._peers).filter((peer) => peer.routerId === routerId).length;
|
||||
|
||||
if (routerLoad < load)
|
||||
{
|
||||
id = routerId;
|
||||
load = routerLoad;
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Room;
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
const { Resolver } = require('dns').promises;
|
||||
const express = require('express');
|
||||
const mediasoup = require('mediasoup');
|
||||
const prom = require('prom-client');
|
||||
|
||||
const Logger = require('./Logger');
|
||||
|
||||
const logger = new Logger('prom');
|
||||
const resolver = new Resolver();
|
||||
const workers = new Map();
|
||||
|
||||
const labelNames = [
|
||||
'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id',
|
||||
'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type'
|
||||
];
|
||||
|
||||
const metadata = {
|
||||
'byteCount' : { metricType: prom.Counter, unit: 'bytes' },
|
||||
'score' : { metricType: prom.Gauge }
|
||||
};
|
||||
|
||||
module.exports = async function(rooms, peers, config)
|
||||
{
|
||||
const collect = async function(registry)
|
||||
{
|
||||
const newMetrics = function(subsystem)
|
||||
{
|
||||
const namespace = 'mediasoup';
|
||||
const metrics = new Map();
|
||||
|
||||
for (const key in metadata)
|
||||
{
|
||||
if (Object.prototype.hasOwnProperty.call(metadata, key))
|
||||
{
|
||||
const value = metadata[key];
|
||||
const name = key.split(/(?=[A-Z])/).join('_')
|
||||
.toLowerCase();
|
||||
const unit = value.unit;
|
||||
const metricType = value.metricType;
|
||||
let s = `${namespace}_${subsystem}_${name}`;
|
||||
|
||||
if (unit)
|
||||
{
|
||||
s += `_${unit}`;
|
||||
}
|
||||
const m = new metricType({
|
||||
name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] });
|
||||
|
||||
metrics.set(key, m);
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
};
|
||||
|
||||
const commonLabels = function(both, fn)
|
||||
{
|
||||
for (const roomId of rooms.keys())
|
||||
{
|
||||
for (const [ peerId, peer ] of peers)
|
||||
{
|
||||
if (fn(peer))
|
||||
{
|
||||
const displayName = peer._displayName;
|
||||
const userAgent = peer._socket.client.request.headers['user-agent'];
|
||||
const kind = both.kind;
|
||||
const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1];
|
||||
|
||||
return { roomId, peerId, displayName, userAgent, kind, codec };
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('cannot find common labels');
|
||||
};
|
||||
|
||||
const addr = async function(ip, port)
|
||||
{
|
||||
if (config.deidentify)
|
||||
{
|
||||
const a = ip.split('.');
|
||||
|
||||
for (let i = 0; i < a.length - 2; i++)
|
||||
{
|
||||
a[i] = 'xx';
|
||||
}
|
||||
|
||||
return `${a.join('.')}:${port}`;
|
||||
}
|
||||
else if (config.numeric)
|
||||
{
|
||||
return `${ip}:${port}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
const a = await resolver.reverse(ip);
|
||||
|
||||
ip = a[0];
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
logger.error(`reverse DNS query failed: ${ip} ${err.code}`);
|
||||
}
|
||||
|
||||
return `${ip}:${port}`;
|
||||
}
|
||||
};
|
||||
|
||||
const quiet = function(s)
|
||||
{
|
||||
return config.quiet ? '' : s;
|
||||
};
|
||||
|
||||
const setValue = function(key, m, labels, v)
|
||||
{
|
||||
logger.debug(`setValue key=${key} v=${v}`);
|
||||
switch (metadata[key].metricType)
|
||||
{
|
||||
case prom.Counter:
|
||||
m.inc(labels, v);
|
||||
break;
|
||||
case prom.Gauge:
|
||||
m.set(labels, v);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unexpected metric: ${m}`);
|
||||
}
|
||||
};
|
||||
|
||||
logger.debug('collect');
|
||||
const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] });
|
||||
|
||||
mRooms.set(rooms.size);
|
||||
const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] });
|
||||
|
||||
for (const [ roomId, room ] of rooms)
|
||||
{
|
||||
mPeers.labels(roomId).set(Object.keys(room._peers).length);
|
||||
}
|
||||
|
||||
const mConsumer = newMetrics('consumer');
|
||||
const mProducer = newMetrics('producer');
|
||||
|
||||
for (const [ pid, worker ] of workers)
|
||||
{
|
||||
logger.debug(`visiting worker ${pid}`);
|
||||
for (const router of worker._routers)
|
||||
{
|
||||
logger.debug(`visiting router ${router.id}`);
|
||||
for (const [ transportId, transport ] of router._transports)
|
||||
{
|
||||
logger.debug(`visiting transport ${transportId}`);
|
||||
const transportJson = await transport.dump();
|
||||
|
||||
if (transportJson.iceState != 'completed')
|
||||
{
|
||||
logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`);
|
||||
continue;
|
||||
}
|
||||
const iceSelectedTuple = transportJson.iceSelectedTuple;
|
||||
const proto = iceSelectedTuple.protocol;
|
||||
const localAddr = await addr(iceSelectedTuple.localIp,
|
||||
iceSelectedTuple.localPort);
|
||||
const remoteAddr = await addr(iceSelectedTuple.remoteIp,
|
||||
iceSelectedTuple.remotePort);
|
||||
|
||||
for (const [ producerId, producer ] of transport._producers)
|
||||
{
|
||||
logger.debug(`visiting producer ${producerId}`);
|
||||
const { roomId, peerId, displayName, userAgent, kind, codec } =
|
||||
commonLabels(producer, (peer) => peer._producers.has(producerId));
|
||||
const a = await producer.getStats();
|
||||
|
||||
for (const x of a)
|
||||
{
|
||||
const type = x.type;
|
||||
const labels = {
|
||||
'pid' : pid,
|
||||
'room_id' : roomId,
|
||||
'peer_id' : peerId,
|
||||
'display_name' : displayName,
|
||||
'user_agent' : userAgent,
|
||||
'transport_id' : quiet(transportId),
|
||||
'proto' : proto,
|
||||
'local_addr' : localAddr,
|
||||
'remote_addr' : remoteAddr,
|
||||
'id' : quiet(producerId),
|
||||
'kind' : kind,
|
||||
'codec' : codec,
|
||||
'type' : type
|
||||
};
|
||||
|
||||
for (const [ key, m ] of mProducer)
|
||||
{
|
||||
setValue(key, m, labels, x[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [ consumerId, consumer ] of transport._consumers)
|
||||
{
|
||||
logger.debug(`visiting consumer ${consumerId}`);
|
||||
const { roomId, peerId, displayName, userAgent, kind, codec } =
|
||||
commonLabels(consumer, (peer) => peer._consumers.has(consumerId));
|
||||
const a = await consumer.getStats();
|
||||
|
||||
for (const x of a)
|
||||
{
|
||||
if (x.type == 'inbound-rtp')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const type = x.type;
|
||||
const labels =
|
||||
{
|
||||
'pid' : pid,
|
||||
'room_id' : roomId,
|
||||
'peer_id' : peerId,
|
||||
'display_name' : displayName,
|
||||
'user_agent' : userAgent,
|
||||
'transport_id' : quiet(transportId),
|
||||
'proto' : proto,
|
||||
'local_addr' : localAddr,
|
||||
'remote_addr' : remoteAddr,
|
||||
'id' : quiet(consumerId),
|
||||
'kind' : kind,
|
||||
'codec' : codec,
|
||||
'type' : type
|
||||
};
|
||||
|
||||
for (const [ key, m ] of mConsumer)
|
||||
{
|
||||
setValue(key, m, labels, x[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
logger.debug(`config.deidentify=${config.deidentify}`);
|
||||
logger.debug(`config.numeric=${config.numeric}`);
|
||||
logger.debug(`config.port=${config.port}`);
|
||||
logger.debug(`config.quiet=${config.quiet}`);
|
||||
|
||||
mediasoup.observer.on('newworker', (worker) =>
|
||||
{
|
||||
logger.debug(`observing newworker ${worker.pid} #${workers.size}`);
|
||||
workers.set(worker.pid, worker);
|
||||
worker.observer.on('close', () =>
|
||||
{
|
||||
logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`);
|
||||
workers.delete(worker.pid);
|
||||
});
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get('/', async (req, res) =>
|
||||
{
|
||||
logger.debug(`GET ${req.originalUrl}`);
|
||||
const registry = new prom.Registry();
|
||||
|
||||
await collect(registry);
|
||||
res.set('Content-Type', registry.contentType);
|
||||
const data = registry.metrics();
|
||||
|
||||
res.end(data);
|
||||
});
|
||||
const server = app.listen(config.port || 8889, () =>
|
||||
{
|
||||
const address = server.address();
|
||||
|
||||
logger.info(`listening ${address.address}:${address.port}`);
|
||||
});
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
+11
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multiparty-meeting-server",
|
||||
"version": "3.1.0",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting server",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
@@ -8,7 +8,8 @@
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js",
|
||||
"connect": "node connect.js"
|
||||
"connect": "node connect.js",
|
||||
"lint": "eslint -c .eslintrc.json --ext .js *.js lib/"
|
||||
},
|
||||
"dependencies": {
|
||||
"awaitqueue": "^1.0.0",
|
||||
@@ -25,13 +26,19 @@
|
||||
"express-socket.io-session": "^1.3.5",
|
||||
"helmet": "^3.21.2",
|
||||
"ims-lti": "^3.0.2",
|
||||
"mediasoup": "^3.5.5",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mediasoup": "^3.5.6",
|
||||
"openid-client": "^3.7.3",
|
||||
"passport": "^0.4.0",
|
||||
"passport-lti": "0.0.7",
|
||||
"pidusage": "^2.0.17",
|
||||
"prom-client": ">=12.0.0",
|
||||
"redis": "^2.8.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"spdy": "^4.0.1"
|
||||
"spdy": "^4.0.1",
|
||||
"uuid": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "6.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
+129
-39
@@ -34,25 +34,28 @@ const expressSession = require('express-session');
|
||||
const RedisStore = require('connect-redis')(expressSession);
|
||||
const sharedSession = require('express-socket.io-session');
|
||||
const interactiveServer = require('./lib/interactiveServer');
|
||||
const promExporter = require('./lib/promExporter');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
||||
console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
|
||||
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||
console.log('- config.mediasoup.worker.logLevel:', config.mediasoup.worker.logLevel);
|
||||
console.log('- config.mediasoup.worker.logTags:', config.mediasoup.worker.logTags);
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const queue = new AwaitQueue();
|
||||
|
||||
let statusLogger = null;
|
||||
|
||||
if ('StatusLogger' in config)
|
||||
statusLogger = new config.StatusLogger();
|
||||
|
||||
// mediasoup Workers.
|
||||
// @type {Array<mediasoup.Worker>}
|
||||
const mediasoupWorkers = [];
|
||||
|
||||
// Index of next mediasoup Worker to use.
|
||||
// @type {Number}
|
||||
let nextMediasoupWorkerIdx = 0;
|
||||
|
||||
// Map of Room instances indexed by roomId.
|
||||
const rooms = new Map();
|
||||
|
||||
@@ -100,6 +103,11 @@ const session = expressSession({
|
||||
}
|
||||
});
|
||||
|
||||
if (config.trustProxy)
|
||||
{
|
||||
app.set('trust proxy', config.trustProxy);
|
||||
}
|
||||
|
||||
app.use(session);
|
||||
|
||||
passport.serializeUser((user, done) =>
|
||||
@@ -122,6 +130,12 @@ async function run()
|
||||
// Open the interactive server.
|
||||
await interactiveServer(rooms, peers);
|
||||
|
||||
// start Prometheus exporter
|
||||
if (config.prometheus)
|
||||
{
|
||||
await promExporter(rooms, peers, config.prometheus);
|
||||
}
|
||||
|
||||
if (typeof(config.auth) === 'undefined')
|
||||
{
|
||||
logger.warn('Auth is not configured properly!');
|
||||
@@ -140,6 +154,25 @@ async function run()
|
||||
// Run WebSocketServer.
|
||||
await runWebSocketServer();
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function errorHandler(err, req, res, next)
|
||||
{
|
||||
const trackingId = uuidv4();
|
||||
|
||||
res.status(500).send(
|
||||
`<h1>Internal Server Error</h1>
|
||||
<p>If you report this error, please also report this
|
||||
<i>tracking ID</i> which makes it possible to locate your session
|
||||
in the logs which are available to the system administrator:
|
||||
<b>${trackingId}</b></p>`
|
||||
);
|
||||
logger.error(
|
||||
'Express error handler dump with tracking ID: %s, error dump: %o',
|
||||
trackingId, err);
|
||||
}
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
// Log rooms status every 30 seconds.
|
||||
setInterval(() =>
|
||||
{
|
||||
@@ -159,6 +192,17 @@ async function run()
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function statusLog()
|
||||
{
|
||||
if (statusLogger)
|
||||
{
|
||||
statusLogger.log({
|
||||
rooms : rooms,
|
||||
peers : peers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupLTI(ltiConfig)
|
||||
{
|
||||
|
||||
@@ -168,7 +212,7 @@ function setupLTI(ltiConfig)
|
||||
|
||||
const ltiStrategy = new LTIStrategy(
|
||||
ltiConfig,
|
||||
function(req, lti, done)
|
||||
(req, lti, done) =>
|
||||
{
|
||||
// LTI launch parameters
|
||||
if (lti)
|
||||
@@ -178,7 +222,7 @@ function setupLTI(ltiConfig)
|
||||
if (lti.user_id && lti.custom_room)
|
||||
{
|
||||
user.id = lti.user_id;
|
||||
user._lti = lti;
|
||||
user._userinfo = { 'lti': lti };
|
||||
}
|
||||
|
||||
if (lti.custom_room)
|
||||
@@ -219,7 +263,18 @@ function setupOIDC(oidcIssuer)
|
||||
// redirect_uri defaults to client.redirect_uris[0]
|
||||
// response type defaults to client.response_types[0], then 'code'
|
||||
// scope defaults to 'openid'
|
||||
const params = config.auth.oidc.clientOptions;
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const params = (({
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope
|
||||
}) => ({
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope
|
||||
}))(config.auth.oidc.clientOptions);
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
// optional, defaults to false, when true req is passed as a first
|
||||
// argument to verify fn
|
||||
@@ -234,12 +289,17 @@ function setupOIDC(oidcIssuer)
|
||||
{ client: oidcClient, params, passReqToCallback, usePKCE },
|
||||
(tokenset, userinfo, done) =>
|
||||
{
|
||||
if (userinfo && tokenset)
|
||||
{
|
||||
// eslint-disable-next-line camelcase
|
||||
userinfo._tokenset_claims = tokenset.claims();
|
||||
}
|
||||
|
||||
const user =
|
||||
{
|
||||
id : tokenset.claims.sub,
|
||||
provider : tokenset.claims.iss,
|
||||
_userinfo : userinfo,
|
||||
_claims : tokenset.claims
|
||||
_userinfo : userinfo
|
||||
};
|
||||
|
||||
return done(null, user);
|
||||
@@ -289,7 +349,7 @@ async function setupAuth()
|
||||
// lti launch
|
||||
app.post('/auth/lti',
|
||||
passport.authenticate('lti', { failureRedirect: '/' }),
|
||||
function(req, res)
|
||||
(req, res) =>
|
||||
{
|
||||
res.redirect(`/${req.user.room}`);
|
||||
}
|
||||
@@ -306,13 +366,13 @@ async function setupAuth()
|
||||
{
|
||||
for (const role of peer.roles)
|
||||
{
|
||||
if (role !== userRoles.ALL)
|
||||
if (role !== userRoles.NORMAL)
|
||||
peer.removeRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
req.logout();
|
||||
res.send(logoutHelper());
|
||||
req.session.destroy(() => res.send(logoutHelper()));
|
||||
});
|
||||
|
||||
// callback
|
||||
@@ -333,10 +393,10 @@ async function setupAuth()
|
||||
if (!peer) // User has no socket session yet, make temporary
|
||||
peer = new Peer({ id: peerId, roomId });
|
||||
|
||||
if (peer && peer.roomId !== roomId) // The peer is mischievous
|
||||
if (peer.roomId !== roomId) // The peer is mischievous
|
||||
throw new Error('peer authenticated with wrong room');
|
||||
|
||||
if (peer && typeof config.userMapping === 'function')
|
||||
if (typeof config.userMapping === 'function')
|
||||
{
|
||||
await config.userMapping({
|
||||
peer,
|
||||
@@ -345,6 +405,8 @@ async function setupAuth()
|
||||
});
|
||||
}
|
||||
|
||||
peer.authenticated = true;
|
||||
|
||||
res.send(loginHelper({
|
||||
displayName : peer.displayName,
|
||||
picture : peer.picture
|
||||
@@ -361,7 +423,7 @@ async function runHttpsServer()
|
||||
|
||||
app.all('*', async (req, res, next) =>
|
||||
{
|
||||
if (req.secure || config.httpOnly )
|
||||
if (req.secure || config.httpOnly)
|
||||
{
|
||||
const ltiURL = new URL(`${req.protocol }://${ req.get('host') }${req.originalUrl}`);
|
||||
|
||||
@@ -404,11 +466,17 @@ async function runHttpsServer()
|
||||
// http
|
||||
const redirectListener = http.createServer(app);
|
||||
|
||||
redirectListener.listen(config.listeningRedirectPort);
|
||||
if (config.listeningHost)
|
||||
redirectListener.listen(config.listeningRedirectPort, config.listeningHost);
|
||||
else
|
||||
redirectListener.listen(config.listeningRedirectPort);
|
||||
}
|
||||
|
||||
// https or http
|
||||
mainListener.listen(config.listeningPort);
|
||||
if (config.listeningHost)
|
||||
mainListener.listen(config.listeningPort, config.listeningHost);
|
||||
else
|
||||
mainListener.listen(config.listeningPort);
|
||||
}
|
||||
|
||||
function isPathAlreadyTaken(url)
|
||||
@@ -464,12 +532,37 @@ async function runWebSocketServer()
|
||||
|
||||
queue.push(async () =>
|
||||
{
|
||||
const { token } = socket.handshake.session;
|
||||
|
||||
const room = await getOrCreateRoom({ roomId });
|
||||
const peer = new Peer({ id: peerId, roomId, socket });
|
||||
|
||||
let peer = peers.get(peerId);
|
||||
let returning = false;
|
||||
|
||||
if (peer && !token)
|
||||
{ // Don't allow hijacking sessions
|
||||
socket.disconnect(true);
|
||||
|
||||
return;
|
||||
}
|
||||
else if (token && room.verifyPeer({ id: peerId, token }))
|
||||
{ // Returning user, remove if old peer exists
|
||||
if (peer)
|
||||
peer.close();
|
||||
|
||||
returning = true;
|
||||
}
|
||||
|
||||
peer = new Peer({ id: peerId, roomId, socket });
|
||||
|
||||
peers.set(peerId, peer);
|
||||
|
||||
peer.on('close', () => peers.delete(peerId));
|
||||
peer.on('close', () =>
|
||||
{
|
||||
peers.delete(peerId);
|
||||
|
||||
statusLog();
|
||||
});
|
||||
|
||||
if (
|
||||
Boolean(socket.handshake.session.passport) &&
|
||||
@@ -484,10 +577,11 @@ async function runWebSocketServer()
|
||||
_userinfo
|
||||
} = socket.handshake.session.passport.user;
|
||||
|
||||
peer.authId= id;
|
||||
peer.authId = id;
|
||||
peer.displayName = displayName;
|
||||
peer.picture = picture;
|
||||
peer.email = email;
|
||||
peer.authenticated = true;
|
||||
|
||||
if (typeof config.userMapping === 'function')
|
||||
{
|
||||
@@ -495,7 +589,9 @@ async function runWebSocketServer()
|
||||
}
|
||||
}
|
||||
|
||||
room.handlePeer(peer);
|
||||
room.handlePeer({ peer, returning });
|
||||
|
||||
statusLog();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
@@ -539,19 +635,6 @@ async function runMediasoupWorkers()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next mediasoup Worker.
|
||||
*/
|
||||
function getMediasoupWorker()
|
||||
{
|
||||
const worker = mediasoupWorkers[nextMediasoupWorkerIdx];
|
||||
|
||||
if (++nextMediasoupWorkerIdx === mediasoupWorkers.length)
|
||||
nextMediasoupWorkerIdx = 0;
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Room instance (or create one if it does not exist).
|
||||
*/
|
||||
@@ -564,13 +647,20 @@ async function getOrCreateRoom({ roomId })
|
||||
{
|
||||
logger.info('creating a new Room [roomId:"%s"]', roomId);
|
||||
|
||||
const mediasoupWorker = getMediasoupWorker();
|
||||
// const mediasoupWorker = getMediasoupWorker();
|
||||
|
||||
room = await Room.create({ mediasoupWorker, roomId });
|
||||
room = await Room.create({ mediasoupWorkers, roomId });
|
||||
|
||||
rooms.set(roomId, room);
|
||||
|
||||
room.on('close', () => rooms.delete(roomId));
|
||||
statusLog();
|
||||
|
||||
room.on('close', () =>
|
||||
{
|
||||
rooms.delete(roomId);
|
||||
|
||||
statusLog();
|
||||
});
|
||||
}
|
||||
|
||||
return room;
|
||||
|
||||
+6
-7
@@ -1,12 +1,11 @@
|
||||
module.exports = {
|
||||
// Allowed to enter locked rooms + all other priveleges
|
||||
// These can be changed
|
||||
ADMIN : 'admin',
|
||||
// Allowed to enter restricted rooms if configured.
|
||||
// Allowed to moderate users in a room (mute all,
|
||||
// spotlight video, kick users)
|
||||
MODERATOR : 'moderator',
|
||||
// Same as MODERATOR, but can't moderate users
|
||||
PRESENTER : 'presenter',
|
||||
AUTHENTICATED : 'authenticated',
|
||||
// No priveleges
|
||||
ALL : 'normal'
|
||||
// Don't change anything after this point
|
||||
|
||||
// All users have this role by default, do not change or remove this role
|
||||
NORMAL : 'normal'
|
||||
};
|
||||
Reference in New Issue
Block a user