Merge branch 'develop'

master
Håvar Aambø Fosstveit 2019-11-07 10:13:25 +01:00
commit 0d705abf11
78 changed files with 5484 additions and 3579 deletions

View File

@ -1,5 +1,14 @@
# Changelog
### 3.1
* Browser session storage
* Virtual lobby for rooms
* Allow minimum TLSv1.2 and recommended ciphers
* Code splitting for faster load times
* Various GUI fixes
* Internationalization support
* Can require sign in for access
### 3.0
* Updated to mediasoup v3
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)

View File

@ -10,8 +10,7 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci
* Screen sharing
* File sharing
* Different layouts
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no.
* Internationalization support
## Docker
If you want the automatic approach, you can find a docker image [here](https://hub.docker.com/r/misi/mm/).
@ -37,7 +36,7 @@ $ cp server/config/config.example.js server/config/config.js
$ cp app/public/config/config.example.js app/public/config/config.js
```
* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).
* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, don't forget ip setting in last section in server config: (webRtcTransport), etc).
* Set up the browser app:
@ -51,6 +50,7 @@ This will build the client application and copy everythink to `server/public` fr
* Set up the server:
```bash
$ sudo apt install redis
$ cd ..
$ cd server
$ npm install
@ -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 Unions 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.

333
app/.eslintrc.json 100644
View File

@ -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
}
]
}
}

View File

@ -1,37 +1,38 @@
{
"name": "multiparty-meeting",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"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",
"dompurify": "^2.0.7",
"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-intl": "^3.4.0",
"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 +42,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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
export const addUserMessage = (text) =>
({
type : 'ADD_NEW_USER_MESSAGE',
payload : { text }
});
export const addResponseMessage = (message) =>
({
type : 'ADD_NEW_RESPONSE_MESSAGE',
payload : { message }
});
export const addChatHistory = (chatHistory) =>
({
type : 'ADD_CHAT_HISTORY',
payload : { chatHistory }
});

View File

@ -0,0 +1,47 @@
export const addConsumer = (consumer, peerId) =>
({
type : 'ADD_CONSUMER',
payload : { consumer, peerId }
});
export const removeConsumer = (consumerId, peerId) =>
({
type : 'REMOVE_CONSUMER',
payload : { consumerId, peerId }
});
export const setConsumerPaused = (consumerId, originator) =>
({
type : 'SET_CONSUMER_PAUSED',
payload : { consumerId, originator }
});
export const setConsumerResumed = (consumerId, originator) =>
({
type : 'SET_CONSUMER_RESUMED',
payload : { consumerId, originator }
});
export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
({
type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
});
export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
({
type : 'SET_CONSUMER_PREFERRED_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
});
export const setConsumerTrack = (consumerId, track) =>
({
type : 'SET_CONSUMER_TRACK',
payload : { consumerId, track }
});
export const setConsumerScore = (consumerId, score) =>
({
type : 'SET_CONSUMER_SCORE',
payload : { consumerId, score }
});

View File

@ -0,0 +1,35 @@
export const addFile = (peerId, magnetUri) =>
({
type : 'ADD_FILE',
payload : { peerId, magnetUri }
});
export const addFileHistory = (fileHistory) =>
({
type : 'ADD_FILE_HISTORY',
payload : { fileHistory }
});
export const setFileActive = (magnetUri) =>
({
type : 'SET_FILE_ACTIVE',
payload : { magnetUri }
});
export const setFileInActive = (magnetUri) =>
({
type : 'SET_FILE_INACTIVE',
payload : { magnetUri }
});
export const setFileProgress = (magnetUri, progress) =>
({
type : 'SET_FILE_PROGRESS',
payload : { magnetUri, progress }
});
export const setFileDone = (magnetUri, sharedFiles) =>
({
type : 'SET_FILE_DONE',
payload : { magnetUri, sharedFiles }
});

View File

@ -0,0 +1,29 @@
export const addLobbyPeer = (peerId) =>
({
type : 'ADD_LOBBY_PEER',
payload : { peerId }
});
export const removeLobbyPeer = (peerId) =>
({
type : 'REMOVE_LOBBY_PEER',
payload : { peerId }
});
export const setLobbyPeerDisplayName = (displayName, peerId) =>
({
type : 'SET_LOBBY_PEER_DISPLAY_NAME',
payload : { displayName, peerId }
});
export const setLobbyPeerPicture = (picture, peerId) =>
({
type : 'SET_LOBBY_PEER_PICTURE',
payload : { picture, peerId }
});
export const setLobbyPeerPromotionInProgress = (peerId, flag) =>
({
type : 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS',
payload : { peerId, flag }
});

View File

@ -0,0 +1,76 @@
export const setMe = ({ peerId, loginEnabled }) =>
({
type : 'SET_ME',
payload : { peerId, loginEnabled }
});
export const loggedIn = (flag) =>
({
type : 'LOGGED_IN',
payload : { flag }
});
export const setPicture = (picture) =>
({
type : 'SET_PICTURE',
payload : { picture }
});
export const setMediaCapabilities = ({
canSendMic,
canSendWebcam,
canShareScreen,
canShareFiles
}) =>
({
type : 'SET_MEDIA_CAPABILITIES',
payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles }
});
export const setAudioDevices = (devices) =>
({
type : 'SET_AUDIO_DEVICES',
payload : { devices }
});
export const setWebcamDevices = (devices) =>
({
type : 'SET_WEBCAM_DEVICES',
payload : { devices }
});
export const setMyRaiseHandState = (flag) =>
({
type : 'SET_MY_RAISE_HAND_STATE',
payload : { flag }
});
export const setAudioInProgress = (flag) =>
({
type : 'SET_AUDIO_IN_PROGRESS',
payload : { flag }
});
export const setWebcamInProgress = (flag) =>
({
type : 'SET_WEBCAM_IN_PROGRESS',
payload : { flag }
});
export const setScreenShareInProgress = (flag) =>
({
type : 'SET_SCREEN_SHARE_IN_PROGRESS',
payload : { flag }
});
export const setMyRaiseHandStateInProgress = (flag) =>
({
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS',
payload : { flag }
});
export const setDisplayNameInProgress = (flag) =>
({
type : 'SET_DISPLAY_NAME_IN_PROGRESS',
payload : { flag }
});

View File

@ -0,0 +1,16 @@
export const addNotification = (notification) =>
({
type : 'ADD_NOTIFICATION',
payload : { notification }
});
export const removeNotification = (notificationId) =>
({
type : 'REMOVE_NOTIFICATION',
payload : { notificationId }
});
export const removeAllNotifications = () =>
({
type : 'REMOVE_ALL_NOTIFICATIONS'
});

View File

@ -0,0 +1,47 @@
export const addPeer = (peer) =>
({
type : 'ADD_PEER',
payload : { peer }
});
export const removePeer = (peerId) =>
({
type : 'REMOVE_PEER',
payload : { peerId }
});
export const setPeerDisplayName = (displayName, peerId) =>
({
type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerId }
});
export const setPeerVideoInProgress = (peerId, flag) =>
({
type : 'SET_PEER_VIDEO_IN_PROGRESS',
payload : { peerId, flag }
});
export const setPeerAudioInProgress = (peerId, flag) =>
({
type : 'SET_PEER_AUDIO_IN_PROGRESS',
payload : { peerId, flag }
});
export const setPeerScreenInProgress = (peerId, flag) =>
({
type : 'SET_PEER_SCREEN_IN_PROGRESS',
payload : { peerId, flag }
});
export const setPeerRaiseHandState = (peerId, raiseHandState) =>
({
type : 'SET_PEER_RAISE_HAND_STATE',
payload : { peerId, raiseHandState }
});
export const setPeerPicture = (peerId, picture) =>
({
type : 'SET_PEER_PICTURE',
payload : { peerId, picture }
});

View File

@ -0,0 +1,5 @@
export const setPeerVolume = (peerId, volume) =>
({
type : 'SET_PEER_VOLUME',
payload : { peerId, volume }
});

View File

@ -0,0 +1,35 @@
export const addProducer = (producer) =>
({
type : 'ADD_PRODUCER',
payload : { producer }
});
export const removeProducer = (producerId) =>
({
type : 'REMOVE_PRODUCER',
payload : { producerId }
});
export const setProducerPaused = (producerId, originator) =>
({
type : 'SET_PRODUCER_PAUSED',
payload : { producerId, originator }
});
export const setProducerResumed = (producerId, originator) =>
({
type : 'SET_PRODUCER_RESUMED',
payload : { producerId, originator }
});
export const setProducerTrack = (producerId, track) =>
({
type : 'SET_PRODUCER_TRACK',
payload : { producerId, track }
});
export const setProducerScore = (producerId, score) =>
({
type : 'SET_PRODUCER_SCORE',
payload : { producerId, score }
});

View File

@ -1,5 +1,5 @@
import randomString from 'random-string';
import * as stateActions from './stateActions';
import * as notificationActions from './notificationActions';
// This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) =>
@ -30,11 +30,11 @@ export const notify = ({ type = 'info', text, timeout }) =>
return (dispatch) =>
{
dispatch(stateActions.addNotification(notification));
dispatch(notificationActions.addNotification(notification));
setTimeout(() =>
{
dispatch(stateActions.removeNotification(notification.id));
dispatch(notificationActions.removeNotification(notification.id));
}, timeout);
};
};

View File

@ -0,0 +1,118 @@
export const setRoomUrl = (url) =>
({
type : 'SET_ROOM_URL',
payload : { url }
});
export const setRoomName = (name) =>
({
type : 'SET_ROOM_NAME',
payload : { name }
});
export const setRoomState = (state) =>
({
type : 'SET_ROOM_STATE',
payload : { state }
});
export const setRoomActiveSpeaker = (peerId) =>
({
type : 'SET_ROOM_ACTIVE_SPEAKER',
payload : { peerId }
});
export const setRoomLocked = () =>
({
type : 'SET_ROOM_LOCKED'
});
export const setRoomUnLocked = () =>
({
type : 'SET_ROOM_UNLOCKED'
});
export const setInLobby = (inLobby) =>
({
type : 'SET_IN_LOBBY',
payload : { inLobby }
});
export const setSignInRequired = (signInRequired) =>
({
type : 'SET_SIGN_IN_REQUIRED',
payload : { signInRequired }
});
export const setAccessCode = (accessCode) =>
({
type : 'SET_ACCESS_CODE',
payload : { accessCode }
});
export const setJoinByAccessCode = (joinByAccessCode) =>
({
type : 'SET_JOIN_BY_ACCESS_CODE',
payload : { joinByAccessCode }
});
export const setSettingsOpen = ({ settingsOpen }) =>
({
type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen }
});
export const setLockDialogOpen = ({ lockDialogOpen }) =>
({
type : 'SET_LOCK_DIALOG_OPEN',
payload : { lockDialogOpen }
});
export const setFileSharingSupported = (supported) =>
({
type : 'FILE_SHARING_SUPPORTED',
payload : { supported }
});
export const toggleConsumerWindow = (consumerId) =>
({
type : 'TOGGLE_WINDOW_CONSUMER',
payload : { consumerId }
});
export const setToolbarsVisible = (toolbarsVisible) =>
({
type : 'SET_TOOLBARS_VISIBLE',
payload : { toolbarsVisible }
});
export const setDisplayMode = (mode) =>
({
type : 'SET_DISPLAY_MODE',
payload : { mode }
});
export const setSelectedPeer = (selectedPeerId) =>
({
type : 'SET_SELECTED_PEER',
payload : { selectedPeerId }
});
export const setSpotlights = (spotlights) =>
({
type : 'SET_SPOTLIGHTS',
payload : { spotlights }
});
export const toggleJoined = () =>
({
type : 'TOGGLE_JOINED'
});
export const toggleConsumerFullscreen = (consumerId) =>
({
type : 'TOGGLE_FULLSCREEN_CONSUMER',
payload : { consumerId }
});

View File

@ -0,0 +1,28 @@
export const setSelectedAudioDevice = (deviceId) =>
({
type : 'CHANGE_AUDIO_DEVICE',
payload : { deviceId }
});
export const setSelectedWebcamDevice = (deviceId) =>
({
type : 'CHANGE_WEBCAM',
payload : { deviceId }
});
export const setVideoResolution = (resolution) =>
({
type : 'SET_VIDEO_RESOLUTION',
payload : { resolution }
});
export const setDisplayName = (displayName) =>
({
type : 'SET_DISPLAY_NAME',
payload : { displayName }
});
export const toggleAdvancedMode = () =>
({
type : 'TOGGLE_ADVANCED_MODE'
});

View File

@ -1,578 +0,0 @@
export const setRoomUrl = (url) =>
{
return {
type : 'SET_ROOM_URL',
payload : { url }
};
};
export const setRoomState = (state) =>
{
return {
type : 'SET_ROOM_STATE',
payload : { state }
};
};
export const setRoomActiveSpeaker = (peerId) =>
{
return {
type : 'SET_ROOM_ACTIVE_SPEAKER',
payload : { peerId }
};
};
export const setRoomLocked = () =>
{
return {
type : 'SET_ROOM_LOCKED'
};
};
export const setRoomUnLocked = () =>
{
return {
type : 'SET_ROOM_UNLOCKED'
};
};
export const setRoomLockedOut = () =>
{
return {
type : 'SET_ROOM_LOCKED_OUT'
};
};
export const setSettingsOpen = ({ settingsOpen }) =>
({
type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen }
});
export const setMe = ({ peerId, device, loginEnabled }) =>
{
return {
type : 'SET_ME',
payload : { peerId, device, loginEnabled }
};
};
export const setMediaCapabilities = ({
canSendMic,
canSendWebcam,
canShareScreen,
canShareFiles
}) =>
{
return {
type : 'SET_MEDIA_CAPABILITIES',
payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles }
};
};
export const setAudioDevices = (devices) =>
{
return {
type : 'SET_AUDIO_DEVICES',
payload : { devices }
};
};
export const setWebcamDevices = (devices) =>
{
return {
type : 'SET_WEBCAM_DEVICES',
payload : { devices }
};
};
export const setSelectedAudioDevice = (deviceId) =>
{
return {
type : 'CHANGE_AUDIO_DEVICE',
payload : { deviceId }
};
};
export const setSelectedWebcamDevice = (deviceId) =>
{
return {
type : 'CHANGE_WEBCAM',
payload : { deviceId }
};
};
export const setVideoResolution = (resolution) =>
{
return {
type : 'SET_VIDEO_RESOLUTION',
payload : { resolution }
};
};
export const setFileSharingSupported = (supported) =>
{
return {
type : 'FILE_SHARING_SUPPORTED',
payload : { supported }
};
};
export const setDisplayName = (displayName) =>
{
return {
type : 'SET_DISPLAY_NAME',
payload : { displayName }
};
};
export const toggleAdvancedMode = () =>
{
return {
type : 'TOGGLE_ADVANCED_MODE'
};
};
export const setDisplayMode = (mode) =>
({
type : 'SET_DISPLAY_MODE',
payload : { mode }
});
export const setPeerVideoInProgress = (peerId, flag) =>
{
return {
type : 'SET_PEER_VIDEO_IN_PROGRESS',
payload : { peerId, flag }
};
};
export const setPeerAudioInProgress = (peerId, flag) =>
{
return {
type : 'SET_PEER_AUDIO_IN_PROGRESS',
payload : { peerId, flag }
};
};
export const setPeerScreenInProgress = (peerId, flag) =>
{
return {
type : 'SET_PEER_SCREEN_IN_PROGRESS',
payload : { peerId, flag }
};
};
export const setMyRaiseHandState = (flag) =>
{
return {
type : 'SET_MY_RAISE_HAND_STATE',
payload : { flag }
};
};
export const toggleSettings = () =>
{
return {
type : 'TOGGLE_SETTINGS'
};
};
export const toggleToolArea = () =>
{
return {
type : 'TOGGLE_TOOL_AREA'
};
};
export const openToolArea = () =>
{
return {
type : 'OPEN_TOOL_AREA'
};
};
export const closeToolArea = () =>
{
return {
type : 'CLOSE_TOOL_AREA'
};
};
export const setToolTab = (toolTab) =>
{
return {
type : 'SET_TOOL_TAB',
payload : { toolTab }
};
};
export const setMyRaiseHandStateInProgress = (flag) =>
{
return {
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS',
payload : { flag }
};
};
export const setPeerRaiseHandState = (peerId, raiseHandState) =>
{
return {
type : 'SET_PEER_RAISE_HAND_STATE',
payload : { peerId, raiseHandState }
};
};
export const addProducer = (producer) =>
{
return {
type : 'ADD_PRODUCER',
payload : { producer }
};
};
export const removeProducer = (producerId) =>
{
return {
type : 'REMOVE_PRODUCER',
payload : { producerId }
};
};
export const setProducerPaused = (producerId, originator) =>
{
return {
type : 'SET_PRODUCER_PAUSED',
payload : { producerId, originator }
};
};
export const setProducerResumed = (producerId, originator) =>
{
return {
type : 'SET_PRODUCER_RESUMED',
payload : { producerId, originator }
};
};
export const setProducerTrack = (producerId, track) =>
{
return {
type : 'SET_PRODUCER_TRACK',
payload : { producerId, track }
};
};
export const setProducerScore = (producerId, score) =>
{
return {
type : 'SET_PRODUCER_SCORE',
payload : { producerId, score }
};
};
export const setAudioInProgress = (flag) =>
{
return {
type : 'SET_AUDIO_IN_PROGRESS',
payload : { flag }
};
};
export const setWebcamInProgress = (flag) =>
{
return {
type : 'SET_WEBCAM_IN_PROGRESS',
payload : { flag }
};
};
export const setScreenShareInProgress = (flag) =>
{
return {
type : 'SET_SCREEN_SHARE_IN_PROGRESS',
payload : { flag }
};
};
export const addPeer = (peer) =>
{
return {
type : 'ADD_PEER',
payload : { peer }
};
};
export const removePeer = (peerId) =>
{
return {
type : 'REMOVE_PEER',
payload : { peerId }
};
};
export const setPeerDisplayName = (displayName, peerId) =>
{
return {
type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerId }
};
};
export const addConsumer = (consumer, peerId) =>
{
return {
type : 'ADD_CONSUMER',
payload : { consumer, peerId }
};
};
export const removeConsumer = (consumerId, peerId) =>
{
return {
type : 'REMOVE_CONSUMER',
payload : { consumerId, peerId }
};
};
export const setConsumerPaused = (consumerId, originator) =>
{
return {
type : 'SET_CONSUMER_PAUSED',
payload : { consumerId, originator }
};
};
export const setConsumerResumed = (consumerId, originator) =>
{
return {
type : 'SET_CONSUMER_RESUMED',
payload : { consumerId, originator }
};
};
export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_PREFERRED_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerTrack = (consumerId, track) =>
{
return {
type : 'SET_CONSUMER_TRACK',
payload : { consumerId, track }
};
};
export const setConsumerScore = (consumerId, score) =>
{
return {
type : 'SET_CONSUMER_SCORE',
payload : { consumerId, score }
};
};
export const setPeerVolume = (peerId, volume) =>
{
return {
type : 'SET_PEER_VOLUME',
payload : { peerId, volume }
};
};
export const addNotification = (notification) =>
{
return {
type : 'ADD_NOTIFICATION',
payload : { notification }
};
};
export const removeNotification = (notificationId) =>
{
return {
type : 'REMOVE_NOTIFICATION',
payload : { notificationId }
};
};
export const removeAllNotifications = () =>
{
return {
type : 'REMOVE_ALL_NOTIFICATIONS'
};
};
export const toggleChat = () =>
{
return {
type : 'TOGGLE_CHAT'
};
};
export const toggleConsumerFullscreen = (consumerId) =>
{
return {
type : 'TOGGLE_FULLSCREEN_CONSUMER',
payload : { consumerId }
};
};
export const toggleConsumerWindow = (consumerId) =>
{
return {
type : 'TOGGLE_WINDOW_CONSUMER',
payload : { consumerId }
};
};
export const setToolbarsVisible = (toolbarsVisible) => ({
type : 'SET_TOOLBARS_VISIBLE',
payload : { toolbarsVisible }
});
export const increaseBadge = () =>
{
return {
type : 'INCREASE_BADGE'
};
};
export const toggleInputDisabled = () =>
{
return {
type : 'TOGGLE_INPUT_DISABLED'
};
};
export const addUserMessage = (text) =>
{
return {
type : 'ADD_NEW_USER_MESSAGE',
payload : { text }
};
};
export const addUserFile = (file) =>
{
return {
type : 'ADD_NEW_USER_FILE',
payload : { file }
};
};
export const addResponseMessage = (message) =>
{
return {
type : 'ADD_NEW_RESPONSE_MESSAGE',
payload : { message }
};
};
export const addChatHistory = (chatHistory) =>
{
return {
type : 'ADD_CHAT_HISTORY',
payload : { chatHistory }
};
};
export const dropMessages = () =>
{
return {
type : 'DROP_MESSAGES'
};
};
export const addFile = (peerId, magnetUri) =>
{
return {
type : 'ADD_FILE',
payload : { peerId, magnetUri }
};
};
export const addFileHistory = (fileHistory) =>
{
return {
type : 'ADD_FILE_HISTORY',
payload : { fileHistory }
};
};
export const setFileActive = (magnetUri) =>
{
return {
type : 'SET_FILE_ACTIVE',
payload : { magnetUri }
};
};
export const setFileInActive = (magnetUri) =>
{
return {
type : 'SET_FILE_INACTIVE',
payload : { magnetUri }
};
};
export const setFileProgress = (magnetUri, progress) =>
{
return {
type : 'SET_FILE_PROGRESS',
payload : { magnetUri, progress }
};
};
export const setFileDone = (magnetUri, sharedFiles) =>
{
return {
type : 'SET_FILE_DONE',
payload : { magnetUri, sharedFiles }
};
};
export const setPicture = (picture) =>
({
type : 'SET_PICTURE',
payload : { picture }
});
export const setPeerPicture = (peerId, picture) =>
({
type : 'SET_PEER_PICTURE',
payload : { peerId, picture }
});
export const loggedIn = () =>
({
type : 'LOGGED_IN'
});
export const toggleJoined = () =>
({
type : 'TOGGLE_JOINED'
});
export const setSelectedPeer = (selectedPeerId) =>
({
type : 'SET_SELECTED_PEER',
payload : { selectedPeerId }
});
export const setSpotlights = (spotlights) =>
({
type : 'SET_SPOTLIGHTS',
payload : { spotlights }
});

View File

@ -0,0 +1,20 @@
export const toggleToolArea = () =>
({
type : 'TOGGLE_TOOL_AREA'
});
export const openToolArea = () =>
({
type : 'OPEN_TOOL_AREA'
});
export const closeToolArea = () =>
({
type : 'CLOSE_TOOL_AREA'
});
export const setToolTab = (toolTab) =>
({
type : 'SET_TOOL_TAB',
payload : { toolTab }
});

View File

@ -0,0 +1,156 @@
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 { useIntl } from 'react-intl';
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 intl = useIntl();
const picture = peer.picture || EmptyAvatar;
return (
<ListItem
className={classnames(classes.ListItem)}
key={peer.peerId}
button
alignItems='flex-start'
>
<ListItemAvatar>
<Avatar alt='Peer avatar' src={picture} />
</ListItemAvatar>
<ListItemText
primary={peer.displayName}
/>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.admitFromLobby',
defaultMessage : 'Click to let them in'
})}
>
<ListItemIcon
className={classnames(classes.button, 'promote', {
disabled : peer.promotionInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
roomClient.promoteLobbyPeer(peer.id);
}}
>
<PromoteIcon />
</ListItemIcon>
</Tooltip>
</ListItem>
);
};
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)));

View File

@ -0,0 +1,215 @@
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 roomActions from '../../../actions/roomActions';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
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 (
<Dialog
className={classes.root}
open={room.lockDialogOpen}
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.lobbyAdministration'
defaultMessage='Lobby administration'
/>
</DialogTitle>
{/*
<FormControl component='fieldset' className={classes.formControl}>
<FormLabel component='legend'>Room lock</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Switch checked={room.locked} onChange={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
/>}
label='Lock'
/>
TODO: access code
<FormControlLabel disabled={ room.locked ? false : true }
control={
<Checkbox checked={room.joinByAccessCode}
onChange={
(event) => roomClient.setJoinByAccessCode(event.target.checked)
}
/>}
label='Join by Access code'
/>
<InputLabel htmlFor='access-code-input' />
<OutlinedInput
disabled={ room.locked ? false : true }
id='acces-code-input'
label='Access code'
labelWidth={0}
variant='outlined'
value={room.accessCode}
onChange={(event) => handleAccessCode(event.target.value)}
>
</OutlinedInput>
<Button onClick={() => roomClient.setAccessCode(room.accessCode)} color='primary'>
save
</Button>
</FormGroup>
</FormControl>
*/}
{ lobbyPeers.length > 0 ?
<List
dense
subheader={
<ListSubheader component='div'>
<FormattedMessage
id='room.peersInLobby'
defaultMessage='Participants in Lobby'
/>
</ListSubheader>
}
>
{
lobbyPeers.map((peerId) =>
{
return (<ListLobbyPeer key={peerId} id={peerId} />);
})
}
</List>
:
<DialogContent>
<DialogContentText gutterBottom>
<FormattedMessage
id='room.lobbyEmpty'
defaultMessage='There are currently no one in the lobby'
/>
</DialogContentText>
</DialogContent>
}
<DialogActions>
<Button onClick={() => handleCloseLockDialog({ lockDialogOpen: false })} color='primary'>
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>
);
};
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 : roomActions.setLockDialogOpen,
handleAccessCode : roomActions.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)));

View File

@ -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 (
<JoinDialog />
);
}
else
{
return (
<Suspense fallback={<LoadingView />}>
<Room />
</Suspense>
);
}
};
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);

View File

@ -3,7 +3,8 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../../actions/stateActions';
import { FormattedMessage } from 'react-intl';
import * as toolareaActions from '../../actions/toolareaActions';
import BuddyImage from '../../images/buddy.svg';
const styles = () =>
@ -95,8 +96,19 @@ class HiddenPeers extends React.PureComponent
className={classnames(classes.root, this.state.className)}
onClick={() => openUsersTab()}
>
<p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount === 1) ? null : 's'}
<p>
+{hiddenPeersCount} <br />
<FormattedMessage
id='room.hiddenPeers'
defaultMessage={
`{hiddenPeersCount, plural,
one {participant}
other {participants}}`
}
values={{
hiddenPeersCount
}}
/>
</p>
</div>
);
@ -115,8 +127,8 @@ const mapDispatchToProps = (dispatch) =>
return {
openUsersTab : () =>
{
dispatch(stateActions.openToolArea());
dispatch(stateActions.setToolTab('users'));
dispatch(toolareaActions.openToolArea());
dispatch(toolareaActions.setToolTab('users'));
}
};
};

View File

@ -7,6 +7,7 @@ import useMediaQuery from '@material-ui/core/useMediaQuery';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume';
import Fab from '@material-ui/core/Fab';
@ -96,6 +97,8 @@ const Me = (props) =>
{
const [ hover, setHover ] = useState(false);
const intl = useIntl();
let touchTimeout = null;
const {
@ -133,22 +136,34 @@ const Me = (props) =>
if (!me.canSendMic)
{
micState = 'unsupported';
micTip = 'Audio unsupported';
micTip = intl.formatMessage({
id : 'device.audioUnsupported',
defaultMessage : 'Audio unsupported'
});
}
else if (!micProducer)
{
micState = 'off';
micTip = 'Activate audio';
micTip = intl.formatMessage({
id : 'device.activateAudio',
defaultMessage : 'Activate audio'
});
}
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
{
micState = 'on';
micTip = 'Mute audio';
micTip = intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
});
}
else
{
micState = 'muted';
micTip = 'Unmute audio';
micTip = intl.formatMessage({
id : 'device.unMuteAudio',
defaultMessage : 'Unmute audio'
});
}
let webcamState;
@ -158,17 +173,26 @@ const Me = (props) =>
if (!me.canSendWebcam)
{
webcamState = 'unsupported';
webcamTip = 'Video unsupported';
webcamTip = intl.formatMessage({
id : 'device.videoUnsupported',
defaultMessage : 'Video unsupported'
});
}
else if (webcamProducer)
{
webcamState = 'on';
webcamTip = 'Stop video';
webcamTip = intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
});
}
else
{
webcamState = 'off';
webcamTip = 'Start video';
webcamTip = intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
});
}
let screenState;
@ -178,17 +202,26 @@ const Me = (props) =>
if (!me.canShareScreen)
{
screenState = 'unsupported';
screenTip = 'Screen sharing not supported';
screenTip = intl.formatMessage({
id : 'device.screenSharingUnsupported',
defaultMessage : 'Screen sharing not supported'
});
}
else if (screenProducer)
{
screenState = 'on';
screenTip = 'Stop screen sharing';
screenTip = intl.formatMessage({
id : 'device.stopScreenSharing',
defaultMessage : 'Stop screen sharing'
});
}
else
{
screenState = 'off';
screenTip = 'Start screen sharing';
screenTip = intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
});
}
const spacingStyle =
@ -253,11 +286,19 @@ const Me = (props) =>
}, 2000);
}}
>
<p>ME</p>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'right'}>
<p>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'left'}>
<div>
<Fab
aria-label='Mute mic'
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
@ -280,10 +321,13 @@ const Me = (props) =>
</Fab>
</div>
</Tooltip>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'right'}>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'left'}>
<div>
<Fab
aria-label='Mute video'
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
@ -303,10 +347,13 @@ const Me = (props) =>
</Fab>
</div>
</Tooltip>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'right'}>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'left'}>
<div>
<Fab
aria-label='Share screen'
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.fab}
disabled={!me.canShareScreen || me.screenShareInProgress}
color={screenState === 'on' ? 'primary' : 'default'}
@ -332,13 +379,11 @@ const Me = (props) =>
}
}}
>
{ screenState === 'on' || screenState === 'unsupported' ?
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
:null
}
{ screenState === 'off' ?
{ screenState === 'off' &&
<ScreenIcon/>
:null
}
</Fab>
</div>
@ -351,10 +396,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 +409,9 @@ const Me = (props) =>
</VideoView>
</div>
</div>
{ screenProducer ?
{ screenProducer &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
className={classnames(classes.root, 'screen', hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -390,7 +435,7 @@ const Me = (props) =>
>
<div className={classnames(classes.viewContainer)} style={style}>
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
className={classnames(classes.controls, hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -412,7 +457,12 @@ const Me = (props) =>
}, 2000);
}}
>
<p>ME</p>
<p>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
</div>
<VideoView
@ -420,13 +470,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}
/>
</div>
</div>
:null
}
</React.Fragment>
);

View File

@ -7,8 +7,10 @@ import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import * as stateActions from '../../actions/stateActions';
import * as roomActions from '../../actions/roomActions';
import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Tooltip from '@material-ui/core/Tooltip';
import Fab from '@material-ui/core/Fab';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
@ -103,6 +105,8 @@ const Peer = (props) =>
{
const [ hover, setHover ] = useState(false);
const intl = useIntl();
let touchTimeout = null;
const {
@ -166,8 +170,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 +196,19 @@ const Peer = (props) =>
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible ?
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
:null
}
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
className={classnames(classes.controls, hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -221,8 +229,19 @@ const Peer = (props) =>
}, 2000);
}}
>
<Tooltip
title={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label='Mute mic'
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
@ -240,10 +259,23 @@ const Peer = (props) =>
<MicOffIcon />
}
</Fab>
</div>
</Tooltip>
{ !smallScreen ?
{ !smallScreen &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label='New window'
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
@ -257,11 +289,23 @@ const Peer = (props) =>
>
<NewWindowIcon />
</Fab>
:null
</div>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label='Fullscreen'
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
@ -273,26 +317,28 @@ const Peer = (props) =>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}
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}
>
<Volume id={peer.id} />
</VideoView>
</div>
</div>
{ screenConsumer ?
{ screenConsumer &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
className={classnames(classes.root, 'screen', hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -314,17 +360,21 @@ const Peer = (props) =>
}}
style={rootStyle}
>
{ !screenVisible ?
{ !screenVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
:null
}
{ screenVisible ?
{ screenVisible &&
<div className={classnames(classes.viewContainer)}>
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
className={classnames(classes.controls, hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -346,9 +396,20 @@ const Peer = (props) =>
}, 2000);
}}
>
{ !smallScreen ?
{ !smallScreen &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label='New window'
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
@ -362,11 +423,23 @@ const Peer = (props) =>
>
<NewWindowIcon />
</Fab>
:null
</div>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label='Fullscreen'
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
@ -378,19 +451,19 @@ const Peer = (props) =>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}
videoContain
videoTrack={screenConsumer ? screenConsumer.track : null}
videoTrack={screenConsumer && screenConsumer.track}
videoVisible={screenVisible}
videoProfile={screenProfile}
videoCodec={screenConsumer ? screenConsumer.codec : null}
videoCodec={screenConsumer && screenConsumer.codec}
/>
</div>
:null
}
</div>
:null
}
</React.Fragment>
);
@ -438,12 +511,12 @@ const mapDispatchToProps = (dispatch) =>
toggleConsumerFullscreen : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
dispatch(roomActions.toggleConsumerFullscreen(consumer.id));
},
toggleConsumerWindow : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerWindow(consumer.id));
dispatch(roomActions.toggleConsumerWindow(consumer.id));
}
};
};

View File

@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume';
@ -117,11 +118,15 @@ const SpeakerPeer = (props) =>
style={spacingStyle}
>
<div className={classnames(classes.viewContainer)} style={style}>
{ !videoVisible ?
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
:null
}
<VideoView
@ -140,18 +145,22 @@ const SpeakerPeer = (props) =>
</div>
</div>
{ screenConsumer ?
{ screenConsumer &&
<div
className={classnames(classes.root, 'screen')}
>
{ !screenVisible ?
{ !screenVisible &&
<div className={classes.videoInfo} style={style}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
:null
}
{ screenVisible ?
{ screenVisible &&
<div className={classnames(classes.viewContainer)} style={style}>
<VideoView
advancedMode={advancedMode}
@ -162,10 +171,8 @@ const SpeakerPeer = (props) =>
videoCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
:null
}
</div>
:null
}
</React.Fragment>
);

View File

@ -0,0 +1,399 @@
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';
import * as roomActions from '../../actions/roomActions';
import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge';
import AccountCircle from '@material-ui/icons/AccountCircle';
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 LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
const styles = (theme) =>
({
menuButton :
{
margin : 0,
padding : 0
},
logo :
{
display : 'none',
marginLeft : 20,
[theme.breakpoints.up('sm')] :
{
display : 'block'
}
},
show :
{
opacity : 1,
transition : 'opacity .5s'
},
hide :
{
opacity : 0,
transition : 'opacity .5s'
},
grow :
{
flexGrow : 1
},
title :
{
display : 'none',
marginLeft : 20,
[theme.breakpoints.up('sm')] :
{
display : 'block'
}
},
actionButtons :
{
display : 'flex'
},
actionButton :
{
margin : theme.spacing(1),
padding : 0
}
});
const PulsingBadge = withStyles((theme) =>
({
badge :
{
backgroundColor : 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);
const TopBar = (props) =>
{
const intl = useIntl();
const {
roomClient,
room,
lobbyPeers,
myPicture,
loggedIn,
loginEnabled,
fullscreenEnabled,
fullscreen,
onFullscreen,
setSettingsOpen,
setLockDialogOpen,
toggleToolArea,
unread,
classes
} = props;
const lockTooltip = room.locked ?
intl.formatMessage({
id : 'tooltip.unLockRoom',
defaultMessage : 'Unlock room'
})
:
intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
});
const fullscreenTooltip = fullscreen ?
intl.formatMessage({
id : 'tooltip.leaveFullscreen',
defaultMessage : 'Leave fullscreen'
})
:
intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
});
const loginTooltip = loggedIn ?
intl.formatMessage({
id : 'tooltip.logout',
defaultMessage : 'Log out'
})
:
intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
});
return (
<AppBar
position='fixed'
className={room.toolbarsVisible ? classes.show : classes.hide}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
>
<IconButton
color='inherit'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
onClick={() => toggleToolArea()}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
{ window.config.title }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<Tooltip title={lockTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
color='inherit'
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</Tooltip>
}
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
</Tooltip>
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle />
}
</IconButton>
</Tooltip>
}
<Button
aria-label={intl.formatMessage({
id : 'label.leave',
defaultMessage : 'Leave'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
<FormattedMessage
id='label.leave'
defaultMessage='Leave'
/>
</Button>
</div>
</Toolbar>
</AppBar>
);
};
TopBar.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
lobbyPeers : PropTypes.array,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
fullscreenEnabled : PropTypes.bool,
fullscreen : PropTypes.bool,
onFullscreen : PropTypes.func.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,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
room : state.room,
lobbyPeers : lobbyPeersKeySelector(state),
advancedMode : state.settings.advancedMode,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles
});
const mapDispatchToProps = (dispatch) =>
({
setToolbarsVisible : (visible) =>
{
dispatch(roomActions.setToolbarsVisible(visible));
},
setSettingsOpen : (settingsOpen) =>
{
dispatch(roomActions.setSettingsOpen({ settingsOpen }));
},
setLockDialogOpen : (lockDialogOpen) =>
{
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen }));
},
toggleToolArea : () =>
{
dispatch(toolareaActions.toggleToolArea());
}
});
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room &&
prev.lobbyPeers === next.lobbyPeers &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles
);
}
}
)(withStyles(styles, { withTheme: true })(TopBar)));

View File

@ -1,62 +1,276 @@
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 settingsActions from '../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
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);
const intl = useIntl();
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 (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography>
{ window.config.loginEnabled &&
<Tooltip
onClose={handleTooltipClose}
onOpen={handleTooltipOpen}
open={open}
title={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Click to log in'
})}
placement='left'
>
<IconButton
aria-label='Account'
className={classes.loginButton}
color='inherit'
onClick={onLogin}
>
{ myPicture ?
<Avatar src={myPicture} className={classes.largeAvatar} />
:
<AccountCircle className={classes.largeIcon} />
}
</IconButton>
</Tooltip>
}
</MuiDialogTitle>
);
});
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
}) =>
{
const intl = useIntl();
const handleKeyDown = (event) =>
{
const { key } = event;
switch (key)
{
case 'Enter':
case 'Escape':
{
if (displayName === '')
changeDisplayName('Guest');
if (room.inLobby)
roomClient.changeDisplayName(displayName);
break;
}
default:
break;
}
};
return (
<div className={classes.root}>
<Dialog
className={classes.root}
open
classes={{
paper : classes.dialogPaper
}}
>
{ window.config.logo ?
<img alt='Logo' className={classes.logo} src={window.config.logo} />
:null
<DialogTitle
myPicture={myPicture}
onLogin={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ window.config.title }
<hr />
</DialogTitle>
<DialogContent>
<DialogContentText gutterBottom>
<FormattedMessage
id='room.aboutToJoin'
defaultMessage='You are about to join a meeting'
/>
</DialogContentText>
<DialogContentText variant='h6' gutterBottom align='center'>
<FormattedMessage
id='room.roomId'
defaultMessage='Room ID: {roomName}'
values={{
roomName : room.name
}}
/>
</DialogContentText>
<DialogContentText gutterBottom>
<FormattedMessage
id='room.setYourName'
defaultMessage={
`Set your name for participation,
and choose how you want to join:`
}
<Typography variant='subtitle1'>You are about to join a meeting, how would you like to join?</Typography>
/>
</DialogContentText>
<TextField
id='displayname'
label={intl.formatMessage({
id : 'label.yourName',
defaultMessage : 'Your name'
})}
value={displayName}
variant='outlined'
margin='normal'
disabled={displayNameInProgress}
onChange={(event) =>
{
const { value } = event.target;
changeDisplayName(value);
}}
onKeyDown={handleKeyDown}
onBlur={() =>
{
if (displayName === '')
changeDisplayName('Guest');
if (room.inLobby)
roomClient.changeDisplayName(displayName);
}}
fullWidth
/>
</DialogContent>
{ !room.inLobby ?
<DialogActions>
<Button
onClick={() =>
@ -64,8 +278,12 @@ const JoinDialog = ({
roomClient.join({ joinVideo: false });
}}
variant='contained'
color='secondary'
>
Audio only
<FormattedMessage
id='room.audioOnly'
defaultMessage='Audio only'
/>
</Button>
<Button
onClick={() =>
@ -73,18 +291,110 @@ const JoinDialog = ({
roomClient.join({ joinVideo: true });
}}
variant='contained'
color='secondary'
>
Audio and Video
<FormattedMessage
id='room.audioVideo'
defaultMessage='Audio and Video'
/>
</Button>
</DialogActions>
:
<DialogContent>
<DialogContentText
className={classes.green}
gutterBottom
variant='h6'
align='center'
>
<FormattedMessage
id='room.youAreReady'
defaultMessage='Ok, you are ready'
/>
</DialogContentText>
{ room.signInRequired ?
<DialogContentText gutterBottom>
<FormattedMessage
id='room.emptyRequireLogin'
defaultMessage={
`The room is empty! You can Log In to start
the meeting or wait until the host joins`
}
/>
</DialogContentText>
:
<DialogContentText gutterBottom>
<FormattedMessage
id='room.locketWait'
defaultMessage='The room is locked - hang on until somebody lets you in ...'
/>
</DialogContentText>
}
</DialogContent>
}
<CookieConsent>
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
</Dialog>
</div>
);
};
JoinDialog.propTypes =
{
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));
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(settingsActions.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)));

View File

@ -1,8 +1,9 @@
import React from 'react';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
import IconButton from '@material-ui/core/IconButton';
@ -28,19 +29,13 @@ const styles = (theme) =>
}
});
class ChatInput extends React.PureComponent
const ChatInput = (props) =>
{
constructor(props)
{
super(props);
const [ message, setMessage ] = useState('');
this.state =
{
message : ''
};
}
const intl = useIntl();
createNewMessage = (text, sender, name, picture) =>
const createNewMessage = (text, sender, name, picture) =>
({
type : 'message',
text,
@ -50,40 +45,41 @@ class ChatInput extends React.PureComponent
picture
});
handleChange = (e) =>
const handleChange = (e) =>
{
this.setState({ message: e.target.value });
}
setMessage(e.target.value);
};
render()
{
const {
roomClient,
displayName,
picture,
classes
} = this.props;
} = props;
return (
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder='Enter chat message...'
value={this.state.message || ''}
onChange={this.handleChange}
placeholder={intl.formatMessage({
id : 'label.chatInput',
defaultMessage : 'Enter chat message...'
})}
value={message || ''}
onChange={handleChange}
onKeyPress={(ev) =>
{
if (ev.key === 'Enter')
{
ev.preventDefault();
if (this.state.message && this.state.message !== '')
if (message && message !== '')
{
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
const sendMessage = createNewMessage(message, 'response', displayName, picture);
roomClient.sendChatMessage(message);
roomClient.sendChatMessage(sendMessage);
this.setState({ message: '' });
setMessage('');
}
}
}}
@ -95,13 +91,13 @@ class ChatInput extends React.PureComponent
aria-label='Send'
onClick={() =>
{
if (this.state.message && this.state.message !== '')
if (message && message !== '')
{
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
const sendMessage = this.createNewMessage(message, 'response', displayName, picture);
roomClient.sendChatMessage(message);
roomClient.sendChatMessage(sendMessage);
this.setState({ message: '' });
setMessage('');
}
}}
>
@ -109,8 +105,7 @@ class ChatInput extends React.PureComponent
</IconButton>
</Paper>
);
}
}
};
ChatInput.propTypes =
{
@ -123,7 +118,7 @@ ChatInput.propTypes =
const mapStateToProps = (state) =>
({
displayName : state.settings.displayName,
picture : state.settings.picture
picture : state.me.picture
});
export default withRoomContext(
@ -136,7 +131,7 @@ export default withRoomContext(
{
return (
prev.settings.displayName === next.settings.displayName &&
prev.settings.picture === next.settings.picture
prev.me.picture === next.me.picture
);
}
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import DOMPurify from 'dompurify';
import marked from 'marked';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
@ -76,9 +77,11 @@ const Message = (props) =>
className={classes.text}
variant='subtitle1'
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html : marked.parse(
dangerouslySetInnerHTML={{ __html : DOMPurify.sanitize(
marked.parse(
text,
{ sanitize: true, renderer: linkRenderer }
{ renderer: linkRenderer }
)
) }}
/>
<Typography variant='caption'>{self ? 'Me' : name} - {time}</Typography>
@ -92,7 +95,7 @@ Message.propTypes =
self : PropTypes.bool,
picture : PropTypes.string,
text : PropTypes.string,
time : PropTypes.string,
time : PropTypes.object,
name : PropTypes.string,
classes : PropTypes.object.isRequired
};

View File

@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { FormattedTime } from 'react-intl';
import Message from './Message';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
@ -33,7 +34,7 @@ class MessageList extends React.Component
shouldComponentUpdate(nextProps)
{
if (nextProps.chatmessages.length !== this.props.chatmessages.length)
if (nextProps.chat.length !== this.props.chat.length)
return true;
return false;
@ -49,13 +50,13 @@ class MessageList extends React.Component
getTimeString(time)
{
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
return (<FormattedTime value={new Date(time)} />);
}
render()
{
const {
chatmessages,
chat,
myPicture,
classes
} = this.props;
@ -63,10 +64,8 @@ class MessageList extends React.Component
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
{
chatmessages.map((message, index) =>
chat.map((message, index) =>
{
const messageTime = new Date(message.time);
const picture = (message.sender === 'response' ?
message.picture : myPicture) || EmptyAvatar;
@ -76,7 +75,7 @@ class MessageList extends React.Component
self={message.sender === 'client'}
picture={picture}
text={message.text}
time={this.getTimeString(messageTime)}
time={this.getTimeString(message.time)}
name={message.name}
/>
);
@ -89,15 +88,15 @@ class MessageList extends React.Component
MessageList.propTypes =
{
chatmessages : PropTypes.array,
chat : PropTypes.array,
myPicture : PropTypes.string,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
chatmessages : state.chatmessages,
myPicture : state.settings.picture
chat : state.chat,
myPicture : state.me.picture
});
export default connect(
@ -108,8 +107,8 @@ export default connect(
areStatesEqual : (next, prev) =>
{
return (
prev.chatmessages === next.chatmessages &&
prev.settings.picture === next.settings.picture
prev.chat === next.chat &&
prev.me.picture === next.me.picture
);
}
}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { FormattedMessage } from 'react-intl';
import magnet from 'magnet-uri';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
@ -67,10 +68,13 @@ class File extends React.PureComponent
<img alt='Avatar' className={classes.avatar} src={picture} />
<div className={classes.fileContent}>
{ file.files ?
{ file.files &&
<Fragment>
<Typography className={classes.text}>
File finished downloading
<FormattedMessage
id='filesharing.finished'
defaultMessage='File finished downloading'
/>
</Typography>
{ file.files.map((sharedFile, i) => (
@ -87,18 +91,26 @@ class File extends React.PureComponent
roomClient.saveFile(sharedFile);
}}
>
Save
<FormattedMessage
id='filesharing.save'
defaultMessage='Save'
/>
</Button>
</div>
))}
</Fragment>
:null
}
<Typography className={classes.text}>
{ `${displayName} shared a file` }
<FormattedMessage
id='filesharing.sharedFile'
defaultMessage='{displayName} shared a file'
values={{
displayName
}}
/>
</Typography>
{ !file.active && !file.files ?
{ (!file.active && !file.files) &&
<div className={classes.fileInfo}>
<Typography className={classes.text}>
{ magnet.decode(magnetUri).dn }
@ -113,28 +125,37 @@ class File extends React.PureComponent
roomClient.handleDownload(magnetUri);
}}
>
Download
<FormattedMessage
id='filesharing.download'
defaultMessage='Download'
/>
</Button>
:
<Typography className={classes.text}>
Your browser does not support downloading files using WebTorrent.
<FormattedMessage
id='label.fileSharingUnsupported'
defaultMessage='File sharing not supported'
/>
</Typography>
}
</div>
:null
}
{ file.timeout ?
{ file.timeout &&
<Typography className={classes.text}>
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.
<FormattedMessage
id='filesharing.missingSeeds'
defaultMessage={
`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.`
}
/>
</Typography>
:null
}
{ file.active ?
{ file.active &&
<progress value={file.progress} />
:null
}
</div>
</div>

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { injectIntl } from 'react-intl';
import File from './File';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
@ -45,8 +46,8 @@ class FileList extends React.PureComponent
const {
files,
me,
picture,
peers,
intl,
classes
} = this.props;
@ -60,8 +61,11 @@ class FileList extends React.PureComponent
if (me.id === file.peerId)
{
displayName = 'You';
filePicture = picture;
displayName = intl.formatMessage({
id : 'room.me',
defaultMessage : 'Me'
});
filePicture = me.picture;
}
else if (peers[file.peerId])
{
@ -70,7 +74,10 @@ class FileList extends React.PureComponent
}
else
{
displayName = 'Unknown';
displayName = intl.formatMessage({
id : 'label.unknown',
defaultMessage : 'Unknown'
});
}
return (
@ -91,8 +98,8 @@ FileList.propTypes =
{
files : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
picture : PropTypes.string,
peers : PropTypes.object.isRequired,
intl : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
@ -101,7 +108,6 @@ const mapStateToProps = (state) =>
return {
files : state.files,
me : state.me,
picture : state.settings.picture,
peers : state.peers
};
};
@ -116,9 +122,8 @@ export default connect(
return (
prev.files === next.files &&
prev.me === next.me &&
prev.settings.picture === next.settings.picture &&
prev.peers === next.peers
);
}
}
)(withStyles(styles)(FileList));
)(withStyles(styles)(injectIntl(FileList)));

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import FileList from './FileList';
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
@ -26,40 +27,40 @@ const styles = (theme) =>
}
});
class FileSharing extends React.PureComponent
const FileSharing = (props) =>
{
constructor(props)
{
super(props);
const intl = useIntl();
this._fileInput = React.createRef();
}
handleFileChange = async (event) =>
const handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
this.props.roomClient.shareFiles(event.target.files);
props.roomClient.shareFiles(event.target.files);
}
};
render()
{
const {
canShareFiles,
classes
} = this.props;
} = props;
const buttonDescription = canShareFiles ?
'Share file' : 'File sharing not supported';
intl.formatMessage({
id : 'label.shareFile',
defaultMessage : 'Share file'
})
:
intl.formatMessage({
id : 'label.fileSharingUnsupported',
defaultMessage : 'File sharing not supported'
});
return (
<Paper className={classes.root}>
<input
ref={this._fileInput}
className={classes.input}
type='file'
onChange={this.handleFileChange}
onChange={handleFileChange}
id='share-files-button'
/>
<label htmlFor='share-files-button'>
@ -76,8 +77,7 @@ class FileSharing extends React.PureComponent
<FileList />
</Paper>
);
}
}
};
FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired,

View File

@ -2,7 +2,8 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../../actions/stateActions';
import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl } from 'react-intl';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
@ -44,6 +45,8 @@ const styles = (theme) =>
const MeetingDrawer = (props) =>
{
const intl = useIntl();
const {
currentToolTab,
unreadMessages,
@ -72,18 +75,29 @@ const MeetingDrawer = (props) =>
<Tab
label={
<Badge color='secondary' badgeContent={unreadMessages}>
Chat
{intl.formatMessage({
id : 'label.chat',
defaultMessage : 'Chat'
})}
</Badge>
}
/>
<Tab
label={
<Badge color='secondary' badgeContent={unreadFiles}>
File sharing
{intl.formatMessage({
id : 'label.filesharing',
defaultMessage : 'File sharing'
})}
</Badge>
}
/>
<Tab label='Participants' />
<Tab
label={intl.formatMessage({
id : 'label.participants',
defaultMessage : 'Participants'
})}
/>
</Tabs>
<IconButton onClick={closeDrawer}>
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
@ -114,7 +128,7 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = {
setToolTab : stateActions.setToolTab
setToolTab : toolareaActions.setToolTab
};
export default connect(

View File

@ -79,7 +79,7 @@ const ListMe = (props) =>
classes
} = props;
const picture = settings.picture || EmptyAvatar;
const picture = me.picture || EmptyAvatar;
return (
<li className={classes.root}>
@ -91,9 +91,8 @@ const ListMe = (props) =>
</div>
<div className={classes.indicators}>
{ me.raisedHand ?
{ me.raisedHand &&
<div className={classnames(classes.icon, 'raise-hand')} />
:null
}
</div>
</div>

View File

@ -159,7 +159,7 @@ const ListPeer = (props) =>
{peer.displayName}
</div>
<div className={classes.indicators}>
{ peer.raiseHandState ?
{ peer.raiseHandState &&
<div className={
classnames(
classes.icon, 'raise-hand', {
@ -169,12 +169,11 @@ const ListPeer = (props) =>
)
}
/>
:null
}
</div>
{children}
<div className={classes.controls}>
{ screenConsumer ?
{ screenConsumer &&
<div
className={classnames(classes.button, 'screen', {
on : screenVisible,
@ -195,7 +194,6 @@ const ListPeer = (props) =>
<ScreenOffIcon />
}
</div>
:null
}
<div
className={classnames(classes.button, 'mic', {

View File

@ -8,6 +8,7 @@ import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ListPeer from './ListPeer';
import ListMe from './ListMe';
import Volume from '../../Containers/Volume';
@ -84,12 +85,21 @@ class ParticipantList extends React.PureComponent
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
<ul className={classes.list}>
<li className={classes.listheader}>Me:</li>
<li className={classes.listheader}>
<FormattedMessage
id='room.me'
defaultMessage='Me'
/>
</li>
<ListMe />
</ul>
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Participants in Spotlight:</li>
<li className={classes.listheader}>
<FormattedMessage
id='room.spotlights'
defaultMessage='Participants in Spotlight'
/>
</li>
{ spotlightPeers.map((peer) => (
<li
key={peer.id}
@ -104,9 +114,13 @@ class ParticipantList extends React.PureComponent
</li>
))}
</ul>
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li>
<li className={classes.listheader}>
<FormattedMessage
id='room.passive'
defaultMessage='Passive Participants'
/>
</li>
{ passivePeers.map((peerId) => (
<li
key={peerId}

View File

@ -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 (
<div className={classes.root} ref={this.peersRef}>
<div
className={classnames(
classes.root,
toolbarsVisible ? classes.showingToolBar : classes.hiddenToolBar
)}
ref={this.peersRef}
>
<Me
advancedMode={advancedMode}
spacing={6}
@ -153,11 +168,10 @@ class Democratic extends React.PureComponent
/>
);
})}
{ spotlightsLength < peersLength ?
{ spotlightsLength < peersLength &&
<HiddenPeers
hiddenPeersCount={peersLength - spotlightsLength}
/>
:null
}
</div>
);
@ -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
);
}
}

View File

@ -228,13 +228,12 @@ class Filmstrip extends React.PureComponent
return (
<div className={classes.root}>
<div className={classes.speaker} ref={this.activePeerContainer}>
{ peers[activePeerId] ?
{ peers[activePeerId] &&
<SpeakerPeer
advancedMode={advancedMode}
id={activePeerId}
style={speakerStyle}
/>
:null
}
</div>
@ -286,11 +285,10 @@ class Filmstrip extends React.PureComponent
</Grid>
</div>
{ spotlightsLength<Object.keys(peers).length ?
{ spotlightsLength<Object.keys(peers).length &&
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>
:null
}
</div>
);

View File

@ -2,7 +2,7 @@ import { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withSnackbar } from 'notistack';
import * as stateActions from '../../actions/stateActions';
import * as notificationActions from '../../actions/notificationActions';
class Notifications extends Component
{
@ -77,7 +77,7 @@ const mapStateToProps = (state) =>
const mapDispatchToProps = (dispatch) =>
({
removeNotification : (notificationId) =>
dispatch(stateActions.removeNotification({ notificationId }))
dispatch(notificationActions.removeNotification({ notificationId }))
});
export default withSnackbar(

View File

@ -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;

View File

@ -0,0 +1,10 @@
import React from 'react';
export const ReactLazyPreload = (importStatement) =>
{
const Component = React.lazy(importStatement);
Component.preload = importStatement;
return Component;
};

View File

@ -2,24 +2,16 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from './appPropTypes';
import { withRoomContext } from '../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../actions/stateActions';
import * as roomActions from '../actions/roomActions';
import * as toolareaActions from '../actions/toolareaActions';
import { idle } from '../utils';
import FullScreen from './FullScreen';
import { FormattedMessage } from 'react-intl';
import CookieConsent from 'react-cookie-consent';
import CssBaseline from '@material-ui/core/CssBaseline';
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';
import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge';
import AccountCircle from '@material-ui/icons/AccountCircle';
import Notifications from './Notifications/Notifications';
import MeetingDrawer from './MeetingDrawer/MeetingDrawer';
import Democratic from './MeetingViews/Democratic';
@ -27,16 +19,11 @@ import Filmstrip from './MeetingViews/Filmstrip';
import AudioPeers from './PeerAudio/AudioPeers';
import FullScreenView from './VideoContainers/FullScreenView';
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 LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import Button from '@material-ui/core/Button';
import LockDialog from './AccessControl/LockDialog/LockDialog';
import Settings from './Settings/Settings';
import JoinDialog from './JoinDialog';
import TopBar from './Controls/TopBar';
const TIMEOUT = 10 * 1000;
const TIMEOUT = 5 * 1000;
const styles = (theme) =>
({
@ -52,44 +39,6 @@ const styles = (theme) =>
backgroundSize : 'cover',
backgroundRepeat : 'no-repeat'
},
message :
{
position : 'absolute',
display : 'flex',
top : '50%',
left : '50%',
transform : 'translateX(-50%) translateY(-50%)',
width : '30vw',
padding : theme.spacing(2),
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center'
},
menuButton :
{
margin : 0,
padding : 0
},
logo :
{
display : 'none',
marginLeft : 20,
[theme.breakpoints.up('sm')] :
{
display : 'block'
}
},
show :
{
opacity : 1,
transition : 'opacity .5s'
},
hide :
{
opacity : 0,
transition : 'opacity .5s'
},
toolbar : theme.mixins.toolbar,
drawerPaper :
{
width : '30vw',
@ -109,44 +58,6 @@ const styles = (theme) =>
{
width : '90vw'
}
},
grow :
{
flexGrow : 1
},
title :
{
display : 'none',
marginLeft : 20,
[theme.breakpoints.up('sm')] :
{
display : 'block'
}
},
actionButtons :
{
display : 'flex'
},
actionButton :
{
margin : theme.spacing(1),
padding : 0
},
meContainer :
{
position : 'fixed',
zIndex : 110,
overflow : 'hidden',
boxShadow : 'var(--me-shadow)',
transitionProperty : 'border-color',
transitionDuration : '0.15s',
top : '5em',
left : '1em',
border : 'var(--me-border)',
'&.active-speaker' :
{
borderColor : 'var(--active-speaker-border-color)'
}
}
});
@ -225,16 +136,10 @@ class Room extends React.PureComponent
render()
{
const {
roomClient,
room,
advancedMode,
myPicture,
loggedIn,
loginEnabled,
setSettingsOpen,
toolAreaOpen,
toggleToolArea,
unread,
classes,
theme
} = this.props;
@ -245,30 +150,13 @@ class Room extends React.PureComponent
democratic : Democratic
}[room.mode];
if (room.lockedOut)
{
return (
<div className={classes.root}>
<Paper className={classes.message}>
<Typography variant='h2'>This room is locked at the moment, try again later.</Typography>
</Paper>
</div>
);
}
else if (!room.joined)
{
return (
<div className={classes.root}>
<JoinDialog />
</div>
);
}
else
{
return (
<div className={classes.root}>
<CookieConsent>
This website uses cookies to enhance the user experience.
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
<FullScreenView advancedMode={advancedMode} />
@ -281,113 +169,12 @@ class Room extends React.PureComponent
<CssBaseline />
<AppBar
position='fixed'
className={room.toolbarsVisible ? classes.show : classes.hide}
>
<Toolbar>
<Badge
color='secondary'
badgeContent={unread}
>
<IconButton
color='inherit'
aria-label='Open drawer'
onClick={() => toggleToolArea()}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</Badge>
{ window.config.logo ?
<img alt='Logo' className={classes.logo} src={window.config.logo} />
:null
}
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
{ window.config.title }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<IconButton
aria-label='Lock room'
className={classes.actionButton}
color='inherit'
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
{ this.fullscreen.fullscreenEnabled ?
<IconButton
aria-label='Fullscreen'
className={classes.actionButton}
color='inherit'
onClick={this.handleToggleFullscreen}
>
{ this.state.fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
:null
}
<IconButton
aria-label='Settings'
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
{ loginEnabled ?
<IconButton
aria-label='Account'
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle />
}
</IconButton>
:null
}
<Button
aria-label='Leave meeting'
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
Leave
</Button>
</div>
</Toolbar>
</AppBar>
<TopBar
fullscreenEnabled={this.fullscreen.fullscreenEnabled}
fullscreen={this.state.fullscreen}
onFullscreen={this.handleToggleFullscreen}
/>
<nav>
<Hidden implementation='css'>
<SwipeableDrawer
@ -407,26 +194,21 @@ class Room extends React.PureComponent
<View advancedMode={advancedMode} />
<LockDialog />
<Settings />
</div>
);
}
}
}
Room.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
advancedMode : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
@ -435,31 +217,22 @@ const mapStateToProps = (state) =>
({
room : state.room,
advancedMode : state.settings.advancedMode,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.settings.picture,
toolAreaOpen : state.toolarea.toolAreaOpen,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles
toolAreaOpen : state.toolarea.toolAreaOpen
});
const mapDispatchToProps = (dispatch) =>
({
setToolbarsVisible : (visible) =>
{
dispatch(stateActions.setToolbarsVisible(visible));
},
setSettingsOpen : (settingsOpen) =>
{
dispatch(stateActions.setSettingsOpen({ settingsOpen }));
dispatch(roomActions.setToolbarsVisible(visible));
},
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
dispatch(toolareaActions.toggleToolArea());
}
});
export default withRoomContext(connect(
export default connect(
mapStateToProps,
mapDispatchToProps,
null,
@ -468,14 +241,9 @@ export default withRoomContext(connect(
{
return (
prev.room === next.room &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.settings.picture === next.settings.picture &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&
prev.settings.advancedMode === next.settings.advancedMode
prev.settings.advancedMode === next.settings.advancedMode &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
);
}
}
)(withStyles(styles, { withTheme: true })(Room)));
)(withStyles(styles, { withTheme: true })(Room));

View File

@ -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')

View File

@ -3,8 +3,10 @@ import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as stateActions from '../../actions/stateActions';
import * as roomActions from '../../actions/roomActions';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
@ -51,35 +53,6 @@ const styles = (theme) =>
}
});
const modes = [ {
value : 'democratic',
label : 'Democratic view'
}, {
value : 'filmstrip',
label : 'Filmstrip view'
} ];
const resolutions = [ {
value : 'low',
label : 'Low'
},
{
value : 'medium',
label : 'Medium'
},
{
value : 'high',
label : 'High (HD)'
},
{
value : 'veryhigh',
label : 'Very high (FHD)'
},
{
value : 'ultra',
label : 'Ultra (UHD)'
} ];
const Settings = ({
roomClient,
room,
@ -91,6 +64,58 @@ const Settings = ({
classes
}) =>
{
const intl = useIntl();
const modes = [ {
value : 'democratic',
label : intl.formatMessage({
id : 'label.democratic',
defaultMessage : 'Democratic view'
})
}, {
value : 'filmstrip',
label : intl.formatMessage({
id : 'label.filmstrip',
defaultMessage : 'Filmstrip view'
})
} ];
const resolutions = [ {
value : 'low',
label : intl.formatMessage({
id : 'label.low',
defaultMessage : 'Low'
})
},
{
value : 'medium',
label : intl.formatMessage({
id : 'label.medium',
defaultMessage : 'Medium'
})
},
{
value : 'high',
label : intl.formatMessage({
id : 'label.high',
defaultMessage : 'High (HD)'
})
},
{
value : 'veryhigh',
label : intl.formatMessage({
id : 'label.veryHigh',
defaultMessage : 'Very high (FHD)'
})
},
{
value : 'ultra',
label : intl.formatMessage({
id : 'label.ultra',
defaultMessage : 'Ultra (UHD)'
})
} ];
let webcams;
if (me.webcamDevices)
@ -114,7 +139,12 @@ const Settings = ({
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>Settings</DialogTitle>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='settings.settings'
defaultMessage='Settings'
/>
</DialogTitle>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
@ -125,7 +155,10 @@ const Settings = ({
roomClient.changeWebcam(event.target.value);
}}
displayEmpty
name='Camera'
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={webcams.length === 0 || me.webcamInProgress}
@ -139,9 +172,15 @@ const Settings = ({
</Select>
<FormHelperText>
{ webcams.length > 0 ?
'Select video device'
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
'Unable to select video device'
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
@ -156,7 +195,10 @@ const Settings = ({
roomClient.changeAudioDevice(event.target.value);
}}
displayEmpty
name='Audio device'
name={intl.formatMessage({
id : 'settings.audio',
defaultMessage : 'Audio device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioDevices.length === 0 || me.audioInProgress}
@ -170,9 +212,15 @@ const Settings = ({
</Select>
<FormHelperText>
{ audioDevices.length > 0 ?
'Select audio device'
intl.formatMessage({
id : 'settings.selectAudio',
defaultMessage : 'Select audio device'
})
:
'Unable to select audio device'
intl.formatMessage({
id : 'settings.cantSelectAudio',
defaultMessage : 'Unable to select audio device'
})
}
</FormHelperText>
</FormControl>
@ -200,7 +248,10 @@ const Settings = ({
})}
</Select>
<FormHelperText>
Select your video resolution
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/>
</FormHelperText>
</FormControl>
</form>
@ -213,7 +264,10 @@ const Settings = ({
if (event.target.value)
handleChangeMode(event.target.value);
}}
name='Room layout'
name={intl.formatMessage({
id : 'settings.layout',
defaultMessage : 'Room layout'
})}
autoWidth
className={classes.selectEmpty}
>
@ -227,18 +281,27 @@ const Settings = ({
})}
</Select>
<FormHelperText>
Select room layout
<FormattedMessage
id='settings.selectRoomLayout'
defaultMessage='Select room layout'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
label='Advanced mode'
label={intl.formatMessage({
id : 'settings.advancedMode',
defaultMessage : 'Advanced mode'
})}
/>
<DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
Close
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>
@ -267,9 +330,9 @@ const mapStateToProps = (state) =>
};
const mapDispatchToProps = {
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode,
handleCloseSettings : stateActions.setSettingsOpen,
onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
handleChangeMode : roomActions.setDisplayMode,
handleCloseSettings : roomActions.setSettingsOpen
};
export default withRoomContext(connect(

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../actions/stateActions';
import * as roomActions from '../../actions/roomActions';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import VideoView from './VideoView';
@ -148,7 +148,7 @@ const mapDispatchToProps = (dispatch) =>
toggleConsumerFullscreen : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
dispatch(roomActions.toggleConsumerFullscreen(consumer.id));
}
});

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import EditableInput from '../Controls/EditableInput';
const styles = (theme) =>
@ -105,16 +104,6 @@ const styles = (theme) =>
{
backgroundColor : 'rgb(174, 255, 0, 0.25)'
}
},
deviceInfo :
{
'& span' :
{
userSelect : 'none',
pointerEvents : 'none',
fontSize : 11,
color : 'rgba(255, 255, 255, 0.55)'
}
}
});
@ -143,7 +132,6 @@ class VideoView extends React.PureComponent
const {
isMe,
isScreen,
peer,
displayName,
showPeerInfo,
videoContain,
@ -171,24 +159,17 @@ class VideoView extends React.PureComponent
})}
>
<div className={classes.box}>
{ audioCodec ?
<p>{audioCodec}</p>
:null
}
{ audioCodec && <p>{audioCodec}</p> }
{ videoCodec ?
<p>{videoCodec} {videoProfile}</p>
:null
}
{ videoCodec && <p>{videoCodec} {videoProfile}</p> }
{ (videoVisible && videoWidth !== null) ?
{ (videoVisible && videoWidth !== null) &&
<p>{videoWidth}x{videoHeight}</p>
:null
}
</div>
</div>
{ showPeerInfo ?
{ showPeerInfo &&
<div className={classes.peer}>
<div className={classes.box}>
{ isMe ?
@ -211,18 +192,8 @@ class VideoView extends React.PureComponent
{displayName}
</span>
}
{ advancedMode ?
<div className={classes.deviceInfo}>
<span>
{peer.device.name} {Math.floor(peer.device.version) || null}
</span>
</div>
:null
}
</div>
</div>
:null
}
</div>
@ -256,7 +227,8 @@ class VideoView extends React.PureComponent
clearInterval(this._videoResolutionTimer);
}
componentWillReceiveProps(nextProps)
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps)
{
const { videoTrack } = nextProps;
@ -323,8 +295,6 @@ VideoView.propTypes =
{
isMe : PropTypes.bool,
isScreen : PropTypes.bool,
peer : PropTypes.oneOfType(
[ appPropTypes.Me, appPropTypes.Peer ]),
displayName : PropTypes.string,
showPeerInfo : PropTypes.bool,
videoContain : PropTypes.bool,

View File

@ -130,7 +130,7 @@ class NewWindow extends React.PureComponent
return ReactDOM.createPortal([
<div key='newwindow' className={classes.root}>
<div className={classes.controls}>
{this.fullscreen.fullscreenEnabled && (
{ this.fullscreen.fullscreenEnabled &&
<div
className={classes.button}
onClick={this.handleToggleFullscreen}
@ -144,9 +144,9 @@ class NewWindow extends React.PureComponent
<FullScreenIcon className={classes.icon} />
}
</div>
)}
}
</div>
{this.props.children}
{ this.props.children }
</div>
], this.container);
}

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import NewWindow from './NewWindow';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../actions/stateActions';
import * as roomActions from '../../actions/roomActions';
import FullView from '../VideoContainers/FullView';
const VideoWindow = (props) =>
@ -59,7 +59,7 @@ const mapDispatchToProps = (dispatch) =>
return {
toggleConsumerWindow : () =>
{
dispatch(stateActions.toggleConsumerWindow());
dispatch(roomActions.toggleConsumerWindow());
}
};
};

View File

@ -8,17 +8,9 @@ export const Room = PropTypes.shape(
activeSpeakerId : PropTypes.string
});
export const Device = PropTypes.shape(
{
flag : PropTypes.string.isRequired,
name : PropTypes.string.isRequired,
version : PropTypes.string
});
export const Me = PropTypes.shape(
{
id : PropTypes.string.isRequired,
device : Device.isRequired,
canSendMic : PropTypes.bool.isRequired,
canSendWebcam : PropTypes.bool.isRequired,
webcamInProgress : PropTypes.bool.isRequired
@ -39,7 +31,6 @@ export const Peer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
displayName : PropTypes.string,
device : Device.isRequired,
consumers : PropTypes.arrayOf(PropTypes.string).isRequired
});

View File

@ -18,6 +18,11 @@ html
font-family: 'Roboto';
font-weight: 300;
margin : 0;
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body

View File

@ -2,14 +2,16 @@ import domready from 'domready';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
import randomString from 'random-string';
import Logger from './Logger';
import debug from 'debug';
import RoomClient from './RoomClient';
import RoomContext from './RoomContext';
import deviceInfo from './deviceInfo';
import * as stateActions from './actions/stateActions';
import Room from './components/Room';
import * as roomActions from './actions/roomActions';
import * as meActions from './actions/meActions';
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';
@ -17,9 +19,27 @@ import { persistor, store } from './store';
import { SnackbarProvider } from 'notistack';
import * as serviceWorker from './serviceWorker';
import messagesEnglish from './translations/en';
import messagesNorwegian from './translations/nb';
import './index.css';
if (process.env.NODE_ENV !== 'production')
const cache = createIntlCache();
const messages =
{
'en' : messagesEnglish,
'nb' : messagesNorwegian
};
const locale = navigator.language.split(/[-_]/)[0]; // language without region code
const intl = createIntl({
locale,
messages : messages[locale]
}, cache);
if (process.env.REACT_APP_DEBUG === '*' || process.env.NODE_ENV !== 'production')
{
debug.enable('* -engine* -socket* -RIE* *WARN* *ERROR*');
}
@ -28,7 +48,7 @@ const logger = new Logger();
let roomClient;
RoomClient.init({ store });
RoomClient.init({ store, intl });
const theme = createMuiTheme(window.config.theme);
@ -62,8 +82,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';
@ -73,31 +93,32 @@ function run()
const device = deviceInfo();
store.dispatch(
stateActions.setRoomUrl(roomUrl));
roomActions.setRoomUrl(roomUrl));
store.dispatch(
stateActions.setMe({
meActions.setMe({
peerId,
device,
loginEnabled : window.config.loginEnabled
})
);
roomClient = new RoomClient(
{ roomId, peerId, device, useSimulcast, produce, consume, forceTcp });
{ roomId, peerId, accessCode, device, useSimulcast, produce, forceTcp });
global.CLIENT = roomClient;
render(
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<RawIntlProvider value={intl}>
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<Room />
<App />
</SnackbarProvider>
</RoomContext.Provider>
</PersistGate>
</RawIntlProvider>
</MuiThemeProvider>
</Provider>,
document.getElementById('multiparty-meeting')

View File

@ -3,7 +3,7 @@ import
createNewMessage
} from './helper';
const chatmessages = (state = [], action) =>
const chat = (state = [], action) =>
{
switch (action.type)
{
@ -30,14 +30,9 @@ const chatmessages = (state = [], action) =>
return [ ...state, ...chatHistory ];
}
case 'DROP_MESSAGES':
{
return [];
}
default:
return state;
}
};
export default chatmessages;
export default chat;

View File

@ -85,13 +85,6 @@ const files = (state = {}, action) =>
return { ...state, [magnetUri]: newFile };
}
case 'REMOVE_FILE':
{
const { magnetUri } = action.payload;
return state.filter((file) => file.magnetUri !== magnetUri);
}
default:
return state;
}

View File

@ -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;

View File

@ -1,7 +1,7 @@
const initialState =
{
id : null,
device : null,
picture : null,
canSendMic : false,
canSendWebcam : false,
canShareScreen : false,
@ -11,6 +11,7 @@ const initialState =
webcamInProgress : false,
audioInProgress : false,
screenShareInProgress : false,
displayNameInProgress : false,
loginEnabled : false,
raiseHand : false,
raiseHandInProgress : false,
@ -25,23 +26,25 @@ const me = (state = initialState, action) =>
{
const {
peerId,
device,
loginEnabled
} = action.payload;
return {
...state,
id : peerId,
device,
loginEnabled
};
}
case 'LOGGED_IN':
return { ...state, loggedIn: true };
{
const { flag } = action.payload;
case 'USER_LOGOUT':
return { ...state, loggedIn: false };
return { ...state, loggedIn: flag };
}
case 'SET_PICTURE':
return { ...state, picture: action.payload.picture };
case 'SET_MEDIA_CAPABILITIES':
{
@ -110,6 +113,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;
}

View File

@ -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,9 +60,39 @@ 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':
@ -74,13 +116,6 @@ const room = (state = initialState, action) =>
return { ...state, torrentSupport: supported };
}
case 'TOGGLE_SETTINGS':
{
const showSettings = !state.showSettings;
return { ...state, showSettings };
}
case 'TOGGLE_JOINED':
{
const joined = !state.joined;

View File

@ -3,10 +3,11 @@ 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';
import chatmessages from './chatmessages';
import chat from './chat';
import toolarea from './toolarea';
import files from './files';
import settings from './settings';
@ -16,10 +17,11 @@ export default combineReducers({
me,
producers,
peers,
lobbyPeers,
consumers,
peerVolumes,
notifications,
chatmessages,
chat,
toolarea,
files,
settings

View File

@ -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;

View File

@ -23,7 +23,7 @@ const reduxMiddlewares =
thunk
];
if (process.env.NODE_ENV !== 'production')
if (process.env.REACT_APP_DEBUG === '*' || process.env.NODE_ENV !== 'production')
{
const reduxLogger = createLogger(
{

View File

@ -0,0 +1,133 @@
{
"socket.disconnected": "You are disconnected",
"socket.reconnecting": "You are disconnected, attempting to reconnect",
"socket.reconnected": "You are reconnected",
"socket.requestError": "Error on server request",
"room.cookieConsent": "This website uses cookies to enhance the user experience",
"room.joined": "You have joined the room",
"room.cantJoin": "Unable to join the room",
"room.youLocked": "You locked the room",
"room.cantLock": "Unable to lock the room",
"room.youUnLocked": "You unlocked the room",
"room.cantUnLock": "Unable to unlock the room",
"room.locked": "Room is now locked",
"room.unlocked": "Room is now unlocked",
"room.newLobbyPeer": "New participant entered the lobby",
"room.lobbyPeerLeft": "Participant in lobby left",
"room.lobbyPeerChangedDisplayName": "Participant in lobby changed name to {displayName}",
"room.lobbyPeerChangedPicture": "Participant in lobby changed picture",
"room.setAccessCode": "Access code for room updated",
"room.accessCodeOn": "Access code for room is now activated",
"room.accessCodeOff": "Access code for room is now deactivated",
"room.peerChangedDisplayName": "{oldDisplayName} is now {displayName}",
"room.newPeer": "{displayName} joined the room",
"room.newFile": "New file available",
"room.toggleAdvancedMode": "Toggled advanced mode",
"room.setDemocraticView": "Changed layout to democratic view",
"room.setFilmStripView": "Changed layout to filmstrip view",
"room.loggedIn": "You are logged in",
"room.loggedOut": "You are logged out",
"room.changedDisplayName": "Your display name changed to {displayName}",
"room.changeDisplayNameError": "An error occured while changing your display name",
"room.chatError": "Unable to send chat message",
"room.aboutToJoin": "You are about to join a meeting",
"room.roomId": "Room ID: {roomName}",
"room.setYourName": "Set your name for participation, and choose how you want to join:",
"room.audioOnly": "Audio only",
"room.audioVideo": "Audio and Video",
"room.youAreReady": "Ok, you are ready",
"room.emptyRequireLogin": "The room is empty! You can Log In to start the meeting or wait until the host joins",
"room.locketWait": "The room is locked - hang on until somebody lets you in ...",
"room.lobbyAdministration": "Lobby administration",
"room.peersInLobby": "Participants in Lobby",
"room.lobbyEmpty": "There are currently no one in the lobby",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Me",
"room.spotlights": "Participants in Spotlight",
"room.passive": "Passive Participants",
"room.videoPaused": "This video is paused",
"tooltip.login": "Log in",
"tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Admit from lobby",
"tooltip.lockRoom": "Lock room",
"tooltip.unLockRoom": "Unlock room",
"tooltip.enterFullscreen": "Enter fullscreen",
"tooltip.leaveFullscreen": "Leave fullscreen",
"tooltip.lobby": "Show lobby",
"tooltip.settings": "Show settings",
"label.yourName": "Your name",
"label.newWindow": "New window",
"label.fullscreen": "Fullscreen",
"label.openDrawer": "Open drawer",
"label.leave": "Leave",
"label.chatInput": "Enter chat message...",
"label.chat": "Chat",
"label.filesharing": "File sharing",
"label.participants": "Participants",
"label.shareFile": "Share file",
"label.fileSharingUnsupported": "File sharing not supported",
"label.unknown": "Unknown",
"label.democratic": "Democratic view",
"label.filmstrip": "Filmstrip view",
"label.low": "Low",
"label.medium": "Medium",
"label.high": "High (HD)",
"label.veryHigh": "Very high (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Close",
"settings.settings": "Settings",
"settings.camera": "Camera",
"settings.selectCamera": "Select video device",
"settings.cantSelectCamera": "Unable to select video device",
"settings.audio": "Audio device",
"settings.selectAudio": "Select audio device",
"settings.cantSelectAudio": "Unable to select audio device",
"settings.resolution": "Select your video resolution",
"settings.layout": "Room layout",
"settings.selectRoomLayout": "Select room layout",
"settings.advancedMode": "Advanced mode",
"filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file",
"filesharing.successfulFileShare": "File successfully shared",
"filesharing.unableToShare": "Unable to share file",
"filesharing.error": "There was a filesharing error",
"filesharing.finished": "File finished downloading",
"filesharing.save": "Save",
"filesharing.sharedFile": "{displayName} shared a file",
"filesharing.download": "Download",
"filesharing.missingSeeds": "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.",
"devices.devicesChanged": "Your devices changed, configure your devices in the settings dialog",
"device.audioUnsupported": "Audio unsupported",
"device.activateAudio": "Activate audio",
"device.muteAudio": "Mute audio",
"device.unMuteAudio": "Unmute audio",
"device.videoUnsupported": "Video unsupported",
"device.startVideo": "Start video",
"device.stopVideo": "Stop video",
"device.screenSharingUnsupported": "Screen sharing not supported",
"device.startScreenSharing": "Start screen sharing",
"device.stopScreenSharing": "Stop screen sharing",
"devices.microphoneDisconnected": "Microphone disconnected",
"devices.microphoneError": "An error occured while accessing your microphone",
"devices.microPhoneMute": "Muted your microphone",
"devices.micophoneUnMute": "Unmuted your microphone",
"devices.microphoneEnable": "Enabled your microphone",
"devices.microphoneMuteError": "Unable to mute your microphone",
"devices.microphoneUnMuteError": "Unable to unmute your microphone",
"devices.screenSharingDisconnected" : "Screen sharing disconnected",
"devices.screenSharingError": "An error occured while accessing your screen",
"devices.cameraDisconnected": "Camera disconnected",
"devices.cameraError": "An error occured while accessing your camera"
}

View File

@ -0,0 +1,133 @@
{
"socket.disconnected": "Du er frakoblet",
"socket.reconnecting": "Du er frakoblet, forsøker å koble til på nytt",
"socket.reconnected": "Du er koblet til igjen",
"socket.requestError": "Feil på server melding",
"room.cookieConsent": "Denne siden bruker cookies for å forbedre brukeropplevelsen",
"room.joined": "Du ble med i møtet",
"room.cantJoin": "Kunne ikke bli med i møtet",
"room.youLocked": "Du låste møtet",
"room.cantLock": "Klarte ikke å låse møtet",
"room.youUnLocked": "Du låste opp møtet",
"room.cantUnLock": "Klarte ikke å låse opp møtet",
"room.locked": "Møtet er låst",
"room.unlocked": "Møtet er låst opp",
"room.newLobbyPeer": "Ny deltaker i lobbyen",
"room.lobbyPeerLeft": "Deltaker i lobbyen forsvant",
"room.lobbyPeerChangedDisplayName": "Deltaker i lobbyen endret navn til {displayName}",
"room.lobbyPeerChangedPicture": "Deltaker i lobbyen endret bilde",
"room.setAccessCode": "Tilgangskode for møtet er oppdatert",
"room.accessCodeOn": "Tilgangskode for møtet er aktivert",
"room.accessCodeOff": "Tilgangskode for møtet er deaktivert",
"room.peerChangedDisplayName": "{oldDisplayName} heter nå {displayName}",
"room.newPeer": "{displayName} ble med i møtet",
"room.newFile": "Ny fil tilgjengelig",
"room.toggleAdvancedMode": "Aktiver avansert modus",
"room.setDemocraticView": "Endret layout til demokratisk",
"room.setFilmStripView": "Endret layout til filmstripe",
"room.loggedIn": "Du er logget inn",
"room.loggedOut": "Du er logget ut",
"room.changedDisplayName": "Navnet ditt er nå {displayName}",
"room.changeDisplayNameError": "Det skjedde en feil ved endring av navnet ditt",
"room.chatError": "Klarte ikke sende melding",
"room.aboutToJoin": "Du er i ferd med å bli med i et møte",
"room.roomId": "Møte ID: {roomName}",
"room.setYourName": "Skriv inn navnet ditt, og velg hvordan du vil bli med i møtet",
"room.audioOnly": "Kun lyd",
"room.audioVideo": "Lyd og bilde",
"room.youAreReady": "Ok, du er klar",
"room.emptyRequireLogin": "Møtet er tomt. Du kan logge inn for å starte møtet, eller vente til verten kommer",
"room.locketWait": "Møtet er låst, vent til noen slipper deg inn",
"room.lobbyAdministration": "Lobby administrasjon",
"room.peersInLobby": "Deltakere i lobbyen",
"room.lobbyEmpty": "Det er for øyeblikket ingen deltakere i lobbyen",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {deltaker} other {deltakere}}",
"room.me": "Meg",
"room.spotlights": "Deltakere i fokus",
"room.passive": "Passive deltakere",
"room.videoPaused": "Denne videoen er inaktiv",
"tooltip.login": "Logg in",
"tooltip.logout": "Logg ut",
"tooltip.admitFromLobby": "Slipp inn fra lobby",
"tooltip.lockRoom": "Lås møtet",
"tooltip.unLockRoom": "Lås opp møtet",
"tooltip.enterFullscreen": "Gå til fullskjerm",
"tooltip.leaveFullscreen": "Forlat fullskjerm",
"tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis innstillinger",
"label.yourName": "Ditt navn",
"label.newWindow": "Flytt til separat vindu",
"label.fullscreen": "Fullskjerm",
"label.openDrawer": "Åpne meny",
"label.leave": "Avslutt",
"label.chatInput": "Skriv melding...",
"label.chat": "Chat",
"label.filesharing": "Fildeling",
"label.participants": "Deltakere",
"label.shareFile": "Del fil",
"label.fileSharingUnsupported": "Fildeling ikke støttet",
"label.unknown": "Ukjent",
"label.democratic": "Demokratisk",
"label.filmstrip": "Filmstripe",
"label.low": "Lav",
"label.medium": "Medium",
"label.high": "Høy (HD)",
"label.veryHigh": "Veldig høy (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Lukk",
"settings.settings": "Innstillinger",
"settings.camera": "Kamera",
"settings.selectCamera": "Velg videoenhet",
"settings.cantSelectCamera": "Kan ikke velge videoenhet",
"settings.audio": "Lydenhet",
"settings.selectAudio": "Velg lydenhet",
"settings.cantSelectAudio": "Kan ikke velge lydenhet",
"settings.resolution": "Velg oppløsning",
"settings.layout": "Møtelayout",
"settings.selectRoomLayout": "Velg møtelayout",
"settings.advancedMode": "Avansert modus",
"filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling",
"filesharing.successfulFileShare": "Filen ble delt",
"filesharing.unableToShare": "Klarte ikke å dele fil",
"filesharing.error": "Det skjedde noe feil med fildeling",
"filesharing.finished": "Fil ferdig lastet ned",
"filesharing.save": "Lagre",
"filesharing.sharedFile": "{displayName} delte en fil",
"filesharing.download": "Last ned",
"filesharing.missingSeeds": "Dersom dette tar lang til mangler det kanskje noen som kan dele denne filen. Prøv å spørre noen om å laste opp filen på nytt.",
"devices.devicesChanged": "Medieenhetene dine endret seg, du kan konfigurere enheter i innstillinger",
"device.audioUnsupported": "Lyd ikke støttet",
"device.activateAudio": "Aktiver lyd",
"device.muteAudio": "Demp lyd",
"device.unMuteAudio": "Aktiver lyd",
"device.videoUnsupported": "Video ikke støttet",
"device.startVideo": "Start video",
"device.stopVideo": "Stopp video",
"device.screenSharingUnsupported": "Skjermdeling ikke støttet",
"device.startScreenSharing": "Start skjermdeling",
"device.stopScreenSharing": "Stopp skjermdeling",
"devices.microphoneDisconnected": "Mikrofon koblet fra",
"devices.microphoneError": "Det skjedde noe feil med mikrofonen din",
"devices.microPhoneMute": "Dempet mikrofonen",
"devices.micophoneUnMute": "Aktiverte mikrofonen",
"devices.microphoneEnable": "Aktiverte mikrofonen",
"devices.microphoneMuteError": "Klarte ikke å dempe mikrofonen",
"devices.microphoneUnMuteError": "Klarte ikke å aktivere mikrofonen",
"devices.screenSharingDisconnected" : "Skjermdelingen forsvant",
"devices.screenSharingError": "Det skjedde noe feil med skjermdelingen din",
"devices.cameraDisconnected": "Kamera koblet fra",
"devices.cameraError": "Det skjedde noe feil med kameraet ditt"
}

View File

@ -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
}
};

View File

@ -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
}
}

View File

@ -3,26 +3,27 @@ const os = require('os');
module.exports =
{
// oAuth2 conf
auth :
{
/*
The issuer URL for OpenID Connect discovery
The OpenID Provider Configuration Document
could be discovered on:
issuerURL + '/.well-known/openid-configuration'
*/
issuerURL : 'https://example.com',
clientOptions :
/* auth :
{
// The issuer URL for OpenID Connect discovery
// The OpenID Provider Configuration Document
// could be discovered on:
// issuerURL + '/.well-known/openid-configuration'
// issuerURL : 'https://example.com',
// clientOptions :
// {
client_id : '',
client_secret : '',
scope : 'openid email profile',
// where client.example.com is your multiparty meeting server
redirect_uri : 'https://client.example.com/auth/callback'
}
},
},*/
// 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 :
{
@ -84,6 +95,7 @@ module.exports =
{
listenIps :
[
// change ip to your servers IP address!
{ ip: '1.2.3.4', announcedIp: null }
],
maxIncomingBitrate : 1500000,

View File

@ -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'));

View File

@ -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();
};

View File

@ -0,0 +1,41 @@
exports.loginHelper = function(data)
{
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Multiparty Meeting</title>
</head>
<body>
<script type='text/javascript'>
let data = ${JSON.stringify(data)};
window.opener.CLIENT.receiveLoginChildWindow(data);
window.close();
</script>
</body>
</html>`;
return html;
};
exports.logoutHelper = function()
{
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Multiparty Meeting</title>
</head>
<body>
<script type='text/javascript'>
window.opener.CLIENT.receiveLogoutChildWindow();
window.close();
</script>
</body>
</html>`;
return html;
};

208
server/lib/Lobby.js 100644
View File

@ -0,0 +1,208 @@
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);
if (peer.authenticated)
{
this.emit('changeDisplayName', peer);
this.emit('changePicture', peer);
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;

View File

@ -1,5 +1,3 @@
'use strict';
const debug = require('debug');
const APP_NAME = 'multiparty-meeting-server';

329
server/lib/Peer.js 100644
View File

@ -0,0 +1,329 @@
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._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 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
};
return peerInfo;
}
}
module.exports = Peer;

File diff suppressed because it is too large Load Diff

View File

@ -1,380 +0,0 @@
'use strict';
const path = require('path');
const fs = require('fs');
const STATS_INTERVAL = 4000; // TODO
function homer(server)
{
if (!process.env.MEDIASOUP_HOMER_OUTPUT)
throw new Error('MEDIASOUP_HOMER_OUTPUT env not set');
server.on('newroom', (room) =>
{
const fileName =
path.join(process.env.MEDIASOUP_HOMER_OUTPUT);
const stream = fs.createWriteStream(fileName, { flags: 'a' });
emit(
{
event : 'server.newroom',
roomId : room.id,
rtpCapabilities : room.rtpCapabilities
},
stream);
handleRoom(room, stream);
});
}
function handleRoom(room, stream)
{
const baseEvent =
{
roomId : room.id
};
room.on('close', () =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'room.close'
}),
stream);
stream.end();
});
room.on('newpeer', (peer) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'room.newpeer',
peerId : peer.id,
rtpCapabilities : peer.rtpCapabilities
}),
stream);
handlePeer(peer, baseEvent, stream);
});
}
function handlePeer(peer, baseEvent, stream)
{
baseEvent = Object.assign({}, baseEvent,
{
peerId : peer.id
});
peer.on('close', (originator) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'peer.close',
originator : originator
}),
stream);
});
peer.on('newtransport', (transport) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'peer.newtransport',
transportId : transport.id,
direction : transport.direction,
iceLocalCandidates : transport.iceLocalCandidates
}),
stream);
handleTransport(transport, baseEvent, stream);
});
peer.on('newproducer', (producer) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'peer.newproducer',
producerId : producer.id,
kind : producer.kind,
transportId : producer.transport.id,
rtpParameters : producer.rtpParameters
}),
stream);
handleProducer(producer, baseEvent, stream);
});
peer.on('newconsumer', (consumer) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'peer.newconsumer',
consumerId : consumer.id,
kind : consumer.kind,
sourceId : consumer.source.id,
rtpParameters : consumer.rtpParameters
}),
stream);
handleConsumer(consumer, baseEvent, stream);
});
// Must also handle existing Consumers at the time the Peer was created.
for (const consumer of peer.consumers)
{
emit(
Object.assign({}, baseEvent,
{
event : 'peer.newconsumer',
consumerId : consumer.id,
kind : consumer.kind,
sourceId : consumer.source.id,
rtpParameters : consumer.rtpParameters
}),
stream);
handleConsumer(consumer, baseEvent, stream);
}
}
function handleTransport(transport, baseEvent, stream)
{
baseEvent = Object.assign({}, baseEvent,
{
transportId : transport.id
});
const statsInterval = setInterval(() =>
{
if (typeof transport.getStats === 'function')
{
transport.getStats()
.then((stats) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'transport.stats',
stats : stats
}),
stream);
});
}
}, STATS_INTERVAL);
transport.on('close', (originator) =>
{
clearInterval(statsInterval);
emit(
Object.assign({}, baseEvent,
{
event : 'transport.close',
originator : originator
}),
stream);
});
transport.on('iceselectedtuplechange', (iceSelectedTuple) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'transport.iceselectedtuplechange',
iceSelectedTuple : iceSelectedTuple
}),
stream);
});
transport.on('icestatechange', (iceState) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'transport.icestatechange',
iceState : iceState
}),
stream);
});
transport.on('dtlsstatechange', (dtlsState) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'transport.dtlsstatechange',
dtlsState : dtlsState
}),
stream);
});
}
function handleProducer(producer, baseEvent, stream)
{
baseEvent = Object.assign({}, baseEvent,
{
producerId : producer.id
});
const statsInterval = setInterval(() =>
{
producer.getStats()
.then((stats) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'producer.stats',
stats : stats
}),
stream);
});
}, STATS_INTERVAL);
producer.on('close', (originator) =>
{
clearInterval(statsInterval);
emit(
Object.assign({}, baseEvent,
{
event : 'producer.close',
originator : originator
}),
stream);
});
producer.on('pause', (originator) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'producer.pause',
originator : originator
}),
stream);
});
producer.on('resume', (originator) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'producer.resume',
originator : originator
}),
stream);
});
}
function handleConsumer(consumer, baseEvent, stream)
{
baseEvent = Object.assign({}, baseEvent,
{
consumerId : consumer.id
});
const statsInterval = setInterval(() =>
{
consumer.getStats()
.then((stats) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.stats',
stats : stats
}),
stream);
});
}, STATS_INTERVAL);
consumer.on('close', (originator) =>
{
clearInterval(statsInterval);
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.close',
originator : originator
}),
stream);
});
consumer.on('handled', () =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.handled',
transportId : consumer.transport.id
}),
stream);
});
consumer.on('unhandled', () =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.handled'
}),
stream);
});
consumer.on('pause', (originator) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.pause',
originator : originator
}),
stream);
});
consumer.on('resume', (originator) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.resume',
originator : originator
}),
stream);
});
consumer.on('effectiveprofilechange', (profile) =>
{
emit(
Object.assign({}, baseEvent,
{
event : 'consumer.effectiveprofilechange',
profile : profile
}),
stream);
});
}
function emit(event, stream)
{
// Add timestamp.
event.timestamp = Date.now();
const line = JSON.stringify(event);
stream.write(line);
stream.write('\n');
}
module.exports = homer;

View File

@ -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;

View File

@ -1,6 +1,6 @@
{
"name": "multiparty-meeting-server",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
@ -9,20 +9,21 @@
"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",
"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": "^2.5.0",
"openid-client": "^3.7.3",
"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"
"redis": "^2.8.0",
"socket.io": "^2.3.0",
"spdy": "^4.0.1"
}
}

View File

@ -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)
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,6 +107,11 @@ passport.deserializeUser((user, done) =>
done(null, user);
});
let httpsServer;
let io;
let oidcClient;
let oidcStrategy;
const auth = config.auth;
async function run()
@ -74,7 +122,7 @@ async function run()
typeof(auth.clientOptions) !== 'undefined'
)
{
Issuer.discover(auth.issuerURL).then( async (oidcIssuer) =>
Issuer.discover(auth.issuerURL).then(async (oidcIssuer) =>
{
// Setup authentication
await setupAuth(oidcIssuer);
@ -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)
@ -136,10 +193,9 @@ async function setupAuth(oidcIssuer)
// resolved from the issuer configuration, instead of true you may provide
// any of the supported values directly, i.e. "S256" (recommended) or "plain"
const usePKCE = false;
const client = oidcClient;
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,10 +282,8 @@ async function setupAuth(oidcIssuer)
{
const state = JSON.parse(base64.decode(req.query.state));
if (rooms.has(state.roomId))
{
let displayName;
let photo;
let picture;
if (req.user != null)
{
@ -247,29 +292,22 @@ async function setupAuth(oidcIssuer)
else
displayName = '';
if (
req.user.Photos != null &&
req.user.Photos[0] != null &&
req.user.Photos[0].value != null
)
photo = req.user.Photos[0].value;
if (req.user.picture != null)
picture = req.user.picture;
else
photo = '/static/media/buddy.403cb9f6.svg';
picture = '/static/media/buddy.403cb9f6.svg';
}
const data =
{
peerId : state.peerId,
displayName : displayName,
picture : photo
};
const peer = peers.get(state.id);
const room = rooms.get(state.roomId);
peer && (peer.displayName = displayName);
peer && (peer.picture = picture);
peer && (peer.authenticated = true);
room.authCallback(data);
}
res.send('');
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,12 +386,17 @@ 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);
logger.error('room creation or room joining failed [error:"%o"]', error);
socket.disconnect(true);
@ -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();

View File

@ -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('');
}