Merge branch 'develop'
commit
a4a7fbe4f7
|
|
@ -1,10 +1,11 @@
|
|||
node_modules/
|
||||
|
||||
/app/build/
|
||||
/app/public/config.js
|
||||
/app/public/config/config.js
|
||||
/app/public/images/logo.*
|
||||
/server/config/
|
||||
!/server/config/config.example.js
|
||||
/server/public/
|
||||
/server/certs/
|
||||
!/server/certs/mediasoup-demo.localhost.*
|
||||
.vscode
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,5 +1,23 @@
|
|||
# Changelog
|
||||
|
||||
### 3.0
|
||||
* Updated to mediasoup v3
|
||||
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)
|
||||
- OpenID Connect discovery
|
||||
- Auth code flow
|
||||
* Add spdy http2 support.
|
||||
- Notice it does not supports node 11.x
|
||||
* Updated to Material UI v4
|
||||
|
||||
### 2.0
|
||||
* Material UI
|
||||
* Separate settings for lastN for desktop and mobile
|
||||
|
||||
### 1.2
|
||||
* Add Lock Room feature
|
||||
* Fix suspended Web Audio context / fixed delayed getUsermedia
|
||||
* Added support for the new getdisplaymedia API in Chrome 72
|
||||
|
||||
### 1.1
|
||||
* Moved Filesharing code out from React code to RoomClient
|
||||
* Major cleanup of CSS. Variables for most colors and sizes exposed in :root
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -9,7 +9,7 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci
|
|||
* Chat
|
||||
* Screen sharing
|
||||
* File sharing
|
||||
* Different video layouts
|
||||
* Different layouts
|
||||
|
||||
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no.
|
||||
|
||||
|
|
@ -25,16 +25,16 @@ $ git clone https://github.com/havfo/multiparty-meeting.git
|
|||
$ cd multiparty-meeting
|
||||
```
|
||||
|
||||
* Copy `server/config.example.js` to `server/config.js` :
|
||||
* Copy `server/config/config.example.js` to `server/config/config.js` :
|
||||
|
||||
```bash
|
||||
$ cp server/config.example.js server/config.js
|
||||
$ cp server/config/config.example.js server/config/config.js
|
||||
```
|
||||
|
||||
* Copy `app/public/config.example.js` to `app/public/config.js` :
|
||||
* Copy `app/public/config/config.example.js` to `app/public/config/config.js` :
|
||||
|
||||
```bash
|
||||
$ cp app/public/config.example.js app/public/config.js
|
||||
$ cp app/public/config/config.example.js app/public/config/config.js
|
||||
```
|
||||
|
||||
* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).
|
||||
|
|
|
|||
|
|
@ -1,47 +1,43 @@
|
|||
{
|
||||
"name": "multiparty-meeting",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting service",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^3.9.2",
|
||||
"@material-ui/icons": "^3.0.2",
|
||||
"@material-ui/core": "^4.1.2",
|
||||
"@material-ui/icons": "^4.2.1",
|
||||
"bowser": "^2.4.0",
|
||||
"create-torrent": "^3.33.0",
|
||||
"domready": "^1.0.8",
|
||||
"file-saver": "^2.0.1",
|
||||
"hark": "^1.2.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.6.1",
|
||||
"mediasoup-client": "^2.4.10",
|
||||
"mediasoup-client": "^3.0.6",
|
||||
"notistack": "^0.5.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^16.8.5",
|
||||
"react-cookie-consent": "^2.2.2",
|
||||
"react-dom": "^16.8.5",
|
||||
"react-draggable": "^3.2.1",
|
||||
"react-redux": "^6.0.1",
|
||||
"react-scripts": "2.1.8",
|
||||
"react-tooltip": "^3.10.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^5.10.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"riek": "^1.1.0",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"source-map-explorer": "^1.8.0",
|
||||
"url-parse": "^1.4.4",
|
||||
"webtorrent": "^0.103.1"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze-main": "source-map-explorer build/static/js/main.*",
|
||||
"analyze-chunk": "source-map-explorer build/static/js/2.*",
|
||||
"start": "HTTPS=true PORT=4443 react-scripts start",
|
||||
"build": "react-scripts build && rm -rf ../server/public/* && cp -r build/* ../server/public/",
|
||||
"build": "react-scripts build && mkdir -p ../server/public && rm -rf ../server/public/* && cp -r build/* ../server/public/",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
|
|
@ -169,7 +165,12 @@
|
|||
"no-case-declarations": 2,
|
||||
"no-catch-shadow": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-confusing-arrow": ["error", {"allowParens": true}],
|
||||
"no-confusing-arrow": [
|
||||
"error",
|
||||
{
|
||||
"allowParens": true
|
||||
}
|
||||
],
|
||||
"no-console": 2,
|
||||
"no-const-assign": 2,
|
||||
"no-debugger": 2,
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@
|
|||
<meta name='description' content='multiparty meeting - Simple web meetings'>
|
||||
<meta name='theme-color' content='#000000' />
|
||||
|
||||
<link rel='chrome-webstore-item' href='https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'>
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
|
||||
<link rel='preconnect' href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
|
||||
<link rel='shortcut icon' href='%PUBLIC_URL%/favicon.ico' />
|
||||
<link rel='manifest' href='%PUBLIC_URL%/manifest.json' />
|
||||
|
||||
<title>Multiparty Meeting</title>
|
||||
|
||||
<script src='%PUBLIC_URL%/config.js' type='text/javascript'></script>
|
||||
<script src='%PUBLIC_URL%/config/config.js' type='text/javascript'></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,113 +1,4 @@
|
|||
class ChromeScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = { })
|
||||
{
|
||||
const state = this;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
window.addEventListener('message', _onExtensionMessage, false);
|
||||
window.postMessage({ type: 'getStreamId' }, '*');
|
||||
|
||||
function _onExtensionMessage({ data })
|
||||
{
|
||||
if (data.type !== 'gotStreamId')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const constraints = state._toConstraints(options, data.streamId);
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
window.removeEventListener('message', _onExtensionMessage);
|
||||
|
||||
state._stream = stream;
|
||||
resolve(stream);
|
||||
})
|
||||
.catch((err) =>
|
||||
{
|
||||
window.removeEventListener('message', _onExtensionMessage);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_toConstraints(options, streamId)
|
||||
{
|
||||
const constraints = {
|
||||
video : {
|
||||
mandatory : {
|
||||
chromeMediaSource : 'desktop',
|
||||
chromeMediaSourceId : streamId
|
||||
},
|
||||
optional : [ {
|
||||
googTemporalLayeredScreencast : true
|
||||
} ]
|
||||
},
|
||||
audio : false
|
||||
};
|
||||
|
||||
if (isFinite(options.width))
|
||||
{
|
||||
constraints.video.mandatory.maxWidth = options.width;
|
||||
constraints.video.mandatory.minWidth = options.width;
|
||||
}
|
||||
if (isFinite(options.height))
|
||||
{
|
||||
constraints.video.mandatory.maxHeight = options.height;
|
||||
constraints.video.mandatory.minHeight = options.height;
|
||||
}
|
||||
if (isFinite(options.frameRate))
|
||||
{
|
||||
constraints.video.mandatory.maxFrameRate = options.frameRate;
|
||||
constraints.video.mandatory.minFrameRate = options.frameRate;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class Chrome72ScreenShare
|
||||
class DisplayMediaScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
|
|
@ -143,11 +34,6 @@ class Chrome72ScreenShare
|
|||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
|
|
@ -194,11 +80,6 @@ class FirefoxScreenShare
|
|||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints(options)
|
||||
{
|
||||
const constraints = {
|
||||
|
|
@ -238,119 +119,12 @@ class FirefoxScreenShare
|
|||
}
|
||||
}
|
||||
|
||||
class Firefox66ScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.mediaDevices.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : true
|
||||
};
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class EdgeScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : true
|
||||
};
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultScreenShare
|
||||
{
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ScreenShare
|
||||
|
|
@ -364,18 +138,15 @@ export default class ScreenShare
|
|||
if (device.version < 66.0)
|
||||
return new FirefoxScreenShare();
|
||||
else
|
||||
return new Firefox66ScreenShare();
|
||||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
case 'chrome':
|
||||
{
|
||||
if (device.version < 72.0)
|
||||
return new ChromeScreenShare();
|
||||
else
|
||||
return new Chrome72ScreenShare();
|
||||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
case 'msedge':
|
||||
{
|
||||
return new EdgeScreenShare();
|
||||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
default:
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ const logger = new Logger('Spotlight');
|
|||
|
||||
export default class Spotlights extends EventEmitter
|
||||
{
|
||||
constructor(maxSpotlights, room)
|
||||
constructor(maxSpotlights, signalingSocket)
|
||||
{
|
||||
super();
|
||||
|
||||
this._room = room;
|
||||
this._signalingSocket = signalingSocket;
|
||||
this._maxSpotlights = maxSpotlights;
|
||||
this._peerList = [];
|
||||
this._selectedSpotlights = [];
|
||||
|
|
@ -19,24 +19,25 @@ export default class Spotlights extends EventEmitter
|
|||
|
||||
start()
|
||||
{
|
||||
const peers = this._room.peers;
|
||||
|
||||
for (const peer of peers)
|
||||
{
|
||||
this._handlePeer(peer);
|
||||
}
|
||||
|
||||
this._handleRoom();
|
||||
this._handleSignaling();
|
||||
|
||||
this._started = true;
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
peerInSpotlights(peerName)
|
||||
addPeers(peers)
|
||||
{
|
||||
for (const peer of peers)
|
||||
{
|
||||
this._newPeer(peer.id);
|
||||
}
|
||||
}
|
||||
|
||||
peerInSpotlights(peerId)
|
||||
{
|
||||
if (this._started)
|
||||
{
|
||||
return this._currentSpotlights.indexOf(peerName) !== -1;
|
||||
return this._currentSpotlights.indexOf(peerId) !== -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -44,11 +45,11 @@ export default class Spotlights extends EventEmitter
|
|||
}
|
||||
}
|
||||
|
||||
setPeerSpotlight(peerName)
|
||||
setPeerSpotlight(peerId)
|
||||
{
|
||||
logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName);
|
||||
logger.debug('setPeerSpotlight() [peerId:"%s"]', peerId);
|
||||
|
||||
const index = this._selectedSpotlights.indexOf(peerName);
|
||||
const index = this._selectedSpotlights.indexOf(peerId);
|
||||
|
||||
if (index !== -1)
|
||||
{
|
||||
|
|
@ -56,13 +57,13 @@ export default class Spotlights extends EventEmitter
|
|||
}
|
||||
else
|
||||
{
|
||||
this._selectedSpotlights = [ peerName ];
|
||||
this._selectedSpotlights = [ peerId ];
|
||||
}
|
||||
|
||||
/*
|
||||
if (index === -1) // We don't have this peer in the list, adding
|
||||
{
|
||||
this._selectedSpotlights.push(peerName);
|
||||
this._selectedSpotlights.push(peerId);
|
||||
}
|
||||
else // We have this peer, remove
|
||||
{
|
||||
|
|
@ -74,16 +75,65 @@ export default class Spotlights extends EventEmitter
|
|||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
_handleRoom()
|
||||
_handleSignaling()
|
||||
{
|
||||
this._room.on('newpeer', (peer) =>
|
||||
this._signalingSocket.on('notification', (notification) =>
|
||||
{
|
||||
logger.debug(
|
||||
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
||||
this._handlePeer(peer);
|
||||
if (notification.method === 'newPeer')
|
||||
{
|
||||
const { id } = notification.data;
|
||||
|
||||
this._newPeer(id);
|
||||
}
|
||||
|
||||
if (notification.method === 'peerClosed')
|
||||
{
|
||||
const { peerId } = notification.data;
|
||||
|
||||
this._closePeer(peerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_newPeer(id)
|
||||
{
|
||||
logger.debug(
|
||||
'room "newpeer" event [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)
|
||||
{
|
||||
this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ];
|
||||
|
|
@ -92,49 +142,16 @@ export default class Spotlights extends EventEmitter
|
|||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
_handlePeer(peer)
|
||||
handleActiveSpeaker(peerId)
|
||||
{
|
||||
logger.debug('_handlePeer() [peerName:"%s"]', peer.name);
|
||||
logger.debug('handleActiveSpeaker() [peerId:"%s"]', peerId);
|
||||
|
||||
if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list
|
||||
{
|
||||
peer.on('close', () =>
|
||||
{
|
||||
let index = this._peerList.indexOf(peer.name);
|
||||
|
||||
if (index !== -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._peerList.splice(index, 1);
|
||||
}
|
||||
|
||||
index = this._selectedSpotlights.indexOf(peer.name);
|
||||
|
||||
if (index !== -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._selectedSpotlights.splice(index, 1);
|
||||
}
|
||||
|
||||
this._spotlightsUpdated();
|
||||
});
|
||||
|
||||
logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name);
|
||||
|
||||
this._peerList.push(peer.name);
|
||||
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
handleActiveSpeaker(peerName)
|
||||
{
|
||||
logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName);
|
||||
|
||||
const index = this._peerList.indexOf(peerName);
|
||||
const index = this._peerList.indexOf(peerId);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this._peerList.splice(index, 1);
|
||||
this._peerList = [ peerName ].concat(this._peerList);
|
||||
this._peerList = [ peerId ].concat(this._peerList);
|
||||
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ export const setRoomState = (state) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setRoomActiveSpeaker = (peerName) =>
|
||||
export const setRoomActiveSpeaker = (peerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_ACTIVE_SPEAKER',
|
||||
payload : { peerName }
|
||||
payload : { peerId }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -43,41 +43,30 @@ export const setRoomLockedOut = () =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setAudioSuspended = ({ audioSuspended }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_SUSPENDED',
|
||||
payload : { audioSuspended }
|
||||
};
|
||||
};
|
||||
|
||||
export const setSettingsOpen = ({ settingsOpen }) =>
|
||||
({
|
||||
type : 'SET_SETTINGS_OPEN',
|
||||
payload : { settingsOpen }
|
||||
});
|
||||
|
||||
export const setMe = ({ peerName, device, loginEnabled }) =>
|
||||
export const setMe = ({ peerId, device, loginEnabled }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ME',
|
||||
payload : { peerName, device, loginEnabled }
|
||||
payload : { peerId, device, loginEnabled }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) =>
|
||||
export const setMediaCapabilities = ({
|
||||
canSendMic,
|
||||
canSendWebcam,
|
||||
canShareScreen,
|
||||
canShareFiles
|
||||
}) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MEDIA_CAPABILITIES',
|
||||
payload : { canSendMic, canSendWebcam }
|
||||
};
|
||||
};
|
||||
|
||||
export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_SCREEN_CAPABILITIES',
|
||||
payload : { canShareScreen, needExtension }
|
||||
payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -150,27 +139,27 @@ export const setDisplayMode = (mode) =>
|
|||
payload : { mode }
|
||||
});
|
||||
|
||||
export const setPeerVideoInProgress = (peerName, flag) =>
|
||||
export const setPeerVideoInProgress = (peerId, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_VIDEO_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
payload : { peerId, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerAudioInProgress = (peerName, flag) =>
|
||||
export const setPeerAudioInProgress = (peerId, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_AUDIO_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
payload : { peerId, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerScreenInProgress = (peerName, flag) =>
|
||||
export const setPeerScreenInProgress = (peerId, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_SCREEN_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
payload : { peerId, flag }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -226,11 +215,11 @@ export const setMyRaiseHandStateInProgress = (flag) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setPeerRaiseHandState = (peerName, raiseHandState) =>
|
||||
export const setPeerRaiseHandState = (peerId, raiseHandState) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_RAISE_HAND_STATE',
|
||||
payload : { peerName, raiseHandState }
|
||||
payload : { peerId, raiseHandState }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -274,6 +263,14 @@ export const setProducerTrack = (producerId, track) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setProducerScore = (producerId, score) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_SCORE',
|
||||
payload : { producerId, score }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
|
|
@ -306,35 +303,35 @@ export const addPeer = (peer) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const removePeer = (peerName) =>
|
||||
export const removePeer = (peerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PEER',
|
||||
payload : { peerName }
|
||||
payload : { peerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerDisplayName = (displayName, peerName) =>
|
||||
export const setPeerDisplayName = (displayName, peerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_DISPLAY_NAME',
|
||||
payload : { displayName, peerName }
|
||||
payload : { displayName, peerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const addConsumer = (consumer, peerName) =>
|
||||
export const addConsumer = (consumer, peerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_CONSUMER',
|
||||
payload : { consumer, peerName }
|
||||
payload : { consumer, peerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeConsumer = (consumerId, peerName) =>
|
||||
export const removeConsumer = (consumerId, peerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_CONSUMER',
|
||||
payload : { consumerId, peerName }
|
||||
payload : { consumerId, peerId }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -354,11 +351,19 @@ export const setConsumerResumed = (consumerId, originator) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setConsumerEffectiveProfile = (consumerId, profile) =>
|
||||
export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_EFFECTIVE_PROFILE',
|
||||
payload : { consumerId, profile }
|
||||
type : 'SET_CONSUMER_CURRENT_LAYERS',
|
||||
payload : { consumerId, spatialLayer, temporalLayer }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_PREFERRED_LAYERS',
|
||||
payload : { consumerId, spatialLayer, temporalLayer }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -370,11 +375,19 @@ export const setConsumerTrack = (consumerId, track) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setPeerVolume = (peerName, volume) =>
|
||||
export const setConsumerScore = (consumerId, score) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_SCORE',
|
||||
payload : { consumerId, score }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerVolume = (peerId, volume) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_VOLUME',
|
||||
payload : { peerName, volume }
|
||||
payload : { peerId, volume }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -482,11 +495,11 @@ export const dropMessages = () =>
|
|||
};
|
||||
};
|
||||
|
||||
export const addFile = (file) =>
|
||||
export const addFile = (peerId, magnetUri) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_FILE',
|
||||
payload : { file }
|
||||
payload : { peerId, magnetUri }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -536,10 +549,10 @@ export const setPicture = (picture) =>
|
|||
payload : { picture }
|
||||
});
|
||||
|
||||
export const setPeerPicture = (peerName, picture) =>
|
||||
export const setPeerPicture = (peerId, picture) =>
|
||||
({
|
||||
type : 'SET_PEER_PICTURE',
|
||||
payload : { peerName, picture }
|
||||
payload : { peerId, picture }
|
||||
});
|
||||
|
||||
export const loggedIn = () =>
|
||||
|
|
@ -547,10 +560,15 @@ export const loggedIn = () =>
|
|||
type : 'LOGGED_IN'
|
||||
});
|
||||
|
||||
export const setSelectedPeer = (selectedPeerName) =>
|
||||
export const toggleJoined = () =>
|
||||
({
|
||||
type : 'TOGGLE_JOINED'
|
||||
});
|
||||
|
||||
export const setSelectedPeer = (selectedPeerId) =>
|
||||
({
|
||||
type : 'SET_SELECTED_PEER',
|
||||
payload : { selectedPeerName }
|
||||
payload : { selectedPeerId }
|
||||
});
|
||||
|
||||
export const setSpotlights = (spotlights) =>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { meProducersSelector } from '../Selectors';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import VideoView from '../VideoContainers/VideoView';
|
||||
import Volume from './Volume';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
import VideoIcon from '@material-ui/icons/Videocam';
|
||||
import VideoOffIcon from '@material-ui/icons/VideocamOff';
|
||||
import ScreenIcon from '@material-ui/icons/ScreenShare';
|
||||
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
|
||||
|
||||
const styles = () =>
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
flexDirection : 'row',
|
||||
margin : 6,
|
||||
flex : '0 0 auto',
|
||||
boxShadow : 'var(--peer-shadow)',
|
||||
border : 'var(--peer-border)',
|
||||
|
|
@ -23,38 +30,88 @@ const styles = () =>
|
|||
backgroundPosition : 'bottom',
|
||||
backgroundSize : 'auto 85%',
|
||||
backgroundRepeat : 'no-repeat',
|
||||
'&.webcam' :
|
||||
{
|
||||
order : 1
|
||||
},
|
||||
'&.screen' :
|
||||
{
|
||||
order : 2
|
||||
},
|
||||
'&.hover' :
|
||||
{
|
||||
boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)'
|
||||
},
|
||||
'&.active-speaker' :
|
||||
{
|
||||
borderColor : 'var(--active-speaker-border-color)'
|
||||
}
|
||||
},
|
||||
fab :
|
||||
{
|
||||
margin : theme.spacing(1),
|
||||
pointerEvents : 'auto'
|
||||
},
|
||||
viewContainer :
|
||||
{
|
||||
position : 'relative',
|
||||
'&.webcam' :
|
||||
position : 'relative',
|
||||
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 [ hover, setHover ] = useState(false);
|
||||
|
||||
let touchTimeout = null;
|
||||
|
||||
const {
|
||||
roomClient,
|
||||
me,
|
||||
settings,
|
||||
activeSpeaker,
|
||||
spacing,
|
||||
style,
|
||||
smallButtons,
|
||||
advancedMode,
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
screenProducer,
|
||||
classes
|
||||
classes,
|
||||
theme
|
||||
} = props;
|
||||
|
||||
const videoVisible = (
|
||||
|
|
@ -69,17 +126,225 @@ const Me = (props) =>
|
|||
!screenProducer.remotelyPaused
|
||||
);
|
||||
|
||||
let micState;
|
||||
|
||||
let micTip;
|
||||
|
||||
if (!me.canSendMic)
|
||||
{
|
||||
micState = 'unsupported';
|
||||
micTip = 'Audio unsupported';
|
||||
}
|
||||
else if (!micProducer)
|
||||
{
|
||||
micState = 'off';
|
||||
micTip = 'Activate audio';
|
||||
}
|
||||
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
|
||||
{
|
||||
micState = 'on';
|
||||
micTip = 'Mute audio';
|
||||
}
|
||||
else
|
||||
{
|
||||
micState = 'muted';
|
||||
micTip = 'Unmute audio';
|
||||
}
|
||||
|
||||
let webcamState;
|
||||
|
||||
let webcamTip;
|
||||
|
||||
if (!me.canSendWebcam)
|
||||
{
|
||||
webcamState = 'unsupported';
|
||||
webcamTip = 'Video unsupported';
|
||||
}
|
||||
else if (webcamProducer)
|
||||
{
|
||||
webcamState = 'on';
|
||||
webcamTip = 'Stop video';
|
||||
}
|
||||
else
|
||||
{
|
||||
webcamState = 'off';
|
||||
webcamTip = 'Start video';
|
||||
}
|
||||
|
||||
let screenState;
|
||||
|
||||
let screenTip;
|
||||
|
||||
if (!me.canShareScreen)
|
||||
{
|
||||
screenState = 'unsupported';
|
||||
screenTip = 'Screen sharing not supported';
|
||||
}
|
||||
else if (screenProducer)
|
||||
{
|
||||
screenState = 'on';
|
||||
screenTip = 'Stop screen sharing';
|
||||
}
|
||||
else
|
||||
{
|
||||
screenState = 'off';
|
||||
screenTip = 'Start screen sharing';
|
||||
}
|
||||
|
||||
const spacingStyle =
|
||||
{
|
||||
'margin' : spacing
|
||||
};
|
||||
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={
|
||||
classnames(
|
||||
classes.root,
|
||||
'webcam',
|
||||
hover ? 'hover' : null,
|
||||
activeSpeaker ? 'active-speaker' : null
|
||||
)
|
||||
}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
setHover(true);
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
touchTimeout = setTimeout(() =>
|
||||
{
|
||||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
style={spacingStyle}
|
||||
>
|
||||
<div className={classnames(classes.viewContainer, 'webcam')} style={style}>
|
||||
<div className={classnames(classes.viewContainer)} style={style}>
|
||||
<div
|
||||
className={classnames(classes.controls, hover ? 'hover' : null)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
setHover(true);
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
touchTimeout = setTimeout(() =>
|
||||
{
|
||||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<p>ME</p>
|
||||
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'right'}>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label='Mute mic'
|
||||
className={classes.fab}
|
||||
disabled={!me.canSendMic || me.audioInProgress}
|
||||
color={micState === 'on' ? 'default' : 'secondary'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
if (micState === 'off')
|
||||
roomClient.enableMic();
|
||||
else if (micState === 'on')
|
||||
roomClient.muteMic();
|
||||
else
|
||||
roomClient.unmuteMic();
|
||||
}}
|
||||
>
|
||||
{ micState === 'on' ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</Fab>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'right'}>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label='Mute video'
|
||||
className={classes.fab}
|
||||
disabled={!me.canSendWebcam || me.webcamInProgress}
|
||||
color={webcamState === 'on' ? 'default' : 'secondary'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ?
|
||||
roomClient.disableWebcam() :
|
||||
roomClient.enableWebcam();
|
||||
}}
|
||||
>
|
||||
{ webcamState === 'on' ?
|
||||
<VideoIcon />
|
||||
:
|
||||
<VideoOffIcon />
|
||||
}
|
||||
</Fab>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'right'}>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label='Share screen'
|
||||
className={classes.fab}
|
||||
disabled={!me.canShareScreen || me.screenShareInProgress}
|
||||
color={screenState === 'on' ? 'primary' : 'default'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
{
|
||||
case 'on':
|
||||
{
|
||||
roomClient.disableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'off':
|
||||
{
|
||||
roomClient.enableScreenSharing();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ screenState === 'on' || screenState === 'unsupported' ?
|
||||
<ScreenOffIcon/>
|
||||
:null
|
||||
}
|
||||
{ screenState === 'off' ?
|
||||
<ScreenIcon/>
|
||||
:null
|
||||
}
|
||||
</Fab>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<VideoView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
|
|
@ -95,15 +360,64 @@ const Me = (props) =>
|
|||
roomClient.changeDisplayName(displayName);
|
||||
}}
|
||||
>
|
||||
<Volume name={me.name} />
|
||||
<Volume id={me.id} />
|
||||
</VideoView>
|
||||
</div>
|
||||
</div>
|
||||
{ screenProducer ?
|
||||
<div className={classes.root}>
|
||||
<div className={classnames(classes.viewContainer, 'screen')} style={style}>
|
||||
<div
|
||||
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
setHover(true);
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
touchTimeout = setTimeout(() =>
|
||||
{
|
||||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
style={spacingStyle}
|
||||
>
|
||||
<div className={classnames(classes.viewContainer)} style={style}>
|
||||
<div
|
||||
className={classnames(classes.controls, hover ? 'hover' : null)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
setHover(true);
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
{
|
||||
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
touchTimeout = setTimeout(() =>
|
||||
{
|
||||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<p>ME</p>
|
||||
</div>
|
||||
|
||||
<VideoView
|
||||
isMe
|
||||
isScreen
|
||||
advancedMode={advancedMode}
|
||||
videoContain
|
||||
videoTrack={screenProducer ? screenProducer.track : null}
|
||||
|
|
@ -128,8 +442,11 @@ Me.propTypes =
|
|||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
spacing : PropTypes.number,
|
||||
style : PropTypes.object,
|
||||
classes : PropTypes.object.isRequired
|
||||
smallButtons : PropTypes.bool,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
|
|
@ -138,7 +455,7 @@ const mapStateToProps = (state) =>
|
|||
me : state.me,
|
||||
...meProducersSelector(state),
|
||||
settings : state.settings,
|
||||
activeSpeaker : state.me.name === state.room.activeSpeakerName
|
||||
activeSpeaker : state.me.id === state.room.activeSpeakerId
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -153,8 +470,8 @@ export default withRoomContext(connect(
|
|||
prev.me === next.me &&
|
||||
prev.producers === next.producers &&
|
||||
prev.settings === next.settings &&
|
||||
prev.room.activeSpeakerName === next.room.activeSpeakerName
|
||||
prev.room.activeSpeakerId === next.room.activeSpeakerId
|
||||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(Me)));
|
||||
)(withStyles(styles, { withTheme: true })(Me)));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import classnames from 'classnames';
|
|||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import VideoView from '../VideoContainers/VideoView';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
|
|
@ -21,7 +21,6 @@ const styles = (theme) =>
|
|||
root :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : 6,
|
||||
boxShadow : 'var(--peer-shadow)',
|
||||
border : 'var(--peer-border)',
|
||||
touchAction : 'none',
|
||||
|
|
@ -32,11 +31,11 @@ const styles = (theme) =>
|
|||
backgroundRepeat : 'no-repeat',
|
||||
'&.webcam' :
|
||||
{
|
||||
order : 2
|
||||
order : 4
|
||||
},
|
||||
'&.screen' :
|
||||
{
|
||||
order : 1
|
||||
order : 3
|
||||
},
|
||||
'&.hover' :
|
||||
{
|
||||
|
|
@ -49,19 +48,13 @@ const styles = (theme) =>
|
|||
},
|
||||
fab :
|
||||
{
|
||||
margin : theme.spacing.unit
|
||||
margin : theme.spacing(1)
|
||||
},
|
||||
viewContainer :
|
||||
{
|
||||
position : 'relative',
|
||||
'&.webcam' :
|
||||
{
|
||||
order : 2
|
||||
},
|
||||
'&.screen' :
|
||||
{
|
||||
order : 1
|
||||
}
|
||||
position : 'relative',
|
||||
width : '100%',
|
||||
height : '100%'
|
||||
},
|
||||
controls :
|
||||
{
|
||||
|
|
@ -73,8 +66,8 @@ const styles = (theme) =>
|
|||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'flex-end',
|
||||
padding : '0.4vmin',
|
||||
zIndex : 20,
|
||||
padding : theme.spacing(1),
|
||||
zIndex : 21,
|
||||
opacity : 0,
|
||||
transition : 'opacity 0.3s',
|
||||
touchAction : 'none',
|
||||
|
|
@ -92,8 +85,8 @@ const styles = (theme) =>
|
|||
display : 'flex',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
padding : '0.4vmin',
|
||||
zIndex : 21,
|
||||
padding : theme.spacing(1),
|
||||
zIndex : 20,
|
||||
'& p' :
|
||||
{
|
||||
padding : '6px 12px',
|
||||
|
|
@ -109,15 +102,9 @@ const styles = (theme) =>
|
|||
const Peer = (props) =>
|
||||
{
|
||||
const [ hover, setHover ] = useState(false);
|
||||
const [ webcamHover, setWebcamHover ] = useState(false);
|
||||
const [ screenHover, setScreenHover ] = useState(false);
|
||||
|
||||
let touchTimeout = null;
|
||||
|
||||
let touchWebcamTimeout = null;
|
||||
|
||||
let touchScreenTimeout = null;
|
||||
|
||||
const {
|
||||
roomClient,
|
||||
advancedMode,
|
||||
|
|
@ -128,7 +115,9 @@ const Peer = (props) =>
|
|||
screenConsumer,
|
||||
toggleConsumerFullscreen,
|
||||
toggleConsumerWindow,
|
||||
spacing,
|
||||
style,
|
||||
smallButtons,
|
||||
windowConsumer,
|
||||
classes,
|
||||
theme
|
||||
|
|
@ -164,6 +153,12 @@ const Peer = (props) =>
|
|||
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const rootStyle =
|
||||
{
|
||||
'margin' : spacing,
|
||||
...style
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div
|
||||
|
|
@ -194,15 +189,9 @@ const Peer = (props) =>
|
|||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
style={rootStyle}
|
||||
>
|
||||
<div className={classnames(classes.viewContainer)} style={style}>
|
||||
{ videoVisible && !webcamConsumer.supported ?
|
||||
<div className={classes.videoInfo}>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classnames(classes.viewContainer)}>
|
||||
{ !videoVisible ?
|
||||
<div className={classes.videoInfo}>
|
||||
<p>this video is paused</p>
|
||||
|
|
@ -210,79 +199,80 @@ const Peer = (props) =>
|
|||
:null
|
||||
}
|
||||
|
||||
{ videoVisible && webcamConsumer.supported ?
|
||||
<div
|
||||
className={classnames(classes.controls, webcamHover ? 'hover' : null)}
|
||||
onMouseOver={() => setWebcamHover(true)}
|
||||
onMouseOut={() => setWebcamHover(false)}
|
||||
onTouchStart={() =>
|
||||
<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(() =>
|
||||
{
|
||||
if (touchWebcamTimeout)
|
||||
clearTimeout(touchWebcamTimeout);
|
||||
|
||||
setWebcamHover(true);
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<Fab
|
||||
aria-label='Mute mic'
|
||||
className={classes.fab}
|
||||
disabled={!micConsumer}
|
||||
color={micEnabled ? 'default' : 'secondary'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
if (touchWebcamTimeout)
|
||||
clearTimeout(touchWebcamTimeout);
|
||||
|
||||
touchWebcamTimeout = setTimeout(() =>
|
||||
{
|
||||
setWebcamHover(false);
|
||||
}, 2000);
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
|
||||
}}
|
||||
>
|
||||
<Fab
|
||||
aria-label='Mute mic'
|
||||
className={classes.fab}
|
||||
color={micEnabled ? 'default' : 'secondary'}
|
||||
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
|
||||
{ micEnabled ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</Fab>
|
||||
|
||||
{ !smallScreen ?
|
||||
<Fab
|
||||
aria-label='Fullscreen'
|
||||
aria-label='New window'
|
||||
className={classes.fab}
|
||||
disabled={!videoVisible}
|
||||
disabled={
|
||||
!videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<FullScreenIcon />
|
||||
<NewWindowIcon />
|
||||
</Fab>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
:null
|
||||
}
|
||||
|
||||
<Fab
|
||||
aria-label='Fullscreen'
|
||||
className={classes.fab}
|
||||
disabled={!videoVisible}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<FullScreenIcon />
|
||||
</Fab>
|
||||
</div>
|
||||
|
||||
<VideoView
|
||||
advancedMode={advancedMode}
|
||||
|
|
@ -295,7 +285,7 @@ const Peer = (props) =>
|
|||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
>
|
||||
<Volume name={peer.name} />
|
||||
<Volume id={peer.id} />
|
||||
</VideoView>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -322,43 +312,37 @@ const Peer = (props) =>
|
|||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
style={rootStyle}
|
||||
>
|
||||
{ screenVisible && !screenConsumer.supported ?
|
||||
<div className={classes.videoInfo} style={style}>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
{ !screenVisible ?
|
||||
<div className={classes.videoInfo} style={style}>
|
||||
<div className={classes.videoInfo}>
|
||||
<p>this video is paused</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
{ screenVisible && screenConsumer.supported ?
|
||||
<div className={classnames(classes.viewContainer)} style={style}>
|
||||
{ screenVisible ?
|
||||
<div className={classnames(classes.viewContainer)}>
|
||||
<div
|
||||
className={classnames(classes.controls, screenHover ? 'hover' : null)}
|
||||
onMouseOver={() => setScreenHover(true)}
|
||||
onMouseOut={() => setScreenHover(false)}
|
||||
className={classnames(classes.controls, hover ? 'hover' : null)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
{
|
||||
if (touchScreenTimeout)
|
||||
clearTimeout(touchScreenTimeout);
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
setScreenHover(true);
|
||||
setHover(true);
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
{
|
||||
|
||||
if (touchScreenTimeout)
|
||||
clearTimeout(touchScreenTimeout);
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
touchScreenTimeout = setTimeout(() =>
|
||||
touchTimeout = setTimeout(() =>
|
||||
{
|
||||
setScreenHover(false);
|
||||
setHover(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
|
|
@ -370,6 +354,7 @@ const Peer = (props) =>
|
|||
!screenVisible ||
|
||||
(windowConsumer === screenConsumer.id)
|
||||
}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerWindow(screenConsumer);
|
||||
|
|
@ -384,6 +369,7 @@ const Peer = (props) =>
|
|||
aria-label='Fullscreen'
|
||||
className={classes.fab}
|
||||
disabled={!screenVisible}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerFullscreen(screenConsumer);
|
||||
|
|
@ -418,9 +404,11 @@ Peer.propTypes =
|
|||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
windowConsumer : PropTypes.number,
|
||||
windowConsumer : PropTypes.string,
|
||||
activeSpeaker : PropTypes.bool,
|
||||
spacing : PropTypes.number,
|
||||
style : PropTypes.object,
|
||||
smallButtons : PropTypes.bool,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
|
|
@ -434,10 +422,10 @@ const makeMapStateToProps = (initialState, props) =>
|
|||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
peer : state.peers[props.name],
|
||||
peer : state.peers[props.id],
|
||||
...getPeerConsumers(state, props),
|
||||
windowConsumer : state.room.windowConsumer,
|
||||
activeSpeaker : props.name === state.room.activeSpeakerName
|
||||
activeSpeaker : props.id === state.room.activeSpeakerId
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -470,7 +458,7 @@ export default withRoomContext(connect(
|
|||
return (
|
||||
prev.peers === next.peers &&
|
||||
prev.consumers === next.consumers &&
|
||||
prev.room.activeSpeakerName === next.room.activeSpeakerName &&
|
||||
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
|
||||
prev.room.windowConsumer === next.room.windowConsumer
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -150,7 +150,7 @@ const makeMapStateToProps = (initialState, props) =>
|
|||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
volume : state.peerVolumes[props.name]
|
||||
volume : state.peerVolumes[props.id]
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
@ -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));
|
||||
|
|
@ -12,7 +12,7 @@ const styles = (theme) =>
|
|||
({
|
||||
root :
|
||||
{
|
||||
padding : theme.spacing.unit,
|
||||
padding : theme.spacing(1),
|
||||
display : 'flex',
|
||||
alignItems : 'center',
|
||||
borderRadius : 0
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ const styles = (theme) =>
|
|||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
marginBottom : theme.spacing.unit,
|
||||
padding : theme.spacing.unit,
|
||||
marginBottom : theme.spacing(1),
|
||||
padding : theme.spacing(1),
|
||||
flexShrink : 0
|
||||
},
|
||||
selfMessage :
|
||||
|
|
@ -42,7 +42,7 @@ const styles = (theme) =>
|
|||
},
|
||||
content :
|
||||
{
|
||||
marginLeft : theme.spacing.unit
|
||||
marginLeft : theme.spacing(1)
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const styles = (theme) =>
|
|||
flexDirection : 'column',
|
||||
alignItems : 'center',
|
||||
overflowY : 'auto',
|
||||
padding : theme.spacing.unit
|
||||
padding : theme.spacing(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { withStyles } from '@material-ui/core/styles';
|
|||
import magnet from 'magnet-uri';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
|
|
@ -15,11 +14,11 @@ const styles = (theme) =>
|
|||
display : 'flex',
|
||||
alignItems : 'center',
|
||||
width : '100%',
|
||||
padding : theme.spacing.unit,
|
||||
padding : theme.spacing(1),
|
||||
boxShadow : '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
|
||||
'&:not(:last-child)' :
|
||||
{
|
||||
marginBottom : theme.spacing.unit
|
||||
marginBottom : theme.spacing(1)
|
||||
}
|
||||
},
|
||||
avatar :
|
||||
|
|
@ -30,7 +29,7 @@ const styles = (theme) =>
|
|||
text :
|
||||
{
|
||||
margin : 0,
|
||||
padding : theme.spacing.unit
|
||||
padding : theme.spacing(1)
|
||||
},
|
||||
fileContent :
|
||||
{
|
||||
|
|
@ -41,7 +40,7 @@ const styles = (theme) =>
|
|||
{
|
||||
display : 'flex',
|
||||
alignItems : 'center',
|
||||
padding : theme.spacing.unit
|
||||
padding : theme.spacing(1)
|
||||
},
|
||||
button :
|
||||
{
|
||||
|
|
@ -55,15 +54,18 @@ class File extends React.PureComponent
|
|||
{
|
||||
const {
|
||||
roomClient,
|
||||
torrentSupport,
|
||||
displayName,
|
||||
picture,
|
||||
canShareFiles,
|
||||
magnetUri,
|
||||
file,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<img alt='Peer avatar' className={classes.avatar} src={file.picture || EmptyAvatar} />
|
||||
|
||||
<img alt='Avatar' className={classes.avatar} src={picture} />
|
||||
|
||||
<div className={classes.fileContent}>
|
||||
{ file.files ?
|
||||
<Fragment>
|
||||
|
|
@ -93,26 +95,22 @@ class File extends React.PureComponent
|
|||
:null
|
||||
}
|
||||
<Typography className={classes.text}>
|
||||
{ file.me ?
|
||||
'You shared a file'
|
||||
:
|
||||
`${file.displayName} shared a file`
|
||||
}
|
||||
{ `${displayName} shared a file` }
|
||||
</Typography>
|
||||
|
||||
{ !file.active && !file.files ?
|
||||
<div className={classes.fileInfo}>
|
||||
<Typography className={classes.text}>
|
||||
{magnet.decode(file.magnetUri).dn}
|
||||
{ magnet.decode(magnetUri).dn }
|
||||
</Typography>
|
||||
{ torrentSupport ?
|
||||
{ canShareFiles ?
|
||||
<Button
|
||||
variant='contained'
|
||||
component='span'
|
||||
className={classes.button}
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.handleDownload(file.magnetUri);
|
||||
roomClient.handleDownload(magnetUri);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
|
|
@ -145,20 +143,34 @@ class File extends React.PureComponent
|
|||
}
|
||||
|
||||
File.propTypes = {
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
magnetUri : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string.isRequired,
|
||||
picture : PropTypes.string,
|
||||
canShareFiles : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { magnetUri }) =>
|
||||
{
|
||||
return {
|
||||
file : state.files[magnetUri],
|
||||
torrentSupport : state.room.torrentSupport
|
||||
file : state.files[magnetUri],
|
||||
canShareFiles : state.me.canShareFiles
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
areStatesEqual : (next, prev) =>
|
||||
{
|
||||
return (
|
||||
prev.files === next.files &&
|
||||
prev.me.canShareFiles === next.me.canShareFiles
|
||||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(File)));
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import File from './File';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
|
|
@ -13,7 +15,7 @@ const styles = (theme) =>
|
|||
flexDirection : 'column',
|
||||
alignItems : 'center',
|
||||
overflowY : 'auto',
|
||||
padding : theme.spacing.unit
|
||||
padding : theme.spacing(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -42,14 +44,44 @@ class FileList extends React.PureComponent
|
|||
{
|
||||
const {
|
||||
files,
|
||||
me,
|
||||
picture,
|
||||
peers,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root} ref={(node) => { this.node = node; }}>
|
||||
{ Object.keys(files).map((magnetUri) =>
|
||||
<File key={magnetUri} magnetUri={magnetUri} />
|
||||
)}
|
||||
{ Object.entries(files).map(([ magnetUri, file ]) =>
|
||||
{
|
||||
let displayName;
|
||||
|
||||
let filePicture;
|
||||
|
||||
if (me.id === file.peerId)
|
||||
{
|
||||
displayName = 'You';
|
||||
filePicture = picture;
|
||||
}
|
||||
else if (peers[file.peerId])
|
||||
{
|
||||
displayName = peers[file.peerId].displayName;
|
||||
filePicture = peers[file.peerId].picture;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayName = 'Unknown';
|
||||
}
|
||||
|
||||
return (
|
||||
<File
|
||||
key={magnetUri}
|
||||
magnetUri={magnetUri}
|
||||
displayName={displayName}
|
||||
picture={filePicture || EmptyAvatar}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -58,14 +90,35 @@ class FileList extends React.PureComponent
|
|||
FileList.propTypes =
|
||||
{
|
||||
files : PropTypes.object.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
picture : PropTypes.string,
|
||||
peers : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
files : state.files
|
||||
files : state.files,
|
||||
me : state.me,
|
||||
picture : state.settings.picture,
|
||||
peers : state.peers
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(FileList));
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
areStatesEqual : (next, prev) =>
|
||||
{
|
||||
return (
|
||||
prev.files === next.files &&
|
||||
prev.me === next.me &&
|
||||
prev.settings.picture === next.settings.picture &&
|
||||
prev.peers === next.peers
|
||||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(FileList));
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const styles = (theme) =>
|
|||
},
|
||||
button :
|
||||
{
|
||||
margin : theme.spacing.unit
|
||||
margin : theme.spacing(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -46,11 +46,11 @@ class FileSharing extends React.PureComponent
|
|||
render()
|
||||
{
|
||||
const {
|
||||
torrentSupport,
|
||||
canShareFiles,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const buttonDescription = torrentSupport ?
|
||||
const buttonDescription = canShareFiles ?
|
||||
'Share file' : 'File sharing not supported';
|
||||
|
||||
return (
|
||||
|
|
@ -67,7 +67,7 @@ class FileSharing extends React.PureComponent
|
|||
variant='contained'
|
||||
component='span'
|
||||
className={classes.button}
|
||||
disabled={!torrentSupport}
|
||||
disabled={!canShareFiles}
|
||||
>
|
||||
{buttonDescription}
|
||||
</Button>
|
||||
|
|
@ -80,17 +80,17 @@ class FileSharing extends React.PureComponent
|
|||
}
|
||||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
canShareFiles : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
torrentSupport : state.room.torrentSupport,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
canShareFiles : state.me.canShareFiles,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import * as appPropTypes from '../../appPropTypes';
|
|||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
import HandIcon from '../../../images/icon-hand-white.svg';
|
||||
|
||||
const styles = () =>
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
|
|
@ -31,7 +31,7 @@ const styles = () =>
|
|||
fontSize : '1rem',
|
||||
border : 'none',
|
||||
display : 'flex',
|
||||
paddingLeft : '0.5rem',
|
||||
paddingLeft : theme.spacing(1),
|
||||
flexGrow : 1,
|
||||
alignItems : 'center'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
|
|||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
import HandIcon from '../../../images/icon-hand-white.svg';
|
||||
|
||||
const styles = () =>
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
|
|
@ -37,7 +37,7 @@ const styles = () =>
|
|||
fontSize : '1rem',
|
||||
border : 'none',
|
||||
display : 'flex',
|
||||
paddingLeft : '0.5rem',
|
||||
paddingLeft : theme.spacing(1),
|
||||
flexGrow : 1,
|
||||
alignItems : 'center'
|
||||
},
|
||||
|
|
@ -185,8 +185,8 @@ const ListPeer = (props) =>
|
|||
{
|
||||
e.stopPropagation();
|
||||
screenVisible ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
|
||||
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
|
||||
}}
|
||||
>
|
||||
{ screenVisible ?
|
||||
|
|
@ -207,8 +207,8 @@ const ListPeer = (props) =>
|
|||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
|
|
@ -241,7 +241,7 @@ const makeMapStateToProps = (initialState, props) =>
|
|||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
peer : state.peers[props.name],
|
||||
peer : state.peers[props.id],
|
||||
...getPeerConsumers(state, props)
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,23 +18,23 @@ const styles = (theme) =>
|
|||
{
|
||||
width : '100%',
|
||||
overflowY : 'auto',
|
||||
padding : 6
|
||||
padding : theme.spacing(1)
|
||||
},
|
||||
list :
|
||||
{
|
||||
listStyleType : 'none',
|
||||
padding : theme.spacing.unit,
|
||||
padding : theme.spacing(1),
|
||||
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
|
||||
backgroundColor : 'rgba(255, 255, 255, 1)'
|
||||
},
|
||||
listheader :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
padding : theme.spacing(1),
|
||||
fontWeight : 'bolder'
|
||||
},
|
||||
listItem :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'pointer',
|
||||
|
|
@ -76,7 +76,7 @@ class ParticipantList extends React.PureComponent
|
|||
roomClient,
|
||||
advancedMode,
|
||||
passivePeers,
|
||||
selectedPeerName,
|
||||
selectedPeerId,
|
||||
spotlightPeers,
|
||||
classes
|
||||
} = this.props;
|
||||
|
|
@ -92,14 +92,14 @@ class ParticipantList extends React.PureComponent
|
|||
<li className={classes.listheader}>Participants in Spotlight:</li>
|
||||
{ spotlightPeers.map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
key={peer.id}
|
||||
className={classNames(classes.listItem, {
|
||||
selected : peer.name === selectedPeerName
|
||||
selected : peer.id === selectedPeerId
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.id)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode}>
|
||||
<Volume small name={peer.name} />
|
||||
<ListPeer id={peer.id} advancedMode={advancedMode}>
|
||||
<Volume small id={peer.id} />
|
||||
</ListPeer>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -107,15 +107,15 @@ class ParticipantList extends React.PureComponent
|
|||
<br />
|
||||
<ul className={classes.list}>
|
||||
<li className={classes.listheader}>Passive Participants:</li>
|
||||
{ passivePeers.map((peerName) => (
|
||||
{ passivePeers.map((peerId) => (
|
||||
<li
|
||||
key={peerName}
|
||||
key={peerId}
|
||||
className={classNames(classes.listItem, {
|
||||
selected : peerName === selectedPeerName
|
||||
selected : peerId === selectedPeerId
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peerName)}
|
||||
onClick={() => roomClient.setSelectedPeer(peerId)}
|
||||
>
|
||||
<ListPeer name={peerName} advancedMode={advancedMode} />
|
||||
<ListPeer id={peerId} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -126,20 +126,20 @@ class ParticipantList extends React.PureComponent
|
|||
|
||||
ParticipantList.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
passivePeers : PropTypes.array,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlightPeers : PropTypes.array,
|
||||
classes : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
passivePeers : PropTypes.array,
|
||||
selectedPeerId : PropTypes.string,
|
||||
spotlightPeers : PropTypes.array,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
passivePeers : passivePeersSelector(state),
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlightPeers : spotlightPeersSelector(state)
|
||||
passivePeers : passivePeersSelector(state),
|
||||
selectedPeerId : state.room.selectedPeerId,
|
||||
spotlightPeers : spotlightPeersSelector(state)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ const ParticipantListContainer = withRoomContext(connect(
|
|||
return (
|
||||
prev.peers === next.peers &&
|
||||
prev.room.spotlights === next.room.spotlights &&
|
||||
prev.room.selectedPeerName === next.room.selectedPeerName
|
||||
prev.room.selectedPeerId === next.room.selectedPeerId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@ import {
|
|||
spotlightsLengthSelector
|
||||
} from '../Selectors';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Peer from '../Containers/Peer';
|
||||
import Me from '../Containers/Me';
|
||||
import HiddenPeers from '../Containers/HiddenPeers';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const RATIO = 1.334;
|
||||
const PADDING_V = 50;
|
||||
|
|
@ -43,15 +41,14 @@ class Democratic extends React.PureComponent
|
|||
{
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
peerWidth : 400,
|
||||
peerHeight : 300
|
||||
};
|
||||
this.state = {};
|
||||
|
||||
this.resizeTimeout = null;
|
||||
|
||||
this.peersRef = React.createRef();
|
||||
}
|
||||
|
||||
updateDimensions = debounce(() =>
|
||||
updateDimensions = () =>
|
||||
{
|
||||
if (!this.peersRef.current)
|
||||
{
|
||||
|
|
@ -93,14 +90,21 @@ class Democratic extends React.PureComponent
|
|||
peerHeight : 0.9 * y
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
window.addEventListener('resize', this.updateDimensions);
|
||||
const observer = new ResizeObserver(this.updateDimensions);
|
||||
// window.resize event listener
|
||||
window.addEventListener('resize', () =>
|
||||
{
|
||||
// clear the timeout
|
||||
clearTimeout(this.resizeTimeout);
|
||||
|
||||
observer.observe(this.peersRef.current);
|
||||
// start timing for event "completion"
|
||||
this.resizeTimeout = setTimeout(() => this.updateDimensions(), 250);
|
||||
});
|
||||
|
||||
this.updateDimensions();
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
|
|
@ -108,9 +112,10 @@ class Democratic extends React.PureComponent
|
|||
window.removeEventListener('resize', this.updateDimensions);
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
componentDidUpdate(prevProps)
|
||||
{
|
||||
this.updateDimensions();
|
||||
if (prevProps !== this.props)
|
||||
this.updateDimensions();
|
||||
}
|
||||
|
||||
render()
|
||||
|
|
@ -125,23 +130,25 @@ class Democratic extends React.PureComponent
|
|||
|
||||
const style =
|
||||
{
|
||||
'width' : this.state.peerWidth,
|
||||
'height' : this.state.peerHeight
|
||||
'width' : this.state.peerWidth ? this.state.peerWidth : 0,
|
||||
'height' : this.state.peerHeight ? this.state.peerHeight : 0
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root} ref={this.peersRef}>
|
||||
<Me
|
||||
advancedMode={advancedMode}
|
||||
spacing={6}
|
||||
style={style}
|
||||
/>
|
||||
{ spotlightsPeers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<Peer
|
||||
key={peer.name}
|
||||
key={peer.id}
|
||||
advancedMode={advancedMode}
|
||||
name={peer.name}
|
||||
id={peer.id}
|
||||
spacing={6}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,89 +1,54 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { connect } from 'react-redux';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
spotlightsLengthSelector,
|
||||
videoBoxesSelector
|
||||
} from '../Selectors';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import Me from '../Containers/Me';
|
||||
import Peer from '../Containers/Peer';
|
||||
import SpeakerPeer from '../Containers/SpeakerPeer';
|
||||
import HiddenPeers from '../Containers/HiddenPeers';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
alignItems : 'center',
|
||||
height : '100%',
|
||||
width : '100%'
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
display : 'grid',
|
||||
gridTemplateColumns : '1fr',
|
||||
gridTemplateRows : '1.6fr minmax(0, 0.4fr)'
|
||||
},
|
||||
activePeerContainer :
|
||||
speaker :
|
||||
{
|
||||
width : '100%',
|
||||
height : '80vh',
|
||||
gridArea : '1 / 1 / 2 / 2',
|
||||
display : 'flex',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center'
|
||||
},
|
||||
activePeer :
|
||||
{
|
||||
width : '100%',
|
||||
border : '5px solid rgba(255, 255, 255, 0.15)',
|
||||
boxShadow : '0px 5px 12px 2px rgba(17, 17, 17, 0.5)',
|
||||
marginTop : 60
|
||||
alignItems : 'center',
|
||||
paddingTop : 40
|
||||
},
|
||||
filmStrip :
|
||||
{
|
||||
display : 'flex',
|
||||
background : 'rgba(0, 0, 0 , 0.5)',
|
||||
width : '100%',
|
||||
overflowX : 'auto',
|
||||
height : '20vh',
|
||||
alignItems : 'center'
|
||||
gridArea : '2 / 1 / 3 / 2'
|
||||
},
|
||||
filmStripContent :
|
||||
filmItem :
|
||||
{
|
||||
margin : '0 auto',
|
||||
display : 'flex',
|
||||
height : '100%',
|
||||
alignItems : 'center'
|
||||
},
|
||||
film :
|
||||
{
|
||||
height : '18vh',
|
||||
flexShrink : 0,
|
||||
paddingLeft : '1vh',
|
||||
'& .active' :
|
||||
{
|
||||
borderColor : 'var(--active-speaker-border-color)'
|
||||
},
|
||||
display : 'flex',
|
||||
marginLeft : '6px',
|
||||
border : 'var(--peer-border)',
|
||||
'&.selected' :
|
||||
{
|
||||
borderColor : 'var(--selected-peer-border-color)'
|
||||
},
|
||||
'&:last-child' :
|
||||
'&.active' :
|
||||
{
|
||||
paddingRight : '1vh'
|
||||
opacity : '0.6'
|
||||
}
|
||||
},
|
||||
filmContent :
|
||||
{
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
border : '1px solid rgba(255,255,255,0.15)',
|
||||
maxWidth : 'calc(18vh * (4 / 3))',
|
||||
cursor : 'pointer',
|
||||
'& .screen' :
|
||||
{
|
||||
maxWidth : 'calc(18vh * (2 * 4 / 3))',
|
||||
border : 0
|
||||
}
|
||||
},
|
||||
hiddenPeers :
|
||||
{
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -93,80 +58,114 @@ class Filmstrip extends React.PureComponent
|
|||
{
|
||||
super(props);
|
||||
|
||||
this.resizeTimeout = null;
|
||||
|
||||
this.activePeerContainer = React.createRef();
|
||||
|
||||
this.filmStripContainer = React.createRef();
|
||||
}
|
||||
|
||||
state = {
|
||||
lastSpeaker : null,
|
||||
width : 400
|
||||
lastSpeaker : null
|
||||
};
|
||||
|
||||
// Find the name of the peer which is currently speaking. This is either
|
||||
// the latest active speaker, or the manually selected peer, or, if no
|
||||
// person has spoken yet, the first peer in the list of peers.
|
||||
getActivePeerName = () =>
|
||||
getActivePeerId = () =>
|
||||
{
|
||||
if (this.props.selectedPeerName)
|
||||
const {
|
||||
selectedPeerId,
|
||||
peers
|
||||
} = this.props;
|
||||
|
||||
const { lastSpeaker } = this.state;
|
||||
|
||||
if (selectedPeerId && peers[selectedPeerId])
|
||||
{
|
||||
return this.props.selectedPeerName;
|
||||
return this.props.selectedPeerId;
|
||||
}
|
||||
|
||||
if (this.state.lastSpeaker)
|
||||
if (lastSpeaker && peers[lastSpeaker])
|
||||
{
|
||||
return this.state.lastSpeaker;
|
||||
}
|
||||
|
||||
const peerNames = Object.keys(this.props.peers);
|
||||
const peerIds = Object.keys(peers);
|
||||
|
||||
if (peerNames.length > 0)
|
||||
if (peerIds.length > 0)
|
||||
{
|
||||
return peerNames[0];
|
||||
return peerIds[0];
|
||||
}
|
||||
};
|
||||
|
||||
isSharingCamera = (peerName) => this.props.peers[peerName] &&
|
||||
this.props.peers[peerName].consumers.some((consumer) =>
|
||||
isSharingCamera = (peerId) => this.props.peers[peerId] &&
|
||||
this.props.peers[peerId].consumers.some((consumer) =>
|
||||
this.props.consumers[consumer].source === 'screen');
|
||||
|
||||
getRatio = () =>
|
||||
updateDimensions = () =>
|
||||
{
|
||||
let ratio = 4 / 3;
|
||||
const newState = {};
|
||||
|
||||
if (this.isSharingCamera(this.getActivePeerName()))
|
||||
const speaker = this.activePeerContainer.current;
|
||||
|
||||
if (speaker)
|
||||
{
|
||||
ratio *= 2;
|
||||
}
|
||||
let speakerWidth = (speaker.clientWidth - 100);
|
||||
|
||||
return ratio;
|
||||
};
|
||||
|
||||
updateDimensions = debounce(() =>
|
||||
{
|
||||
const container = this.activePeerContainer.current;
|
||||
|
||||
if (container)
|
||||
{
|
||||
const ratio = this.getRatio();
|
||||
|
||||
let width = container.clientWidth;
|
||||
|
||||
if (width / ratio > (container.clientHeight - 100))
|
||||
let speakerHeight = (speakerWidth / 4) * 3;
|
||||
|
||||
if (this.isSharingCamera(this.getActivePeerId()))
|
||||
{
|
||||
width = (container.clientHeight - 100) * ratio;
|
||||
speakerWidth /= 2;
|
||||
speakerHeight = (speakerWidth / 4) * 3;
|
||||
}
|
||||
|
||||
if (speakerHeight > (speaker.clientHeight - 60))
|
||||
{
|
||||
speakerHeight = (speaker.clientHeight - 60);
|
||||
speakerWidth = (speakerHeight / 3) * 4;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
width
|
||||
});
|
||||
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()
|
||||
{
|
||||
window.addEventListener('resize', this.updateDimensions);
|
||||
const observer = new ResizeObserver(this.updateDimensions);
|
||||
// window.resize event listener
|
||||
window.addEventListener('resize', () =>
|
||||
{
|
||||
// clear the timeout
|
||||
clearTimeout(this.resizeTimeout);
|
||||
|
||||
// start timing for event "completion"
|
||||
this.resizeTimeout = setTimeout(() => this.updateDimensions(), 250);
|
||||
});
|
||||
|
||||
observer.observe(this.activePeerContainer.current);
|
||||
this.updateDimensions();
|
||||
}
|
||||
|
||||
|
|
@ -175,19 +174,28 @@ class Filmstrip extends React.PureComponent
|
|||
window.removeEventListener('resize', this.updateDimensions);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps)
|
||||
{
|
||||
if (nextProps !== this.props)
|
||||
{
|
||||
if (
|
||||
nextProps.activeSpeakerId != null &&
|
||||
nextProps.activeSpeakerId !== this.props.myId
|
||||
)
|
||||
{
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
lastSpeaker : nextProps.activeSpeakerId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps)
|
||||
{
|
||||
if (prevProps !== this.props)
|
||||
{
|
||||
this.updateDimensions();
|
||||
|
||||
if (this.props.activeSpeakerName !== this.props.myName)
|
||||
{
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
lastSpeaker : this.props.activeSpeakerName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,56 +204,78 @@ class Filmstrip extends React.PureComponent
|
|||
const {
|
||||
roomClient,
|
||||
peers,
|
||||
myId,
|
||||
advancedMode,
|
||||
spotlights,
|
||||
spotlightsLength,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const activePeerName = this.getActivePeerName();
|
||||
const activePeerId = this.getActivePeerId();
|
||||
|
||||
const speakerStyle =
|
||||
{
|
||||
width : this.state.speakerWidth,
|
||||
height : this.state.speakerHeight
|
||||
};
|
||||
|
||||
const peerStyle =
|
||||
{
|
||||
width : this.state.filmStripWidth,
|
||||
height : this.state.filmStripHeight
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.activePeerContainer} ref={this.activePeerContainer}>
|
||||
{ peers[activePeerName] ?
|
||||
<div
|
||||
className={classes.activePeer}
|
||||
style={{
|
||||
width : this.state.width,
|
||||
height : this.state.width / this.getRatio()
|
||||
}}
|
||||
>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={activePeerName}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.speaker} ref={this.activePeerContainer}>
|
||||
{ peers[activePeerId] ?
|
||||
<SpeakerPeer
|
||||
advancedMode={advancedMode}
|
||||
id={activePeerId}
|
||||
style={speakerStyle}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={classes.filmStrip}>
|
||||
<div className={classes.filmStripContent}>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
<div className={classes.filmStrip} ref={this.filmStripContainer}>
|
||||
<Grid container justify='center' spacing={0}>
|
||||
<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 (
|
||||
<div
|
||||
key={peerName}
|
||||
onClick={() => roomClient.setSelectedPeer(peerName)}
|
||||
className={classnames(classes.film, {
|
||||
selected : this.props.selectedPeerName === peerName,
|
||||
active : this.state.lastSpeaker === peerName
|
||||
})}
|
||||
>
|
||||
<div className={classes.filmContent}>
|
||||
<Grid key={peerId} item>
|
||||
<div
|
||||
key={peerId}
|
||||
onClick={() => roomClient.setSelectedPeer(peerId)}
|
||||
className={classnames(classes.filmItem, {
|
||||
selected : this.props.selectedPeerId === peerId,
|
||||
active : peerId === activePeerId
|
||||
})}
|
||||
>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
id={peerId}
|
||||
style={peerStyle}
|
||||
smallButtons
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
else
|
||||
|
|
@ -253,51 +283,63 @@ class Filmstrip extends React.PureComponent
|
|||
return ('');
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.hiddenPeers}>
|
||||
{ spotlightsLength<Object.keys(peers).length ?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{ spotlightsLength<Object.keys(peers).length ?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Filmstrip.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
activeSpeakerName : PropTypes.string,
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.object.isRequired,
|
||||
consumers : PropTypes.object.isRequired,
|
||||
myName : PropTypes.string.isRequired,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
activeSpeakerId : PropTypes.string,
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.object.isRequired,
|
||||
consumers : PropTypes.object.isRequired,
|
||||
myId : PropTypes.string.isRequired,
|
||||
selectedPeerId : PropTypes.string,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired,
|
||||
boxes : PropTypes.number,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
|
||||
|
||||
return {
|
||||
activeSpeakerName : state.room.activeSpeakerName,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
peers : state.peers,
|
||||
consumers : state.consumers,
|
||||
myName : state.me.name,
|
||||
spotlights : state.room.spotlights,
|
||||
spotlightsLength
|
||||
activeSpeakerId : state.room.activeSpeakerId,
|
||||
selectedPeerId : state.room.selectedPeerId,
|
||||
peers : state.peers,
|
||||
consumers : state.consumers,
|
||||
myId : state.me.id,
|
||||
spotlights : state.room.spotlights,
|
||||
spotlightsLength : spotlightsLengthSelector(state),
|
||||
boxes : videoBoxesSelector(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
undefined
|
||||
null,
|
||||
null,
|
||||
{
|
||||
areStatesEqual : (next, prev) =>
|
||||
{
|
||||
return (
|
||||
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
|
||||
prev.room.selectedPeerId === next.room.selectedPeerId &&
|
||||
prev.peers === next.peers &&
|
||||
prev.consumers === next.consumers &&
|
||||
prev.room.spotlights === next.room.spotlights &&
|
||||
prev.me.id === next.me.id
|
||||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(Filmstrip)));
|
||||
|
|
@ -15,7 +15,6 @@ import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
|
|||
import Hidden from '@material-ui/core/Hidden';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
|
|
@ -28,11 +27,14 @@ import Filmstrip from './MeetingViews/Filmstrip';
|
|||
import AudioPeers from './PeerAudio/AudioPeers';
|
||||
import FullScreenView from './VideoContainers/FullScreenView';
|
||||
import VideoWindow from './VideoWindow/VideoWindow';
|
||||
import Sidebar from './Controls/Sidebar';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import LockIcon from '@material-ui/icons/Lock';
|
||||
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Settings from './Settings/Settings';
|
||||
import JoinDialog from './JoinDialog';
|
||||
|
||||
const TIMEOUT = 10 * 1000;
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ const styles = (theme) =>
|
|||
left : '50%',
|
||||
transform : 'translateX(-50%) translateY(-50%)',
|
||||
width : '30vw',
|
||||
padding : theme.spacing.unit * 2,
|
||||
padding : theme.spacing(2),
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center'
|
||||
|
|
@ -125,6 +127,11 @@ const styles = (theme) =>
|
|||
{
|
||||
display : 'flex'
|
||||
},
|
||||
actionButton :
|
||||
{
|
||||
margin : theme.spacing(1),
|
||||
padding : 0
|
||||
},
|
||||
meContainer :
|
||||
{
|
||||
position : 'fixed',
|
||||
|
|
@ -176,10 +183,6 @@ class Room extends React.PureComponent
|
|||
|
||||
componentDidMount()
|
||||
{
|
||||
const { roomClient } = this.props;
|
||||
|
||||
roomClient.join();
|
||||
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
|
|
@ -242,29 +245,7 @@ class Room extends React.PureComponent
|
|||
democratic : Democratic
|
||||
}[room.mode];
|
||||
|
||||
if (room.audioSuspended)
|
||||
{
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Paper className={classes.message}>
|
||||
<Typography variant='h2'>
|
||||
This webpage required sound and video to play, please click to allow.
|
||||
</Typography>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.notify('Joining.');
|
||||
roomClient.resumeAudio();
|
||||
}}
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (room.lockedOut)
|
||||
if (room.lockedOut)
|
||||
{
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
|
@ -274,6 +255,14 @@ class Room extends React.PureComponent
|
|||
</div>
|
||||
);
|
||||
}
|
||||
else if (!room.joined)
|
||||
{
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<JoinDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
|
|
@ -324,9 +313,32 @@ class Room extends React.PureComponent
|
|||
</Typography>
|
||||
<div className={classes.grow} />
|
||||
<div className={classes.actionButtons}>
|
||||
<IconButton
|
||||
aria-label='Lock room'
|
||||
className={classes.actionButton}
|
||||
color='inherit'
|
||||
onClick={() =>
|
||||
{
|
||||
if (room.locked)
|
||||
{
|
||||
roomClient.unlockRoom();
|
||||
}
|
||||
else
|
||||
{
|
||||
roomClient.lockRoom();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ room.locked ?
|
||||
<LockIcon />
|
||||
:
|
||||
<LockOpenIcon />
|
||||
}
|
||||
</IconButton>
|
||||
{ this.fullscreen.fullscreenEnabled ?
|
||||
<IconButton
|
||||
aria-label='Fullscreen'
|
||||
className={classes.actionButton}
|
||||
color='inherit'
|
||||
onClick={this.handleToggleFullscreen}
|
||||
>
|
||||
|
|
@ -340,6 +352,7 @@ class Room extends React.PureComponent
|
|||
}
|
||||
<IconButton
|
||||
aria-label='Settings'
|
||||
className={classes.actionButton}
|
||||
color='inherit'
|
||||
onClick={() => setSettingsOpen(!room.settingsOpen)}
|
||||
>
|
||||
|
|
@ -348,6 +361,7 @@ class Room extends React.PureComponent
|
|||
{ loginEnabled ?
|
||||
<IconButton
|
||||
aria-label='Account'
|
||||
className={classes.actionButton}
|
||||
color='inherit'
|
||||
onClick={() =>
|
||||
{
|
||||
|
|
@ -362,6 +376,15 @@ class Room extends React.PureComponent
|
|||
</IconButton>
|
||||
:null
|
||||
}
|
||||
<Button
|
||||
aria-label='Leave meeting'
|
||||
className={classes.actionButton}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
onClick={() => roomClient.close()}
|
||||
>
|
||||
Leave
|
||||
</Button>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
|
@ -384,8 +407,6 @@ class Room extends React.PureComponent
|
|||
|
||||
<View advancedMode={advancedMode} />
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<Settings />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const consumersSelect = (state) => state.consumers;
|
|||
const spotlightsSelector = (state) => state.room.spotlights;
|
||||
const peersSelector = (state) => state.peers;
|
||||
const getPeerConsumers = (state, props) =>
|
||||
(state.peers[props.name] ? state.peers[props.name].consumers : null);
|
||||
(state.peers[props.id] ? state.peers[props.id].consumers : null);
|
||||
const getAllConsumers = (state) => state.consumers;
|
||||
const peersKeySelector = createSelector(
|
||||
peersSelector,
|
||||
|
|
@ -66,10 +66,10 @@ export const spotlightPeersSelector = createSelector(
|
|||
spotlightsSelector,
|
||||
peersSelector,
|
||||
(spotlights, peers) =>
|
||||
spotlights.reduce((result, peerName) =>
|
||||
spotlights.reduce((result, peerId) =>
|
||||
{
|
||||
if (peers[peerName])
|
||||
result.push(peers[peerName]);
|
||||
if (peers[peerId])
|
||||
result.push(peers[peerId]);
|
||||
|
||||
return result;
|
||||
}, [])
|
||||
|
|
@ -83,7 +83,7 @@ export const peersLengthSelector = createSelector(
|
|||
export const passivePeersSelector = createSelector(
|
||||
peersKeySelector,
|
||||
spotlightsSelector,
|
||||
(peers, spotlights) => peers.filter((peerName) => !spotlights.includes(peerName))
|
||||
(peers, spotlights) => peers.filter((peerId) => !spotlights.includes(peerId))
|
||||
);
|
||||
|
||||
export const videoBoxesSelector = createSelector(
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const styles = (theme) =>
|
|||
},
|
||||
setting :
|
||||
{
|
||||
padding : theme.spacing.unit * 2
|
||||
padding : theme.spacing(2)
|
||||
},
|
||||
formControl :
|
||||
{
|
||||
|
|
@ -51,13 +51,13 @@ const styles = (theme) =>
|
|||
}
|
||||
});
|
||||
|
||||
/* const modes = [ {
|
||||
const modes = [ {
|
||||
value : 'democratic',
|
||||
label : 'Democratic view'
|
||||
}, {
|
||||
value : 'filmstrip',
|
||||
label : 'Filmstrip view'
|
||||
} ]; */
|
||||
} ];
|
||||
|
||||
const resolutions = [ {
|
||||
value : 'low',
|
||||
|
|
@ -87,6 +87,7 @@ const Settings = ({
|
|||
settings,
|
||||
onToggleAdvancedMode,
|
||||
handleCloseSettings,
|
||||
handleChangeMode,
|
||||
classes
|
||||
}) =>
|
||||
{
|
||||
|
|
@ -203,6 +204,33 @@ const Settings = ({
|
|||
</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
<form className={classes.setting} autoComplete='off'>
|
||||
<FormControl className={classes.formControl}>
|
||||
<Select
|
||||
value={room.mode || ''}
|
||||
onChange={(event) =>
|
||||
{
|
||||
if (event.target.value)
|
||||
handleChangeMode(event.target.value);
|
||||
}}
|
||||
name='Room layout'
|
||||
autoWidth
|
||||
className={classes.selectEmpty}
|
||||
>
|
||||
{ modes.map((mode, index) =>
|
||||
{
|
||||
return (
|
||||
<MenuItem key={index} value={mode.value}>
|
||||
{mode.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Select room layout
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
<FormControlLabel
|
||||
className={classes.setting}
|
||||
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
|
||||
|
|
@ -241,7 +269,7 @@ const mapStateToProps = (state) =>
|
|||
const mapDispatchToProps = {
|
||||
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
|
||||
handleChangeMode : stateActions.setDisplayMode,
|
||||
handleCloseSettings : stateActions.setSettingsOpen
|
||||
handleCloseSettings : stateActions.setSettingsOpen,
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import * as stateActions from '../../actions/stateActions';
|
|||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import VideoView from './VideoView';
|
||||
|
||||
const styles = () =>
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
|
|
@ -29,7 +29,7 @@ const styles = () =>
|
|||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
padding : '0.4vmin'
|
||||
padding : theme.spacing(1)
|
||||
},
|
||||
button :
|
||||
{
|
||||
|
|
@ -102,13 +102,6 @@ const FullScreenView = (props) =>
|
|||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{ consumerVisible && !consumer.supported ?
|
||||
<div className={classes.incompatibleVideo}>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classes.controls}>
|
||||
<div
|
||||
className={classnames(classes.button, {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { withStyles } from '@material-ui/core/styles';
|
|||
import * as appPropTypes from '../appPropTypes';
|
||||
import EditableInput from '../Controls/EditableInput';
|
||||
|
||||
const styles = () =>
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
|
|
@ -27,7 +27,7 @@ const styles = () =>
|
|||
transitionProperty : 'opacity',
|
||||
transitionDuration : '.15s',
|
||||
backgroundColor : 'var(--peer-video-bg-color)',
|
||||
'&.is-me' :
|
||||
'&.isMe' :
|
||||
{
|
||||
transform : 'scaleX(-1)'
|
||||
},
|
||||
|
|
@ -48,54 +48,42 @@ const styles = () =>
|
|||
},
|
||||
info :
|
||||
{
|
||||
width : '100%',
|
||||
height : '100%',
|
||||
padding : theme.spacing(1),
|
||||
position : 'absolute',
|
||||
zIndex : 10,
|
||||
top : '0.6vmin',
|
||||
left : '0.6vmin',
|
||||
bottom : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'space-between'
|
||||
},
|
||||
media :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
display : 'flex',
|
||||
flexDirection : 'row'
|
||||
display : 'flex',
|
||||
transitionProperty : 'opacity',
|
||||
transitionDuration : '.15s',
|
||||
'&.hidden' :
|
||||
{
|
||||
opacity : 0,
|
||||
transitionDuration : '0s'
|
||||
}
|
||||
},
|
||||
box :
|
||||
{
|
||||
padding : '0.4vmin',
|
||||
padding : theme.spacing(0.5),
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.25)',
|
||||
'& p' :
|
||||
{
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
margin : 0,
|
||||
color : 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize : 10,
|
||||
|
||||
'&:last-child' :
|
||||
{
|
||||
marginBottom : 0
|
||||
}
|
||||
userSelect : 'none',
|
||||
margin : 0,
|
||||
color : 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize : '0.8em'
|
||||
}
|
||||
},
|
||||
peer :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'flex-end',
|
||||
position : 'absolute',
|
||||
bottom : '0.6vmin',
|
||||
left : 0,
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.25)',
|
||||
padding : '0.5vmin',
|
||||
alignItems : 'flex-start'
|
||||
display : 'flex'
|
||||
},
|
||||
displayNameEdit :
|
||||
{
|
||||
|
|
@ -120,12 +108,7 @@ const styles = () =>
|
|||
},
|
||||
deviceInfo :
|
||||
{
|
||||
marginTop : '0.4vmin',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'flex-end',
|
||||
'& span' :
|
||||
'& span' :
|
||||
{
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
|
|
@ -159,6 +142,7 @@ class VideoView extends React.PureComponent
|
|||
{
|
||||
const {
|
||||
isMe,
|
||||
isScreen,
|
||||
peer,
|
||||
displayName,
|
||||
showPeerInfo,
|
||||
|
|
@ -181,59 +165,62 @@ class VideoView extends React.PureComponent
|
|||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.info}>
|
||||
{ advancedMode ?
|
||||
<div className={classes.media}>
|
||||
<div className={classes.box}>
|
||||
{ audioCodec ?
|
||||
<p>{audioCodec}</p>
|
||||
:null
|
||||
}
|
||||
<div className={classnames(classes.media,
|
||||
{
|
||||
hidden : !advancedMode
|
||||
})}
|
||||
>
|
||||
<div className={classes.box}>
|
||||
{ audioCodec ?
|
||||
<p>{audioCodec}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{ videoCodec ?
|
||||
<p>{videoCodec} {videoProfile}</p>
|
||||
:null
|
||||
}
|
||||
{ videoCodec ?
|
||||
<p>{videoCodec} {videoProfile}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{ (videoVisible && videoWidth !== null) ?
|
||||
<p>{videoWidth}x{videoHeight}</p>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
{ (videoVisible && videoWidth !== null) ?
|
||||
<p>{videoWidth}x{videoHeight}</p>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
|
||||
{ showPeerInfo ?
|
||||
<div className={classes.peer}>
|
||||
{ isMe ?
|
||||
<EditableInput
|
||||
value={displayName}
|
||||
propName='newDisplayName'
|
||||
className={classnames(classes.displayNameEdit, 'display-name')}
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 30,
|
||||
autoCorrect : false,
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ newDisplayName }) => onChangeDisplayName(newDisplayName)}
|
||||
/>
|
||||
:
|
||||
<span className={classes.displayNameStatic}>
|
||||
{displayName}
|
||||
</span>
|
||||
}
|
||||
|
||||
{ advancedMode ?
|
||||
<div className={classes.deviceInfo}>
|
||||
<span>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
<div className={classes.box}>
|
||||
{ isMe ?
|
||||
<EditableInput
|
||||
value={displayName}
|
||||
propName='newDisplayName'
|
||||
className={classes.displayNameEdit}
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 30,
|
||||
autoCorrect : 'off',
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ newDisplayName }) => onChangeDisplayName(newDisplayName)}
|
||||
/>
|
||||
:
|
||||
<span className={classes.displayNameStatic}>
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
}
|
||||
|
||||
{ advancedMode ?
|
||||
<div className={classes.deviceInfo}>
|
||||
<span>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
</span>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
|
@ -243,7 +230,7 @@ class VideoView extends React.PureComponent
|
|||
ref='video'
|
||||
className={classnames(classes.video, {
|
||||
hidden : !videoVisible,
|
||||
'is-me' : isMe,
|
||||
'isMe' : isMe && !isScreen,
|
||||
loading : videoProfile === 'none',
|
||||
contain : videoContain
|
||||
})}
|
||||
|
|
@ -334,8 +321,9 @@ class VideoView extends React.PureComponent
|
|||
|
||||
VideoView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
isMe : PropTypes.bool,
|
||||
isScreen : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
[ appPropTypes.Me, appPropTypes.Peer ]),
|
||||
displayName : PropTypes.string,
|
||||
showPeerInfo : PropTypes.bool,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import FullScreen from '../FullScreen';
|
|||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
|
||||
const styles = () =>
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
|
|
@ -27,7 +27,7 @@ const styles = () =>
|
|||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
padding : '0.4vmin'
|
||||
padding : theme.spacing(1)
|
||||
},
|
||||
button :
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export const Room = PropTypes.shape(
|
|||
url : PropTypes.string.isRequired,
|
||||
state : PropTypes.oneOf(
|
||||
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
activeSpeakerId : PropTypes.string
|
||||
});
|
||||
|
||||
export const Device = PropTypes.shape(
|
||||
|
|
@ -17,7 +17,7 @@ export const Device = PropTypes.shape(
|
|||
|
||||
export const Me = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
id : PropTypes.string.isRequired,
|
||||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
|
|
@ -26,30 +26,28 @@ export const Me = PropTypes.shape(
|
|||
|
||||
export const Producer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
id : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
|
||||
paused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
export const Peer = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
id : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
device : Device.isRequired,
|
||||
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
consumers : PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
});
|
||||
|
||||
export const Consumer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
peerName : PropTypes.string.isRequired,
|
||||
id : PropTypes.string.isRequired,
|
||||
peerId : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
supported : PropTypes.bool.isRequired,
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
|
||||
|
|
@ -75,7 +73,7 @@ export const Message = PropTypes.shape(
|
|||
export const FileEntryProps = PropTypes.shape(
|
||||
{
|
||||
data : PropTypes.shape({
|
||||
name : PropTypes.string.isRequired,
|
||||
id : PropTypes.string.isRequired,
|
||||
picture : PropTypes.string,
|
||||
file : PropTypes.shape({
|
||||
magnet : PropTypes.string.isRequired
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
import domready from 'domready';
|
||||
import UrlParse from 'url-parse';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import randomString from 'random-string';
|
||||
import Logger from './Logger';
|
||||
import debug from 'debug';
|
||||
import RoomClient from './RoomClient';
|
||||
import RoomContext from './RoomContext';
|
||||
import deviceInfo from './deviceInfo';
|
||||
import * as stateActions from './actions/stateActions';
|
||||
import Room from './components/Room';
|
||||
import LoadingView from './components/LoadingView';
|
||||
|
|
@ -44,57 +43,48 @@ function run()
|
|||
{
|
||||
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
|
||||
|
||||
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new UrlParse(window.location.href, true);
|
||||
const peerId = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new URL(window.location);
|
||||
const parameters = urlParser.searchParams;
|
||||
|
||||
let roomId = (urlParser.pathname).substr(1)
|
||||
? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
|
||||
const produce = urlParser.query.produce !== 'false';
|
||||
const useSimulcast = urlParser.query.simulcast === 'true';
|
||||
let roomId = (urlParser.pathname).substr(1);
|
||||
|
||||
if (!roomId)
|
||||
roomId = parameters.get('roomId');
|
||||
|
||||
if (roomId)
|
||||
roomId = roomId.toLowerCase();
|
||||
else
|
||||
{
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
|
||||
urlParser.query.roomId = roomId;
|
||||
parameters.set('roomId', roomId);
|
||||
window.history.pushState('', '', urlParser.toString());
|
||||
}
|
||||
|
||||
// Get the effective/shareable Room URL.
|
||||
const roomUrlParser = new UrlParse(window.location.href, true);
|
||||
const produce = parameters.get('produce') !== 'false';
|
||||
const consume = parameters.get('consume') !== 'false';
|
||||
const useSimulcast = parameters.get('simulcast') === 'true';
|
||||
const forceTcp = parameters.get('forceTcp') === 'true';
|
||||
|
||||
for (const key of Object.keys(roomUrlParser.query))
|
||||
{
|
||||
// Don't keep some custom params.
|
||||
switch (key)
|
||||
{
|
||||
case 'roomId':
|
||||
case 'simulcast':
|
||||
break;
|
||||
default:
|
||||
delete roomUrlParser.query[key];
|
||||
}
|
||||
}
|
||||
delete roomUrlParser.hash;
|
||||
|
||||
const roomUrl = roomUrlParser.toString();
|
||||
const roomUrl = window.location.href.split('?')[0];
|
||||
|
||||
// Get current device.
|
||||
const device = getDeviceInfo();
|
||||
const device = deviceInfo();
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setRoomUrl(roomUrl));
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setMe({
|
||||
peerName,
|
||||
peerId,
|
||||
device,
|
||||
loginEnabled : window.config.loginEnabled
|
||||
})
|
||||
);
|
||||
|
||||
roomClient = new RoomClient(
|
||||
{ roomId, peerName, device, useSimulcast, produce });
|
||||
{ roomId, peerId, device, useSimulcast, produce, consume, forceTcp });
|
||||
|
||||
global.CLIENT = roomClient;
|
||||
|
||||
|
|
|
|||
|
|
@ -51,11 +51,30 @@ const consumers = (state = initialState, action) =>
|
|||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_EFFECTIVE_PROFILE':
|
||||
case 'SET_CONSUMER_CURRENT_LAYERS':
|
||||
{
|
||||
const { consumerId, profile } = action.payload;
|
||||
const { consumerId, spatialLayer, temporalLayer } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, profile };
|
||||
const newConsumer =
|
||||
{
|
||||
...consumer,
|
||||
currentSpatialLayer : spatialLayer,
|
||||
currentTemporalLayer : temporalLayer
|
||||
};
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_PREFERRED_LAYERS':
|
||||
{
|
||||
const { consumerId, spatialLayer, temporalLayer } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer =
|
||||
{
|
||||
...consumer,
|
||||
preferredSpatialLayer : spatialLayer,
|
||||
preferredTemporalLayer : temporalLayer
|
||||
};
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
|
@ -69,6 +88,19 @@ const consumers = (state = initialState, action) =>
|
|||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_SCORE':
|
||||
{
|
||||
const { consumerId, score } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
|
||||
if (!consumer)
|
||||
return state;
|
||||
|
||||
const newConsumer = { ...consumer, score };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ const files = (state = {}, action) =>
|
|||
{
|
||||
case 'ADD_FILE':
|
||||
{
|
||||
const { file } = action.payload;
|
||||
const { peerId, magnetUri } = action.payload;
|
||||
|
||||
const newFile = {
|
||||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
me : false,
|
||||
...file
|
||||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
peerId : peerId,
|
||||
magnetUri : magnetUri
|
||||
};
|
||||
|
||||
return { ...state, [file.magnetUri]: newFile };
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'ADD_FILE_HISTORY':
|
||||
|
|
@ -30,7 +30,6 @@ const files = (state = {}, action) =>
|
|||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
me : false,
|
||||
...file
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
const initialState =
|
||||
{
|
||||
name : null,
|
||||
id : null,
|
||||
device : null,
|
||||
canSendMic : false,
|
||||
canSendWebcam : false,
|
||||
canShareScreen : false,
|
||||
needExtension : false,
|
||||
canShareFiles : false,
|
||||
audioDevices : null,
|
||||
webcamDevices : null,
|
||||
webcamInProgress : false,
|
||||
|
|
@ -24,14 +24,14 @@ const me = (state = initialState, action) =>
|
|||
case 'SET_ME':
|
||||
{
|
||||
const {
|
||||
peerName,
|
||||
peerId,
|
||||
device,
|
||||
loginEnabled
|
||||
} = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
name : peerName,
|
||||
id : peerId,
|
||||
device,
|
||||
loginEnabled
|
||||
};
|
||||
|
|
@ -45,16 +45,20 @@ const me = (state = initialState, action) =>
|
|||
|
||||
case 'SET_MEDIA_CAPABILITIES':
|
||||
{
|
||||
const { canSendMic, canSendWebcam } = action.payload;
|
||||
const {
|
||||
canSendMic,
|
||||
canSendWebcam,
|
||||
canShareScreen,
|
||||
canShareFiles
|
||||
} = action.payload;
|
||||
|
||||
return { ...state, canSendMic, canSendWebcam };
|
||||
}
|
||||
|
||||
case 'SET_SCREEN_CAPABILITIES':
|
||||
{
|
||||
const { canShareScreen, needExtension } = action.payload;
|
||||
|
||||
return { ...state, canShareScreen, needExtension };
|
||||
return {
|
||||
...state,
|
||||
canSendMic,
|
||||
canSendWebcam,
|
||||
canShareScreen,
|
||||
canShareFiles
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_DEVICES':
|
||||
|
|
|
|||
|
|
@ -7,33 +7,33 @@ const peerVolumes = (state = initialState, action) =>
|
|||
case 'SET_ME':
|
||||
{
|
||||
const {
|
||||
peerName
|
||||
peerId
|
||||
} = action.payload;
|
||||
|
||||
return { ...state, [peerName]: 0 };
|
||||
return { ...state, [peerId]: 0 };
|
||||
}
|
||||
case 'ADD_PEER':
|
||||
{
|
||||
const { peer } = action.payload;
|
||||
|
||||
return { ...state, [peer.name]: 0 };
|
||||
return { ...state, [peer.id]: 0 };
|
||||
}
|
||||
|
||||
case 'REMOVE_PEER':
|
||||
{
|
||||
const { peerName } = action.payload;
|
||||
const { peerId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[peerName];
|
||||
delete newState[peerId];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_PEER_VOLUME':
|
||||
{
|
||||
const { peerName, volume } = action.payload;
|
||||
const { peerId, volume } = action.payload;
|
||||
|
||||
return { ...state, [peerName]: volume };
|
||||
return { ...state, [peerId]: volume };
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import omit from 'lodash/omit';
|
||||
|
||||
const peer = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
|
|
@ -53,12 +51,17 @@ const peers = (state = {}, action) =>
|
|||
{
|
||||
case 'ADD_PEER':
|
||||
{
|
||||
return { ...state, [action.payload.peer.name]: peer(undefined, action) };
|
||||
return { ...state, [action.payload.peer.id]: peer(undefined, action) };
|
||||
}
|
||||
|
||||
case 'REMOVE_PEER':
|
||||
{
|
||||
return omit(state, [ action.payload.peerName ]);
|
||||
const { peerId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[peerId];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_PEER_DISPLAY_NAME':
|
||||
|
|
@ -69,25 +72,25 @@ const peers = (state = {}, action) =>
|
|||
case 'SET_PEER_PICTURE':
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const oldPeer = state[action.payload.peerName];
|
||||
const oldPeer = state[action.payload.peerId];
|
||||
|
||||
if (!oldPeer)
|
||||
{
|
||||
throw new Error('no Peer found');
|
||||
}
|
||||
|
||||
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const oldPeer = state[action.payload.peerName];
|
||||
const oldPeer = state[action.payload.peerId];
|
||||
|
||||
// NOTE: This means that the Peer was closed before, so it's ok.
|
||||
if (!oldPeer)
|
||||
return state;
|
||||
|
||||
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ const initialState =
|
|||
state : 'new', // new/connecting/connected/disconnected/closed,
|
||||
locked : false,
|
||||
lockedOut : false,
|
||||
audioSuspended : false,
|
||||
activeSpeakerName : null,
|
||||
activeSpeakerId : null,
|
||||
torrentSupport : false,
|
||||
showSettings : false,
|
||||
fullScreenConsumer : null, // ConsumerID
|
||||
windowConsumer : null, // ConsumerID
|
||||
toolbarsVisible : true,
|
||||
mode : 'democratic',
|
||||
selectedPeerName : null,
|
||||
selectedPeerId : null,
|
||||
spotlights : [],
|
||||
settingsOpen : false
|
||||
settingsOpen : false,
|
||||
joined : false
|
||||
};
|
||||
|
||||
const room = (state = initialState, action) =>
|
||||
|
|
@ -35,7 +35,7 @@ const room = (state = initialState, action) =>
|
|||
if (roomState === 'connected')
|
||||
return { ...state, state: roomState };
|
||||
else
|
||||
return { ...state, state: roomState, activeSpeakerName: null };
|
||||
return { ...state, state: roomState, activeSpeakerId: null };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_LOCKED':
|
||||
|
|
@ -53,13 +53,6 @@ const room = (state = initialState, action) =>
|
|||
return { ...state, lockedOut: true };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_SUSPENDED':
|
||||
{
|
||||
const { audioSuspended } = action.payload;
|
||||
|
||||
return { ...state, audioSuspended };
|
||||
}
|
||||
|
||||
case 'SET_SETTINGS_OPEN':
|
||||
{
|
||||
const { settingsOpen } = action.payload;
|
||||
|
|
@ -69,9 +62,9 @@ const room = (state = initialState, action) =>
|
|||
|
||||
case 'SET_ROOM_ACTIVE_SPEAKER':
|
||||
{
|
||||
const { peerName } = action.payload;
|
||||
const { peerId } = action.payload;
|
||||
|
||||
return { ...state, activeSpeakerName: peerName };
|
||||
return { ...state, activeSpeakerId: peerId };
|
||||
}
|
||||
|
||||
case 'FILE_SHARING_SUPPORTED':
|
||||
|
|
@ -88,6 +81,13 @@ const room = (state = initialState, action) =>
|
|||
return { ...state, showSettings };
|
||||
}
|
||||
|
||||
case 'TOGGLE_JOINED':
|
||||
{
|
||||
const joined = !state.joined;
|
||||
|
||||
return { ...state, joined };
|
||||
}
|
||||
|
||||
case 'TOGGLE_FULLSCREEN_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
|
|
@ -119,13 +119,13 @@ const room = (state = initialState, action) =>
|
|||
|
||||
case 'SET_SELECTED_PEER':
|
||||
{
|
||||
const { selectedPeerName } = action.payload;
|
||||
const { selectedPeerId } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
selectedPeerName : state.selectedPeerName === selectedPeerName ?
|
||||
null : selectedPeerName
|
||||
selectedPeerId : state.selectedPeerId === selectedPeerId ?
|
||||
null : selectedPeerId
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
export function getSignalingUrl(peerName, roomId)
|
||||
export function getSignalingUrl(peerId, roomId)
|
||||
{
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.location.port;
|
||||
|
||||
const url = `wss://${hostname}:${port}/?peerName=${peerName}&roomId=${roomId}`;
|
||||
const url = `wss://${hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
const os = require('os');
|
||||
|
||||
module.exports =
|
||||
{
|
||||
// oAuth2 conf
|
||||
oauth2 :
|
||||
auth :
|
||||
{
|
||||
clientID : '',
|
||||
clientSecret : '',
|
||||
callbackURL : 'https://mYDomainName:port/auth-callback'
|
||||
/*
|
||||
The issuer URL for OpenID Connect discovery
|
||||
The OpenID Provider Configuration Document
|
||||
could be discovered on:
|
||||
issuerURL + '/.well-known/openid-configuration'
|
||||
*/
|
||||
issuerURL : 'https://example.com',
|
||||
clientOptions :
|
||||
{
|
||||
client_id : '',
|
||||
client_secret : '',
|
||||
scope : 'openid email profile',
|
||||
// where client.example.com is your multiparty meeting server
|
||||
redirect_uri : 'https://client.example.com/auth/callback'
|
||||
}
|
||||
},
|
||||
// Listening hostname for `gulp live|open`.
|
||||
domain : 'localhost',
|
||||
tls :
|
||||
// session cookie secret
|
||||
cookieSecret : 'T0P-S3cR3t_cook!e',
|
||||
tls :
|
||||
{
|
||||
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
|
||||
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
|
||||
|
|
@ -19,59 +33,61 @@ module.exports =
|
|||
// Any http request is redirected to https.
|
||||
// Listening port for http server.
|
||||
listeningRedirectPort : 80,
|
||||
// STUN/TURN
|
||||
// Mediasoup settings
|
||||
mediasoup :
|
||||
{
|
||||
// mediasoup Server settings.
|
||||
logLevel : 'warn',
|
||||
logTags :
|
||||
[
|
||||
'info',
|
||||
'ice',
|
||||
'dtls',
|
||||
'rtp',
|
||||
'srtp',
|
||||
'rtcp',
|
||||
'rbe',
|
||||
'rtx'
|
||||
],
|
||||
rtcIPv4 : true,
|
||||
rtcIPv6 : true,
|
||||
rtcAnnouncedIPv4 : null,
|
||||
rtcAnnouncedIPv6 : null,
|
||||
rtcMinPort : 40000,
|
||||
rtcMaxPort : 49999,
|
||||
// mediasoup Room codecs.
|
||||
mediaCodecs :
|
||||
[
|
||||
{
|
||||
kind : 'audio',
|
||||
name : 'opus',
|
||||
clockRate : 48000,
|
||||
channels : 2,
|
||||
parameters :
|
||||
numWorkers : Object.keys(os.cpus()).length,
|
||||
// mediasoup Worker settings.
|
||||
worker :
|
||||
{
|
||||
logLevel : 'warn',
|
||||
logTags :
|
||||
[
|
||||
'info',
|
||||
'ice',
|
||||
'dtls',
|
||||
'rtp',
|
||||
'srtp',
|
||||
'rtcp'
|
||||
],
|
||||
rtcMinPort : 40000,
|
||||
rtcMaxPort : 49999
|
||||
},
|
||||
// mediasoup Router settings.
|
||||
router :
|
||||
{
|
||||
// Router media codecs.
|
||||
mediaCodecs :
|
||||
[
|
||||
{
|
||||
useinbandfec : 1
|
||||
}
|
||||
},
|
||||
// {
|
||||
// kind : 'video',
|
||||
// name : 'VP8',
|
||||
// clockRate : 90000
|
||||
// }
|
||||
{
|
||||
kind : 'video',
|
||||
name : 'H264',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
kind : 'audio',
|
||||
mimeType : 'audio/opus',
|
||||
clockRate : 48000,
|
||||
channels : 2
|
||||
},
|
||||
{
|
||||
'packetization-mode' : 1,
|
||||
'profile-level-id' : '42e01f',
|
||||
'level-asymmetry-allowed' : 1
|
||||
kind : 'video',
|
||||
mimeType : 'video/h264',
|
||||
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).
|
||||
maxBitrate : 500000
|
||||
]
|
||||
},
|
||||
// mediasoup WebRtcTransport settings.
|
||||
webRtcTransport :
|
||||
{
|
||||
listenIps :
|
||||
[
|
||||
{ ip: '1.2.3.4', announcedIp: null }
|
||||
],
|
||||
maxIncomingBitrate : 1500000,
|
||||
initialAvailableOutgoingBitrate : 1000000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
1400
server/lib/Room.js
1400
server/lib/Room.js
File diff suppressed because it is too large
Load Diff
|
|
@ -54,7 +54,7 @@ function handleRoom(room, stream)
|
|||
Object.assign({}, baseEvent,
|
||||
{
|
||||
event : 'room.newpeer',
|
||||
peerName : peer.name,
|
||||
peerId : peer.id,
|
||||
rtpCapabilities : peer.rtpCapabilities
|
||||
}),
|
||||
stream);
|
||||
|
|
@ -67,7 +67,7 @@ function handlePeer(peer, baseEvent, stream)
|
|||
{
|
||||
baseEvent = Object.assign({}, baseEvent,
|
||||
{
|
||||
peerName : peer.name
|
||||
peerId : peer.id
|
||||
});
|
||||
|
||||
peer.on('close', (originator) =>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
{
|
||||
"name": "multiparty-meeting-server",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting server",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"awaitqueue": "^1.0.0",
|
||||
"base-64": "^0.1.0",
|
||||
"colors": "^1.1.2",
|
||||
"compression": "^1.7.3",
|
||||
"debug": "^4.1.0",
|
||||
"express": "^4.16.3",
|
||||
"mediasoup": "^2.6.11",
|
||||
"passport-dataporten": "^1.3.0",
|
||||
"socket.io": "^2.1.1"
|
||||
"express-session": "^1.16.1",
|
||||
"mediasoup": "^3.0.12",
|
||||
"openid-client": "^2.5.0",
|
||||
"passport": "^0.4.0",
|
||||
"socket.io": "^2.1.1",
|
||||
"spdy": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "^4.0.0",
|
||||
|
|
|
|||
470
server/server.js
470
server/server.js
|
|
@ -6,15 +6,20 @@ process.title = 'multiparty-meeting-server';
|
|||
|
||||
const config = require('./config/config');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const spdy = require('spdy');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const mediasoup = require('mediasoup');
|
||||
const AwaitQueue = require('awaitqueue');
|
||||
const Logger = require('./lib/Logger');
|
||||
const Room = require('./lib/Room');
|
||||
const Dataporten = require('passport-dataporten');
|
||||
const utils = require('./util');
|
||||
const base64 = require('base-64');
|
||||
// auth
|
||||
const passport = require('passport');
|
||||
const { Issuer, Strategy } = require('openid-client');
|
||||
const session = require('express-session');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
||||
|
|
@ -22,11 +27,18 @@ console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
|
|||
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||
/* eslint-enable no-console */
|
||||
|
||||
// Start the mediasoup server.
|
||||
const mediaServer = require('./mediasoup');
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const queue = new AwaitQueue();
|
||||
|
||||
// mediasoup Workers.
|
||||
// @type {Array<mediasoup.Worker>}
|
||||
const mediasoupWorkers = [];
|
||||
|
||||
// Index of next mediasoup Worker to use.
|
||||
// @type {Number}
|
||||
let nextMediasoupWorkerIdx = 0;
|
||||
|
||||
// Map of Room instances indexed by roomId.
|
||||
const rooms = new Map();
|
||||
|
||||
|
|
@ -38,147 +50,377 @@ const tls =
|
|||
};
|
||||
|
||||
const app = express();
|
||||
let httpsServer;
|
||||
let oidcClient;
|
||||
let oidcStrategy;
|
||||
|
||||
app.use(compression());
|
||||
|
||||
const dataporten = new Dataporten.Setup(config.oauth2);
|
||||
|
||||
app.all('*', (req, res, next) =>
|
||||
passport.serializeUser((user, done) =>
|
||||
{
|
||||
if (req.secure)
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser((user, done) =>
|
||||
{
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
const auth = config.auth;
|
||||
|
||||
async function run()
|
||||
{
|
||||
if (
|
||||
typeof(auth) !== 'undefined' &&
|
||||
typeof(auth.issuerURL) !== 'undefined' &&
|
||||
typeof(auth.clientOptions) !== 'undefined'
|
||||
)
|
||||
{
|
||||
return next();
|
||||
Issuer.discover(auth.issuerURL).then( async (oidcIssuer) =>
|
||||
{
|
||||
// Setup authentication
|
||||
await setupAuth(oidcIssuer);
|
||||
|
||||
// Run a mediasoup Worker.
|
||||
await runMediasoupWorkers();
|
||||
|
||||
// Run HTTPS server.
|
||||
await runHttpsServer();
|
||||
|
||||
// Run WebSocketServer.
|
||||
await runWebSocketServer();
|
||||
})
|
||||
.catch((err) =>
|
||||
{
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error('Auth is not configure properly!');
|
||||
|
||||
// Run a mediasoup Worker.
|
||||
await runMediasoupWorkers();
|
||||
|
||||
// Run HTTPS server.
|
||||
await runHttpsServer();
|
||||
|
||||
// Run WebSocketServer.
|
||||
await runWebSocketServer();
|
||||
}
|
||||
|
||||
res.redirect(`https://${req.hostname}${req.url}`);
|
||||
});
|
||||
|
||||
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) =>
|
||||
// Log rooms status every 30 seconds.
|
||||
setInterval(() =>
|
||||
{
|
||||
const state = JSON.parse(base64.decode(req.query.state));
|
||||
|
||||
if (rooms.has(state.roomId))
|
||||
for (const room of rooms.values())
|
||||
{
|
||||
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,
|
||||
name : req.user.data.displayName,
|
||||
picture : req.user.data.photos[0]
|
||||
id : tokenset.claims.sub,
|
||||
provider : tokenset.claims.iss,
|
||||
_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.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)
|
||||
app.get('/', (req, res) =>
|
||||
{
|
||||
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(
|
||||
'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName);
|
||||
httpsServer = spdy.createServer(tls, app);
|
||||
|
||||
let room;
|
||||
|
||||
// If an unknown roomId, create a new Room.
|
||||
if (!rooms.has(roomId))
|
||||
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
|
||||
{
|
||||
logger.info('creating a new Room [roomId:"%s"]', roomId);
|
||||
logger.info('Server running on port: ', config.listeningPort);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
room = new Room(roomId, mediaServer, io);
|
||||
const httpServer = http.createServer(app);
|
||||
|
||||
global.APP_ROOM = room;
|
||||
}
|
||||
catch (error)
|
||||
httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
|
||||
{
|
||||
logger.info('Server redirecting port: ', config.listeningRedirectPort);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a protoo WebSocketServer to allow WebSocket connections from browsers.
|
||||
*/
|
||||
async function runWebSocketServer()
|
||||
{
|
||||
const io = require('socket.io')(httpsServer);
|
||||
|
||||
// Handle connections from clients.
|
||||
io.on('connection', (socket) =>
|
||||
{
|
||||
const { roomId, peerId } = socket.handshake.query;
|
||||
|
||||
if (!roomId || !peerId)
|
||||
{
|
||||
logger.error('error creating a new Room: %s', error);
|
||||
logger.warn('connection request without roomId and/or peerId');
|
||||
|
||||
socket.disconnect(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const logStatusTimer = setInterval(() =>
|
||||
logger.info(
|
||||
'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
|
||||
|
||||
queue.push(async () =>
|
||||
{
|
||||
room.logStatus();
|
||||
}, 30000);
|
||||
const room = await getOrCreateRoom({ roomId });
|
||||
|
||||
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);
|
||||
|
||||
room.on('close', () =>
|
||||
{
|
||||
rooms.delete(roomId);
|
||||
clearInterval(logStatusTimer);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
room = rooms.get(roomId);
|
||||
room.on('close', () => rooms.delete(roomId));
|
||||
}
|
||||
|
||||
socket.room = roomId;
|
||||
return room;
|
||||
}
|
||||
|
||||
room.handleConnection(peerName, socket);
|
||||
});
|
||||
run();
|
||||
Loading…
Reference in New Issue