Merge branch 'feat-lobby' into develop

master
Håvar Aambø Fosstveit 2019-11-04 23:40:47 +01:00
commit 51e4d6664d
49 changed files with 3570 additions and 1911 deletions

View File

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

@ -6,32 +6,32 @@
"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",
"create-torrent": "^4.4.1",
"domready": "^1.0.8",
"file-saver": "^2.0.1",
"file-saver": "^2.0.2",
"hark": "^1.2.3",
"marked": "^0.6.1",
"mediasoup-client": "^3.0.6",
"notistack": "^0.5.1",
"marked": "^0.7.0",
"mediasoup-client": "^3.2.7",
"notistack": "^0.9.5",
"prop-types": "^15.7.2",
"random-string": "^0.2.0",
"react": "^16.8.5",
"react-cookie-consent": "^2.2.2",
"react-dom": "^16.8.5",
"react-redux": "^6.0.1",
"react-scripts": "2.1.8",
"redux": "^4.0.1",
"react": "^16.10.2",
"react-cookie-consent": "^2.5.0",
"react-dom": "^16.10.2",
"react-redux": "^7.1.1",
"react-scripts": "3.2.0",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-persist": "^5.10.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"riek": "^1.1.0",
"socket.io-client": "^2.2.0",
"source-map-explorer": "^1.8.0",
"webtorrent": "^0.103.1"
"socket.io-client": "^2.3.0",
"source-map-explorer": "^2.1.0",
"webtorrent": "^0.107.16"
},
"scripts": {
"analyze-main": "source-map-explorer build/static/js/main.*",
@ -41,342 +41,15 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"env": {
"browser": true,
"es6": true,
"node": true
},
"plugins": [
"import",
"react"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"settings": {
"react": {
"pragma": "React",
"version": "16"
}
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true,
"jsx": true
}
},
"rules": {
"array-bracket-spacing": [
2,
"always",
{
"objectsInArrays": true,
"arraysInArrays": true
}
],
"arrow-parens": [
2,
"always"
],
"arrow-spacing": 2,
"block-spacing": [
2,
"always"
],
"brace-style": [
2,
"allman",
{
"allowSingleLine": true
}
],
"camelcase": 2,
"comma-dangle": 2,
"comma-spacing": [
2,
{
"before": false,
"after": true
}
],
"comma-style": 2,
"computed-property-spacing": 2,
"constructor-super": 2,
"func-call-spacing": 2,
"generator-star-spacing": 2,
"guard-for-in": 2,
"indent": [
2,
"tab",
{
"SwitchCase": 1
}
],
"key-spacing": [
2,
{
"singleLine": {
"beforeColon": false,
"afterColon": true
},
"multiLine": {
"beforeColon": true,
"afterColon": true,
"align": "colon"
}
}
],
"keyword-spacing": 2,
"linebreak-style": [
2,
"unix"
],
"lines-around-comment": [
2,
{
"allowBlockStart": true,
"allowObjectStart": true,
"beforeBlockComment": true,
"beforeLineComment": false
}
],
"max-len": [
2,
90,
{
"tabWidth": 2,
"comments": 110,
"ignoreUrls": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true,
"ignoreRegExpLiterals": true
}
],
"newline-after-var": 2,
"newline-before-return": 2,
"newline-per-chained-call": 2,
"no-alert": 2,
"no-caller": 2,
"no-case-declarations": 2,
"no-catch-shadow": 2,
"no-class-assign": 2,
"no-confusing-arrow": [
"error",
{
"allowParens": true
}
],
"no-console": 2,
"no-const-assign": 2,
"no-debugger": 2,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-div-regex": 2,
"no-empty": [
2,
{
"allowEmptyCatch": true
}
],
"no-empty-pattern": 2,
"no-else-return": 0,
"no-eval": 2,
"no-extend-native": 2,
"no-ex-assign": 2,
"no-extra-bind": 2,
"no-extra-boolean-cast": 2,
"no-extra-label": 2,
"no-extra-semi": 2,
"no-fallthrough": 2,
"no-func-assign": 2,
"no-global-assign": 2,
"no-implicit-coercion": 2,
"no-implicit-globals": 2,
"no-inner-declarations": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-lonely-if": 2,
"no-mixed-operators": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": [
2,
{
"max": 1,
"maxEOF": 0,
"maxBOF": 0
}
],
"no-native-reassign": 2,
"no-negated-in-lhs": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-obj-calls": 2,
"no-proto": 2,
"no-prototype-builtins": 0,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-restricted-imports": 2,
"no-return-assign": 2,
"no-self-assign": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-sparse-arrays": 2,
"no-this-before-super": 2,
"no-throw-literal": 2,
"no-undef": 2,
"no-unexpected-multiline": 2,
"no-unmodified-loop-condition": 2,
"no-unreachable": 2,
"no-unused-vars": [
1,
{
"vars": "all",
"args": "after-used"
}
],
"no-use-before-define": [
2,
{
"functions": false
}
],
"no-useless-call": 2,
"no-useless-computed-key": 2,
"no-useless-concat": 2,
"no-useless-rename": 2,
"no-var": 2,
"no-whitespace-before-property": 2,
"object-curly-newline": 0,
"object-curly-spacing": [
2,
"always"
],
"object-property-newline": [
2,
{
"allowMultiplePropertiesPerLine": true
}
],
"prefer-const": 2,
"prefer-rest-params": 2,
"prefer-spread": 2,
"prefer-template": 2,
"quotes": [
2,
"single",
{
"avoidEscape": true
}
],
"semi": [
2,
"always"
],
"semi-spacing": 2,
"space-before-blocks": 2,
"space-before-function-paren": [
2,
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": [
2,
"never"
],
"spaced-comment": [
2,
"always"
],
"strict": 2,
"valid-typeof": 2,
"eol-last": 0,
"yoda": 2,
"import/extensions": 2,
"import/no-duplicates": 2,
"jsx-quotes": [
2,
"prefer-single"
],
"react/display-name": [
2,
{
"ignoreTranspilerName": false
}
],
"react/forbid-prop-types": 0,
"react/jsx-boolean-value": 2,
"react/jsx-closing-bracket-location": 2,
"react/jsx-curly-spacing": 2,
"react/jsx-equals-spacing": 2,
"react/jsx-handler-names": 2,
"react/jsx-indent-props": [
2,
"tab"
],
"react/jsx-indent": [
2,
"tab"
],
"react/jsx-key": 2,
"react/jsx-max-props-per-line": 0,
"react/jsx-no-bind": 0,
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-literals": 0,
"react/jsx-no-undef": 0,
"react/jsx-pascal-case": 2,
"react/jsx-sort-prop-types": 0,
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/no-danger": 2,
"react/no-deprecated": 2,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
"react/no-is-mounted": 2,
"react/no-multi-comp": 0,
"react/no-set-state": 0,
"react/no-string-refs": 0,
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
"react/prop-types": [
2,
{
"skipUndeclared": true
}
],
"react/react-in-jsx-scope": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 0,
"react/jsx-wrap-multilines": [
2,
{
"declaration": false,
"assignment": false,
"return": true
}
]
}
},
"browserslist": [
">0.2%",
"not dead",
"not ie > 0",
"not op_mini all"
]
],
"devDependencies": {
"eslint": "^6.5.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-react": "^7.16.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,14 @@ export const setRoomUrl = (url) =>
};
};
export const setRoomName = (name) =>
{
return {
type : 'SET_ROOM_NAME',
payload : { name }
};
};
export const setRoomState = (state) =>
{
return {
@ -36,10 +44,35 @@ export const setRoomUnLocked = () =>
};
};
export const setRoomLockedOut = () =>
export const setInLobby = (inLobby) =>
{
return {
type : 'SET_ROOM_LOCKED_OUT'
type : 'SET_IN_LOBBY',
payload : { inLobby }
};
};
export const setSignInRequired = (signInRequired) =>
{
return {
type : 'SET_SIGN_IN_REQUIRED',
payload : { signInRequired }
};
};
export const setAccessCode = (accessCode) =>
{
return {
type : 'SET_ACCESS_CODE',
payload : { accessCode }
};
};
export const setJoinByAccessCode = (joinByAccessCode) =>
{
return {
type : 'SET_JOIN_BY_ACCESS_CODE',
payload : { joinByAccessCode }
};
};
@ -49,6 +82,12 @@ export const setSettingsOpen = ({ settingsOpen }) =>
payload : { settingsOpen }
});
export const setLockDialogOpen = ({ lockDialogOpen }) =>
({
type : 'SET_LOCK_DIALOG_OPEN',
payload : { lockDialogOpen }
});
export const setMe = ({ peerId, device, loginEnabled }) =>
{
return {
@ -126,6 +165,14 @@ export const setDisplayName = (displayName) =>
};
};
export const setDisplayNameInProgress = (flag) =>
{
return {
type : 'SET_DISPLAY_NAME_IN_PROGRESS',
payload : { flag }
};
};
export const toggleAdvancedMode = () =>
{
return {
@ -178,6 +225,13 @@ export const toggleSettings = () =>
};
};
export const toggleLockDialog = () =>
{
return {
type : 'TOGGLE_LOCK_DIALOG'
};
};
export const toggleToolArea = () =>
{
return {
@ -391,6 +445,46 @@ export const setPeerVolume = (peerId, volume) =>
};
};
export const addLobbyPeer = (peerId) =>
{
return {
type : 'ADD_LOBBY_PEER',
payload : { peerId }
};
};
export const removeLobbyPeer = (peerId) =>
{
return {
type : 'REMOVE_LOBBY_PEER',
payload : { peerId }
};
};
export const setLobbyPeerDisplayName = (displayName, peerId) =>
{
return {
type : 'SET_LOBBY_PEER_DISPLAY_NAME',
payload : { displayName, peerId }
};
};
export const setLobbyPeerPicture = (picture, peerId) =>
{
return {
type : 'SET_LOBBY_PEER_PICTURE',
payload : { picture, peerId }
};
};
export const setLobbyPeerPromotionInProgress = (peerId, flag) =>
{
return {
type : 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS',
payload : { peerId, flag }
};
};
export const addNotification = (notification) =>
{
return {
@ -555,9 +649,10 @@ export const setPeerPicture = (peerId, picture) =>
payload : { peerId, picture }
});
export const loggedIn = () =>
export const loggedIn = (flag) =>
({
type : 'LOGGED_IN'
type : 'LOGGED_IN',
payload : { flag }
});
export const toggleJoined = () =>

View File

@ -0,0 +1,148 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withRoomContext } from '../../../RoomContext';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Avatar from '@material-ui/core/Avatar';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import PromoteIcon from '@material-ui/icons/OpenInBrowser';
import Tooltip from '@material-ui/core/Tooltip';
const styles = (theme) =>
({
root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
avatar :
{
borderRadius : '50%',
height : '2rem'
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : theme.spacing(1),
flexGrow : 1,
alignItems : 'center'
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
},
button :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.5)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.promote' :
{
backgroundColor : 'var(--media-control-botton-on)'
}
},
ListItem :
{
alignItems : 'center'
}
});
const ListLobbyPeer = (props) =>
{
const {
roomClient,
peer,
classes
} = props;
const picture = peer.picture || EmptyAvatar;
return (
<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='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,200 @@
import React from 'react';
import { connect } from 'react-redux';
import {
lobbyPeersKeySelector
} from '../../Selectors';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import * as stateActions from '../../../actions/stateActions';
import PropTypes from 'prop-types';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import Button from '@material-ui/core/Button';
// import FormLabel from '@material-ui/core/FormLabel';
// import FormControl from '@material-ui/core/FormControl';
// import FormGroup from '@material-ui/core/FormGroup';
// import FormControlLabel from '@material-ui/core/FormControlLabel';
// import Checkbox from '@material-ui/core/Checkbox';
// import InputLabel from '@material-ui/core/InputLabel';
// import OutlinedInput from '@material-ui/core/OutlinedInput';
// import Switch from '@material-ui/core/Switch';
import List from '@material-ui/core/List';
import ListSubheader from '@material-ui/core/ListSubheader';
import ListLobbyPeer from './ListLobbyPeer';
const styles = (theme) =>
({
root :
{
},
dialogPaper :
{
width : '30vw',
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
}
},
lock :
{
padding : theme.spacing(2)
}
});
const LockDialog = ({
// roomClient,
room,
handleCloseLockDialog,
// handleAccessCode,
lobbyPeers,
classes
}) =>
{
return (
<Dialog
className={classes.root}
open={room.lockDialogOpen}
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>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'>
Participants in Lobby
</ListSubheader>
}
>
{
lobbyPeers.map((peerId) =>
{
return (<ListLobbyPeer key={peerId} id={peerId} />);
})
}
</List>
:
<DialogContent>
<DialogContentText gutterBottom>
There are currently no one in the lobby.
</DialogContentText>
</DialogContent>
}
<DialogActions>
<Button onClick={() => handleCloseLockDialog({ lockDialogOpen: false })} color='primary'>
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 : stateActions.setLockDialogOpen,
handleAccessCode : stateActions.setAccessCode
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.locked === next.room.locked &&
prev.room.joinByAccessCode === next.room.joinByAccessCode &&
prev.room.accessCode === next.room.accessCode &&
prev.room.code === next.room.code &&
prev.room.lockDialogOpen === next.room.lockDialogOpen &&
prev.room.codeHidden === next.room.codeHidden &&
prev.lobbyPeers === next.lobbyPeers
);
}
}
)(withStyles(styles)(LockDialog)));

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

@ -96,7 +96,7 @@ class HiddenPeers extends React.PureComponent
onClick={() => openUsersTab()}
>
<p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount === 1) ? null : 's'}
{(hiddenPeersCount > 1) && 's'}
</p>
</div>
);

View File

@ -332,13 +332,11 @@ const Me = (props) =>
}
}}
>
{ screenState === 'on' || screenState === 'unsupported' ?
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
:null
}
{ screenState === 'off' ?
{ screenState === 'off' &&
<ScreenIcon/>
:null
}
</Fab>
</div>
@ -351,10 +349,10 @@ const Me = (props) =>
peer={me}
displayName={settings.displayName}
showPeerInfo
videoTrack={webcamProducer ? webcamProducer.track : null}
videoTrack={webcamProducer && webcamProducer.track}
videoVisible={videoVisible}
audioCodec={micProducer ? micProducer.codec : null}
videoCodec={webcamProducer ? webcamProducer.codec : null}
audioCodec={micProducer && micProducer.codec}
videoCodec={webcamProducer && webcamProducer.codec}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
@ -364,9 +362,9 @@ const Me = (props) =>
</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 +388,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={() =>
@ -420,13 +418,12 @@ const Me = (props) =>
isScreen
advancedMode={advancedMode}
videoContain
videoTrack={screenProducer ? screenProducer.track : null}
videoTrack={screenProducer && screenProducer.track}
videoVisible={screenVisible}
videoCodec={screenProducer ? screenProducer.codec : null}
videoCodec={screenProducer && screenProducer.codec}
/>
</div>
</div>
:null
}
</React.Fragment>
);

View File

@ -166,8 +166,8 @@ const Peer = (props) =>
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
hover && 'hover',
activeSpeaker && 'active-speaker'
)
}
onMouseOver={() => setHover(true)}
@ -192,15 +192,14 @@ const Peer = (props) =>
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible ?
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>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={() =>
@ -241,7 +240,7 @@ const Peer = (props) =>
}
</Fab>
{ !smallScreen ?
{ !smallScreen &&
<Fab
aria-label='New window'
className={classes.fab}
@ -257,7 +256,6 @@ const Peer = (props) =>
>
<NewWindowIcon />
</Fab>
:null
}
<Fab
@ -279,20 +277,20 @@ const Peer = (props) =>
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 +312,16 @@ const Peer = (props) =>
}}
style={rootStyle}
>
{ !screenVisible ?
{ !screenVisible &&
<div className={classes.videoInfo}>
<p>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,7 +343,7 @@ const Peer = (props) =>
}, 2000);
}}
>
{ !smallScreen ?
{ !smallScreen &&
<Fab
aria-label='New window'
className={classes.fab}
@ -362,7 +359,6 @@ const Peer = (props) =>
>
<NewWindowIcon />
</Fab>
:null
}
<Fab
@ -381,16 +377,14 @@ const Peer = (props) =>
<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>
);

View File

@ -117,11 +117,10 @@ const SpeakerPeer = (props) =>
style={spacingStyle}
>
<div className={classnames(classes.viewContainer)} style={style}>
{ !videoVisible ?
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
</div>
:null
}
<VideoView
@ -140,18 +139,17 @@ 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>
</div>
:null
}
{ screenVisible ?
{ screenVisible &&
<div className={classnames(classes.viewContainer)} style={style}>
<VideoView
advancedMode={advancedMode}
@ -162,10 +160,8 @@ const SpeakerPeer = (props) =>
videoCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
:null
}
</div>
:null
}
</React.Fragment>
);

View File

@ -1,90 +1,355 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import * as stateActions from '../actions/stateActions';
import PropTypes from 'prop-types';
import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText';
import IconButton from '@material-ui/core/IconButton';
import AccountCircle from '@material-ui/icons/AccountCircle';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import CookieConsent from 'react-cookie-consent';
import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent';
import MuiDialogActions from '@material-ui/core/DialogActions';
const styles = (theme) =>
({
root :
{
display : 'flex',
width : '100%',
height : '100%',
backgroundColor : 'var(--background-color)',
backgroundImage : `url(${window.config.background})`,
backgroundAttachment : 'fixed',
backgroundPosition : 'center',
backgroundSize : 'cover',
backgroundRepeat : 'no-repeat'
},
dialogTitle :
{
},
dialogPaper :
{
width : '20vw',
width : '30vw',
padding : theme.spacing(2),
[theme.breakpoints.down('lg')] :
{
width : '30vw'
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '40vw'
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '60vw'
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '80vw'
width : '90vw'
}
},
logo :
{
display : 'block'
display : 'block',
paddingBottom : '1vh'
},
loginButton :
{
position : 'absolute',
right : theme.spacing(2),
top : theme.spacing(2),
padding : 0
},
largeIcon :
{
fontSize : '2em'
},
largeAvatar :
{
width : 50,
height : 50
},
green :
{
color : 'rgba(0, 153, 0, 1)'
}
});
const DialogTitle = withStyles(styles)((props) =>
{
const [ open, setOpen ] = useState(false);
useEffect(() =>
{
const openTimer = setTimeout(() => setOpen(true), 1000);
const closeTimer = setTimeout(() => setOpen(false), 4000);
return () =>
{
clearTimeout(openTimer);
clearTimeout(closeTimer);
};
}, []);
const { children, classes, myPicture, onLogin, ...other } = props;
const handleTooltipClose = () =>
{
setOpen(false);
};
const handleTooltipOpen = () =>
{
setOpen(true);
};
return (
<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='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
}) =>
{
return (
<Dialog
className={classes.root}
open
classes={{
paper : classes.dialogPaper
}}
>
{ window.config.logo ?
<img alt='Logo' className={classes.logo} src={window.config.logo} />
:null
const handleKeyDown = (event) =>
{
const { key } = event;
switch (key)
{
case 'Enter':
case 'Escape':
{
if (displayName === '')
changeDisplayName('Guest');
if (room.inLobby)
roomClient.changeDisplayName(displayName);
break;
}
<Typography variant='subtitle1'>You are about to join a meeting, how would you like to join?</Typography>
<DialogActions>
<Button
onClick={() =>
default:
break;
}
};
return (
<div className={classes.root}>
<Dialog
open
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle
myPicture={myPicture}
onLogin={() =>
{
roomClient.join({ joinVideo: false });
loggedIn ? roomClient.logout() : roomClient.login();
}}
variant='contained'
>
Audio only
</Button>
<Button
onClick={() =>
{
roomClient.join({ joinVideo: true });
}}
variant='contained'
>
Audio and Video
</Button>
</DialogActions>
</Dialog>
{ window.config.title }
<hr />
</DialogTitle>
<DialogContent>
<DialogContentText gutterBottom>
You are about to join a meeting.
</DialogContentText>
<DialogContentText variant='h6' gutterBottom align='center'>
Room ID: { room.name }
</DialogContentText>
<DialogContentText gutterBottom>
Set your name for participation,
and choose how you want to join:
</DialogContentText>
<TextField
id='displayname'
label='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={() =>
{
roomClient.join({ joinVideo: false });
}}
variant='contained'
color='secondary'
>
Audio only
</Button>
<Button
onClick={() =>
{
roomClient.join({ joinVideo: true });
}}
variant='contained'
color='secondary'
>
Audio and Video
</Button>
</DialogActions>
:
<DialogContent>
<DialogContentText
className={classes.green}
gutterBottom
variant='h6'
align='center'
>
Ok, you are ready
</DialogContentText>
{ room.signInRequired ?
<DialogContentText gutterBottom>
The room is empty!
You can Log In to start the meeting or wait until the host joins.
</DialogContentText>
:
<DialogContentText gutterBottom>
The room is locked - hang on until somebody lets you in ...
</DialogContentText>
}
</DialogContent>
}
<CookieConsent>
This website uses cookies to enhance the user experience.
</CookieConsent>
</Dialog>
</div>
);
};
JoinDialog.propTypes =
{
roomClient : PropTypes.any.isRequired,
classes : PropTypes.object.isRequired
roomClient : PropTypes.any.isRequired,
room : PropTypes.object.isRequired,
displayName : PropTypes.string.isRequired,
displayNameInProgress : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
loggedIn : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
changeDisplayName : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
export default withRoomContext(withStyles(styles)(JoinDialog));
const mapStateToProps = (state) =>
{
return {
room : state.room,
displayName : state.settings.displayName,
displayNameInProgress : state.me.displayNameInProgress,
loginEnabled : state.me.loginEnabled,
loggedIn : state.me.loggedIn,
myPicture : state.me.picture
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
changeDisplayName : (displayName) =>
{
dispatch(stateActions.setDisplayName(displayName));
}
};
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.inLobby === next.room.inLobby &&
prev.room.signInRequired === next.room.signInRequired &&
prev.settings.displayName === next.settings.displayName &&
prev.me.displayNameInProgress === next.me.displayNameInProgress &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.picture === next.me.picture
);
}
}
)(withStyles(styles)(JoinDialog)));

View File

@ -123,7 +123,7 @@ ChatInput.propTypes =
const mapStateToProps = (state) =>
({
displayName : state.settings.displayName,
picture : state.settings.picture
picture : state.me.picture
});
export default withRoomContext(
@ -136,7 +136,7 @@ export default withRoomContext(
{
return (
prev.settings.displayName === next.settings.displayName &&
prev.settings.picture === next.settings.picture
prev.me.picture === next.me.picture
);
}
}

View File

@ -97,7 +97,7 @@ MessageList.propTypes =
const mapStateToProps = (state) =>
({
chatmessages : state.chatmessages,
myPicture : state.settings.picture
myPicture : state.me.picture
});
export default connect(
@ -109,7 +109,7 @@ export default connect(
{
return (
prev.chatmessages === next.chatmessages &&
prev.settings.picture === next.settings.picture
prev.me.picture === next.me.picture
);
}
}

View File

@ -67,7 +67,7 @@ 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
@ -92,13 +92,12 @@ class File extends React.PureComponent
</div>
))}
</Fragment>
:null
}
<Typography className={classes.text}>
{ `${displayName} shared a file` }
</Typography>
{ !file.active && !file.files ?
{ (!file.active && !file.files) &&
<div className={classes.fileInfo}>
<Typography className={classes.text}>
{ magnet.decode(magnetUri).dn }
@ -121,20 +120,17 @@ class File extends React.PureComponent
</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.
</Typography>
:null
}
{ file.active ?
{ file.active &&
<progress value={file.progress} />
:null
}
</div>
</div>

View File

@ -45,7 +45,6 @@ class FileList extends React.PureComponent
const {
files,
me,
picture,
peers,
classes
} = this.props;
@ -61,7 +60,7 @@ class FileList extends React.PureComponent
if (me.id === file.peerId)
{
displayName = 'You';
filePicture = picture;
filePicture = me.picture;
}
else if (peers[file.peerId])
{
@ -91,7 +90,6 @@ FileList.propTypes =
{
files : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
picture : PropTypes.string,
peers : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
@ -99,10 +97,9 @@ FileList.propTypes =
const mapStateToProps = (state) =>
{
return {
files : state.files,
me : state.me,
picture : state.settings.picture,
peers : state.peers
files : state.files,
me : state.me,
peers : state.peers
};
};
@ -116,7 +113,6 @@ export default connect(
return (
prev.files === next.files &&
prev.me === next.me &&
prev.settings.picture === next.settings.picture &&
prev.peers === next.peers
);
}

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

@ -87,7 +87,6 @@ class ParticipantList extends React.PureComponent
<li className={classes.listheader}>Me:</li>
<ListMe />
</ul>
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Participants in Spotlight:</li>
{ spotlightPeers.map((peer) => (
@ -104,7 +103,6 @@ class ParticipantList extends React.PureComponent
</li>
))}
</ul>
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li>
{ passivePeers.map((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

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

@ -1,6 +1,9 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
lobbyPeersKeySelector
} from './Selectors';
import * as appPropTypes from './appPropTypes';
import { withRoomContext } from '../RoomContext';
import { withStyles } from '@material-ui/core/styles';
@ -13,7 +16,6 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
import Hidden from '@material-ui/core/Hidden';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
@ -30,11 +32,13 @@ import VideoWindow from './VideoWindow/VideoWindow';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import SettingsIcon from '@material-ui/icons/Settings';
import SecurityIcon from '@material-ui/icons/Security';
import LockDialog from './AccessControl/LockDialog/LockDialog';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import Button from '@material-ui/core/Button';
import Settings from './Settings/Settings';
import JoinDialog from './JoinDialog';
import Tooltip from '@material-ui/core/Tooltip';
const TIMEOUT = 10 * 1000;
@ -150,6 +154,38 @@ const styles = (theme) =>
}
});
const PulsingBadge = withStyles((theme) =>
({
badge :
{
backgroundColor : theme.palette.secondary.main,
// boxShadow : `0 0 0 2px ${theme.palette.secondary.main}`,
'&::after' :
{
position : 'absolute',
width : '100%',
height : '100%',
borderRadius : '50%',
animation : '$ripple 1.2s infinite ease-in-out',
border : `3px solid ${theme.palette.secondary.main}`,
content : '""'
}
},
'@keyframes ripple' :
{
'0%' :
{
transform : 'scale(.8)',
opacity : 1
},
'100%' :
{
transform : 'scale(2.4)',
opacity : 0
}
}
}))(Badge);
class Room extends React.PureComponent
{
constructor(props)
@ -227,11 +263,13 @@ class Room extends React.PureComponent
const {
roomClient,
room,
lobbyPeers,
advancedMode,
myPicture,
loggedIn,
loginEnabled,
setSettingsOpen,
setLockDialogOpen,
toolAreaOpen,
toggleToolArea,
unread,
@ -245,74 +283,52 @@ class Room extends React.PureComponent
democratic : Democratic
}[room.mode];
if (room.lockedOut)
{
return (
<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.
</CookieConsent>
return (
<div className={classes.root}>
<CookieConsent>
This website uses cookies to enhance the user experience.
</CookieConsent>
<FullScreenView advancedMode={advancedMode} />
<FullScreenView advancedMode={advancedMode} />
<VideoWindow advancedMode={advancedMode} />
<VideoWindow advancedMode={advancedMode} />
<AudioPeers />
<AudioPeers />
<Notifications />
<Notifications />
<CssBaseline />
<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'
<AppBar
position='fixed'
className={room.toolbarsVisible ? classes.show : classes.hide}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
>
<IconButton
color='inherit'
noWrap
aria-label='Open drawer'
onClick={() => toggleToolArea()}
className={classes.menuButton}
>
{ window.config.title }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<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={`${room.locked ? 'Unlock' : 'Lock'} room`}>
<IconButton
aria-label='Lock room'
className={classes.actionButton}
@ -335,7 +351,25 @@ class Room extends React.PureComponent
<LockOpenIcon />
}
</IconButton>
{ this.fullscreen.fullscreenEnabled ?
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip title='Show lobby'>
<IconButton
aria-label='Lobby'
color='inherit'
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</Tooltip>
}
{ this.fullscreen.fullscreenEnabled &&
<Tooltip title={`${this.state.fullscreen ? 'Leave' : 'Enter'} fullscreen`}>
<IconButton
aria-label='Fullscreen'
className={classes.actionButton}
@ -348,8 +382,9 @@ class Room extends React.PureComponent
<FullScreenIcon />
}
</IconButton>
:null
}
</Tooltip>
}
<Tooltip title='Show settings'>
<IconButton
aria-label='Settings'
className={classes.actionButton}
@ -358,7 +393,9 @@ class Room extends React.PureComponent
>
<SettingsIcon />
</IconButton>
{ loginEnabled ?
</Tooltip>
{ loginEnabled &&
<Tooltip title={`Log ${loggedIn ? 'out' : 'in'}`}>
<IconButton
aria-label='Account'
className={classes.actionButton}
@ -374,43 +411,44 @@ class Room extends React.PureComponent
<AccountCircle />
}
</IconButton>
:null
}
<Button
aria-label='Leave meeting'
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
Leave
</Button>
</div>
</Toolbar>
</AppBar>
<nav>
<Hidden implementation='css'>
<SwipeableDrawer
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={toolAreaOpen}
onClose={() => toggleToolArea()}
onOpen={() => toggleToolArea()}
classes={{
paper : classes.drawerPaper
}}
</Tooltip>
}
<Button
aria-label='Leave meeting'
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
<MeetingDrawer closeDrawer={toggleToolArea} />
</SwipeableDrawer>
</Hidden>
</nav>
Leave
</Button>
</div>
</Toolbar>
</AppBar>
<nav>
<Hidden implementation='css'>
<SwipeableDrawer
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={toolAreaOpen}
onClose={() => toggleToolArea()}
onOpen={() => toggleToolArea()}
classes={{
paper : classes.drawerPaper
}}
>
<MeetingDrawer closeDrawer={toggleToolArea} />
</SwipeableDrawer>
</Hidden>
</nav>
<View advancedMode={advancedMode} />
<View advancedMode={advancedMode} />
<Settings />
</div>
);
}
<LockDialog />
<Settings />
</div>
);
}
}
@ -418,6 +456,7 @@ Room.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
lobbyPeers : PropTypes.array,
advancedMode : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
@ -425,6 +464,7 @@ Room.propTypes =
toolAreaOpen : PropTypes.bool.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
classes : PropTypes.object.isRequired,
@ -434,10 +474,11 @@ Room.propTypes =
const mapStateToProps = (state) =>
({
room : state.room,
lobbyPeers : lobbyPeersKeySelector(state),
advancedMode : state.settings.advancedMode,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.settings.picture,
myPicture : state.me.picture,
toolAreaOpen : state.toolarea.toolAreaOpen,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles
@ -453,6 +494,10 @@ const mapDispatchToProps = (dispatch) =>
{
dispatch(stateActions.setSettingsOpen({ settingsOpen }));
},
setLockDialogOpen : (lockDialogOpen) =>
{
dispatch(stateActions.setLockDialogOpen({ lockDialogOpen }));
},
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
@ -468,9 +513,10 @@ export default withRoomContext(connect(
{
return (
prev.room === next.room &&
prev.lobbyPeers === next.lobbyPeers &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.settings.picture === next.settings.picture &&
prev.me.picture === next.me.picture &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&

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

@ -171,24 +171,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 ?
@ -212,17 +205,15 @@ class VideoView extends React.PureComponent
</span>
}
{ advancedMode ?
{ advancedMode &&
<div className={classes.deviceInfo}>
<span>
{peer.device.name} {Math.floor(peer.device.version) || null}
</span>
</div>
:null
}
</div>
</div>
:null
}
</div>
@ -230,7 +221,7 @@ class VideoView extends React.PureComponent
ref='video'
className={classnames(classes.video, {
hidden : !videoVisible,
'isMe' : isMe && !isScreen,
'isMe' : isMe && !isScreen,
loading : videoProfile === 'none',
contain : videoContain
})}
@ -256,7 +247,8 @@ class VideoView extends React.PureComponent
clearInterval(this._videoResolutionTimer);
}
componentWillReceiveProps(nextProps)
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps)
{
const { videoTrack } = nextProps;

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

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

@ -9,7 +9,7 @@ import RoomClient from './RoomClient';
import RoomContext from './RoomContext';
import deviceInfo from './deviceInfo';
import * as stateActions from './actions/stateActions';
import Room from './components/Room';
import App from './components/App';
import LoadingView from './components/LoadingView';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import { PersistGate } from 'redux-persist/lib/integration/react';
@ -19,7 +19,7 @@ import * as serviceWorker from './serviceWorker';
import './index.css';
if (process.env.NODE_ENV !== 'production')
if (process.env.REACT_APP_DEBUG === '*' || process.env.NODE_ENV !== 'production')
{
debug.enable('* -engine* -socket* -RIE* *WARN* *ERROR*');
}
@ -62,8 +62,8 @@ function run()
window.history.pushState('', '', urlParser.toString());
}
const accessCode = parameters.get('code');
const produce = parameters.get('produce') !== 'false';
const consume = parameters.get('consume') !== 'false';
const useSimulcast = parameters.get('simulcast') === 'true';
const forceTcp = parameters.get('forceTcp') === 'true';
@ -84,7 +84,7 @@ function run()
);
roomClient = new RoomClient(
{ roomId, peerId, device, useSimulcast, produce, consume, forceTcp });
{ roomId, peerId, accessCode, device, useSimulcast, produce, forceTcp });
global.CLIENT = roomClient;
@ -94,7 +94,7 @@ function run()
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<Room />
<App />
</SnackbarProvider>
</RoomContext.Provider>
</PersistGate>

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

@ -2,6 +2,7 @@ const initialState =
{
id : null,
device : null,
picture : null,
canSendMic : false,
canSendWebcam : false,
canShareScreen : false,
@ -11,6 +12,7 @@ const initialState =
webcamInProgress : false,
audioInProgress : false,
screenShareInProgress : false,
displayNameInProgress : false,
loginEnabled : false,
raiseHand : false,
raiseHandInProgress : false,
@ -38,11 +40,18 @@ const me = (state = initialState, action) =>
}
case 'LOGGED_IN':
return { ...state, loggedIn: true };
{
const { flag } = action.payload;
return { ...state, loggedIn: flag };
}
case 'USER_LOGOUT':
return { ...state, loggedIn: false };
case 'SET_PICTURE':
return { ...state, picture: action.payload.picture };
case 'SET_MEDIA_CAPABILITIES':
{
const {
@ -110,6 +119,13 @@ const me = (state = initialState, action) =>
return { ...state, raiseHandInProgress: flag };
}
case 'SET_DISPLAY_NAME_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, displayNameInProgress: flag };
}
default:
return state;
}

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':

View File

@ -3,6 +3,7 @@ import room from './room';
import me from './me';
import producers from './producers';
import peers from './peers';
import lobbyPeers from './lobbyPeers';
import consumers from './consumers';
import peerVolumes from './peerVolumes';
import notifications from './notifications';
@ -16,6 +17,7 @@ export default combineReducers({
me,
producers,
peers,
lobbyPeers,
consumers,
peerVolumes,
notifications,

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

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

@ -23,6 +23,7 @@ module.exports =
},*/
// session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e',
cookieName : 'multiparty-meeting.sid',
tls :
{
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
@ -33,6 +34,16 @@ module.exports =
// Any http request is redirected to https.
// Listening port for http server.
listeningRedirectPort : 80,
// If this is set to true, only signed-in users will be able
// to join a room directly. Non-signed-in users (guests) will
// always be put in the lobby regardless of room lock status.
// If false, there is no difference between guests and signed-in
// users when joining.
requireSignInToAccess : true,
// This flag has no effect when requireSignInToAccess is false
// When truthy, the room will be open to all users when the first
// authenticated user has already joined the room.
activateOnHostJoin : true,
// Mediasoup settings
mediasoup :
{

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

203
server/lib/Lobby.js 100644
View File

@ -0,0 +1,203 @@
const EventEmitter = require('events').EventEmitter;
const Logger = require('./Logger');
const logger = new Logger('Lobby');
class Lobby extends EventEmitter
{
constructor()
{
logger.info('constructor()');
super();
// Closed flag.
this._closed = false;
this._peers = new Map();
}
close()
{
logger.info('close()');
this._closed = true;
this._peers.forEach((peer) =>
{
if (!peer.closed)
peer.close();
});
this._peers.clear();
}
checkEmpty()
{
logger.info('checkEmpty()');
return this._peers.size === 0;
}
peerList()
{
logger.info('peerList()');
return Array.from(this._peers.values()).map((peer) =>
({
peerId : peer.id,
displayName : peer.displayName
}));
}
hasPeer(peerId)
{
return this._peers.has(peerId);
}
promoteAllPeers()
{
logger.info('promoteAllPeers()');
this._peers.forEach((peer) =>
{
if (peer.socket)
this.promotePeer(peer.id);
});
}
promotePeer(peerId)
{
logger.info('promotePeer() [peer:"%s"]', peerId);
const peer = this._peers.get(peerId);
if (peer)
{
peer.socket.removeListener('request', peer.socketRequestHandler);
peer.removeListener('authenticationChanged', peer.authenticationHandler);
peer.removeListener('close', peer.closeHandler);
peer.socketRequestHandler = null;
peer.authenticationHandler = null;
peer.closeHandler = null;
this.emit('promotePeer', peer);
this._peers.delete(peerId);
}
}
parkPeer(peer)
{
logger.info('parkPeer() [peer:"%s"]', peer.id);
if (this._closed)
return;
peer.socketRequestHandler = (request, cb) =>
{
logger.debug(
'Peer "request" event [method:"%s", peer:"%s"]',
request.method, peer.id);
if (this._closed)
return;
this._handleSocketRequest(peer, request, cb)
.catch((error) =>
{
logger.error('request failed [error:"%o"]', error);
cb(error);
});
};
peer.authenticationHandler = () =>
{
logger.info('parkPeer() | authenticationChange [peer:"%s"]', peer.id);
peer.authenticated && this.emit('peerAuthenticated', peer);
};
peer.closeHandler = () =>
{
logger.debug('Peer "close" event [peer:"%s"]', peer.id);
if (this._closed)
return;
this.emit('peerClosed', peer);
this._peers.delete(peer.id);
if (this.checkEmpty())
this.emit('lobbyEmpty');
};
this._notification(peer.socket, 'enteredLobby');
this._peers.set(peer.id, peer);
peer.on('authenticationChanged', peer.authenticationHandler);
peer.socket.on('request', peer.socketRequestHandler);
peer.on('close', peer.closeHandler);
}
async _handleSocketRequest(peer, request, cb)
{
logger.debug(
'_handleSocketRequest [peer:"%s"], [request:"%s"]',
peer.id,
request.method
);
if (this._closed)
return;
switch (request.method)
{
case 'changeDisplayName':
{
const { displayName } = request.data;
peer.displayName = displayName;
this.emit('changeDisplayName', peer);
cb();
break;
}
case 'changePicture':
{
const { picture } = request.data;
peer.picture = picture;
this.emit('changePicture', peer);
cb();
break;
}
}
}
_notification(socket, method, data = {}, broadcast = false)
{
if (broadcast)
{
socket.broadcast.to(this._roomId).emit(
'notification', { method, data }
);
}
else
{
socket.emit('notification', { method, data });
}
}
}
module.exports = Lobby;

View File

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

342
server/lib/Peer.js 100644
View File

@ -0,0 +1,342 @@
const EventEmitter = require('events').EventEmitter;
const Logger = require('./Logger');
const logger = new Logger('Peer');
class Peer extends EventEmitter
{
constructor({ id, socket })
{
logger.info('constructor() [id:"%s", socket:"%s"]', id, socket.id);
super();
this._id = id;
this._authId = null;
this._socket = socket;
this._closed = false;
this._joined = false;
this._inLobby = false;
this._authenticated = false;
this._displayName = false;
this._picture = null;
this._email = null;
this._device = null;
this._rtpCapabilities = null;
this._raisedHand = false;
this._transports = new Map();
this._producers = new Map();
this._consumers = new Map();
this._checkAuthentication();
this._handlePeer();
}
close()
{
logger.info('close()');
this._closed = true;
// Iterate and close all mediasoup Transport associated to this Peer, so all
// its Producers and Consumers will also be closed.
this.transports.forEach((transport) =>
{
transport.close();
});
if (this._socket)
this._socket.disconnect(true);
this.emit('close');
}
_handlePeer()
{
this.socket.use((packet, next) =>
{
this._checkAuthentication();
return next();
});
this.socket.on('disconnect', () =>
{
if (this.closed)
return;
logger.debug('"disconnect" event [id:%s]', this.id);
this.close();
});
}
_checkAuthentication()
{
if (
Boolean(this.socket.handshake.session.passport) &&
Boolean(this.socket.handshake.session.passport.user)
)
{
const {
id,
displayName,
picture,
email
} = this.socket.handshake.session.passport.user;
id && (this.authId = id);
displayName && (this.displayName = displayName);
picture && (this.picture = picture);
email && (this.email = email);
this.authenticated = true;
}
else
{
this.authenticated = false;
}
}
get id()
{
return this._id;
}
set id(id)
{
this._id = id;
}
get authId()
{
return this._authId;
}
set authId(authId)
{
this._authId = authId;
}
get socket()
{
return this._socket;
}
set socket(socket)
{
this._socket = socket;
}
get closed()
{
return this._closed;
}
get joined()
{
return this._joined;
}
set joined(joined)
{
this._joined = joined;
}
get inLobby()
{
return this._inLobby;
}
set inLobby(inLobby)
{
this._inLobby = inLobby;
}
get authenticated()
{
return this._authenticated;
}
set authenticated(authenticated)
{
if (authenticated !== this._authenticated)
{
const oldAuthenticated = this._authenticated;
this._authenticated = authenticated;
this.emit('authenticationChanged', { oldAuthenticated });
}
}
get displayName()
{
return this._displayName;
}
set displayName(displayName)
{
if (displayName !== this._displayName)
{
const oldDisplayName = this._displayName;
this._displayName = displayName;
this.emit('displayNameChanged', { oldDisplayName });
}
}
get picture()
{
return this._picture;
}
set picture(picture)
{
if (picture !== this._picture)
{
const oldPicture = this._picture;
this._picture = picture;
this.emit('pictureChanged', { oldPicture });
}
}
get email()
{
return this._email;
}
set email(email)
{
this._email = email;
}
get device()
{
return this._device;
}
set device(device)
{
this._device = device;
}
get rtpCapabilities()
{
return this._rtpCapabilities;
}
set rtpCapabilities(rtpCapabilities)
{
this._rtpCapabilities = rtpCapabilities;
}
get raisedHand()
{
return this._raisedHand;
}
set raisedHand(raisedHand)
{
this._raisedHand = raisedHand;
}
get transports()
{
return this._transports;
}
get producers()
{
return this._producers;
}
get consumers()
{
return this._consumers;
}
addTransport(id, transport)
{
this.transports.set(id, transport);
}
getTransport(id)
{
return this.transports.get(id);
}
getConsumerTransport()
{
return Array.from(this.transports.values())
.find((t) => t.appData.consuming);
}
removeTransport(id)
{
this.transports.delete(id);
}
addProducer(id, producer)
{
this.producers.set(id, producer);
}
getProducer(id)
{
return this.producers.get(id);
}
removeProducer(id)
{
this.producers.delete(id);
}
addConsumer(id, consumer)
{
this.consumers.set(id, consumer);
}
getConsumer(id)
{
return this.consumers.get(id);
}
removeConsumer(id)
{
this.consumers.delete(id);
}
get peerInfo()
{
const peerInfo =
{
id : this.id,
displayName : this.displayName,
picture : this.picture,
device : this.device
};
return peerInfo;
}
}
module.exports = Peer;

File diff suppressed because it is too large Load Diff

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,28 +1,29 @@
{
"name": "multiparty-meeting-server",
"version": "3.0.0",
"private": true,
"description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT",
"main": "lib/index.js",
"dependencies": {
"awaitqueue": "^1.0.0",
"base-64": "^0.1.0",
"colors": "^1.1.2",
"compression": "^1.7.3",
"debug": "^4.1.0",
"express": "^4.16.3",
"express-session": "^1.16.1",
"mediasoup": "^3.0.12",
"openid-client": "^2.5.0",
"passport": "^0.4.0",
"socket.io": "^2.1.1",
"spdy": "^4.0.0"
},
"devDependencies": {
"gulp": "^4.0.0",
"gulp-eslint": "^5.0.0",
"gulp-plumber": "^1.2.0"
}
"name": "multiparty-meeting-server",
"version": "3.0.0",
"private": true,
"description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT",
"main": "lib/index.js",
"dependencies": {
"awaitqueue": "^1.0.0",
"base-64": "^0.1.0",
"body-parser": "^1.19.0",
"colors": "^1.4.0",
"compression": "^1.7.4",
"connect-redis": "^4.0.3",
"cookie-parser": "^1.4.4",
"debug": "^4.1.1",
"express": "^4.17.1",
"express-session": "^1.17.0",
"express-socket.io-session": "^1.3.5",
"helmet": "^3.21.2",
"mediasoup": "^3.0.12",
"openid-client": "^3.7.3",
"passport": "^0.4.0",
"redis": "^2.8.0",
"socket.io": "^2.3.0",
"spdy": "^4.0.1"
}
}

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)
cert : fs.readFileSync(config.tls.cert),
key : fs.readFileSync(config.tls.key),
secureOptions : 'tlsv12',
ciphers :
[
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-RSA-AES256-GCM-SHA384'
].join(':'),
honorCipherOrder : true
};
const app = express();
let httpsServer;
let oidcClient;
let oidcStrategy;
app.use(helmet.hsts());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const session = expressSession({
secret : config.cookieSecret,
name : config.cookieName,
resave : true,
saveUninitialized : true,
store : new RedisStore({ client }),
cookie : {
secure : true,
httpOnly : true,
maxAge : 60 * 60 * 1000 // Expire after 1 hour since last request from user
}
});
app.use(session);
passport.serializeUser((user, done) =>
{
@ -64,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,41 +282,32 @@ async function setupAuth(oidcIssuer)
{
const state = JSON.parse(base64.decode(req.query.state));
if (rooms.has(state.roomId))
let displayName;
let picture;
if (req.user != null)
{
let displayName;
let photo;
if (req.user.displayName != null)
displayName = req.user.displayName;
else
displayName = '';
if (req.user != null)
{
if (req.user.displayName != null)
displayName = req.user.displayName;
else
displayName = '';
if (
req.user.Photos != null &&
req.user.Photos[0] != null &&
req.user.Photos[0].value != null
)
photo = req.user.Photos[0].value;
else
photo = '/static/media/buddy.403cb9f6.svg';
}
const data =
{
peerId : state.peerId,
displayName : displayName,
picture : photo
};
const room = rooms.get(state.roomId);
room.authCallback(data);
if (req.user.picture != null)
picture = req.user.picture;
else
picture = '/static/media/buddy.403cb9f6.svg';
}
res.send('');
const peer = peers.get(state.id);
peer && (peer.authenticated = true);
peer && (peer.displayName = displayName);
peer && (peer.picture = picture);
res.send(loginHelper({
displayName,
picture
}));
}
);
}
@ -316,11 +354,17 @@ async function runHttpsServer()
}
/**
* Create a protoo WebSocketServer to allow WebSocket connections from browsers.
* Create a WebSocketServer to allow WebSocket connections from browsers.
*/
async function runWebSocketServer()
{
const io = require('socket.io')(httpsServer);
io = require('socket.io')(httpsServer);
io.use(
sharedSession(session, {
autoSave : true
})
);
// Handle connections from clients.
io.on('connection', (socket) =>
@ -342,17 +386,22 @@ async function runWebSocketServer()
queue.push(async () =>
{
const room = await getOrCreateRoom({ roomId });
const peer = new Peer({ id: peerId, socket });
room.handleConnection({ peerId, socket });
peers.set(peerId, peer);
peer.on('close', () => peers.delete(peerId));
room.handlePeer(peer);
})
.catch((error) =>
{
logger.error('room creation or room joining failed:%o', error);
.catch((error) =>
{
logger.error('room creation or room joining failed [error:"%o"]', error);
socket.disconnect(true);
socket.disconnect(true);
return;
});
return;
});
});
}
@ -410,7 +459,7 @@ async function getOrCreateRoom({ roomId })
// If the Room does not exist create a new one.
if (!room)
{
logger.info('creating a new Room [roomId:%s]', roomId);
logger.info('creating a new Room [roomId:"%s"]', roomId);
const mediasoupWorker = getMediasoupWorker();

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