diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index cf2703d..3f0ce49 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -33,7 +33,7 @@ var config = */ audioOutputSupportedBrowsers : [ - 'chrome', + 'chrome', 'opera' ], // Socket.io request timeout diff --git a/app/src/index.js b/app/src/index.js index 90bfa4d..cdefb40 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -38,6 +38,7 @@ import messagesCzech from './translations/cs'; import messagesItalian from './translations/it'; import messagesUkrainian from './translations/uk'; import messagesTurkish from './translations/tr'; +import messagesLatvian from './translations/lv'; import './index.css'; @@ -63,7 +64,8 @@ const messages = 'cs' : messagesCzech, 'it' : messagesItalian, 'uk' : messagesUkrainian, - 'tr' : messagesTurkish + 'tr' : messagesTurkish, + 'lv' : messagesLatvian }; const locale = navigator.language.split(/[-_]/)[0]; // language without region code diff --git a/app/src/translations/lv.json b/app/src/translations/lv.json new file mode 100644 index 0000000..dce2204 --- /dev/null +++ b/app/src/translations/lv.json @@ -0,0 +1,170 @@ +{ + "socket.disconnected": "Esat bezsaistē", + "socket.reconnecting": "Esat bezsaistē, tiek mēģināts pievienoties", + "socket.reconnected": "Esat atkārtoti pievienojies", + "socket.requestError": "Kļūme servera pieprasījumā", + + "room.chooseRoom": "Ievadiet sapulces telpas nosaukumu (ID), kurai vēlaties pievienoties", + "room.cookieConsent": "Lai uzlabotu lietotāja pieredzi, šī vietne izmanto sīkfailus", + "room.consentUnderstand": "Es saprotu un piekrītu", + "room.joined": "Jūs esiet pievienojies sapulces telpai", + "room.cantJoin": "Nav iespējams pievienoties sapulces telpai", + "room.youLocked": "Jūs aizslēdzāt sapulces telpu", + "room.cantLock": "Nav iespējams aizslēgt sapulces telpu", + "room.youUnLocked": "Jūs atslēdzāt sapulces telpu", + "room.cantUnLock": "Nav iespējams atslēgt sapulces telpu", + "room.locked": "Sapulces telpa tagad ir AIZSLĒGTA", + "room.unlocked": "Sapulces telpa tagad ir ATSLĒGTA", + "room.newLobbyPeer": "Jauns dalībnieks ienācis uzgaidāmajā telpā", + "room.lobbyPeerLeft": "Dalībnieks uzgaidāmo telpu pameta", + "room.lobbyPeerChangedDisplayName": "Dalībnieks uzgaidāmajā telpā nomainīja vārdu uz {displayName}", + "room.lobbyPeerChangedPicture": "Dalībnieks uzgaidāmajā telpā nomainīja pašattēlu", + "room.setAccessCode": "Pieejas kods sapulces telpai aktualizēts", + "room.accessCodeOn": "Pieejas kods sapulces telpai tagad ir aktivēts", + "room.accessCodeOff": "Pieejas kods sapulces telpai tagad ir deaktivēts (atslēgts)", + "room.peerChangedDisplayName": "{oldDisplayName} pārsaucās par {displayName}", + "room.newPeer": "{displayName} pievienojās sapulces telpai", + "room.newFile": "Pieejams jauns fails", + "room.toggleAdvancedMode": "Pārslēgt uz advancēto režīmu", + "room.setDemocraticView": "Nomainīts izkārtojums uz demokrātisko skatu", + "room.setFilmStripView": "Nomainīts izkārtojums uz diapozitīvu (filmstrip) skatu", + "room.loggedIn": "Jūs esat ierakstījies (sistēmā)", + "room.loggedOut": "Jūs esat izrakstījies (no sistēmas)", + "room.changedDisplayName": "Jūsu vārds mainīts uz {displayName}", + "room.changeDisplayNameError": "Gadījās ķibele ar Jūsu vārda nomaiņu", + "room.chatError": "Nav iespējams nosūtīt tērziņa ziņu", + "room.aboutToJoin": "Jūs grasāties pievienoties sapulcei", + "room.roomId": "Sapulces telpas nosaukums (ID): {roomName}", + "room.setYourName": "Norādiet savu dalības vārdu un izvēlieties kā vēlaties pievienoties sapulcei:", + "room.audioOnly": "Vienīgi audio", + "room.audioVideo": "Audio & video", + "room.youAreReady": "Ok, Jūs esiet gatavi!", + "room.emptyRequireLogin": "Sapulces telpa ir tukša! Jūs varat Ierakstīties sistēmā, lai uzsāktu vadīt sapulci vai pagaidīt kamēr pievienojas sapulces rīkotājs/vadītājs", + "room.locketWait": "Sapulce telpa ir slēgta. Jūs atrodaties tās uzgaidāmajā telpā. Uzkavējieties, kamēr kāds Jūs sapulcē ielaiž ...", + "room.lobbyAdministration": "Uzgaidāmās telpas administrēšana", + "room.peersInLobby": "Dalībnieki uzgaidāmajā telpā", + "room.lobbyEmpty": "Pašreiz uzgaidāmajā telpā neviena nav", + "room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}", + "room.me": "Es", + "room.spotlights": "Aktīvie (referējošie) dalībnieki", + "room.passive": "Pasīvie dalībnieki", + "room.videoPaused": "Šis video ir pauzēts", + "room.muteAll": "Noklusināt visus dalībnieku mikrofonus", + "room.stopAllVideo": "Izslēgt visu dalībnieku kameras", + "room.closeMeeting": "Beigt sapulci", + "room.clearChat": "Nodzēst visus tērziņus", + "room.clearFileSharing": "Notīrīt visus kopīgotos failus", + "room.speechUnsupported": "Jūsu pārlūks neatbalsta balss atpazīšanu", + "room.moderatoractions": "Moderatora rīcība", + "room.raisedHand": "{displayName} pacēla roku", + "room.loweredHand": "{displayName} nolaida roku", + "room.extraVideo": "Papildus video", + + "me.mutedPTT": "Jūs esat noklusināts. Turiet taustiņu SPACE-BAR, lai runātu", + + "roles.gotRole": "Jūs ieguvāt lomu: {role}", + "roles.lostRole": "Jūs zaudējāt lomu: {role}", + + "tooltip.login": "Ierakstīties", + "tooltip.logout": "Izrakstīties", + "tooltip.admitFromLobby": "Ielaist no uzgaidāmās telpas", + "tooltip.lockRoom": "Aizslēgt sapulces telpu", + "tooltip.unLockRoom": "Atlēgt sapulces telpu", + "tooltip.enterFullscreen": "Aktivēt pilnekrāna režīmu", + "tooltip.leaveFullscreen": "Pamest pilnekrānu", + "tooltip.lobby": "Parādīt uzgaidāmo telpu", + "tooltip.settings": "Parādīt iestatījumus", + "tooltip.participants": "Parādīt dalībniekus", + "tooltip.kickParticipant": "Izvadīt (izspert) dalībnieku", + "tooltip.muteParticipant": "Noklusināt dalībnieku", + "tooltip.muteParticipantVideo": "Atslēgt dalībnieka video", + "tooltip.raisedHand": "Pacelt roku", + + "label.roomName": "Sapulces telpas nosaukums (ID)", + "label.chooseRoomButton": "Turpināt", + "label.yourName": "Jūu vārds", + "label.newWindow": "Jauns logs", + "label.fullscreen": "Pilnekrāns", + "label.openDrawer": "Atvērt atvilkni", + "label.leave": "Pamest", + "label.chatInput": "Rakstiet tērziņa ziņu...", + "label.chat": "Tērzētava", + "label.filesharing": "Failu koplietošana", + "label.participants": "Dalībnieki", + "label.shareFile": "Koplietot failu", + "label.fileSharingUnsupported": "Failu koplietošana netiek atbalstīta", + "label.unknown": "Nezināms", + "label.democratic": "Demokrātisks skats", + "label.filmstrip": "Diapozitīvu (filmstrip) skats", + "label.low": "Zema", + "label.medium": "Vidēja", + "label.high": "Augsta (HD)", + "label.veryHigh": "Ļoti augsta (FHD)", + "label.ultra": "Ultra (UHD)", + "label.close": "Aizvērt", + "label.media": "Mediji", + "label.appearence": "Izskats", + "label.advanced": "Advancēts", + "label.addVideo": "Pievienot video", + + "settings.settings": "Iestatījumi", + "settings.camera": "Kamera", + "settings.selectCamera": "Izvēlieties kameru (video ierīci)", + "settings.cantSelectCamera": "Nav iespējams lietot šo kameru (video ierīci)", + "settings.audio": "Skaņas ierīce", + "settings.selectAudio": "Izvēlieties skaņas ierīci", + "settings.cantSelectAudio": "Nav iespējams lietot šo skaņas (audio) ierīci", + "settings.resolution": "Iestatiet jūsu video izšķirtspēju", + "settings.layout": "Sapulces telpas izkārtojums", + "settings.selectRoomLayout": "Iestatiet sapulces telpas izkārtojumu", + "settings.advancedMode": "Advancētais režīms", + "settings.permanentTopBar": "Pastāvīga augšējā (ekrānaugšas) josla", + "settings.lastn": "Jums redzamo video/kameru skaits", + "settings.hiddenControls": "Slēpto mediju vadība", + "settings.notificationSounds": "Paziņojumu skaņas", + + "filesharing.saveFileError": "Nav iespējams saglabāt failu", + "filesharing.startingFileShare": "Tiek mēģināts kopīgot failu", + "filesharing.successfulFileShare": "Fails sekmīgi kopīgots", + "filesharing.unableToShare": "Nav iespējams kopīgot failu", + "filesharing.error": "Atgadījās faila kopīgošanas kļūme", + "filesharing.finished": "Fails ir lejupielādēts", + "filesharing.save": "Saglabāt", + "filesharing.sharedFile": "{displayName} kopīgoja failu", + "filesharing.download": "Lejuplādēt", + "filesharing.missingSeeds": "Ja šis process aizņem ilgu laiku, iespējams nav neviena, kas sēklo (seed) šo torentu. Mēģiniet palūgt kādu atkārtoti augšuplādēt Jūsu gribēto failu.", + + "devices.devicesChanged": "Jūsu ierīces pamainījās. Iestatījumu izvēlnē (dialogā) iestatiet jaunās ierīces.", + + "device.audioUnsupported": "Skaņa (audio) netiek atbalstīta", + "device.activateAudio": "Iespējot/aktivēt mikrofonu (izejošo skaņu)", + "device.muteAudio": "Atslēgt/noklusināt mikrofonu (izejošo skaņu) ", + "device.unMuteAudio": "Ieslēgt mikrofonu (izejošo skaņu)", + + "device.videoUnsupported": "Kamera (izejošais video) netiek atbalstīta", + "device.startVideo": "Ieslēgt kameru (izejošo video)", + "device.stopVideo": "Izslēgt kameru (izejošo video)", + + "device.screenSharingUnsupported": "Ekrāna kopīgošana netiek atbalstīta", + "device.startScreenSharing": "Sākt ekrāna kopīgošanu", + "device.stopScreenSharing": "Beigt ekrāna kopīgošanu", + + "devices.microphoneDisconnected": "Mikrofons atvienots", + "devices.microphoneError": "Atgadījās kļūme, piekļūstot jūsu mikrofonam", + "devices.microPhoneMute": "Mikrofons izslēgts/noklusināts", + "devices.micophoneUnMute": "Mikrofons ieslēgts", + "devices.microphoneEnable": "Mikrofons iespējots", + "devices.microphoneMuteError": "Nav iespējams izslēgt Jūsu mikrofonu", + "devices.microphoneUnMuteError": "Nav iespējams ieslēgt Jūsu mikrofonu", + + "devices.screenSharingDisconnected" : "Ekrāna kopīgošana nenotiek (atvienota)", + "devices.screenSharingError": "Atgadījās kļūme, piekļūstot Jūsu ekrānam", + + "devices.cameraDisconnected": "Kamera atvienota", + "devices.cameraError": "Atgadījās kļūme, piekļūstot Jūsu kamerai", + + "moderator.clearChat": "Moderators nodzēsa tērziņus", + "moderator.clearFiles": "Moderators notīrīja failus", + "moderator.muteAudio": "Moderators noklusināja jūsu mikrofonu", + "moderator.muteVideo": "Moderators atslēdza jūsu kameru" +} diff --git a/server/config/config.example.js b/server/config/config.example.js index 6ff279a..e8960f7 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -247,6 +247,8 @@ module.exports = // When truthy, the room will be open to all users when as long as there // are allready users in the room activateOnHostJoin : true, + // Room size before spreading to new router + routerScaleSize : 20, // Mediasoup settings mediasoup : { diff --git a/server/lib/Peer.js b/server/lib/Peer.js index a345a16..163e648 100644 --- a/server/lib/Peer.js +++ b/server/lib/Peer.js @@ -39,6 +39,8 @@ class Peer extends EventEmitter this._email = null; + this._routerId = null; + this._rtpCapabilities = null; this._raisedHand = false; @@ -238,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; diff --git a/server/lib/Room.js b/server/lib/Room.js index 9200be9..6067e75 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -31,6 +31,8 @@ const permissionsFromRoles = ...config.permissionsFromRoles }; +const ROUTER_SCALE_SIZE = config.routerScaleSize || 20; + class Room extends EventEmitter { /** @@ -38,32 +40,45 @@ 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); // 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 mediasoupWorkers) + { + 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; + + return new Room({ roomId, mediasoupRouters, audioLevelObserver }); } - constructor({ roomId, mediasoupRouter, audioLevelObserver }) + constructor({ roomId, mediasoupRouters, audioLevelObserver }) { logger.info('constructor() [roomId:"%s"]', roomId); @@ -98,8 +113,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; @@ -122,8 +142,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) { @@ -133,8 +159,19 @@ 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'); @@ -401,6 +438,9 @@ class Room extends EventEmitter this._peers[peer.id] = peer; + // Assign routerId + peer.routerId = await this._getRouterId(); + this._handlePeer(peer); if (returning) @@ -560,11 +600,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; } @@ -673,7 +716,7 @@ class Room extends EventEmitter webRtcTransportOptions.enableTcp = true; } - const transport = await this._mediasoupRouter.createWebRtcTransport( + const transport = await router.createWebRtcTransport( webRtcTransportOptions ); @@ -772,6 +815,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); @@ -1379,6 +1435,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. @@ -1389,7 +1447,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 @@ -1601,6 +1659,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; diff --git a/server/server.js b/server/server.js index dbf9a8e..c3aec39 100755 --- a/server/server.js +++ b/server/server.js @@ -644,9 +644,9 @@ 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);