Merge branch 'develop'

master
Håvar Aambø Fosstveit 2019-07-01 19:47:54 +02:00
commit a4a7fbe4f7
49 changed files with 4063 additions and 2939 deletions

3
.gitignore vendored
View File

@ -1,10 +1,11 @@
node_modules/
/app/build/
/app/public/config.js
/app/public/config/config.js
/app/public/images/logo.*
/server/config/
!/server/config/config.example.js
/server/public/
/server/certs/
!/server/certs/mediasoup-demo.localhost.*
.vscode

View File

@ -1,5 +1,23 @@
# Changelog
### 3.0
* Updated to mediasoup v3
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)
- OpenID Connect discovery
- Auth code flow
* Add spdy http2 support.
- Notice it does not supports node 11.x
* Updated to Material UI v4
### 2.0
* Material UI
* Separate settings for lastN for desktop and mobile
### 1.2
* Add Lock Room feature
* Fix suspended Web Audio context / fixed delayed getUsermedia
* Added support for the new getdisplaymedia API in Chrome 72
### 1.1
* Moved Filesharing code out from React code to RoomClient
* Major cleanup of CSS. Variables for most colors and sizes exposed in :root

View File

@ -9,7 +9,7 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci
* Chat
* Screen sharing
* File sharing
* Different video layouts
* Different layouts
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no.
@ -25,16 +25,16 @@ $ git clone https://github.com/havfo/multiparty-meeting.git
$ cd multiparty-meeting
```
* Copy `server/config.example.js` to `server/config.js` :
* Copy `server/config/config.example.js` to `server/config/config.js` :
```bash
$ cp server/config.example.js server/config.js
$ cp server/config/config.example.js server/config/config.js
```
* Copy `app/public/config.example.js` to `app/public/config.js` :
* Copy `app/public/config/config.example.js` to `app/public/config/config.js` :
```bash
$ cp app/public/config.example.js app/public/config.js
$ cp app/public/config/config.example.js app/public/config/config.js
```
* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).

View File

@ -1,47 +1,43 @@
{
"name": "multiparty-meeting",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT",
"dependencies": {
"@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.2",
"@material-ui/core": "^4.1.2",
"@material-ui/icons": "^4.2.1",
"bowser": "^2.4.0",
"create-torrent": "^3.33.0",
"domready": "^1.0.8",
"file-saver": "^2.0.1",
"hark": "^1.2.3",
"js-cookie": "^2.2.0",
"marked": "^0.6.1",
"mediasoup-client": "^2.4.10",
"mediasoup-client": "^3.0.6",
"notistack": "^0.5.1",
"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-draggable": "^3.2.1",
"react-redux": "^6.0.1",
"react-scripts": "2.1.8",
"react-tooltip": "^3.10.0",
"redux": "^4.0.1",
"redux-logger": "^3.0.6",
"redux-persist": "^5.10.0",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",
"riek": "^1.1.0",
"socket.io-client": "^2.2.0",
"source-map-explorer": "^1.8.0",
"url-parse": "^1.4.4",
"webtorrent": "^0.103.1"
},
"scripts": {
"analyze-main": "source-map-explorer build/static/js/main.*",
"analyze-chunk": "source-map-explorer build/static/js/2.*",
"start": "HTTPS=true PORT=4443 react-scripts start",
"build": "react-scripts build && rm -rf ../server/public/* && cp -r build/* ../server/public/",
"build": "react-scripts build && mkdir -p ../server/public && rm -rf ../server/public/* && cp -r build/* ../server/public/",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
@ -169,7 +165,12 @@
"no-case-declarations": 2,
"no-catch-shadow": 2,
"no-class-assign": 2,
"no-confusing-arrow": ["error", {"allowParens": true}],
"no-confusing-arrow": [
"error",
{
"allowParens": true
}
],
"no-console": 2,
"no-const-assign": 2,
"no-debugger": 2,

View File

@ -9,14 +9,13 @@
<meta name='description' content='multiparty meeting - Simple web meetings'>
<meta name='theme-color' content='#000000' />
<link rel='chrome-webstore-item' href='https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'>
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<link rel='preconnect' href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<link rel='shortcut icon' href='%PUBLIC_URL%/favicon.ico' />
<link rel='manifest' href='%PUBLIC_URL%/manifest.json' />
<title>Multiparty Meeting</title>
<script src='%PUBLIC_URL%/config.js' type='text/javascript'></script>
<script src='%PUBLIC_URL%/config/config.js' type='text/javascript'></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -0,0 +1,3 @@
# Allow crawling of all content
User-agent: *
Disallow:

File diff suppressed because it is too large Load Diff

View File

@ -1,113 +1,4 @@
class ChromeScreenShare
{
constructor()
{
this._stream = null;
}
start(options = { })
{
const state = this;
return new Promise((resolve, reject) =>
{
window.addEventListener('message', _onExtensionMessage, false);
window.postMessage({ type: 'getStreamId' }, '*');
function _onExtensionMessage({ data })
{
if (data.type !== 'gotStreamId')
{
return;
}
const constraints = state._toConstraints(options, data.streamId);
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) =>
{
window.removeEventListener('message', _onExtensionMessage);
state._stream = stream;
resolve(stream);
})
.catch((err) =>
{
window.removeEventListener('message', _onExtensionMessage);
reject(err);
});
}
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
{
return true;
}
return false;
}
needExtension()
{
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
{
return false;
}
return true;
}
_toConstraints(options, streamId)
{
const constraints = {
video : {
mandatory : {
chromeMediaSource : 'desktop',
chromeMediaSourceId : streamId
},
optional : [ {
googTemporalLayeredScreencast : true
} ]
},
audio : false
};
if (isFinite(options.width))
{
constraints.video.mandatory.maxWidth = options.width;
constraints.video.mandatory.minWidth = options.width;
}
if (isFinite(options.height))
{
constraints.video.mandatory.maxHeight = options.height;
constraints.video.mandatory.minHeight = options.height;
}
if (isFinite(options.frameRate))
{
constraints.video.mandatory.maxFrameRate = options.frameRate;
constraints.video.mandatory.minFrameRate = options.frameRate;
}
return constraints;
}
}
class Chrome72ScreenShare
class DisplayMediaScreenShare
{
constructor()
{
@ -143,11 +34,6 @@ class Chrome72ScreenShare
return true;
}
needExtension()
{
return false;
}
_toConstraints()
{
const constraints = {
@ -194,11 +80,6 @@ class FirefoxScreenShare
return true;
}
needExtension()
{
return false;
}
_toConstraints(options)
{
const constraints = {
@ -238,119 +119,12 @@ class FirefoxScreenShare
}
}
class Firefox66ScreenShare
{
constructor()
{
this._stream = null;
}
start(options = {})
{
const constraints = this._toConstraints(options);
return navigator.mediaDevices.getDisplayMedia(constraints)
.then((stream) =>
{
this._stream = stream;
return Promise.resolve(stream);
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
return true;
}
needExtension()
{
return false;
}
_toConstraints()
{
const constraints = {
video : true
};
return constraints;
}
}
class EdgeScreenShare
{
constructor()
{
this._stream = null;
}
start(options = {})
{
const constraints = this._toConstraints(options);
return navigator.getDisplayMedia(constraints)
.then((stream) =>
{
this._stream = stream;
return Promise.resolve(stream);
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
return true;
}
needExtension()
{
return false;
}
_toConstraints()
{
const constraints = {
video : true
};
return constraints;
}
}
class DefaultScreenShare
{
isScreenShareAvailable()
{
return false;
}
needExtension()
{
return false;
}
}
export default class ScreenShare
@ -364,18 +138,15 @@ export default class ScreenShare
if (device.version < 66.0)
return new FirefoxScreenShare();
else
return new Firefox66ScreenShare();
return new DisplayMediaScreenShare();
}
case 'chrome':
{
if (device.version < 72.0)
return new ChromeScreenShare();
else
return new Chrome72ScreenShare();
return new DisplayMediaScreenShare();
}
case 'msedge':
{
return new EdgeScreenShare();
return new DisplayMediaScreenShare();
}
default:
{

View File

@ -5,11 +5,11 @@ const logger = new Logger('Spotlight');
export default class Spotlights extends EventEmitter
{
constructor(maxSpotlights, room)
constructor(maxSpotlights, signalingSocket)
{
super();
this._room = room;
this._signalingSocket = signalingSocket;
this._maxSpotlights = maxSpotlights;
this._peerList = [];
this._selectedSpotlights = [];
@ -19,24 +19,25 @@ export default class Spotlights extends EventEmitter
start()
{
const peers = this._room.peers;
for (const peer of peers)
{
this._handlePeer(peer);
}
this._handleRoom();
this._handleSignaling();
this._started = true;
this._spotlightsUpdated();
}
peerInSpotlights(peerName)
addPeers(peers)
{
for (const peer of peers)
{
this._newPeer(peer.id);
}
}
peerInSpotlights(peerId)
{
if (this._started)
{
return this._currentSpotlights.indexOf(peerName) !== -1;
return this._currentSpotlights.indexOf(peerId) !== -1;
}
else
{
@ -44,11 +45,11 @@ export default class Spotlights extends EventEmitter
}
}
setPeerSpotlight(peerName)
setPeerSpotlight(peerId)
{
logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName);
logger.debug('setPeerSpotlight() [peerId:"%s"]', peerId);
const index = this._selectedSpotlights.indexOf(peerName);
const index = this._selectedSpotlights.indexOf(peerId);
if (index !== -1)
{
@ -56,13 +57,13 @@ export default class Spotlights extends EventEmitter
}
else
{
this._selectedSpotlights = [ peerName ];
this._selectedSpotlights = [ peerId ];
}
/*
if (index === -1) // We don't have this peer in the list, adding
{
this._selectedSpotlights.push(peerName);
this._selectedSpotlights.push(peerId);
}
else // We have this peer, remove
{
@ -74,14 +75,63 @@ export default class Spotlights extends EventEmitter
this._spotlightsUpdated();
}
_handleRoom()
_handleSignaling()
{
this._room.on('newpeer', (peer) =>
this._signalingSocket.on('notification', (notification) =>
{
if (notification.method === 'newPeer')
{
const { id } = notification.data;
this._newPeer(id);
}
if (notification.method === 'peerClosed')
{
const { peerId } = notification.data;
this._closePeer(peerId);
}
});
}
_newPeer(id)
{
logger.debug(
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
this._handlePeer(peer);
});
'room "newpeer" event [id: "%s"]', id);
if (this._peerList.indexOf(id) === -1) // We don't have this peer in the list
{
logger.debug('_handlePeer() | adding peer [peerId: "%s"]', id);
this._peerList.push(id);
if (this._started)
this._spotlightsUpdated();
}
}
_closePeer(id)
{
logger.debug(
'room "peerClosed" event [peerId:%o]', id);
let index = this._peerList.indexOf(id);
if (index !== -1) // We have this peer in the list, remove
{
this._peerList.splice(index, 1);
}
index = this._selectedSpotlights.indexOf(id);
if (index !== -1) // We have this peer in the list, remove
{
this._selectedSpotlights.splice(index, 1);
}
if (this._started)
this._spotlightsUpdated();
}
addSpeakerList(speakerList)
@ -92,49 +142,16 @@ export default class Spotlights extends EventEmitter
this._spotlightsUpdated();
}
_handlePeer(peer)
handleActiveSpeaker(peerId)
{
logger.debug('_handlePeer() [peerName:"%s"]', peer.name);
logger.debug('handleActiveSpeaker() [peerId:"%s"]', peerId);
if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list
{
peer.on('close', () =>
{
let index = this._peerList.indexOf(peer.name);
if (index !== -1) // We have this peer in the list, remove
{
this._peerList.splice(index, 1);
}
index = this._selectedSpotlights.indexOf(peer.name);
if (index !== -1) // We have this peer in the list, remove
{
this._selectedSpotlights.splice(index, 1);
}
this._spotlightsUpdated();
});
logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name);
this._peerList.push(peer.name);
this._spotlightsUpdated();
}
}
handleActiveSpeaker(peerName)
{
logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName);
const index = this._peerList.indexOf(peerName);
const index = this._peerList.indexOf(peerId);
if (index > -1)
{
this._peerList.splice(index, 1);
this._peerList = [ peerName ].concat(this._peerList);
this._peerList = [ peerId ].concat(this._peerList);
this._spotlightsUpdated();
}

View File

@ -14,11 +14,11 @@ export const setRoomState = (state) =>
};
};
export const setRoomActiveSpeaker = (peerName) =>
export const setRoomActiveSpeaker = (peerId) =>
{
return {
type : 'SET_ROOM_ACTIVE_SPEAKER',
payload : { peerName }
payload : { peerId }
};
};
@ -43,41 +43,30 @@ export const setRoomLockedOut = () =>
};
};
export const setAudioSuspended = ({ audioSuspended }) =>
{
return {
type : 'SET_AUDIO_SUSPENDED',
payload : { audioSuspended }
};
};
export const setSettingsOpen = ({ settingsOpen }) =>
({
type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen }
});
export const setMe = ({ peerName, device, loginEnabled }) =>
export const setMe = ({ peerId, device, loginEnabled }) =>
{
return {
type : 'SET_ME',
payload : { peerName, device, loginEnabled }
payload : { peerId, device, loginEnabled }
};
};
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) =>
export const setMediaCapabilities = ({
canSendMic,
canSendWebcam,
canShareScreen,
canShareFiles
}) =>
{
return {
type : 'SET_MEDIA_CAPABILITIES',
payload : { canSendMic, canSendWebcam }
};
};
export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
{
return {
type : 'SET_SCREEN_CAPABILITIES',
payload : { canShareScreen, needExtension }
payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles }
};
};
@ -150,27 +139,27 @@ export const setDisplayMode = (mode) =>
payload : { mode }
});
export const setPeerVideoInProgress = (peerName, flag) =>
export const setPeerVideoInProgress = (peerId, flag) =>
{
return {
type : 'SET_PEER_VIDEO_IN_PROGRESS',
payload : { peerName, flag }
payload : { peerId, flag }
};
};
export const setPeerAudioInProgress = (peerName, flag) =>
export const setPeerAudioInProgress = (peerId, flag) =>
{
return {
type : 'SET_PEER_AUDIO_IN_PROGRESS',
payload : { peerName, flag }
payload : { peerId, flag }
};
};
export const setPeerScreenInProgress = (peerName, flag) =>
export const setPeerScreenInProgress = (peerId, flag) =>
{
return {
type : 'SET_PEER_SCREEN_IN_PROGRESS',
payload : { peerName, flag }
payload : { peerId, flag }
};
};
@ -226,11 +215,11 @@ export const setMyRaiseHandStateInProgress = (flag) =>
};
};
export const setPeerRaiseHandState = (peerName, raiseHandState) =>
export const setPeerRaiseHandState = (peerId, raiseHandState) =>
{
return {
type : 'SET_PEER_RAISE_HAND_STATE',
payload : { peerName, raiseHandState }
payload : { peerId, raiseHandState }
};
};
@ -274,6 +263,14 @@ export const setProducerTrack = (producerId, track) =>
};
};
export const setProducerScore = (producerId, score) =>
{
return {
type : 'SET_PRODUCER_SCORE',
payload : { producerId, score }
};
};
export const setAudioInProgress = (flag) =>
{
return {
@ -306,35 +303,35 @@ export const addPeer = (peer) =>
};
};
export const removePeer = (peerName) =>
export const removePeer = (peerId) =>
{
return {
type : 'REMOVE_PEER',
payload : { peerName }
payload : { peerId }
};
};
export const setPeerDisplayName = (displayName, peerName) =>
export const setPeerDisplayName = (displayName, peerId) =>
{
return {
type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerName }
payload : { displayName, peerId }
};
};
export const addConsumer = (consumer, peerName) =>
export const addConsumer = (consumer, peerId) =>
{
return {
type : 'ADD_CONSUMER',
payload : { consumer, peerName }
payload : { consumer, peerId }
};
};
export const removeConsumer = (consumerId, peerName) =>
export const removeConsumer = (consumerId, peerId) =>
{
return {
type : 'REMOVE_CONSUMER',
payload : { consumerId, peerName }
payload : { consumerId, peerId }
};
};
@ -354,11 +351,19 @@ export const setConsumerResumed = (consumerId, originator) =>
};
};
export const setConsumerEffectiveProfile = (consumerId, profile) =>
export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_EFFECTIVE_PROFILE',
payload : { consumerId, profile }
type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_PREFERRED_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
@ -370,11 +375,19 @@ export const setConsumerTrack = (consumerId, track) =>
};
};
export const setPeerVolume = (peerName, volume) =>
export const setConsumerScore = (consumerId, score) =>
{
return {
type : 'SET_CONSUMER_SCORE',
payload : { consumerId, score }
};
};
export const setPeerVolume = (peerId, volume) =>
{
return {
type : 'SET_PEER_VOLUME',
payload : { peerName, volume }
payload : { peerId, volume }
};
};
@ -482,11 +495,11 @@ export const dropMessages = () =>
};
};
export const addFile = (file) =>
export const addFile = (peerId, magnetUri) =>
{
return {
type : 'ADD_FILE',
payload : { file }
payload : { peerId, magnetUri }
};
};
@ -536,10 +549,10 @@ export const setPicture = (picture) =>
payload : { picture }
});
export const setPeerPicture = (peerName, picture) =>
export const setPeerPicture = (peerId, picture) =>
({
type : 'SET_PEER_PICTURE',
payload : { peerName, picture }
payload : { peerId, picture }
});
export const loggedIn = () =>
@ -547,10 +560,15 @@ export const loggedIn = () =>
type : 'LOGGED_IN'
});
export const setSelectedPeer = (selectedPeerName) =>
export const toggleJoined = () =>
({
type : 'TOGGLE_JOINED'
});
export const setSelectedPeer = (selectedPeerId) =>
({
type : 'SET_SELECTED_PEER',
payload : { selectedPeerName }
payload : { selectedPeerId }
});
export const setSpotlights = (spotlights) =>

View File

@ -1,20 +1,27 @@
import React from 'react';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume';
import Fab from '@material-ui/core/Fab';
import Tooltip from '@material-ui/core/Tooltip';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VideoIcon from '@material-ui/icons/Videocam';
import VideoOffIcon from '@material-ui/icons/VideocamOff';
import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
const styles = () =>
const styles = (theme) =>
({
root :
{
flexDirection : 'row',
margin : 6,
flex : '0 0 auto',
boxShadow : 'var(--peer-shadow)',
border : 'var(--peer-border)',
@ -23,38 +30,88 @@ const styles = () =>
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat',
'&.webcam' :
{
order : 1
},
'&.screen' :
{
order : 2
},
'&.hover' :
{
boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)'
},
'&.active-speaker' :
{
borderColor : 'var(--active-speaker-border-color)'
}
},
fab :
{
margin : theme.spacing(1),
pointerEvents : 'auto'
},
viewContainer :
{
position : 'relative',
'&.webcam' :
{
order : 2
width : '100%',
height : '100%'
},
'&.screen' :
controls :
{
order : 1
position : 'absolute',
width : '100%',
height : '100%',
backgroundColor : 'rgba(0, 0, 0, 0.3)',
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'flex-end',
padding : theme.spacing(1),
zIndex : 21,
opacity : 0,
transition : 'opacity 0.3s',
touchAction : 'none',
pointerEvents : 'none',
'&.hover' :
{
opacity : 1
},
'& p' :
{
position : 'absolute',
float : 'left',
top : '50%',
left : '50%',
transform : 'translate(-50%, -50%)',
color : 'rgba(255, 255, 255, 0.5)',
fontSize : '7em',
margin : 0
}
}
});
const Me = (props) =>
{
const [ hover, setHover ] = useState(false);
let touchTimeout = null;
const {
roomClient,
me,
settings,
activeSpeaker,
spacing,
style,
smallButtons,
advancedMode,
micProducer,
webcamProducer,
screenProducer,
classes
classes,
theme
} = props;
const videoVisible = (
@ -69,17 +126,225 @@ const Me = (props) =>
!screenProducer.remotelyPaused
);
let micState;
let micTip;
if (!me.canSendMic)
{
micState = 'unsupported';
micTip = 'Audio unsupported';
}
else if (!micProducer)
{
micState = 'off';
micTip = 'Activate audio';
}
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
{
micState = 'on';
micTip = 'Mute audio';
}
else
{
micState = 'muted';
micTip = 'Unmute audio';
}
let webcamState;
let webcamTip;
if (!me.canSendWebcam)
{
webcamState = 'unsupported';
webcamTip = 'Video unsupported';
}
else if (webcamProducer)
{
webcamState = 'on';
webcamTip = 'Stop video';
}
else
{
webcamState = 'off';
webcamTip = 'Start video';
}
let screenState;
let screenTip;
if (!me.canShareScreen)
{
screenState = 'unsupported';
screenTip = 'Screen sharing not supported';
}
else if (screenProducer)
{
screenState = 'on';
screenTip = 'Stop screen sharing';
}
else
{
screenState = 'off';
screenTip = 'Start screen sharing';
}
const spacingStyle =
{
'margin' : spacing
};
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<React.Fragment>
<div
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={spacingStyle}
>
<div className={classnames(classes.viewContainer, 'webcam')} style={style}>
<div className={classnames(classes.viewContainer)} style={style}>
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p>ME</p>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'right'}>
<div>
<Fab
aria-label='Mute mic'
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
</div>
</Tooltip>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'right'}>
<div>
<Fab
aria-label='Mute video'
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</Fab>
</div>
</Tooltip>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'right'}>
<div>
<Fab
aria-label='Share screen'
className={classes.fab}
disabled={!me.canShareScreen || me.screenShareInProgress}
color={screenState === 'on' ? 'primary' : 'default'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ screenState === 'on' || screenState === 'unsupported' ?
<ScreenOffIcon/>
:null
}
{ screenState === 'off' ?
<ScreenIcon/>
:null
}
</Fab>
</div>
</Tooltip>
</div>
<VideoView
isMe
advancedMode={advancedMode}
@ -95,15 +360,64 @@ const Me = (props) =>
roomClient.changeDisplayName(displayName);
}}
>
<Volume name={me.name} />
<Volume id={me.id} />
</VideoView>
</div>
</div>
{ screenProducer ?
<div className={classes.root}>
<div className={classnames(classes.viewContainer, 'screen')} style={style}>
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={spacingStyle}
>
<div className={classnames(classes.viewContainer)} style={style}>
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p>ME</p>
</div>
<VideoView
isMe
isScreen
advancedMode={advancedMode}
videoContain
videoTrack={screenProducer ? screenProducer.track : null}
@ -128,8 +442,11 @@ Me.propTypes =
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
spacing : PropTypes.number,
style : PropTypes.object,
classes : PropTypes.object.isRequired
smallButtons : PropTypes.bool,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
@ -138,7 +455,7 @@ const mapStateToProps = (state) =>
me : state.me,
...meProducersSelector(state),
settings : state.settings,
activeSpeaker : state.me.name === state.room.activeSpeakerName
activeSpeaker : state.me.id === state.room.activeSpeakerId
};
};
@ -153,8 +470,8 @@ export default withRoomContext(connect(
prev.me === next.me &&
prev.producers === next.producers &&
prev.settings === next.settings &&
prev.room.activeSpeakerName === next.room.activeSpeakerName
prev.room.activeSpeakerId === next.room.activeSpeakerId
);
}
}
)(withStyles(styles)(Me)));
)(withStyles(styles, { withTheme: true })(Me)));

View File

@ -6,7 +6,7 @@ import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import * as stateActions from '../../actions/stateActions';
import VideoView from '../VideoContainers/VideoView';
import Fab from '@material-ui/core/Fab';
@ -21,7 +21,6 @@ const styles = (theme) =>
root :
{
flex : '0 0 auto',
margin : 6,
boxShadow : 'var(--peer-shadow)',
border : 'var(--peer-border)',
touchAction : 'none',
@ -32,11 +31,11 @@ const styles = (theme) =>
backgroundRepeat : 'no-repeat',
'&.webcam' :
{
order : 2
order : 4
},
'&.screen' :
{
order : 1
order : 3
},
'&.hover' :
{
@ -49,19 +48,13 @@ const styles = (theme) =>
},
fab :
{
margin : theme.spacing.unit
margin : theme.spacing(1)
},
viewContainer :
{
position : 'relative',
'&.webcam' :
{
order : 2
},
'&.screen' :
{
order : 1
}
width : '100%',
height : '100%'
},
controls :
{
@ -73,8 +66,8 @@ const styles = (theme) =>
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'flex-end',
padding : '0.4vmin',
zIndex : 20,
padding : theme.spacing(1),
zIndex : 21,
opacity : 0,
transition : 'opacity 0.3s',
touchAction : 'none',
@ -92,8 +85,8 @@ const styles = (theme) =>
display : 'flex',
justifyContent : 'center',
alignItems : 'center',
padding : '0.4vmin',
zIndex : 21,
padding : theme.spacing(1),
zIndex : 20,
'& p' :
{
padding : '6px 12px',
@ -109,15 +102,9 @@ const styles = (theme) =>
const Peer = (props) =>
{
const [ hover, setHover ] = useState(false);
const [ webcamHover, setWebcamHover ] = useState(false);
const [ screenHover, setScreenHover ] = useState(false);
let touchTimeout = null;
let touchWebcamTimeout = null;
let touchScreenTimeout = null;
const {
roomClient,
advancedMode,
@ -128,7 +115,9 @@ const Peer = (props) =>
screenConsumer,
toggleConsumerFullscreen,
toggleConsumerWindow,
spacing,
style,
smallButtons,
windowConsumer,
classes,
theme
@ -164,6 +153,12 @@ const Peer = (props) =>
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const rootStyle =
{
'margin' : spacing,
...style
};
return (
<React.Fragment>
<div
@ -194,15 +189,9 @@ const Peer = (props) =>
setHover(false);
}, 2000);
}}
style={rootStyle}
>
<div className={classnames(classes.viewContainer)} style={style}>
{ videoVisible && !webcamConsumer.supported ?
<div className={classes.videoInfo}>
<p>incompatible video</p>
</div>
:null
}
<div className={classnames(classes.viewContainer)}>
{ !videoVisible ?
<div className={classes.videoInfo}>
<p>this video is paused</p>
@ -210,38 +199,39 @@ const Peer = (props) =>
:null
}
{ videoVisible && webcamConsumer.supported ?
<div
className={classnames(classes.controls, webcamHover ? 'hover' : null)}
onMouseOver={() => setWebcamHover(true)}
onMouseOut={() => setWebcamHover(false)}
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchWebcamTimeout)
clearTimeout(touchWebcamTimeout);
if (touchTimeout)
clearTimeout(touchTimeout);
setWebcamHover(true);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchWebcamTimeout)
clearTimeout(touchWebcamTimeout);
if (touchTimeout)
clearTimeout(touchTimeout);
touchWebcamTimeout = setTimeout(() =>
touchTimeout = setTimeout(() =>
{
setWebcamHover(false);
setHover(false);
}, 2000);
}}
>
<Fab
aria-label='Mute mic'
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
@ -259,6 +249,7 @@ const Peer = (props) =>
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
@ -273,6 +264,7 @@ const Peer = (props) =>
aria-label='Fullscreen'
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
@ -281,8 +273,6 @@ const Peer = (props) =>
<FullScreenIcon />
</Fab>
</div>
:null
}
<VideoView
advancedMode={advancedMode}
@ -295,7 +285,7 @@ const Peer = (props) =>
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
>
<Volume name={peer.name} />
<Volume id={peer.id} />
</VideoView>
</div>
</div>
@ -322,43 +312,37 @@ const Peer = (props) =>
setHover(false);
}, 2000);
}}
style={rootStyle}
>
{ screenVisible && !screenConsumer.supported ?
<div className={classes.videoInfo} style={style}>
<p>incompatible video</p>
</div>
:null
}
{ !screenVisible ?
<div className={classes.videoInfo} style={style}>
<div className={classes.videoInfo}>
<p>this video is paused</p>
</div>
:null
}
{ screenVisible && screenConsumer.supported ?
<div className={classnames(classes.viewContainer)} style={style}>
{ screenVisible ?
<div className={classnames(classes.viewContainer)}>
<div
className={classnames(classes.controls, screenHover ? 'hover' : null)}
onMouseOver={() => setScreenHover(true)}
onMouseOut={() => setScreenHover(false)}
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchScreenTimeout)
clearTimeout(touchScreenTimeout);
if (touchTimeout)
clearTimeout(touchTimeout);
setScreenHover(true);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchScreenTimeout)
clearTimeout(touchScreenTimeout);
if (touchTimeout)
clearTimeout(touchTimeout);
touchScreenTimeout = setTimeout(() =>
touchTimeout = setTimeout(() =>
{
setScreenHover(false);
setHover(false);
}, 2000);
}}
>
@ -370,6 +354,7 @@ const Peer = (props) =>
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
@ -384,6 +369,7 @@ const Peer = (props) =>
aria-label='Fullscreen'
className={classes.fab}
disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
@ -418,9 +404,11 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number,
windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool,
spacing : PropTypes.number,
style : PropTypes.object,
smallButtons : PropTypes.bool,
toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired,
@ -434,10 +422,10 @@ const makeMapStateToProps = (initialState, props) =>
const mapStateToProps = (state) =>
{
return {
peer : state.peers[props.name],
peer : state.peers[props.id],
...getPeerConsumers(state, props),
windowConsumer : state.room.windowConsumer,
activeSpeaker : props.name === state.room.activeSpeakerName
activeSpeaker : props.id === state.room.activeSpeakerId
};
};
@ -470,7 +458,7 @@ export default withRoomContext(connect(
return (
prev.peers === next.peers &&
prev.consumers === next.consumers &&
prev.room.activeSpeakerName === next.room.activeSpeakerName &&
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.windowConsumer === next.room.windowConsumer
);
}

View File

@ -0,0 +1,210 @@
import React from 'react';
import { connect } from 'react-redux';
import { makePeerConsumerSelector } from '../Selectors';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume';
const styles = (theme) =>
({
root :
{
flex : '0 0 auto',
boxShadow : 'var(--peer-shadow)',
border : 'var(--peer-border)',
touchAction : 'none',
backgroundColor : 'var(--peer-bg-color)',
backgroundImage : 'var(--peer-empty-avatar)',
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat',
'&.webcam' :
{
order : 2
},
'&.screen' :
{
order : 1
}
},
viewContainer :
{
position : 'relative',
'&.webcam' :
{
order : 2
},
'&.screen' :
{
order : 1
}
},
videoInfo :
{
position : 'absolute',
width : '100%',
height : '100%',
backgroundColor : 'rgba(0, 0, 0, 0.3)',
display : 'flex',
justifyContent : 'center',
alignItems : 'center',
padding : theme.spacing(1),
zIndex : 21,
'& p' :
{
padding : '6px 12px',
borderRadius : 6,
userSelect : 'none',
pointerEvents : 'none',
fontSize : 20,
color : 'rgba(255, 255, 255, 0.55)'
}
}
});
const SpeakerPeer = (props) =>
{
const {
advancedMode,
peer,
micConsumer,
webcamConsumer,
screenConsumer,
spacing,
style,
classes
} = props;
const videoVisible = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!webcamConsumer.remotelyPaused
);
const screenVisible = (
Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
!screenConsumer.remotelyPaused
);
let videoProfile;
if (webcamConsumer)
videoProfile = webcamConsumer.profile;
let screenProfile;
if (screenConsumer)
screenProfile = screenConsumer.profile;
const spacingStyle =
{
'margin' : spacing
};
return (
<React.Fragment>
<div
className={
classnames(
classes.root,
'webcam'
)
}
style={spacingStyle}
>
<div className={classnames(classes.viewContainer)} style={style}>
{ !videoVisible ?
<div className={classes.videoInfo}>
<p>this video is paused</p>
</div>
:null
}
<VideoView
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
showPeerInfo
videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible}
videoProfile={videoProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
>
<Volume id={peer.id} />
</VideoView>
</div>
</div>
{ screenConsumer ?
<div
className={classnames(classes.root, 'screen')}
>
{ !screenVisible ?
<div className={classes.videoInfo} style={style}>
<p>this video is paused</p>
</div>
:null
}
{ screenVisible ?
<div className={classnames(classes.viewContainer)} style={style}>
<VideoView
advancedMode={advancedMode}
videoContain
videoTrack={screenConsumer ? screenConsumer.track : null}
videoVisible={screenVisible}
videoProfile={screenProfile}
videoCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
:null
}
</div>
:null
}
</React.Fragment>
);
};
SpeakerPeer.propTypes =
{
advancedMode : PropTypes.bool,
peer : appPropTypes.Peer,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
spacing : PropTypes.number,
style : PropTypes.object,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state, props) =>
{
const getPeerConsumers = makePeerConsumerSelector();
return {
peer : state.peers[props.id],
...getPeerConsumers(state, props)
};
};
export default connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.peers === next.peers &&
prev.consumers === next.consumers &&
prev.room.activeSpeakerId === next.room.activeSpeakerId
);
}
}
)(withStyles(styles, { withTheme: true })(SpeakerPeer));

View File

@ -150,7 +150,7 @@ const makeMapStateToProps = (initialState, props) =>
const mapStateToProps = (state) =>
{
return {
volume : state.peerVolumes[props.name]
volume : state.peerVolumes[props.id]
};
};

View File

@ -1,332 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors';
import { withStyles } from '@material-ui/core/styles';
import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import Fab from '@material-ui/core/Fab';
import Tooltip from '@material-ui/core/Tooltip';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VideoIcon from '@material-ui/icons/Videocam';
import VideoOffIcon from '@material-ui/icons/VideocamOff';
import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExtensionIcon from '@material-ui/icons/Extension';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import LeaveIcon from '@material-ui/icons/Cancel';
const styles = (theme) =>
({
root :
{
position : 'fixed',
zIndex : 500,
display : 'flex',
[theme.breakpoints.up('md')] :
{
top : '50%',
transform : 'translate(0%, -50%)',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
left : '1.0em',
width : '2.6em'
},
[theme.breakpoints.down('sm')] :
{
flexDirection : 'row',
bottom : '0.5em',
left : '50%',
transform : 'translate(-50%, -0%)'
}
},
fab :
{
margin : theme.spacing.unit
},
show :
{
opacity : 1,
transition : 'opacity .5s'
},
hide :
{
opacity : 0,
transition : 'opacity .5s'
}
});
const Sidebar = (props) =>
{
const {
roomClient,
toolbarsVisible,
me,
micProducer,
webcamProducer,
screenProducer,
locked,
classes,
theme
} = props;
let micState;
let micTip;
if (!me.canSendMic || !micProducer)
{
micState = 'unsupported';
micTip = 'Audio unsupported';
}
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
{
micState = 'on';
micTip = 'Mute audio';
}
else
{
micState = 'off';
micTip = 'Unmute audio';
}
let webcamState;
let webcamTip;
if (!me.canSendWebcam)
{
webcamState = 'unsupported';
webcamTip = 'Video unsupported';
}
else if (webcamProducer)
{
webcamState = 'on';
webcamTip = 'Stop video';
}
else
{
webcamState = 'off';
webcamTip = 'Start video';
}
let screenState;
let screenTip;
if (me.needExtension)
{
screenState = 'need-extension';
screenTip = 'Install screen sharing extension';
}
else if (!me.canShareScreen)
{
screenState = 'unsupported';
screenTip = 'Screen sharing not supported';
}
else if (screenProducer)
{
screenState = 'on';
screenTip = 'Stop screen sharing';
}
else
{
screenState = 'off';
screenTip = 'Start screen sharing';
}
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<div
className={
classnames(classes.root, toolbarsVisible ? classes.show : classes.hide)
}
>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label='Mute mic'
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
micState === 'on' ?
roomClient.muteMic() :
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
</Tooltip>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label='Mute video'
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</Fab>
</Tooltip>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label='Share screen'
className={classes.fab}
disabled={!me.canShareScreen || me.screenShareInProgress}
color={screenState === 'on' ? 'primary' : 'default'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
case 'need-extension':
{
roomClient.installExtension();
break;
}
default:
{
break;
}
}
}}
>
{ screenState === 'on' || screenState === 'unsupported' ?
<ScreenOffIcon/>
:null
}
{ screenState === 'off' ?
<ScreenIcon/>
:null
}
{ screenState === 'need-extension' ?
<ExtensionIcon/>
:null
}
</Fab>
</Tooltip>
<Tooltip
title={locked ? 'Unlock room' : 'Lock room'}
placement={smallScreen ? 'top' : 'right'}
>
<Fab
aria-label='Room lock'
className={classes.fab}
color={locked ? 'primary' : 'default'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
if (locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ locked ?
<LockIcon />
:
<LockOpenIcon />
}
</Fab>
</Tooltip>
{ /* <Fab
aria-label='Raise hand'
className={classes.fab}
disabled={me.raiseHandInProgress}
color={me.raiseHand ? 'primary' : 'default'}
size='large'
onClick={() => roomClient.sendRaiseHandState(!me.raiseHand)}
>
<Avatar alt='Hand' src={me.raiseHand ? HandOn : HandOff} />
</Fab> */ }
<Tooltip title='Leave meeting' placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label='Leave meeting'
className={classes.fab}
color='secondary'
size={smallScreen ? 'large' : 'medium'}
onClick={() => roomClient.close()}
>
<LeaveIcon />
</Fab>
</Tooltip>
</div>
);
};
Sidebar.propTypes =
{
roomClient : PropTypes.any.isRequired,
toolbarsVisible : PropTypes.bool.isRequired,
me : appPropTypes.Me.isRequired,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
locked : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
toolbarsVisible : state.room.toolbarsVisible,
...meProducersSelector(state),
me : state.me,
locked : state.room.locked
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.room.locked === next.room.locked &&
prev.producers === next.producers &&
prev.me === next.me
);
}
}
)(withStyles(styles, { withTheme: true })(Sidebar)));

View File

@ -0,0 +1,90 @@
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import PropTypes from 'prop-types';
import Dialog from '@material-ui/core/Dialog';
import Typography from '@material-ui/core/Typography';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
root :
{
},
dialogPaper :
{
width : '20vw',
padding : theme.spacing(2),
[theme.breakpoints.down('lg')] :
{
width : '30vw'
},
[theme.breakpoints.down('md')] :
{
width : '40vw'
},
[theme.breakpoints.down('sm')] :
{
width : '60vw'
},
[theme.breakpoints.down('xs')] :
{
width : '80vw'
}
},
logo :
{
display : 'block'
}
});
const JoinDialog = ({
roomClient,
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
}
<Typography variant='subtitle1'>You are about to join a meeting, how would you like to join?</Typography>
<DialogActions>
<Button
onClick={() =>
{
roomClient.join({ joinVideo: false });
}}
variant='contained'
>
Audio only
</Button>
<Button
onClick={() =>
{
roomClient.join({ joinVideo: true });
}}
variant='contained'
>
Audio and Video
</Button>
</DialogActions>
</Dialog>
);
};
JoinDialog.propTypes =
{
roomClient : PropTypes.any.isRequired,
classes : PropTypes.object.isRequired
};
export default withRoomContext(withStyles(styles)(JoinDialog));

View File

@ -12,7 +12,7 @@ const styles = (theme) =>
({
root :
{
padding : theme.spacing.unit,
padding : theme.spacing(1),
display : 'flex',
alignItems : 'center',
borderRadius : 0

View File

@ -21,8 +21,8 @@ const styles = (theme) =>
root :
{
display : 'flex',
marginBottom : theme.spacing.unit,
padding : theme.spacing.unit,
marginBottom : theme.spacing(1),
padding : theme.spacing(1),
flexShrink : 0
},
selfMessage :
@ -42,7 +42,7 @@ const styles = (theme) =>
},
content :
{
marginLeft : theme.spacing.unit
marginLeft : theme.spacing(1)
},
avatar :
{

View File

@ -14,7 +14,7 @@ const styles = (theme) =>
flexDirection : 'column',
alignItems : 'center',
overflowY : 'auto',
padding : theme.spacing.unit
padding : theme.spacing(1)
}
});

View File

@ -6,7 +6,6 @@ import { withStyles } from '@material-ui/core/styles';
import magnet from 'magnet-uri';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
const styles = (theme) =>
({
@ -15,11 +14,11 @@ const styles = (theme) =>
display : 'flex',
alignItems : 'center',
width : '100%',
padding : theme.spacing.unit,
padding : theme.spacing(1),
boxShadow : '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
'&:not(:last-child)' :
{
marginBottom : theme.spacing.unit
marginBottom : theme.spacing(1)
}
},
avatar :
@ -30,7 +29,7 @@ const styles = (theme) =>
text :
{
margin : 0,
padding : theme.spacing.unit
padding : theme.spacing(1)
},
fileContent :
{
@ -41,7 +40,7 @@ const styles = (theme) =>
{
display : 'flex',
alignItems : 'center',
padding : theme.spacing.unit
padding : theme.spacing(1)
},
button :
{
@ -55,14 +54,17 @@ class File extends React.PureComponent
{
const {
roomClient,
torrentSupport,
displayName,
picture,
canShareFiles,
magnetUri,
file,
classes
} = this.props;
return (
<div className={classes.root}>
<img alt='Peer avatar' className={classes.avatar} src={file.picture || EmptyAvatar} />
<img alt='Avatar' className={classes.avatar} src={picture} />
<div className={classes.fileContent}>
{ file.files ?
@ -93,26 +95,22 @@ class File extends React.PureComponent
:null
}
<Typography className={classes.text}>
{ file.me ?
'You shared a file'
:
`${file.displayName} shared a file`
}
{ `${displayName} shared a file` }
</Typography>
{ !file.active && !file.files ?
<div className={classes.fileInfo}>
<Typography className={classes.text}>
{magnet.decode(file.magnetUri).dn}
{ magnet.decode(magnetUri).dn }
</Typography>
{ torrentSupport ?
{ canShareFiles ?
<Button
variant='contained'
component='span'
className={classes.button}
onClick={() =>
{
roomClient.handleDownload(file.magnetUri);
roomClient.handleDownload(magnetUri);
}}
>
Download
@ -146,7 +144,10 @@ class File extends React.PureComponent
File.propTypes = {
roomClient : PropTypes.object.isRequired,
torrentSupport : PropTypes.bool.isRequired,
magnetUri : PropTypes.string.isRequired,
displayName : PropTypes.string.isRequired,
picture : PropTypes.string,
canShareFiles : PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
@ -155,10 +156,21 @@ const mapStateToProps = (state, { magnetUri }) =>
{
return {
file : state.files[magnetUri],
torrentSupport : state.room.torrentSupport
canShareFiles : state.me.canShareFiles
};
};
export default withRoomContext(connect(
mapStateToProps
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.files === next.files &&
prev.me.canShareFiles === next.me.canShareFiles
);
}
}
)(withStyles(styles)(File)));

View File

@ -1,8 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import File from './File';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
const styles = (theme) =>
({
@ -13,7 +15,7 @@ const styles = (theme) =>
flexDirection : 'column',
alignItems : 'center',
overflowY : 'auto',
padding : theme.spacing.unit
padding : theme.spacing(1)
}
});
@ -42,14 +44,44 @@ class FileList extends React.PureComponent
{
const {
files,
me,
picture,
peers,
classes
} = this.props;
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
{ Object.keys(files).map((magnetUri) =>
<File key={magnetUri} magnetUri={magnetUri} />
)}
{ Object.entries(files).map(([ magnetUri, file ]) =>
{
let displayName;
let filePicture;
if (me.id === file.peerId)
{
displayName = 'You';
filePicture = picture;
}
else if (peers[file.peerId])
{
displayName = peers[file.peerId].displayName;
filePicture = peers[file.peerId].picture;
}
else
{
displayName = 'Unknown';
}
return (
<File
key={magnetUri}
magnetUri={magnetUri}
displayName={displayName}
picture={filePicture || EmptyAvatar}
/>
);
})}
</div>
);
}
@ -58,14 +90,35 @@ class FileList extends React.PureComponent
FileList.propTypes =
{
files : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
picture : PropTypes.string,
peers : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
files : state.files
files : state.files,
me : state.me,
picture : state.settings.picture,
peers : state.peers
};
};
export default connect(mapStateToProps)(withStyles(styles)(FileList));
export default connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.files === next.files &&
prev.me === next.me &&
prev.settings.picture === next.settings.picture &&
prev.peers === next.peers
);
}
}
)(withStyles(styles)(FileList));

View File

@ -22,7 +22,7 @@ const styles = (theme) =>
},
button :
{
margin : theme.spacing.unit
margin : theme.spacing(1)
}
});
@ -46,11 +46,11 @@ class FileSharing extends React.PureComponent
render()
{
const {
torrentSupport,
canShareFiles,
classes
} = this.props;
const buttonDescription = torrentSupport ?
const buttonDescription = canShareFiles ?
'Share file' : 'File sharing not supported';
return (
@ -67,7 +67,7 @@ class FileSharing extends React.PureComponent
variant='contained'
component='span'
className={classes.button}
disabled={!torrentSupport}
disabled={!canShareFiles}
>
{buttonDescription}
</Button>
@ -81,7 +81,7 @@ class FileSharing extends React.PureComponent
FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired,
torrentSupport : PropTypes.bool.isRequired,
canShareFiles : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired
};
@ -89,7 +89,7 @@ FileSharing.propTypes = {
const mapStateToProps = (state) =>
{
return {
torrentSupport : state.room.torrentSupport,
canShareFiles : state.me.canShareFiles,
tabOpen : state.toolarea.currentToolTab === 'files'
};
};

View File

@ -7,11 +7,11 @@ import * as appPropTypes from '../../appPropTypes';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = () =>
const styles = (theme) =>
({
root :
{
padding : '0.5rem',
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
@ -31,7 +31,7 @@ const styles = () =>
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : '0.5rem',
paddingLeft : theme.spacing(1),
flexGrow : 1,
alignItems : 'center'
},

View File

@ -13,11 +13,11 @@ import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = () =>
const styles = (theme) =>
({
root :
{
padding : '0.5rem',
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
@ -37,7 +37,7 @@ const styles = () =>
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : '0.5rem',
paddingLeft : theme.spacing(1),
flexGrow : 1,
alignItems : 'center'
},
@ -185,8 +185,8 @@ const ListPeer = (props) =>
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
}}
>
{ screenVisible ?
@ -207,8 +207,8 @@ const ListPeer = (props) =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
@ -241,7 +241,7 @@ const makeMapStateToProps = (initialState, props) =>
const mapStateToProps = (state) =>
{
return {
peer : state.peers[props.name],
peer : state.peers[props.id],
...getPeerConsumers(state, props)
};
};

View File

@ -18,23 +18,23 @@ const styles = (theme) =>
{
width : '100%',
overflowY : 'auto',
padding : 6
padding : theme.spacing(1)
},
list :
{
listStyleType : 'none',
padding : theme.spacing.unit,
padding : theme.spacing(1),
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)'
},
listheader :
{
padding : '0.5rem',
padding : theme.spacing(1),
fontWeight : 'bolder'
},
listItem :
{
padding : '0.5rem',
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'pointer',
@ -76,7 +76,7 @@ class ParticipantList extends React.PureComponent
roomClient,
advancedMode,
passivePeers,
selectedPeerName,
selectedPeerId,
spotlightPeers,
classes
} = this.props;
@ -92,14 +92,14 @@ class ParticipantList extends React.PureComponent
<li className={classes.listheader}>Participants in Spotlight:</li>
{ spotlightPeers.map((peer) => (
<li
key={peer.name}
key={peer.id}
className={classNames(classes.listItem, {
selected : peer.name === selectedPeerName
selected : peer.id === selectedPeerId
})}
onClick={() => roomClient.setSelectedPeer(peer.name)}
onClick={() => roomClient.setSelectedPeer(peer.id)}
>
<ListPeer name={peer.name} advancedMode={advancedMode}>
<Volume small name={peer.name} />
<ListPeer id={peer.id} advancedMode={advancedMode}>
<Volume small id={peer.id} />
</ListPeer>
</li>
))}
@ -107,15 +107,15 @@ class ParticipantList extends React.PureComponent
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li>
{ passivePeers.map((peerName) => (
{ passivePeers.map((peerId) => (
<li
key={peerName}
key={peerId}
className={classNames(classes.listItem, {
selected : peerName === selectedPeerName
selected : peerId === selectedPeerId
})}
onClick={() => roomClient.setSelectedPeer(peerName)}
onClick={() => roomClient.setSelectedPeer(peerId)}
>
<ListPeer name={peerName} advancedMode={advancedMode} />
<ListPeer id={peerId} advancedMode={advancedMode} />
</li>
))}
</ul>
@ -129,7 +129,7 @@ ParticipantList.propTypes =
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
passivePeers : PropTypes.array,
selectedPeerName : PropTypes.string,
selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array,
classes : PropTypes.object.isRequired
};
@ -138,7 +138,7 @@ const mapStateToProps = (state) =>
{
return {
passivePeers : passivePeersSelector(state),
selectedPeerName : state.room.selectedPeerName,
selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state)
};
};
@ -153,7 +153,7 @@ const ParticipantListContainer = withRoomContext(connect(
return (
prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerName === next.room.selectedPeerName
prev.room.selectedPeerId === next.room.selectedPeerId
);
}
}

View File

@ -7,12 +7,10 @@ import {
spotlightsLengthSelector
} from '../Selectors';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import { withStyles } from '@material-ui/core/styles';
import Peer from '../Containers/Peer';
import Me from '../Containers/Me';
import HiddenPeers from '../Containers/HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334;
const PADDING_V = 50;
@ -43,15 +41,14 @@ class Democratic extends React.PureComponent
{
super(props);
this.state = {
peerWidth : 400,
peerHeight : 300
};
this.state = {};
this.resizeTimeout = null;
this.peersRef = React.createRef();
}
updateDimensions = debounce(() =>
updateDimensions = () =>
{
if (!this.peersRef.current)
{
@ -93,14 +90,21 @@ class Democratic extends React.PureComponent
peerHeight : 0.9 * y
});
}
}, 200);
};
componentDidMount()
{
window.addEventListener('resize', this.updateDimensions);
const observer = new ResizeObserver(this.updateDimensions);
// window.resize event listener
window.addEventListener('resize', () =>
{
// clear the timeout
clearTimeout(this.resizeTimeout);
observer.observe(this.peersRef.current);
// start timing for event "completion"
this.resizeTimeout = setTimeout(() => this.updateDimensions(), 250);
});
this.updateDimensions();
}
componentWillUnmount()
@ -108,8 +112,9 @@ class Democratic extends React.PureComponent
window.removeEventListener('resize', this.updateDimensions);
}
componentDidUpdate()
componentDidUpdate(prevProps)
{
if (prevProps !== this.props)
this.updateDimensions();
}
@ -125,23 +130,25 @@ class Democratic extends React.PureComponent
const style =
{
'width' : this.state.peerWidth,
'height' : this.state.peerHeight
'width' : this.state.peerWidth ? this.state.peerWidth : 0,
'height' : this.state.peerHeight ? this.state.peerHeight : 0
};
return (
<div className={classes.root} ref={this.peersRef}>
<Me
advancedMode={advancedMode}
spacing={6}
style={style}
/>
{ spotlightsPeers.map((peer) =>
{
return (
<Peer
key={peer.name}
key={peer.id}
advancedMode={advancedMode}
name={peer.name}
id={peer.id}
spacing={6}
style={style}
/>
);

View File

@ -1,89 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux';
import debounce from 'lodash/debounce';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import {
spotlightsLengthSelector,
videoBoxesSelector
} from '../Selectors';
import { withRoomContext } from '../../RoomContext';
import Me from '../Containers/Me';
import Peer from '../Containers/Peer';
import SpeakerPeer from '../Containers/SpeakerPeer';
import HiddenPeers from '../Containers/HiddenPeers';
import Grid from '@material-ui/core/Grid';
const styles = () =>
({
root :
{
display : 'flex',
flexDirection : 'column',
alignItems : 'center',
height : '100%',
width : '100%'
},
activePeerContainer :
{
width : '100%',
height : '80vh',
display : 'grid',
gridTemplateColumns : '1fr',
gridTemplateRows : '1.6fr minmax(0, 0.4fr)'
},
speaker :
{
gridArea : '1 / 1 / 2 / 2',
display : 'flex',
justifyContent : 'center',
alignItems : 'center'
},
activePeer :
{
width : '100%',
border : '5px solid rgba(255, 255, 255, 0.15)',
boxShadow : '0px 5px 12px 2px rgba(17, 17, 17, 0.5)',
marginTop : 60
alignItems : 'center',
paddingTop : 40
},
filmStrip :
{
gridArea : '2 / 1 / 3 / 2'
},
filmItem :
{
display : 'flex',
background : 'rgba(0, 0, 0 , 0.5)',
width : '100%',
overflowX : 'auto',
height : '20vh',
alignItems : 'center'
},
filmStripContent :
{
margin : '0 auto',
display : 'flex',
height : '100%',
alignItems : 'center'
},
film :
{
height : '18vh',
flexShrink : 0,
paddingLeft : '1vh',
'& .active' :
{
borderColor : 'var(--active-speaker-border-color)'
},
marginLeft : '6px',
border : 'var(--peer-border)',
'&.selected' :
{
borderColor : 'var(--selected-peer-border-color)'
},
'&:last-child' :
'&.active' :
{
paddingRight : '1vh'
opacity : '0.6'
}
},
filmContent :
{
height : '100%',
width : '100%',
border : '1px solid rgba(255,255,255,0.15)',
maxWidth : 'calc(18vh * (4 / 3))',
cursor : 'pointer',
'& .screen' :
{
maxWidth : 'calc(18vh * (2 * 4 / 3))',
border : 0
}
},
hiddenPeers :
{
}
});
@ -93,80 +58,114 @@ class Filmstrip extends React.PureComponent
{
super(props);
this.resizeTimeout = null;
this.activePeerContainer = React.createRef();
this.filmStripContainer = React.createRef();
}
state = {
lastSpeaker : null,
width : 400
lastSpeaker : null
};
// Find the name of the peer which is currently speaking. This is either
// the latest active speaker, or the manually selected peer, or, if no
// person has spoken yet, the first peer in the list of peers.
getActivePeerName = () =>
getActivePeerId = () =>
{
if (this.props.selectedPeerName)
const {
selectedPeerId,
peers
} = this.props;
const { lastSpeaker } = this.state;
if (selectedPeerId && peers[selectedPeerId])
{
return this.props.selectedPeerName;
return this.props.selectedPeerId;
}
if (this.state.lastSpeaker)
if (lastSpeaker && peers[lastSpeaker])
{
return this.state.lastSpeaker;
}
const peerNames = Object.keys(this.props.peers);
const peerIds = Object.keys(peers);
if (peerNames.length > 0)
if (peerIds.length > 0)
{
return peerNames[0];
return peerIds[0];
}
};
isSharingCamera = (peerName) => this.props.peers[peerName] &&
this.props.peers[peerName].consumers.some((consumer) =>
isSharingCamera = (peerId) => this.props.peers[peerId] &&
this.props.peers[peerId].consumers.some((consumer) =>
this.props.consumers[consumer].source === 'screen');
getRatio = () =>
updateDimensions = () =>
{
let ratio = 4 / 3;
const newState = {};
if (this.isSharingCamera(this.getActivePeerName()))
const speaker = this.activePeerContainer.current;
if (speaker)
{
ratio *= 2;
let speakerWidth = (speaker.clientWidth - 100);
let speakerHeight = (speakerWidth / 4) * 3;
if (this.isSharingCamera(this.getActivePeerId()))
{
speakerWidth /= 2;
speakerHeight = (speakerWidth / 4) * 3;
}
return ratio;
};
updateDimensions = debounce(() =>
if (speakerHeight > (speaker.clientHeight - 60))
{
const container = this.activePeerContainer.current;
speakerHeight = (speaker.clientHeight - 60);
speakerWidth = (speakerHeight / 3) * 4;
}
if (container)
newState.speakerWidth = speakerWidth;
newState.speakerHeight = speakerHeight;
}
const filmStrip = this.filmStripContainer.current;
if (filmStrip)
{
const ratio = this.getRatio();
let filmStripHeight = filmStrip.clientHeight - 10;
let width = container.clientWidth;
let filmStripWidth = (filmStripHeight / 3) * 4;
if (width / ratio > (container.clientHeight - 100))
if (filmStripWidth * this.props.boxes > (filmStrip.clientWidth - 50))
{
width = (container.clientHeight - 100) * ratio;
filmStripWidth = (filmStrip.clientWidth - 50) / this.props.boxes;
filmStripHeight = (filmStripWidth / 4) * 3;
}
newState.filmStripWidth = filmStripWidth;
newState.filmStripHeight = filmStripHeight;
}
this.setState({
width
...newState
});
}
}, 200);
};
componentDidMount()
{
window.addEventListener('resize', this.updateDimensions);
const observer = new ResizeObserver(this.updateDimensions);
// window.resize event listener
window.addEventListener('resize', () =>
{
// clear the timeout
clearTimeout(this.resizeTimeout);
// start timing for event "completion"
this.resizeTimeout = setTimeout(() => this.updateDimensions(), 250);
});
observer.observe(this.activePeerContainer.current);
this.updateDimensions();
}
@ -175,19 +174,28 @@ class Filmstrip extends React.PureComponent
window.removeEventListener('resize', this.updateDimensions);
}
componentWillUpdate(nextProps)
{
if (nextProps !== this.props)
{
if (
nextProps.activeSpeakerId != null &&
nextProps.activeSpeakerId !== this.props.myId
)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : nextProps.activeSpeakerId
});
}
}
}
componentDidUpdate(prevProps)
{
if (prevProps !== this.props)
{
this.updateDimensions();
if (this.props.activeSpeakerName !== this.props.myName)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : this.props.activeSpeakerName
});
}
}
}
@ -196,56 +204,78 @@ class Filmstrip extends React.PureComponent
const {
roomClient,
peers,
myId,
advancedMode,
spotlights,
spotlightsLength,
classes
} = this.props;
const activePeerName = this.getActivePeerName();
const activePeerId = this.getActivePeerId();
const speakerStyle =
{
width : this.state.speakerWidth,
height : this.state.speakerHeight
};
const peerStyle =
{
width : this.state.filmStripWidth,
height : this.state.filmStripHeight
};
return (
<div className={classes.root}>
<div className={classes.activePeerContainer} ref={this.activePeerContainer}>
{ peers[activePeerName] ?
<div
className={classes.activePeer}
style={{
width : this.state.width,
height : this.state.width / this.getRatio()
}}
>
<Peer
<div className={classes.speaker} ref={this.activePeerContainer}>
{ peers[activePeerId] ?
<SpeakerPeer
advancedMode={advancedMode}
name={activePeerName}
id={activePeerId}
style={speakerStyle}
/>
</div>
:null
}
</div>
<div className={classes.filmStrip}>
<div className={classes.filmStripContent}>
{ Object.keys(peers).map((peerName) =>
{
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
{
return (
<div className={classes.filmStrip} ref={this.filmStripContainer}>
<Grid container justify='center' spacing={0}>
<Grid item>
<div
key={peerName}
onClick={() => roomClient.setSelectedPeer(peerName)}
className={classnames(classes.film, {
selected : this.props.selectedPeerName === peerName,
active : this.state.lastSpeaker === peerName
className={classnames(classes.filmItem, {
active : myId === activePeerId
})}
>
<div className={classes.filmContent}>
<Peer
<Me
advancedMode={advancedMode}
name={peerName}
style={peerStyle}
smallButtons
/>
</div>
</Grid>
{ Object.keys(peers).map((peerId) =>
{
if (spotlights.find((spotlightsElement) => spotlightsElement === peerId))
{
return (
<Grid key={peerId} item>
<div
key={peerId}
onClick={() => roomClient.setSelectedPeer(peerId)}
className={classnames(classes.filmItem, {
selected : this.props.selectedPeerId === peerId,
active : peerId === activePeerId
})}
>
<Peer
advancedMode={advancedMode}
id={peerId}
style={peerStyle}
smallButtons
/>
</div>
</Grid>
);
}
else
@ -253,9 +283,9 @@ class Filmstrip extends React.PureComponent
return ('');
}
})}
</Grid>
</div>
</div>
<div className={classes.hiddenPeers}>
{ spotlightsLength<Object.keys(peers).length ?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
@ -263,41 +293,53 @@ class Filmstrip extends React.PureComponent
:null
}
</div>
</div>
);
}
}
Filmstrip.propTypes = {
roomClient : PropTypes.any.isRequired,
activeSpeakerName : PropTypes.string,
activeSpeakerId : PropTypes.string,
advancedMode : PropTypes.bool,
peers : PropTypes.object.isRequired,
consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string,
myId : PropTypes.string.isRequired,
selectedPeerId : PropTypes.string,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired,
boxes : PropTypes.number,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
return {
activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName,
activeSpeakerId : state.room.activeSpeakerId,
selectedPeerId : state.room.selectedPeerId,
peers : state.peers,
consumers : state.consumers,
myName : state.me.name,
myId : state.me.id,
spotlights : state.room.spotlights,
spotlightsLength
spotlightsLength : spotlightsLengthSelector(state),
boxes : videoBoxesSelector(state),
};
};
export default withRoomContext(connect(
mapStateToProps,
undefined
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.selectedPeerId === next.room.selectedPeerId &&
prev.peers === next.peers &&
prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights &&
prev.me.id === next.me.id
);
}
}
)(withStyles(styles)(Filmstrip)));

View File

@ -15,7 +15,6 @@ 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 Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar';
@ -28,11 +27,14 @@ import Filmstrip from './MeetingViews/Filmstrip';
import AudioPeers from './PeerAudio/AudioPeers';
import FullScreenView from './VideoContainers/FullScreenView';
import VideoWindow from './VideoWindow/VideoWindow';
import Sidebar from './Controls/Sidebar';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import SettingsIcon from '@material-ui/icons/Settings';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import Button from '@material-ui/core/Button';
import Settings from './Settings/Settings';
import JoinDialog from './JoinDialog';
const TIMEOUT = 10 * 1000;
@ -58,7 +60,7 @@ const styles = (theme) =>
left : '50%',
transform : 'translateX(-50%) translateY(-50%)',
width : '30vw',
padding : theme.spacing.unit * 2,
padding : theme.spacing(2),
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center'
@ -125,6 +127,11 @@ const styles = (theme) =>
{
display : 'flex'
},
actionButton :
{
margin : theme.spacing(1),
padding : 0
},
meContainer :
{
position : 'fixed',
@ -176,10 +183,6 @@ class Room extends React.PureComponent
componentDidMount()
{
const { roomClient } = this.props;
roomClient.join();
if (this.fullscreen.fullscreenEnabled)
{
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
@ -242,29 +245,7 @@ class Room extends React.PureComponent
democratic : Democratic
}[room.mode];
if (room.audioSuspended)
{
return (
<div className={classes.root}>
<Paper className={classes.message}>
<Typography variant='h2'>
This webpage required sound and video to play, please click to allow.
</Typography>
<Button
variant='contained'
onClick={() =>
{
roomClient.notify('Joining.');
roomClient.resumeAudio();
}}
>
Allow
</Button>
</Paper>
</div>
);
}
else if (room.lockedOut)
if (room.lockedOut)
{
return (
<div className={classes.root}>
@ -274,6 +255,14 @@ class Room extends React.PureComponent
</div>
);
}
else if (!room.joined)
{
return (
<div className={classes.root}>
<JoinDialog />
</div>
);
}
else
{
return (
@ -324,9 +313,32 @@ class Room extends React.PureComponent
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<IconButton
aria-label='Lock room'
className={classes.actionButton}
color='inherit'
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
{ this.fullscreen.fullscreenEnabled ?
<IconButton
aria-label='Fullscreen'
className={classes.actionButton}
color='inherit'
onClick={this.handleToggleFullscreen}
>
@ -340,6 +352,7 @@ class Room extends React.PureComponent
}
<IconButton
aria-label='Settings'
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
@ -348,6 +361,7 @@ class Room extends React.PureComponent
{ loginEnabled ?
<IconButton
aria-label='Account'
className={classes.actionButton}
color='inherit'
onClick={() =>
{
@ -362,6 +376,15 @@ class Room extends React.PureComponent
</IconButton>
:null
}
<Button
aria-label='Leave meeting'
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
Leave
</Button>
</div>
</Toolbar>
</AppBar>
@ -384,8 +407,6 @@ class Room extends React.PureComponent
<View advancedMode={advancedMode} />
<Sidebar />
<Settings />
</div>
);

View File

@ -5,7 +5,7 @@ const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights;
const peersSelector = (state) => state.peers;
const getPeerConsumers = (state, props) =>
(state.peers[props.name] ? state.peers[props.name].consumers : null);
(state.peers[props.id] ? state.peers[props.id].consumers : null);
const getAllConsumers = (state) => state.consumers;
const peersKeySelector = createSelector(
peersSelector,
@ -66,10 +66,10 @@ export const spotlightPeersSelector = createSelector(
spotlightsSelector,
peersSelector,
(spotlights, peers) =>
spotlights.reduce((result, peerName) =>
spotlights.reduce((result, peerId) =>
{
if (peers[peerName])
result.push(peers[peerName]);
if (peers[peerId])
result.push(peers[peerId]);
return result;
}, [])
@ -83,7 +83,7 @@ export const peersLengthSelector = createSelector(
export const passivePeersSelector = createSelector(
peersKeySelector,
spotlightsSelector,
(peers, spotlights) => peers.filter((peerName) => !spotlights.includes(peerName))
(peers, spotlights) => peers.filter((peerId) => !spotlights.includes(peerId))
);
export const videoBoxesSelector = createSelector(

View File

@ -43,7 +43,7 @@ const styles = (theme) =>
},
setting :
{
padding : theme.spacing.unit * 2
padding : theme.spacing(2)
},
formControl :
{
@ -51,13 +51,13 @@ const styles = (theme) =>
}
});
/* const modes = [ {
const modes = [ {
value : 'democratic',
label : 'Democratic view'
}, {
value : 'filmstrip',
label : 'Filmstrip view'
} ]; */
} ];
const resolutions = [ {
value : 'low',
@ -87,6 +87,7 @@ const Settings = ({
settings,
onToggleAdvancedMode,
handleCloseSettings,
handleChangeMode,
classes
}) =>
{
@ -203,6 +204,33 @@ const Settings = ({
</FormHelperText>
</FormControl>
</form>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={room.mode || ''}
onChange={(event) =>
{
if (event.target.value)
handleChangeMode(event.target.value);
}}
name='Room layout'
autoWidth
className={classes.selectEmpty}
>
{ modes.map((mode, index) =>
{
return (
<MenuItem key={index} value={mode.value}>
{mode.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
Select room layout
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
@ -241,7 +269,7 @@ const mapStateToProps = (state) =>
const mapDispatchToProps = {
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode,
handleCloseSettings : stateActions.setSettingsOpen
handleCloseSettings : stateActions.setSettingsOpen,
};
export default withRoomContext(connect(

View File

@ -8,7 +8,7 @@ import * as stateActions from '../../actions/stateActions';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import VideoView from './VideoView';
const styles = () =>
const styles = (theme) =>
({
root :
{
@ -29,7 +29,7 @@ const styles = () =>
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
padding : '0.4vmin'
padding : theme.spacing(1)
},
button :
{
@ -102,13 +102,6 @@ const FullScreenView = (props) =>
return (
<div className={classes.root}>
{ consumerVisible && !consumer.supported ?
<div className={classes.incompatibleVideo}>
<p>incompatible video</p>
</div>
:null
}
<div className={classes.controls}>
<div
className={classnames(classes.button, {

View File

@ -5,7 +5,7 @@ import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import EditableInput from '../Controls/EditableInput';
const styles = () =>
const styles = (theme) =>
({
root :
{
@ -27,7 +27,7 @@ const styles = () =>
transitionProperty : 'opacity',
transitionDuration : '.15s',
backgroundColor : 'var(--peer-video-bg-color)',
'&.is-me' :
'&.isMe' :
{
transform : 'scaleX(-1)'
},
@ -48,54 +48,42 @@ const styles = () =>
},
info :
{
width : '100%',
height : '100%',
padding : theme.spacing(1),
position : 'absolute',
zIndex : 10,
top : '0.6vmin',
left : '0.6vmin',
bottom : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'space-between'
},
media :
{
flex : '0 0 auto',
display : 'flex',
flexDirection : 'row'
transitionProperty : 'opacity',
transitionDuration : '.15s',
'&.hidden' :
{
opacity : 0,
transitionDuration : '0s'
}
},
box :
{
padding : '0.4vmin',
padding : theme.spacing(0.5),
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.25)',
'& p' :
{
userSelect : 'none',
pointerEvents : 'none',
margin : 0,
color : 'rgba(255, 255, 255, 0.7)',
fontSize : 10,
'&:last-child' :
{
marginBottom : 0
}
fontSize : '0.8em'
}
},
peer :
{
flex : '0 0 auto',
display : 'flex',
flexDirection : 'column',
justifyContent : 'flex-end',
position : 'absolute',
bottom : '0.6vmin',
left : 0,
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.25)',
padding : '0.5vmin',
alignItems : 'flex-start'
display : 'flex'
},
displayNameEdit :
{
@ -120,11 +108,6 @@ const styles = () =>
},
deviceInfo :
{
marginTop : '0.4vmin',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'flex-end',
'& span' :
{
userSelect : 'none',
@ -159,6 +142,7 @@ class VideoView extends React.PureComponent
{
const {
isMe,
isScreen,
peer,
displayName,
showPeerInfo,
@ -181,8 +165,11 @@ class VideoView extends React.PureComponent
return (
<div className={classes.root}>
<div className={classes.info}>
{ advancedMode ?
<div className={classes.media}>
<div className={classnames(classes.media,
{
hidden : !advancedMode
})}
>
<div className={classes.box}>
{ audioCodec ?
<p>{audioCodec}</p>
@ -200,22 +187,21 @@ class VideoView extends React.PureComponent
}
</div>
</div>
:null
}
{ showPeerInfo ?
<div className={classes.peer}>
<div className={classes.box}>
{ isMe ?
<EditableInput
value={displayName}
propName='newDisplayName'
className={classnames(classes.displayNameEdit, 'display-name')}
className={classes.displayNameEdit}
classLoading='loading'
classInvalid='invalid'
shouldBlockWhileLoading
editProps={{
maxLength : 30,
autoCorrect : false,
autoCorrect : 'off',
spellCheck : false
}}
onChange={({ newDisplayName }) => onChangeDisplayName(newDisplayName)}
@ -235,6 +221,7 @@ class VideoView extends React.PureComponent
:null
}
</div>
</div>
:null
}
</div>
@ -243,7 +230,7 @@ class VideoView extends React.PureComponent
ref='video'
className={classnames(classes.video, {
hidden : !videoVisible,
'is-me' : isMe,
'isMe' : isMe && !isScreen,
loading : videoProfile === 'none',
contain : videoContain
})}
@ -335,6 +322,7 @@ class VideoView extends React.PureComponent
VideoView.propTypes =
{
isMe : PropTypes.bool,
isScreen : PropTypes.bool,
peer : PropTypes.oneOfType(
[ appPropTypes.Me, appPropTypes.Peer ]),
displayName : PropTypes.string,

View File

@ -6,7 +6,7 @@ import FullScreen from '../FullScreen';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
const styles = () =>
const styles = (theme) =>
({
root :
{
@ -27,7 +27,7 @@ const styles = () =>
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
padding : '0.4vmin'
padding : theme.spacing(1)
},
button :
{

View File

@ -5,7 +5,7 @@ export const Room = PropTypes.shape(
url : PropTypes.string.isRequired,
state : PropTypes.oneOf(
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
activeSpeakerName : PropTypes.string
activeSpeakerId : PropTypes.string
});
export const Device = PropTypes.shape(
@ -17,7 +17,7 @@ export const Device = PropTypes.shape(
export const Me = PropTypes.shape(
{
name : PropTypes.string.isRequired,
id : PropTypes.string.isRequired,
device : Device.isRequired,
canSendMic : PropTypes.bool.isRequired,
canSendWebcam : PropTypes.bool.isRequired,
@ -26,30 +26,28 @@ export const Me = PropTypes.shape(
export const Producer = PropTypes.shape(
{
id : PropTypes.number.isRequired,
id : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
paused : PropTypes.bool.isRequired,
track : PropTypes.any,
codec : PropTypes.string.isRequired
});
export const Peer = PropTypes.shape(
{
name : PropTypes.string.isRequired,
id : PropTypes.string.isRequired,
displayName : PropTypes.string,
device : Device.isRequired,
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
consumers : PropTypes.arrayOf(PropTypes.string).isRequired
});
export const Consumer = PropTypes.shape(
{
id : PropTypes.number.isRequired,
peerName : PropTypes.string.isRequired,
id : PropTypes.string.isRequired,
peerId : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
supported : PropTypes.bool.isRequired,
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
@ -75,7 +73,7 @@ export const Message = PropTypes.shape(
export const FileEntryProps = PropTypes.shape(
{
data : PropTypes.shape({
name : PropTypes.string.isRequired,
id : PropTypes.string.isRequired,
picture : PropTypes.string,
file : PropTypes.shape({
magnet : PropTypes.string.isRequired

View File

@ -0,0 +1,31 @@
import bowser from 'bowser';
window.BB = bowser;
export default function()
{
const ua = navigator.userAgent;
const browser = bowser.getParser(ua);
let flag;
if (browser.satisfies({ chrome: '>=0', chromium: '>=0' }))
flag = 'chrome';
else if (browser.satisfies({ firefox: '>=0' }))
flag = 'firefox';
else if (browser.satisfies({ safari: '>=0' }))
flag = 'safari';
else if (browser.satisfies({ opera: '>=0' }))
flag = 'opera';
else if (browser.satisfies({ 'microsoft edge': '>=0' }))
flag = 'edge';
else
flag = 'unknown';
return {
flag,
name : browser.getBrowserName(),
version : browser.getBrowserVersion(),
bowser : browser
};
}

View File

@ -1,14 +1,13 @@
import domready from 'domready';
import UrlParse from 'url-parse';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { getDeviceInfo } from 'mediasoup-client';
import randomString from 'random-string';
import Logger from './Logger';
import debug from 'debug';
import RoomClient from './RoomClient';
import RoomContext from './RoomContext';
import deviceInfo from './deviceInfo';
import * as stateActions from './actions/stateActions';
import Room from './components/Room';
import LoadingView from './components/LoadingView';
@ -44,57 +43,48 @@ function run()
{
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
const peerName = randomString({ length: 8 }).toLowerCase();
const urlParser = new UrlParse(window.location.href, true);
const peerId = randomString({ length: 8 }).toLowerCase();
const urlParser = new URL(window.location);
const parameters = urlParser.searchParams;
let roomId = (urlParser.pathname).substr(1)
? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
const produce = urlParser.query.produce !== 'false';
const useSimulcast = urlParser.query.simulcast === 'true';
let roomId = (urlParser.pathname).substr(1);
if (!roomId)
roomId = parameters.get('roomId');
if (roomId)
roomId = roomId.toLowerCase();
else
{
roomId = randomString({ length: 8 }).toLowerCase();
urlParser.query.roomId = roomId;
parameters.set('roomId', roomId);
window.history.pushState('', '', urlParser.toString());
}
// Get the effective/shareable Room URL.
const roomUrlParser = new UrlParse(window.location.href, true);
const produce = parameters.get('produce') !== 'false';
const consume = parameters.get('consume') !== 'false';
const useSimulcast = parameters.get('simulcast') === 'true';
const forceTcp = parameters.get('forceTcp') === 'true';
for (const key of Object.keys(roomUrlParser.query))
{
// Don't keep some custom params.
switch (key)
{
case 'roomId':
case 'simulcast':
break;
default:
delete roomUrlParser.query[key];
}
}
delete roomUrlParser.hash;
const roomUrl = roomUrlParser.toString();
const roomUrl = window.location.href.split('?')[0];
// Get current device.
const device = getDeviceInfo();
const device = deviceInfo();
store.dispatch(
stateActions.setRoomUrl(roomUrl));
store.dispatch(
stateActions.setMe({
peerName,
peerId,
device,
loginEnabled : window.config.loginEnabled
})
);
roomClient = new RoomClient(
{ roomId, peerName, device, useSimulcast, produce });
{ roomId, peerId, device, useSimulcast, produce, consume, forceTcp });
global.CLIENT = roomClient;

View File

@ -51,11 +51,30 @@ const consumers = (state = initialState, action) =>
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_EFFECTIVE_PROFILE':
case 'SET_CONSUMER_CURRENT_LAYERS':
{
const { consumerId, profile } = action.payload;
const { consumerId, spatialLayer, temporalLayer } = action.payload;
const consumer = state[consumerId];
const newConsumer = { ...consumer, profile };
const newConsumer =
{
...consumer,
currentSpatialLayer : spatialLayer,
currentTemporalLayer : temporalLayer
};
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_PREFERRED_LAYERS':
{
const { consumerId, spatialLayer, temporalLayer } = action.payload;
const consumer = state[consumerId];
const newConsumer =
{
...consumer,
preferredSpatialLayer : spatialLayer,
preferredTemporalLayer : temporalLayer
};
return { ...state, [consumerId]: newConsumer };
}
@ -69,6 +88,19 @@ const consumers = (state = initialState, action) =>
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_SCORE':
{
const { consumerId, score } = action.payload;
const consumer = state[consumerId];
if (!consumer)
return state;
const newConsumer = { ...consumer, score };
return { ...state, [consumerId]: newConsumer };
}
default:
return state;
}

View File

@ -4,17 +4,17 @@ const files = (state = {}, action) =>
{
case 'ADD_FILE':
{
const { file } = action.payload;
const { peerId, magnetUri } = action.payload;
const newFile = {
active : false,
progress : 0,
files : null,
me : false,
...file
peerId : peerId,
magnetUri : magnetUri
};
return { ...state, [file.magnetUri]: newFile };
return { ...state, [magnetUri]: newFile };
}
case 'ADD_FILE_HISTORY':
@ -30,7 +30,6 @@ const files = (state = {}, action) =>
active : false,
progress : 0,
files : null,
me : false,
...file
};

View File

@ -1,11 +1,11 @@
const initialState =
{
name : null,
id : null,
device : null,
canSendMic : false,
canSendWebcam : false,
canShareScreen : false,
needExtension : false,
canShareFiles : false,
audioDevices : null,
webcamDevices : null,
webcamInProgress : false,
@ -24,14 +24,14 @@ const me = (state = initialState, action) =>
case 'SET_ME':
{
const {
peerName,
peerId,
device,
loginEnabled
} = action.payload;
return {
...state,
name : peerName,
id : peerId,
device,
loginEnabled
};
@ -45,16 +45,20 @@ const me = (state = initialState, action) =>
case 'SET_MEDIA_CAPABILITIES':
{
const { canSendMic, canSendWebcam } = action.payload;
const {
canSendMic,
canSendWebcam,
canShareScreen,
canShareFiles
} = action.payload;
return { ...state, canSendMic, canSendWebcam };
}
case 'SET_SCREEN_CAPABILITIES':
{
const { canShareScreen, needExtension } = action.payload;
return { ...state, canShareScreen, needExtension };
return {
...state,
canSendMic,
canSendWebcam,
canShareScreen,
canShareFiles
};
}
case 'SET_AUDIO_DEVICES':

View File

@ -7,33 +7,33 @@ const peerVolumes = (state = initialState, action) =>
case 'SET_ME':
{
const {
peerName
peerId
} = action.payload;
return { ...state, [peerName]: 0 };
return { ...state, [peerId]: 0 };
}
case 'ADD_PEER':
{
const { peer } = action.payload;
return { ...state, [peer.name]: 0 };
return { ...state, [peer.id]: 0 };
}
case 'REMOVE_PEER':
{
const { peerName } = action.payload;
const { peerId } = action.payload;
const newState = { ...state };
delete newState[peerName];
delete newState[peerId];
return newState;
}
case 'SET_PEER_VOLUME':
{
const { peerName, volume } = action.payload;
const { peerId, volume } = action.payload;
return { ...state, [peerName]: volume };
return { ...state, [peerId]: volume };
}
default:

View File

@ -1,5 +1,3 @@
import omit from 'lodash/omit';
const peer = (state = {}, action) =>
{
switch (action.type)
@ -53,12 +51,17 @@ const peers = (state = {}, action) =>
{
case 'ADD_PEER':
{
return { ...state, [action.payload.peer.name]: peer(undefined, action) };
return { ...state, [action.payload.peer.id]: peer(undefined, action) };
}
case 'REMOVE_PEER':
{
return omit(state, [ action.payload.peerName ]);
const { peerId } = action.payload;
const newState = { ...state };
delete newState[peerId];
return newState;
}
case 'SET_PEER_DISPLAY_NAME':
@ -69,25 +72,25 @@ const peers = (state = {}, action) =>
case 'SET_PEER_PICTURE':
case 'ADD_CONSUMER':
{
const oldPeer = state[action.payload.peerName];
const oldPeer = state[action.payload.peerId];
if (!oldPeer)
{
throw new Error('no Peer found');
}
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
}
case 'REMOVE_CONSUMER':
{
const oldPeer = state[action.payload.peerName];
const oldPeer = state[action.payload.peerId];
// NOTE: This means that the Peer was closed before, so it's ok.
if (!oldPeer)
return state;
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
}
default:

View File

@ -4,17 +4,17 @@ const initialState =
state : 'new', // new/connecting/connected/disconnected/closed,
locked : false,
lockedOut : false,
audioSuspended : false,
activeSpeakerName : null,
activeSpeakerId : null,
torrentSupport : false,
showSettings : false,
fullScreenConsumer : null, // ConsumerID
windowConsumer : null, // ConsumerID
toolbarsVisible : true,
mode : 'democratic',
selectedPeerName : null,
selectedPeerId : null,
spotlights : [],
settingsOpen : false
settingsOpen : false,
joined : false
};
const room = (state = initialState, action) =>
@ -35,7 +35,7 @@ const room = (state = initialState, action) =>
if (roomState === 'connected')
return { ...state, state: roomState };
else
return { ...state, state: roomState, activeSpeakerName: null };
return { ...state, state: roomState, activeSpeakerId: null };
}
case 'SET_ROOM_LOCKED':
@ -53,13 +53,6 @@ const room = (state = initialState, action) =>
return { ...state, lockedOut: true };
}
case 'SET_AUDIO_SUSPENDED':
{
const { audioSuspended } = action.payload;
return { ...state, audioSuspended };
}
case 'SET_SETTINGS_OPEN':
{
const { settingsOpen } = action.payload;
@ -69,9 +62,9 @@ const room = (state = initialState, action) =>
case 'SET_ROOM_ACTIVE_SPEAKER':
{
const { peerName } = action.payload;
const { peerId } = action.payload;
return { ...state, activeSpeakerName: peerName };
return { ...state, activeSpeakerId: peerId };
}
case 'FILE_SHARING_SUPPORTED':
@ -88,6 +81,13 @@ const room = (state = initialState, action) =>
return { ...state, showSettings };
}
case 'TOGGLE_JOINED':
{
const joined = !state.joined;
return { ...state, joined };
}
case 'TOGGLE_FULLSCREEN_CONSUMER':
{
const { consumerId } = action.payload;
@ -119,13 +119,13 @@ const room = (state = initialState, action) =>
case 'SET_SELECTED_PEER':
{
const { selectedPeerName } = action.payload;
const { selectedPeerId } = action.payload;
return {
...state,
selectedPeerName : state.selectedPeerName === selectedPeerName ?
null : selectedPeerName
selectedPeerId : state.selectedPeerId === selectedPeerId ?
null : selectedPeerId
};
}

View File

@ -1,10 +1,10 @@
export function getSignalingUrl(peerName, roomId)
export function getSignalingUrl(peerId, roomId)
{
const hostname = window.location.hostname;
const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.location.port;
const url = `wss://${hostname}:${port}/?peerName=${peerName}&roomId=${roomId}`;
const url = `wss://${hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`;
return url;
}

View File

@ -1,14 +1,28 @@
const os = require('os');
module.exports =
{
// oAuth2 conf
oauth2 :
auth :
{
clientID : '',
clientSecret : '',
callbackURL : 'https://mYDomainName:port/auth-callback'
/*
The issuer URL for OpenID Connect discovery
The OpenID Provider Configuration Document
could be discovered on:
issuerURL + '/.well-known/openid-configuration'
*/
issuerURL : 'https://example.com',
clientOptions :
{
client_id : '',
client_secret : '',
scope : 'openid email profile',
// where client.example.com is your multiparty meeting server
redirect_uri : 'https://client.example.com/auth/callback'
}
},
// Listening hostname for `gulp live|open`.
domain : 'localhost',
// session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e',
tls :
{
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
@ -19,10 +33,13 @@ module.exports =
// Any http request is redirected to https.
// Listening port for http server.
listeningRedirectPort : 80,
// STUN/TURN
// Mediasoup settings
mediasoup :
{
// mediasoup Server settings.
numWorkers : Object.keys(os.cpus()).length,
// mediasoup Worker settings.
worker :
{
logLevel : 'warn',
logTags :
[
@ -31,47 +48,46 @@ module.exports =
'dtls',
'rtp',
'srtp',
'rtcp',
'rbe',
'rtx'
'rtcp'
],
rtcIPv4 : true,
rtcIPv6 : true,
rtcAnnouncedIPv4 : null,
rtcAnnouncedIPv6 : null,
rtcMinPort : 40000,
rtcMaxPort : 49999,
// mediasoup Room codecs.
rtcMaxPort : 49999
},
// mediasoup Router settings.
router :
{
// Router media codecs.
mediaCodecs :
[
{
kind : 'audio',
name : 'opus',
mimeType : 'audio/opus',
clockRate : 48000,
channels : 2,
parameters :
{
useinbandfec : 1
}
channels : 2
},
// {
// kind : 'video',
// name : 'VP8',
// clockRate : 90000
// }
{
kind : 'video',
name : 'H264',
mimeType : 'video/h264',
clockRate : 90000,
parameters :
{
'packetization-mode' : 1,
'profile-level-id' : '42e01f',
'level-asymmetry-allowed' : 1
'level-asymmetry-allowed' : 1,
'x-google-start-bitrate' : 1000
}
}
]
},
// mediasoup WebRtcTransport settings.
webRtcTransport :
{
listenIps :
[
{ ip: '1.2.3.4', announcedIp: null }
],
// mediasoup per Peer max sending bitrate (in bps).
maxBitrate : 500000
maxIncomingBitrate : 1500000,
initialAvailableOutgoingBitrate : 1000000
}
}
};

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,7 @@ function handleRoom(room, stream)
Object.assign({}, baseEvent,
{
event : 'room.newpeer',
peerName : peer.name,
peerId : peer.id,
rtpCapabilities : peer.rtpCapabilities
}),
stream);
@ -67,7 +67,7 @@ function handlePeer(peer, baseEvent, stream)
{
baseEvent = Object.assign({}, baseEvent,
{
peerName : peer.name
peerId : peer.id
});
peer.on('close', (originator) =>

View File

@ -1,20 +1,24 @@
{
"name": "multiparty-meeting-server",
"version": "2.0.0",
"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",
"mediasoup": "^2.6.11",
"passport-dataporten": "^1.3.0",
"socket.io": "^2.1.1"
"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",

View File

@ -6,15 +6,20 @@ process.title = 'multiparty-meeting-server';
const config = require('./config/config');
const fs = require('fs');
const https = require('https');
const http = require('http');
const spdy = require('spdy');
const express = require('express');
const compression = require('compression');
const mediasoup = require('mediasoup');
const AwaitQueue = require('awaitqueue');
const Logger = require('./lib/Logger');
const Room = require('./lib/Room');
const Dataporten = require('passport-dataporten');
const utils = require('./util');
const base64 = require('base-64');
// auth
const passport = require('passport');
const { Issuer, Strategy } = require('openid-client');
const session = require('express-session');
/* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG);
@ -22,11 +27,18 @@ console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
/* eslint-enable no-console */
// Start the mediasoup server.
const mediaServer = require('./mediasoup');
const logger = new Logger();
const queue = new AwaitQueue();
// mediasoup Workers.
// @type {Array<mediasoup.Worker>}
const mediasoupWorkers = [];
// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;
// Map of Room instances indexed by roomId.
const rooms = new Map();
@ -38,57 +50,218 @@ const tls =
};
const app = express();
let httpsServer;
let oidcClient;
let oidcStrategy;
app.use(compression());
const dataporten = new Dataporten.Setup(config.oauth2);
app.all('*', (req, res, next) =>
passport.serializeUser((user, done) =>
{
if (req.secure)
done(null, user);
});
passport.deserializeUser((user, done) =>
{
done(null, user);
});
const auth = config.auth;
async function run()
{
if (
typeof(auth) !== 'undefined' &&
typeof(auth.issuerURL) !== 'undefined' &&
typeof(auth.clientOptions) !== 'undefined'
)
{
return next();
Issuer.discover(auth.issuerURL).then( async (oidcIssuer) =>
{
// Setup authentication
await setupAuth(oidcIssuer);
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
})
.catch((err) =>
{
logger.error(err);
});
}
else
{
logger.error('Auth is not configure properly!');
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
}
res.redirect(`https://${req.hostname}${req.url}`);
});
// Log rooms status every 30 seconds.
setInterval(() =>
{
for (const room of rooms.values())
{
room.logStatus();
}
}, 120000);
}
app.use(dataporten.passport.initialize());
app.use(dataporten.passport.session());
app.get('/login', (req, res, next) =>
async function setupAuth(oidcIssuer)
{
dataporten.passport.authenticate('dataporten', {
oidcClient = new oidcIssuer.Client(auth.clientOptions);
// ... any authorization request parameters go here
// client_id defaults to client.client_id
// redirect_uri defaults to client.redirect_uris[0]
// response type defaults to client.response_types[0], then 'code'
// scope defaults to 'openid'
const params = auth.clientOptions;
// optional, defaults to false, when true req is passed as a first
// argument to verify fn
const passReqToCallback = false;
// optional, defaults to false, when true the code_challenge_method will be
// resolved from the issuer configuration, instead of true you may provide
// any of the supported values directly, i.e. "S256" (recommended) or "plain"
const usePKCE = false;
const client = oidcClient;
oidcStrategy = new Strategy(
{ client, params, passReqToCallback, usePKCE },
(tokenset, userinfo, done) =>
{
const user =
{
id : tokenset.claims.sub,
provider : tokenset.claims.iss,
_userinfo : userinfo,
_claims : tokenset.claims
};
if (typeof(userinfo.picture) !== 'undefined')
{
if (!userinfo.picture.match(/^http/g))
{
user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ];
}
else
{
user.Photos = [ { value: userinfo.picture } ];
}
}
if (userinfo.nickname != null)
{
user.displayName = userinfo.nickname;
}
if (userinfo.name != null)
{
user.displayName = userinfo.name;
}
if (userinfo.email != null)
{
user.emails = [ { value: userinfo.email } ];
}
if (userinfo.given_name != null)
{
user.name = { givenName: userinfo.given_name };
}
if (userinfo.family_name != null)
{
user.name = { familyName: userinfo.family_name };
}
if (userinfo.middle_name != null)
{
user.name = { middleName: userinfo.middle_name };
}
return done(null, user);
}
);
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
app.get('/auth/login', (req, res, next) =>
{
passport.authenticate('oidc', {
state : base64.encode(JSON.stringify({
roomId : req.query.roomId,
peerName : req.query.peerName,
peerId : req.query.peerId,
code : utils.random(10)
}))
})(req, res, next);
});
});
dataporten.setupLogout(app, '/logout');
// logout
app.get('/auth/logout', (req, res) =>
{
req.logout();
res.redirect('/');
});
app.get('/', (req, res) =>
{
res.sendFile(`${__dirname}/public/chooseRoom.html`);
});
app.get(
'/auth-callback',
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
// callback
app.get(
'/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
(req, res) =>
{
const state = JSON.parse(base64.decode(req.query.state));
if (rooms.has(state.roomId))
{
let displayName;
let photo;
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 =
{
peerName : state.peerName,
name : req.user.data.displayName,
picture : req.user.data.photos[0]
peerId : state.peerId,
displayName : displayName,
picture : photo
};
const room = rooms.get(state.roomId);
@ -98,37 +271,65 @@ app.get(
res.send('');
}
);
);
}
// Serve all files in the public folder as static files.
app.use(express.static('public'));
app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));
const httpsServer = https.createServer(tls, app);
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
async function runHttpsServer()
{
logger.info('Server running on port: ', config.listeningPort);
});
app.use(compression());
const httpServer = http.createServer(app);
app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge'));
httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
{
logger.info('Server redirecting port: ', config.listeningRedirectPort);
});
const io = require('socket.io')(httpsServer);
// Handle connections from clients.
io.on('connection', (socket) =>
{
const { roomId, peerName } = socket.handshake.query;
if (!roomId || !peerName)
app.all('*', (req, res, next) =>
{
logger.warn('connection request without roomId and/or peerName');
if (req.secure)
{
return next();
}
res.redirect(`https://${req.hostname}${req.url}`);
});
app.get('/', (req, res) =>
{
res.sendFile(`${__dirname}/public/chooseRoom.html`);
});
// Serve all files in the public folder as static files.
app.use(express.static('public'));
app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));
httpsServer = spdy.createServer(tls, app);
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
{
logger.info('Server running on port: ', config.listeningPort);
});
const httpServer = http.createServer(app);
httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
{
logger.info('Server redirecting port: ', config.listeningRedirectPort);
});
}
/**
* Create a protoo WebSocketServer to allow WebSocket connections from browsers.
*/
async function runWebSocketServer()
{
const io = require('socket.io')(httpsServer);
// Handle connections from clients.
io.on('connection', (socket) =>
{
const { roomId, peerId } = socket.handshake.query;
if (!roomId || !peerId)
{
logger.warn('connection request without roomId and/or peerId');
socket.disconnect(true);
@ -136,49 +337,90 @@ io.on('connection', (socket) =>
}
logger.info(
'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName);
'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
let room;
// If an unknown roomId, create a new Room.
if (!rooms.has(roomId))
queue.push(async () =>
{
logger.info('creating a new Room [roomId:"%s"]', roomId);
const room = await getOrCreateRoom({ roomId });
try
room.handleConnection({ peerId, socket });
})
.catch((error) =>
{
room = new Room(roomId, mediaServer, io);
global.APP_ROOM = room;
}
catch (error)
{
logger.error('error creating a new Room: %s', error);
logger.error('room creation or room joining failed:%o', error);
socket.disconnect(true);
return;
}
});
});
}
const logStatusTimer = setInterval(() =>
/**
* Launch as many mediasoup Workers as given in the configuration file.
*/
async function runMediasoupWorkers()
{
const { numWorkers } = config.mediasoup;
logger.info('running %d mediasoup Workers...', numWorkers);
for (let i = 0; i < numWorkers; ++i)
{
room.logStatus();
}, 30000);
const worker = await mediasoup.createWorker(
{
logLevel : config.mediasoup.worker.logLevel,
logTags : config.mediasoup.worker.logTags,
rtcMinPort : config.mediasoup.worker.rtcMinPort,
rtcMaxPort : config.mediasoup.worker.rtcMaxPort
});
worker.on('died', () =>
{
logger.error(
'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid);
setTimeout(() => process.exit(1), 2000);
});
mediasoupWorkers.push(worker);
}
}
/**
* Get next mediasoup Worker.
*/
function getMediasoupWorker()
{
const worker = mediasoupWorkers[nextMediasoupWorkerIdx];
if (++nextMediasoupWorkerIdx === mediasoupWorkers.length)
nextMediasoupWorkerIdx = 0;
return worker;
}
/**
* Get a Room instance (or create one if it does not exist).
*/
async function getOrCreateRoom({ roomId })
{
let room = rooms.get(roomId);
// If the Room does not exist create a new one.
if (!room)
{
logger.info('creating a new Room [roomId:%s]', roomId);
const mediasoupWorker = getMediasoupWorker();
room = await Room.create({ mediasoupWorker, roomId });
rooms.set(roomId, room);
room.on('close', () =>
{
rooms.delete(roomId);
clearInterval(logStatusTimer);
});
}
else
{
room = rooms.get(roomId);
room.on('close', () => rooms.delete(roomId));
}
socket.room = roomId;
return room;
}
room.handleConnection(peerName, socket);
});
run();