merge from develop

This commit is contained in:
Stefan Otto
2020-05-06 02:33:37 +02:00
parent da6c9d3ecf
commit aca3499afb
99 changed files with 6709 additions and 2038 deletions
+111 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+284
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'
};