diff --git a/README.md b/README.md index 82137d3..c699a76 100644 --- a/README.md +++ b/README.md @@ -110,3 +110,8 @@ This started as a fork of the [work](https://github.com/versatica/mediasoup-demo ## License MIT + + +Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Union’s Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project. + +GÉANT Vereniging (Association) is registered with the Chamber of Commerce in Amsterdam with registration number 40535155 and operates in the UK as a branch of GÉANT Vereniging. Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK. \ No newline at end of file diff --git a/app/.eslintrc.json b/app/.eslintrc.json new file mode 100644 index 0000000..44a50ba --- /dev/null +++ b/app/.eslintrc.json @@ -0,0 +1,333 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "plugins": [ + "import", + "react" + ], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "react-app" + ], + "settings": { + "react": { + "pragma": "React", + "version": "16" + } + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "impliedStrict": true, + "jsx": true + } + }, + "rules": { + "array-bracket-spacing": [ + 2, + "always", + { + "objectsInArrays": true, + "arraysInArrays": true + } + ], + "arrow-parens": [ + 2, + "always" + ], + "arrow-spacing": 2, + "block-spacing": [ + 2, + "always" + ], + "brace-style": [ + 2, + "allman", + { + "allowSingleLine": true + } + ], + "camelcase": 2, + "comma-dangle": 2, + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": 2, + "computed-property-spacing": 2, + "constructor-super": 2, + "func-call-spacing": 2, + "generator-star-spacing": 2, + "guard-for-in": 2, + "indent": [ + 2, + "tab", + { + "SwitchCase": 1 + } + ], + "key-spacing": [ + 2, + { + "singleLine": { + "beforeColon": false, + "afterColon": true + }, + "multiLine": { + "beforeColon": true, + "afterColon": true, + "align": "colon" + } + } + ], + "keyword-spacing": 2, + "linebreak-style": [ + 2, + "unix" + ], + "lines-around-comment": [ + 2, + { + "allowBlockStart": true, + "allowObjectStart": true, + "beforeBlockComment": true, + "beforeLineComment": false + } + ], + "max-len": [ + 2, + 90, + { + "tabWidth": 2, + "comments": 110, + "ignoreUrls": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreRegExpLiterals": true + } + ], + "newline-after-var": 2, + "newline-before-return": 2, + "newline-per-chained-call": 2, + "no-alert": 2, + "no-caller": 2, + "no-case-declarations": 2, + "no-catch-shadow": 2, + "no-class-assign": 2, + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], + "no-console": 2, + "no-const-assign": 2, + "no-debugger": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-div-regex": 2, + "no-empty": [ + 2, + { + "allowEmptyCatch": true + } + ], + "no-empty-pattern": 2, + "no-else-return": 0, + "no-eval": 2, + "no-extend-native": 2, + "no-ex-assign": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-label": 2, + "no-extra-semi": 2, + "no-fallthrough": 2, + "no-func-assign": 2, + "no-global-assign": 2, + "no-implicit-coercion": 2, + "no-implicit-globals": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-lonely-if": 2, + "no-mixed-operators": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [ + 2, + { + "max": 1, + "maxEOF": 0, + "maxBOF": 0 + } + ], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-proto": 2, + "no-prototype-builtins": 0, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-imports": 2, + "no-return-assign": 2, + "no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-undef": 2, + "no-unexpected-multiline": 2, + "no-unmodified-loop-condition": 2, + "no-unreachable": 2, + "no-unused-vars": [ + 1, + { + "vars": "all", + "args": "after-used" + } + ], + "no-use-before-define": [ + 2, + { + "functions": false + } + ], + "no-useless-call": 2, + "no-useless-computed-key": 2, + "no-useless-concat": 2, + "no-useless-rename": 2, + "no-var": 2, + "no-whitespace-before-property": 2, + "object-curly-newline": 0, + "object-curly-spacing": [ + 2, + "always" + ], + "object-property-newline": [ + 2, + { + "allowMultiplePropertiesPerLine": true + } + ], + "prefer-const": 2, + "prefer-rest-params": 2, + "prefer-spread": 2, + "prefer-template": 2, + "quotes": [ + 2, + "single", + { + "avoidEscape": true + } + ], + "semi": [ + 2, + "always" + ], + "semi-spacing": 2, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], + "space-in-parens": [ + 2, + "never" + ], + "spaced-comment": [ + 2, + "always" + ], + "strict": 2, + "valid-typeof": 2, + "eol-last": 0, + "yoda": 2, + "import/extensions": 2, + "import/no-duplicates": 2, + "jsx-quotes": [ + 2, + "prefer-single" + ], + "react/display-name": [ + 2, + { + "ignoreTranspilerName": false + } + ], + "react/forbid-prop-types": 0, + "react/jsx-boolean-value": 2, + "react/jsx-closing-bracket-location": 2, + "react/jsx-curly-spacing": 2, + "react/jsx-equals-spacing": 2, + "react/jsx-handler-names": 2, + "react/jsx-indent-props": [ + 2, + "tab" + ], + "react/jsx-indent": [ + 2, + "tab" + ], + "react/jsx-key": 2, + "react/jsx-max-props-per-line": 0, + "react/jsx-no-bind": 0, + "react/jsx-no-duplicate-props": 2, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 0, + "react/jsx-pascal-case": 2, + "react/jsx-sort-prop-types": 0, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/no-danger": 2, + "react/no-deprecated": 2, + "react/no-did-mount-set-state": 2, + "react/no-did-update-set-state": 2, + "react/no-direct-mutation-state": 2, + "react/no-is-mounted": 2, + "react/no-multi-comp": 0, + "react/no-set-state": 0, + "react/no-string-refs": 0, + "react/no-unknown-property": 2, + "react/prefer-es6-class": 2, + "react/prop-types": [ + 2, + { + "skipUndeclared": true + } + ], + "react/react-in-jsx-scope": 2, + "react/self-closing-comp": 2, + "react/sort-comp": 0, + "react/jsx-wrap-multilines": [ + 2, + { + "declaration": false, + "assignment": false, + "return": true + } + ] + } +} \ No newline at end of file diff --git a/app/package.json b/app/package.json index b85d265..191ff59 100644 --- a/app/package.json +++ b/app/package.json @@ -6,32 +6,32 @@ "author": "Håvar Aambø Fosstveit ", "license": "MIT", "dependencies": { - "@material-ui/core": "^4.1.2", - "@material-ui/icons": "^4.2.1", - "bowser": "^2.4.0", - "create-torrent": "^3.33.0", + "@material-ui/core": "^4.5.1", + "@material-ui/icons": "^4.5.1", + "bowser": "^2.7.0", + "create-torrent": "^4.4.1", "domready": "^1.0.8", - "file-saver": "^2.0.1", + "file-saver": "^2.0.2", "hark": "^1.2.3", - "marked": "^0.6.1", - "mediasoup-client": "^3.0.6", - "notistack": "^0.5.1", + "marked": "^0.7.0", + "mediasoup-client": "^3.2.7", + "notistack": "^0.9.5", "prop-types": "^15.7.2", "random-string": "^0.2.0", - "react": "^16.8.5", - "react-cookie-consent": "^2.2.2", - "react-dom": "^16.8.5", - "react-redux": "^6.0.1", - "react-scripts": "2.1.8", - "redux": "^4.0.1", + "react": "^16.10.2", + "react-cookie-consent": "^2.5.0", + "react-dom": "^16.10.2", + "react-redux": "^7.1.1", + "react-scripts": "3.2.0", + "redux": "^4.0.4", "redux-logger": "^3.0.6", - "redux-persist": "^5.10.0", + "redux-persist": "^6.0.0", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "riek": "^1.1.0", - "socket.io-client": "^2.2.0", - "source-map-explorer": "^1.8.0", - "webtorrent": "^0.103.1" + "socket.io-client": "^2.3.0", + "source-map-explorer": "^2.1.0", + "webtorrent": "^0.107.16" }, "scripts": { "analyze-main": "source-map-explorer build/static/js/main.*", @@ -41,342 +41,15 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, - "eslintConfig": { - "env": { - "browser": true, - "es6": true, - "node": true - }, - "plugins": [ - "import", - "react" - ], - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "settings": { - "react": { - "pragma": "React", - "version": "16" - } - }, - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true, - "jsx": true - } - }, - "rules": { - "array-bracket-spacing": [ - 2, - "always", - { - "objectsInArrays": true, - "arraysInArrays": true - } - ], - "arrow-parens": [ - 2, - "always" - ], - "arrow-spacing": 2, - "block-spacing": [ - 2, - "always" - ], - "brace-style": [ - 2, - "allman", - { - "allowSingleLine": true - } - ], - "camelcase": 2, - "comma-dangle": 2, - "comma-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "comma-style": 2, - "computed-property-spacing": 2, - "constructor-super": 2, - "func-call-spacing": 2, - "generator-star-spacing": 2, - "guard-for-in": 2, - "indent": [ - 2, - "tab", - { - "SwitchCase": 1 - } - ], - "key-spacing": [ - 2, - { - "singleLine": { - "beforeColon": false, - "afterColon": true - }, - "multiLine": { - "beforeColon": true, - "afterColon": true, - "align": "colon" - } - } - ], - "keyword-spacing": 2, - "linebreak-style": [ - 2, - "unix" - ], - "lines-around-comment": [ - 2, - { - "allowBlockStart": true, - "allowObjectStart": true, - "beforeBlockComment": true, - "beforeLineComment": false - } - ], - "max-len": [ - 2, - 90, - { - "tabWidth": 2, - "comments": 110, - "ignoreUrls": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreRegExpLiterals": true - } - ], - "newline-after-var": 2, - "newline-before-return": 2, - "newline-per-chained-call": 2, - "no-alert": 2, - "no-caller": 2, - "no-case-declarations": 2, - "no-catch-shadow": 2, - "no-class-assign": 2, - "no-confusing-arrow": [ - "error", - { - "allowParens": true - } - ], - "no-console": 2, - "no-const-assign": 2, - "no-debugger": 2, - "no-dupe-args": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-div-regex": 2, - "no-empty": [ - 2, - { - "allowEmptyCatch": true - } - ], - "no-empty-pattern": 2, - "no-else-return": 0, - "no-eval": 2, - "no-extend-native": 2, - "no-ex-assign": 2, - "no-extra-bind": 2, - "no-extra-boolean-cast": 2, - "no-extra-label": 2, - "no-extra-semi": 2, - "no-fallthrough": 2, - "no-func-assign": 2, - "no-global-assign": 2, - "no-implicit-coercion": 2, - "no-implicit-globals": 2, - "no-inner-declarations": 2, - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-lonely-if": 2, - "no-mixed-operators": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-multiple-empty-lines": [ - 2, - { - "max": 1, - "maxEOF": 0, - "maxBOF": 0 - } - ], - "no-native-reassign": 2, - "no-negated-in-lhs": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-wrappers": 2, - "no-obj-calls": 2, - "no-proto": 2, - "no-prototype-builtins": 0, - "no-redeclare": 2, - "no-regex-spaces": 2, - "no-restricted-imports": 2, - "no-return-assign": 2, - "no-self-assign": 2, - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow": 2, - "no-shadow-restricted-names": 2, - "no-spaced-func": 2, - "no-sparse-arrays": 2, - "no-this-before-super": 2, - "no-throw-literal": 2, - "no-undef": 2, - "no-unexpected-multiline": 2, - "no-unmodified-loop-condition": 2, - "no-unreachable": 2, - "no-unused-vars": [ - 1, - { - "vars": "all", - "args": "after-used" - } - ], - "no-use-before-define": [ - 2, - { - "functions": false - } - ], - "no-useless-call": 2, - "no-useless-computed-key": 2, - "no-useless-concat": 2, - "no-useless-rename": 2, - "no-var": 2, - "no-whitespace-before-property": 2, - "object-curly-newline": 0, - "object-curly-spacing": [ - 2, - "always" - ], - "object-property-newline": [ - 2, - { - "allowMultiplePropertiesPerLine": true - } - ], - "prefer-const": 2, - "prefer-rest-params": 2, - "prefer-spread": 2, - "prefer-template": 2, - "quotes": [ - 2, - "single", - { - "avoidEscape": true - } - ], - "semi": [ - 2, - "always" - ], - "semi-spacing": 2, - "space-before-blocks": 2, - "space-before-function-paren": [ - 2, - { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - } - ], - "space-in-parens": [ - 2, - "never" - ], - "spaced-comment": [ - 2, - "always" - ], - "strict": 2, - "valid-typeof": 2, - "eol-last": 0, - "yoda": 2, - "import/extensions": 2, - "import/no-duplicates": 2, - "jsx-quotes": [ - 2, - "prefer-single" - ], - "react/display-name": [ - 2, - { - "ignoreTranspilerName": false - } - ], - "react/forbid-prop-types": 0, - "react/jsx-boolean-value": 2, - "react/jsx-closing-bracket-location": 2, - "react/jsx-curly-spacing": 2, - "react/jsx-equals-spacing": 2, - "react/jsx-handler-names": 2, - "react/jsx-indent-props": [ - 2, - "tab" - ], - "react/jsx-indent": [ - 2, - "tab" - ], - "react/jsx-key": 2, - "react/jsx-max-props-per-line": 0, - "react/jsx-no-bind": 0, - "react/jsx-no-duplicate-props": 2, - "react/jsx-no-literals": 0, - "react/jsx-no-undef": 0, - "react/jsx-pascal-case": 2, - "react/jsx-sort-prop-types": 0, - "react/jsx-sort-props": 0, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - "react/no-danger": 2, - "react/no-deprecated": 2, - "react/no-did-mount-set-state": 2, - "react/no-did-update-set-state": 2, - "react/no-direct-mutation-state": 2, - "react/no-is-mounted": 2, - "react/no-multi-comp": 0, - "react/no-set-state": 0, - "react/no-string-refs": 0, - "react/no-unknown-property": 2, - "react/prefer-es6-class": 2, - "react/prop-types": [ - 2, - { - "skipUndeclared": true - } - ], - "react/react-in-jsx-scope": 2, - "react/self-closing-comp": 2, - "react/sort-comp": 0, - "react/jsx-wrap-multilines": [ - 2, - { - "declaration": false, - "assignment": false, - "return": true - } - ] - } - }, "browserslist": [ ">0.2%", "not dead", "not ie > 0", "not op_mini all" - ] + ], + "devDependencies": { + "eslint": "^6.5.1", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-react": "^7.16.0" + } } diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index aea8f94..b6c0ea6 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1,15 +1,30 @@ -import io from 'socket.io-client'; -import * as mediasoupClient from 'mediasoup-client'; -import WebTorrent from 'webtorrent'; -import createTorrent from 'create-torrent'; -import saveAs from 'file-saver'; +// import io from 'socket.io-client'; +// import * as mediasoupClient from 'mediasoup-client'; +// import WebTorrent from 'webtorrent'; +// import createTorrent from 'create-torrent'; +// import saveAs from 'file-saver'; import Logger from './Logger'; import hark from 'hark'; -import ScreenShare from './ScreenShare'; -import Spotlights from './Spotlights'; +// import ScreenShare from './ScreenShare'; +// import Spotlights from './Spotlights'; import { getSignalingUrl } from './urlFactory'; import * as requestActions from './actions/requestActions'; import * as stateActions from './actions/stateActions'; + +let WebTorrent; + +let createTorrent; + +let saveAs; + +let mediasoupClient; + +let io; + +let ScreenShare; + +let Spotlights; + const { turnServers, requestTimeout, @@ -77,31 +92,25 @@ export default class RoomClient } constructor( - { roomId, peerId, device, useSimulcast, produce, consume, forceTcp }) + { roomId, peerId, accessCode, device, useSimulcast, produce, forceTcp }) { logger.debug( - 'constructor() [roomId: "%s", peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", consume: "%s", forceTcp: "%s"]', - roomId, peerId, device.flag, useSimulcast, produce, consume, forceTcp); + 'constructor() [roomId: "%s", peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s"]', + roomId, peerId, device.flag, useSimulcast, produce, forceTcp); this._signalingUrl = getSignalingUrl(peerId, roomId); - // window element to external login site - this._loginWindow = null; - // Closed flag. this._closed = false; // Whether we should produce. this._produce = produce; - // Whether we should consume. - this._consume = consume; - // Wheter we force TCP this._forceTcp = forceTcp; // Torrent support - this._torrentSupport = WebTorrent.WEBRTC_SUPPORT; + this._torrentSupport = null; // Whether simulcast should be used. this._useSimulcast = useSimulcast; @@ -112,6 +121,9 @@ export default class RoomClient // My peer name. this._peerId = peerId; + // Access code + this._accessCode = accessCode; + // Alert sound this._soundAlert = new Audio('/sounds/notify.mp3'); @@ -120,21 +132,14 @@ export default class RoomClient // The room ID this._roomId = roomId; + store.dispatch(stateActions.setRoomName(roomId)); // mediasoup-client Device instance. // @type {mediasoupClient.Device} this._mediasoupDevice = null; - this._doneJoining = false; - // Our WebTorrent client - this._webTorrent = this._torrentSupport && new WebTorrent({ - tracker : { - rtcConfig : { - iceServers : ROOM_OPTIONS.turnServers - } - } - }); + this._webTorrent = null; // Max spotlights if (device.bowser.ios || device.bowser.mobile || device.bowser.android) @@ -170,7 +175,7 @@ export default class RoomClient // @type {Map} this._consumers = new Map(); - this._screenSharing = ScreenShare.create(device); + this._screenSharing = null; this._screenSharingProducer = null; @@ -198,6 +203,8 @@ export default class RoomClient this._recvTransport.close(); store.dispatch(stateActions.setRoomState('closed')); + + window.location = '/'; } _startKeyListener() @@ -305,19 +312,51 @@ export default class RoomClient login() { - const url = `/auth/login?roomId=${this._roomId}&peerId=${this._peerId}`; + const url = `/auth/login?id=${this._peerId}`; - this._loginWindow = window.open(url, 'loginWindow'); + window.open(url, 'loginWindow'); } logout() { - window.location = '/auth/logout'; + window.open('/auth/logout', 'logoutWindow'); } - closeLoginWindow() + receiveLoginChildWindow(data) { - this._loginWindow.close(); + logger.debug('receiveFromChildWindow() | [data:"%o"]', data); + + const { displayName, picture } = data; + + if (store.getState().room.state === 'connected') + { + this.changeDisplayName(displayName); + this.changePicture(picture); + } + else + { + store.dispatch(stateActions.setDisplayName(displayName)); + store.dispatch(stateActions.setPicture(picture)); + } + + store.dispatch(stateActions.loggedIn(true)); + + store.dispatch(requestActions.notify( + { + text : 'You are logged in.' + })); + } + + receiveLogoutChildWindow() + { + logger.debug('receiveLogoutChildWindow()'); + + store.dispatch(stateActions.loggedIn(false)); + + store.dispatch(requestActions.notify( + { + text : 'You are logged out.' + })); } _soundNotification() @@ -399,6 +438,12 @@ export default class RoomClient { logger.debug('changeDisplayName() [displayName:"%s"]', displayName); + if (!displayName) + displayName = 'Guest'; + + store.dispatch( + stateActions.setDisplayNameInProgress(true)); + try { await this.sendRequest('changeDisplayName', { displayName }); @@ -419,24 +464,23 @@ export default class RoomClient type : 'error', text : 'An error occured while changing your display name.' })); - - // We need to refresh the component for it to render the previous - // displayName again. - store.dispatch(stateActions.setDisplayName()); } + + store.dispatch( + stateActions.setDisplayNameInProgress(false)); } - async changeProfilePicture(picture) + async changePicture(picture) { - logger.debug('changeProfilePicture() [picture: "%s"]', picture); + logger.debug('changePicture() [picture: "%s"]', picture); try { - await this.sendRequest('changeProfilePicture', { picture }); + await this.sendRequest('changePicture', { picture }); } catch (error) { - logger.error('shareProfilePicure() | failed: %o', error); + logger.error('changePicture() | failed: %o', error); } } @@ -607,34 +651,46 @@ export default class RoomClient const { chatHistory, fileHistory, - lastN + lastNHistory, + locked, + lobbyPeers, + accessCode } = await this.sendRequest('serverHistory'); - if (chatHistory.length > 0) - { - logger.debug('Got chat history'); - store.dispatch( - stateActions.addChatHistory(chatHistory)); - } + (chatHistory.length > 0) && store.dispatch( + stateActions.addChatHistory(chatHistory)); - if (fileHistory.length > 0) - { - logger.debug('Got files history'); + (fileHistory.length > 0) && store.dispatch( + stateActions.addFileHistory(fileHistory)); - store.dispatch(stateActions.addFileHistory(fileHistory)); - } - - if (lastN.length > 0) + if (lastNHistory.length > 0) { - logger.debug('Got lastN'); + logger.debug('Got lastNHistory'); // Remove our self from list - const index = lastN.indexOf(this._peerId); + const index = lastNHistory.indexOf(this._peerId); - lastN.splice(index, 1); + lastNHistory.splice(index, 1); - this._spotlights.addSpeakerList(lastN); + this._spotlights.addSpeakerList(lastNHistory); } + + locked ? + store.dispatch(stateActions.setRoomLocked()) : + store.dispatch(stateActions.setRoomUnLocked()); + + (lobbyPeers.length > 0) && lobbyPeers.forEach((peer) => + { + store.dispatch( + stateActions.addLobbyPeer(peer.peerId)); + store.dispatch( + stateActions.setLobbyPeerDisplayName(peer.displayName)); + store.dispatch( + stateActions.setLobbyPeerPicture(peer.picture)); + }); + + (accessCode != null) && store.dispatch( + stateActions.setAccessCode(accessCode)); } catch (error) { @@ -735,6 +791,22 @@ export default class RoomClient } } + async getAudioTrack() + { + await navigator.mediaDevices.getUserMedia( + { + audio : true, video : false + }); + } + + async getVideoTrack() + { + await navigator.mediaDevices.getUserMedia( + { + audio : false, video : true + }); + } + async changeAudioDevice(deviceId) { logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); @@ -934,6 +1006,26 @@ export default class RoomClient stateActions.setSelectedPeer(peerId)); } + async promoteLobbyPeer(peerId) + { + logger.debug('promoteLobbyPeer() [peerId:"%s"]', peerId); + + store.dispatch( + stateActions.setLobbyPeerPromotionInProgress(peerId, true)); + + try + { + await this.sendRequest('promotePeer', { peerId }); + } + catch (error) + { + logger.error('promoteLobbyPeer() failed: %o', error); + } + + store.dispatch( + stateActions.setLobbyPeerPromotionInProgress(peerId, false)); + } + // type: mic/webcam/screen // mute: true/false async modifyPeerConsumer(peerId, type, mute) @@ -1061,13 +1153,78 @@ export default class RoomClient stateActions.setMyRaiseHandStateInProgress(false)); } + async _loadDynamicImports() + { + ({ default: WebTorrent } = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "webtorrent" */ + 'webtorrent' + )); + + ({ default: createTorrent } = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "create-torrent" */ + 'create-torrent' + )); + + ({ default: saveAs } = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "file-saver" */ + 'file-saver' + )); + + ({ default: ScreenShare } = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "screensharing" */ + './ScreenShare' + )); + + ({ default: Spotlights } = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "spotlights" */ + './Spotlights' + )); + + mediasoupClient = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "mediasoup" */ + 'mediasoup-client' + ); + + ({ default: io } = await import( + + /* webpackPrefetch: true */ + /* webpackChunkName: "socket.io" */ + 'socket.io-client' + )); + } + async join({ joinVideo }) { + await this._loadDynamicImports(); + + this._torrentSupport = WebTorrent.WEBRTC_SUPPORT; + + this._webTorrent = this._torrentSupport && new WebTorrent({ + tracker : { + rtcConfig : { + iceServers : ROOM_OPTIONS.turnServers + } + } + }); + + this._screenSharing = ScreenShare.create(this._device); + this._signalingSocket = io(this._signalingUrl); this._spotlights = new Spotlights(this._maxSpotlights, this._signalingSocket); - store.dispatch(stateActions.toggleJoined()); store.dispatch(stateActions.setRoomState('connecting')); this._signalingSocket.on('connect', () => @@ -1075,39 +1232,53 @@ export default class RoomClient logger.debug('signaling Peer "connect" event'); }); - this._signalingSocket.on('disconnect', () => + this._signalingSocket.on('disconnect', (reason) => { - logger.warn('signaling Peer "disconnect" event'); + logger.warn('signaling Peer "disconnect" event [reason:"%s"]', reason); + + if (this._closed) + return; + + if (reason === 'io server disconnect') + { + store.dispatch(requestActions.notify( + { + text : 'You are disconnected.' + })); + + this.close(); + } + + store.dispatch(requestActions.notify( + { + text : 'You are disconnected, attempting to reconnect.' + })); + + store.dispatch(stateActions.setRoomState('connecting')); + }); + + this._signalingSocket.on('reconnect_failed', () => + { + logger.warn('signaling Peer "reconnect_failed" event'); store.dispatch(requestActions.notify( { text : 'You are disconnected.' })); - // Close mediasoup Transports. - if (this._sendTransport) - { - this._sendTransport.close(); - this._sendTransport = null; - } - - if (this._recvTransport) - { - this._recvTransport.close(); - this._recvTransport = null; - } - - store.dispatch(stateActions.setRoomState('closed')); + this.close(); }); - this._signalingSocket.on('close', () => + this._signalingSocket.on('reconnect', (attemptNumber) => { - if (this._closed) - return; + logger.debug('signaling Peer "reconnect" event [attempts:"%s"]', attemptNumber); - logger.warn('signaling Peer "close" event'); + store.dispatch(requestActions.notify( + { + text : 'You are reconnected.' + })); - this.close(); + store.dispatch(stateActions.setRoomState('connected')); }); this._signalingSocket.on('request', async (request, cb) => @@ -1241,263 +1412,388 @@ export default class RoomClient 'socket "notification" event [method:%s, data:%o]', notification.method, notification.data); - switch (notification.method) + try { - case 'roomReady': + switch (notification.method) { - await this._joinRoom({ joinVideo }); - - break; - } - - case 'roomLocked': - { - store.dispatch(stateActions.setRoomLockedOut()); - - break; - } - - case 'lockRoom': - { - store.dispatch( - stateActions.setRoomLocked()); - - store.dispatch(requestActions.notify( - { - text : 'Room is now locked.' - })); - - break; - } - - case 'unlockRoom': - { - store.dispatch( - stateActions.setRoomUnLocked()); - - store.dispatch(requestActions.notify( - { - text : 'Room is now unlocked.' - })); - - break; - } - - case 'activeSpeaker': - { - const { peerId } = notification.data; - - store.dispatch( - stateActions.setRoomActiveSpeaker(peerId)); - - if (peerId && peerId !== this._peerId) - this._spotlights.handleActiveSpeaker(peerId); - - break; - } - - case 'changeDisplayName': - { - const { peerId, displayName, oldDisplayName } = notification.data; - - store.dispatch( - stateActions.setPeerDisplayName(displayName, peerId)); - - store.dispatch(requestActions.notify( - { - text : `${oldDisplayName} is now ${displayName}` - })); - - break; - } - - case 'changeProfilePicture': - { - const { peerId, picture } = notification.data; - - store.dispatch(stateActions.setPeerPicture(peerId, picture)); - - break; - } - - case 'auth': - { - const { displayName, picture } = notification.data; - - this.changeDisplayName(displayName); - - this.changeProfilePicture(picture); - store.dispatch(stateActions.setPicture(picture)); - store.dispatch(stateActions.loggedIn()); - - store.dispatch(requestActions.notify( - { - text : 'You are logged in.' - })); - - this.closeLoginWindow(); - - break; - } - - case 'chatMessage': - { - const { peerId, chatMessage } = notification.data; - - store.dispatch( - stateActions.addResponseMessage({ ...chatMessage, peerId })); - - if ( - !store.getState().toolarea.toolAreaOpen || - (store.getState().toolarea.toolAreaOpen && - store.getState().toolarea.currentToolTab !== 'chat') - ) // Make sound + case 'enteredLobby': { - this._soundNotification(); + store.dispatch(stateActions.setInLobby(true)); + + const { displayName } = store.getState().settings; + const { picture } = store.getState().me; + + await this.sendRequest('changeDisplayName', { displayName }); + await this.sendRequest('changePicture', { picture }); + break; } - break; - } - - case 'sendFile': - { - const { peerId, magnetUri } = notification.data; - - store.dispatch(stateActions.addFile(peerId, magnetUri)); - - store.dispatch(requestActions.notify( - { - text : 'New file available.' - })); - - if ( - !store.getState().toolarea.toolAreaOpen || - (store.getState().toolarea.toolAreaOpen && - store.getState().toolarea.currentToolTab !== 'files') - ) // Make sound + case 'signInRequired': { - this._soundNotification(); + store.dispatch(stateActions.setSignInRequired(true)); + + break; + } + + case 'roomReady': + { + store.dispatch(stateActions.toggleJoined()); + store.dispatch(stateActions.setInLobby(false)); + + await this._joinRoom({ joinVideo }); + + break; + } + + case 'lockRoom': + { + store.dispatch( + stateActions.setRoomLocked()); + + store.dispatch(requestActions.notify( + { + text : 'Room is now locked.' + })); + + break; + } + + case 'unlockRoom': + { + store.dispatch( + stateActions.setRoomUnLocked()); + + store.dispatch(requestActions.notify( + { + text : 'Room is now unlocked.' + })); + + break; + } + + case 'parkedPeer': + { + const { peerId } = notification.data; + + store.dispatch( + stateActions.addLobbyPeer(peerId)); + store.dispatch( + stateActions.setToolbarsVisible(true)); + + store.dispatch(requestActions.notify( + { + text : 'New participant entered the lobby.' + })); + + break; + } + + case 'lobby:peerClosed': + { + const { peerId } = notification.data; + + store.dispatch( + stateActions.removeLobbyPeer(peerId)); + + store.dispatch(requestActions.notify( + { + text : 'Participant in lobby left.' + })); + + break; + } + + case 'lobby:promotedPeer': + { + const { peerId } = notification.data; + + store.dispatch( + stateActions.removeLobbyPeer(peerId)); + + break; + } + + case 'lobby:changeDisplayName': + { + const { peerId, displayName } = notification.data; + + store.dispatch( + stateActions.setLobbyPeerDisplayName(displayName, peerId)); + + store.dispatch(requestActions.notify( + { + text : `Participant in lobby changed name to ${displayName}.` + })); + + break; + } + + case 'lobby:changePicture': + { + const { peerId, picture } = notification.data; + + store.dispatch( + stateActions.setLobbyPeerPicture(picture, peerId)); + + store.dispatch(requestActions.notify( + { + text : 'Participant in lobby changed picture.' + })); + + break; } - break; - } - - case 'producerScore': - { - const { producerId, score } = notification.data; - - store.dispatch( - stateActions.setProducerScore(producerId, score)); - - break; - } - - case 'newPeer': - { - const { id, displayName, picture, device } = notification.data; - - store.dispatch( - stateActions.addPeer({ id, displayName, picture, device, consumers: [] })); - - store.dispatch(requestActions.notify( + case 'setAccessCode': + { + const { accessCode } = notification.data; + + store.dispatch( + stateActions.setAccessCode(accessCode)); + + store.dispatch(requestActions.notify( + { + text : 'Access code for room updated' + })); + + break; + } + + case 'setJoinByAccessCode': + { + const { joinByAccessCode } = notification.data; + + store.dispatch( + stateActions.setJoinByAccessCode(joinByAccessCode)); + + if (joinByAccessCode) { - text : `${displayName} joined the room.` - })); - - break; - } - - case 'peerClosed': - { - const { peerId } = notification.data; - - store.dispatch( - stateActions.removePeer(peerId)); - - break; - } - - case 'consumerClosed': - { - const { consumerId } = notification.data; - const consumer = this._consumers.get(consumerId); - - if (!consumer) + store.dispatch(requestActions.notify( + { + text : 'Access code for room is now activated' + })); + } + else + { + store.dispatch(requestActions.notify( + { + text : 'Access code for room is now deactivated' + })); + } + break; - - consumer.close(); - - if (consumer.hark != null) - consumer.hark.stop(); - - this._consumers.delete(consumerId); - - const { peerId } = consumer.appData; - - store.dispatch( - stateActions.removeConsumer(consumerId, peerId)); - - break; - } - - case 'consumerPaused': - { - const { consumerId } = notification.data; - const consumer = this._consumers.get(consumerId); - - if (!consumer) + } + + case 'activeSpeaker': + { + const { peerId } = notification.data; + + store.dispatch( + stateActions.setRoomActiveSpeaker(peerId)); + + if (peerId && peerId !== this._peerId) + this._spotlights.handleActiveSpeaker(peerId); + break; - - store.dispatch( - stateActions.setConsumerPaused(consumerId, 'remote')); - - break; - } - - case 'consumerResumed': - { - const { consumerId } = notification.data; - const consumer = this._consumers.get(consumerId); - - if (!consumer) + } + + case 'changeDisplayName': + { + const { peerId, displayName, oldDisplayName } = notification.data; + + store.dispatch( + stateActions.setPeerDisplayName(displayName, peerId)); + + store.dispatch(requestActions.notify( + { + text : `${oldDisplayName} is now ${displayName}` + })); + break; - - store.dispatch( - stateActions.setConsumerResumed(consumerId, 'remote')); - - break; - } - - case 'consumerLayersChanged': - { - const { consumerId, spatialLayer, temporalLayer } = notification.data; - const consumer = this._consumers.get(consumerId); - - if (!consumer) + } + + case 'changePicture': + { + const { peerId, picture } = notification.data; + + store.dispatch(stateActions.setPeerPicture(peerId, picture)); + break; - - store.dispatch(stateActions.setConsumerCurrentLayers( - consumerId, spatialLayer, temporalLayer)); - - break; - } - - case 'consumerScore': - { - const { consumerId, score } = notification.data; - - store.dispatch( - stateActions.setConsumerScore(consumerId, score)); - - break; - } - - default: - { - logger.error( - 'unknown notification.method "%s"', notification.method); + } + + case 'chatMessage': + { + const { peerId, chatMessage } = notification.data; + + store.dispatch( + stateActions.addResponseMessage({ ...chatMessage, peerId })); + + if ( + !store.getState().toolarea.toolAreaOpen || + (store.getState().toolarea.toolAreaOpen && + store.getState().toolarea.currentToolTab !== 'chat') + ) // Make sound + { + store.dispatch( + stateActions.setToolbarsVisible(true)); + this._soundNotification(); + } + + break; + } + + case 'sendFile': + { + const { peerId, magnetUri } = notification.data; + + store.dispatch(stateActions.addFile(peerId, magnetUri)); + + store.dispatch(requestActions.notify( + { + text : 'New file available.' + })); + + if ( + !store.getState().toolarea.toolAreaOpen || + (store.getState().toolarea.toolAreaOpen && + store.getState().toolarea.currentToolTab !== 'files') + ) // Make sound + { + store.dispatch( + stateActions.setToolbarsVisible(true)); + this._soundNotification(); + } + + break; + } + + case 'producerScore': + { + const { producerId, score } = notification.data; + + store.dispatch( + stateActions.setProducerScore(producerId, score)); + + break; + } + + case 'newPeer': + { + const { id, displayName, picture, device } = notification.data; + + store.dispatch( + stateActions.addPeer({ id, displayName, picture, device, consumers: [] })); + + store.dispatch(requestActions.notify( + { + text : `${displayName} joined the room.` + })); + + break; + } + + case 'peerClosed': + { + const { peerId } = notification.data; + + store.dispatch( + stateActions.removePeer(peerId)); + + break; + } + + case 'consumerClosed': + { + const { consumerId } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + consumer.close(); + + if (consumer.hark != null) + consumer.hark.stop(); + + this._consumers.delete(consumerId); + + const { peerId } = consumer.appData; + + store.dispatch( + stateActions.removeConsumer(consumerId, peerId)); + + break; + } + + case 'consumerPaused': + { + const { consumerId } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + store.dispatch( + stateActions.setConsumerPaused(consumerId, 'remote')); + + break; + } + + case 'consumerResumed': + { + const { consumerId } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + store.dispatch( + stateActions.setConsumerResumed(consumerId, 'remote')); + + break; + } + + case 'consumerLayersChanged': + { + const { consumerId, spatialLayer, temporalLayer } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + store.dispatch(stateActions.setConsumerCurrentLayers( + consumerId, spatialLayer, temporalLayer)); + + break; + } + + case 'consumerScore': + { + const { consumerId, score } = notification.data; + + store.dispatch( + stateActions.setConsumerScore(consumerId, score)); + + break; + } + + default: + { + logger.error( + 'unknown notification.method "%s"', notification.method); + } } } + catch (error) + { + logger.error('error on socket "notification" event failed:"%o"', error); + + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Error on server request.' + })); + } + }); } @@ -1573,44 +1869,41 @@ export default class RoomClient }); } - if (this._consume) - { - const transportInfo = await this.sendRequest( - 'createWebRtcTransport', - { - forceTcp : this._forceTcp, - producing : false, - consuming : true - }); + const transportInfo = await this.sendRequest( + 'createWebRtcTransport', + { + forceTcp : this._forceTcp, + producing : false, + consuming : true + }); - const { + const { + id, + iceParameters, + iceCandidates, + dtlsParameters + } = transportInfo; + + this._recvTransport = this._mediasoupDevice.createRecvTransport( + { id, iceParameters, iceCandidates, dtlsParameters - } = transportInfo; + }); - this._recvTransport = this._mediasoupDevice.createRecvTransport( - { - id, - iceParameters, - iceCandidates, - dtlsParameters - }); - - this._recvTransport.on( - 'connect', ({ dtlsParameters }, callback, errback) => // eslint-disable-line no-shadow - { - this.sendRequest( - 'connectWebRtcTransport', - { - transportId : this._recvTransport.id, - dtlsParameters - }) - .then(callback) - .catch(errback); - }); - } + this._recvTransport.on( + 'connect', ({ dtlsParameters }, callback, errback) => // eslint-disable-line no-shadow + { + this.sendRequest( + 'connectWebRtcTransport', + { + transportId : this._recvTransport.id, + dtlsParameters + }) + .then(callback) + .catch(errback); + }); // Set our media capabilities. store.dispatch(stateActions.setMediaCapabilities( @@ -1628,11 +1921,11 @@ export default class RoomClient displayName : displayName, picture : picture, device : this._device, - rtpCapabilities : this._consume - ? this._mediasoupDevice.rtpCapabilities - : undefined + rtpCapabilities : this._mediasoupDevice.rtpCapabilities }); - + + logger.debug('_joinRoom() joined, got peers [peers:"%o"]', peers); + for (const peer of peers) { store.dispatch( @@ -1741,6 +2034,60 @@ export default class RoomClient } } + async setAccessCode(code) + { + logger.debug('setAccessCode()'); + + try + { + await this.sendRequest('setAccessCode', { accessCode: code }); + + store.dispatch( + stateActions.setAccessCode(code)); + + store.dispatch(requestActions.notify( + { + text : 'Access code saved.' + })); + } + catch (error) + { + logger.error('setAccessCode() | failed: %o', error); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to set access code.' + })); + } + } + + async setJoinByAccessCode(value) + { + logger.debug('setJoinByAccessCode()'); + + try + { + await this.sendRequest('setJoinByAccessCode', { joinByAccessCode: value }); + + store.dispatch( + stateActions.setJoinByAccessCode(value)); + + store.dispatch(requestActions.notify( + { + text : `You switched Join by access-code to ${value}` + })); + } + catch (error) + { + logger.error('setAccessCode() | failed: %o', error); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to set join by access code.' + })); + } + } + async enableMic() { if (this._micProducer) @@ -2278,7 +2625,7 @@ export default class RoomClient try { - logger.debug('_getAudioDeviceId() | calling _updateWebcams()'); + logger.debug('_getAudioDeviceId() | calling _updateAudioDeviceId()'); await this._updateAudioDevices(); diff --git a/app/src/actions/stateActions.js b/app/src/actions/stateActions.js index fda3ceb..89bc05a 100644 --- a/app/src/actions/stateActions.js +++ b/app/src/actions/stateActions.js @@ -6,6 +6,14 @@ export const setRoomUrl = (url) => }; }; +export const setRoomName = (name) => +{ + return { + type : 'SET_ROOM_NAME', + payload : { name } + }; +}; + export const setRoomState = (state) => { return { @@ -36,10 +44,35 @@ export const setRoomUnLocked = () => }; }; -export const setRoomLockedOut = () => +export const setInLobby = (inLobby) => { return { - type : 'SET_ROOM_LOCKED_OUT' + type : 'SET_IN_LOBBY', + payload : { inLobby } + }; +}; + +export const setSignInRequired = (signInRequired) => +{ + return { + type : 'SET_SIGN_IN_REQUIRED', + payload : { signInRequired } + }; +}; + +export const setAccessCode = (accessCode) => +{ + return { + type : 'SET_ACCESS_CODE', + payload : { accessCode } + }; +}; + +export const setJoinByAccessCode = (joinByAccessCode) => +{ + return { + type : 'SET_JOIN_BY_ACCESS_CODE', + payload : { joinByAccessCode } }; }; @@ -49,6 +82,12 @@ export const setSettingsOpen = ({ settingsOpen }) => payload : { settingsOpen } }); +export const setLockDialogOpen = ({ lockDialogOpen }) => + ({ + type : 'SET_LOCK_DIALOG_OPEN', + payload : { lockDialogOpen } + }); + export const setMe = ({ peerId, device, loginEnabled }) => { return { @@ -126,6 +165,14 @@ export const setDisplayName = (displayName) => }; }; +export const setDisplayNameInProgress = (flag) => +{ + return { + type : 'SET_DISPLAY_NAME_IN_PROGRESS', + payload : { flag } + }; +}; + export const toggleAdvancedMode = () => { return { @@ -178,6 +225,13 @@ export const toggleSettings = () => }; }; +export const toggleLockDialog = () => +{ + return { + type : 'TOGGLE_LOCK_DIALOG' + }; +}; + export const toggleToolArea = () => { return { @@ -391,6 +445,46 @@ export const setPeerVolume = (peerId, volume) => }; }; +export const addLobbyPeer = (peerId) => +{ + return { + type : 'ADD_LOBBY_PEER', + payload : { peerId } + }; +}; + +export const removeLobbyPeer = (peerId) => +{ + return { + type : 'REMOVE_LOBBY_PEER', + payload : { peerId } + }; +}; + +export const setLobbyPeerDisplayName = (displayName, peerId) => +{ + return { + type : 'SET_LOBBY_PEER_DISPLAY_NAME', + payload : { displayName, peerId } + }; +}; + +export const setLobbyPeerPicture = (picture, peerId) => +{ + return { + type : 'SET_LOBBY_PEER_PICTURE', + payload : { picture, peerId } + }; +}; + +export const setLobbyPeerPromotionInProgress = (peerId, flag) => +{ + return { + type : 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS', + payload : { peerId, flag } + }; +}; + export const addNotification = (notification) => { return { @@ -555,9 +649,10 @@ export const setPeerPicture = (peerId, picture) => payload : { peerId, picture } }); -export const loggedIn = () => +export const loggedIn = (flag) => ({ - type : 'LOGGED_IN' + type : 'LOGGED_IN', + payload : { flag } }); export const toggleJoined = () => diff --git a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js new file mode 100644 index 0000000..ba4adde --- /dev/null +++ b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js @@ -0,0 +1,148 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/core/styles'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { withRoomContext } from '../../../RoomContext'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import Avatar from '@material-ui/core/Avatar'; +import EmptyAvatar from '../../../images/avatar-empty.jpeg'; +import PromoteIcon from '@material-ui/icons/OpenInBrowser'; +import Tooltip from '@material-ui/core/Tooltip'; + +const styles = (theme) => + ({ + root : + { + padding : theme.spacing(1), + width : '100%', + overflow : 'hidden', + cursor : 'auto', + display : 'flex' + }, + avatar : + { + borderRadius : '50%', + height : '2rem' + }, + peerInfo : + { + fontSize : '1rem', + border : 'none', + display : 'flex', + paddingLeft : theme.spacing(1), + flexGrow : 1, + alignItems : 'center' + }, + controls : + { + float : 'right', + display : 'flex', + flexDirection : 'row', + justifyContent : 'flex-start', + alignItems : 'center' + }, + button : + { + flex : '0 0 auto', + margin : '0.3rem', + borderRadius : 2, + backgroundColor : 'rgba(0, 0, 0, 0.5)', + cursor : 'pointer', + transitionProperty : 'opacity, background-color', + transitionDuration : '0.15s', + width : 'var(--media-control-button-size)', + height : 'var(--media-control-button-size)', + opacity : 0.85, + '&:hover' : + { + opacity : 1 + }, + '&.disabled' : + { + pointerEvents : 'none', + backgroundColor : 'var(--media-control-botton-disabled)' + }, + '&.promote' : + { + backgroundColor : 'var(--media-control-botton-on)' + } + }, + ListItem : + { + alignItems : 'center' + } + }); + +const ListLobbyPeer = (props) => +{ + const { + roomClient, + peer, + classes + } = props; + + const picture = peer.picture || EmptyAvatar; + + return ( + + + + + + + + { + e.stopPropagation(); + roomClient.promoteLobbyPeer(peer.id); + }} + > + + + + + ); +}; + +ListLobbyPeer.propTypes = +{ + roomClient : PropTypes.any.isRequired, + advancedMode : PropTypes.bool, + peer : PropTypes.object.isRequired, + classes : PropTypes.object.isRequired +}; + +const mapStateToProps = (state, { id }) => +{ + return { + peer : state.lobbyPeers[id] + }; +}; + +export default withRoomContext(connect( + mapStateToProps, + null, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.lobbyPeers === next.lobbyPeers + ); + } + } +)(withStyles(styles)(ListLobbyPeer))); \ No newline at end of file diff --git a/app/src/components/AccessControl/LockDialog/LockDialog.js b/app/src/components/AccessControl/LockDialog/LockDialog.js new file mode 100644 index 0000000..01c6c35 --- /dev/null +++ b/app/src/components/AccessControl/LockDialog/LockDialog.js @@ -0,0 +1,200 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { + lobbyPeersKeySelector +} from '../../Selectors'; +import * as appPropTypes from '../../appPropTypes'; +import { withStyles } from '@material-ui/core/styles'; +import { withRoomContext } from '../../../RoomContext'; +import * as stateActions from '../../../actions/stateActions'; +import PropTypes from 'prop-types'; +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import Button from '@material-ui/core/Button'; +// import FormLabel from '@material-ui/core/FormLabel'; +// import FormControl from '@material-ui/core/FormControl'; +// import FormGroup from '@material-ui/core/FormGroup'; +// import FormControlLabel from '@material-ui/core/FormControlLabel'; +// import Checkbox from '@material-ui/core/Checkbox'; +// import InputLabel from '@material-ui/core/InputLabel'; +// import OutlinedInput from '@material-ui/core/OutlinedInput'; +// import Switch from '@material-ui/core/Switch'; +import List from '@material-ui/core/List'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import ListLobbyPeer from './ListLobbyPeer'; + +const styles = (theme) => + ({ + root : + { + }, + dialogPaper : + { + width : '30vw', + [theme.breakpoints.down('lg')] : + { + width : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : '90vw' + } + }, + lock : + { + padding : theme.spacing(2) + } + }); + +const LockDialog = ({ + // roomClient, + room, + handleCloseLockDialog, + // handleAccessCode, + lobbyPeers, + classes +}) => +{ + return ( + handleCloseLockDialog({ lockDialogOpen: false })} + classes={{ + paper : classes.dialogPaper + }} + > + Lobby administration + {/* + + Room lock + + + { + if (room.locked) + { + roomClient.unlockRoom(); + } + else + { + roomClient.lockRoom(); + } + }} + />} + label='Lock' + /> + TODO: access code + roomClient.setJoinByAccessCode(event.target.checked) + } + />} + label='Join by Access code' + /> + + handleAccessCode(event.target.value)} + > + + + + + + */} + { lobbyPeers.length > 0 ? + + Participants in Lobby + + } + > + { + lobbyPeers.map((peerId) => + { + return (); + }) + } + + : + + + There are currently no one in the lobby. + + + } + + + + + ); +}; + +LockDialog.propTypes = +{ + // roomClient : PropTypes.any.isRequired, + room : appPropTypes.Room.isRequired, + handleCloseLockDialog : PropTypes.func.isRequired, + handleAccessCode : PropTypes.func.isRequired, + lobbyPeers : PropTypes.array, + classes : PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => +{ + return { + room : state.room, + lobbyPeers : lobbyPeersKeySelector(state) + }; +}; + +const mapDispatchToProps = { + handleCloseLockDialog : stateActions.setLockDialogOpen, + handleAccessCode : stateActions.setAccessCode +}; + +export default withRoomContext(connect( + mapStateToProps, + mapDispatchToProps, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.room.locked === next.room.locked && + prev.room.joinByAccessCode === next.room.joinByAccessCode && + prev.room.accessCode === next.room.accessCode && + prev.room.code === next.room.code && + prev.room.lockDialogOpen === next.room.lockDialogOpen && + prev.room.codeHidden === next.room.codeHidden && + prev.lobbyPeers === next.lobbyPeers + ); + } + } +)(withStyles(styles)(LockDialog))); \ No newline at end of file diff --git a/app/src/components/App.js b/app/src/components/App.js new file mode 100644 index 0000000..e02aff7 --- /dev/null +++ b/app/src/components/App.js @@ -0,0 +1,61 @@ +import React, { useEffect, Suspense } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import JoinDialog from './JoinDialog'; +import LoadingView from './LoadingView'; +import { ReactLazyPreload } from './ReactLazyPreload'; + +const Room = ReactLazyPreload(() => import(/* webpackChunkName: "room" */ './Room')); + +const App = (props) => +{ + const { + room + } = props; + + useEffect(() => + { + Room.preload(); + + return; + }, []); + + if (!room.joined) + { + return ( + + ); + } + else + { + return ( + }> + + + ); + } +}; + +App.propTypes = +{ + room : PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => + ({ + room : state.room + }); + +export default connect( + mapStateToProps, + null, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.room === next.room + ); + } + } +)(App); \ No newline at end of file diff --git a/app/src/components/Containers/HiddenPeers.js b/app/src/components/Containers/HiddenPeers.js index 7d155d0..4afb943 100644 --- a/app/src/components/Containers/HiddenPeers.js +++ b/app/src/components/Containers/HiddenPeers.js @@ -96,7 +96,7 @@ class HiddenPeers extends React.PureComponent onClick={() => openUsersTab()} >

+{hiddenPeersCount}
participant - {(hiddenPeersCount === 1) ? null : 's'} + {(hiddenPeersCount > 1) && 's'}

); diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 8d3e465..2ff2adf 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -332,13 +332,11 @@ const Me = (props) => } }} > - { screenState === 'on' || screenState === 'unsupported' ? + { (screenState === 'on' || screenState === 'unsupported') && - :null } - { screenState === 'off' ? + { screenState === 'off' && - :null } @@ -351,10 +349,10 @@ const Me = (props) => peer={me} displayName={settings.displayName} showPeerInfo - videoTrack={webcamProducer ? webcamProducer.track : null} + videoTrack={webcamProducer && webcamProducer.track} videoVisible={videoVisible} - audioCodec={micProducer ? micProducer.codec : null} - videoCodec={webcamProducer ? webcamProducer.codec : null} + audioCodec={micProducer && micProducer.codec} + videoCodec={webcamProducer && webcamProducer.codec} onChangeDisplayName={(displayName) => { roomClient.changeDisplayName(displayName); @@ -364,9 +362,9 @@ const Me = (props) => - { screenProducer ? + { screenProducer &&
setHover(true)} onMouseOut={() => setHover(false)} onTouchStart={() => @@ -390,7 +388,7 @@ const Me = (props) => >
setHover(true)} onMouseOut={() => setHover(false)} onTouchStart={() => @@ -420,13 +418,12 @@ const Me = (props) => isScreen advancedMode={advancedMode} videoContain - videoTrack={screenProducer ? screenProducer.track : null} + videoTrack={screenProducer && screenProducer.track} videoVisible={screenVisible} - videoCodec={screenProducer ? screenProducer.codec : null} + videoCodec={screenProducer && screenProducer.codec} />
- :null } ); diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index ee7fd2e..4abb38f 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -166,8 +166,8 @@ const Peer = (props) => classnames( classes.root, 'webcam', - hover ? 'hover' : null, - activeSpeaker ? 'active-speaker' : null + hover && 'hover', + activeSpeaker && 'active-speaker' ) } onMouseOver={() => setHover(true)} @@ -192,15 +192,14 @@ const Peer = (props) => style={rootStyle} >
- { !videoVisible ? + { !videoVisible &&

this video is paused

- :null }
setHover(true)} onMouseOut={() => setHover(false)} onTouchStart={() => @@ -241,7 +240,7 @@ const Peer = (props) => } - { !smallScreen ? + { !smallScreen && > - :null } peer={peer} displayName={peer.displayName} showPeerInfo - videoTrack={webcamConsumer ? webcamConsumer.track : null} + videoTrack={webcamConsumer && webcamConsumer.track} videoVisible={videoVisible} videoProfile={videoProfile} - audioCodec={micConsumer ? micConsumer.codec : null} - videoCodec={webcamConsumer ? webcamConsumer.codec : null} + audioCodec={micConsumer && micConsumer.codec} + videoCodec={webcamConsumer && webcamConsumer.codec} >
- { screenConsumer ? + { screenConsumer &&
setHover(true)} onMouseOut={() => setHover(false)} onTouchStart={() => @@ -314,17 +312,16 @@ const Peer = (props) => }} style={rootStyle} > - { !screenVisible ? + { !screenVisible &&

this video is paused

- :null } - { screenVisible ? + { screenVisible &&
setHover(true)} onMouseOut={() => setHover(false)} onTouchStart={() => @@ -346,7 +343,7 @@ const Peer = (props) => }, 2000); }} > - { !smallScreen ? + { !smallScreen && > - :null }
- :null }
- :null } ); diff --git a/app/src/components/Containers/SpeakerPeer.js b/app/src/components/Containers/SpeakerPeer.js index 85aeb0b..c31baca 100644 --- a/app/src/components/Containers/SpeakerPeer.js +++ b/app/src/components/Containers/SpeakerPeer.js @@ -117,11 +117,10 @@ const SpeakerPeer = (props) => style={spacingStyle} >
- { !videoVisible ? + { !videoVisible &&

this video is paused

- :null }
- { screenConsumer ? + { screenConsumer &&
- { !screenVisible ? + { !screenVisible &&

this video is paused

- :null } - { screenVisible ? + { screenVisible &&
videoCodec={screenConsumer ? screenConsumer.codec : null} />
- :null }
- :null } ); diff --git a/app/src/components/JoinDialog.js b/app/src/components/JoinDialog.js index ae11771..b0f10c4 100644 --- a/app/src/components/JoinDialog.js +++ b/app/src/components/JoinDialog.js @@ -1,90 +1,355 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../RoomContext'; +import * as stateActions from '../actions/stateActions'; import PropTypes from 'prop-types'; import Dialog from '@material-ui/core/Dialog'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import IconButton from '@material-ui/core/IconButton'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import Avatar from '@material-ui/core/Avatar'; import Typography from '@material-ui/core/Typography'; -import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Tooltip from '@material-ui/core/Tooltip'; +import CookieConsent from 'react-cookie-consent'; +import MuiDialogTitle from '@material-ui/core/DialogTitle'; +import MuiDialogContent from '@material-ui/core/DialogContent'; +import MuiDialogActions from '@material-ui/core/DialogActions'; const styles = (theme) => ({ root : + { + display : 'flex', + width : '100%', + height : '100%', + backgroundColor : 'var(--background-color)', + backgroundImage : `url(${window.config.background})`, + backgroundAttachment : 'fixed', + backgroundPosition : 'center', + backgroundSize : 'cover', + backgroundRepeat : 'no-repeat' + }, + dialogTitle : { }, dialogPaper : { - width : '20vw', + width : '30vw', padding : theme.spacing(2), [theme.breakpoints.down('lg')] : { - width : '30vw' + width : '40vw' }, [theme.breakpoints.down('md')] : { - width : '40vw' + width : '50vw' }, [theme.breakpoints.down('sm')] : { - width : '60vw' + width : '70vw' }, [theme.breakpoints.down('xs')] : { - width : '80vw' + width : '90vw' } }, logo : { - display : 'block' + display : 'block', + paddingBottom : '1vh' + }, + loginButton : + { + position : 'absolute', + right : theme.spacing(2), + top : theme.spacing(2), + padding : 0 + }, + largeIcon : + { + fontSize : '2em' + }, + largeAvatar : + { + width : 50, + height : 50 + }, + green : + { + color : 'rgba(0, 153, 0, 1)' } }); +const DialogTitle = withStyles(styles)((props) => +{ + const [ open, setOpen ] = useState(false); + + useEffect(() => + { + const openTimer = setTimeout(() => setOpen(true), 1000); + const closeTimer = setTimeout(() => setOpen(false), 4000); + + return () => + { + clearTimeout(openTimer); + clearTimeout(closeTimer); + }; + }, []); + + const { children, classes, myPicture, onLogin, ...other } = props; + + const handleTooltipClose = () => + { + setOpen(false); + }; + + const handleTooltipOpen = () => + { + setOpen(true); + }; + + return ( + + { window.config.logo && Logo } + {children} + { window.config.loginEnabled && + + + { myPicture ? + + : + + } + + + } + + ); +}); + +const DialogContent = withStyles((theme) => ({ + root : + { + padding : theme.spacing(2) + } +}))(MuiDialogContent); + +const DialogActions = withStyles((theme) => ({ + root : + { + margin : 0, + padding : theme.spacing(1) + } +}))(MuiDialogActions); + const JoinDialog = ({ roomClient, + room, + displayName, + displayNameInProgress, + loggedIn, + myPicture, + changeDisplayName, classes }) => { - return ( - - { window.config.logo ? - Logo - :null + const handleKeyDown = (event) => + { + const { key } = event; + + switch (key) + { + case 'Enter': + case 'Escape': + { + if (displayName === '') + changeDisplayName('Guest'); + if (room.inLobby) + roomClient.changeDisplayName(displayName); + break; } - You are about to join a meeting, how would you like to join? - - - - - + { window.config.title } +
+ + + + You are about to join a meeting. + + + + Room ID: { room.name } + + + + Set your name for participation, + and choose how you want to join: + + + + { + const { value } = event.target; + + changeDisplayName(value); + }} + onKeyDown={handleKeyDown} + onBlur={() => + { + if (displayName === '') + changeDisplayName('Guest'); + if (room.inLobby) + roomClient.changeDisplayName(displayName); + }} + fullWidth + /> + + + + { !room.inLobby ? + + + + + : + + + Ok, you are ready + + { room.signInRequired ? + + The room is empty! + You can Log In to start the meeting or wait until the host joins. + + : + + The room is locked - hang on until somebody lets you in ... + + } + + } + + + This website uses cookies to enhance the user experience. + + +
); }; JoinDialog.propTypes = { - roomClient : PropTypes.any.isRequired, - classes : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + room : PropTypes.object.isRequired, + displayName : PropTypes.string.isRequired, + displayNameInProgress : PropTypes.bool.isRequired, + loginEnabled : PropTypes.bool.isRequired, + loggedIn : PropTypes.bool.isRequired, + myPicture : PropTypes.string, + changeDisplayName : PropTypes.func.isRequired, + classes : PropTypes.object.isRequired }; -export default withRoomContext(withStyles(styles)(JoinDialog)); \ No newline at end of file +const mapStateToProps = (state) => +{ + return { + room : state.room, + displayName : state.settings.displayName, + displayNameInProgress : state.me.displayNameInProgress, + loginEnabled : state.me.loginEnabled, + loggedIn : state.me.loggedIn, + myPicture : state.me.picture + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + changeDisplayName : (displayName) => + { + dispatch(stateActions.setDisplayName(displayName)); + } + }; +}; + +export default withRoomContext(connect( + mapStateToProps, + mapDispatchToProps, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.room.inLobby === next.room.inLobby && + prev.room.signInRequired === next.room.signInRequired && + prev.settings.displayName === next.settings.displayName && + prev.me.displayNameInProgress === next.me.displayNameInProgress && + prev.me.loginEnabled === next.me.loginEnabled && + prev.me.loggedIn === next.me.loggedIn && + prev.me.picture === next.me.picture + ); + } + } +)(withStyles(styles)(JoinDialog))); \ No newline at end of file diff --git a/app/src/components/MeetingDrawer/Chat/ChatInput.js b/app/src/components/MeetingDrawer/Chat/ChatInput.js index bafaa20..0f0235d 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatInput.js +++ b/app/src/components/MeetingDrawer/Chat/ChatInput.js @@ -123,7 +123,7 @@ ChatInput.propTypes = const mapStateToProps = (state) => ({ displayName : state.settings.displayName, - picture : state.settings.picture + picture : state.me.picture }); export default withRoomContext( @@ -136,7 +136,7 @@ export default withRoomContext( { return ( prev.settings.displayName === next.settings.displayName && - prev.settings.picture === next.settings.picture + prev.me.picture === next.me.picture ); } } diff --git a/app/src/components/MeetingDrawer/Chat/MessageList.js b/app/src/components/MeetingDrawer/Chat/MessageList.js index cdc8809..a5bc154 100644 --- a/app/src/components/MeetingDrawer/Chat/MessageList.js +++ b/app/src/components/MeetingDrawer/Chat/MessageList.js @@ -97,7 +97,7 @@ MessageList.propTypes = const mapStateToProps = (state) => ({ chatmessages : state.chatmessages, - myPicture : state.settings.picture + myPicture : state.me.picture }); export default connect( @@ -109,7 +109,7 @@ export default connect( { return ( prev.chatmessages === next.chatmessages && - prev.settings.picture === next.settings.picture + prev.me.picture === next.me.picture ); } } diff --git a/app/src/components/MeetingDrawer/FileSharing/File.js b/app/src/components/MeetingDrawer/FileSharing/File.js index cd3a985..dd89cd2 100644 --- a/app/src/components/MeetingDrawer/FileSharing/File.js +++ b/app/src/components/MeetingDrawer/FileSharing/File.js @@ -67,7 +67,7 @@ class File extends React.PureComponent Avatar
- { file.files ? + { file.files && File finished downloading @@ -92,13 +92,12 @@ class File extends React.PureComponent
))} - :null } { `${displayName} shared a file` } - { !file.active && !file.files ? + { (!file.active && !file.files) &&
{ magnet.decode(magnetUri).dn } @@ -121,20 +120,17 @@ class File extends React.PureComponent }
- :null } - { file.timeout ? + { file.timeout && If this process takes a long time, there might not be anyone seeding this torrent. Try asking someone to reupload the file that you want. - :null } - { file.active ? + { file.active && - :null } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileList.js b/app/src/components/MeetingDrawer/FileSharing/FileList.js index 1dcb905..d2142d6 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileList.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileList.js @@ -45,7 +45,6 @@ class FileList extends React.PureComponent const { files, me, - picture, peers, classes } = this.props; @@ -61,7 +60,7 @@ class FileList extends React.PureComponent if (me.id === file.peerId) { displayName = 'You'; - filePicture = picture; + filePicture = me.picture; } else if (peers[file.peerId]) { @@ -91,7 +90,6 @@ FileList.propTypes = { files : PropTypes.object.isRequired, me : appPropTypes.Me.isRequired, - picture : PropTypes.string, peers : PropTypes.object.isRequired, classes : PropTypes.object.isRequired }; @@ -99,10 +97,9 @@ FileList.propTypes = const mapStateToProps = (state) => { return { - files : state.files, - me : state.me, - picture : state.settings.picture, - peers : state.peers + files : state.files, + me : state.me, + peers : state.peers }; }; @@ -116,7 +113,6 @@ export default connect( return ( prev.files === next.files && prev.me === next.me && - prev.settings.picture === next.settings.picture && prev.peers === next.peers ); } diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js index f506f9c..304cbb9 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js @@ -79,7 +79,7 @@ const ListMe = (props) => classes } = props; - const picture = settings.picture || EmptyAvatar; + const picture = me.picture || EmptyAvatar; return (
  • @@ -91,9 +91,8 @@ const ListMe = (props) =>
    - { me.raisedHand ? + { me.raisedHand &&
    - :null }
    diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index 82f6840..cd19e9e 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -159,7 +159,7 @@ const ListPeer = (props) => {peer.displayName}
    - { peer.raiseHandState ? + { peer.raiseHandState &&
    ) } /> - :null }
    {children}
    - { screenConsumer ? + { screenConsumer &&
    }
    - :null }
    Me:
  • -
    • Participants in Spotlight:
    • { spotlightPeers.map((peer) => ( @@ -104,7 +103,6 @@ class ParticipantList extends React.PureComponent ))}
    -
    • Passive Participants:
    • { passivePeers.map((peerId) => ( diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index e618719..ca735bd 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -6,6 +6,7 @@ import { videoBoxesSelector, spotlightsLengthSelector } from '../Selectors'; +import classnames from 'classnames'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import Peer from '../Containers/Peer'; @@ -14,7 +15,7 @@ import HiddenPeers from '../Containers/HiddenPeers'; const RATIO = 1.334; const PADDING_V = 50; -const PADDING_H = 20; +const PADDING_H = 0; const styles = () => ({ @@ -27,11 +28,17 @@ const styles = () => flexWrap : 'wrap', justifyContent : 'center', alignItems : 'center', - alignContent : 'center', - paddingTop : 40, - paddingBottom : 10, - paddingLeft : 10, - paddingRight : 10 + alignContent : 'center' + }, + hiddenToolBar : + { + paddingTop : 0, + transition : 'padding .5s' + }, + showingToolBar : + { + paddingTop : 60, + transition : 'padding .5s' } }); @@ -63,7 +70,8 @@ class Democratic extends React.PureComponent } const width = this.peersRef.current.clientWidth - PADDING_H; - const height = this.peersRef.current.clientHeight - PADDING_V; + const height = this.peersRef.current.clientHeight - + (this.props.toolbarsVisible ? PADDING_V : PADDING_H); let x, y, space; @@ -86,8 +94,8 @@ class Democratic extends React.PureComponent if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x)) { this.setState({ - peerWidth : 0.9 * x, - peerHeight : 0.9 * y + peerWidth : 0.95 * x, + peerHeight : 0.95 * y }); } }; @@ -125,6 +133,7 @@ class Democratic extends React.PureComponent peersLength, spotlightsPeers, spotlightsLength, + toolbarsVisible, classes } = this.props; @@ -135,7 +144,13 @@ class Democratic extends React.PureComponent }; return ( -
      +
      ); })} - { spotlightsLength < peersLength ? + { spotlightsLength < peersLength && - :null }
      ); @@ -171,6 +185,7 @@ Democratic.propTypes = boxes : PropTypes.number, spotlightsLength : PropTypes.number, spotlightsPeers : PropTypes.array.isRequired, + toolbarsVisible : PropTypes.bool.isRequired, classes : PropTypes.object.isRequired }; @@ -180,7 +195,8 @@ const mapStateToProps = (state) => peersLength : peersLengthSelector(state), boxes : videoBoxesSelector(state), spotlightsPeers : spotlightPeersSelector(state), - spotlightsLength : spotlightsLengthSelector(state) + spotlightsLength : spotlightsLengthSelector(state), + toolbarsVisible : state.room.toolbarsVisible }; }; @@ -195,7 +211,8 @@ export default connect( prev.peers === next.peers && prev.producers === next.producers && prev.consumers === next.consumers && - prev.room.spotlights === next.room.spotlights + prev.room.spotlights === next.room.spotlights && + prev.room.toolbarsVisible === next.room.toolbarsVisible ); } } diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index 934db64..fde6515 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -228,13 +228,12 @@ class Filmstrip extends React.PureComponent return (
      - { peers[activePeerId] ? + { peers[activePeerId] && - :null }
      @@ -286,11 +285,10 @@ class Filmstrip extends React.PureComponent
      - { spotlightsLength - :null }
      ); diff --git a/app/src/components/PeerAudio/PeerAudio.js b/app/src/components/PeerAudio/PeerAudio.js index 2e9c195..38d7faf 100644 --- a/app/src/components/PeerAudio/PeerAudio.js +++ b/app/src/components/PeerAudio/PeerAudio.js @@ -29,7 +29,8 @@ export default class PeerAudio extends React.PureComponent this._setTrack(audioTrack); } - componentWillReceiveProps(nextProps) + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { const { audioTrack } = nextProps; diff --git a/app/src/components/ReactLazyPreload.js b/app/src/components/ReactLazyPreload.js new file mode 100644 index 0000000..4f43c1f --- /dev/null +++ b/app/src/components/ReactLazyPreload.js @@ -0,0 +1,10 @@ +import React from 'react'; + +export const ReactLazyPreload = (importStatement) => +{ + const Component = React.lazy(importStatement); + + Component.preload = importStatement; + + return Component; +}; \ No newline at end of file diff --git a/app/src/components/Room.js b/app/src/components/Room.js index 040511e..6ae0152 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -1,6 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { + lobbyPeersKeySelector +} from './Selectors'; import * as appPropTypes from './appPropTypes'; import { withRoomContext } from '../RoomContext'; import { withStyles } from '@material-ui/core/styles'; @@ -13,7 +16,6 @@ import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'; import Hidden from '@material-ui/core/Hidden'; -import Paper from '@material-ui/core/Paper'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; @@ -30,11 +32,13 @@ import VideoWindow from './VideoWindow/VideoWindow'; import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import SettingsIcon from '@material-ui/icons/Settings'; +import SecurityIcon from '@material-ui/icons/Security'; +import LockDialog from './AccessControl/LockDialog/LockDialog'; import LockIcon from '@material-ui/icons/Lock'; import LockOpenIcon from '@material-ui/icons/LockOpen'; import Button from '@material-ui/core/Button'; import Settings from './Settings/Settings'; -import JoinDialog from './JoinDialog'; +import Tooltip from '@material-ui/core/Tooltip'; const TIMEOUT = 10 * 1000; @@ -150,6 +154,38 @@ const styles = (theme) => } }); +const PulsingBadge = withStyles((theme) => + ({ + badge : + { + backgroundColor : theme.palette.secondary.main, + // boxShadow : `0 0 0 2px ${theme.palette.secondary.main}`, + '&::after' : + { + position : 'absolute', + width : '100%', + height : '100%', + borderRadius : '50%', + animation : '$ripple 1.2s infinite ease-in-out', + border : `3px solid ${theme.palette.secondary.main}`, + content : '""' + } + }, + '@keyframes ripple' : + { + '0%' : + { + transform : 'scale(.8)', + opacity : 1 + }, + '100%' : + { + transform : 'scale(2.4)', + opacity : 0 + } + } + }))(Badge); + class Room extends React.PureComponent { constructor(props) @@ -227,11 +263,13 @@ class Room extends React.PureComponent const { roomClient, room, + lobbyPeers, advancedMode, myPicture, loggedIn, loginEnabled, setSettingsOpen, + setLockDialogOpen, toolAreaOpen, toggleToolArea, unread, @@ -245,74 +283,52 @@ class Room extends React.PureComponent democratic : Democratic }[room.mode]; - if (room.lockedOut) - { - return ( -
      - - This room is locked at the moment, try again later. - -
      - ); - } - else if (!room.joined) - { - return ( -
      - -
      - ); - } - else - { - return ( -
      - - This website uses cookies to enhance the user experience. - + return ( +
      + + This website uses cookies to enhance the user experience. + - + - + - + - + - + - - - - toggleToolArea()} - className={classes.menuButton} - > - - - - { window.config.logo ? - Logo - :null - } - + + + toggleToolArea()} + className={classes.menuButton} > - { window.config.title } - -
      -
      + + + + { window.config.logo && Logo } + + { window.config.title } + +
      +
      + } - { this.fullscreen.fullscreenEnabled ? + + { lobbyPeers.length > 0 && + + setLockDialogOpen(!room.lockDialogOpen)} + > + + + + + + } + { this.fullscreen.fullscreenEnabled && + } - :null - } + + } + - { loginEnabled ? + + { loginEnabled && + } - :null - } - -
      - - - + Leave + +
      + + + - + - -
      - ); - } + + + +
      + ); } } @@ -418,6 +456,7 @@ Room.propTypes = { roomClient : PropTypes.object.isRequired, room : appPropTypes.Room.isRequired, + lobbyPeers : PropTypes.array, advancedMode : PropTypes.bool.isRequired, myPicture : PropTypes.string, loggedIn : PropTypes.bool.isRequired, @@ -425,6 +464,7 @@ Room.propTypes = toolAreaOpen : PropTypes.bool.isRequired, setToolbarsVisible : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired, + setLockDialogOpen : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired, unread : PropTypes.number.isRequired, classes : PropTypes.object.isRequired, @@ -434,10 +474,11 @@ Room.propTypes = const mapStateToProps = (state) => ({ room : state.room, + lobbyPeers : lobbyPeersKeySelector(state), advancedMode : state.settings.advancedMode, loggedIn : state.me.loggedIn, loginEnabled : state.me.loginEnabled, - myPicture : state.settings.picture, + myPicture : state.me.picture, toolAreaOpen : state.toolarea.toolAreaOpen, unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles @@ -453,6 +494,10 @@ const mapDispatchToProps = (dispatch) => { dispatch(stateActions.setSettingsOpen({ settingsOpen })); }, + setLockDialogOpen : (lockDialogOpen) => + { + dispatch(stateActions.setLockDialogOpen({ lockDialogOpen })); + }, toggleToolArea : () => { dispatch(stateActions.toggleToolArea()); @@ -468,9 +513,10 @@ export default withRoomContext(connect( { return ( prev.room === next.room && + prev.lobbyPeers === next.lobbyPeers && prev.me.loggedIn === next.me.loggedIn && prev.me.loginEnabled === next.me.loginEnabled && - prev.settings.picture === next.settings.picture && + prev.me.picture === next.me.picture && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen && prev.toolarea.unreadMessages === next.toolarea.unreadMessages && prev.toolarea.unreadFiles === next.toolarea.unreadFiles && diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index 95291f4..d3e293d 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -4,6 +4,7 @@ const producersSelect = (state) => state.producers; const consumersSelect = (state) => state.consumers; const spotlightsSelector = (state) => state.room.spotlights; const peersSelector = (state) => state.peers; +const lobbyPeersSelector = (state) => state.lobbyPeers; const getPeerConsumers = (state, props) => (state.peers[props.id] ? state.peers[props.id].consumers : null); const getAllConsumers = (state) => state.consumers; @@ -12,6 +13,11 @@ const peersKeySelector = createSelector( (peers) => Object.keys(peers) ); +export const lobbyPeersKeySelector = createSelector( + lobbyPeersSelector, + (lobbyPeers) => Object.keys(lobbyPeers) +); + export const micProducersSelector = createSelector( producersSelect, (producers) => Object.values(producers).filter((producer) => producer.source === 'mic') diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index 89f4c55..b90cc81 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -171,24 +171,17 @@ class VideoView extends React.PureComponent })} >
      - { audioCodec ? -

      {audioCodec}

      - :null - } + { audioCodec &&

      {audioCodec}

      } - { videoCodec ? -

      {videoCodec} {videoProfile}

      - :null - } + { videoCodec &&

      {videoCodec} {videoProfile}

      } - { (videoVisible && videoWidth !== null) ? + { (videoVisible && videoWidth !== null) &&

      {videoWidth}x{videoHeight}

      - :null }
      - { showPeerInfo ? + { showPeerInfo &&
      { isMe ? @@ -212,17 +205,15 @@ class VideoView extends React.PureComponent } - { advancedMode ? + { advancedMode &&
      {peer.device.name} {Math.floor(peer.device.version) || null}
      - :null }
      - :null }
      @@ -230,7 +221,7 @@ class VideoView extends React.PureComponent ref='video' className={classnames(classes.video, { hidden : !videoVisible, - 'isMe' : isMe && !isScreen, + 'isMe' : isMe && !isScreen, loading : videoProfile === 'none', contain : videoContain })} @@ -256,7 +247,8 @@ class VideoView extends React.PureComponent clearInterval(this._videoResolutionTimer); } - componentWillReceiveProps(nextProps) + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { const { videoTrack } = nextProps; diff --git a/app/src/components/VideoWindow/NewWindow.js b/app/src/components/VideoWindow/NewWindow.js index b2255f4..1f58aa1 100644 --- a/app/src/components/VideoWindow/NewWindow.js +++ b/app/src/components/VideoWindow/NewWindow.js @@ -130,7 +130,7 @@ class NewWindow extends React.PureComponent return ReactDOM.createPortal([
      - {this.fullscreen.fullscreenEnabled && ( + { this.fullscreen.fullscreenEnabled &&
      }
      - )} + }
      - {this.props.children} + { this.props.children }
      ], this.container); } diff --git a/app/src/index.css b/app/src/index.css index 3fbbe8f..b1a7397 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -18,6 +18,11 @@ html font-family: 'Roboto'; font-weight: 300; margin : 0; + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; } body diff --git a/app/src/index.js b/app/src/index.js index 8e30c9d..a412bb6 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -9,7 +9,7 @@ import RoomClient from './RoomClient'; import RoomContext from './RoomContext'; import deviceInfo from './deviceInfo'; import * as stateActions from './actions/stateActions'; -import Room from './components/Room'; +import App from './components/App'; import LoadingView from './components/LoadingView'; import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; import { PersistGate } from 'redux-persist/lib/integration/react'; @@ -19,7 +19,7 @@ import * as serviceWorker from './serviceWorker'; import './index.css'; -if (process.env.NODE_ENV !== 'production') +if (process.env.REACT_APP_DEBUG === '*' || process.env.NODE_ENV !== 'production') { debug.enable('* -engine* -socket* -RIE* *WARN* *ERROR*'); } @@ -62,8 +62,8 @@ function run() window.history.pushState('', '', urlParser.toString()); } + const accessCode = parameters.get('code'); const produce = parameters.get('produce') !== 'false'; - const consume = parameters.get('consume') !== 'false'; const useSimulcast = parameters.get('simulcast') === 'true'; const forceTcp = parameters.get('forceTcp') === 'true'; @@ -84,7 +84,7 @@ function run() ); roomClient = new RoomClient( - { roomId, peerId, device, useSimulcast, produce, consume, forceTcp }); + { roomId, peerId, accessCode, device, useSimulcast, produce, forceTcp }); global.CLIENT = roomClient; @@ -94,7 +94,7 @@ function run() } persistor={persistor}> - + diff --git a/app/src/reducers/lobbyPeers.js b/app/src/reducers/lobbyPeers.js new file mode 100644 index 0000000..fef9190 --- /dev/null +++ b/app/src/reducers/lobbyPeers.js @@ -0,0 +1,59 @@ +const lobbyPeer = (state = {}, action) => +{ + switch (action.type) + { + case 'ADD_LOBBY_PEER': + return { id: action.payload.peerId }; + + case 'SET_LOBBY_PEER_DISPLAY_NAME': + return { ...state, displayName: action.payload.displayName }; + case 'SET_LOBBY_PEER_PICTURE': + return { ...state, picture: action.payload.picture }; + case 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS': + return { ...state, promotionInProgress: action.payload.flag }; + + default: + return state; + } +}; + +const lobbyPeers = (state = {}, action) => +{ + switch (action.type) + { + case 'ADD_LOBBY_PEER': + { + return { ...state, [action.payload.peerId]: lobbyPeer(undefined, action) }; + } + + case 'REMOVE_LOBBY_PEER': + { + const { peerId } = action.payload; + const newState = { ...state }; + + delete newState[peerId]; + + return newState; + } + + case 'SET_LOBBY_PEER_DISPLAY_NAME': + case 'SET_LOBBY_PEER_PICTURE': + case 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS': + { + const oldLobbyPeer = state[action.payload.peerId]; + + if (!oldLobbyPeer) + { + // Tried to update non-existant lobbyPeer. Has probably been promoted, or left. + return state; + } + + return { ...state, [oldLobbyPeer.id]: lobbyPeer(oldLobbyPeer, action) }; + } + + default: + return state; + } +}; + +export default lobbyPeers; diff --git a/app/src/reducers/me.js b/app/src/reducers/me.js index 8f0969b..13219b0 100644 --- a/app/src/reducers/me.js +++ b/app/src/reducers/me.js @@ -2,6 +2,7 @@ const initialState = { id : null, device : null, + picture : null, canSendMic : false, canSendWebcam : false, canShareScreen : false, @@ -11,6 +12,7 @@ const initialState = webcamInProgress : false, audioInProgress : false, screenShareInProgress : false, + displayNameInProgress : false, loginEnabled : false, raiseHand : false, raiseHandInProgress : false, @@ -38,11 +40,18 @@ const me = (state = initialState, action) => } case 'LOGGED_IN': - return { ...state, loggedIn: true }; + { + const { flag } = action.payload; + + return { ...state, loggedIn: flag }; + } case 'USER_LOGOUT': return { ...state, loggedIn: false }; + case 'SET_PICTURE': + return { ...state, picture: action.payload.picture }; + case 'SET_MEDIA_CAPABILITIES': { const { @@ -110,6 +119,13 @@ const me = (state = initialState, action) => return { ...state, raiseHandInProgress: flag }; } + case 'SET_DISPLAY_NAME_IN_PROGRESS': + { + const { flag } = action.payload; + + return { ...state, displayNameInProgress: flag }; + } + default: return state; } diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index e1444c3..0d650d3 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -1,9 +1,13 @@ const initialState = { url : null, + name : '', state : 'new', // new/connecting/connected/disconnected/closed, locked : false, - lockedOut : false, + inLobby : false, + signInRequired : false, + accessCode : '', // access code to the room if locked and joinByAccessCode == true + joinByAccessCode : true, // if true: accessCode is a possibility to open the room activeSpeakerId : null, torrentSupport : false, showSettings : false, @@ -14,6 +18,7 @@ const initialState = selectedPeerId : null, spotlights : [], settingsOpen : false, + lockDialogOpen : false, joined : false }; @@ -28,6 +33,13 @@ const room = (state = initialState, action) => return { ...state, url }; } + case 'SET_ROOM_NAME': + { + const { name } = action.payload; + + return { ...state, name }; + } + case 'SET_ROOM_STATE': { const roomState = action.payload.state; @@ -48,11 +60,41 @@ const room = (state = initialState, action) => return { ...state, locked: false }; } - case 'SET_ROOM_LOCKED_OUT': + case 'SET_IN_LOBBY': { - return { ...state, lockedOut: true }; + const { inLobby } = action.payload; + + return { ...state, inLobby }; } + case 'SET_SIGN_IN_REQUIRED': + { + const { signInRequired } = action.payload; + + return { ...state, signInRequired }; + } + + case 'SET_ACCESS_CODE': + { + const { accessCode } = action.payload; + + return { ...state, accessCode }; + } + + case 'SET_JOIN_BY_ACCESS_CODE': + { + const { joinByAccessCode } = action.payload; + + return { ...state, joinByAccessCode }; + } + + case 'SET_LOCK_DIALOG_OPEN': + { + const { lockDialogOpen } = action.payload; + + return { ...state, lockDialogOpen }; + } + case 'SET_SETTINGS_OPEN': { const { settingsOpen } = action.payload; diff --git a/app/src/reducers/rootReducer.js b/app/src/reducers/rootReducer.js index ea78cdb..539c8d7 100644 --- a/app/src/reducers/rootReducer.js +++ b/app/src/reducers/rootReducer.js @@ -3,6 +3,7 @@ import room from './room'; import me from './me'; import producers from './producers'; import peers from './peers'; +import lobbyPeers from './lobbyPeers'; import consumers from './consumers'; import peerVolumes from './peerVolumes'; import notifications from './notifications'; @@ -16,6 +17,7 @@ export default combineReducers({ me, producers, peers, + lobbyPeers, consumers, peerVolumes, notifications, diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index 1f0a0db..ec9e322 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -1,7 +1,6 @@ const initialState = { displayName : 'Guest', - picture : null, selectedWebcam : null, selectedAudioDevice : null, advancedMode : false, @@ -24,20 +23,11 @@ const settings = (state = initialState, action) => case 'SET_DISPLAY_NAME': { - let { displayName } = action.payload; - - // Be ready for undefined displayName (so keep previous one). - if (!displayName) - displayName = state.displayName; + const { displayName } = action.payload; return { ...state, displayName }; } - case 'SET_PICTURE': - { - return { ...state, picture: action.payload.picture }; - } - case 'TOGGLE_ADVANCED_MODE': { const advancedMode = !state.advancedMode; diff --git a/server/.eslintrc.js b/server/.eslintrc.js deleted file mode 100644 index bcdd959..0000000 --- a/server/.eslintrc.js +++ /dev/null @@ -1,168 +0,0 @@ -module.exports = -{ - env: - { - browser: true, - es6: true, - node: true - }, - extends: - [ - 'eslint:recommended' - ], - settings: {}, - parserOptions: - { - ecmaVersion: 6, - sourceType: 'module', - ecmaFeatures: - { - impliedStrict: true - } - }, - rules: - { - 'array-bracket-spacing': [ 2, 'always', - { - objectsInArrays: true, - arraysInArrays: true - }], - 'arrow-parens': [ 2, 'always' ], - 'arrow-spacing': 2, - 'block-spacing': [ 2, 'always' ], - 'brace-style': [ 2, 'allman', { allowSingleLine: true } ], - 'camelcase': 2, - 'comma-dangle': 2, - 'comma-spacing': [ 2, { before: false, after: true } ], - 'comma-style': 2, - 'computed-property-spacing': 2, - 'constructor-super': 2, - 'func-call-spacing': 2, - 'generator-star-spacing': 2, - 'guard-for-in': 2, - 'indent': [ 2, 'tab', { 'SwitchCase': 1 } ], - 'key-spacing': [ 2, - { - singleLine: - { - beforeColon: false, - afterColon: true - }, - multiLine: - { - beforeColon: true, - afterColon: true, - align: 'colon' - } - }], - 'keyword-spacing': 2, - 'linebreak-style': [ 2, 'unix' ], - 'lines-around-comment': [ 2, - { - allowBlockStart: true, - allowObjectStart: true, - beforeBlockComment: true, - beforeLineComment: false - }], - 'max-len': [ 2, 90, - { - tabWidth: 2, - comments: 110, - ignoreUrls: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - ignoreRegExpLiterals: true - }], - 'newline-after-var': 2, - 'newline-before-return': 2, - 'newline-per-chained-call': 2, - 'no-alert': 2, - 'no-caller': 2, - 'no-case-declarations': 2, - 'no-catch-shadow': 2, - 'no-class-assign': 2, - 'no-confusing-arrow': 2, - 'no-console': 2, - 'no-const-assign': 2, - 'no-debugger': 2, - 'no-dupe-args': 2, - 'no-dupe-keys': 2, - 'no-duplicate-case': 2, - 'no-div-regex': 2, - 'no-empty': [ 2, { allowEmptyCatch: true } ], - 'no-empty-pattern': 2, - 'no-else-return': 0, - 'no-eval': 2, - 'no-extend-native': 2, - 'no-ex-assign': 2, - 'no-extra-bind': 2, - 'no-extra-boolean-cast': 2, - 'no-extra-label': 2, - 'no-extra-semi': 2, - 'no-fallthrough': 2, - 'no-func-assign': 2, - 'no-global-assign': 2, - 'no-implicit-coercion': 2, - 'no-implicit-globals': 2, - 'no-inner-declarations': 2, - 'no-invalid-regexp': 2, - 'no-invalid-this': 2, - 'no-irregular-whitespace': 2, - 'no-lonely-if': 2, - 'no-mixed-operators': 2, - 'no-mixed-spaces-and-tabs': 2, - 'no-multi-spaces': 2, - 'no-multi-str': 2, - 'no-multiple-empty-lines': [ 2, { max: 1, maxEOF: 0, maxBOF: 0 } ], - 'no-native-reassign': 2, - 'no-negated-in-lhs': 2, - 'no-new': 2, - 'no-new-func': 2, - 'no-new-wrappers': 2, - 'no-obj-calls': 2, - 'no-proto': 2, - 'no-prototype-builtins': 0, - 'no-redeclare': 2, - 'no-regex-spaces': 2, - 'no-restricted-imports': 2, - 'no-return-assign': 2, - 'no-self-assign': 2, - 'no-self-compare': 2, - 'no-sequences': 2, - 'no-shadow': 2, - 'no-shadow-restricted-names': 2, - 'no-spaced-func': 2, - 'no-sparse-arrays': 2, - 'no-this-before-super': 2, - 'no-throw-literal': 2, - 'no-undef': 2, - 'no-unexpected-multiline': 2, - 'no-unmodified-loop-condition': 2, - 'no-unreachable': 2, - 'no-unused-vars': [ 1, { vars: 'all', args: 'after-used' }], - 'no-use-before-define': [ 2, { functions: false } ], - 'no-useless-call': 2, - 'no-useless-computed-key': 2, - 'no-useless-concat': 2, - 'no-useless-rename': 2, - 'no-var': 2, - 'no-whitespace-before-property': 2, - 'object-curly-newline': 0, - 'object-curly-spacing': [ 2, 'always' ], - 'object-property-newline': [ 2, { allowMultiplePropertiesPerLine: true } ], - 'prefer-const': 2, - 'prefer-rest-params': 2, - 'prefer-spread': 2, - 'prefer-template': 2, - 'quotes': [ 2, 'single', { avoidEscape: true } ], - 'semi': [ 2, 'always' ], - 'semi-spacing': 2, - 'space-before-blocks': 2, - 'space-before-function-paren': [ 2, 'never' ], - 'space-in-parens': [ 2, 'never' ], - 'spaced-comment': [ 2, 'always' ], - 'strict': 0, - 'valid-typeof': 2, - 'yoda': 2 - } -}; diff --git a/server/.eslintrc.json b/server/.eslintrc.json new file mode 100644 index 0000000..d98852a --- /dev/null +++ b/server/.eslintrc.json @@ -0,0 +1,171 @@ +{ + "env": + { + "es6": true, + "node": true + }, + "extends": + [ + "eslint:recommended" + ], + "settings": {}, + "parserOptions": + { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": + { + "impliedStrict": true + } + }, + "rules": + { + "array-bracket-spacing": [ 2, "always", + { + "objectsInArrays": true, + "arraysInArrays": true + }], + "arrow-parens": [ 2, "always" ], + "arrow-spacing": 2, + "block-spacing": [ 2, "always" ], + "brace-style": [ 2, "allman", { "allowSingleLine": true } ], + "camelcase": 2, + "comma-dangle": 2, + "comma-spacing": [ 2, { "before": false, "after": true } ], + "comma-style": 2, + "computed-property-spacing": 2, + "constructor-super": 2, + "func-call-spacing": 2, + "generator-star-spacing": 2, + "guard-for-in": 2, + "indent": [ 2, "tab", { "SwitchCase": 1 } ], + "key-spacing": [ 2, + { + "singleLine": + { + "beforeColon": false, + "afterColon": true + }, + "multiLine": + { + "beforeColon": true, + "afterColon": true, + "align": "colon" + } + }], + "keyword-spacing": 2, + "linebreak-style": [ 2, "unix" ], + "lines-around-comment": [ 2, + { + "allowBlockStart": true, + "allowObjectStart": true, + "beforeBlockComment": true, + "beforeLineComment": false + }], + "max-len": [ 2, 90, + { + "tabWidth": 2, + "comments": 90, + "ignoreUrls": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreRegExpLiterals": true + }], + "newline-after-var": 2, + "newline-before-return": 2, + "newline-per-chained-call": 2, + "no-alert": 2, + "no-caller": 2, + "no-case-declarations": 2, + "no-catch-shadow": 2, + "no-class-assign": 2, + "no-confusing-arrow": 2, + "no-console": 2, + "no-const-assign": 2, + "no-debugger": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-div-regex": 2, + "no-empty": [ 2, { "allowEmptyCatch": true } ], + "no-empty-pattern": 2, + "no-else-return": 0, + "no-eval": 2, + "no-extend-native": 2, + "no-ex-assign": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-label": 2, + "no-extra-semi": 2, + "no-fallthrough": 2, + "no-func-assign": 2, + "no-global-assign": 2, + "no-implicit-coercion": 2, + "no-implicit-globals": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-invalid-this": 2, + "no-irregular-whitespace": 2, + "no-lonely-if": 2, + "no-mixed-operators": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [ 1, { "max": 1, "maxEOF": 0, "maxBOF": 0 } ], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-proto": 2, + "no-prototype-builtins": 0, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-imports": 2, + "no-return-assign": 2, + "no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-undef": 2, + "no-unexpected-multiline": 2, + "no-unmodified-loop-condition": 2, + "no-unreachable": 2, + "no-unused-vars": [ 1, { "vars": "all", "args": "after-used" }], + "no-use-before-define": [ 2, { "functions": false } ], + "no-useless-call": 2, + "no-useless-computed-key": 2, + "no-useless-concat": 2, + "no-useless-rename": 2, + "no-var": 2, + "no-whitespace-before-property": 2, + "object-curly-newline": 0, + "object-curly-spacing": [ 2, "always" ], + "object-property-newline": [ 2, { "allowMultiplePropertiesPerLine": true } ], + "prefer-const": 2, + "prefer-rest-params": 2, + "prefer-spread": 2, + "prefer-template": 2, + "quotes": [ 2, "single", { "avoidEscape": true } ], + "semi": [ 2, "always" ], + "semi-spacing": 2, + "space-before-blocks": 2, + "space-before-function-paren": [ 2, + { + "anonymous" : "never", + "named" : "never", + "asyncArrow" : "always" + }], + "space-in-parens": [ 2, "never" ], + "spaced-comment": [ 2, "always" ], + "strict": 2, + "valid-typeof": 2, + "yoda": 2 + } +} \ No newline at end of file diff --git a/server/config/config.example.js b/server/config/config.example.js index 5f2a42f..84abff6 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -23,6 +23,7 @@ module.exports = },*/ // session cookie secret cookieSecret : 'T0P-S3cR3t_cook!e', + cookieName : 'multiparty-meeting.sid', tls : { cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, @@ -33,6 +34,16 @@ module.exports = // Any http request is redirected to https. // Listening port for http server. listeningRedirectPort : 80, + // If this is set to true, only signed-in users will be able + // to join a room directly. Non-signed-in users (guests) will + // always be put in the lobby regardless of room lock status. + // If false, there is no difference between guests and signed-in + // users when joining. + requireSignInToAccess : true, + // This flag has no effect when requireSignInToAccess is false + // When truthy, the room will be open to all users when the first + // authenticated user has already joined the room. + activateOnHostJoin : true, // Mediasoup settings mediasoup : { diff --git a/server/gulpfile.js b/server/gulpfile.js deleted file mode 100644 index fa03823..0000000 --- a/server/gulpfile.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Tasks: - * - * gulp lint - * Checks source code - * - * gulp watch - * Observes changes in the code - * - * gulp - * Invokes both `lint` and `watch` tasks - */ - -const gulp = require('gulp'); -const plumber = require('gulp-plumber'); -const eslint = require('gulp-eslint'); - -const LINTING_FILES = -[ - 'gulpfile.js', - 'server.js', - 'config/config.example.js', - 'lib/**/*.js' -]; - -gulp.task('lint', () => -{ - - return gulp.src(LINTING_FILES) - .pipe(plumber()) - .pipe(eslint()) - .pipe(eslint.format()); -}); - -gulp.task('lint-fix', function() -{ - return gulp.src(LINTING_FILES) - .pipe(plumber()) - .pipe(eslint({ fix: true })) - .pipe(eslint.format()) - .pipe(gulp.dest((file) => file.base)); -}); - -gulp.task('default', gulp.series('lint')); diff --git a/server/http-helpers.js b/server/http-helpers.js deleted file mode 100644 index 9195b2f..0000000 --- a/server/http-helpers.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const headers = { - 'access-control-allow-origin': '*', - 'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'access-control-allow-headers': 'content-type, accept', - 'access-control-max-age': 10, - 'Content-Type': 'application/json' -}; - -exports.prepareResponse = (req, cb) => -{ - let data = ''; - - req.on('data', (chunk) => - { - data += chunk; - }); - - req.on('end', () => - { - cb(data); - }); -}; - -exports.respond = (res, data, status) => -{ - status = status || 200; - res.writeHead(status, headers); - res.end(data); -}; - -exports.send404 = (res) => -{ - exports.respond(res, 'Not Found', 404); -}; - -exports.redirector = (res, loc, status) => -{ - status = status || 302; - res.writeHead(status, { Location: loc }); - res.end(); -}; diff --git a/server/httpHelper.js b/server/httpHelper.js new file mode 100644 index 0000000..a39bf17 --- /dev/null +++ b/server/httpHelper.js @@ -0,0 +1,41 @@ +exports.loginHelper = function(data) +{ + const html = ` + + + + Multiparty Meeting + + + + + `; + + return html; +}; + +exports.logoutHelper = function() +{ + const html = ` + + + + Multiparty Meeting + + + + + `; + + return html; +}; \ No newline at end of file diff --git a/server/lib/Lobby.js b/server/lib/Lobby.js new file mode 100644 index 0000000..6d0f9f8 --- /dev/null +++ b/server/lib/Lobby.js @@ -0,0 +1,203 @@ +const EventEmitter = require('events').EventEmitter; +const Logger = require('./Logger'); + +const logger = new Logger('Lobby'); + +class Lobby extends EventEmitter +{ + constructor() + { + logger.info('constructor()'); + + super(); + + // Closed flag. + this._closed = false; + + this._peers = new Map(); + } + + close() + { + logger.info('close()'); + + this._closed = true; + + this._peers.forEach((peer) => + { + if (!peer.closed) + peer.close(); + }); + + this._peers.clear(); + } + + checkEmpty() + { + logger.info('checkEmpty()'); + + return this._peers.size === 0; + } + + peerList() + { + logger.info('peerList()'); + + return Array.from(this._peers.values()).map((peer) => + ({ + peerId : peer.id, + displayName : peer.displayName + })); + } + + hasPeer(peerId) + { + return this._peers.has(peerId); + } + + promoteAllPeers() + { + logger.info('promoteAllPeers()'); + + this._peers.forEach((peer) => + { + if (peer.socket) + this.promotePeer(peer.id); + }); + } + + promotePeer(peerId) + { + logger.info('promotePeer() [peer:"%s"]', peerId); + + const peer = this._peers.get(peerId); + + if (peer) + { + peer.socket.removeListener('request', peer.socketRequestHandler); + peer.removeListener('authenticationChanged', peer.authenticationHandler); + peer.removeListener('close', peer.closeHandler); + + peer.socketRequestHandler = null; + peer.authenticationHandler = null; + peer.closeHandler = null; + + this.emit('promotePeer', peer); + this._peers.delete(peerId); + } + } + + parkPeer(peer) + { + logger.info('parkPeer() [peer:"%s"]', peer.id); + + if (this._closed) + return; + + peer.socketRequestHandler = (request, cb) => + { + logger.debug( + 'Peer "request" event [method:"%s", peer:"%s"]', + request.method, peer.id); + + if (this._closed) + return; + + this._handleSocketRequest(peer, request, cb) + .catch((error) => + { + logger.error('request failed [error:"%o"]', error); + + cb(error); + }); + }; + + peer.authenticationHandler = () => + { + logger.info('parkPeer() | authenticationChange [peer:"%s"]', peer.id); + + peer.authenticated && this.emit('peerAuthenticated', peer); + }; + + peer.closeHandler = () => + { + logger.debug('Peer "close" event [peer:"%s"]', peer.id); + + if (this._closed) + return; + + this.emit('peerClosed', peer); + + this._peers.delete(peer.id); + + if (this.checkEmpty()) + this.emit('lobbyEmpty'); + }; + + this._notification(peer.socket, 'enteredLobby'); + + this._peers.set(peer.id, peer); + + peer.on('authenticationChanged', peer.authenticationHandler); + + peer.socket.on('request', peer.socketRequestHandler); + + peer.on('close', peer.closeHandler); + } + + async _handleSocketRequest(peer, request, cb) + { + logger.debug( + '_handleSocketRequest [peer:"%s"], [request:"%s"]', + peer.id, + request.method + ); + + if (this._closed) + return; + + switch (request.method) + { + case 'changeDisplayName': + { + const { displayName } = request.data; + + peer.displayName = displayName; + + this.emit('changeDisplayName', peer); + + cb(); + + break; + } + case 'changePicture': + { + const { picture } = request.data; + + peer.picture = picture; + + this.emit('changePicture', peer); + + cb(); + + break; + } + } + } + + _notification(socket, method, data = {}, broadcast = false) + { + if (broadcast) + { + socket.broadcast.to(this._roomId).emit( + 'notification', { method, data } + ); + } + else + { + socket.emit('notification', { method, data }); + } + } +} + +module.exports = Lobby; \ No newline at end of file diff --git a/server/lib/Logger.js b/server/lib/Logger.js index e1659dd..dc85948 100644 --- a/server/lib/Logger.js +++ b/server/lib/Logger.js @@ -1,5 +1,3 @@ -'use strict'; - const debug = require('debug'); const APP_NAME = 'multiparty-meeting-server'; diff --git a/server/lib/Peer.js b/server/lib/Peer.js new file mode 100644 index 0000000..9253aa9 --- /dev/null +++ b/server/lib/Peer.js @@ -0,0 +1,342 @@ +const EventEmitter = require('events').EventEmitter; +const Logger = require('./Logger'); + +const logger = new Logger('Peer'); + +class Peer extends EventEmitter +{ + constructor({ id, socket }) + { + logger.info('constructor() [id:"%s", socket:"%s"]', id, socket.id); + super(); + + this._id = id; + + this._authId = null; + + this._socket = socket; + + this._closed = false; + + this._joined = false; + + this._inLobby = false; + + this._authenticated = false; + + this._displayName = false; + + this._picture = null; + + this._email = null; + + this._device = null; + + this._rtpCapabilities = null; + + this._raisedHand = false; + + this._transports = new Map(); + + this._producers = new Map(); + + this._consumers = new Map(); + + this._checkAuthentication(); + + this._handlePeer(); + } + + close() + { + logger.info('close()'); + + this._closed = true; + + // Iterate and close all mediasoup Transport associated to this Peer, so all + // its Producers and Consumers will also be closed. + this.transports.forEach((transport) => + { + transport.close(); + }); + + if (this._socket) + this._socket.disconnect(true); + + this.emit('close'); + } + + _handlePeer() + { + this.socket.use((packet, next) => + { + this._checkAuthentication(); + + return next(); + }); + + this.socket.on('disconnect', () => + { + if (this.closed) + return; + + logger.debug('"disconnect" event [id:%s]', this.id); + + this.close(); + }); + } + + _checkAuthentication() + { + if ( + Boolean(this.socket.handshake.session.passport) && + Boolean(this.socket.handshake.session.passport.user) + ) + { + const { + id, + displayName, + picture, + email + } = this.socket.handshake.session.passport.user; + + id && (this.authId = id); + displayName && (this.displayName = displayName); + picture && (this.picture = picture); + email && (this.email = email); + + this.authenticated = true; + } + else + { + this.authenticated = false; + } + } + + get id() + { + return this._id; + } + + set id(id) + { + this._id = id; + } + + get authId() + { + return this._authId; + } + + set authId(authId) + { + this._authId = authId; + } + + get socket() + { + return this._socket; + } + + set socket(socket) + { + this._socket = socket; + } + + get closed() + { + return this._closed; + } + + get joined() + { + return this._joined; + } + + set joined(joined) + { + this._joined = joined; + } + + get inLobby() + { + return this._inLobby; + } + + set inLobby(inLobby) + { + this._inLobby = inLobby; + } + + get authenticated() + { + return this._authenticated; + } + + set authenticated(authenticated) + { + if (authenticated !== this._authenticated) + { + const oldAuthenticated = this._authenticated; + + this._authenticated = authenticated; + + this.emit('authenticationChanged', { oldAuthenticated }); + } + } + + get displayName() + { + return this._displayName; + } + + set displayName(displayName) + { + if (displayName !== this._displayName) + { + const oldDisplayName = this._displayName; + + this._displayName = displayName; + + this.emit('displayNameChanged', { oldDisplayName }); + } + } + + get picture() + { + return this._picture; + } + + set picture(picture) + { + if (picture !== this._picture) + { + const oldPicture = this._picture; + + this._picture = picture; + + this.emit('pictureChanged', { oldPicture }); + } + } + + get email() + { + return this._email; + } + + set email(email) + { + this._email = email; + } + + get device() + { + return this._device; + } + + set device(device) + { + this._device = device; + } + + get rtpCapabilities() + { + return this._rtpCapabilities; + } + + set rtpCapabilities(rtpCapabilities) + { + this._rtpCapabilities = rtpCapabilities; + } + + get raisedHand() + { + return this._raisedHand; + } + + set raisedHand(raisedHand) + { + this._raisedHand = raisedHand; + } + + get transports() + { + return this._transports; + } + + get producers() + { + return this._producers; + } + + get consumers() + { + return this._consumers; + } + + addTransport(id, transport) + { + this.transports.set(id, transport); + } + + getTransport(id) + { + return this.transports.get(id); + } + + getConsumerTransport() + { + return Array.from(this.transports.values()) + .find((t) => t.appData.consuming); + } + + removeTransport(id) + { + this.transports.delete(id); + } + + addProducer(id, producer) + { + this.producers.set(id, producer); + } + + getProducer(id) + { + return this.producers.get(id); + } + + removeProducer(id) + { + this.producers.delete(id); + } + + addConsumer(id, consumer) + { + this.consumers.set(id, consumer); + } + + getConsumer(id) + { + return this.consumers.get(id); + } + + removeConsumer(id) + { + this.consumers.delete(id); + } + + get peerInfo() + { + const peerInfo = + { + id : this.id, + displayName : this.displayName, + picture : this.picture, + device : this.device + }; + + return peerInfo; + } +} + +module.exports = Peer; diff --git a/server/lib/Room.js b/server/lib/Room.js index 8c7f8d6..9ae9a80 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -1,7 +1,6 @@ -'use strict'; - const EventEmitter = require('events').EventEmitter; const Logger = require('./Logger'); +const Lobby = require('./Lobby'); const config = require('../config/config'); const logger = new Logger('Room'); @@ -19,10 +18,10 @@ class Room extends EventEmitter */ static async create({ mediasoupWorker, roomId }) { - logger.info('create() [roomId:%s, forceH264:%s]', roomId); + logger.info('create() [roomId:"%s"]', roomId); // Router media codecs. - let mediaCodecs = config.mediasoup.router.mediaCodecs; + const mediaCodecs = config.mediasoup.router.mediaCodecs; // Create a mediasoup Router. const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs }); @@ -54,33 +53,182 @@ class Room extends EventEmitter // Locked flag. this._locked = false; + // if true: accessCode is a possibility to open the room + this._joinByAccesCode = true; + + // access code to the room, + // applicable if ( _locked == true and _joinByAccessCode == true ) + this._accessCode = ''; + + this._lobby = new Lobby(); + this._chatHistory = []; this._fileHistory = []; this._lastN = []; - // this._io = io; - this._peers = new Map(); // mediasoup Router instance. - // @type {mediasoup.Router} this._mediasoupRouter = mediasoupRouter; // mediasoup AudioLevelObserver. - // @type {mediasoup.AudioLevelObserver} this._audioLevelObserver = audioLevelObserver; + // Current active speaker. + this._currentActiveSpeaker = null; + + this._handleLobby(); + this._handleAudioLevelObserver(); + } + + isLocked() + { + return this._locked; + } + + close() + { + logger.debug('close()'); + + this._closed = true; + + this._lobby.close(); + + this._peers.forEach((peer) => + { + if (!peer.closed) + peer.close(); + }); + + this._peers.clear(); + + // Close the mediasoup Router. + this._mediasoupRouter.close(); + + // Emit 'close' event. + this.emit('close'); + } + + handlePeer(peer) + { + logger.info('handlePeer() [peer:"%s"]', peer.id); + + // This will allow reconnects to join despite lock + if (this._peers.has(peer.id)) + { + logger.warn( + 'handleConnection() | there is already a peer with same peerId [peer:"%s"]', + peer.id); + + peer.close(); + + return; + } + else if (this._locked) + { + this._parkPeer(peer); + } + else + { + peer.authenticated ? + this._peerJoining(peer) : + this._handleGuest(peer); + } + } + + _handleGuest(peer) + { + if (config.requireSignInToAccess) + { + if (config.activateOnHostJoin && !this.checkEmpty()) + { + this._peerJoining(peer); + } + else + { + this._parkPeer(peer); + this._notification(peer.socket, 'signInRequired'); + } + } + else + { + this._peerJoining(peer); + } + } + + _handleLobby() + { + this._lobby.on('promotePeer', (promotedPeer) => + { + logger.info('promotePeer() [promotedPeer:"%s"]', promotedPeer.id); + + const { id } = promotedPeer; + + this._peerJoining(promotedPeer); + + this._peers.forEach((peer) => + { + this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); + }); + }); + + this._lobby.on('peerAuthenticated', (peer) => + { + !this._locked && this._lobby.promotePeer(peer.id); + }); + + this._lobby.on('changeDisplayName', (changedPeer) => + { + const { id, displayName } = changedPeer; + + this._peers.forEach((peer) => + { + this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); + }); + }); + + this._lobby.on('changePicture', (changedPeer) => + { + const { id, picture } = changedPeer; + + this._peers.forEach((peer) => + { + this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); + }); + }); + + this._lobby.on('peerClosed', (closedPeer) => + { + logger.info('peerClosed() [closedPeer:"%s"]', closedPeer.id); + + const { id } = closedPeer; + + this._peers.forEach((peer) => + { + this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); + }); + }); + + // If nobody left in lobby we should check if room is empty too and initiating + // rooms selfdestruction sequence + this._lobby.on('lobbyEmpty', () => + { + if (this.checkEmpty()) + { + this.selfDestructCountdown(); + } + }); + } + + _handleAudioLevelObserver() + { // Set audioLevelObserver events. this._audioLevelObserver.on('volumes', (volumes) => { const { producer, volume } = volumes[0]; - // logger.debug( - // 'audioLevelObserver "volumes" event [producerId:%s, volume:%s]', - // producer.id, volume); - // Notify all Peers. this._peers.forEach((peer) => { @@ -93,18 +241,21 @@ class Room extends EventEmitter this._audioLevelObserver.on('silence', () => { - // logger.debug('audioLevelObserver "silence" event'); - // Notify all Peers. this._peers.forEach((peer) => { - this._notification(peer.socket, 'activeSpeaker', { peerId : null }); + this._notification(peer.socket, 'activeSpeaker', { peerId: null }); }); }); + } - // Current active speaker. - // @type {mediasoup.Peer} - this._currentActiveSpeaker = null; + logStatus() + { + logger.info( + 'logStatus() [room id:"%s", peers:"%s"]', + this._roomId, + this._peers.size + ); } get id() @@ -112,151 +263,86 @@ class Room extends EventEmitter return this._roomId; } - close() + selfDestructCountdown() { - logger.debug('close()'); + logger.debug('selfDestructCountdown() started'); - this._closed = true; - - // Close the peers - if (this._peers) + setTimeout(() => { - this._peers.forEach((peer) => + if (this._closed) + return; + + if (this.checkEmpty() && this._lobby.checkEmpty()) { - if (peer.socket) - peer.socket.disconnect(); - }); - } - - this._peers.clear(); - - // Close the mediasoup Router. - this._mediasoupRouter.close(); - - // Emit 'close' event. - this.emit('close'); + logger.info( + 'Room deserted for some time, closing the room [roomId:"%s"]', + this._roomId); + this.close(); + } + else + logger.debug('selfDestructCountdown() aborted; room is not empty!'); + }, 10000); } - logStatus() + // checks both room and lobby + checkEmpty() { - logger.info( - 'logStatus() [room id:"%s", peers:%s]', - this._roomId, - this._peers.size - ); + return this._peers.size === 0; } - handleConnection({ peerId, consume, socket }) + _parkPeer(parkPeer) { - logger.info('handleConnection() [peerId:"%s"]', peerId); + this._lobby.parkPeer(parkPeer); - // This will allow reconnects to join despite lock - if (this._peers.has(peerId)) + this._peers.forEach((peer) => { - logger.warn( - 'handleConnection() | there is already a peer with same peerId, ' + - 'closing the previous one [peerId:"%s"]', - peerId); + this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); + }); + } - const peer = this._peers.get(peerId); + _peerJoining(peer) + { + peer.socket.join(this._roomId); - peer.socket.disconnect(); - this._peers.delete(peerId); - } - else if (this._locked) // Don't allow connections to a locked room - { - this._notification(socket, 'roomLocked'); - socket.disconnect(true); - return; - } - - socket.join(this._roomId); - - const peer = { id : peerId, socket : socket }; - - const index = this._lastN.indexOf(peerId); + const index = this._lastN.indexOf(peer.id); if (index === -1) // We don't have this peer, add to end { - this._lastN.push(peerId); + this._lastN.push(peer.id); } - this._peers.set(peerId, peer); + this._peers.set(peer.id, peer); - this._handlePeer({ peer, consume }); - this._notification(socket, 'roomReady'); + this._handlePeer(peer); + this._notification(peer.socket, 'roomReady'); } - isLocked() - { - return this._locked; - } - - authCallback(data) - { - logger.debug('authCallback()'); - - const { - peerId, - displayName, - picture - } = data; - - const peer = this._peers.get(peerId); - - if (peer) - { - this._notification(peer.socket, 'auth', { - displayName : displayName, - picture : picture - }); - } - } - - _handlePeer({ peer, consume }) + _handlePeer(peer) { logger.debug('_handlePeer() [peer:"%s"]', peer.id); - peer.data = {}; - - // Not joined after a custom protoo 'join' request is later received. - peer.data.consume = consume; - peer.data.joined = false; - peer.data.displayName = undefined; - peer.data.device = undefined; - peer.data.rtpCapabilities = undefined; - peer.data.raiseHandState = false; - - // Have mediasoup related maps ready even before the Peer joins since we - // allow creating Transports before joining. - peer.data.transports = new Map(); - peer.data.producers = new Map(); - peer.data.consumers = new Map(); - peer.socket.on('request', (request, cb) => { logger.debug( - 'Peer "request" event [method:%s, peerId:%s]', + 'Peer "request" event [method:"%s", peerId:"%s"]', request.method, peer.id); this._handleSocketRequest(peer, request, cb) .catch((error) => { - logger.error('request failed:%o', error); + logger.error('"request" failed [error:"%o"]', error); cb(error); }); }); - peer.socket.on('disconnect', () => + peer.on('close', () => { if (this._closed) return; - logger.debug('Peer "close" event [peerId:%s]', peer.id); - // If the Peer was joined, notify all Peers. - if (peer.data.joined) + if (peer.joined) { this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); } @@ -268,41 +354,48 @@ class Room extends EventEmitter this._lastN.splice(index, 1); } - // Iterate and close all mediasoup Transport associated to this Peer, so all - // its Producers and Consumers will also be closed. - for (const transport of peer.data.transports.values()) - { - transport.close(); - } - this._peers.delete(peer.id); - // If this is the latest Peer in the room, close the room after a while. - if (this._peers.size === 0) + // If this is the last Peer in the room and + // lobby is empty, close the room after a while. + if (this.checkEmpty() && this._lobby.checkEmpty()) { - setTimeout(() => - { - if (this._closed) - return; - - if (this._peers.size === 0) - { - logger.info( - 'last Peer in the room left, closing the room [roomId:%s]', - this._roomId); - - this.close(); - } - }, 10000); + this.selfDestructCountdown(); } }); + + peer.on('displayNameChanged', ({ oldDisplayName }) => + { + // Ensure the Peer is joined. + if (!peer.joined) + return; + + // Spread to others + this._notification(peer.socket, 'changeDisplayName', { + peerId : peer.id, + displayName : peer.displayName, + oldDisplayName : oldDisplayName + }, true); + }); + + peer.on('pictureChanged', () => + { + // Ensure the Peer is joined. + if (!peer.joined) + return; + + // Spread to others + this._notification(peer.socket, 'changePicture', { + peerId : peer.id, + picture : peer.picture + }, true); + }); } async _handleSocketRequest(peer, request, cb) { switch (request.method) { - case 'getRouterRtpCapabilities': { cb(null, this._mediasoupRouter.rtpCapabilities); @@ -313,7 +406,7 @@ class Room extends EventEmitter case 'join': { // Ensure the Peer is not already joined. - if (peer.data.joined) + if (peer.joined) throw new Error('Peer already joined'); const { @@ -323,11 +416,11 @@ class Room extends EventEmitter rtpCapabilities } = request.data; - // Store client data into the protoo Peer data object. - peer.data.displayName = displayName; - peer.data.picture = picture; - peer.data.device = device; - peer.data.rtpCapabilities = rtpCapabilities; + // Store client data into the Peer data object. + peer.displayName = displayName; + peer.picture = picture; + peer.device = device; + peer.rtpCapabilities = rtpCapabilities; // Tell the new Peer about already joined Peers. // And also create Consumers for existing Producers. @@ -336,17 +429,11 @@ class Room extends EventEmitter this._peers.forEach((joinedPeer) => { - if (joinedPeer.data.joined) + if (joinedPeer.joined) { - peerInfos.push( - { - id : joinedPeer.id, - displayName : joinedPeer.data.displayName, - picture : joinedPeer.data.picture, - device : joinedPeer.data.device - }); + peerInfos.push(joinedPeer.peerInfo); - for (const producer of joinedPeer.data.producers.values()) + joinedPeer.producers.forEach((producer) => { this._createConsumer( { @@ -354,14 +441,14 @@ class Room extends EventEmitter producerPeer : joinedPeer, producer }); - } + }); } }); cb(null, { peers: peerInfos }); // Mark the new Peer as joined. - peer.data.joined = true; + peer.joined = true; this._notification( peer.socket, @@ -376,8 +463,8 @@ class Room extends EventEmitter ); logger.debug( - 'peer joined [peeerId: %s, displayName: %s, picture: %s, device: %o]', - peer.id, displayName, picture, device); + 'peer joined [peer: "%s", displayName: "%s", picture: "%s"]', + peer.id, displayName, picture); break; } @@ -403,8 +490,8 @@ class Room extends EventEmitter appData : { producing, consuming } }); - // Store the WebRtcTransport into the protoo Peer data Object. - peer.data.transports.set(transport.id, transport); + // Store the WebRtcTransport into the Peer data Object. + peer.addTransport(transport.id, transport); cb( null, @@ -428,7 +515,7 @@ class Room extends EventEmitter case 'connectWebRtcTransport': { const { transportId, dtlsParameters } = request.data; - const transport = peer.data.transports.get(transportId); + const transport = peer.getTransport(transportId); if (!transport) throw new Error(`transport with id "${transportId}" not found`); @@ -443,7 +530,7 @@ class Room extends EventEmitter case 'restartIce': { const { transportId } = request.data; - const transport = peer.data.transports.get(transportId); + const transport = peer.getTransport(transportId); if (!transport) throw new Error(`transport with id "${transportId}" not found`); @@ -458,12 +545,12 @@ class Room extends EventEmitter case 'produce': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { transportId, kind, rtpParameters } = request.data; let { appData } = request.data; - const transport = peer.data.transports.get(transportId); + const transport = peer.getTransport(transportId); if (!transport) throw new Error(`transport with id "${transportId}" not found`); @@ -475,23 +562,19 @@ class Room extends EventEmitter const producer = await transport.produce({ kind, rtpParameters, appData }); - // Store the Producer into the protoo Peer data Object. - peer.data.producers.set(producer.id, producer); + // Store the Producer into the Peer data Object. + peer.addProducer(producer.id, producer); // Set Producer events. producer.on('score', (score) => { - // logger.debug( - // 'producer "score" event [producerId:%s, score:%o]', - // producer.id, score); - this._notification(peer.socket, 'producerScore', { producerId: producer.id, score }); }); producer.on('videoorientationchange', (videoOrientation) => { logger.debug( - 'producer "videoorientationchange" event [producerId:%s, videoOrientation:%o]', + 'producer "videoorientationchange" event [producerId:"%s", videoOrientation:"%o"]', producer.id, videoOrientation); }); @@ -499,7 +582,7 @@ class Room extends EventEmitter this._peers.forEach((otherPeer) => { - if (otherPeer.data.joined && otherPeer !== peer) + if (otherPeer.joined && otherPeer !== peer) { this._createConsumer( { @@ -523,11 +606,11 @@ class Room extends EventEmitter case 'closeProducer': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { producerId } = request.data; - const producer = peer.data.producers.get(producerId); + const producer = peer.getProducer(producerId); if (!producer) throw new Error(`producer with id "${producerId}" not found`); @@ -535,7 +618,7 @@ class Room extends EventEmitter producer.close(); // Remove from its map. - peer.data.producers.delete(producer.id); + peer.removeProducer(producer.id); cb(); @@ -545,11 +628,11 @@ class Room extends EventEmitter case 'pauseProducer': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { producerId } = request.data; - const producer = peer.data.producers.get(producerId); + const producer = peer.getProducer(producerId); if (!producer) throw new Error(`producer with id "${producerId}" not found`); @@ -564,11 +647,11 @@ class Room extends EventEmitter case 'resumeProducer': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { producerId } = request.data; - const producer = peer.data.producers.get(producerId); + const producer = peer.getProducer(producerId); if (!producer) throw new Error(`producer with id "${producerId}" not found`); @@ -583,11 +666,11 @@ class Room extends EventEmitter case 'pauseConsumer': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { consumerId } = request.data; - const consumer = peer.data.consumers.get(consumerId); + const consumer = peer.getConsumer(consumerId); if (!consumer) throw new Error(`consumer with id "${consumerId}" not found`); @@ -602,11 +685,11 @@ class Room extends EventEmitter case 'resumeConsumer': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { consumerId } = request.data; - const consumer = peer.data.consumers.get(consumerId); + const consumer = peer.getConsumer(consumerId); if (!consumer) throw new Error(`consumer with id "${consumerId}" not found`); @@ -621,11 +704,11 @@ class Room extends EventEmitter case 'setConsumerPreferedLayers': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { consumerId, spatialLayer, temporalLayer } = request.data; - const consumer = peer.data.consumers.get(consumerId); + const consumer = peer.getConsumer(consumerId); if (!consumer) throw new Error(`consumer with id "${consumerId}" not found`); @@ -640,11 +723,11 @@ class Room extends EventEmitter case 'requestConsumerKeyFrame': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { consumerId } = request.data; - const consumer = peer.data.consumers.get(consumerId); + const consumer = peer.getConsumer(consumerId); if (!consumer) throw new Error(`consumer with id "${consumerId}" not found`); @@ -659,7 +742,7 @@ class Room extends EventEmitter case 'getTransportStats': { const { transportId } = request.data; - const transport = peer.data.transports.get(transportId); + const transport = peer.getTransport(transportId); if (!transport) throw new Error(`transport with id "${transportId}" not found`); @@ -674,7 +757,7 @@ class Room extends EventEmitter case 'getProducerStats': { const { producerId } = request.data; - const producer = peer.data.producers.get(producerId); + const producer = peer.getProducer(producerId); if (!producer) throw new Error(`producer with id "${producerId}" not found`); @@ -689,7 +772,7 @@ class Room extends EventEmitter case 'getConsumerStats': { const { consumerId } = request.data; - const consumer = peer.data.consumers.get(consumerId); + const consumer = peer.getConsumer(consumerId); if (!consumer) throw new Error(`consumer with id "${consumerId}" not found`); @@ -704,13 +787,13 @@ class Room extends EventEmitter case 'changeDisplayName': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { displayName } = request.data; - const oldDisplayName = peer.data.displayName; + const oldDisplayName = peer.displayName; - peer.data.displayName = displayName; + peer.displayName = displayName; // Spread to others this._notification(peer.socket, 'changeDisplayName', { @@ -725,16 +808,18 @@ class Room extends EventEmitter break; } - case 'changeProfilePicture': + case 'changePicture': { // Ensure the Peer is joined. - if (!peer.data.joined) + if (!peer.joined) throw new Error('Peer not yet joined'); const { picture } = request.data; + peer.picture = picture; + // Spread to others - this._notification(peer.socket, 'changeProfilePicture', { + this._notification(peer.socket, 'changePicture', { peerId : peer.id, picture : picture }, true); @@ -766,12 +851,17 @@ class Room extends EventEmitter case 'serverHistory': { // Return to sender + const lobbyPeers = this._lobby.peerList(); + cb( null, { - chatHistory : this._chatHistory, - fileHistory : this._fileHistory, - lastN : this._lastN + chatHistory : this._chatHistory, + fileHistory : this._fileHistory, + lastNHistory : this._lastN, + locked : this._locked, + lobbyPeers : lobbyPeers, + accessCode : this._accessCode } ); @@ -801,6 +891,67 @@ class Room extends EventEmitter this._notification(peer.socket, 'unlockRoom', { peerId : peer.id }, true); + this._lobby.promoteAllPeers(); + + // Return no error + cb(); + + break; + } + + case 'setAccessCode': + { + const { accessCode } = request.data; + + this._accessCode = accessCode; + + // Spread to others + // if (request.public) { + this._notification(peer.socket, 'setAccessCode', { + peerId : peer.id, + accessCode : accessCode + }, true); + // } + + // Return no error + cb(); + + break; + } + + case 'setJoinByAccessCode': + { + const { joinByAccessCode } = request.data; + + this._joinByAccessCode = joinByAccessCode; + + // Spread to others + this._notification(peer.socket, 'setJoinByAccessCode', { + peerId : peer.id, + joinByAccessCode : joinByAccessCode + }, true); + + // Return no error + cb(); + + break; + } + + case 'promotePeer': + { + const { peerId } = request.data; + + this._lobby.promotePeer(peerId); + + // Return no error + cb(); + + break; + } + + case 'promoteAllPeers': + { + this._lobby.promoteAllPeers(); // Return no error cb(); @@ -828,14 +979,14 @@ class Room extends EventEmitter case 'raiseHand': { - const { raiseHandState } = request.data; + const { raisedHand } = request.data; - peer.data.raiseHandState = raiseHandState; + peer.raisedHand = raisedHand; // Spread to others this._notification(peer.socket, 'raiseHand', { - peerId : peer.id, - raiseHandState : raiseHandState + peerId : peer.id, + raisedHand : raisedHand }, true); // Return no error @@ -860,6 +1011,13 @@ class Room extends EventEmitter */ async _createConsumer({ consumerPeer, producerPeer, producer }) { + logger.debug( + '_createConsumer() [consumerPeer:"%s", producerPeer:"%s", producer:"%s"]', + consumerPeer.id, + producerPeer.id, + producer.id + ); + // Optimization: // - Create the server-side Consumer. If video, do it paused. // - Tell its Peer about it and wait for its response. @@ -869,11 +1027,11 @@ class Room extends EventEmitter // NOTE: Don't create the Consumer if the remote Peer cannot consume it. if ( - !consumerPeer.data.rtpCapabilities || + !consumerPeer.rtpCapabilities || !this._mediasoupRouter.canConsume( { producerId : producer.id, - rtpCapabilities : consumerPeer.data.rtpCapabilities + rtpCapabilities : consumerPeer.rtpCapabilities }) ) { @@ -881,8 +1039,7 @@ class Room extends EventEmitter } // Must take the Transport the remote Peer is using for consuming. - const transport = Array.from(consumerPeer.data.transports.values()) - .find((t) => t.appData.consuming); + const transport = consumerPeer.getConsumerTransport(); // This should not happen. if (!transport) @@ -900,31 +1057,31 @@ class Room extends EventEmitter consumer = await transport.consume( { producerId : producer.id, - rtpCapabilities : consumerPeer.data.rtpCapabilities, + rtpCapabilities : consumerPeer.rtpCapabilities, paused : producer.kind === 'video' }); } catch (error) { - logger.warn('_createConsumer() | transport.consume():%o', error); + logger.warn('_createConsumer() | [error:"%o"]', error); return; } - // Store the Consumer into the protoo consumerPeer data Object. - consumerPeer.data.consumers.set(consumer.id, consumer); + // Store the Consumer into the consumerPeer data Object. + consumerPeer.addConsumer(consumer.id, consumer); // Set Consumer events. consumer.on('transportclose', () => { // Remove from its map. - consumerPeer.data.consumers.delete(consumer.id); + consumerPeer.removeConsumer(consumer.id); }); consumer.on('producerclose', () => { // Remove from its map. - consumerPeer.data.consumers.delete(consumer.id); + consumerPeer.removeConsumer(consumer.id); this._notification(consumerPeer.socket, 'consumerClosed', { consumerId: consumer.id }); }); @@ -941,10 +1098,6 @@ class Room extends EventEmitter consumer.on('score', (score) => { - // logger.debug( - // 'consumer "score" event [consumerId:%s, score:%o]', - // consumer.id, score); - this._notification(consumerPeer.socket, 'consumerScore', { consumerId: consumer.id, score }); }); @@ -961,7 +1114,7 @@ class Room extends EventEmitter ); }); - // Send a protoo request to the remote Peer with Consumer parameters. + // Send a request to the remote Peer with Consumer parameters. try { await this._request( @@ -972,7 +1125,6 @@ class Room extends EventEmitter kind : producer.kind, producerId : producer.id, id : consumer.id, - kind : consumer.kind, rtpParameters : consumer.rtpParameters, type : consumer.type, appData : producer.appData, @@ -996,7 +1148,7 @@ class Room extends EventEmitter } catch (error) { - logger.warn('_createConsumer() | failed:%o', error); + logger.warn('_createConsumer() | [error:"%o"]', error); } } diff --git a/server/mediasoup.js b/server/mediasoup.js deleted file mode 100644 index 99a4862..0000000 --- a/server/mediasoup.js +++ /dev/null @@ -1,321 +0,0 @@ -const mediasoup = require('mediasoup'); -const readline = require('readline'); -const colors = require('colors/safe'); -const repl = require('repl'); -const homer = require('./lib/homer'); -const config = require('./config/config'); - -// mediasoup server. -const mediaServer = mediasoup.Server( - { - numWorkers : null, - logLevel : config.mediasoup.logLevel, - logTags : config.mediasoup.logTags, - rtcIPv4 : config.mediasoup.rtcIPv4, - rtcIPv6 : config.mediasoup.rtcIPv6, - rtcAnnouncedIPv4 : config.mediasoup.rtcAnnouncedIPv4, - rtcAnnouncedIPv6 : config.mediasoup.rtcAnnouncedIPv6, - rtcMinPort : config.mediasoup.rtcMinPort, - rtcMaxPort : config.mediasoup.rtcMaxPort - }); - -// Do Homer stuff. -if (process.env.MEDIASOUP_HOMER_OUTPUT) - homer(mediaServer); - -global.SERVER = mediaServer; - -mediaServer.on('newroom', (room) => -{ - global.ROOM = room; - - room.on('newpeer', (peer) => - { - global.PEER = peer; - - if (peer.consumers.length > 0) - global.CONSUMER = peer.consumers[peer.consumers.length - 1]; - - peer.on('newtransport', (transport) => - { - global.TRANSPORT = transport; - }); - - peer.on('newproducer', (producer) => - { - global.PRODUCER = producer; - }); - - peer.on('newconsumer', (consumer) => - { - global.CONSUMER = consumer; - }); - }); -}); - -// Listen for keyboard input. - -let cmd; -let terminal; - -openCommandConsole(); - -function openCommandConsole() -{ - stdinLog('[opening Readline Command Console...]'); - - closeCommandConsole(); - closeTerminal(); - - cmd = readline.createInterface( - { - input : process.stdin, - output : process.stdout - }); - - cmd.on('SIGINT', () => - { - process.exit(); - }); - - readStdin(); - - function readStdin() - { - cmd.question('cmd> ', (answer) => - { - switch (answer) - { - case '': - { - readStdin(); - break; - } - - case 'h': - case 'help': - { - stdinLog(''); - stdinLog('available commands:'); - stdinLog('- h, help : show this message'); - stdinLog('- sd, serverdump : execute server.dump()'); - stdinLog('- rd, roomdump : execute room.dump() for the latest created mediasoup Room'); - stdinLog('- pd, peerdump : execute peer.dump() for the latest created mediasoup Peer'); - stdinLog('- td, transportdump : execute transport.dump() for the latest created mediasoup Transport'); - stdinLog('- prd, producerdump : execute producer.dump() for the latest created mediasoup Producer'); - stdinLog('- cd, consumerdump : execute consumer.dump() for the latest created mediasoup Consumer'); - stdinLog('- t, terminal : open REPL Terminal'); - stdinLog(''); - readStdin(); - - break; - } - - case 'sd': - case 'serverdump': - { - mediaServer.dump() - .then((data) => - { - stdinLog(`server.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`); - readStdin(); - }) - .catch((error) => - { - stdinError(`mediaServer.dump() failed: ${error}`); - readStdin(); - }); - - break; - } - - case 'rd': - case 'roomdump': - { - if (!global.ROOM) - { - readStdin(); - break; - } - - global.ROOM.dump() - .then((data) => - { - stdinLog(`room.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`); - readStdin(); - }) - .catch((error) => - { - stdinError(`room.dump() failed: ${error}`); - readStdin(); - }); - - break; - } - - case 'pd': - case 'peerdump': - { - if (!global.PEER) - { - readStdin(); - break; - } - - global.PEER.dump() - .then((data) => - { - stdinLog(`peer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`); - readStdin(); - }) - .catch((error) => - { - stdinError(`peer.dump() failed: ${error}`); - readStdin(); - }); - - break; - } - - case 'td': - case 'transportdump': - { - if (!global.TRANSPORT) - { - readStdin(); - break; - } - - global.TRANSPORT.dump() - .then((data) => - { - stdinLog(`transport.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`); - readStdin(); - }) - .catch((error) => - { - stdinError(`transport.dump() failed: ${error}`); - readStdin(); - }); - - break; - } - - case 'prd': - case 'producerdump': - { - if (!global.PRODUCER) - { - readStdin(); - break; - } - - global.PRODUCER.dump() - .then((data) => - { - stdinLog(`producer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`); - readStdin(); - }) - .catch((error) => - { - stdinError(`producer.dump() failed: ${error}`); - readStdin(); - }); - - break; - } - - case 'cd': - case 'consumerdump': - { - if (!global.CONSUMER) - { - readStdin(); - break; - } - - global.CONSUMER.dump() - .then((data) => - { - stdinLog(`consumer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`); - readStdin(); - }) - .catch((error) => - { - stdinError(`consumer.dump() failed: ${error}`); - readStdin(); - }); - - break; - } - - case 't': - case 'terminal': - { - openTerminal(); - - break; - } - - default: - { - stdinError(`unknown command: ${answer}`); - stdinLog('press \'h\' or \'help\' to get the list of available commands'); - - readStdin(); - } - } - }); - } -} - -function openTerminal() -{ - stdinLog('[opening REPL Terminal...]'); - - closeCommandConsole(); - closeTerminal(); - - terminal = repl.start( - { - prompt : 'terminal> ', - useColors : true, - useGlobal : true, - ignoreUndefined : false - }); - - terminal.on('exit', () => openCommandConsole()); -} - -function closeCommandConsole() -{ - if (cmd) - { - cmd.close(); - cmd = undefined; - } -} - -function closeTerminal() -{ - if (terminal) - { - terminal.removeAllListeners('exit'); - terminal.close(); - terminal = undefined; - } -} - -function stdinLog(msg) -{ - // eslint-disable-next-line no-console - console.log(colors.green(msg)); -} - -function stdinError(msg) -{ - // eslint-disable-next-line no-console - console.error(colors.red.bold('ERROR: ') + colors.red(msg)); -} - -module.exports = mediaServer; diff --git a/server/package.json b/server/package.json index 1762e0c..2079986 100644 --- a/server/package.json +++ b/server/package.json @@ -1,28 +1,29 @@ { - "name": "multiparty-meeting-server", - "version": "3.0.0", - "private": true, - "description": "multiparty meeting server", - "author": "Håvar Aambø Fosstveit ", - "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": "^3.0.12", - "openid-client": "^2.5.0", - "passport": "^0.4.0", - "socket.io": "^2.1.1", - "spdy": "^4.0.0" - }, - "devDependencies": { - "gulp": "^4.0.0", - "gulp-eslint": "^5.0.0", - "gulp-plumber": "^1.2.0" - } + "name": "multiparty-meeting-server", + "version": "3.0.0", + "private": true, + "description": "multiparty meeting server", + "author": "Håvar Aambø Fosstveit ", + "license": "MIT", + "main": "lib/index.js", + "dependencies": { + "awaitqueue": "^1.0.0", + "base-64": "^0.1.0", + "body-parser": "^1.19.0", + "colors": "^1.4.0", + "compression": "^1.7.4", + "connect-redis": "^4.0.3", + "cookie-parser": "^1.4.4", + "debug": "^4.1.1", + "express": "^4.17.1", + "express-session": "^1.17.0", + "express-socket.io-session": "^1.3.5", + "helmet": "^3.21.2", + "mediasoup": "^3.0.12", + "openid-client": "^3.7.3", + "passport": "^0.4.0", + "redis": "^2.8.0", + "socket.io": "^2.3.0", + "spdy": "^4.0.1" + } } diff --git a/server/server.js b/server/server.js index 366181e..f035a65 100755 --- a/server/server.js +++ b/server/server.js @@ -1,7 +1,5 @@ #!/usr/bin/env node -'use strict'; - process.title = 'multiparty-meeting-server'; const config = require('./config/config'); @@ -9,17 +7,28 @@ const fs = require('fs'); const http = require('http'); const spdy = require('spdy'); const express = require('express'); +const bodyParser = require('body-parser'); +const cookieParser = require('cookie-parser'); 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'); +const Peer = require('./lib/Peer'); const base64 = require('base-64'); +const helmet = require('helmet'); +const { + loginHelper, + logoutHelper +} = require('./httpHelper'); // auth const passport = require('passport'); +const redis = require('redis'); +const client = redis.createClient(); const { Issuer, Strategy } = require('openid-client'); -const session = require('express-session'); +const expressSession = require('express-session'); +const RedisStore = require('connect-redis')(expressSession); +const sharedSession = require('express-socket.io-session'); /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); @@ -42,17 +51,51 @@ let nextMediasoupWorkerIdx = 0; // Map of Room instances indexed by roomId. const rooms = new Map(); +// Map of Peer instances indexed by peerId. +const peers = new Map(); + // TLS server configuration. const tls = { - cert : fs.readFileSync(config.tls.cert), - key : fs.readFileSync(config.tls.key) + cert : fs.readFileSync(config.tls.cert), + key : fs.readFileSync(config.tls.key), + secureOptions : 'tlsv12', + ciphers : + [ + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-CHACHA20-POLY1305', + 'ECDHE-RSA-CHACHA20-POLY1305', + 'DHE-RSA-AES128-GCM-SHA256', + 'DHE-RSA-AES256-GCM-SHA384' + ].join(':'), + honorCipherOrder : true }; const app = express(); -let httpsServer; -let oidcClient; -let oidcStrategy; + +app.use(helmet.hsts()); + +app.use(cookieParser()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +const session = expressSession({ + secret : config.cookieSecret, + name : config.cookieName, + resave : true, + saveUninitialized : true, + store : new RedisStore({ client }), + cookie : { + secure : true, + httpOnly : true, + maxAge : 60 * 60 * 1000 // Expire after 1 hour since last request from user + } +}); + +app.use(session); passport.serializeUser((user, done) => { @@ -64,17 +107,22 @@ passport.deserializeUser((user, done) => done(null, user); }); +let httpsServer; +let io; +let oidcClient; +let oidcStrategy; + const auth = config.auth; async function run() { - if ( + if ( typeof(auth) !== 'undefined' && typeof(auth.issuerURL) !== 'undefined' && typeof(auth.clientOptions) !== 'undefined' ) { - Issuer.discover(auth.issuerURL).then( async (oidcIssuer) => + Issuer.discover(auth.issuerURL).then(async (oidcIssuer) => { // Setup authentication await setupAuth(oidcIssuer); @@ -89,8 +137,8 @@ async function run() await runWebSocketServer(); }) .catch((err) => - { - logger.error(err); + { + logger.error(err); }); } else @@ -115,6 +163,15 @@ async function run() room.logStatus(); } }, 120000); + + // check for deserted rooms + setInterval(() => + { + for (const room of rooms.values()) + { + room.checkEmpty(); + } + }, 10000); } async function setupAuth(oidcIssuer) @@ -130,16 +187,15 @@ async function setupAuth(oidcIssuer) // optional, defaults to false, when true req is passed as a first // argument to verify fn - const passReqToCallback = false; + const passReqToCallback = false; // optional, defaults to false, when true the code_challenge_method will be // 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; oidcStrategy = new Strategy( - { client, params, passReqToCallback, usePKCE }, + { client: oidcClient, params, passReqToCallback, usePKCE }, (tokenset, userinfo, done) => { const user = @@ -150,15 +206,15 @@ async function setupAuth(oidcIssuer) _claims : tokenset.claims }; - if (typeof(userinfo.picture) !== 'undefined') + if (userinfo.picture != null) { if (!userinfo.picture.match(/^http/g)) { - user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ]; + user.picture = `data:image/jpeg;base64, ${userinfo.picture}`; } else { - user.Photos = [ { value: userinfo.picture } ]; + user.picture = userinfo.picture; } } @@ -174,22 +230,22 @@ async function setupAuth(oidcIssuer) if (userinfo.email != null) { - user.emails = [ { value: userinfo.email } ]; + user.email = userinfo.email; } if (userinfo.given_name != null) { - user.name = { givenName: userinfo.given_name }; + user.name.givenName = userinfo.given_name; } if (userinfo.family_name != null) { - user.name = { familyName: userinfo.family_name }; + user.name.familyName = userinfo.family_name; } if (userinfo.middle_name != null) { - user.name = { middleName: userinfo.middle_name }; + user.name.middleName = userinfo.middle_name; } return done(null, user); @@ -198,24 +254,15 @@ async function setupAuth(oidcIssuer) passport.use('oidc', oidcStrategy); - app.use(session({ - secret : config.cookieSecret, - resave : true, - saveUninitialized : true, - cookie : { secure: true } - })); - app.use(passport.initialize()); app.use(passport.session()); - // login + // loginparams app.get('/auth/login', (req, res, next) => { passport.authenticate('oidc', { state : base64.encode(JSON.stringify({ - roomId : req.query.roomId, - peerId : req.query.peerId, - code : utils.random(10) + id : req.query.id })) })(req, res, next); }); @@ -224,7 +271,7 @@ async function setupAuth(oidcIssuer) app.get('/auth/logout', (req, res) => { req.logout(); - res.redirect('/'); + res.send(logoutHelper()); }); // callback @@ -235,41 +282,32 @@ async function setupAuth(oidcIssuer) { const state = JSON.parse(base64.decode(req.query.state)); - if (rooms.has(state.roomId)) + let displayName; + let picture; + + if (req.user != null) { - let displayName; - let photo; + if (req.user.displayName != null) + displayName = req.user.displayName; + else + displayName = ''; - if (req.user != null) - { - if (req.user.displayName != null) - displayName = req.user.displayName; - else - displayName = ''; - - if ( - req.user.Photos != null && - req.user.Photos[0] != null && - req.user.Photos[0].value != null - ) - photo = req.user.Photos[0].value; - else - photo = '/static/media/buddy.403cb9f6.svg'; - } - - const data = - { - peerId : state.peerId, - displayName : displayName, - picture : photo - }; - - const room = rooms.get(state.roomId); - - room.authCallback(data); + if (req.user.picture != null) + picture = req.user.picture; + else + picture = '/static/media/buddy.403cb9f6.svg'; } - res.send(''); + const peer = peers.get(state.id); + + peer && (peer.authenticated = true); + peer && (peer.displayName = displayName); + peer && (peer.picture = picture); + + res.send(loginHelper({ + displayName, + picture + })); } ); } @@ -316,11 +354,17 @@ async function runHttpsServer() } /** - * Create a protoo WebSocketServer to allow WebSocket connections from browsers. + * Create a WebSocketServer to allow WebSocket connections from browsers. */ async function runWebSocketServer() { - const io = require('socket.io')(httpsServer); + io = require('socket.io')(httpsServer); + + io.use( + sharedSession(session, { + autoSave : true + }) + ); // Handle connections from clients. io.on('connection', (socket) => @@ -342,17 +386,22 @@ async function runWebSocketServer() queue.push(async () => { const room = await getOrCreateRoom({ roomId }); + const peer = new Peer({ id: peerId, socket }); - room.handleConnection({ peerId, socket }); + peers.set(peerId, peer); + + peer.on('close', () => peers.delete(peerId)); + + room.handlePeer(peer); }) - .catch((error) => - { - logger.error('room creation or room joining failed:%o', error); + .catch((error) => + { + logger.error('room creation or room joining failed [error:"%o"]', error); - socket.disconnect(true); + socket.disconnect(true); - return; - }); + return; + }); }); } @@ -410,7 +459,7 @@ async function getOrCreateRoom({ roomId }) // If the Room does not exist create a new one. if (!room) { - logger.info('creating a new Room [roomId:%s]', roomId); + logger.info('creating a new Room [roomId:"%s"]', roomId); const mediasoupWorker = getMediasoupWorker(); diff --git a/server/util.js b/server/util.js deleted file mode 100644 index cfa4548..0000000 --- a/server/util.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var crypto = require('crypto'); - -exports.random = function (howMany, chars) { - chars = chars - || "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789"; - var rnd = crypto.randomBytes(howMany) - , value = new Array(howMany) - , len = len = Math.min(256, chars.length) - , d = 256 / len - - for (var i = 0; i < howMany; i++) { - value[i] = chars[Math.floor(rnd[i] / d)] - }; - - return value.join(''); -}