diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index fe0998c..5524293 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -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; + if (this._produce) + { + 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). - .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')); @@ -1171,15 +1283,23 @@ export default class RoomClient this._dispatch(stateActions.removeAllNotifications()); this.getServerHistory(); - + 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) => diff --git a/app/lib/Spotlights.js b/app/lib/Spotlights.js new file mode 100644 index 0000000..8b91e99 --- /dev/null +++ b/app/lib/Spotlights.js @@ -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; + } +} diff --git a/app/lib/components/Chat/Chat.jsx b/app/lib/components/Chat/Chat.jsx index 898bf75..33d3d53 100644 --- a/app/lib/components/Chat/Chat.jsx +++ b/app/lib/components/Chat/Chat.jsx @@ -34,6 +34,11 @@ class Chat extends Component autoFocus={autofocus} autoComplete='off' /> + ); diff --git a/app/lib/components/Chat/MessageList.jsx b/app/lib/components/Chat/MessageList.jsx index e80e17b..43a9671 100644 --- a/app/lib/components/Chat/MessageList.jsx +++ b/app/lib/components/Chat/MessageList.jsx @@ -30,7 +30,7 @@ class MessageList extends Component return (
- { + { chatmessages.length > 0 ? chatmessages.map((message, i) => { const messageTime = new Date(message.time); @@ -61,6 +61,9 @@ class MessageList extends Component
); }) + :
+

No one has said anything yet...

+
} ); diff --git a/app/lib/components/FileSharing/SharedFilesList.jsx b/app/lib/components/FileSharing/SharedFilesList.jsx index 9b2b6ce..28fc6b1 100644 --- a/app/lib/components/FileSharing/SharedFilesList.jsx +++ b/app/lib/components/FileSharing/SharedFilesList.jsx @@ -13,14 +13,21 @@ class SharedFilesList extends Component { render() { + const { sharing } = this.props; + return (
- {this.props.sharing.map((entry, i) => ( - - ))} + { sharing.length > 0 ? + sharing.map((entry, i) => ( + + )) + :
+

No one has shared files yet...

+
+ }
); } diff --git a/app/lib/components/Filmstrip.jsx b/app/lib/components/Filmstrip.jsx index 56adbe0..b0fe105 100644 --- a/app/lib/components/Filmstrip.jsx +++ b/app/lib/components/Filmstrip.jsx @@ -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 { @@ -26,11 +27,11 @@ class Filmstrip extends Component // person has spoken yet, the first peer in the list of peers. getActivePeerName = () => { - if (this.props.selectedPeerName) + if (this.props.selectedPeerName) { return this.props.selectedPeerName; } - + if (this.state.lastSpeaker) { return this.state.lastSpeaker; @@ -52,7 +53,7 @@ class Filmstrip extends Component { let ratio = 4 / 3; - if (this.isSharingCamera(this.getActivePeerName())) + if (this.isSharingCamera(this.getActivePeerName())) { ratio *= 2; } @@ -70,11 +71,11 @@ class Filmstrip extends Component let width = container.clientWidth; - if (width / ratio > container.clientHeight) + if (width / ratio > container.clientHeight) { width = container.clientHeight * ratio; } - + this.setState({ width }); @@ -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,25 +139,40 @@ class Filmstrip extends Component
- {Object.keys(peers).map((peerName) => ( -
this.props.setSelectedPeer(peerName)} - className={classnames('film', { - selected : this.props.selectedPeerName === peerName, - active : this.state.lastSpeaker === peerName - })} - > -
- -
-
- ))} + { + Object.keys(peers).map((peerName) => + { + return ( + spotlights.find((spotlightsElement) => spotlightsElement === peerName)? +
this.props.setSelectedPeer(peerName)} + className={classnames('film', { + selected : this.props.selectedPeerName === peerName, + active : this.state.lastSpeaker === peerName + })} + > +
+ +
+
+ :null + ); + }) + }
+
+ { (spotlightsLength:null + } +
+ ); } @@ -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) => ({ - activeSpeakerName : state.room.activeSpeakerName, - selectedPeerName : state.room.selectedPeerName, - peers : state.peers, - consumers : state.consumers, - myName : state.me.name -}); +const mapStateToProps = (state) => +{ + const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0; + + return { + activeSpeakerName : state.room.activeSpeakerName, + selectedPeerName : state.room.selectedPeerName, + peers : state.peers, + consumers : state.consumers, + myName : state.me.name, + spotlights : state.room.spotlights, + spotlightsLength + }; +}; const mapDispatchToProps = { - setSelectedPeer : stateActions.setSelectedPeer + setSelectedPeer : requestActions.setSelectedPeer }; export default connect( diff --git a/app/lib/components/HiddenPeers.jsx b/app/lib/components/HiddenPeers.jsx new file mode 100644 index 0000000..5c34d70 --- /dev/null +++ b/app/lib/components/HiddenPeers.jsx @@ -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 ( +
+
+
openUsersTab()}> +

+{hiddenPeersCount}
participant + {(hiddenPeersCount === 1) ? null : 's'} +

+
+
+
+ ); + } +} + +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; diff --git a/app/lib/components/ParticipantList/ListMe.jsx b/app/lib/components/ParticipantList/ListMe.jsx index 6b86025..460c738 100644 --- a/app/lib/components/ParticipantList/ListMe.jsx +++ b/app/lib/components/ParticipantList/ListMe.jsx @@ -35,4 +35,4 @@ const mapStateToProps = (state) => ({ export default connect( mapStateToProps -)(ListMe); \ No newline at end of file +)(ListMe); diff --git a/app/lib/components/ParticipantList/ListPeer.jsx b/app/lib/components/ParticipantList/ListPeer.jsx index fec2269..82e252c 100644 --- a/app/lib/components/ParticipantList/ListPeer.jsx +++ b/app/lib/components/ParticipantList/ListPeer.jsx @@ -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 } +
+
+
{ screenConsumer ?
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); }} /> - -
- { - e.stopPropagation(); - videoVisible ? - onDisableWebcam(peer.name) : onEnableWebcam(peer.name); - }} - />
); diff --git a/app/lib/components/ParticipantList/ParticipantList.jsx b/app/lib/components/ParticipantList/ParticipantList.jsx index 5f56983..e9f8492 100644 --- a/app/lib/components/ParticipantList/ParticipantList.jsx +++ b/app/lib/components/ParticipantList/ParticipantList.jsx @@ -2,37 +2,73 @@ 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 + }) => ( +
    +
      +
    • Me:
    • + +
    +
    +
      +
    • Participants in Spotlight:
    • + {peers.filter((peer) => + { + return (spotlights.find((spotlight) => + { return (spotlight === peer.name); })); + }).map((peer) => ( +
    • setSelectedPeer(peer.name)} + > + +
    • + ))} +
    +
    +
      +
    • Passive Participants:
    • + {peers.filter((peer) => + { + return !(spotlights.find((spotlight) => + { return (spotlight === peer.name); })); + }).map((peer) => ( +
    • setSelectedPeer(peer.name)} + > + +
    • + ))} +
    - {peers.map((peer) => ( -
  • setSelectedPeer(peer.name)} - > - -
  • - ))} -
-
-); +
+ ); 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( diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index 2aec88a..fc11ce3 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -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 ? +
+

this video is paused

+
+ :null + } +
{peer.raiseHandState ? @@ -123,20 +126,6 @@ class Peer extends Component }} /> -
- { - e.stopPropagation(); - videoVisible ? - onDisableWebcam(peer.name) : onEnableWebcam(peer.name); - }} - /> -
@@ -146,10 +135,10 @@ class Peer extends Component }} />
+ -
- { - e.stopPropagation(); - screenVisible ? - onDisableScreen(peer.name) : onEnableScreen(peer.name); - }} - /> -
@@ -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) diff --git a/app/lib/components/PeerAudio/AudioPeer.jsx b/app/lib/components/PeerAudio/AudioPeer.jsx new file mode 100644 index 0000000..ad1a3b6 --- /dev/null +++ b/app/lib/components/PeerAudio/AudioPeer.jsx @@ -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 ( + + ); +}; + +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; diff --git a/app/lib/components/PeerAudio/AudioPeers.jsx b/app/lib/components/PeerAudio/AudioPeers.jsx new file mode 100644 index 0000000..3dce02e --- /dev/null +++ b/app/lib/components/PeerAudio/AudioPeers.jsx @@ -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 ( +
+ { + peers.map((peer) => + { + return ( + + ); + }) + } +
+ ); +}; + +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; diff --git a/app/lib/components/PeerAudio/PeerAudio.jsx b/app/lib/components/PeerAudio/PeerAudio.jsx new file mode 100644 index 0000000..871c8c4 --- /dev/null +++ b/app/lib/components/PeerAudio/PeerAudio.jsx @@ -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 ( +
); } @@ -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 }; }; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 7a0bffe..64c10af 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -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'; @@ -32,16 +34,16 @@ class Room extends React.Component * given amount of time has passed since the * last time the cursor was moved. */ - waitForHide = idle(() => + waitForHide = idle(() => { this.props.setToolbarsVisible(false); }, TIMEOUT); - handleMovement = () => + handleMovement = () => { // If the toolbars were hidden, show them again when // the user moves their cursor. - if (!this.props.room.toolbarsVisible) + if (!this.props.room.toolbarsVisible) { this.props.setToolbarsVisible(true); } @@ -80,9 +82,16 @@ class Room extends React.Component
+ + This website uses cookies to enhance the user experience. + + +
+ + @@ -94,7 +103,7 @@ class Room extends React.Component
:null } - +
diff --git a/app/lib/cookiesManager.js b/app/lib/cookiesManager.js index 22e9e21..18f31cd 100644 --- a/app/lib/cookiesManager.js +++ b/app/lib/cookiesManager.js @@ -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 }); +} diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js index 19872ec..6a956fd 100644 --- a/app/lib/redux/reducers/room.js +++ b/app/lib/redux/reducers/room.js @@ -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; } diff --git a/app/lib/redux/reducers/toolarea.js b/app/lib/redux/reducers/toolarea.js index 0d25eca..556c3fd 100644 --- a/app/lib/redux/reducers/toolarea.js +++ b/app/lib/redux/reducers/toolarea.js @@ -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; @@ -30,7 +46,7 @@ const toolarea = (state = initialState, action) => case 'ADD_NEW_RESPONSE_MESSAGE': { - if (state.toolAreaOpen && state.currentToolTab === 'chat') + if (state.toolAreaOpen && state.currentToolTab === 'chat') { return state; } diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index 6ad7fbc..d9ba059 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -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 }) => { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index b4e266c..265c8cb 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -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); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 1d8d09f..fc2c096 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -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) => ({ - type : 'SET_SELECTED_PEER', - payload : { selectedPeerName } -}); +export const setSelectedPeer = (selectedPeerName) => + ({ + type : 'SET_SELECTED_PEER', + payload : { selectedPeerName } + }); + +export const setSpotlights = (spotlights) => + ({ + type : 'SET_SPOTLIGHTS', + payload : { spotlights } + }); diff --git a/app/package.json b/app/package.json index f4a3bbc..ff04fbd 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/resources/sounds/notify.mp3 b/app/resources/sounds/notify.mp3 new file mode 100644 index 0000000..cd09ed5 Binary files /dev/null and b/app/resources/sounds/notify.mp3 differ diff --git a/app/stylus/components/AudioPeers.styl b/app/stylus/components/AudioPeers.styl new file mode 100644 index 0000000..99e9c0a --- /dev/null +++ b/app/stylus/components/AudioPeers.styl @@ -0,0 +1,6 @@ +[data-component='AudioPeers'] { + position: absolute; + left: 0; + top: 0; + opacity: 0; +} diff --git a/app/stylus/components/Chat.styl b/app/stylus/components/Chat.styl index eeaa916..b6a480d 100644 --- a/app/stylus/components/Chat.styl +++ b/app/stylus/components/Chat.styl @@ -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; } } -} \ No newline at end of file + + > .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; + } +} diff --git a/app/stylus/components/FileSharing.styl b/app/stylus/components/FileSharing.styl index 45ef52a..1bf88fa 100644 --- a/app/stylus/components/FileSharing.styl +++ b/app/stylus/components/FileSharing.styl @@ -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; + } + } } } @@ -96,4 +112,4 @@ justify-content: center; font-size: 2rem; z-index: 3000; -} \ No newline at end of file +} diff --git a/app/stylus/components/Filmstrip.styl b/app/stylus/components/Filmstrip.styl index 65b94ef..5753049 100644 --- a/app/stylus/components/Filmstrip.styl +++ b/app/stylus/components/Filmstrip.styl @@ -1,78 +1,78 @@ [data-component='Filmstrip'] { - display: flex; - flex-direction: column; - align-items: center; - height: 100%; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; - > .active-peer-container { - width: 100%; - height: 80vh; - display: flex; - justify-content: center; - align-items: center; + > .active-peer-container { + width: 100%; + height: 80vh; + display: flex; + justify-content: center; + align-items: center; - > .active-peer { - width: 100%; - padding: 1vmin; + > .active-peer { + width: 100%; + padding: 1vmin; - > [data-component='Peer'] { - border: 5px solid rgba(255, 255, 255, 0.15); - box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5); - } - } - } + > [data-component='Peer'] { + border: 5px solid rgba(255, 255, 255, 0.15); + box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5); + } + } + } - > .filmstrip { - display: flex; - background: rgba(0, 0, 0 , 0.5); - width: 100%; - overflow-x: auto; - height: 20vh; - align-items: center; + > .filmstrip { + display: flex; + background: rgba(0, 0, 0 , 0.5); + width: 100%; + overflow-x: auto; + height: 20vh; + align-items: center; - > .filmstrip-content { - margin: 0 auto; - display: flex; - height: 100%; - align-items: center; + > .filmstrip-content { + margin: 0 auto; + display: flex; + height: 100%; + align-items: center; - > .film { - height: 18vh; - flex-shrink: 0; - padding-left: 1vh; + > .film { + height: 18vh; + flex-shrink: 0; + padding-left: 1vh; - &:last-child { - padding-right: 1vh; - } + &:last-child { + padding-right: 1vh; + } - > .film-content { - height: 100%; - width: 100%; - border: 1px solid rgba(255,255,255,0.15); + > .film-content { + height: 100%; + width: 100%; + border: 1px solid rgba(255,255,255,0.15); - > [data-component='Peer'] { - max-width: 18vh * (4 / 3); - cursor: pointer; + > [data-component='Peer'] { + max-width: 18vh * (4 / 3); + cursor: pointer; - &.screen { - max-width: 18vh * (2 * 4 / 3); - border: 0; - } - } - } + &.screen { + max-width: 18vh * (2 * 4 / 3); + border: 0; + } + } + } - &.active { - > .film-content { - border-color: #FFF; - } - } + &.active { + > .film-content { + border-color: #FFF; + } + } - &.selected { - > .film-content { - border-color: #377EFF; - } - } - } - } - } -} \ No newline at end of file + &.selected { + > .film-content { + border-color: #377EFF; + } + } + } + } + } +} diff --git a/app/stylus/components/HiddenPeersView.styl b/app/stylus/components/HiddenPeersView.styl new file mode 100644 index 0000000..b87ddf0 --- /dev/null +++ b/app/stylus/components/HiddenPeersView.styl @@ -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); + } +} diff --git a/app/stylus/components/ParticipantList.styl b/app/stylus/components/ParticipantList.styl index 2096b35..06ea717 100644 --- a/app/stylus/components/ParticipantList.styl +++ b/app/stylus/components/ParticipantList.styl @@ -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; diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index 7221917..1f1d493 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -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); } } diff --git a/app/stylus/components/Peers.styl b/app/stylus/components/Peers.styl index 0cd52e5..f9283f0 100644 --- a/app/stylus/components/Peers.styl +++ b/app/stylus/components/Peers.styl @@ -39,6 +39,12 @@ &.active-speaker { border-color: #fff; } + + &.selected { + > .peer-content { + border: 1px solid #377eff; + } + } } +mobile() { diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index ceec356..5984bfb 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -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 { diff --git a/app/stylus/components/ToolArea.styl b/app/stylus/components/ToolArea.styl index 8aed834..a2a8fd8 100644 --- a/app/stylus/components/ToolArea.styl +++ b/app/stylus/components/ToolArea.styl @@ -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; } -} \ No newline at end of file +} diff --git a/app/stylus/index.styl b/app/stylus/index.styl index 8a05a2e..309fa2b 100644 --- a/app/stylus/index.styl +++ b/app/stylus/index.styl @@ -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 { diff --git a/server/lib/Room.js b/server/lib/Room.js index 9f643fd..28fafb5 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -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,7 +72,8 @@ class Room extends EventEmitter if (this._signalingPeers) for (let peer of this._signalingPeers) { - peer.socket.disconnect(); + if (peer.socket) + peer.socket.disconnect(); }; this._signalingPeers.clear(); @@ -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(() => diff --git a/server/package.json b/server/package.json index 4bd415f..fd346eb 100644 --- a/server/package.json +++ b/server/package.json @@ -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": { diff --git a/server/server.js b/server/server.js index 4257887..4b56dbf 100755 --- a/server/server.js +++ b/server/server.js @@ -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); + peerName : state.peerName, + name : req.user.data.displayName, + picture : req.user.data.photos[0] + }; - peer.send('auth', { - name : req.user.data.displayName, - picture : req.user.data.photos[0] - }); - } + const room = rooms.get(state.roomId); + + room.authCallback(data); } res.send('');