diff --git a/CHANGELOG.md b/CHANGELOG.md index bda55dd..471b71f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,32 @@ # Changelog +## 3.2.1 + +* Fix: permananent top bar by default +* Fix: `httpOnly` mode https redirect +* Add some extra checks for video stream and track +* Add Italian translation +* Add Czech translation +* Add new server option `trustProxy` for load balancing http only use case +* Add HAproxy load balance example +* Add LTI LMS integration documentation +* Fix spacing of leave button +* Fix for sharing same file multiple times + ## 3.2 * Add munin plugin -* Add muted=true search param to disble audio by deffault +* Add `muted=true` search param to disable audio by default * Modify webtorrent tracker * Add key shortcut `space` for audio mute * Add key shortcut `v` for video mute * Add user configurable LastN -* Add option to sticky top bar (sticky by default) -* update mediasoup server -* Add simulcast options to app config (disabled by default) -* Add stats option to get counts of rooms and peers -* Add httpOnly option for loadbalancer backend setups +* Add option to permananent top bar (permanent by default) +* Update mediasoup server +* Add `simulcast` options to app config (disabled by default) +* Add `stats` option to get counts of rooms and peers +* Add `httpOnly` option for loadbalancer backend setups * LTI integration for LMS systems like moodle -* Add muted=false search parameter * Add translations (12+1 languages) * Add support IPv6 * Many other fixes and refactorings @@ -33,10 +45,10 @@ * Updated to mediasoup v3 * Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client) - - OpenID Connect discovery - - Auth code flow + * OpenID Connect discovery + * Auth code flow * Add spdy http2 support. - - Notice it does not supports node 11.x + * Notice it does not supports node 11.x * Updated to Material UI v4 ## 2.0 diff --git a/HAproxy.md b/HAproxy.md new file mode 100644 index 0000000..095a6bf --- /dev/null +++ b/HAproxy.md @@ -0,0 +1,109 @@ +# Howto deploy a (room based) load balanced cluster + +This example will show how to setup an HA proxy to provide load balancing between several +multiparty-meeting servers. + +## IP and DNS + +In this basic example we use the following names and ips: + +### Backend + +* `mm1.example.com` <=> `192.0.2.1` +* `mm2.example.com` <=> `192.0.2.2` +* `mm3.example.com` <=> `192.0.2.3` + +### Redis + +* `redis.example.com` <=> `192.0.2.4` + +### Load balancer HAproxy + +* `meet.example.com` <=> `192.0.2.5` + +## Deploy multiple multiparty-meeting servers + +This is most easily done using Ansible (see below), but can be done +in any way you choose (manual, Docker, Ansible). + +Read more here: [mm-ansible](https://github.com/misi/mm-ansible) +[![asciicast](https://asciinema.org/a/311365.svg)](https://asciinema.org/a/311365) + +## Setup Redis for central HTTP session store + +### Use one Redis for all multiparty-meeting servers + +* Deploy a Redis cluster for all instances. + * We will use in our actual example `192.0.2.4` as redis HA cluster ip. It is out of scope howto deploy it. + +OR + +* For testing you can use Redis from one the multiparty-meeting servers. e.g. If you plan only for testing on your first multiparty-meeting server. + * Configure Redis `redis.conf` to not only bind to your loopback but also to your global ip address too: + + ``` plaintext + bind 192.0.2.1 + ``` + + This example sets this to `192.0.2.1`, change this according to your local installation. + + * Change your firewall config to allow incoming Redis. Example (depends on the type of firewall): + + ``` plaintext + chain INPUT { + policy DROP; + + saddr mm2.example.com proto tcp dport 6379 ACCEPT; + saddr mm3.example.com proto tcp dport 6379 ACCEPT; + } + ``` + + * **Set a password, or if you don't (like in this basic example) take care to set strict firewall rules** + +## Configure multiparty-meeting servers + +### App config + +mm/configs/app/config.js + +``` js +multipartyServer : 'meet.example.com', +``` + +### Server config + +mm/configs/server/config.js + +``` js +redisOptions : { host: '192.0.2.4'}, +listeningPort: 80, +httpOnly: true, +trustProxy : ['192.0.2.5'], +``` + +## Deploy HA proxy + +* Configure cerificate / letsencrypt for `meet.example.com` + * In this example we put a complete chain and private key in /root/certificate.pem. +* Install and setup haproxy + + `apt install haproxy` + +* Add to /etc/haproxy/haproxy.cfg config + + ``` plaintext + backend multipartymeeting + balance url_param roomId + hash-type consistent + + server mm1 192.0.2.1:80 check maxconn 20 verify none + server mm2 192.0.2.2:80 check maxconn 20 verify none + server mm3 192.0.2.3:80 check maxconn 20 verify none + + frontend meet.example.com + bind 192.0.2.5:80 + bind 192.0.2.5:443 ssl crt /root/certificate.pem + http-request redirect scheme https unless { ssl_fc } + reqadd X-Forwarded-Proto:\ https + default_backend multipartymeeting + ``` diff --git a/LTI/LTI.md b/LTI/LTI.md new file mode 100644 index 0000000..ff6b9ec --- /dev/null +++ b/LTI/LTI.md @@ -0,0 +1,61 @@ +# Learning Tools Interoperability (LTI) + +## LTI + +Read more about IMS Global defined interface for tools like our VideoConference system integration with Learning Managment Systems(LMS) (e.g. moodle). +See: [IMS Global Learning Tool Interoperability](https://www.imsglobal.org/activity/learning-tools-interoperability) + +We implemented LTI interface version 1.0/1.1 + +### Server config auth section LTI settings + +Set in server configuration a random key and secret + +``` json +auth : + { + lti : + { + consumerKey : 'key', + consumerSecret : 'secret' + }, + } +``` + +### Configure your LMS system with secret and key settings above + +#### Auth tool URL + +Set tool URL to your server with path /auth/lti + +``` url +https://mm.example.com/auth/lti +``` + +#### In moodle find external tool plugin setting and external tool action + +See: [moodle external tool settings](https://docs.moodle.org/38/en/External_tool_settings) + +#### Add and activity + +![Add external tool](lti1.png) + +#### Setup Activity + +##### Activity setup basic form + +Open fully the settings **Click on show more!!** +![Add external tool config](lti2.png) + +##### Empty full form + +![Opened external tool config](lti3.png) + +##### Filled out form + +![Filled out external tool config](lti4.png) + +## moodle plugin + +Alternativly you can use multipartymeeting moodle plugin: +[https://github.com/misi/moodle-mod_multipartymeeting](https://github.com/misi/moodle-mod_multipartymeeting) diff --git a/LTI/lti1.png b/LTI/lti1.png new file mode 100644 index 0000000..cc420c4 Binary files /dev/null and b/LTI/lti1.png differ diff --git a/LTI/lti2.png b/LTI/lti2.png new file mode 100644 index 0000000..8c1d271 Binary files /dev/null and b/LTI/lti2.png differ diff --git a/LTI/lti3.png b/LTI/lti3.png new file mode 100644 index 0000000..3b16d35 Binary files /dev/null and b/LTI/lti3.png differ diff --git a/LTI/lti4.png b/LTI/lti4.png new file mode 100644 index 0000000..c36bbe2 Binary files /dev/null and b/LTI/lti4.png differ diff --git a/README.md b/README.md index dc2c8cf..0a47de2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ If you want the automatic approach, you can find a docker image [here](https://h If you want the ansible approach, you can find ansible role [here](https://github.com/misi/mm-ansible/). [![asciicast](https://asciinema.org/a/311365.svg)](https://asciinema.org/a/311365) - ## Manual installation * Prerequisites: Currently multiparty-meeting will only run on nodejs v10.* @@ -106,6 +105,12 @@ $ systemctl enable multiparty-meeting * 4443/tcp (default `npm start` port for developing with live browser reload, not needed in production enviroments - adjustable in app/package.json) * 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`) +## Load balanced installation +To deploy this as a load balanced cluster, have a look at [HAproxy](HAproxy.md). + +## Learning management integration +To integrate with an LMS (e.g. Moodle), have a look at [LTI](LTI/LTI.md). + ## TURN configuration * You need an addtional [TURN](https://github.com/coturn/coturn)-server for clients located behind restrictive firewalls! Add your server and credentials to `app/config.js` diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 094af23..2cc380a 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -78,8 +78,8 @@ const styles = (theme) => }, actionButton : { - margin : theme.spacing(1), - padding : 0 + margin : theme.spacing(1, 0), + padding : theme.spacing(0, 1) } }); @@ -446,4 +446,4 @@ export default withRoomContext(connect( ); } } -)(withStyles(styles, { withTheme: true })(TopBar))); \ No newline at end of file +)(withStyles(styles, { withTheme: true })(TopBar))); diff --git a/app/src/index.js b/app/src/index.js index c7f0d90..dd052fa 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -34,7 +34,9 @@ import messagesPortuguese from './translations/pt'; import messagesChinese from './translations/cn'; import messagesSpanish from './translations/es'; import messagesCroatian from './translations/hr'; -import messagesCzech from './translations/cz'; +import messagesCzech from './translations/cs'; +import messagesItalian from './translations/it'; +import messagesUkrainian from './translations/uk'; import './index.css'; @@ -57,7 +59,9 @@ const messages = 'zh' : messagesChinese, 'es' : messagesSpanish, 'hr' : messagesCroatian, - 'cz' : messagesCzech + 'cs' : messagesCzech, + 'it' : messagesItalian, + 'uk' : messagesUkrainian }; const locale = navigator.language.split(/[-_]/)[0]; // language without region code diff --git a/app/src/translations/cz.json b/app/src/translations/cs.json similarity index 100% rename from app/src/translations/cz.json rename to app/src/translations/cs.json diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index c5415fb..3172ab8 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -6,7 +6,7 @@ "room.chooseRoom": "Wybór konferencji", "room.cookieConsent": "Ta strona internetowa wykorzystuje pliki cookie w celu zwiększenia wygody użytkowania.", - "room.consentUnderstand": "I understand", + "room.consentUnderstand": "Rozumiem", "room.joined": "Podłączono do konferencji", "room.cantJoin": "Brak możliwości dołączenia do pokoju", "room.youLocked": "Zakluczono pokój", @@ -103,7 +103,7 @@ "settings.selectRoomLayout": "Ustawienia układu konferencji", "settings.advancedMode": "Tryb zaawansowany", "settings.permanentTopBar": "Stały górny pasek", - "settings.lastn": "Liczba widocznych filmów", + "settings.lastn": "Liczba widocznych uczestników (zdalnych)", "filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku", diff --git a/app/src/translations/uk.json b/app/src/translations/uk.json new file mode 100644 index 0000000..4c051ed --- /dev/null +++ b/app/src/translations/uk.json @@ -0,0 +1,140 @@ +{ +"socket.disconnected": "Ви відключені", +"socket.reconnecting": "Ви від'єдналися, намагаєтесь знову підключитися", +"socket.reconnected": "Ви знову підключилися", +"socket.requestError": "Помилка при запиті сервера", + +"room.chooseRoom": "Виберіть назву кімнати, до якої хочете приєднатися", +"room.cookieConsent": "Цей веб-сайт використовує файли cookie для поліпшення роботи користувачів", +"room.consentUnderstand": "Я розумію", +"room.joined": "Ви приєдналися до кімнати", +"room.cantJoin": "Неможливо приєднатися до кімнати", +"room.youLocked": "Ви заблокували кімнату", +"room.cantLock": "Не вдається заблокувати кімнату", +"room.youUnLocked": "Ви розблокували кімнату", +"room.cantUnLock": "Не вдається розблокувати кімнату", +"room.locked": "Кімната зараз заблокована", +"room.unlocked": "Кімната зараз розблокована", +"room.newLobbyPeer": "Новий учасник увійшов у зал очікування", +"room.lobbyPeerLeft": "Учасник вийшов із зала очікування", +"room.lobbyPeerChangedDisplayName": "Учасник у залі очікування змінив ім'я на {displayName}", +"room.lobbyPeerChangedPicture": "Учасник залу очікування змінив зображення", +"room.setAccessCode": "Код доступу до кімнати оновлений", +"room.accessCodeOn": "Код доступу до кімнати зараз активований", +"room.accessCodeOff": "Код доступу до кімнати зараз відключений", +"room.peerChangedDisplayName": "{oldDisplayName} змінив ім'я на {displayName}", +"room.newPeer": "{displayName} приєднався до кімнати", +"room.newFile": "Новий файл є у доступі", +"room.toggleAdvancedMode": "Увімкнено розширений режим", +"room.setDemocratView": "Змінено макет на демократичний вигляд", +"room.setFilmStripView": "Змінено макет на вид фільму", +"room.loggedIn": "Ви ввійшли в систему", +"room.loggedOut": "Ви вийшли з системи", +"room.changedDisplayName": "Відображуване ім’я змінено на {displayName}", +"room.changeDisplayNameError": "Сталася помилка під час зміни вашого відображуваного імені", +"room.chatError": "Не вдається надіслати повідомлення в чаті", +"room.aboutToJoin": "Ви збираєтесь приєднатися до зустрічі", +"room.roomId": "Ідентифікатор кімнати: {roomName}", +"room.setYourName": "Встановіть своє ім'я для участі та виберіть, як ви хочете приєднатися:", +"room.audioOnly": "Тільки аудіо", +"room.audioVideo": "Аудіо та відео", +"room.youAreReady": "Добре, ви готові", +"room.emptyRequireLogin": "Кімната порожня! Ви можете увійти, щоб розпочати зустріч або чекати, поки хост приєднається", +"room.locketWait": "Кімната заблокована - дочекайтеся, поки хтось не впустить вас у ...", +"room.lobbyAdministration": "Адміністрація залу очікування", +"room.peersInLobby": "Учасники залу очікувань", +"room.lobbyEmpty": "Наразі у залі очікувань немає нікого", +"room.hiddenPeers": "{hiddenPeersCount, множина, один {учасник} інший {учасників}}", +"room.me": "Я", +"room.spotlights": "Учасники у центрі уваги", +"room.passive": "Пасивні учасники", +"room.videoPaused": "Це відео призупинено", + +"tooltip.login": "Увійти", +"tooltip.logout": "Вихід", +"tooltip.admitFromLobby": "Вхід із залу очікувань", +"tooltip.lockRoom": "Заблокувати кімнату", +"tooltip.unLockRoom": "Розблокувати кімнату", +"tooltip.enterFullscreen": "Вивести повний екран", +"tooltip.leaveFullscreen": "Залишити повноекранний екран", +"tooltip.lobby": "Показати зал очікувань", +"tooltip.settings": "Показати налаштування", +"tooltip.participants": "Показати учасників", + +"label.roomName": "Назва кімнати", +"label.chooseRoomButton": "Продовжити", +"label.yourName": "Ваше ім'я", +"label.newWindow": "Нове вікно", +"label.fullscreen": "Повний екран", +"label.openDrawer": "Відкрити ящик", +"label.leave": "Залишити", +"label.chatInput": "Введіть повідомлення чату ...", +"label.chat": "Чат", +"label.filesharing": "Обмін файлами", +"label.participants": "Учасники", +"label.shareFile": "Надіслати файл", +"label.fileSharingUnsupported": "Обмін файлами не підтримується", +"label.unknown": "Невідомо", +"label.democrat": "Демократичний вигляд", +"label.filmstrip": "У вигляді кінострічки", +"label.low": "Низький", +"label.medium": "Середній", +"label.high": "Високий (HD)", +"label.veryHigh": "Дуже високий (FHD)", +"label.ultra": "Ультра (UHD)", +"label.close": "Закрити", + +"settings.settings": "Налаштування", +"settings.camera": "Камера", +"settings.selectCamera": "Вибрати відеопристрій", +"settings.cantSelectCamera": "Неможливо вибрати відеопристрій", +"settings.audio": "Аудіопристрій", +"settings.selectAudio": "Вибрати аудіопристрій", +"settings.cantSelectAudio": "Неможливо вибрати аудіопристрій", +"settings.resolution": "Виберіть роздільну здатність відео", +"settings.layout": "Розміщення кімнати", +"settings.selectRoomLayout": "Вибір розташування кімнати", +"settings.advancedMode": "Розширений режим", +"settings.permanentTopBar": "Постійний верхній рядок", +"settings.lastn": "Кількість видимих ​​відео", + +"filesharing.saveFileError": "Неможливо зберегти файл", +"filesharing.startingFileShare": "Спроба поділитися файлом", +"filesharing.successfulFileShare": "Файл готовий для обміну", +"filesharing.unableToShare": "Неможливо поділитися файлом", +"filesharing.error": "Виникла помилка обміну файлами", +"filesharing.finished": "Завантаження файлу закінчено", +"filesharing.save": "Зберегти", +"filesharing.sharedFile": "{displayName} поділився файлом", +"filesharing.download": "Завантажити", +"filesharing.missingSeeds": "Якщо цей процес триває тривалий час, може не з’явиться хтось, хто роздає цей торрент. Спробуйте попросити когось перезавантажити потрібний файл.", + +"devices.devicesChanged": "Ваші пристрої змінилися, налаштуйте ваші пристрої в діалоговому вікні налаштувань", + +"device.audioUnsupported": "Аудіо не підтримується", +"device.activateAudio": "Активувати звук", +"device.muteAudio": "Вимкнути звук", +"device.unMuteAudio": "Увімкнути звук", + +"device.videoUnsupported": "Відео не підтримується", +"device.startVideo": "Запустити відео", +"device.stopVideo": "Зупинити відео", + +"device.screenSharingUnsupported": "Обмін екраном не підтримується", +"device.startScreenSharing": "Початок спільного використання екрана", +"device.stopScreenSharing": "Зупинити спільний доступ до екрана", + +"devices.microphoneDisconnected": "Мікрофон відключений", +"devices.microphoneError": "Сталася помилка під час доступу до мікрофона", +"devices.microPhoneMute": "Вимкнено ваш мікрофон", +"devices.micophoneUnMute": "Не ввімкнено ваш мікрофон", +"devices.microphoneEnable": "Увімкнено мікрофон", +"devices.microphoneMuteError": "Не вдається вимкнути мікрофон", +"devices.microphoneUnMuteError": "Неможливо ввімкнути мікрофон", + +"devices.screenSharingDisconnected": "Спільний доступ до екрана відключений", +"devices.screenSharingError": "Сталася помилка під час доступу до екрану", + +"devices.cameraDisconnected": "Камера відключена", +"devices.cameraError": "Під час доступу до камери сталася помилка" +} \ No newline at end of file diff --git a/server/config/config.example.js b/server/config/config.example.js index 3787b33..ada1553 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -58,6 +58,9 @@ module.exports = cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` }, + // listening Host or IP + // If ommitted 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. diff --git a/server/server.js b/server/server.js index 119f8e6..70cb5bc 100755 --- a/server/server.js +++ b/server/server.js @@ -189,7 +189,7 @@ function setupLTI(ltiConfig) const ltiStrategy = new LTIStrategy( ltiConfig, - function(req, lti, done) + (req, lti, done) => { // LTI launch parameters if (lti) @@ -310,7 +310,7 @@ async function setupAuth() // lti launch app.post('/auth/lti', passport.authenticate('lti', { failureRedirect: '/' }), - function(req, res) + (req, res) => { res.redirect(`/${req.user.room}`); } @@ -333,7 +333,7 @@ async function setupAuth() } req.logout(); - res.send(logoutHelper()); + req.session.destroy(() => res.send(logoutHelper())); }); // callback @@ -427,11 +427,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)