Mostly working mediasoup v3

master
Håvar Aambø Fosstveit 2019-06-03 11:55:23 +02:00
parent e9b946ba93
commit 30f42d6ced
31 changed files with 2563 additions and 1741 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
node_modules/ node_modules/
/app/build/ /app/build/
/app/public/config.js /app/public/config/config.js
/app/public/images/logo.* /app/public/images/logo.*
/server/config/ /server/config/
!/server/config/config.example.js !/server/config/config.example.js

View File

@ -8,13 +8,14 @@
"dependencies": { "dependencies": {
"@material-ui/core": "^3.9.2", "@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.2", "@material-ui/icons": "^3.0.2",
"bowser": "^2.4.0",
"create-torrent": "^3.33.0", "create-torrent": "^3.33.0",
"domready": "^1.0.8", "domready": "^1.0.8",
"file-saver": "^2.0.1", "file-saver": "^2.0.1",
"hark": "^1.2.3", "hark": "^1.2.3",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"marked": "^0.6.1", "marked": "^0.6.1",
"mediasoup-client": "^2.4.10", "mediasoup-client": "^3.0.6",
"notistack": "^0.5.1", "notistack": "^0.5.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"random-string": "^0.2.0", "random-string": "^0.2.0",
@ -169,7 +170,12 @@
"no-case-declarations": 2, "no-case-declarations": 2,
"no-catch-shadow": 2, "no-catch-shadow": 2,
"no-class-assign": 2, "no-class-assign": 2,
"no-confusing-arrow": ["error", {"allowParens": true}], "no-confusing-arrow": [
"error",
{
"allowParens": true
}
],
"no-console": 2, "no-console": 2,
"no-const-assign": 2, "no-const-assign": 2,
"no-debugger": 2, "no-debugger": 2,

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -14,11 +14,11 @@ export const setRoomState = (state) =>
}; };
}; };
export const setRoomActiveSpeaker = (peerName) => export const setRoomActiveSpeaker = (peerId) =>
{ {
return { return {
type : 'SET_ROOM_ACTIVE_SPEAKER', type : 'SET_ROOM_ACTIVE_SPEAKER',
payload : { peerName } payload : { peerId }
}; };
}; };
@ -57,19 +57,25 @@ export const setSettingsOpen = ({ settingsOpen }) =>
payload : { settingsOpen } payload : { settingsOpen }
}); });
export const setMe = ({ peerName, device, loginEnabled }) => export const setMe = ({ peerId, device, loginEnabled }) =>
{ {
return { return {
type : 'SET_ME', type : 'SET_ME',
payload : { peerName, device, loginEnabled } payload : { peerId, device, loginEnabled }
}; };
}; };
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) => export const setMediaCapabilities = ({
canSendMic,
canSendWebcam,
canShareScreen,
needExtension,
canShareFiles
}) =>
{ {
return { return {
type : 'SET_MEDIA_CAPABILITIES', type : 'SET_MEDIA_CAPABILITIES',
payload : { canSendMic, canSendWebcam } payload : { canSendMic, canSendWebcam, canShareScreen, needExtension, canShareFiles }
}; };
}; };
@ -150,27 +156,27 @@ export const setDisplayMode = (mode) =>
payload : { mode } payload : { mode }
}); });
export const setPeerVideoInProgress = (peerName, flag) => export const setPeerVideoInProgress = (peerId, flag) =>
{ {
return { return {
type : 'SET_PEER_VIDEO_IN_PROGRESS', type : 'SET_PEER_VIDEO_IN_PROGRESS',
payload : { peerName, flag } payload : { peerId, flag }
}; };
}; };
export const setPeerAudioInProgress = (peerName, flag) => export const setPeerAudioInProgress = (peerId, flag) =>
{ {
return { return {
type : 'SET_PEER_AUDIO_IN_PROGRESS', type : 'SET_PEER_AUDIO_IN_PROGRESS',
payload : { peerName, flag } payload : { peerId, flag }
}; };
}; };
export const setPeerScreenInProgress = (peerName, flag) => export const setPeerScreenInProgress = (peerId, flag) =>
{ {
return { return {
type : 'SET_PEER_SCREEN_IN_PROGRESS', type : 'SET_PEER_SCREEN_IN_PROGRESS',
payload : { peerName, flag } payload : { peerId, flag }
}; };
}; };
@ -226,11 +232,11 @@ export const setMyRaiseHandStateInProgress = (flag) =>
}; };
}; };
export const setPeerRaiseHandState = (peerName, raiseHandState) => export const setPeerRaiseHandState = (peerId, raiseHandState) =>
{ {
return { return {
type : 'SET_PEER_RAISE_HAND_STATE', type : 'SET_PEER_RAISE_HAND_STATE',
payload : { peerName, raiseHandState } payload : { peerId, raiseHandState }
}; };
}; };
@ -274,6 +280,14 @@ export const setProducerTrack = (producerId, track) =>
}; };
}; };
export const setProducerScore = (producerId, score) =>
{
return {
type : 'SET_PRODUCER_SCORE',
payload : { producerId, score }
};
};
export const setAudioInProgress = (flag) => export const setAudioInProgress = (flag) =>
{ {
return { return {
@ -306,35 +320,35 @@ export const addPeer = (peer) =>
}; };
}; };
export const removePeer = (peerName) => export const removePeer = (peerId) =>
{ {
return { return {
type : 'REMOVE_PEER', type : 'REMOVE_PEER',
payload : { peerName } payload : { peerId }
}; };
}; };
export const setPeerDisplayName = (displayName, peerName) => export const setPeerDisplayName = (displayName, peerId) =>
{ {
return { return {
type : 'SET_PEER_DISPLAY_NAME', type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerName } payload : { displayName, peerId }
}; };
}; };
export const addConsumer = (consumer, peerName) => export const addConsumer = (consumer, peerId) =>
{ {
return { return {
type : 'ADD_CONSUMER', type : 'ADD_CONSUMER',
payload : { consumer, peerName } payload : { consumer, peerId }
}; };
}; };
export const removeConsumer = (consumerId, peerName) => export const removeConsumer = (consumerId, peerId) =>
{ {
return { return {
type : 'REMOVE_CONSUMER', type : 'REMOVE_CONSUMER',
payload : { consumerId, peerName } payload : { consumerId, peerId }
}; };
}; };
@ -354,11 +368,19 @@ export const setConsumerResumed = (consumerId, originator) =>
}; };
}; };
export const setConsumerEffectiveProfile = (consumerId, profile) => export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
{ {
return { return {
type : 'SET_CONSUMER_EFFECTIVE_PROFILE', type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, profile } payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_PREFERRED_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
}; };
}; };
@ -370,11 +392,19 @@ export const setConsumerTrack = (consumerId, track) =>
}; };
}; };
export const setPeerVolume = (peerName, volume) => export const setConsumerScore = (consumerId, score) =>
{
return {
type : 'SET_CONSUMER_SCORE',
payload : { consumerId, score }
};
};
export const setPeerVolume = (peerId, volume) =>
{ {
return { return {
type : 'SET_PEER_VOLUME', type : 'SET_PEER_VOLUME',
payload : { peerName, volume } payload : { peerId, volume }
}; };
}; };
@ -536,10 +566,10 @@ export const setPicture = (picture) =>
payload : { picture } payload : { picture }
}); });
export const setPeerPicture = (peerName, picture) => export const setPeerPicture = (peerId, picture) =>
({ ({
type : 'SET_PEER_PICTURE', type : 'SET_PEER_PICTURE',
payload : { peerName, picture } payload : { peerId, picture }
}); });
export const loggedIn = () => export const loggedIn = () =>
@ -547,10 +577,10 @@ export const loggedIn = () =>
type : 'LOGGED_IN' type : 'LOGGED_IN'
}); });
export const setSelectedPeer = (selectedPeerName) => export const setSelectedPeer = (selectedpeerId) =>
({ ({
type : 'SET_SELECTED_PEER', type : 'SET_SELECTED_PEER',
payload : { selectedPeerName } payload : { selectedpeerId }
}); });
export const setSpotlights = (spotlights) => export const setSpotlights = (spotlights) =>

View File

@ -95,7 +95,7 @@ const Me = (props) =>
roomClient.changeDisplayName(displayName); roomClient.changeDisplayName(displayName);
}} }}
> >
<Volume name={me.name} /> <Volume id={me.id} />
</VideoView> </VideoView>
</div> </div>
</div> </div>
@ -138,7 +138,7 @@ const mapStateToProps = (state) =>
me : state.me, me : state.me,
...meProducersSelector(state), ...meProducersSelector(state),
settings : state.settings, settings : state.settings,
activeSpeaker : state.me.name === state.room.activeSpeakerName activeSpeaker : state.me.id === state.room.activeSpeakerId
}; };
}; };
@ -153,7 +153,7 @@ export default withRoomContext(connect(
prev.me === next.me && prev.me === next.me &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings && prev.settings === next.settings &&
prev.room.activeSpeakerName === next.room.activeSpeakerName prev.room.activeSpeakerId === next.room.activeSpeakerId
); );
} }
} }

View File

@ -196,13 +196,6 @@ const Peer = (props) =>
}} }}
> >
<div className={classnames(classes.viewContainer)} style={style}> <div className={classnames(classes.viewContainer)} style={style}>
{ videoVisible && !webcamConsumer.supported ?
<div className={classes.videoInfo}>
<p>incompatible video</p>
</div>
:null
}
{ !videoVisible ? { !videoVisible ?
<div className={classes.videoInfo}> <div className={classes.videoInfo}>
<p>this video is paused</p> <p>this video is paused</p>
@ -210,7 +203,7 @@ const Peer = (props) =>
:null :null
} }
{ videoVisible && webcamConsumer.supported ? { videoVisible ?
<div <div
className={classnames(classes.controls, webcamHover ? 'hover' : null)} className={classnames(classes.controls, webcamHover ? 'hover' : null)}
onMouseOver={() => setWebcamHover(true)} onMouseOver={() => setWebcamHover(true)}
@ -240,8 +233,8 @@ const Peer = (props) =>
onClick={() => onClick={() =>
{ {
micEnabled ? micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false); roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}} }}
> >
{ micEnabled ? { micEnabled ?
@ -295,7 +288,7 @@ const Peer = (props) =>
audioCodec={micConsumer ? micConsumer.codec : null} audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null} videoCodec={webcamConsumer ? webcamConsumer.codec : null}
> >
<Volume name={peer.name} /> <Volume id={peer.id} />
</VideoView> </VideoView>
</div> </div>
</div> </div>
@ -323,13 +316,6 @@ const Peer = (props) =>
}, 2000); }, 2000);
}} }}
> >
{ screenVisible && !screenConsumer.supported ?
<div className={classes.videoInfo} style={style}>
<p>incompatible video</p>
</div>
:null
}
{ !screenVisible ? { !screenVisible ?
<div className={classes.videoInfo} style={style}> <div className={classes.videoInfo} style={style}>
<p>this video is paused</p> <p>this video is paused</p>
@ -337,7 +323,7 @@ const Peer = (props) =>
:null :null
} }
{ screenVisible && screenConsumer.supported ? { screenVisible ?
<div className={classnames(classes.viewContainer)} style={style}> <div className={classnames(classes.viewContainer)} style={style}>
<div <div
className={classnames(classes.controls, screenHover ? 'hover' : null)} className={classnames(classes.controls, screenHover ? 'hover' : null)}
@ -418,7 +404,7 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number, windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
style : PropTypes.object, style : PropTypes.object,
toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerFullscreen : PropTypes.func.isRequired,
@ -434,10 +420,10 @@ const makeMapStateToProps = (initialState, props) =>
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
peer : state.peers[props.name], peer : state.peers[props.id],
...getPeerConsumers(state, props), ...getPeerConsumers(state, props),
windowConsumer : state.room.windowConsumer, windowConsumer : state.room.windowConsumer,
activeSpeaker : props.name === state.room.activeSpeakerName activeSpeaker : props.id === state.room.activeSpeakerId
}; };
}; };
@ -470,7 +456,7 @@ export default withRoomContext(connect(
return ( return (
prev.peers === next.peers && prev.peers === next.peers &&
prev.consumers === next.consumers && prev.consumers === next.consumers &&
prev.room.activeSpeakerName === next.room.activeSpeakerName && prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.windowConsumer === next.room.windowConsumer prev.room.windowConsumer === next.room.windowConsumer
); );
} }

View File

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

View File

@ -158,8 +158,8 @@ const Sidebar = (props) =>
onClick={() => onClick={() =>
{ {
micState === 'on' ? micState === 'on' ?
roomClient.muteMic() : roomClient.disableMic() :
roomClient.unmuteMic(); roomClient.enableMic();
}} }}
> >
{ micState === 'on' ? { micState === 'on' ?

View File

@ -55,7 +55,7 @@ class File extends React.PureComponent
{ {
const { const {
roomClient, roomClient,
torrentSupport, canShareFiles,
file, file,
classes classes
} = this.props; } = this.props;
@ -105,7 +105,7 @@ class File extends React.PureComponent
<Typography className={classes.text}> <Typography className={classes.text}>
{magnet.decode(file.magnetUri).dn} {magnet.decode(file.magnetUri).dn}
</Typography> </Typography>
{ torrentSupport ? { canShareFiles ?
<Button <Button
variant='contained' variant='contained'
component='span' component='span'
@ -145,17 +145,17 @@ class File extends React.PureComponent
} }
File.propTypes = { File.propTypes = {
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
torrentSupport : PropTypes.bool.isRequired, canShareFiles : PropTypes.bool.isRequired,
file : PropTypes.object.isRequired, file : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state, { magnetUri }) => const mapStateToProps = (state, { magnetUri }) =>
{ {
return { return {
file : state.files[magnetUri], file : state.files[magnetUri],
torrentSupport : state.room.torrentSupport canShareFiles : state.me.canShareFiles
}; };
}; };

View File

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

View File

@ -185,8 +185,8 @@ const ListPeer = (props) =>
{ {
e.stopPropagation(); e.stopPropagation();
screenVisible ? screenVisible ?
roomClient.modifyPeerConsumer(peer.name, 'screen', true) : roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.name, 'screen', false); roomClient.modifyPeerConsumer(peer.id, 'screen', false);
}} }}
> >
{ screenVisible ? { screenVisible ?
@ -207,8 +207,8 @@ const ListPeer = (props) =>
{ {
e.stopPropagation(); e.stopPropagation();
micEnabled ? micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false); roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}} }}
> >
{ micEnabled ? { micEnabled ?
@ -241,7 +241,7 @@ const makeMapStateToProps = (initialState, props) =>
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
peer : state.peers[props.name], peer : state.peers[props.id],
...getPeerConsumers(state, props) ...getPeerConsumers(state, props)
}; };
}; };

View File

@ -76,7 +76,7 @@ class ParticipantList extends React.PureComponent
roomClient, roomClient,
advancedMode, advancedMode,
passivePeers, passivePeers,
selectedPeerName, selectedPeerId,
spotlightPeers, spotlightPeers,
classes classes
} = this.props; } = this.props;
@ -92,14 +92,14 @@ class ParticipantList extends React.PureComponent
<li className={classes.listheader}>Participants in Spotlight:</li> <li className={classes.listheader}>Participants in Spotlight:</li>
{ spotlightPeers.map((peer) => ( { spotlightPeers.map((peer) => (
<li <li
key={peer.name} key={peer.id}
className={classNames(classes.listItem, { className={classNames(classes.listItem, {
selected : peer.name === selectedPeerName selected : peer.id === selectedPeerId
})} })}
onClick={() => roomClient.setSelectedPeer(peer.name)} onClick={() => roomClient.setSelectedPeer(peer.id)}
> >
<ListPeer name={peer.name} advancedMode={advancedMode}> <ListPeer id={peer.id} advancedMode={advancedMode}>
<Volume small name={peer.name} /> <Volume small id={peer.id} />
</ListPeer> </ListPeer>
</li> </li>
))} ))}
@ -107,15 +107,15 @@ class ParticipantList extends React.PureComponent
<br /> <br />
<ul className={classes.list}> <ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li> <li className={classes.listheader}>Passive Participants:</li>
{ passivePeers.map((peerName) => ( { passivePeers.map((peerId) => (
<li <li
key={peerName} key={peerId}
className={classNames(classes.listItem, { className={classNames(classes.listItem, {
selected : peerName === selectedPeerName selected : peerId === selectedPeerId
})} })}
onClick={() => roomClient.setSelectedPeer(peerName)} onClick={() => roomClient.setSelectedPeer(peerId)}
> >
<ListPeer name={peerName} advancedMode={advancedMode} /> <ListPeer id={peerId} advancedMode={advancedMode} />
</li> </li>
))} ))}
</ul> </ul>
@ -126,20 +126,20 @@ class ParticipantList extends React.PureComponent
ParticipantList.propTypes = ParticipantList.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
passivePeers : PropTypes.array, passivePeers : PropTypes.array,
selectedPeerName : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array, spotlightPeers : PropTypes.array,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
passivePeers : passivePeersSelector(state), passivePeers : passivePeersSelector(state),
selectedPeerName : state.room.selectedPeerName, selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state) spotlightPeers : spotlightPeersSelector(state)
}; };
}; };
@ -153,7 +153,7 @@ const ParticipantListContainer = withRoomContext(connect(
return ( return (
prev.peers === next.peers && prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights && prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerName === next.room.selectedPeerName prev.room.selectedPeerId === next.room.selectedPeerId
); );
} }
} }

View File

@ -139,9 +139,9 @@ class Democratic extends React.PureComponent
{ {
return ( return (
<Peer <Peer
key={peer.name} key={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
name={peer.name} id={peer.id}
style={style} style={style}
/> />
); );

View File

@ -104,11 +104,11 @@ class Filmstrip extends React.PureComponent
// Find the name of the peer which is currently speaking. This is either // Find the name of the peer which is currently speaking. This is either
// the latest active speaker, or the manually selected peer, or, if no // the latest active speaker, or the manually selected peer, or, if no
// person has spoken yet, the first peer in the list of peers. // person has spoken yet, the first peer in the list of peers.
getActivePeerName = () => getActivePeerId = () =>
{ {
if (this.props.selectedPeerName) if (this.props.selectedPeerId)
{ {
return this.props.selectedPeerName; return this.props.selectedPeerId;
} }
if (this.state.lastSpeaker) if (this.state.lastSpeaker)
@ -116,23 +116,23 @@ class Filmstrip extends React.PureComponent
return this.state.lastSpeaker; return this.state.lastSpeaker;
} }
const peerNames = Object.keys(this.props.peers); const peerIds = Object.keys(this.props.peers);
if (peerNames.length > 0) if (peerIds.length > 0)
{ {
return peerNames[0]; return peerIds[0];
} }
}; };
isSharingCamera = (peerName) => this.props.peers[peerName] && isSharingCamera = (peerId) => this.props.peers[peerId] &&
this.props.peers[peerName].consumers.some((consumer) => this.props.peers[peerId].consumers.some((consumer) =>
this.props.consumers[consumer].source === 'screen'); this.props.consumers[consumer].source === 'screen');
getRatio = () => getRatio = () =>
{ {
let ratio = 4 / 3; let ratio = 4 / 3;
if (this.isSharingCamera(this.getActivePeerName())) if (this.isSharingCamera(this.getActivePeerId()))
{ {
ratio *= 2; ratio *= 2;
} }
@ -202,12 +202,12 @@ class Filmstrip extends React.PureComponent
classes classes
} = this.props; } = this.props;
const activePeerName = this.getActivePeerName(); const activePeerId = this.getActivePeerId();
return ( return (
<div className={classes.root}> <div className={classes.root}>
<div className={classes.activePeerContainer} ref={this.activePeerContainer}> <div className={classes.activePeerContainer} ref={this.activePeerContainer}>
{ peers[activePeerName] ? { peers[activePeerId] ?
<div <div
className={classes.activePeer} className={classes.activePeer}
style={{ style={{
@ -217,7 +217,7 @@ class Filmstrip extends React.PureComponent
> >
<Peer <Peer
advancedMode={advancedMode} advancedMode={advancedMode}
name={activePeerName} name={activePeerId}
/> />
</div> </div>
:null :null
@ -226,23 +226,23 @@ class Filmstrip extends React.PureComponent
<div className={classes.filmStrip}> <div className={classes.filmStrip}>
<div className={classes.filmStripContent}> <div className={classes.filmStripContent}>
{ Object.keys(peers).map((peerName) => { Object.keys(peers).map((peerId) =>
{ {
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) if (spotlights.find((spotlightsElement) => spotlightsElement === peerId))
{ {
return ( return (
<div <div
key={peerName} key={peerId}
onClick={() => roomClient.setSelectedPeer(peerName)} onClick={() => roomClient.setSelectedPeer(peerId)}
className={classnames(classes.film, { className={classnames(classes.film, {
selected : this.props.selectedPeerName === peerName, selected : this.props.selectedPeerId === peerId,
active : this.state.lastSpeaker === peerName active : this.state.lastSpeaker === peerId
})} })}
> >
<div className={classes.filmContent}> <div className={classes.filmContent}>
<Peer <Peer
advancedMode={advancedMode} advancedMode={advancedMode}
name={peerName} name={peerId}
/> />
</div> </div>
</div> </div>
@ -276,7 +276,7 @@ Filmstrip.propTypes = {
peers : PropTypes.object.isRequired, peers : PropTypes.object.isRequired,
consumers : PropTypes.object.isRequired, consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired, myName : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightsLength : PropTypes.number, spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired, spotlights : PropTypes.array.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
@ -288,7 +288,7 @@ const mapStateToProps = (state) =>
return { return {
activeSpeakerName : state.room.activeSpeakerName, activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName, selectedPeerId : state.room.selectedPeerId,
peers : state.peers, peers : state.peers,
consumers : state.consumers, consumers : state.consumers,
myName : state.me.name, myName : state.me.name,

View File

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

View File

@ -102,13 +102,6 @@ const FullScreenView = (props) =>
return ( return (
<div className={classes.root}> <div className={classes.root}>
{ consumerVisible && !consumer.supported ?
<div className={classes.incompatibleVideo}>
<p>incompatible video</p>
</div>
:null
}
<div className={classes.controls}> <div className={classes.controls}>
<div <div
className={classnames(classes.button, { className={classnames(classes.button, {

View File

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

View File

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

View File

@ -3,12 +3,12 @@ import UrlParse from 'url-parse';
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getDeviceInfo } from 'mediasoup-client';
import randomString from 'random-string'; import randomString from 'random-string';
import Logger from './Logger'; import Logger from './Logger';
import debug from 'debug'; import debug from 'debug';
import RoomClient from './RoomClient'; import RoomClient from './RoomClient';
import RoomContext from './RoomContext'; import RoomContext from './RoomContext';
import deviceInfo from './deviceInfo';
import * as stateActions from './actions/stateActions'; import * as stateActions from './actions/stateActions';
import Room from './components/Room'; import Room from './components/Room';
import LoadingView from './components/LoadingView'; import LoadingView from './components/LoadingView';
@ -44,13 +44,15 @@ function run()
{ {
logger.debug('run() [environment:%s]', process.env.NODE_ENV); logger.debug('run() [environment:%s]', process.env.NODE_ENV);
const peerName = randomString({ length: 8 }).toLowerCase(); const peerId = randomString({ length: 8 }).toLowerCase();
const urlParser = new UrlParse(window.location.href, true); const urlParser = new UrlParse(window.location.href, true);
let roomId = (urlParser.pathname).substr(1) let roomId = (urlParser.pathname).substr(1)
? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase(); ? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
const produce = urlParser.query.produce !== 'false'; const produce = urlParser.query.produce !== 'false';
const consume = urlParser.query.consume !== 'false';
const useSimulcast = urlParser.query.simulcast === 'true'; const useSimulcast = urlParser.query.simulcast === 'true';
const forceTcp = urlParser.query.forceTcp === 'true';
if (!roomId) if (!roomId)
{ {
@ -80,21 +82,21 @@ function run()
const roomUrl = roomUrlParser.toString(); const roomUrl = roomUrlParser.toString();
// Get current device. // Get current device.
const device = getDeviceInfo(); const device = deviceInfo();
store.dispatch( store.dispatch(
stateActions.setRoomUrl(roomUrl)); stateActions.setRoomUrl(roomUrl));
store.dispatch( store.dispatch(
stateActions.setMe({ stateActions.setMe({
peerName, peerId,
device, device,
loginEnabled : window.config.loginEnabled loginEnabled : window.config.loginEnabled
}) })
); );
roomClient = new RoomClient( roomClient = new RoomClient(
{ roomId, peerName, device, useSimulcast, produce }); { roomId, peerId, device, useSimulcast, produce, consume, forceTcp });
global.CLIENT = roomClient; global.CLIENT = roomClient;

View File

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

View File

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

View File

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

View File

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

View File

@ -5,14 +5,14 @@ const initialState =
locked : false, locked : false,
lockedOut : false, lockedOut : false,
audioSuspended : false, audioSuspended : false,
activeSpeakerName : null, activeSpeakerId : null,
torrentSupport : false, torrentSupport : false,
showSettings : false, showSettings : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
windowConsumer : null, // ConsumerID windowConsumer : null, // ConsumerID
toolbarsVisible : true, toolbarsVisible : true,
mode : 'democratic', mode : 'democratic',
selectedPeerName : null, selectedPeerId : null,
spotlights : [], spotlights : [],
settingsOpen : false settingsOpen : false
}; };
@ -35,7 +35,7 @@ const room = (state = initialState, action) =>
if (roomState === 'connected') if (roomState === 'connected')
return { ...state, state: roomState }; return { ...state, state: roomState };
else else
return { ...state, state: roomState, activeSpeakerName: null }; return { ...state, state: roomState, activeSpeakerId: null };
} }
case 'SET_ROOM_LOCKED': case 'SET_ROOM_LOCKED':
@ -69,9 +69,9 @@ const room = (state = initialState, action) =>
case 'SET_ROOM_ACTIVE_SPEAKER': case 'SET_ROOM_ACTIVE_SPEAKER':
{ {
const { peerName } = action.payload; const { peerId } = action.payload;
return { ...state, activeSpeakerName: peerName }; return { ...state, activeSpeakerId: peerId };
} }
case 'FILE_SHARING_SUPPORTED': case 'FILE_SHARING_SUPPORTED':
@ -119,13 +119,13 @@ const room = (state = initialState, action) =>
case 'SET_SELECTED_PEER': case 'SET_SELECTED_PEER':
{ {
const { selectedPeerName } = action.payload; const { selectedPeerId } = action.payload;
return { return {
...state, ...state,
selectedPeerName : state.selectedPeerName === selectedPeerName ? selectedPeerId : state.selectedPeerId === selectedPeerId ?
null : selectedPeerName null : selectedPeerId
}; };
} }

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -10,6 +10,8 @@ const http = require('http');
const spdy = require('spdy'); const spdy = require('spdy');
const express = require('express'); const express = require('express');
const compression = require('compression'); const compression = require('compression');
const mediasoup = require('mediasoup');
const AwaitQueue = require('awaitqueue');
const Logger = require('./lib/Logger'); const Logger = require('./lib/Logger');
const Room = require('./lib/Room'); const Room = require('./lib/Room');
const utils = require('./util'); const utils = require('./util');
@ -17,7 +19,7 @@ const base64 = require('base-64');
// auth // auth
const passport = require('passport'); const passport = require('passport');
const { Issuer, Strategy } = require('openid-client'); const { Issuer, Strategy } = require('openid-client');
const session = require('express-session') const session = require('express-session');
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG); console.log('- process.env.DEBUG:', process.env.DEBUG);
@ -25,11 +27,18 @@ console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags); console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
/* eslint-enable no-console */ /* eslint-enable no-console */
// Start the mediasoup server.
const mediaServer = require('./mediasoup');
const logger = new Logger(); const logger = new Logger();
const queue = new AwaitQueue();
// mediasoup Workers.
// @type {Array<mediasoup.Worker>}
const mediasoupWorkers = [];
// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;
// Map of Room instances indexed by roomId. // Map of Room instances indexed by roomId.
const rooms = new Map(); const rooms = new Map();
@ -40,35 +49,84 @@ const tls =
key : fs.readFileSync(config.tls.key) key : fs.readFileSync(config.tls.key)
}; };
let app = express(); const app = express();
let httpsServer; let httpsServer;
let oidcClient; let oidcClient;
let oidcStrategy; let oidcStrategy;
passport.serializeUser(function(user, done) passport.serializeUser((user, done) =>
{ {
done(null, user); done(null, user);
}); });
passport.deserializeUser(function(user, done) passport.deserializeUser((user, done) =>
{ {
done(null, user); done(null, user);
}); });
const auth=config.auth; const auth = config.auth;
function setupAuth(oidcIssuer) async function run()
{
if (
typeof(auth) !== 'undefined' &&
typeof(auth.issuerURL) !== 'undefined' &&
typeof(auth.clientOptions) !== 'undefined'
)
{
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();
}
// Log rooms status every 30 seconds.
setInterval(() =>
{
for (const room of rooms.values())
{
room.logStatus();
}
}, 120000);
}
async function setupAuth(oidcIssuer)
{ {
oidcClient = new oidcIssuer.Client(auth.clientOptions); oidcClient = new oidcIssuer.Client(auth.clientOptions);
const params =
{ // ... any authorization request parameters go here
...auth.clientOptions // client_id defaults to client.client_id
// ... any authorization request parameters go here // redirect_uri defaults to client.redirect_uris[0]
// client_id defaults to client.client_id // response type defaults to client.response_types[0], then 'code'
// redirect_uri defaults to client.redirect_uris[0] // scope defaults to 'openid'
// response type defaults to client.response_types[0], then 'code' const params = auth.clientOptions;
// scope defaults to 'openid'
};
// optional, defaults to false, when true req is passed as a first // optional, defaults to false, when true req is passed as a first
// argument to verify fn // argument to verify fn
@ -78,63 +136,73 @@ function setupAuth(oidcIssuer)
// resolved from the issuer configuration, instead of true you may provide // resolved from the issuer configuration, instead of true you may provide
// any of the supported values directly, i.e. "S256" (recommended) or "plain" // any of the supported values directly, i.e. "S256" (recommended) or "plain"
const usePKCE = false; const usePKCE = false;
const client=oidcClient; const client = oidcClient;
oidcStrategy = new Strategy( oidcStrategy = new Strategy(
{ client, params, passReqToCallback, usePKCE }, { client, params, passReqToCallback, usePKCE },
(tokenset, userinfo, done) => (tokenset, userinfo, done) =>
{ {
let user = { const user =
id : tokenset.claims.sub, {
provider : tokenset.claims.iss, id : tokenset.claims.sub,
_userinfo : userinfo, provider : tokenset.claims.iss,
_claims : tokenset.claims, _userinfo : userinfo,
_claims : tokenset.claims
}; };
if (typeof(userinfo.picture) !== 'undefined')
if ( typeof(userinfo.picture) !== 'undefined' ){ {
if ( ! userinfo.picture.match(/^http/g) ) { if (!userinfo.picture.match(/^http/g))
{
user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ]; user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ];
} else { }
user.Photos= [ { value: userinfo.picture } ]; else
{
user.Photos = [ { value: userinfo.picture } ];
} }
} }
if ( typeof(userinfo.nickname) !== 'undefined' ){ if (typeof(userinfo.nickname) !== 'undefined')
user.displayName=userinfo.nickname; {
user.displayName = userinfo.nickname;
} }
if ( typeof(userinfo.name) !== 'undefined' ){ if (typeof(userinfo.name) !== 'undefined')
user.displayName=userinfo.name; {
user.displayName = userinfo.name;
} }
if ( typeof(userinfo.email) !== 'undefined' ){ if (typeof(userinfo.email) !== 'undefined')
user.emails=[{value: userinfo.email}]; {
user.emails = [ { value: userinfo.email } ];
} }
if ( typeof(userinfo.given_name) !== 'undefined' ){ if (typeof(userinfo.given_name) !== 'undefined')
user.name={givenName: userinfo.given_name}; {
user.name = { givenName: userinfo.given_name };
} }
if ( typeof(userinfo.family_name) !== 'undefined' ){ if (typeof(userinfo.family_name) !== 'undefined')
user.name={familyName: userinfo.family_name}; {
user.name = { familyName: userinfo.family_name };
} }
if ( typeof(userinfo.middle_name) !== 'undefined' ){ if (typeof(userinfo.middle_name) !== 'undefined')
user.name={middleName: userinfo.middle_name}; {
user.name = { middleName: userinfo.middle_name };
} }
return done(null, user); return done(null, user);
} }
); );
passport.use('oidc', oidcStrategy); passport.use('oidc', oidcStrategy);
app.use(session({ app.use(session({
secret: config.cookieSecret, secret : config.cookieSecret,
resave: true, resave : true,
saveUninitialized: true, saveUninitialized : true,
cookie: { secure: true } cookie : { secure: true }
})); }));
app.use(passport.initialize()); app.use(passport.initialize());
@ -145,20 +213,20 @@ function setupAuth(oidcIssuer)
{ {
passport.authenticate('oidc', { passport.authenticate('oidc', {
state : base64.encode(JSON.stringify({ state : base64.encode(JSON.stringify({
roomId : req.query.roomId, roomId : req.query.roomId,
peerName : req.query.peerName, peerId : req.query.peerId,
code : utils.random(10) code : utils.random(10)
})) }))
})(req, res, next); })(req, res, next);
}); });
// logout // logout
app.get('/auth/logout', function(req, res) app.get('/auth/logout', (req, res) =>
{ {
req.logout(); req.logout();
res.redirect('/'); res.redirect('/');
} });
);
// callback // callback
app.get( app.get(
'/auth/callback', '/auth/callback',
@ -169,21 +237,29 @@ function setupAuth(oidcIssuer)
if (rooms.has(state.roomId)) if (rooms.has(state.roomId))
{ {
let displayName,photo let displayName;
if (typeof(req.user) !== 'undefined'){ let photo;
if (typeof(req.user.displayName) !== 'undefined') displayName=req.user.displayName;
else displayName=""; if (typeof(req.user) !== 'undefined')
{
if (typeof(req.user.displayName) !== 'undefined')
displayName = req.user.displayName;
else
displayName = '';
if ( if (
typeof(req.user.Photos) !== 'undefined' && typeof(req.user.Photos) !== 'undefined' &&
typeof(req.user.Photos[0]) !== 'undefined' && typeof(req.user.Photos[0]) !== 'undefined' &&
typeof(req.user.Photos[0].value) !== 'undefined' typeof(req.user.Photos[0].value) !== 'undefined'
) photo=req.user.Photos[0].value; )
else photo="/static/media/buddy.403cb9f6.svg"; photo = req.user.Photos[0].value;
else
photo = '/static/media/buddy.403cb9f6.svg';
} }
const data = const data =
{ {
peerName : state.peerName, peerId : state.peerId,
name : displayName, name : displayName,
picture : photo picture : photo
}; };
@ -198,9 +274,12 @@ function setupAuth(oidcIssuer)
); );
} }
function setupWebServer() { async function runHttpsServer()
{
app.use(compression()); app.use(compression());
app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge'));
app.all('*', (req, res, next) => app.all('*', (req, res, next) =>
{ {
if (req.secure) if (req.secure)
@ -234,19 +313,23 @@ function setupWebServer() {
{ {
logger.info('Server redirecting port: ', config.listeningRedirectPort); logger.info('Server redirecting port: ', config.listeningRedirectPort);
}); });
}; }
function setupSocketIO(){ /**
* Create a protoo WebSocketServer to allow WebSocket connections from browsers.
*/
async function runWebSocketServer()
{
const io = require('socket.io')(httpsServer); const io = require('socket.io')(httpsServer);
// Handle connections from clients. // Handle connections from clients.
io.on('connection', (socket) => io.on('connection', (socket) =>
{ {
const { roomId, peerName } = socket.handshake.query; const { roomId, peerId } = socket.handshake.query;
if (!roomId || !peerName) if (!roomId || !peerId)
{ {
logger.warn('connection request without roomId and/or peerName'); logger.warn('connection request without roomId and/or peerId');
socket.disconnect(true); socket.disconnect(true);
@ -254,72 +337,90 @@ function setupSocketIO(){
} }
logger.info( logger.info(
'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName); 'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
let room; queue.push(async () =>
// If an unknown roomId, create a new Room.
if (!rooms.has(roomId))
{ {
logger.info('creating a new Room [roomId:"%s"]', roomId); const room = await getOrCreateRoom({ roomId });
try room.handleConnection({ peerId, socket });
{ })
room = new Room(roomId, mediaServer, io); .catch((error) =>
global.APP_ROOM = room;
}
catch (error)
{
logger.error('error creating a new Room: %s', error);
socket.disconnect(true);
return;
}
const logStatusTimer = setInterval(() =>
{
room.logStatus();
}, 30000);
rooms.set(roomId, room);
room.on('close', () =>
{
rooms.delete(roomId);
clearInterval(logStatusTimer);
});
}
else
{ {
room = rooms.get(roomId); logger.error('room creation or room joining failed:%o', error);
}
socket.room = roomId; socket.disconnect(true);
room.handleConnection(peerName, socket); return;
});
}); });
} }
if (
typeof(auth) !== 'undefined' && /**
typeof(auth.issuerURL) !== 'undefined' && * Launch as many mediasoup Workers as given in the configuration file.
typeof(auth.clientOptions) !== 'undefined' */
) async function runMediasoupWorkers()
{ {
Issuer.discover(auth.issuerURL).then((oidcIssuer) => const { numWorkers } = config.mediasoup;
logger.info('running %d mediasoup Workers...', numWorkers);
for (let i = 0; i < numWorkers; ++i)
{ {
setupAuth(oidcIssuer); const worker = await mediasoup.createWorker(
setupWebServer(); {
setupSocketIO(); logLevel : config.mediasoup.worker.logLevel,
}).catch((err) => { logTags : config.mediasoup.worker.logTags,
logger.error(err); 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);
} }
);
} else
{
logger.error('Auth is not configure properly!');
setupWebServer();
setupSocketIO();
} }
/**
* 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));
}
return room;
}
run();