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

View File

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

View File

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

View File

@ -4,8 +4,9 @@ import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import classnames from 'classnames'; import classnames from 'classnames';
import * as stateActions from '../redux/stateActions'; import * as requestActions from '../redux/requestActions';
import Peer from './Peer'; import Peer from './Peer';
import HiddenPeers from './HiddenPeers';
class Filmstrip extends Component class Filmstrip extends Component
{ {
@ -26,11 +27,11 @@ class Filmstrip extends Component
// 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 = () => getActivePeerName = () =>
{ {
if (this.props.selectedPeerName) if (this.props.selectedPeerName)
{ {
return this.props.selectedPeerName; return this.props.selectedPeerName;
} }
if (this.state.lastSpeaker) if (this.state.lastSpeaker)
{ {
return this.state.lastSpeaker; return this.state.lastSpeaker;
@ -52,7 +53,7 @@ class Filmstrip extends Component
{ {
let ratio = 4 / 3; let ratio = 4 / 3;
if (this.isSharingCamera(this.getActivePeerName())) if (this.isSharingCamera(this.getActivePeerName()))
{ {
ratio *= 2; ratio *= 2;
} }
@ -70,11 +71,11 @@ class Filmstrip extends Component
let width = container.clientWidth; let width = container.clientWidth;
if (width / ratio > container.clientHeight) if (width / ratio > container.clientHeight)
{ {
width = container.clientHeight * ratio; width = container.clientHeight * ratio;
} }
this.setState({ this.setState({
width width
}); });
@ -113,7 +114,7 @@ class Filmstrip extends Component
render() render()
{ {
const { peers, advancedMode } = this.props; const { peers, advancedMode, spotlights, spotlightsLength } = this.props;
const activePeerName = this.getActivePeerName(); const activePeerName = this.getActivePeerName();
@ -138,25 +139,40 @@ class Filmstrip extends Component
<div className='filmstrip'> <div className='filmstrip'>
<div className='filmstrip-content'> <div className='filmstrip-content'>
{Object.keys(peers).map((peerName) => ( {
<div Object.keys(peers).map((peerName) =>
key={peerName} {
onClick={() => this.props.setSelectedPeer(peerName)} return (
className={classnames('film', { spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
selected : this.props.selectedPeerName === peerName, <div
active : this.state.lastSpeaker === peerName key={peerName}
})} onClick={() => this.props.setSelectedPeer(peerName)}
> className={classnames('film', {
<div className='film-content'> selected : this.props.selectedPeerName === peerName,
<Peer active : this.state.lastSpeaker === peerName
advancedMode={advancedMode} })}
name={peerName} >
/> <div className='film-content'>
</div> <Peer
</div> advancedMode={advancedMode}
))} name={peerName}
/>
</div>
</div>
:null
);
})
}
</div> </div>
</div> </div>
<div className='hidden-peer-container'>
{ (spotlightsLength<Object.keys(peers).length)?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>:null
}
</div>
</div> </div>
); );
} }
@ -169,19 +185,28 @@ Filmstrip.propTypes = {
consumers : PropTypes.object.isRequired, consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired, myName : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string, selectedPeerName : PropTypes.string,
setSelectedPeer : PropTypes.func.isRequired setSelectedPeer : PropTypes.func.isRequired,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) =>
activeSpeakerName : state.room.activeSpeakerName, {
selectedPeerName : state.room.selectedPeerName, const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
peers : state.peers,
consumers : state.consumers, return {
myName : state.me.name activeSpeakerName : state.room.activeSpeakerName,
}); selectedPeerName : state.room.selectedPeerName,
peers : state.peers,
consumers : state.consumers,
myName : state.me.name,
spotlights : state.room.spotlights,
spotlightsLength
};
};
const mapDispatchToProps = { const mapDispatchToProps = {
setSelectedPeer : stateActions.setSelectedPeer setSelectedPeer : requestActions.setSelectedPeer
}; };
export default connect( 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

@ -35,4 +35,4 @@ const mapStateToProps = (state) => ({
export default connect( export default connect(
mapStateToProps mapStateToProps
)(ListMe); )(ListMe);

View File

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

View File

@ -2,37 +2,73 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../redux/stateActions'; import * as requestActions from '../../redux/requestActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ListPeer from './ListPeer'; import ListPeer from './ListPeer';
import ListMe from './ListMe'; import ListMe from './ListMe';
const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerName }) => ( const ParticipantList =
<div data-component='ParticipantList'> ({
<ul className='list'> advancedMode,
<ListMe /> peers,
setSelectedPeer,
selectedPeerName,
spotlights
}) => (
<div data-component='ParticipantList'>
<ul className='list'>
<li className='list-header'>Me:</li>
<ListMe />
</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', {
selected : peer.name === selectedPeerName
})}
onClick={() => setSelectedPeer(peer.name)}
>
<ListPeer name={peer.name} advancedMode={advancedMode} />
</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>
{peers.map((peer) => ( </div>
<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 = ParticipantList.propTypes =
{ {
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
setSelectedPeer : PropTypes.func.isRequired, setSelectedPeer : PropTypes.func.isRequired,
selectedPeerName : PropTypes.string selectedPeerName : PropTypes.string,
spotlights : PropTypes.array.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -41,12 +77,13 @@ const mapStateToProps = (state) =>
return { return {
peers : peersArray, peers : peersArray,
selectedPeerName : state.room.selectedPeerName selectedPeerName : state.room.selectedPeerName,
spotlights : state.room.spotlights
}; };
}; };
const mapDispatchToProps = { const mapDispatchToProps = {
setSelectedPeer : stateActions.setSelectedPeer setSelectedPeer : requestActions.setSelectedPeer
}; };
const ParticipantListContainer = connect( const ParticipantListContainer = connect(

View File

@ -38,10 +38,6 @@ class Peer extends Component
screenConsumer, screenConsumer,
onMuteMic, onMuteMic,
onUnmuteMic, onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen,
toggleConsumerFullscreen, toggleConsumerFullscreen,
style style
} = this.props; } = this.props;
@ -90,6 +86,13 @@ class Peer extends Component
:null :null
} }
{!videoVisible ?
<div className='paused-video'>
<p>this video is paused</p>
</div>
:null
}
<div className={classnames('view-container', 'webcam')} style={style}> <div className={classnames('view-container', 'webcam')} style={style}>
<div className='indicators'> <div className='indicators'>
{peer.raiseHandState ? {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 <div
className={classnames('button', 'fullscreen')} className={classnames('button', 'fullscreen')}
onClick={(e) => onClick={(e) =>
@ -146,10 +135,10 @@ class Peer extends Component
}} }}
/> />
</div> </div>
<PeerView <PeerView
advancedMode={advancedMode} advancedMode={advancedMode}
peer={peer} peer={peer}
audioTrack={micConsumer ? micConsumer.track : null}
volume={micConsumer ? micConsumer.volume : null} volume={micConsumer ? micConsumer.volume : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null} videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible} videoVisible={videoVisible}
@ -166,20 +155,6 @@ class Peer extends Component
visible : this.state.controlsVisible 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 <div
className={classnames('button', 'fullscreen')} className={classnames('button', 'fullscreen')}
onClick={(e) => onClick={(e) =>
@ -213,12 +188,8 @@ Peer.propTypes =
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
onMuteMic : PropTypes.func.isRequired, onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired, onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired,
streamDimensions : PropTypes.object, streamDimensions : PropTypes.object,
style : PropTypes.object, style : PropTypes.object,
onEnableScreen : PropTypes.func.isRequired,
onDisableScreen : PropTypes.func.isRequired,
toggleConsumerFullscreen : PropTypes.func.isRequired toggleConsumerFullscreen : PropTypes.func.isRequired
}; };
@ -253,23 +224,6 @@ const mapDispatchToProps = (dispatch) =>
{ {
dispatch(requestActions.unmutePeerAudio(peerName)); 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) => toggleConsumerFullscreen : (consumer) =>
{ {
if (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 * as appPropTypes from './appPropTypes';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Peer from './Peer'; import Peer from './Peer';
import HiddenPeers from './HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334; const RATIO = 1.334;
@ -26,7 +27,7 @@ class Peers extends React.Component
updateDimensions = debounce(() => updateDimensions = debounce(() =>
{ {
if (!this.peersRef.current) if (!this.peersRef.current)
{ {
return; return;
} }
@ -91,7 +92,9 @@ class Peers extends React.Component
const { const {
advancedMode, advancedMode,
activeSpeakerName, activeSpeakerName,
peers peers,
spotlights,
spotlightsLength
} = this.props; } = this.props;
const style = const style =
@ -106,22 +109,35 @@ class Peers extends React.Component
peers.map((peer) => peers.map((peer) =>
{ {
return ( return (
<Appear key={peer.name} duration={1000}> (spotlights.find(function(spotlightsElement)
<div { return spotlightsElement == peer.name; }))?
className={classnames('peer-container', { <Appear key={peer.name} duration={1000}>
'active-speaker' : peer.name === activeSpeakerName <div
})} className={classnames('peer-container', {
> 'selected' : this.props.selectedPeerName === peer.name,
<Peer 'active-speaker' : peer.name === activeSpeakerName
advancedMode={advancedMode} })}
name={peer.name} >
style={style} <div className='peer-content'>
/> <Peer
</div> advancedMode={advancedMode}
</Appear> 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> </div>
); );
} }
@ -132,20 +148,27 @@ Peers.propTypes =
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
boxes : PropTypes.number, boxes : PropTypes.number,
activeSpeakerName : PropTypes.string activeSpeakerName : PropTypes.string,
selectedPeerName : PropTypes.string,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
const peers = Object.values(state.peers); const peers = Object.values(state.peers);
const spotlights = state.room.spotlights;
const boxes = peers.length + Object.values(state.consumers) const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
const boxes = spotlightsLength + Object.values(state.consumers)
.filter((consumer) => consumer.source === 'screen').length; .filter((consumer) => consumer.source === 'screen').length;
return { return {
peers, peers,
boxes, 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 PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import CookieConsent from 'react-cookie-consent';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../redux/stateActions';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Me from './Me'; import Me from './Me';
import Peers from './Peers'; import Peers from './Peers';
import AudioPeers from './PeerAudio/AudioPeers';
import Notifications from './Notifications'; import Notifications from './Notifications';
import ToolAreaButton from './ToolArea/ToolAreaButton'; import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea'; import ToolArea from './ToolArea/ToolArea';
@ -32,16 +34,16 @@ class Room extends React.Component
* given amount of time has passed since the * given amount of time has passed since the
* last time the cursor was moved. * last time the cursor was moved.
*/ */
waitForHide = idle(() => waitForHide = idle(() =>
{ {
this.props.setToolbarsVisible(false); this.props.setToolbarsVisible(false);
}, TIMEOUT); }, TIMEOUT);
handleMovement = () => handleMovement = () =>
{ {
// If the toolbars were hidden, show them again when // If the toolbars were hidden, show them again when
// the user moves their cursor. // the user moves their cursor.
if (!this.props.room.toolbarsVisible) if (!this.props.room.toolbarsVisible)
{ {
this.props.setToolbarsVisible(true); this.props.setToolbarsVisible(true);
} }
@ -80,9 +82,16 @@ class Room extends React.Component
<Appear duration={300}> <Appear duration={300}>
<div data-component='Room'> <div data-component='Room'>
<CookieConsent>
This website uses cookies to enhance the user experience.
</CookieConsent>
<FullScreenView advancedMode={room.advancedMode} /> <FullScreenView advancedMode={room.advancedMode} />
<div className='room-wrapper'> <div className='room-wrapper'>
<div data-component='Logo' /> <div data-component='Logo' />
<AudioPeers />
<Notifications /> <Notifications />
<ToolAreaButton /> <ToolAreaButton />
@ -94,7 +103,7 @@ class Room extends React.Component
</div> </div>
:null :null
} }
<div <div
className={classnames('room-link-wrapper room-controls', { className={classnames('room-link-wrapper room-controls', {
'visible' : this.props.room.toolbarsVisible 'visible' : this.props.room.toolbarsVisible
@ -123,7 +132,7 @@ class Room extends React.Component
{ {
return; return;
} }
event.preventDefault(); event.preventDefault();
}} }}
> >

View File

@ -22,3 +22,13 @@ export function setDevices({ webcamEnabled })
{ {
jsCookie.set(DEVICES_COOKIE, { 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 fullScreenConsumer : null, // ConsumerID
toolbarsVisible : true, toolbarsVisible : true,
mode : 'democratic', mode : 'democratic',
selectedPeerName : null selectedPeerName : null,
spotlights : []
}; };
const room = (state = initialState, action) => 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: default:
return state; return state;
} }

View File

@ -19,6 +19,22 @@ const toolarea = (state = initialState, action) =>
return { ...state, toolAreaOpen, unreadMessages, unreadFiles }; 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': case 'SET_TOOL_TAB':
{ {
const { toolTab } = action.payload; const { toolTab } = action.payload;
@ -30,7 +46,7 @@ const toolarea = (state = initialState, action) =>
case 'ADD_NEW_RESPONSE_MESSAGE': case 'ADD_NEW_RESPONSE_MESSAGE':
{ {
if (state.toolAreaOpen && state.currentToolTab === 'chat') if (state.toolAreaOpen && state.currentToolTab === 'chat')
{ {
return state; return state;
} }

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). // This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) => export const notify = ({ type = 'info', text, timeout }) =>
{ {

View File

@ -237,6 +237,15 @@ export default ({ dispatch, getState }) => (next) =>
client.sendFile(action.payload); client.sendFile(action.payload);
break; break;
} }
case 'REQUEST_SELECTED_PEER':
{
const { selectedPeerName } = action.payload;
client.setSelectedPeer(selectedPeerName);
break;
}
} }
return next(action); 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) => export const setToolTab = (toolTab) =>
{ {
return { return {
@ -474,7 +488,14 @@ export const loggedIn = () =>
type : 'LOGGED_IN' type : 'LOGGED_IN'
}); });
export const setSelectedPeer = (selectedPeerName) => ({ export const setSelectedPeer = (selectedPeerName) =>
type : 'SET_SELECTED_PEER', ({
payload : { 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", "prop-types": "^15.6.2",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^16.5.2", "react": "^16.5.2",
"react-cookie-consent": "^1.9.0",
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.5.2", "react-dom": "^16.5.2",
"react-draggable": "^3.0.5", "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'] { [data-component='MessageList'] {
overflow-y: scroll; overflow-y: auto;
flex-grow: 1; flex-grow: 1;
> .message { > .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'] { [data-component='Sender'] {
display: flex; display: flex;
background-color: rgba(0, 0, 0, 0.1); background-color: #fff;
padding: 1rem; color: #000;
flex-shrink: 0; flex-shrink: 0;
border-radius: 5px;
margin-top: 0.5rem; margin-top: 0.5rem;
height: 3rem; height: 3rem;
> .new-message { > .new-message {
width: 100%; width: 80%;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
border: 0; border: 0;
color: #FFF;
font-size: 1rem; font-size: 1rem;
margin-right: 1vmin;
border-radius: 0.5vmin;
padding-left: 1vmin;
color: #000;
&.focus { &.focus {
outline: none; 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 { > .share-file {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
background: #252525; background: #aef;
border: 1px solid #151515;
padding: 1rem; padding: 1rem;
border-bottom: 5px solid #151515; border-radius: 1vmin;
border-radius: 3px 3px 0 0; box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
@ -21,7 +20,7 @@
> .shared-files { > .shared-files {
flex-grow: 1; flex-grow: 1;
overflow-y: scroll; overflow-y: auto;
margin-top: 0.5rem; margin-top: 0.5rem;
> .file-entry { > .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;
}
}
} }
} }
@ -96,4 +112,4 @@
justify-content: center; justify-content: center;
font-size: 2rem; font-size: 2rem;
z-index: 3000; z-index: 3000;
} }

View File

@ -1,78 +1,78 @@
[data-component='Filmstrip'] { [data-component='Filmstrip'] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 100%; height: 100%;
> .active-peer-container { > .active-peer-container {
width: 100%; width: 100%;
height: 80vh; height: 80vh;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
> .active-peer { > .active-peer {
width: 100%; width: 100%;
padding: 1vmin; padding: 1vmin;
> [data-component='Peer'] { > [data-component='Peer'] {
border: 5px solid rgba(255, 255, 255, 0.15); border: 5px solid rgba(255, 255, 255, 0.15);
box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5); box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
} }
} }
} }
> .filmstrip { > .filmstrip {
display: flex; display: flex;
background: rgba(0, 0, 0 , 0.5); background: rgba(0, 0, 0 , 0.5);
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
height: 20vh; height: 20vh;
align-items: center; align-items: center;
> .filmstrip-content { > .filmstrip-content {
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
height: 100%; height: 100%;
align-items: center; align-items: center;
> .film { > .film {
height: 18vh; height: 18vh;
flex-shrink: 0; flex-shrink: 0;
padding-left: 1vh; padding-left: 1vh;
&:last-child { &:last-child {
padding-right: 1vh; padding-right: 1vh;
} }
> .film-content { > .film-content {
height: 100%; height: 100%;
width: 100%; width: 100%;
border: 1px solid rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.15);
> [data-component='Peer'] { > [data-component='Peer'] {
max-width: 18vh * (4 / 3); max-width: 18vh * (4 / 3);
cursor: pointer; cursor: pointer;
&.screen { &.screen {
max-width: 18vh * (2 * 4 / 3); max-width: 18vh * (2 * 4 / 3);
border: 0; border: 0;
} }
} }
} }
&.active { &.active {
> .film-content { > .film-content {
border-color: #FFF; border-color: #FFF;
} }
} }
&.selected { &.selected {
> .film-content { > .film-content {
border-color: #377EFF; border-color: #377EFF;
} }
} }
} }
} }
} }
} }

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'] { [data-component='ParticipantList'] {
width: 100%; width: 100%;
overflow-y: auto;
padding: 6px;
> .list { > .list {
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \ box-shadow: 0 2px 5px 2px rgba(0,0,0,0.2);
0 4px 20px 0 rgba(0,0,0,0.19); background-color: #fff;
> .list-header {
padding: 0.5rem;
font-weight: bolder;
}
> .list-item { > .list-item {
padding: 0.5rem; padding: 0.5rem;
border-bottom: 1px solid #CBCBCB; border-bottom: 1px solid #CBCBCB;
@ -17,7 +23,7 @@
} }
&.selected { &.selected {
border-bottom-color: #377EFF; background-color: #377eff;
} }
} }
} }
@ -25,7 +31,6 @@
[data-component='ListPeer'] { [data-component='ListPeer'] {
display: flex; display: flex;
align-items: center;
> .indicators { > .indicators {
left: 0; 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 { > .controls {
float: right; float: right;
display: flex; 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 { .incompatible-video {
position: absolute; position: absolute;
z-index: 10; z-index: 10;
@ -210,7 +232,7 @@
border-radius: 6px; border-radius: 6px;
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
font-size: 15px; font-size: 20px;
color: rgba(#fff, 0.55); color: rgba(#fff, 0.55);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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