Mostly working mediasoup v3
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
const os = require('os');
|
||||
|
||||
module.exports =
|
||||
{
|
||||
// oAuth2 conf
|
||||
@@ -9,21 +11,21 @@ module.exports =
|
||||
could be discovered on:
|
||||
issuerURL + '/.well-known/openid-configuration'
|
||||
*/
|
||||
issuerURL : 'https://example.com'
|
||||
clientOptions :
|
||||
issuerURL : 'https://example.com',
|
||||
clientOptions :
|
||||
{
|
||||
client_id : '',
|
||||
client_secret : '',
|
||||
scope : 'openid email profile'
|
||||
client_id : '',
|
||||
client_secret : '',
|
||||
scope : 'openid email profile',
|
||||
// where client.example.com is your multiparty meeting server
|
||||
redirect_uri : 'https://client.example.com/auth/callback'
|
||||
redirect_uri : 'https://client.example.com/auth/callback'
|
||||
}
|
||||
},
|
||||
// session cookie secret
|
||||
cookieSecret : 'T0P-S3cR3t_cook!e',
|
||||
cookieSecret : 'T0P-S3cR3t_cook!e',
|
||||
// Listening hostname for `gulp live|open`.
|
||||
domain : 'localhost',
|
||||
tls :
|
||||
domain : 'localhost',
|
||||
tls :
|
||||
{
|
||||
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
|
||||
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
|
||||
@@ -33,59 +35,61 @@ module.exports =
|
||||
// Any http request is redirected to https.
|
||||
// Listening port for http server.
|
||||
listeningRedirectPort : 80,
|
||||
// STUN/TURN
|
||||
// Mediasoup settings
|
||||
mediasoup :
|
||||
{
|
||||
// mediasoup Server settings.
|
||||
logLevel : 'warn',
|
||||
logTags :
|
||||
[
|
||||
'info',
|
||||
'ice',
|
||||
'dtls',
|
||||
'rtp',
|
||||
'srtp',
|
||||
'rtcp',
|
||||
'rbe',
|
||||
'rtx'
|
||||
],
|
||||
rtcIPv4 : true,
|
||||
rtcIPv6 : true,
|
||||
rtcAnnouncedIPv4 : null,
|
||||
rtcAnnouncedIPv6 : null,
|
||||
rtcMinPort : 40000,
|
||||
rtcMaxPort : 49999,
|
||||
// mediasoup Room codecs.
|
||||
mediaCodecs :
|
||||
[
|
||||
{
|
||||
kind : 'audio',
|
||||
name : 'opus',
|
||||
clockRate : 48000,
|
||||
channels : 2,
|
||||
parameters :
|
||||
numWorkers : Object.keys(os.cpus()).length,
|
||||
// mediasoup Worker settings.
|
||||
worker :
|
||||
{
|
||||
logLevel : 'warn',
|
||||
logTags :
|
||||
[
|
||||
'info',
|
||||
'ice',
|
||||
'dtls',
|
||||
'rtp',
|
||||
'srtp',
|
||||
'rtcp'
|
||||
],
|
||||
rtcMinPort : 40000,
|
||||
rtcMaxPort : 49999
|
||||
},
|
||||
// mediasoup Router settings.
|
||||
router :
|
||||
{
|
||||
// Router media codecs.
|
||||
mediaCodecs :
|
||||
[
|
||||
{
|
||||
useinbandfec : 1
|
||||
}
|
||||
},
|
||||
// {
|
||||
// kind : 'video',
|
||||
// name : 'VP8',
|
||||
// clockRate : 90000
|
||||
// }
|
||||
{
|
||||
kind : 'video',
|
||||
name : 'H264',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
kind : 'audio',
|
||||
mimeType : 'audio/opus',
|
||||
clockRate : 48000,
|
||||
channels : 2
|
||||
},
|
||||
{
|
||||
'packetization-mode' : 1,
|
||||
'profile-level-id' : '42e01f',
|
||||
'level-asymmetry-allowed' : 1
|
||||
kind : 'video',
|
||||
mimeType : 'video/h264',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
{
|
||||
'packetization-mode' : 1,
|
||||
'profile-level-id' : '42e01f',
|
||||
'level-asymmetry-allowed' : 1,
|
||||
'x-google-start-bitrate' : 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
// mediasoup per Peer max sending bitrate (in bps).
|
||||
maxBitrate : 500000
|
||||
]
|
||||
},
|
||||
// mediasoup WebRtcTransport settings.
|
||||
webRtcTransport :
|
||||
{
|
||||
listenIps :
|
||||
[
|
||||
{ ip: '1.2.3.4', announcedIp: null }
|
||||
],
|
||||
maxIncomingBitrate : 1500000,
|
||||
initialAvailableOutgoingBitrate : 1000000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+865
-526
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -54,7 +54,7 @@ function handleRoom(room, stream)
|
||||
Object.assign({}, baseEvent,
|
||||
{
|
||||
event : 'room.newpeer',
|
||||
peerName : peer.name,
|
||||
peerId : peer.id,
|
||||
rtpCapabilities : peer.rtpCapabilities
|
||||
}),
|
||||
stream);
|
||||
@@ -67,7 +67,7 @@ function handlePeer(peer, baseEvent, stream)
|
||||
{
|
||||
baseEvent = Object.assign({}, baseEvent,
|
||||
{
|
||||
peerName : peer.name
|
||||
peerId : peer.id
|
||||
});
|
||||
|
||||
peer.on('close', (originator) =>
|
||||
|
||||
+3
-2
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "multiparty-meeting-server",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting server",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"awaitqueue": "^1.0.0",
|
||||
"base-64": "^0.1.0",
|
||||
"colors": "^1.1.2",
|
||||
"compression": "^1.7.3",
|
||||
"debug": "^4.1.0",
|
||||
"express": "^4.16.3",
|
||||
"express-session": "^1.16.1",
|
||||
"mediasoup": "^2.6.11",
|
||||
"mediasoup": "^3.0.12",
|
||||
"openid-client": "^2.5.0",
|
||||
"passport": "^0.4.0",
|
||||
"socket.io": "^2.1.1",
|
||||
|
||||
+227
-126
@@ -10,6 +10,8 @@ const http = require('http');
|
||||
const spdy = require('spdy');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const mediasoup = require('mediasoup');
|
||||
const AwaitQueue = require('awaitqueue');
|
||||
const Logger = require('./lib/Logger');
|
||||
const Room = require('./lib/Room');
|
||||
const utils = require('./util');
|
||||
@@ -17,7 +19,7 @@ const base64 = require('base-64');
|
||||
// auth
|
||||
const passport = require('passport');
|
||||
const { Issuer, Strategy } = require('openid-client');
|
||||
const session = require('express-session')
|
||||
const session = require('express-session');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
||||
@@ -25,11 +27,18 @@ console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
|
||||
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||
/* eslint-enable no-console */
|
||||
|
||||
// Start the mediasoup server.
|
||||
const mediaServer = require('./mediasoup');
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const queue = new AwaitQueue();
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -40,35 +49,84 @@ const tls =
|
||||
key : fs.readFileSync(config.tls.key)
|
||||
};
|
||||
|
||||
let app = express();
|
||||
const app = express();
|
||||
let httpsServer;
|
||||
let oidcClient;
|
||||
let oidcStrategy;
|
||||
|
||||
passport.serializeUser(function(user, done)
|
||||
passport.serializeUser((user, done) =>
|
||||
{
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function(user, done)
|
||||
passport.deserializeUser((user, done) =>
|
||||
{
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
const auth=config.auth;
|
||||
const auth = config.auth;
|
||||
|
||||
function setupAuth(oidcIssuer)
|
||||
async function run()
|
||||
{
|
||||
if (
|
||||
typeof(auth) !== 'undefined' &&
|
||||
typeof(auth.issuerURL) !== 'undefined' &&
|
||||
typeof(auth.clientOptions) !== 'undefined'
|
||||
)
|
||||
{
|
||||
Issuer.discover(auth.issuerURL).then( async (oidcIssuer) =>
|
||||
{
|
||||
// Setup authentication
|
||||
await setupAuth(oidcIssuer);
|
||||
|
||||
// Run a mediasoup Worker.
|
||||
await runMediasoupWorkers();
|
||||
|
||||
// Run HTTPS server.
|
||||
await runHttpsServer();
|
||||
|
||||
// Run WebSocketServer.
|
||||
await runWebSocketServer();
|
||||
})
|
||||
.catch((err) =>
|
||||
{
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error('Auth is not configure properly!');
|
||||
|
||||
// Run a mediasoup Worker.
|
||||
await runMediasoupWorkers();
|
||||
|
||||
// Run HTTPS server.
|
||||
await runHttpsServer();
|
||||
|
||||
// Run WebSocketServer.
|
||||
await runWebSocketServer();
|
||||
}
|
||||
|
||||
// Log rooms status every 30 seconds.
|
||||
setInterval(() =>
|
||||
{
|
||||
for (const room of rooms.values())
|
||||
{
|
||||
room.logStatus();
|
||||
}
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
async function setupAuth(oidcIssuer)
|
||||
{
|
||||
oidcClient = new oidcIssuer.Client(auth.clientOptions);
|
||||
const params =
|
||||
{
|
||||
...auth.clientOptions
|
||||
// ... any authorization request parameters go here
|
||||
// client_id defaults to client.client_id
|
||||
// redirect_uri defaults to client.redirect_uris[0]
|
||||
// response type defaults to client.response_types[0], then 'code'
|
||||
// scope defaults to 'openid'
|
||||
};
|
||||
|
||||
// ... any authorization request parameters go here
|
||||
// client_id defaults to client.client_id
|
||||
// redirect_uri defaults to client.redirect_uris[0]
|
||||
// response type defaults to client.response_types[0], then 'code'
|
||||
// scope defaults to 'openid'
|
||||
const params = auth.clientOptions;
|
||||
|
||||
// optional, defaults to false, when true req is passed as a first
|
||||
// argument to verify fn
|
||||
@@ -78,63 +136,73 @@ function setupAuth(oidcIssuer)
|
||||
// resolved from the issuer configuration, instead of true you may provide
|
||||
// any of the supported values directly, i.e. "S256" (recommended) or "plain"
|
||||
const usePKCE = false;
|
||||
const client=oidcClient;
|
||||
const client = oidcClient;
|
||||
|
||||
oidcStrategy = new Strategy(
|
||||
{ client, params, passReqToCallback, usePKCE },
|
||||
(tokenset, userinfo, done) =>
|
||||
{
|
||||
let user = {
|
||||
id : tokenset.claims.sub,
|
||||
provider : tokenset.claims.iss,
|
||||
_userinfo : userinfo,
|
||||
_claims : tokenset.claims,
|
||||
{ client, params, passReqToCallback, usePKCE },
|
||||
(tokenset, userinfo, done) =>
|
||||
{
|
||||
const user =
|
||||
{
|
||||
id : tokenset.claims.sub,
|
||||
provider : tokenset.claims.iss,
|
||||
_userinfo : userinfo,
|
||||
_claims : tokenset.claims
|
||||
};
|
||||
|
||||
|
||||
if ( typeof(userinfo.picture) !== 'undefined' ){
|
||||
if ( ! userinfo.picture.match(/^http/g) ) {
|
||||
if (typeof(userinfo.picture) !== 'undefined')
|
||||
{
|
||||
if (!userinfo.picture.match(/^http/g))
|
||||
{
|
||||
user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ];
|
||||
} else {
|
||||
user.Photos= [ { value: userinfo.picture } ];
|
||||
}
|
||||
else
|
||||
{
|
||||
user.Photos = [ { value: userinfo.picture } ];
|
||||
}
|
||||
}
|
||||
|
||||
if ( typeof(userinfo.nickname) !== 'undefined' ){
|
||||
user.displayName=userinfo.nickname;
|
||||
if (typeof(userinfo.nickname) !== 'undefined')
|
||||
{
|
||||
user.displayName = userinfo.nickname;
|
||||
}
|
||||
|
||||
if ( typeof(userinfo.name) !== 'undefined' ){
|
||||
user.displayName=userinfo.name;
|
||||
if (typeof(userinfo.name) !== 'undefined')
|
||||
{
|
||||
user.displayName = userinfo.name;
|
||||
}
|
||||
|
||||
if ( typeof(userinfo.email) !== 'undefined' ){
|
||||
user.emails=[{value: userinfo.email}];
|
||||
if (typeof(userinfo.email) !== 'undefined')
|
||||
{
|
||||
user.emails = [ { value: userinfo.email } ];
|
||||
}
|
||||
|
||||
if ( typeof(userinfo.given_name) !== 'undefined' ){
|
||||
user.name={givenName: userinfo.given_name};
|
||||
if (typeof(userinfo.given_name) !== 'undefined')
|
||||
{
|
||||
user.name = { givenName: userinfo.given_name };
|
||||
}
|
||||
|
||||
if ( typeof(userinfo.family_name) !== 'undefined' ){
|
||||
user.name={familyName: userinfo.family_name};
|
||||
if (typeof(userinfo.family_name) !== 'undefined')
|
||||
{
|
||||
user.name = { familyName: userinfo.family_name };
|
||||
}
|
||||
|
||||
if ( typeof(userinfo.middle_name) !== 'undefined' ){
|
||||
user.name={middleName: userinfo.middle_name};
|
||||
if (typeof(userinfo.middle_name) !== 'undefined')
|
||||
{
|
||||
user.name = { middleName: userinfo.middle_name };
|
||||
}
|
||||
|
||||
|
||||
return done(null, user);
|
||||
}
|
||||
);
|
||||
|
||||
passport.use('oidc', oidcStrategy);
|
||||
|
||||
app.use(session({
|
||||
secret: config.cookieSecret,
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: { secure: true }
|
||||
secret : config.cookieSecret,
|
||||
resave : true,
|
||||
saveUninitialized : true,
|
||||
cookie : { secure: true }
|
||||
}));
|
||||
|
||||
app.use(passport.initialize());
|
||||
@@ -145,20 +213,20 @@ function setupAuth(oidcIssuer)
|
||||
{
|
||||
passport.authenticate('oidc', {
|
||||
state : base64.encode(JSON.stringify({
|
||||
roomId : req.query.roomId,
|
||||
peerName : req.query.peerName,
|
||||
code : utils.random(10)
|
||||
roomId : req.query.roomId,
|
||||
peerId : req.query.peerId,
|
||||
code : utils.random(10)
|
||||
}))
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
// logout
|
||||
app.get('/auth/logout', function(req, res)
|
||||
app.get('/auth/logout', (req, res) =>
|
||||
{
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// callback
|
||||
app.get(
|
||||
'/auth/callback',
|
||||
@@ -168,22 +236,30 @@ function setupAuth(oidcIssuer)
|
||||
const state = JSON.parse(base64.decode(req.query.state));
|
||||
|
||||
if (rooms.has(state.roomId))
|
||||
{
|
||||
let displayName,photo
|
||||
if (typeof(req.user) !== 'undefined'){
|
||||
if (typeof(req.user.displayName) !== 'undefined') displayName=req.user.displayName;
|
||||
else displayName="";
|
||||
{
|
||||
let displayName;
|
||||
let photo;
|
||||
|
||||
if (typeof(req.user) !== 'undefined')
|
||||
{
|
||||
if (typeof(req.user.displayName) !== 'undefined')
|
||||
displayName = req.user.displayName;
|
||||
else
|
||||
displayName = '';
|
||||
|
||||
if (
|
||||
typeof(req.user.Photos) !== 'undefined' &&
|
||||
typeof(req.user.Photos[0]) !== 'undefined' &&
|
||||
typeof(req.user.Photos[0].value) !== 'undefined'
|
||||
) photo=req.user.Photos[0].value;
|
||||
else photo="/static/media/buddy.403cb9f6.svg";
|
||||
)
|
||||
photo = req.user.Photos[0].value;
|
||||
else
|
||||
photo = '/static/media/buddy.403cb9f6.svg';
|
||||
}
|
||||
|
||||
|
||||
const data =
|
||||
{
|
||||
peerName : state.peerName,
|
||||
peerId : state.peerId,
|
||||
name : displayName,
|
||||
picture : photo
|
||||
};
|
||||
@@ -198,9 +274,12 @@ function setupAuth(oidcIssuer)
|
||||
);
|
||||
}
|
||||
|
||||
function setupWebServer() {
|
||||
async function runHttpsServer()
|
||||
{
|
||||
app.use(compression());
|
||||
|
||||
app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge'));
|
||||
|
||||
app.all('*', (req, res, next) =>
|
||||
{
|
||||
if (req.secure)
|
||||
@@ -234,19 +313,23 @@ function setupWebServer() {
|
||||
{
|
||||
logger.info('Server redirecting port: ', config.listeningRedirectPort);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setupSocketIO(){
|
||||
/**
|
||||
* Create a protoo WebSocketServer to allow WebSocket connections from browsers.
|
||||
*/
|
||||
async function runWebSocketServer()
|
||||
{
|
||||
const io = require('socket.io')(httpsServer);
|
||||
|
||||
// Handle connections from clients.
|
||||
io.on('connection', (socket) =>
|
||||
{
|
||||
const { roomId, peerName } = socket.handshake.query;
|
||||
const { roomId, peerId } = socket.handshake.query;
|
||||
|
||||
if (!roomId || !peerName)
|
||||
if (!roomId || !peerId)
|
||||
{
|
||||
logger.warn('connection request without roomId and/or peerName');
|
||||
logger.warn('connection request without roomId and/or peerId');
|
||||
|
||||
socket.disconnect(true);
|
||||
|
||||
@@ -254,72 +337,90 @@ function setupSocketIO(){
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName);
|
||||
'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
|
||||
|
||||
let room;
|
||||
|
||||
// If an unknown roomId, create a new Room.
|
||||
if (!rooms.has(roomId))
|
||||
queue.push(async () =>
|
||||
{
|
||||
logger.info('creating a new Room [roomId:"%s"]', roomId);
|
||||
const room = await getOrCreateRoom({ roomId });
|
||||
|
||||
try
|
||||
{
|
||||
room = new Room(roomId, mediaServer, io);
|
||||
|
||||
global.APP_ROOM = room;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logger.error('error creating a new Room: %s', error);
|
||||
|
||||
socket.disconnect(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const logStatusTimer = setInterval(() =>
|
||||
{
|
||||
room.logStatus();
|
||||
}, 30000);
|
||||
|
||||
rooms.set(roomId, room);
|
||||
|
||||
room.on('close', () =>
|
||||
{
|
||||
rooms.delete(roomId);
|
||||
clearInterval(logStatusTimer);
|
||||
});
|
||||
}
|
||||
else
|
||||
room.handleConnection({ peerId, socket });
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
room = rooms.get(roomId);
|
||||
}
|
||||
logger.error('room creation or room joining failed:%o', error);
|
||||
|
||||
socket.room = roomId;
|
||||
socket.disconnect(true);
|
||||
|
||||
room.handleConnection(peerName, socket);
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (
|
||||
typeof(auth) !== 'undefined' &&
|
||||
typeof(auth.issuerURL) !== 'undefined' &&
|
||||
typeof(auth.clientOptions) !== 'undefined'
|
||||
)
|
||||
|
||||
/**
|
||||
* Launch as many mediasoup Workers as given in the configuration file.
|
||||
*/
|
||||
async function runMediasoupWorkers()
|
||||
{
|
||||
Issuer.discover(auth.issuerURL).then((oidcIssuer) =>
|
||||
const { numWorkers } = config.mediasoup;
|
||||
|
||||
logger.info('running %d mediasoup Workers...', numWorkers);
|
||||
|
||||
for (let i = 0; i < numWorkers; ++i)
|
||||
{
|
||||
setupAuth(oidcIssuer);
|
||||
setupWebServer();
|
||||
setupSocketIO();
|
||||
}).catch((err) => {
|
||||
logger.error(err);
|
||||
const worker = await mediasoup.createWorker(
|
||||
{
|
||||
logLevel : config.mediasoup.worker.logLevel,
|
||||
logTags : config.mediasoup.worker.logTags,
|
||||
rtcMinPort : config.mediasoup.worker.rtcMinPort,
|
||||
rtcMaxPort : config.mediasoup.worker.rtcMaxPort
|
||||
});
|
||||
|
||||
worker.on('died', () =>
|
||||
{
|
||||
logger.error(
|
||||
'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid);
|
||||
|
||||
setTimeout(() => process.exit(1), 2000);
|
||||
});
|
||||
|
||||
mediasoupWorkers.push(worker);
|
||||
}
|
||||
);
|
||||
} else
|
||||
{
|
||||
logger.error('Auth is not configure properly!');
|
||||
setupWebServer();
|
||||
setupSocketIO();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
async function getOrCreateRoom({ roomId })
|
||||
{
|
||||
let room = rooms.get(roomId);
|
||||
|
||||
// If the Room does not exist create a new one.
|
||||
if (!room)
|
||||
{
|
||||
logger.info('creating a new Room [roomId:%s]', roomId);
|
||||
|
||||
const mediasoupWorker = getMediasoupWorker();
|
||||
|
||||
room = await Room.create({ mediasoupWorker, roomId });
|
||||
|
||||
rooms.set(roomId, room);
|
||||
room.on('close', () => rooms.delete(roomId));
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user