Merge branch 'feature-lastn' into develop

master
Stefan Otto 2018-10-31 14:25:39 +01:00
commit c23522f978
38 changed files with 1240 additions and 358 deletions

View File

@ -3,6 +3,7 @@ import * as mediasoupClient from 'mediasoup-client';
import Logger from './Logger';
import hark from 'hark';
import ScreenShare from './ScreenShare';
import Spotlights from './Spotlights';
import { getSignalingUrl } from './urlFactory';
import * as cookiesManager from './cookiesManager';
import * as requestActions from './redux/requestActions';
@ -19,7 +20,8 @@ const ROOM_OPTIONS =
{
requestTimeout : requestTimeout,
transportOptions : transportOptions,
turnServers : turnServers
turnServers : turnServers,
maxSpotlights : 4
};
const VIDEO_CONSTRAINS =
@ -63,6 +65,9 @@ export default class RoomClient
// My peer name.
this._peerName = peerName;
// Alert sound
this._soundAlert = new Audio('/resources/sounds/notify.mp3');
// Socket.io peer connection
this._signalingSocket = io(signalingUrl);
@ -70,6 +75,12 @@ export default class RoomClient
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
this._room.roomId = roomId;
// Max spotlights
this._maxSpotlights = ROOM_OPTIONS.maxSpotlights;
// Manager of spotlight
this._spotlights = new Spotlights(this._maxSpotlights, this._room);
// Transport for sending.
this._sendTransport = null;
@ -280,7 +291,8 @@ export default class RoomClient
{
const {
chatHistory,
fileHistory
fileHistory,
lastN
} = await this.sendRequest('server-history');
if (chatHistory.length > 0)
@ -296,6 +308,18 @@ export default class RoomClient
this._dispatch(stateActions.addFileHistory(fileHistory));
}
if (lastN.length > 0)
{
logger.debug('Got lastN');
// Remove our self from list
const index = lastN.indexOf(this._peerName);
lastN.splice(index, 1);
this._spotlights.addSpeakerList(lastN);
}
}
catch (error)
{
@ -319,6 +343,43 @@ export default class RoomClient
this._micProducer.resume();
}
// Updated consumers based on spotlights
async updateSpotlights(spotlights)
{
logger.debug('updateSpotlights()');
try
{
for (const peer of this._room.peers)
{
if (spotlights.indexOf(peer.name) > -1) // Resume video for speaker
{
for (const consumer of peer.consumers)
{
if (consumer.kind !== 'video' || !consumer.supported)
continue;
await consumer.resume();
}
}
else // Pause video for everybody else
{
for (const consumer of peer.consumers)
{
if (consumer.kind !== 'video')
continue;
await consumer.pause('not-speaker');
}
}
}
}
catch (error)
{
logger.error('updateSpotlights() failed: %o', error);
}
}
installExtension()
{
logger.debug('installExtension()');
@ -395,7 +456,6 @@ export default class RoomClient
try
{
await this._updateWebcams();
await this._setWebcamProducer();
}
catch (error)
@ -484,6 +544,8 @@ export default class RoomClient
this._dispatch(
stateActions.setProducerTrack(this._micProducer.id, newTrack));
cookiesManager.setAudioDevice({ audioDeviceId: deviceId });
await this._updateAudioDevices();
}
catch (error)
@ -538,6 +600,8 @@ export default class RoomClient
this._dispatch(
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
cookiesManager.setVideoDevice({ videoDeviceId: deviceId });
await this._updateWebcams();
}
catch (error)
@ -611,6 +675,16 @@ export default class RoomClient
stateActions.setWebcamInProgress(false));
}
setSelectedPeer(peerName)
{
logger.debug('setSelectedPeer() [peerName:"%s"]', peerName);
this._spotlights.setPeerSpotlight(peerName);
this._dispatch(
stateActions.setSelectedPeer(peerName));
}
async mutePeerAudio(peerName)
{
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName);
@ -972,6 +1046,9 @@ export default class RoomClient
this._dispatch(
stateActions.setRoomActiveSpeaker(peerName));
if (peerName && peerName !== this._peerName)
this._spotlights.handleActiveSpeaker(peerName);
});
this._signalingSocket.on('display-name-changed', (data) =>
@ -1038,6 +1115,23 @@ export default class RoomClient
this._dispatch(
stateActions.addResponseMessage({ ...chatMessage, peerName }));
if (!this._getState().toolarea.toolAreaOpen ||
(this._getState().toolarea.toolAreaOpen &&
this._getState().toolarea.currentToolTab !== 'chat')) // Make sound
{
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
}
});
this._signalingSocket.on('file-receive', (data) =>
@ -1047,6 +1141,23 @@ export default class RoomClient
this._dispatch(stateActions.addFile(payload));
this.notify(`${payload.name} shared a file`);
if (!this._getState().toolarea.toolAreaOpen ||
(this._getState().toolarea.toolAreaOpen &&
this._getState().toolarea.currentToolTab !== 'files')) // Make sound
{
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
}
});
}
@ -1099,6 +1210,18 @@ export default class RoomClient
logger.debug(
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
this._handlePeer(peer);
});
@ -1139,31 +1262,20 @@ export default class RoomClient
}));
// Don't produce if explicitely requested to not to do it.
if (!this._produce)
return;
// NOTE: Don't depend on this Promise to continue (so we don't do return).
Promise.resolve()
// Add our mic.
.then(() =>
if (this._produce)
{
if (!this._room.canSend('audio'))
return;
if (this._room.canSend('audio'))
await this._setMicProducer();
this._setMicProducer()
.catch(() => {});
})
// Add our webcam (unless the cookie says no).
.then(() =>
if (this._room.canSend('video'))
{
if (!this._room.canSend('video'))
return;
const devicesCookie = cookiesManager.getDevices();
if (!devicesCookie || devicesCookie.webcamEnabled)
this.enableWebcam();
});
await this.enableWebcam();
}
}
this._dispatch(stateActions.setRoomState('connected'));
@ -1174,12 +1286,20 @@ export default class RoomClient
this.notify('You are in the room');
this._spotlights.on('spotlights-updated', (spotlights) =>
{
this._dispatch(stateActions.setSpotlights(spotlights));
this.updateSpotlights(spotlights);
});
const peers = this._room.peers;
for (const peer of peers)
{
this._handlePeer(peer, { notify: false });
}
this._spotlights.start();
}
catch (error)
{
@ -1203,10 +1323,6 @@ export default class RoomClient
try
{
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
await this._updateAudioDevices();
logger.debug('_setMicProducer() | calling getUserMedia()');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@ -1233,6 +1349,10 @@ export default class RoomClient
codec : producer.rtpParameters.codecs[0].name
}));
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
await this._updateAudioDevices();
producer.on('close', (originator) =>
{
logger.debug(
@ -1268,9 +1388,12 @@ export default class RoomClient
logger.debug('mic Producer "unhandled" event');
});
if (!stream.getAudioTracks()[0])
const harkStream = new MediaStream;
harkStream.addTrack(producer.track);
if (!harkStream.getAudioTracks()[0])
throw new Error('_setMicProducer(): given stream has no audio track');
producer.hark = hark(stream, { play: false });
producer.hark = hark(harkStream, { play: false });
// eslint-disable-next-line no-unused-vars
producer.hark.on('volume_change', (dBs, threshold) =>
@ -1423,18 +1546,12 @@ export default class RoomClient
try
{
const { device } = this._webcam;
if (!device)
throw new Error('no webcam devices');
logger.debug('_setWebcamProducer() | calling getUserMedia()');
const stream = await navigator.mediaDevices.getUserMedia(
{
video :
{
deviceId : { exact: device.deviceId },
...VIDEO_CONSTRAINS
}
});
@ -1456,14 +1573,15 @@ export default class RoomClient
{
id : producer.id,
source : 'webcam',
deviceLabel : device.label,
type : this._getWebcamType(device),
locallyPaused : producer.locallyPaused,
remotelyPaused : producer.remotelyPaused,
track : producer.track,
codec : producer.rtpParameters.codecs[0].name
}));
logger.debug('_setWebcamProducer() | calling _updateWebcams()');
await this._updateWebcams();
producer.on('close', (originator) =>
{
logger.debug(
@ -1549,9 +1667,6 @@ export default class RoomClient
else if (!this._audioDevices.has(currentAudioDeviceId))
this._audioDevice.device = array[0];
this._dispatch(
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
this._dispatch(
stateActions.setCanChangeAudioDevice(len >= 2));
if (len >= 1)
@ -1599,9 +1714,6 @@ export default class RoomClient
else if (!this._webcams.has(currentWebcamId))
this._webcam.device = array[0];
this._dispatch(
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
this._dispatch(
stateActions.setCanChangeWebcam(len >= 2));
if (len >= 1)
@ -1614,22 +1726,6 @@ export default class RoomClient
}
}
_getWebcamType(device)
{
if (/(back|rear)/i.test(device.label))
{
logger.debug('_getWebcamType() | it seems to be a back camera');
return 'back';
}
else
{
logger.debug('_getWebcamType() | it seems to be a front camera');
return 'front';
}
}
_handlePeer(peer, { notify = true } = {})
{
const displayName = peer.appData.displayName;
@ -1772,9 +1868,13 @@ export default class RoomClient
// Receive the consumer (if we can).
if (consumer.supported)
{
// Pause it if video and we are in audio-only mode.
if (consumer.kind === 'video' && this._getState().me.audioOnly)
consumer.pause('audio-only-mode');
if (consumer.kind === 'video' &&
!this._spotlights.peerInSpotlights(consumer.peer.name))
{ // Start paused
logger.debug(
'consumer paused by default');
consumer.pause('not-speaker');
}
consumer.receive(this._recvTransport)
.then((track) =>

View File

@ -0,0 +1,184 @@
import { EventEmitter } from 'events';
import Logger from './Logger';
const logger = new Logger('Spotlight');
export default class Spotlights extends EventEmitter
{
constructor(maxSpotlights, room)
{
super();
this._room = room;
this._maxSpotlights = maxSpotlights;
this._peerList = [];
this._selectedSpotlights = [];
this._currentSpotlights = [];
this._started = false;
}
start()
{
const peers = this._room.peers;
for (const peer of peers)
{
this._handlePeer(peer);
}
this._handleRoom();
this._started = true;
this._spotlightsUpdated();
}
peerInSpotlights(peerName)
{
if (this._started)
{
return this._currentSpotlights.indexOf(peerName) !== -1;
}
else
{
return false;
}
}
setPeerSpotlight(peerName)
{
logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName);
const index = this._selectedSpotlights.indexOf(peerName);
if (index !== -1)
{
this._selectedSpotlights = [];
}
else
{
this._selectedSpotlights = [ peerName ];
}
/*
if (index === -1) // We don't have this peer in the list, adding
{
this._selectedSpotlights.push(peerName);
}
else // We have this peer, remove
{
this._selectedSpotlights.splice(index, 1);
}
*/
if (this._started)
this._spotlightsUpdated();
}
_handleRoom()
{
this._room.on('newpeer', (peer) =>
{
logger.debug(
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
this._handlePeer(peer);
});
}
addSpeakerList(speakerList)
{
this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ];
if (this._started)
this._spotlightsUpdated();
}
_handlePeer(peer)
{
logger.debug('_handlePeer() [peerName:"%s"]', peer.name);
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);
if (index > -1)
{
this._peerList.splice(index, 1);
this._peerList = [ peerName ].concat(this._peerList);
this._spotlightsUpdated();
}
}
_spotlightsUpdated()
{
let spotlights;
if (this._selectedSpotlights.length > 0)
{
spotlights = [ ...new Set([ ...this._selectedSpotlights, ...this._peerList ]) ];
}
else
{
spotlights = this._peerList;
}
if (
!this._arraysEqual(
this._currentSpotlights, spotlights.slice(0, this._maxSpotlights)
)
)
{
logger.debug('_spotlightsUpdated() | spotlights updated, emitting');
this._currentSpotlights = spotlights.slice(0, this._maxSpotlights);
this.emit('spotlights-updated', this._currentSpotlights);
}
else
logger.debug('_spotlightsUpdated() | spotlights not updated');
}
_arraysEqual(arr1, arr2)
{
if (arr1.length !== arr2.length)
return false;
for (let i = arr1.length; i--;)
{
if (arr1[i] !== arr2[i])
return false;
}
return true;
}
}

View File

@ -34,6 +34,11 @@ class Chat extends Component
autoFocus={autofocus}
autoComplete='off'
/>
<input
type='submit'
className='send'
value='Send'
/>
</form>
</div>
);

View File

@ -30,7 +30,7 @@ class MessageList extends Component
return (
<div data-component='MessageList' id='messages'>
{
{ chatmessages.length > 0 ?
chatmessages.map((message, i) =>
{
const messageTime = new Date(message.time);
@ -61,6 +61,9 @@ class MessageList extends Component
</div>
);
})
:<div className='empty'>
<p>No one has said anything yet...</p>
</div>
}
</div>
);

View File

@ -13,14 +13,21 @@ class SharedFilesList extends Component
{
render()
{
const { sharing } = this.props;
return (
<div className='shared-files'>
{this.props.sharing.map((entry, i) => (
{ sharing.length > 0 ?
sharing.map((entry, i) => (
<FileEntry
data={entry}
key={i}
/>
))}
))
:<div className='empty'>
<p>No one has shared files yet...</p>
</div>
}
</div>
);
}

View File

@ -4,8 +4,9 @@ import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux';
import debounce from 'lodash/debounce';
import classnames from 'classnames';
import * as stateActions from '../redux/stateActions';
import * as requestActions from '../redux/requestActions';
import Peer from './Peer';
import HiddenPeers from './HiddenPeers';
class Filmstrip extends Component
{
@ -113,7 +114,7 @@ class Filmstrip extends Component
render()
{
const { peers, advancedMode } = this.props;
const { peers, advancedMode, spotlights, spotlightsLength } = this.props;
const activePeerName = this.getActivePeerName();
@ -138,7 +139,11 @@ class Filmstrip extends Component
<div className='filmstrip'>
<div className='filmstrip-content'>
{Object.keys(peers).map((peerName) => (
{
Object.keys(peers).map((peerName) =>
{
return (
spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
<div
key={peerName}
onClick={() => this.props.setSelectedPeer(peerName)}
@ -154,9 +159,20 @@ class Filmstrip extends Component
/>
</div>
</div>
))}
:null
);
})
}
</div>
</div>
<div className='hidden-peer-container'>
{ (spotlightsLength<Object.keys(peers).length)?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>:null
}
</div>
</div>
);
}
@ -169,19 +185,28 @@ Filmstrip.propTypes = {
consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string,
setSelectedPeer : PropTypes.func.isRequired
setSelectedPeer : PropTypes.func.isRequired,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired
};
const mapStateToProps = (state) => ({
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
});
myName : state.me.name,
spotlights : state.room.spotlights,
spotlightsLength
};
};
const mapDispatchToProps = {
setSelectedPeer : stateActions.setSelectedPeer
setSelectedPeer : requestActions.setSelectedPeer
};
export default connect(

View File

@ -0,0 +1,85 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as stateActions from '../redux/stateActions';
class HiddenPeers extends Component
{
constructor(props)
{
super(props);
this.state = { className: '' };
}
componentDidUpdate(prevProps)
{
const { hiddenPeersCount } = this.props;
if (hiddenPeersCount !== prevProps.hiddenPeersCount)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ className: 'pulse' }, () =>
{
if (this.timeout)
{
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() =>
{
this.setState({ className: '' });
}, 2000);
});
}
}
render()
{
const {
hiddenPeersCount,
openUsersTab
} = this.props;
return (
<div
data-component='HiddenPeers'
className={this.state.className}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<div data-component='HiddenPeersView'>
<div className={classnames('view-container', this.state.className)} onClick={() => openUsersTab()}>
<p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount === 1) ? null : 's'}
</p>
</div>
</div>
</div>
);
}
}
HiddenPeers.propTypes =
{
hiddenPeersCount : PropTypes.number,
openUsersTab : PropTypes.func.isRequired
};
const mapDispatchToProps = (dispatch) =>
{
return {
openUsersTab : () =>
{
dispatch(stateActions.openToolArea());
dispatch(stateActions.setToolTab('users'));
}
};
};
const HiddenPeersContainer = connect(
null,
mapDispatchToProps
)(HiddenPeers);
export default HiddenPeersContainer;

View File

@ -10,12 +10,9 @@ const ListPeer = (props) =>
const {
peer,
micConsumer,
webcamConsumer,
screenConsumer,
onMuteMic,
onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen
} = props;
@ -26,12 +23,6 @@ const ListPeer = (props) =>
!micConsumer.remotelyPaused
);
const videoVisible = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!webcamConsumer.remotelyPaused
);
const screenVisible = (
Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
@ -61,6 +52,9 @@ const ListPeer = (props) =>
:null
}
</div>
<div className='volume-container'>
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
</div>
<div className='controls'>
{ screenConsumer ?
<div
@ -84,28 +78,12 @@ const ListPeer = (props) =>
off : !micEnabled,
disabled : peer.peerAudioInProgress
})}
style={{ opacity : micEnabled && micConsumer ? (micConsumer.volume/10)
+ 0.2 :1 }}
onClick={(e) =>
{
e.stopPropagation();
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
}}
/>
<div
className={classnames('button', 'webcam', {
on : videoVisible,
off : !videoVisible,
disabled : peer.peerVideoInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
videoVisible ?
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
}}
/>
</div>
</div>
);

View File

@ -2,17 +2,32 @@ import React from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../redux/stateActions';
import * as requestActions from '../../redux/requestActions';
import PropTypes from 'prop-types';
import ListPeer from './ListPeer';
import ListMe from './ListMe';
const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerName }) => (
const ParticipantList =
({
advancedMode,
peers,
setSelectedPeer,
selectedPeerName,
spotlights
}) => (
<div data-component='ParticipantList'>
<ul className='list'>
<li className='list-header'>Me:</li>
<ListMe />
{peers.map((peer) => (
</ul>
<br />
<ul className='list'>
<li className='list-header'>Participants in Spotlight:</li>
{peers.filter((peer) =>
{
return (spotlights.find((spotlight) =>
{ return (spotlight === peer.name); }));
}).map((peer) => (
<li
key={peer.name}
className={classNames('list-item', {
@ -24,15 +39,36 @@ const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerNam
</li>
))}
</ul>
<br />
<ul className='list'>
<li className='list-header'>Passive Participants:</li>
{peers.filter((peer) =>
{
return !(spotlights.find((spotlight) =>
{ return (spotlight === peer.name); }));
}).map((peer) => (
<li
key={peer.name}
className={classNames('list-item', {
selected : peer.name === selectedPeerName
})}
onClick={() => setSelectedPeer(peer.name)}
>
<ListPeer name={peer.name} advancedMode={advancedMode} />
</li>
))}
</ul>
</div>
);
);
ParticipantList.propTypes =
{
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
setSelectedPeer : PropTypes.func.isRequired,
selectedPeerName : PropTypes.string
selectedPeerName : PropTypes.string,
spotlights : PropTypes.array.isRequired
};
const mapStateToProps = (state) =>
@ -41,12 +77,13 @@ const mapStateToProps = (state) =>
return {
peers : peersArray,
selectedPeerName : state.room.selectedPeerName
selectedPeerName : state.room.selectedPeerName,
spotlights : state.room.spotlights
};
};
const mapDispatchToProps = {
setSelectedPeer : stateActions.setSelectedPeer
setSelectedPeer : requestActions.setSelectedPeer
};
const ParticipantListContainer = connect(

View File

@ -38,10 +38,6 @@ class Peer extends Component
screenConsumer,
onMuteMic,
onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen,
toggleConsumerFullscreen,
style
} = this.props;
@ -90,6 +86,13 @@ class Peer extends Component
:null
}
{!videoVisible ?
<div className='paused-video'>
<p>this video is paused</p>
</div>
:null
}
<div className={classnames('view-container', 'webcam')} style={style}>
<div className='indicators'>
{peer.raiseHandState ?
@ -123,20 +126,6 @@ class Peer extends Component
}}
/>
<div
className={classnames('button', 'webcam', {
on : videoVisible,
off : !videoVisible,
disabled : peer.peerVideoInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
videoVisible ?
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
}}
/>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
@ -146,10 +135,10 @@ class Peer extends Component
}}
/>
</div>
<PeerView
advancedMode={advancedMode}
peer={peer}
audioTrack={micConsumer ? micConsumer.track : null}
volume={micConsumer ? micConsumer.volume : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible}
@ -166,20 +155,6 @@ class Peer extends Component
visible : this.state.controlsVisible
})}
>
<div
className={classnames('button', 'screen', {
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
onDisableScreen(peer.name) : onEnableScreen(peer.name);
}}
/>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
@ -213,12 +188,8 @@ Peer.propTypes =
screenConsumer : appPropTypes.Consumer,
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired,
streamDimensions : PropTypes.object,
style : PropTypes.object,
onEnableScreen : PropTypes.func.isRequired,
onDisableScreen : PropTypes.func.isRequired,
toggleConsumerFullscreen : PropTypes.func.isRequired
};
@ -253,23 +224,6 @@ const mapDispatchToProps = (dispatch) =>
{
dispatch(requestActions.unmutePeerAudio(peerName));
},
onEnableWebcam : (peerName) =>
{
dispatch(requestActions.resumePeerVideo(peerName));
},
onDisableWebcam : (peerName) =>
{
dispatch(requestActions.pausePeerVideo(peerName));
},
onEnableScreen : (peerName) =>
{
dispatch(requestActions.resumePeerScreen(peerName));
},
onDisableScreen : (peerName) =>
{
dispatch(requestActions.pausePeerScreen(peerName));
},
toggleConsumerFullscreen : (consumer) =>
{
if (consumer)

View File

@ -0,0 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import PeerAudio from './PeerAudio';
const AudioPeer = ({ micConsumer }) =>
{
return (
<PeerAudio
audioTrack={micConsumer ? micConsumer.track : null}
/>
);
};
AudioPeer.propTypes =
{
micConsumer : appPropTypes.Consumer,
name : PropTypes.string
};
const mapStateToProps = (state, { name }) =>
{
const peer = state.peers[name];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const micConsumer =
consumersArray.find((consumer) => consumer.source === 'mic');
return {
micConsumer
};
};
const AudioPeerContainer = connect(
mapStateToProps
)(AudioPeer);
export default AudioPeerContainer;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import AudioPeer from './AudioPeer';
const AudioPeers = ({ peers }) =>
{
return (
<div data-component='AudioPeers'>
{
peers.map((peer) =>
{
return (
<AudioPeer
key={peer.name}
name={peer.name}
/>
);
})
}
</div>
);
};
AudioPeers.propTypes =
{
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
};
const mapStateToProps = (state) =>
{
const peers = Object.values(state.peers);
return {
peers
};
};
const AudioPeersContainer = connect(
mapStateToProps
)(AudioPeers);
export default AudioPeersContainer;

View File

@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class PeerAudio extends React.Component
{
constructor(props)
{
super(props);
// Latest received audio track.
// @type {MediaStreamTrack}
this._audioTrack = null;
}
render()
{
return (
<audio
ref='audio'
autoPlay
/>
);
}
componentDidMount()
{
const { audioTrack } = this.props;
this._setTrack(audioTrack);
}
componentWillReceiveProps(nextProps)
{
const { audioTrack } = nextProps;
this._setTrack(audioTrack);
}
_setTrack(audioTrack)
{
if (this._audioTrack === audioTrack)
return;
this._audioTrack = audioTrack;
const { audio } = this.refs;
if (audioTrack)
{
const stream = new MediaStream;
if (audioTrack)
stream.addTrack(audioTrack);
audio.srcObject = stream;
}
else
{
audio.srcObject = null;
}
}
}
PeerAudio.propTypes =
{
audioTrack : PropTypes.any
};

View File

@ -6,6 +6,7 @@ import debounce from 'lodash/debounce';
import * as appPropTypes from './appPropTypes';
import { Appear } from './transitions';
import Peer from './Peer';
import HiddenPeers from './HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334;
@ -91,7 +92,9 @@ class Peers extends React.Component
const {
advancedMode,
activeSpeakerName,
peers
peers,
spotlights,
spotlightsLength
} = this.props;
const style =
@ -106,22 +109,35 @@ class Peers extends React.Component
peers.map((peer) =>
{
return (
(spotlights.find(function(spotlightsElement)
{ return spotlightsElement == peer.name; }))?
<Appear key={peer.name} duration={1000}>
<div
className={classnames('peer-container', {
'selected' : this.props.selectedPeerName === peer.name,
'active-speaker' : peer.name === activeSpeakerName
})}
>
<div className='peer-content'>
<Peer
advancedMode={advancedMode}
name={peer.name}
style={style}
/>
</div>
</div>
</Appear>
:null
);
})
}
<div className='hidden-peer-container'>
{ (spotlightsLength<peers.length)?
<HiddenPeers
hiddenPeersCount={peers.length-spotlightsLength}
/>:null
}
</div>
</div>
);
}
@ -132,20 +148,27 @@ Peers.propTypes =
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
boxes : PropTypes.number,
activeSpeakerName : PropTypes.string
activeSpeakerName : PropTypes.string,
selectedPeerName : PropTypes.string,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired
};
const mapStateToProps = (state) =>
{
const peers = Object.values(state.peers);
const boxes = peers.length + Object.values(state.consumers)
const spotlights = state.room.spotlights;
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
const boxes = spotlightsLength + Object.values(state.consumers)
.filter((consumer) => consumer.source === 'screen').length;
return {
peers,
boxes,
activeSpeakerName : state.room.activeSpeakerName
activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName,
spotlights,
spotlightsLength
};
};

View File

@ -4,12 +4,14 @@ import ReactTooltip from 'react-tooltip';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import CopyToClipboard from 'react-copy-to-clipboard';
import CookieConsent from 'react-cookie-consent';
import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions';
import { Appear } from './transitions';
import Me from './Me';
import Peers from './Peers';
import AudioPeers from './PeerAudio/AudioPeers';
import Notifications from './Notifications';
import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea';
@ -80,9 +82,16 @@ class Room extends React.Component
<Appear duration={300}>
<div data-component='Room'>
<CookieConsent>
This website uses cookies to enhance the user experience.
</CookieConsent>
<FullScreenView advancedMode={room.advancedMode} />
<div className='room-wrapper'>
<div data-component='Logo' />
<AudioPeers />
<Notifications />
<ToolAreaButton />

View File

@ -22,3 +22,13 @@ export function setDevices({ webcamEnabled })
{
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
}
export function setAudioDevice({ audioDeviceId })
{
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
}
export function setVideoDevice({ videoDeviceId })
{
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
}

View File

@ -8,7 +8,8 @@ const initialState =
fullScreenConsumer : null, // ConsumerID
toolbarsVisible : true,
mode : 'democratic',
selectedPeerName : null
selectedPeerName : null,
spotlights : []
};
const room = (state = initialState, action) =>
@ -83,6 +84,13 @@ const room = (state = initialState, action) =>
};
}
case 'SET_SPOTLIGHTS':
{
const { spotlights } = action.payload;
return { ...state, spotlights };
}
default:
return state;
}

View File

@ -19,6 +19,22 @@ const toolarea = (state = initialState, action) =>
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
}
case 'OPEN_TOOL_AREA':
{
const toolAreaOpen = true;
const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
const unreadFiles = state.currentToolTab === 'files' ? 0 : state.unreadFiles;
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
}
case 'CLOSE_TOOL_AREA':
{
const toolAreaOpen = false;
return { ...state, toolAreaOpen };
}
case 'SET_TOOL_TAB':
{
const { toolTab } = action.payload;

View File

@ -221,6 +221,14 @@ export const sendFile = (file, name, picture) =>
};
};
export const setSelectedPeer = (selectedPeerName) =>
{
return {
type : 'REQUEST_SELECTED_PEER',
payload : { selectedPeerName }
};
};
// This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) =>
{

View File

@ -237,6 +237,15 @@ export default ({ dispatch, getState }) => (next) =>
client.sendFile(action.payload);
break;
}
case 'REQUEST_SELECTED_PEER':
{
const { selectedPeerName } = action.payload;
client.setSelectedPeer(selectedPeerName);
break;
}
}
return next(action);

View File

@ -161,6 +161,20 @@ export const toggleToolArea = () =>
};
};
export const openToolArea = () =>
{
return {
type : 'OPEN_TOOL_AREA'
};
};
export const closeToolArea = () =>
{
return {
type : 'CLOSE_TOOL_AREA'
};
};
export const setToolTab = (toolTab) =>
{
return {
@ -474,7 +488,14 @@ export const loggedIn = () =>
type : 'LOGGED_IN'
});
export const setSelectedPeer = (selectedPeerName) => ({
export const setSelectedPeer = (selectedPeerName) =>
({
type : 'SET_SELECTED_PEER',
payload : { selectedPeerName }
});
});
export const setSpotlights = (spotlights) =>
({
type : 'SET_SPOTLIGHTS',
payload : { spotlights }
});

View File

@ -23,6 +23,7 @@
"prop-types": "^15.6.2",
"random-string": "^0.2.0",
"react": "^16.5.2",
"react-cookie-consent": "^1.9.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.5.2",
"react-draggable": "^3.0.5",

Binary file not shown.

View File

@ -0,0 +1,6 @@
[data-component='AudioPeers'] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
}

View File

@ -6,7 +6,7 @@
}
[data-component='MessageList'] {
overflow-y: scroll;
overflow-y: auto;
flex-grow: 1;
> .message {
@ -49,25 +49,55 @@
}
}
}
> .empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 20vmin;
> p {
padding: 6px 12px;
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 20px;
color: #000;
}
}
}
[data-component='Sender'] {
display: flex;
background-color: rgba(0, 0, 0, 0.1);
padding: 1rem;
background-color: #fff;
color: #000;
flex-shrink: 0;
border-radius: 5px;
margin-top: 0.5rem;
height: 3rem;
> .new-message {
width: 100%;
width: 80%;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
border: 0;
color: #FFF;
font-size: 1rem;
margin-right: 1vmin;
border-radius: 0.5vmin;
padding-left: 1vmin;
color: #000;
&.focus {
outline: none;
}
}
> .send {
width: 20%;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
border: 0;
background-color: #aef;
color: #000;
font-size: 1rem;
border-radius: 0.5vmin;
}
}

View File

@ -7,11 +7,10 @@
> .share-file {
cursor: pointer;
width: 100%;
background: #252525;
border: 1px solid #151515;
background: #aef;
padding: 1rem;
border-bottom: 5px solid #151515;
border-radius: 3px 3px 0 0;
border-radius: 1vmin;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
&.disabled {
cursor: not-allowed;
@ -21,7 +20,7 @@
> .shared-files {
flex-grow: 1;
overflow-y: scroll;
overflow-y: auto;
margin-top: 0.5rem;
> .file-entry {
@ -76,6 +75,23 @@
}
}
}
> .empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 20vmin;
> p {
padding: 6px 12px;
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 20px;
color: #000;
}
}
}
}

View File

@ -0,0 +1,93 @@
[data-component='HiddenPeersView'] {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
> .info {
$backgroundTint = #000;
position: absolute;
z-index: 10;
top: 0.6vmin;
left: 0.6vmin;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
> .view-container {
width: 12vmin;
height: 9vmin;
position: absolute;
bottom: 3%;
right: 3%;
color: #aaa;
cursor: pointer;
background-image: url('/resources/images/buddy.svg');
background-color: rgba(#2a4b58, 1);
background-position: bottom;
background-size: auto 85%;
background-repeat: no-repeat;
text-align: center;
vertical-align: middle;
line-height: 1.8vmin;
font-size: 1.7vmin;
font-weight: bolder;
animation: none;
&.pulse {
animation: pulse 2s;
}
}
.view-container>p{
transform: translate(0%,50%);
}
.view-container,
.view-container::before,
.view-container::after {
/* Add shadow to distinguish sheets from one another */
box-shadow: 2px 1px 1px rgba(0,0,0,0.15);
}
.view-container::before,
.view-container::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: #2a4b58;
}
/* Second sheet of paper */
.view-container::before {
left: .7vmin;
top: .7vmin;
z-index: -1;
}
/* Third sheet of paper */
.view-container::after {
left: 1.4vmin;
top: 1.4vmin;
z-index: -2;
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 1.0);
}
70% {
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}

View File

@ -1,10 +1,16 @@
[data-component='ParticipantList'] {
width: 100%;
overflow-y: auto;
padding: 6px;
> .list {
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \
0 4px 20px 0 rgba(0,0,0,0.19);
box-shadow: 0 2px 5px 2px rgba(0,0,0,0.2);
background-color: #fff;
> .list-header {
padding: 0.5rem;
font-weight: bolder;
}
> .list-item {
padding: 0.5rem;
border-bottom: 1px solid #CBCBCB;
@ -17,7 +23,7 @@
}
&.selected {
border-bottom-color: #377EFF;
background-color: #377eff;
}
}
}
@ -25,7 +31,6 @@
[data-component='ListPeer'] {
display: flex;
align-items: center;
> .indicators {
left: 0;
@ -75,6 +80,41 @@
}
}
}
> .volume-container {
float: right;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 1vmin;
position: relative;
background-size: 75%;
> .bar {
flex: 0 0 auto;
margin: 0.3rem;
background-size: 75%;
background-repeat: no-repeat;
cursor: pointer;
transition-property: opacity, background-color;
width: 3px;
border-radius: 6px;
transition-duration: 0.25s;
position: absolute;
bottom: 0px;
&.level0 { height: 0; background-color: rgba(#000, 0.8); }
&.level1 { height: 0.2vh; background-color: rgba(#000, 0.8); }
&.level2 { height: 0.4vh; background-color: rgba(#000, 0.8); }
&.level3 { height: 0.6vh; background-color: rgba(#000, 0.8); }
&.level4 { height: 0.8vh; background-color: rgba(#000, 0.8); }
&.level5 { height: 1.0vh; background-color: rgba(#000, 0.8); }
&.level6 { height: 1.2vh; background-color: rgba(#000, 0.8); }
&.level7 { height: 1.4vh; background-color: rgba(#000, 0.8); }
&.level8 { height: 1.6vh; background-color: rgba(#000, 0.8); }
&.level9 { height: 1.8vh; background-color: rgba(#000, 0.8); }
&.level10 { height: 2.0vh; background-color: rgba(#000, 0.8); }
}
}
> .controls {
float: right;
display: flex;

View File

@ -193,6 +193,28 @@
}
}
.paused-video {
position: absolute;
z-index: 11;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> p {
padding: 6px 12px;
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 20px;
color: rgba(#fff, 0.55);
}
}
.incompatible-video {
position: absolute;
z-index: 10;
@ -210,7 +232,7 @@
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 15px;
font-size: 20px;
color: rgba(#fff, 0.55);
}
}

View File

@ -39,6 +39,12 @@
&.active-speaker {
border-color: #fff;
}
&.selected {
> .peer-content {
border: 1px solid #377eff;
}
}
}
+mobile() {

View File

@ -195,6 +195,7 @@
outline: none;
padding: 8px 52px 8px 10px;
transition: all 200ms ease;
box-shadow: 0vmin 0vmin 0.2vmin 0vmin rgba(17,17,17,0.5);
}
.Dropdown-control:hover {

View File

@ -153,13 +153,13 @@
[data-component='ToolArea'] {
width: 100%;
height: 100%;
color: #fff;
color: #000;
position: fixed;
width: 0;
top: 0;
right: 0;
height: 100%;
background: rgba(50, 50, 50, 0.9);
background: #fff;
transition: width 0.3s;
z-index: 1010;
display: flex;
@ -168,7 +168,7 @@
> .tab-headers {
display: flex;
background: rgba(0, 0, 0, 0.1);
background: #ddd;
flex-shrink: 0;
> .tab-header {
@ -179,7 +179,9 @@
text-align: center;
&.checked {
background: rgba(0, 0, 0, 0.3);
background: #fff;
border-radius: 1vmin 1vmin 0vmin 0vmin;
box-shadow: 0.5vmin 0vmin 1vmin -0.5vmin #aaa;
}
> .badge {
@ -200,5 +202,6 @@
padding: 0.5rem;
display: flex;
flex-direction: column;
min-height: 0;
}
}

View File

@ -48,6 +48,7 @@ body {
@import './components/Peers';
@import './components/Peer';
@import './components/PeerView';
@import './components/HiddenPeersView';
@import './components/ScreenView';
@import './components/Notifications';
@import './components/Chat';
@ -58,6 +59,7 @@ body {
@import './components/FullView';
@import './components/Filmstrip';
@import './components/FileSharing';
@import './components/AudioPeers';
// Hack to detect in JS the current media query
#multiparty-meeting-media-query-detector {

View File

@ -1,7 +1,6 @@
'use strict';
const EventEmitter = require('events').EventEmitter;
const WebTorrent = require('webtorrent-hybrid');
const Logger = require('./Logger');
const config = require('../config');
@ -11,14 +10,6 @@ const BITRATE_FACTOR = 0.75;
const logger = new Logger('Room');
const torrentClient = new WebTorrent({
tracker : {
rtcConfig : {
iceServers : config.turnServers
}
}
});
class Room extends EventEmitter
{
constructor(roomId, mediaServer, io)
@ -38,6 +29,8 @@ class Room extends EventEmitter
this._fileHistory = [];
this._lastN = [];
this._io = io;
this._signalingPeers = new Map();
@ -79,6 +72,7 @@ class Room extends EventEmitter
if (this._signalingPeers)
for (let peer of this._signalingPeers)
{
if (peer.socket)
peer.socket.disconnect();
};
@ -123,9 +117,40 @@ class Room extends EventEmitter
const signalingPeer = { peerName : peerName, socket : socket };
const index = this._lastN.indexOf(peerName);
if (index === -1) // We don't have this peer, add to end
{
this._lastN.push(peerName);
}
this._signalingPeers.set(peerName, signalingPeer);
this._handleSignalingPeer(signalingPeer);
}
authCallback(data)
{
logger.debug('authCallback()');
const {
peerName,
name,
picture
} = data;
const signalingPeer = this._signalingPeers.get(peerName);
if (signalingPeer)
{
signalingPeer.socket.emit('auth',
{
name : name,
picture : picture
});
}
}
_handleMediaRoom()
{
logger.debug('_handleMediaRoom()');
@ -140,6 +165,14 @@ class Room extends EventEmitter
this._currentActiveSpeaker = activePeer;
const index = this._lastN.indexOf(activePeer.name);
if (index > -1) // We have this speaker in the list, move to front
{
this._lastN.splice(index, 1);
this._lastN = [activePeer.name].concat(this._lastN);
}
const activeVideoProducer = activePeer.producers
.find((producer) => producer.kind === 'video');
@ -269,7 +302,8 @@ class Room extends EventEmitter
null,
{
chatHistory : this._chatHistory,
fileHistory : this._fileHistory
fileHistory : this._fileHistory,
lastN : this._lastN
}
);
});
@ -279,15 +313,10 @@ class Room extends EventEmitter
// Return no error
cb(null);
const fileData = request.data.file;
const fileData = request.file;
this._fileHistory.push(fileData);
if (!torrentClient.get(fileData.file.magnet))
{
torrentClient.add(fileData.file.magnet);
}
// Spread to others
signalingPeer.socket.broadcast.to(this._roomId).emit(
'file-receive',
@ -302,10 +331,10 @@ class Room extends EventEmitter
// Return no error
cb(null);
const { raiseHandState } = request.data;
const { raiseHandState } = request;
const { mediaPeer } = signalingPeer;
mediaPeer.appData.raiseHandState = request.data.raiseHandState;
mediaPeer.appData.raiseHandState = request.raiseHandState;
// Spread to others
signalingPeer.socket.broadcast.to(this._roomId).emit(
'raisehand-message',
@ -325,6 +354,13 @@ class Room extends EventEmitter
if (mediaPeer && !mediaPeer.closed)
mediaPeer.close();
const index = this._lastN.indexOf(signalingPeer.peerName);
if (index > -1) // We have this peer in the list, remove
{
this._lastN.splice(index, 1);
}
// If this is the latest peer in the room, close the room.
// However wait a bit (for reconnections).
setTimeout(() =>

View File

@ -13,7 +13,6 @@
"express": "^4.16.3",
"mediasoup": "^2.1.0",
"passport-dataporten": "^1.3.0",
"webtorrent-hybrid": "^1.0.6",
"socket.io": "^2.1.1"
},
"devDependencies": {

View File

@ -43,14 +43,12 @@ const dataporten = new Dataporten.Setup(config.oauth2);
app.all('*', (req, res, next) =>
{
if(req.headers['x-forwarded-proto'] == 'http')
{
res.redirect('https://' + req.hostname + req.url);
}
else
if(req.secure)
{
return next();
}
res.redirect('https://' + req.hostname + req.url);
});
app.use(dataporten.passport.initialize());
@ -72,26 +70,23 @@ dataporten.setupLogout(app, '/logout');
app.get(
'/auth-callback',
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
(req, res) =>
{
const state = JSON.parse(base64.decode(req.query.state));
if (rooms.has(state.roomId))
{
const room = rooms.get(state.roomId)._protooRoom;
if (room.hasPeer(state.peerName))
const data =
{
const peer = room.getPeer(state.peerName);
peer.send('auth', {
peerName : state.peerName,
name : req.user.data.displayName,
picture : req.user.data.photos[0]
});
}
};
const room = rooms.get(state.roomId);
room.authCallback(data);
}
res.send('');