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/ node_modules/
/app/build/ /app/build/
/app/public/config.js /app/public/config/config.js
/app/public/images/logo.* /app/public/images/logo.*
/server/config/ /server/config/
!/server/config/config.example.js !/server/config/config.example.js
/server/public/ /server/public/
/server/certs/ /server/certs/
!/server/certs/mediasoup-demo.localhost.* !/server/certs/mediasoup-demo.localhost.*
.vscode

View File

@ -1,5 +1,23 @@
# Changelog # 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 ### 1.1
* Moved Filesharing code out from React code to RoomClient * Moved Filesharing code out from React code to RoomClient
* Major cleanup of CSS. Variables for most colors and sizes exposed in :root * 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 * Chat
* Screen sharing * Screen sharing
* File 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. 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 $ cd multiparty-meeting
``` ```
* Copy `server/config.example.js` to `server/config.js` : * Copy `server/config/config.example.js` to `server/config/config.js` :
```bash ```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 ```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). * 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", "name": "multiparty-meeting",
"version": "2.0.0", "version": "3.0.0",
"private": true, "private": true,
"description": "multiparty meeting service", "description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@material-ui/core": "^3.9.2", "@material-ui/core": "^4.1.2",
"@material-ui/icons": "^3.0.2", "@material-ui/icons": "^4.2.1",
"bowser": "^2.4.0",
"create-torrent": "^3.33.0", "create-torrent": "^3.33.0",
"domready": "^1.0.8", "domready": "^1.0.8",
"file-saver": "^2.0.1", "file-saver": "^2.0.1",
"hark": "^1.2.3", "hark": "^1.2.3",
"js-cookie": "^2.2.0",
"marked": "^0.6.1", "marked": "^0.6.1",
"mediasoup-client": "^2.4.10", "mediasoup-client": "^3.0.6",
"notistack": "^0.5.1", "notistack": "^0.5.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^16.8.5", "react": "^16.8.5",
"react-cookie-consent": "^2.2.2", "react-cookie-consent": "^2.2.2",
"react-dom": "^16.8.5", "react-dom": "^16.8.5",
"react-draggable": "^3.2.1",
"react-redux": "^6.0.1", "react-redux": "^6.0.1",
"react-scripts": "2.1.8", "react-scripts": "2.1.8",
"react-tooltip": "^3.10.0",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-persist": "^5.10.0", "redux-persist": "^5.10.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",
"riek": "^1.1.0", "riek": "^1.1.0",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"source-map-explorer": "^1.8.0", "source-map-explorer": "^1.8.0",
"url-parse": "^1.4.4",
"webtorrent": "^0.103.1" "webtorrent": "^0.103.1"
}, },
"scripts": { "scripts": {
"analyze-main": "source-map-explorer build/static/js/main.*", "analyze-main": "source-map-explorer build/static/js/main.*",
"analyze-chunk": "source-map-explorer build/static/js/2.*", "analyze-chunk": "source-map-explorer build/static/js/2.*",
"start": "HTTPS=true PORT=4443 react-scripts start", "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", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
@ -169,7 +165,12 @@
"no-case-declarations": 2, "no-case-declarations": 2,
"no-catch-shadow": 2, "no-catch-shadow": 2,
"no-class-assign": 2, "no-class-assign": 2,
"no-confusing-arrow": ["error", {"allowParens": true}], "no-confusing-arrow": [
"error",
{
"allowParens": true
}
],
"no-console": 2, "no-console": 2,
"no-const-assign": 2, "no-const-assign": 2,
"no-debugger": 2, "no-debugger": 2,

View File

@ -9,14 +9,13 @@
<meta name='description' content='multiparty meeting - Simple web meetings'> <meta name='description' content='multiparty meeting - Simple web meetings'>
<meta name='theme-color' content='#000000' /> <meta name='theme-color' content='#000000' />
<link rel='chrome-webstore-item' href='https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'> <link rel='preconnect' href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<link rel='shortcut icon' href='%PUBLIC_URL%/favicon.ico' /> <link rel='shortcut icon' href='%PUBLIC_URL%/favicon.ico' />
<link rel='manifest' href='%PUBLIC_URL%/manifest.json' /> <link rel='manifest' href='%PUBLIC_URL%/manifest.json' />
<title>Multiparty Meeting</title> <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> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <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 class DisplayMediaScreenShare
{
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
{ {
constructor() constructor()
{ {
@ -143,11 +34,6 @@ class Chrome72ScreenShare
return true; return true;
} }
needExtension()
{
return false;
}
_toConstraints() _toConstraints()
{ {
const constraints = { const constraints = {
@ -194,11 +80,6 @@ class FirefoxScreenShare
return true; return true;
} }
needExtension()
{
return false;
}
_toConstraints(options) _toConstraints(options)
{ {
const constraints = { 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 class DefaultScreenShare
{ {
isScreenShareAvailable() isScreenShareAvailable()
{ {
return false; return false;
} }
needExtension()
{
return false;
}
} }
export default class ScreenShare export default class ScreenShare
@ -364,18 +138,15 @@ export default class ScreenShare
if (device.version < 66.0) if (device.version < 66.0)
return new FirefoxScreenShare(); return new FirefoxScreenShare();
else else
return new Firefox66ScreenShare(); return new DisplayMediaScreenShare();
} }
case 'chrome': case 'chrome':
{ {
if (device.version < 72.0) return new DisplayMediaScreenShare();
return new ChromeScreenShare();
else
return new Chrome72ScreenShare();
} }
case 'msedge': case 'msedge':
{ {
return new EdgeScreenShare(); return new DisplayMediaScreenShare();
} }
default: default:
{ {

View File

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

View File

@ -14,11 +14,11 @@ export const setRoomState = (state) =>
}; };
}; };
export const setRoomActiveSpeaker = (peerName) => export const setRoomActiveSpeaker = (peerId) =>
{ {
return { return {
type : 'SET_ROOM_ACTIVE_SPEAKER', 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 }) => export const setSettingsOpen = ({ settingsOpen }) =>
({ ({
type : 'SET_SETTINGS_OPEN', type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen } payload : { settingsOpen }
}); });
export const setMe = ({ peerName, device, loginEnabled }) => export const setMe = ({ peerId, device, loginEnabled }) =>
{ {
return { return {
type : 'SET_ME', type : 'SET_ME',
payload : { peerName, device, loginEnabled } payload : { peerId, device, loginEnabled }
}; };
}; };
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) => export const setMediaCapabilities = ({
canSendMic,
canSendWebcam,
canShareScreen,
canShareFiles
}) =>
{ {
return { return {
type : 'SET_MEDIA_CAPABILITIES', type : 'SET_MEDIA_CAPABILITIES',
payload : { canSendMic, canSendWebcam } payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles }
};
};
export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
{
return {
type : 'SET_SCREEN_CAPABILITIES',
payload : { canShareScreen, needExtension }
}; };
}; };
@ -150,27 +139,27 @@ export const setDisplayMode = (mode) =>
payload : { mode } payload : { mode }
}); });
export const setPeerVideoInProgress = (peerName, flag) => export const setPeerVideoInProgress = (peerId, flag) =>
{ {
return { return {
type : 'SET_PEER_VIDEO_IN_PROGRESS', type : 'SET_PEER_VIDEO_IN_PROGRESS',
payload : { peerName, flag } payload : { peerId, flag }
}; };
}; };
export const setPeerAudioInProgress = (peerName, flag) => export const setPeerAudioInProgress = (peerId, flag) =>
{ {
return { return {
type : 'SET_PEER_AUDIO_IN_PROGRESS', type : 'SET_PEER_AUDIO_IN_PROGRESS',
payload : { peerName, flag } payload : { peerId, flag }
}; };
}; };
export const setPeerScreenInProgress = (peerName, flag) => export const setPeerScreenInProgress = (peerId, flag) =>
{ {
return { return {
type : 'SET_PEER_SCREEN_IN_PROGRESS', 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 { return {
type : 'SET_PEER_RAISE_HAND_STATE', 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) => export const setAudioInProgress = (flag) =>
{ {
return { return {
@ -306,35 +303,35 @@ export const addPeer = (peer) =>
}; };
}; };
export const removePeer = (peerName) => export const removePeer = (peerId) =>
{ {
return { return {
type : 'REMOVE_PEER', type : 'REMOVE_PEER',
payload : { peerName } payload : { peerId }
}; };
}; };
export const setPeerDisplayName = (displayName, peerName) => export const setPeerDisplayName = (displayName, peerId) =>
{ {
return { return {
type : 'SET_PEER_DISPLAY_NAME', type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerName } payload : { displayName, peerId }
}; };
}; };
export const addConsumer = (consumer, peerName) => export const addConsumer = (consumer, peerId) =>
{ {
return { return {
type : 'ADD_CONSUMER', type : 'ADD_CONSUMER',
payload : { consumer, peerName } payload : { consumer, peerId }
}; };
}; };
export const removeConsumer = (consumerId, peerName) => export const removeConsumer = (consumerId, peerId) =>
{ {
return { return {
type : 'REMOVE_CONSUMER', 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 { return {
type : 'SET_CONSUMER_EFFECTIVE_PROFILE', type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, profile } 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 { return {
type : 'SET_PEER_VOLUME', 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 { return {
type : 'ADD_FILE', type : 'ADD_FILE',
payload : { file } payload : { peerId, magnetUri }
}; };
}; };
@ -536,10 +549,10 @@ export const setPicture = (picture) =>
payload : { picture } payload : { picture }
}); });
export const setPeerPicture = (peerName, picture) => export const setPeerPicture = (peerId, picture) =>
({ ({
type : 'SET_PEER_PICTURE', type : 'SET_PEER_PICTURE',
payload : { peerName, picture } payload : { peerId, picture }
}); });
export const loggedIn = () => export const loggedIn = () =>
@ -547,10 +560,15 @@ export const loggedIn = () =>
type : 'LOGGED_IN' type : 'LOGGED_IN'
}); });
export const setSelectedPeer = (selectedPeerName) => export const toggleJoined = () =>
({
type : 'TOGGLE_JOINED'
});
export const setSelectedPeer = (selectedPeerId) =>
({ ({
type : 'SET_SELECTED_PEER', type : 'SET_SELECTED_PEER',
payload : { selectedPeerName } payload : { selectedPeerId }
}); });
export const setSpotlights = (spotlights) => 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 { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors'; import { meProducersSelector } from '../Selectors';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import VideoView from '../VideoContainers/VideoView'; import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume'; 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 : root :
{ {
flexDirection : 'row',
margin : 6,
flex : '0 0 auto', flex : '0 0 auto',
boxShadow : 'var(--peer-shadow)', boxShadow : 'var(--peer-shadow)',
border : 'var(--peer-border)', border : 'var(--peer-border)',
@ -23,38 +30,88 @@ const styles = () =>
backgroundPosition : 'bottom', backgroundPosition : 'bottom',
backgroundSize : 'auto 85%', backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat', 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' : '&.active-speaker' :
{ {
borderColor : 'var(--active-speaker-border-color)' borderColor : 'var(--active-speaker-border-color)'
} }
}, },
fab :
{
margin : theme.spacing(1),
pointerEvents : 'auto'
},
viewContainer : viewContainer :
{ {
position : 'relative', position : 'relative',
'&.webcam' : width : '100%',
height : '100%'
},
controls :
{
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' :
{ {
order : 2 opacity : 1
}, },
'&.screen' : '& p' :
{ {
order : 1 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 Me = (props) =>
{ {
const [ hover, setHover ] = useState(false);
let touchTimeout = null;
const { const {
roomClient, roomClient,
me, me,
settings, settings,
activeSpeaker, activeSpeaker,
spacing,
style, style,
smallButtons,
advancedMode, advancedMode,
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer, screenProducer,
classes classes,
theme
} = props; } = props;
const videoVisible = ( const videoVisible = (
@ -69,17 +126,225 @@ const Me = (props) =>
!screenProducer.remotelyPaused !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 ( return (
<React.Fragment> <React.Fragment>
<div <div
className={ className={
classnames( classnames(
classes.root, classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : 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 <VideoView
isMe isMe
advancedMode={advancedMode} advancedMode={advancedMode}
@ -95,15 +360,64 @@ const Me = (props) =>
roomClient.changeDisplayName(displayName); roomClient.changeDisplayName(displayName);
}} }}
> >
<Volume name={me.name} /> <Volume id={me.id} />
</VideoView> </VideoView>
</div> </div>
</div> </div>
{ screenProducer ? { screenProducer ?
<div className={classes.root}> <div
<div className={classnames(classes.viewContainer, 'screen')} style={style}> 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 <VideoView
isMe isMe
isScreen
advancedMode={advancedMode} advancedMode={advancedMode}
videoContain videoContain
videoTrack={screenProducer ? screenProducer.track : null} videoTrack={screenProducer ? screenProducer.track : null}
@ -128,8 +442,11 @@ Me.propTypes =
micProducer : appPropTypes.Producer, micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer,
spacing : PropTypes.number,
style : PropTypes.object, style : PropTypes.object,
classes : PropTypes.object.isRequired smallButtons : PropTypes.bool,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -138,7 +455,7 @@ const mapStateToProps = (state) =>
me : state.me, me : state.me,
...meProducersSelector(state), ...meProducersSelector(state),
settings : state.settings, 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.me === next.me &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings && 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 * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; 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 * as stateActions from '../../actions/stateActions';
import VideoView from '../VideoContainers/VideoView'; import VideoView from '../VideoContainers/VideoView';
import Fab from '@material-ui/core/Fab'; import Fab from '@material-ui/core/Fab';
@ -21,7 +21,6 @@ const styles = (theme) =>
root : root :
{ {
flex : '0 0 auto', flex : '0 0 auto',
margin : 6,
boxShadow : 'var(--peer-shadow)', boxShadow : 'var(--peer-shadow)',
border : 'var(--peer-border)', border : 'var(--peer-border)',
touchAction : 'none', touchAction : 'none',
@ -32,11 +31,11 @@ const styles = (theme) =>
backgroundRepeat : 'no-repeat', backgroundRepeat : 'no-repeat',
'&.webcam' : '&.webcam' :
{ {
order : 2 order : 4
}, },
'&.screen' : '&.screen' :
{ {
order : 1 order : 3
}, },
'&.hover' : '&.hover' :
{ {
@ -49,19 +48,13 @@ const styles = (theme) =>
}, },
fab : fab :
{ {
margin : theme.spacing.unit margin : theme.spacing(1)
}, },
viewContainer : viewContainer :
{ {
position : 'relative', position : 'relative',
'&.webcam' : width : '100%',
{ height : '100%'
order : 2
},
'&.screen' :
{
order : 1
}
}, },
controls : controls :
{ {
@ -73,8 +66,8 @@ const styles = (theme) =>
flexDirection : 'column', flexDirection : 'column',
justifyContent : 'center', justifyContent : 'center',
alignItems : 'flex-end', alignItems : 'flex-end',
padding : '0.4vmin', padding : theme.spacing(1),
zIndex : 20, zIndex : 21,
opacity : 0, opacity : 0,
transition : 'opacity 0.3s', transition : 'opacity 0.3s',
touchAction : 'none', touchAction : 'none',
@ -92,8 +85,8 @@ const styles = (theme) =>
display : 'flex', display : 'flex',
justifyContent : 'center', justifyContent : 'center',
alignItems : 'center', alignItems : 'center',
padding : '0.4vmin', padding : theme.spacing(1),
zIndex : 21, zIndex : 20,
'& p' : '& p' :
{ {
padding : '6px 12px', padding : '6px 12px',
@ -109,15 +102,9 @@ const styles = (theme) =>
const Peer = (props) => const Peer = (props) =>
{ {
const [ hover, setHover ] = useState(false); const [ hover, setHover ] = useState(false);
const [ webcamHover, setWebcamHover ] = useState(false);
const [ screenHover, setScreenHover ] = useState(false);
let touchTimeout = null; let touchTimeout = null;
let touchWebcamTimeout = null;
let touchScreenTimeout = null;
const { const {
roomClient, roomClient,
advancedMode, advancedMode,
@ -128,7 +115,9 @@ const Peer = (props) =>
screenConsumer, screenConsumer,
toggleConsumerFullscreen, toggleConsumerFullscreen,
toggleConsumerWindow, toggleConsumerWindow,
spacing,
style, style,
smallButtons,
windowConsumer, windowConsumer,
classes, classes,
theme theme
@ -164,6 +153,12 @@ const Peer = (props) =>
const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const rootStyle =
{
'margin' : spacing,
...style
};
return ( return (
<React.Fragment> <React.Fragment>
<div <div
@ -194,15 +189,9 @@ const Peer = (props) =>
setHover(false); setHover(false);
}, 2000); }, 2000);
}} }}
style={rootStyle}
> >
<div className={classnames(classes.viewContainer)} style={style}> <div className={classnames(classes.viewContainer)}>
{ videoVisible && !webcamConsumer.supported ?
<div className={classes.videoInfo}>
<p>incompatible video</p>
</div>
:null
}
{ !videoVisible ? { !videoVisible ?
<div className={classes.videoInfo}> <div className={classes.videoInfo}>
<p>this video is paused</p> <p>this video is paused</p>
@ -210,79 +199,80 @@ const Peer = (props) =>
:null :null
} }
{ videoVisible && webcamConsumer.supported ? <div
<div className={classnames(classes.controls, hover ? 'hover' : null)}
className={classnames(classes.controls, webcamHover ? 'hover' : null)} onMouseOver={() => setHover(true)}
onMouseOver={() => setWebcamHover(true)} onMouseOut={() => setHover(false)}
onMouseOut={() => setWebcamHover(false)} onTouchStart={() =>
onTouchStart={() => {
{ if (touchTimeout)
if (touchWebcamTimeout) clearTimeout(touchTimeout);
clearTimeout(touchWebcamTimeout);
setWebcamHover(true); setHover(true);
}} }}
onTouchEnd={() => onTouchEnd={() =>
{ {
if (touchWebcamTimeout) if (touchTimeout)
clearTimeout(touchWebcamTimeout); clearTimeout(touchTimeout);
touchWebcamTimeout = setTimeout(() => touchTimeout = setTimeout(() =>
{ {
setWebcamHover(false); setHover(false);
}, 2000); }, 2000);
}}
>
<Fab
aria-label='Mute mic'
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}} }}
> >
<Fab { micEnabled ?
aria-label='Mute mic' <MicIcon />
className={classes.fab} :
color={micEnabled ? 'default' : 'secondary'} <MicOffIcon />
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}}
>
{ micEnabled ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
{ !smallScreen ?
<Fab
aria-label='New window'
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</Fab>
:null
} }
</Fab>
{ !smallScreen ?
<Fab <Fab
aria-label='Fullscreen' aria-label='New window'
className={classes.fab} className={classes.fab}
disabled={!videoVisible} disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerFullscreen(webcamConsumer); toggleConsumerWindow(webcamConsumer);
}} }}
> >
<FullScreenIcon /> <NewWindowIcon />
</Fab> </Fab>
</div> :null
:null }
}
<Fab
aria-label='Fullscreen'
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
<VideoView <VideoView
advancedMode={advancedMode} advancedMode={advancedMode}
@ -295,7 +285,7 @@ const Peer = (props) =>
audioCodec={micConsumer ? micConsumer.codec : null} audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null} videoCodec={webcamConsumer ? webcamConsumer.codec : null}
> >
<Volume name={peer.name} /> <Volume id={peer.id} />
</VideoView> </VideoView>
</div> </div>
</div> </div>
@ -322,43 +312,37 @@ const Peer = (props) =>
setHover(false); setHover(false);
}, 2000); }, 2000);
}} }}
style={rootStyle}
> >
{ screenVisible && !screenConsumer.supported ?
<div className={classes.videoInfo} style={style}>
<p>incompatible video</p>
</div>
:null
}
{ !screenVisible ? { !screenVisible ?
<div className={classes.videoInfo} style={style}> <div className={classes.videoInfo}>
<p>this video is paused</p> <p>this video is paused</p>
</div> </div>
:null :null
} }
{ screenVisible && screenConsumer.supported ? { screenVisible ?
<div className={classnames(classes.viewContainer)} style={style}> <div className={classnames(classes.viewContainer)}>
<div <div
className={classnames(classes.controls, screenHover ? 'hover' : null)} className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setScreenHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setScreenHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
{ {
if (touchScreenTimeout) if (touchTimeout)
clearTimeout(touchScreenTimeout); clearTimeout(touchTimeout);
setScreenHover(true); setHover(true);
}} }}
onTouchEnd={() => onTouchEnd={() =>
{ {
if (touchScreenTimeout) if (touchTimeout)
clearTimeout(touchScreenTimeout); clearTimeout(touchTimeout);
touchScreenTimeout = setTimeout(() => touchTimeout = setTimeout(() =>
{ {
setScreenHover(false); setHover(false);
}, 2000); }, 2000);
}} }}
> >
@ -370,6 +354,7 @@ const Peer = (props) =>
!screenVisible || !screenVisible ||
(windowConsumer === screenConsumer.id) (windowConsumer === screenConsumer.id)
} }
size={smallButtons ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerWindow(screenConsumer); toggleConsumerWindow(screenConsumer);
@ -384,6 +369,7 @@ const Peer = (props) =>
aria-label='Fullscreen' aria-label='Fullscreen'
className={classes.fab} className={classes.fab}
disabled={!screenVisible} disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerFullscreen(screenConsumer); toggleConsumerFullscreen(screenConsumer);
@ -418,9 +404,11 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number, windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
spacing : PropTypes.number,
style : PropTypes.object, style : PropTypes.object,
smallButtons : PropTypes.bool,
toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
@ -434,10 +422,10 @@ const makeMapStateToProps = (initialState, props) =>
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
peer : state.peers[props.name], peer : state.peers[props.id],
...getPeerConsumers(state, props), ...getPeerConsumers(state, props),
windowConsumer : state.room.windowConsumer, 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 ( return (
prev.peers === next.peers && prev.peers === next.peers &&
prev.consumers === next.consumers && prev.consumers === next.consumers &&
prev.room.activeSpeakerName === next.room.activeSpeakerName && prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.windowConsumer === next.room.windowConsumer 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) => const mapStateToProps = (state) =>
{ {
return { 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 : root :
{ {
padding : theme.spacing.unit, padding : theme.spacing(1),
display : 'flex', display : 'flex',
alignItems : 'center', alignItems : 'center',
borderRadius : 0 borderRadius : 0

View File

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

View File

@ -14,7 +14,7 @@ const styles = (theme) =>
flexDirection : 'column', flexDirection : 'column',
alignItems : 'center', alignItems : 'center',
overflowY : 'auto', 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 magnet from 'magnet-uri';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -15,11 +14,11 @@ const styles = (theme) =>
display : 'flex', display : 'flex',
alignItems : 'center', alignItems : 'center',
width : '100%', 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)', 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)' : '&:not(:last-child)' :
{ {
marginBottom : theme.spacing.unit marginBottom : theme.spacing(1)
} }
}, },
avatar : avatar :
@ -30,7 +29,7 @@ const styles = (theme) =>
text : text :
{ {
margin : 0, margin : 0,
padding : theme.spacing.unit padding : theme.spacing(1)
}, },
fileContent : fileContent :
{ {
@ -41,7 +40,7 @@ const styles = (theme) =>
{ {
display : 'flex', display : 'flex',
alignItems : 'center', alignItems : 'center',
padding : theme.spacing.unit padding : theme.spacing(1)
}, },
button : button :
{ {
@ -55,14 +54,17 @@ class File extends React.PureComponent
{ {
const { const {
roomClient, roomClient,
torrentSupport, displayName,
picture,
canShareFiles,
magnetUri,
file, file,
classes classes
} = this.props; } = this.props;
return ( return (
<div className={classes.root}> <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}> <div className={classes.fileContent}>
{ file.files ? { file.files ?
@ -93,26 +95,22 @@ class File extends React.PureComponent
:null :null
} }
<Typography className={classes.text}> <Typography className={classes.text}>
{ file.me ? { `${displayName} shared a file` }
'You shared a file'
:
`${file.displayName} shared a file`
}
</Typography> </Typography>
{ !file.active && !file.files ? { !file.active && !file.files ?
<div className={classes.fileInfo}> <div className={classes.fileInfo}>
<Typography className={classes.text}> <Typography className={classes.text}>
{magnet.decode(file.magnetUri).dn} { magnet.decode(magnetUri).dn }
</Typography> </Typography>
{ torrentSupport ? { canShareFiles ?
<Button <Button
variant='contained' variant='contained'
component='span' component='span'
className={classes.button} className={classes.button}
onClick={() => onClick={() =>
{ {
roomClient.handleDownload(file.magnetUri); roomClient.handleDownload(magnetUri);
}} }}
> >
Download Download
@ -145,20 +143,34 @@ class File extends React.PureComponent
} }
File.propTypes = { File.propTypes = {
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
torrentSupport : PropTypes.bool.isRequired, magnetUri : PropTypes.string.isRequired,
file : PropTypes.object.isRequired, displayName : PropTypes.string.isRequired,
classes : PropTypes.object.isRequired picture : PropTypes.string,
canShareFiles : PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state, { magnetUri }) => const mapStateToProps = (state, { magnetUri }) =>
{ {
return { return {
file : state.files[magnetUri], file : state.files[magnetUri],
torrentSupport : state.room.torrentSupport canShareFiles : state.me.canShareFiles
}; };
}; };
export default withRoomContext(connect( 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))); )(withStyles(styles)(File)));

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import File from './File'; import File from './File';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -13,7 +15,7 @@ const styles = (theme) =>
flexDirection : 'column', flexDirection : 'column',
alignItems : 'center', alignItems : 'center',
overflowY : 'auto', overflowY : 'auto',
padding : theme.spacing.unit padding : theme.spacing(1)
} }
}); });
@ -42,14 +44,44 @@ class FileList extends React.PureComponent
{ {
const { const {
files, files,
me,
picture,
peers,
classes classes
} = this.props; } = this.props;
return ( return (
<div className={classes.root} ref={(node) => { this.node = node; }}> <div className={classes.root} ref={(node) => { this.node = node; }}>
{ Object.keys(files).map((magnetUri) => { Object.entries(files).map(([ magnetUri, file ]) =>
<File key={magnetUri} magnetUri={magnetUri} /> {
)} 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> </div>
); );
} }
@ -58,14 +90,35 @@ class FileList extends React.PureComponent
FileList.propTypes = FileList.propTypes =
{ {
files : PropTypes.object.isRequired, files : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
picture : PropTypes.string,
peers : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { 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 : button :
{ {
margin : theme.spacing.unit margin : theme.spacing(1)
} }
}); });
@ -46,11 +46,11 @@ class FileSharing extends React.PureComponent
render() render()
{ {
const { const {
torrentSupport, canShareFiles,
classes classes
} = this.props; } = this.props;
const buttonDescription = torrentSupport ? const buttonDescription = canShareFiles ?
'Share file' : 'File sharing not supported'; 'Share file' : 'File sharing not supported';
return ( return (
@ -67,7 +67,7 @@ class FileSharing extends React.PureComponent
variant='contained' variant='contained'
component='span' component='span'
className={classes.button} className={classes.button}
disabled={!torrentSupport} disabled={!canShareFiles}
> >
{buttonDescription} {buttonDescription}
</Button> </Button>
@ -80,17 +80,17 @@ class FileSharing extends React.PureComponent
} }
FileSharing.propTypes = { FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
torrentSupport : PropTypes.bool.isRequired, canShareFiles : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired, tabOpen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
torrentSupport : state.room.torrentSupport, canShareFiles : state.me.canShareFiles,
tabOpen : state.toolarea.currentToolTab === 'files' tabOpen : state.toolarea.currentToolTab === 'files'
}; };
}; };

View File

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

View File

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

View File

@ -18,23 +18,23 @@ const styles = (theme) =>
{ {
width : '100%', width : '100%',
overflowY : 'auto', overflowY : 'auto',
padding : 6 padding : theme.spacing(1)
}, },
list : list :
{ {
listStyleType : 'none', listStyleType : 'none',
padding : theme.spacing.unit, padding : theme.spacing(1),
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)', boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)' backgroundColor : 'rgba(255, 255, 255, 1)'
}, },
listheader : listheader :
{ {
padding : '0.5rem', padding : theme.spacing(1),
fontWeight : 'bolder' fontWeight : 'bolder'
}, },
listItem : listItem :
{ {
padding : '0.5rem', padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'pointer', cursor : 'pointer',
@ -76,7 +76,7 @@ class ParticipantList extends React.PureComponent
roomClient, roomClient,
advancedMode, advancedMode,
passivePeers, passivePeers,
selectedPeerName, selectedPeerId,
spotlightPeers, spotlightPeers,
classes classes
} = this.props; } = this.props;
@ -92,14 +92,14 @@ class ParticipantList extends React.PureComponent
<li className={classes.listheader}>Participants in Spotlight:</li> <li className={classes.listheader}>Participants in Spotlight:</li>
{ spotlightPeers.map((peer) => ( { spotlightPeers.map((peer) => (
<li <li
key={peer.name} key={peer.id}
className={classNames(classes.listItem, { 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}> <ListPeer id={peer.id} advancedMode={advancedMode}>
<Volume small name={peer.name} /> <Volume small id={peer.id} />
</ListPeer> </ListPeer>
</li> </li>
))} ))}
@ -107,15 +107,15 @@ class ParticipantList extends React.PureComponent
<br /> <br />
<ul className={classes.list}> <ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li> <li className={classes.listheader}>Passive Participants:</li>
{ passivePeers.map((peerName) => ( { passivePeers.map((peerId) => (
<li <li
key={peerName} key={peerId}
className={classNames(classes.listItem, { 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> </li>
))} ))}
</ul> </ul>
@ -126,20 +126,20 @@ class ParticipantList extends React.PureComponent
ParticipantList.propTypes = ParticipantList.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
passivePeers : PropTypes.array, passivePeers : PropTypes.array,
selectedPeerName : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array, spotlightPeers : PropTypes.array,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
passivePeers : passivePeersSelector(state), passivePeers : passivePeersSelector(state),
selectedPeerName : state.room.selectedPeerName, selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state) spotlightPeers : spotlightPeersSelector(state)
}; };
}; };
@ -153,7 +153,7 @@ const ParticipantListContainer = withRoomContext(connect(
return ( return (
prev.peers === next.peers && prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights && 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 spotlightsLengthSelector
} from '../Selectors'; } from '../Selectors';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Peer from '../Containers/Peer'; import Peer from '../Containers/Peer';
import Me from '../Containers/Me'; import Me from '../Containers/Me';
import HiddenPeers from '../Containers/HiddenPeers'; import HiddenPeers from '../Containers/HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334; const RATIO = 1.334;
const PADDING_V = 50; const PADDING_V = 50;
@ -43,15 +41,14 @@ class Democratic extends React.PureComponent
{ {
super(props); super(props);
this.state = { this.state = {};
peerWidth : 400,
peerHeight : 300 this.resizeTimeout = null;
};
this.peersRef = React.createRef(); this.peersRef = React.createRef();
} }
updateDimensions = debounce(() => updateDimensions = () =>
{ {
if (!this.peersRef.current) if (!this.peersRef.current)
{ {
@ -93,14 +90,21 @@ class Democratic extends React.PureComponent
peerHeight : 0.9 * y peerHeight : 0.9 * y
}); });
} }
}, 200); };
componentDidMount() componentDidMount()
{ {
window.addEventListener('resize', this.updateDimensions); // window.resize event listener
const observer = new ResizeObserver(this.updateDimensions); 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() componentWillUnmount()
@ -108,9 +112,10 @@ class Democratic extends React.PureComponent
window.removeEventListener('resize', this.updateDimensions); window.removeEventListener('resize', this.updateDimensions);
} }
componentDidUpdate() componentDidUpdate(prevProps)
{ {
this.updateDimensions(); if (prevProps !== this.props)
this.updateDimensions();
} }
render() render()
@ -125,23 +130,25 @@ class Democratic extends React.PureComponent
const style = const style =
{ {
'width' : this.state.peerWidth, 'width' : this.state.peerWidth ? this.state.peerWidth : 0,
'height' : this.state.peerHeight 'height' : this.state.peerHeight ? this.state.peerHeight : 0
}; };
return ( return (
<div className={classes.root} ref={this.peersRef}> <div className={classes.root} ref={this.peersRef}>
<Me <Me
advancedMode={advancedMode} advancedMode={advancedMode}
spacing={6}
style={style} style={style}
/> />
{ spotlightsPeers.map((peer) => { spotlightsPeers.map((peer) =>
{ {
return ( return (
<Peer <Peer
key={peer.name} key={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
name={peer.name} id={peer.id}
spacing={6}
style={style} style={style}
/> />
); );

View File

@ -1,89 +1,54 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import debounce from 'lodash/debounce';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames'; import classnames from 'classnames';
import {
spotlightsLengthSelector,
videoBoxesSelector
} from '../Selectors';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import Me from '../Containers/Me';
import Peer from '../Containers/Peer'; import Peer from '../Containers/Peer';
import SpeakerPeer from '../Containers/SpeakerPeer';
import HiddenPeers from '../Containers/HiddenPeers'; import HiddenPeers from '../Containers/HiddenPeers';
import Grid from '@material-ui/core/Grid';
const styles = () => const styles = () =>
({ ({
root : root :
{ {
display : 'flex', height : '100%',
flexDirection : 'column', width : '100%',
alignItems : 'center', display : 'grid',
height : '100%', gridTemplateColumns : '1fr',
width : '100%' gridTemplateRows : '1.6fr minmax(0, 0.4fr)'
}, },
activePeerContainer : speaker :
{ {
width : '100%', gridArea : '1 / 1 / 2 / 2',
height : '80vh',
display : 'flex', display : 'flex',
justifyContent : 'center', justifyContent : 'center',
alignItems : 'center' alignItems : 'center',
}, paddingTop : 40
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
}, },
filmStrip : filmStrip :
{ {
display : 'flex', gridArea : '2 / 1 / 3 / 2'
background : 'rgba(0, 0, 0 , 0.5)',
width : '100%',
overflowX : 'auto',
height : '20vh',
alignItems : 'center'
}, },
filmStripContent : filmItem :
{ {
margin : '0 auto', display : 'flex',
display : 'flex', marginLeft : '6px',
height : '100%', border : 'var(--peer-border)',
alignItems : 'center'
},
film :
{
height : '18vh',
flexShrink : 0,
paddingLeft : '1vh',
'& .active' :
{
borderColor : 'var(--active-speaker-border-color)'
},
'&.selected' : '&.selected' :
{ {
borderColor : 'var(--selected-peer-border-color)' 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); super(props);
this.resizeTimeout = null;
this.activePeerContainer = React.createRef(); this.activePeerContainer = React.createRef();
this.filmStripContainer = React.createRef();
} }
state = { state = {
lastSpeaker : null, lastSpeaker : null
width : 400
}; };
// Find the name of the peer which is currently speaking. This is either // 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 // the latest active speaker, or the manually selected peer, or, if no
// person has spoken yet, the first peer in the list of peers. // 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; 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] && isSharingCamera = (peerId) => this.props.peers[peerId] &&
this.props.peers[peerName].consumers.some((consumer) => this.props.peers[peerId].consumers.some((consumer) =>
this.props.consumers[consumer].source === 'screen'); 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);
}
return ratio; let speakerHeight = (speakerWidth / 4) * 3;
};
updateDimensions = debounce(() => if (this.isSharingCamera(this.getActivePeerId()))
{
const container = this.activePeerContainer.current;
if (container)
{
const ratio = this.getRatio();
let width = container.clientWidth;
if (width / ratio > (container.clientHeight - 100))
{ {
width = (container.clientHeight - 100) * ratio; speakerWidth /= 2;
speakerHeight = (speakerWidth / 4) * 3;
} }
this.setState({ if (speakerHeight > (speaker.clientHeight - 60))
width {
}); speakerHeight = (speaker.clientHeight - 60);
speakerWidth = (speakerHeight / 3) * 4;
}
newState.speakerWidth = speakerWidth;
newState.speakerHeight = speakerHeight;
} }
}, 200);
const filmStrip = this.filmStripContainer.current;
if (filmStrip)
{
let filmStripHeight = filmStrip.clientHeight - 10;
let filmStripWidth = (filmStripHeight / 3) * 4;
if (filmStripWidth * this.props.boxes > (filmStrip.clientWidth - 50))
{
filmStripWidth = (filmStrip.clientWidth - 50) / this.props.boxes;
filmStripHeight = (filmStripWidth / 4) * 3;
}
newState.filmStripWidth = filmStripWidth;
newState.filmStripHeight = filmStripHeight;
}
this.setState({
...newState
});
};
componentDidMount() componentDidMount()
{ {
window.addEventListener('resize', this.updateDimensions); // window.resize event listener
const observer = new ResizeObserver(this.updateDimensions); 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(); this.updateDimensions();
} }
@ -175,19 +174,28 @@ class Filmstrip extends React.PureComponent
window.removeEventListener('resize', this.updateDimensions); 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) componentDidUpdate(prevProps)
{ {
if (prevProps !== this.props) if (prevProps !== this.props)
{ {
this.updateDimensions(); 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 { const {
roomClient, roomClient,
peers, peers,
myId,
advancedMode, advancedMode,
spotlights, spotlights,
spotlightsLength, spotlightsLength,
classes classes
} = this.props; } = 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 ( return (
<div className={classes.root}> <div className={classes.root}>
<div className={classes.activePeerContainer} ref={this.activePeerContainer}> <div className={classes.speaker} ref={this.activePeerContainer}>
{ peers[activePeerName] ? { peers[activePeerId] ?
<div <SpeakerPeer
className={classes.activePeer} advancedMode={advancedMode}
style={{ id={activePeerId}
width : this.state.width, style={speakerStyle}
height : this.state.width / this.getRatio() />
}}
>
<Peer
advancedMode={advancedMode}
name={activePeerName}
/>
</div>
:null :null
} }
</div> </div>
<div className={classes.filmStrip}> <div className={classes.filmStrip} ref={this.filmStripContainer}>
<div className={classes.filmStripContent}> <Grid container justify='center' spacing={0}>
{ Object.keys(peers).map((peerName) => <Grid item>
<div
className={classnames(classes.filmItem, {
active : myId === activePeerId
})}
>
<Me
advancedMode={advancedMode}
style={peerStyle}
smallButtons
/>
</div>
</Grid>
{ Object.keys(peers).map((peerId) =>
{ {
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) if (spotlights.find((spotlightsElement) => spotlightsElement === peerId))
{ {
return ( return (
<div <Grid key={peerId} item>
key={peerName} <div
onClick={() => roomClient.setSelectedPeer(peerName)} key={peerId}
className={classnames(classes.film, { onClick={() => roomClient.setSelectedPeer(peerId)}
selected : this.props.selectedPeerName === peerName, className={classnames(classes.filmItem, {
active : this.state.lastSpeaker === peerName selected : this.props.selectedPeerId === peerId,
})} active : peerId === activePeerId
> })}
<div className={classes.filmContent}> >
<Peer <Peer
advancedMode={advancedMode} advancedMode={advancedMode}
name={peerName} id={peerId}
style={peerStyle}
smallButtons
/> />
</div> </div>
</div> </Grid>
); );
} }
else else
@ -253,51 +283,63 @@ class Filmstrip extends React.PureComponent
return (''); return ('');
} }
})} })}
</div> </Grid>
</div>
<div className={classes.hiddenPeers}>
{ spotlightsLength<Object.keys(peers).length ?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>
:null
}
</div> </div>
{ spotlightsLength<Object.keys(peers).length ?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>
:null
}
</div> </div>
); );
} }
} }
Filmstrip.propTypes = { Filmstrip.propTypes = {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
activeSpeakerName : PropTypes.string, activeSpeakerId : PropTypes.string,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.object.isRequired, peers : PropTypes.object.isRequired,
consumers : PropTypes.object.isRequired, consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired, myId : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightsLength : PropTypes.number, spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired, spotlights : PropTypes.array.isRequired,
classes : PropTypes.object.isRequired boxes : PropTypes.number,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
return { return {
activeSpeakerName : state.room.activeSpeakerName, activeSpeakerId : state.room.activeSpeakerId,
selectedPeerName : state.room.selectedPeerName, selectedPeerId : state.room.selectedPeerId,
peers : state.peers, peers : state.peers,
consumers : state.consumers, consumers : state.consumers,
myName : state.me.name, myId : state.me.id,
spotlights : state.room.spotlights, spotlights : state.room.spotlights,
spotlightsLength spotlightsLength : spotlightsLengthSelector(state),
boxes : videoBoxesSelector(state),
}; };
}; };
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, 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))); )(withStyles(styles)(Filmstrip)));

View File

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

View File

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

View File

@ -43,7 +43,7 @@ const styles = (theme) =>
}, },
setting : setting :
{ {
padding : theme.spacing.unit * 2 padding : theme.spacing(2)
}, },
formControl : formControl :
{ {
@ -51,13 +51,13 @@ const styles = (theme) =>
} }
}); });
/* const modes = [ { const modes = [ {
value : 'democratic', value : 'democratic',
label : 'Democratic view' label : 'Democratic view'
}, { }, {
value : 'filmstrip', value : 'filmstrip',
label : 'Filmstrip view' label : 'Filmstrip view'
} ]; */ } ];
const resolutions = [ { const resolutions = [ {
value : 'low', value : 'low',
@ -87,6 +87,7 @@ const Settings = ({
settings, settings,
onToggleAdvancedMode, onToggleAdvancedMode,
handleCloseSettings, handleCloseSettings,
handleChangeMode,
classes classes
}) => }) =>
{ {
@ -203,6 +204,33 @@ const Settings = ({
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
</form> </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 <FormControlLabel
className={classes.setting} className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />} control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
@ -241,7 +269,7 @@ const mapStateToProps = (state) =>
const mapDispatchToProps = { const mapDispatchToProps = {
onToggleAdvancedMode : stateActions.toggleAdvancedMode, onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode, handleChangeMode : stateActions.setDisplayMode,
handleCloseSettings : stateActions.setSettingsOpen handleCloseSettings : stateActions.setSettingsOpen,
}; };
export default withRoomContext(connect( export default withRoomContext(connect(

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ export const Room = PropTypes.shape(
url : PropTypes.string.isRequired, url : PropTypes.string.isRequired,
state : PropTypes.oneOf( state : PropTypes.oneOf(
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired, [ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
activeSpeakerName : PropTypes.string activeSpeakerId : PropTypes.string
}); });
export const Device = PropTypes.shape( export const Device = PropTypes.shape(
@ -17,7 +17,7 @@ export const Device = PropTypes.shape(
export const Me = PropTypes.shape( export const Me = PropTypes.shape(
{ {
name : PropTypes.string.isRequired, id : PropTypes.string.isRequired,
device : Device.isRequired, device : Device.isRequired,
canSendMic : PropTypes.bool.isRequired, canSendMic : PropTypes.bool.isRequired,
canSendWebcam : PropTypes.bool.isRequired, canSendWebcam : PropTypes.bool.isRequired,
@ -26,30 +26,28 @@ export const Me = PropTypes.shape(
export const Producer = PropTypes.shape( export const Producer = PropTypes.shape(
{ {
id : PropTypes.number.isRequired, id : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired, source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
deviceLabel : PropTypes.string, deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]), type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
locallyPaused : PropTypes.bool.isRequired, paused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired, track : PropTypes.any,
track : PropTypes.any, codec : PropTypes.string.isRequired
codec : PropTypes.string.isRequired
}); });
export const Peer = PropTypes.shape( export const Peer = PropTypes.shape(
{ {
name : PropTypes.string.isRequired, id : PropTypes.string.isRequired,
displayName : PropTypes.string, displayName : PropTypes.string,
device : Device.isRequired, device : Device.isRequired,
consumers : PropTypes.arrayOf(PropTypes.number).isRequired consumers : PropTypes.arrayOf(PropTypes.string).isRequired
}); });
export const Consumer = PropTypes.shape( export const Consumer = PropTypes.shape(
{ {
id : PropTypes.number.isRequired, id : PropTypes.string.isRequired,
peerName : PropTypes.string.isRequired, peerId : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired, source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
supported : PropTypes.bool.isRequired,
locallyPaused : PropTypes.bool.isRequired, locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired, remotelyPaused : PropTypes.bool.isRequired,
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]), profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
@ -75,7 +73,7 @@ export const Message = PropTypes.shape(
export const FileEntryProps = PropTypes.shape( export const FileEntryProps = PropTypes.shape(
{ {
data : PropTypes.shape({ data : PropTypes.shape({
name : PropTypes.string.isRequired, id : PropTypes.string.isRequired,
picture : PropTypes.string, picture : PropTypes.string,
file : PropTypes.shape({ file : PropTypes.shape({
magnet : PropTypes.string.isRequired 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 domready from 'domready';
import UrlParse from 'url-parse';
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getDeviceInfo } from 'mediasoup-client';
import randomString from 'random-string'; import randomString from 'random-string';
import Logger from './Logger'; import Logger from './Logger';
import debug from 'debug'; import debug from 'debug';
import RoomClient from './RoomClient'; import RoomClient from './RoomClient';
import RoomContext from './RoomContext'; import RoomContext from './RoomContext';
import deviceInfo from './deviceInfo';
import * as stateActions from './actions/stateActions'; import * as stateActions from './actions/stateActions';
import Room from './components/Room'; import Room from './components/Room';
import LoadingView from './components/LoadingView'; import LoadingView from './components/LoadingView';
@ -44,57 +43,48 @@ function run()
{ {
logger.debug('run() [environment:%s]', process.env.NODE_ENV); logger.debug('run() [environment:%s]', process.env.NODE_ENV);
const peerName = randomString({ length: 8 }).toLowerCase(); const peerId = randomString({ length: 8 }).toLowerCase();
const urlParser = new UrlParse(window.location.href, true); const urlParser = new URL(window.location);
const parameters = urlParser.searchParams;
let roomId = (urlParser.pathname).substr(1) 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';
if (!roomId) if (!roomId)
roomId = parameters.get('roomId');
if (roomId)
roomId = roomId.toLowerCase();
else
{ {
roomId = randomString({ length: 8 }).toLowerCase(); roomId = randomString({ length: 8 }).toLowerCase();
urlParser.query.roomId = roomId; parameters.set('roomId', roomId);
window.history.pushState('', '', urlParser.toString()); window.history.pushState('', '', urlParser.toString());
} }
// Get the effective/shareable Room URL. const produce = parameters.get('produce') !== 'false';
const roomUrlParser = new UrlParse(window.location.href, true); 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)) const roomUrl = window.location.href.split('?')[0];
{
// 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();
// Get current device. // Get current device.
const device = getDeviceInfo(); const device = deviceInfo();
store.dispatch( store.dispatch(
stateActions.setRoomUrl(roomUrl)); stateActions.setRoomUrl(roomUrl));
store.dispatch( store.dispatch(
stateActions.setMe({ stateActions.setMe({
peerName, peerId,
device, device,
loginEnabled : window.config.loginEnabled loginEnabled : window.config.loginEnabled
}) })
); );
roomClient = new RoomClient( roomClient = new RoomClient(
{ roomId, peerName, device, useSimulcast, produce }); { roomId, peerId, device, useSimulcast, produce, consume, forceTcp });
global.CLIENT = roomClient; global.CLIENT = roomClient;

View File

@ -51,11 +51,30 @@ const consumers = (state = initialState, action) =>
return { ...state, [consumerId]: newConsumer }; 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 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 }; return { ...state, [consumerId]: newConsumer };
} }
@ -69,6 +88,19 @@ const consumers = (state = initialState, action) =>
return { ...state, [consumerId]: newConsumer }; 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: default:
return state; return state;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
export function getSignalingUrl(peerName, roomId) export function getSignalingUrl(peerId, roomId)
{ {
const hostname = window.location.hostname; const hostname = window.location.hostname;
const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.location.port; 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; return url;
} }

View File

@ -1,15 +1,29 @@
const os = require('os');
module.exports = module.exports =
{ {
// oAuth2 conf // oAuth2 conf
oauth2 : auth :
{ {
clientID : '', /*
clientSecret : '', The issuer URL for OpenID Connect discovery
callbackURL : 'https://mYDomainName:port/auth-callback' 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`. // session cookie secret
domain : 'localhost', cookieSecret : 'T0P-S3cR3t_cook!e',
tls : tls :
{ {
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
@ -19,59 +33,61 @@ module.exports =
// Any http request is redirected to https. // Any http request is redirected to https.
// Listening port for http server. // Listening port for http server.
listeningRedirectPort : 80, listeningRedirectPort : 80,
// STUN/TURN // Mediasoup settings
mediasoup : mediasoup :
{ {
// mediasoup Server settings. numWorkers : Object.keys(os.cpus()).length,
logLevel : 'warn', // mediasoup Worker settings.
logTags : worker :
[ {
'info', logLevel : 'warn',
'ice', logTags :
'dtls', [
'rtp', 'info',
'srtp', 'ice',
'rtcp', 'dtls',
'rbe', 'rtp',
'rtx' 'srtp',
], 'rtcp'
rtcIPv4 : true, ],
rtcIPv6 : true, rtcMinPort : 40000,
rtcAnnouncedIPv4 : null, rtcMaxPort : 49999
rtcAnnouncedIPv6 : null, },
rtcMinPort : 40000, // mediasoup Router settings.
rtcMaxPort : 49999, router :
// mediasoup Room codecs. {
mediaCodecs : // Router media codecs.
[ mediaCodecs :
{ [
kind : 'audio',
name : 'opus',
clockRate : 48000,
channels : 2,
parameters :
{ {
useinbandfec : 1 kind : 'audio',
} mimeType : 'audio/opus',
}, clockRate : 48000,
// { channels : 2
// kind : 'video', },
// name : 'VP8',
// clockRate : 90000
// }
{
kind : 'video',
name : 'H264',
clockRate : 90000,
parameters :
{ {
'packetization-mode' : 1, kind : 'video',
'profile-level-id' : '42e01f', mimeType : 'video/h264',
'level-asymmetry-allowed' : 1 clockRate : 90000,
parameters :
{
'packetization-mode' : 1,
'profile-level-id' : '42e01f',
'level-asymmetry-allowed' : 1,
'x-google-start-bitrate' : 1000
}
} }
} ]
], },
// mediasoup per Peer max sending bitrate (in bps). // mediasoup WebRtcTransport settings.
maxBitrate : 500000 webRtcTransport :
{
listenIps :
[
{ ip: '1.2.3.4', announcedIp: null }
],
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, Object.assign({}, baseEvent,
{ {
event : 'room.newpeer', event : 'room.newpeer',
peerName : peer.name, peerId : peer.id,
rtpCapabilities : peer.rtpCapabilities rtpCapabilities : peer.rtpCapabilities
}), }),
stream); stream);
@ -67,7 +67,7 @@ function handlePeer(peer, baseEvent, stream)
{ {
baseEvent = Object.assign({}, baseEvent, baseEvent = Object.assign({}, baseEvent,
{ {
peerName : peer.name peerId : peer.id
}); });
peer.on('close', (originator) => peer.on('close', (originator) =>

View File

@ -1,20 +1,24 @@
{ {
"name": "multiparty-meeting-server", "name": "multiparty-meeting-server",
"version": "2.0.0", "version": "3.0.0",
"private": true, "private": true,
"description": "multiparty meeting server", "description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "main": "lib/index.js",
"dependencies": { "dependencies": {
"awaitqueue": "^1.0.0",
"base-64": "^0.1.0", "base-64": "^0.1.0",
"colors": "^1.1.2", "colors": "^1.1.2",
"compression": "^1.7.3", "compression": "^1.7.3",
"debug": "^4.1.0", "debug": "^4.1.0",
"express": "^4.16.3", "express": "^4.16.3",
"mediasoup": "^2.6.11", "express-session": "^1.16.1",
"passport-dataporten": "^1.3.0", "mediasoup": "^3.0.12",
"socket.io": "^2.1.1" "openid-client": "^2.5.0",
"passport": "^0.4.0",
"socket.io": "^2.1.1",
"spdy": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"gulp": "^4.0.0", "gulp": "^4.0.0",

View File

@ -6,15 +6,20 @@ process.title = 'multiparty-meeting-server';
const config = require('./config/config'); const config = require('./config/config');
const fs = require('fs'); const fs = require('fs');
const https = require('https');
const http = require('http'); const http = require('http');
const spdy = require('spdy');
const express = require('express'); const express = require('express');
const compression = require('compression'); const compression = require('compression');
const mediasoup = require('mediasoup');
const AwaitQueue = require('awaitqueue');
const Logger = require('./lib/Logger'); const Logger = require('./lib/Logger');
const Room = require('./lib/Room'); const Room = require('./lib/Room');
const Dataporten = require('passport-dataporten');
const utils = require('./util'); const utils = require('./util');
const base64 = require('base-64'); 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 */ /* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG); 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); console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
/* eslint-enable no-console */ /* eslint-enable no-console */
// Start the mediasoup server.
const mediaServer = require('./mediasoup');
const logger = new Logger(); 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. // Map of Room instances indexed by roomId.
const rooms = new Map(); const rooms = new Map();
@ -38,147 +50,377 @@ const tls =
}; };
const app = express(); const app = express();
let httpsServer;
let oidcClient;
let oidcStrategy;
app.use(compression()); passport.serializeUser((user, done) =>
const dataporten = new Dataporten.Setup(config.oauth2);
app.all('*', (req, res, next) =>
{ {
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(() =>
app.use(dataporten.passport.initialize());
app.use(dataporten.passport.session());
app.get('/login', (req, res, next) =>
{
dataporten.passport.authenticate('dataporten', {
state : base64.encode(JSON.stringify({
roomId : req.query.roomId,
peerName : req.query.peerName,
code : utils.random(10)
}))
})(req, res, next);
});
dataporten.setupLogout(app, '/logout');
app.get('/', (req, res) =>
{
res.sendFile(`${__dirname}/public/chooseRoom.html`);
});
app.get(
'/auth-callback',
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
(req, res) =>
{ {
const state = JSON.parse(base64.decode(req.query.state)); for (const room of rooms.values())
if (rooms.has(state.roomId))
{ {
const data = room.logStatus();
}
}, 120000);
}
async function setupAuth(oidcIssuer)
{
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 =
{ {
peerName : state.peerName, id : tokenset.claims.sub,
name : req.user.data.displayName, provider : tokenset.claims.iss,
picture : req.user.data.photos[0] _userinfo : userinfo,
_claims : tokenset.claims
}; };
const room = rooms.get(state.roomId); 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 } ];
}
}
room.authCallback(data); 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,
peerId : req.query.peerId,
code : utils.random(10)
}))
})(req, res, next);
});
// logout
app.get('/auth/logout', (req, res) =>
{
req.logout();
res.redirect('/');
});
// 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 =
{
peerId : state.peerId,
displayName : displayName,
picture : photo
};
const room = rooms.get(state.roomId);
room.authCallback(data);
}
res.send('');
}
);
}
async function runHttpsServer()
{
app.use(compression());
app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge'));
app.all('*', (req, res, next) =>
{
if (req.secure)
{
return next();
} }
res.send(''); res.redirect(`https://${req.hostname}${req.url}`);
} });
);
// Serve all files in the public folder as static files. app.get('/', (req, res) =>
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', () =>
{
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);
});
const io = require('socket.io')(httpsServer);
// Handle connections from clients.
io.on('connection', (socket) =>
{
const { roomId, peerName } = socket.handshake.query;
if (!roomId || !peerName)
{ {
logger.warn('connection request without roomId and/or peerName'); res.sendFile(`${__dirname}/public/chooseRoom.html`);
});
socket.disconnect(true); // Serve all files in the public folder as static files.
app.use(express.static('public'));
return; app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));
}
logger.info( httpsServer = spdy.createServer(tls, app);
'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName);
let room; httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
// If an unknown roomId, create a new Room.
if (!rooms.has(roomId))
{ {
logger.info('creating a new Room [roomId:"%s"]', roomId); logger.info('Server running on port: ', config.listeningPort);
});
try const httpServer = http.createServer(app);
{
room = new Room(roomId, mediaServer, io);
global.APP_ROOM = room; httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
} {
catch (error) 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.error('error creating a new Room: %s', error); logger.warn('connection request without roomId and/or peerId');
socket.disconnect(true); socket.disconnect(true);
return; return;
} }
const logStatusTimer = setInterval(() => logger.info(
'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
queue.push(async () =>
{ {
room.logStatus(); const room = await getOrCreateRoom({ roomId });
}, 30000);
room.handleConnection({ peerId, socket });
})
.catch((error) =>
{
logger.error('room creation or room joining failed:%o', error);
socket.disconnect(true);
return;
});
});
}
/**
* 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)
{
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); rooms.set(roomId, room);
room.on('close', () => rooms.delete(roomId));
room.on('close', () =>
{
rooms.delete(roomId);
clearInterval(logStatusTimer);
});
}
else
{
room = rooms.get(roomId);
} }
socket.room = roomId; return room;
}
room.handleConnection(peerName, socket); run();
});