Merge branch 'feature-lastn' into develop
This commit is contained in:
+162
-62
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,11 @@ class Chat extends Component
|
||||
autoFocus={autofocus}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
className='send'
|
||||
value='Send'
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ class MessageList extends Component
|
||||
|
||||
return (
|
||||
<div data-component='MessageList' id='messages'>
|
||||
{
|
||||
{ chatmessages.length > 0 ?
|
||||
chatmessages.map((message, i) =>
|
||||
{
|
||||
const messageTime = new Date(message.time);
|
||||
@@ -61,6 +61,9 @@ class MessageList extends Component
|
||||
</div>
|
||||
);
|
||||
})
|
||||
:<div className='empty'>
|
||||
<p>No one has said anything yet...</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,14 +13,21 @@ class SharedFilesList extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const { sharing } = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
{this.props.sharing.map((entry, i) => (
|
||||
<FileEntry
|
||||
data={entry}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
{ sharing.length > 0 ?
|
||||
sharing.map((entry, i) => (
|
||||
<FileEntry
|
||||
data={entry}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
:<div className='empty'>
|
||||
<p>No one has shared files yet...</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
<div className='filmstrip'>
|
||||
<div className='filmstrip-content'>
|
||||
{Object.keys(peers).map((peerName) => (
|
||||
<div
|
||||
key={peerName}
|
||||
onClick={() => this.props.setSelectedPeer(peerName)}
|
||||
className={classnames('film', {
|
||||
selected : this.props.selectedPeerName === peerName,
|
||||
active : this.state.lastSpeaker === peerName
|
||||
})}
|
||||
>
|
||||
<div className='film-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{
|
||||
Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
return (
|
||||
spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
|
||||
<div
|
||||
key={peerName}
|
||||
onClick={() => this.props.setSelectedPeer(peerName)}
|
||||
className={classnames('film', {
|
||||
selected : this.props.selectedPeerName === peerName,
|
||||
active : this.state.lastSpeaker === peerName
|
||||
})}
|
||||
>
|
||||
<div className='film-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:null
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden-peer-container'>
|
||||
{ (spotlightsLength<Object.keys(peers).length)?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||
/>:null
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -169,19 +185,28 @@ Filmstrip.propTypes = {
|
||||
consumers : PropTypes.object.isRequired,
|
||||
myName : PropTypes.string.isRequired,
|
||||
selectedPeerName : PropTypes.string,
|
||||
setSelectedPeer : PropTypes.func.isRequired
|
||||
setSelectedPeer : PropTypes.func.isRequired,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
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(
|
||||
|
||||
@@ -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;
|
||||
@@ -35,4 +35,4 @@ const mapStateToProps = (state) => ({
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(ListMe);
|
||||
)(ListMe);
|
||||
|
||||
@@ -10,12 +10,9 @@ const ListPeer = (props) =>
|
||||
const {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
onMuteMic,
|
||||
onUnmuteMic,
|
||||
onDisableWebcam,
|
||||
onEnableWebcam,
|
||||
onDisableScreen,
|
||||
onEnableScreen
|
||||
} = props;
|
||||
@@ -26,12 +23,6 @@ const ListPeer = (props) =>
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
@@ -61,6 +52,9 @@ const ListPeer = (props) =>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||
</div>
|
||||
<div className='controls'>
|
||||
{ screenConsumer ?
|
||||
<div
|
||||
@@ -84,28 +78,12 @@ const ListPeer = (props) =>
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
style={{ opacity : micEnabled && micConsumer ? (micConsumer.volume/10)
|
||||
+ 0.2 :1 }}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'webcam', {
|
||||
on : videoVisible,
|
||||
off : !videoVisible,
|
||||
disabled : peer.peerVideoInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
videoVisible ?
|
||||
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 }) => (
|
||||
<div data-component='ParticipantList'>
|
||||
<ul className='list'>
|
||||
<ListMe />
|
||||
const ParticipantList =
|
||||
({
|
||||
advancedMode,
|
||||
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) => (
|
||||
<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>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
|
||||
ParticipantList.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
setSelectedPeer : PropTypes.func.isRequired,
|
||||
selectedPeerName : PropTypes.string
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
@@ -41,12 +77,13 @@ const mapStateToProps = (state) =>
|
||||
|
||||
return {
|
||||
peers : peersArray,
|
||||
selectedPeerName : state.room.selectedPeerName
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights : state.room.spotlights
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setSelectedPeer : stateActions.setSelectedPeer
|
||||
setSelectedPeer : requestActions.setSelectedPeer
|
||||
};
|
||||
|
||||
const ParticipantListContainer = connect(
|
||||
|
||||
@@ -38,10 +38,6 @@ class Peer extends Component
|
||||
screenConsumer,
|
||||
onMuteMic,
|
||||
onUnmuteMic,
|
||||
onDisableWebcam,
|
||||
onEnableWebcam,
|
||||
onDisableScreen,
|
||||
onEnableScreen,
|
||||
toggleConsumerFullscreen,
|
||||
style
|
||||
} = this.props;
|
||||
@@ -90,6 +86,13 @@ class Peer extends Component
|
||||
:null
|
||||
}
|
||||
|
||||
{!videoVisible ?
|
||||
<div className='paused-video'>
|
||||
<p>this video is paused</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||
<div className='indicators'>
|
||||
{peer.raiseHandState ?
|
||||
@@ -123,20 +126,6 @@ class Peer extends Component
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'webcam', {
|
||||
on : videoVisible,
|
||||
off : !videoVisible,
|
||||
disabled : peer.peerVideoInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
videoVisible ?
|
||||
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'fullscreen')}
|
||||
onClick={(e) =>
|
||||
@@ -146,10 +135,10 @@ class Peer extends Component
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PeerView
|
||||
advancedMode={advancedMode}
|
||||
peer={peer}
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
volume={micConsumer ? micConsumer.volume : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
@@ -166,20 +155,6 @@ class Peer extends Component
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('button', 'screen', {
|
||||
on : screenVisible,
|
||||
off : !screenVisible,
|
||||
disabled : peer.peerScreenInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
screenVisible ?
|
||||
onDisableScreen(peer.name) : onEnableScreen(peer.name);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'fullscreen')}
|
||||
onClick={(e) =>
|
||||
@@ -213,12 +188,8 @@ Peer.propTypes =
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
onMuteMic : PropTypes.func.isRequired,
|
||||
onUnmuteMic : PropTypes.func.isRequired,
|
||||
onEnableWebcam : PropTypes.func.isRequired,
|
||||
onDisableWebcam : PropTypes.func.isRequired,
|
||||
streamDimensions : PropTypes.object,
|
||||
style : PropTypes.object,
|
||||
onEnableScreen : PropTypes.func.isRequired,
|
||||
onDisableScreen : PropTypes.func.isRequired,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -253,23 +224,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
dispatch(requestActions.unmutePeerAudio(peerName));
|
||||
},
|
||||
onEnableWebcam : (peerName) =>
|
||||
{
|
||||
|
||||
dispatch(requestActions.resumePeerVideo(peerName));
|
||||
},
|
||||
onDisableWebcam : (peerName) =>
|
||||
{
|
||||
dispatch(requestActions.pausePeerVideo(peerName));
|
||||
},
|
||||
onEnableScreen : (peerName) =>
|
||||
{
|
||||
dispatch(requestActions.resumePeerScreen(peerName));
|
||||
},
|
||||
onDisableScreen : (peerName) =>
|
||||
{
|
||||
dispatch(requestActions.pausePeerScreen(peerName));
|
||||
},
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import debounce from 'lodash/debounce';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import { Appear } from './transitions';
|
||||
import Peer from './Peer';
|
||||
import HiddenPeers from './HiddenPeers';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const RATIO = 1.334;
|
||||
@@ -26,7 +27,7 @@ class Peers extends React.Component
|
||||
|
||||
updateDimensions = debounce(() =>
|
||||
{
|
||||
if (!this.peersRef.current)
|
||||
if (!this.peersRef.current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -91,7 +92,9 @@ class Peers extends React.Component
|
||||
const {
|
||||
advancedMode,
|
||||
activeSpeakerName,
|
||||
peers
|
||||
peers,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
} = this.props;
|
||||
|
||||
const style =
|
||||
@@ -106,22 +109,35 @@ class Peers extends React.Component
|
||||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={peer.name} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'active-speaker' : peer.name === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peer.name}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
</Appear>
|
||||
(spotlights.find(function(spotlightsElement)
|
||||
{ return spotlightsElement == peer.name; }))?
|
||||
<Appear key={peer.name} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'selected' : this.props.selectedPeerName === peer.name,
|
||||
'active-speaker' : peer.name === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<div className='peer-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peer.name}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
:null
|
||||
);
|
||||
})
|
||||
}
|
||||
<div className='hidden-peer-container'>
|
||||
{ (spotlightsLength<peers.length)?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={peers.length-spotlightsLength}
|
||||
/>:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -132,20 +148,27 @@ Peers.propTypes =
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
boxes : PropTypes.number,
|
||||
activeSpeakerName : PropTypes.string
|
||||
activeSpeakerName : PropTypes.string,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peers = Object.values(state.peers);
|
||||
|
||||
const boxes = peers.length + Object.values(state.consumers)
|
||||
const spotlights = state.room.spotlights;
|
||||
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
|
||||
const boxes = spotlightsLength + Object.values(state.consumers)
|
||||
.filter((consumer) => consumer.source === 'screen').length;
|
||||
|
||||
return {
|
||||
peers,
|
||||
boxes,
|
||||
activeSpeakerName : state.room.activeSpeakerName
|
||||
activeSpeakerName : state.room.activeSpeakerName,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<CookieConsent>
|
||||
This website uses cookies to enhance the user experience.
|
||||
</CookieConsent>
|
||||
|
||||
<FullScreenView advancedMode={room.advancedMode} />
|
||||
|
||||
<div className='room-wrapper'>
|
||||
<div data-component='Logo' />
|
||||
<AudioPeers />
|
||||
|
||||
<Notifications />
|
||||
|
||||
<ToolAreaButton />
|
||||
@@ -94,7 +103,7 @@ class Room extends React.Component
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
|
||||
<div
|
||||
className={classnames('room-link-wrapper room-controls', {
|
||||
'visible' : this.props.room.toolbarsVisible
|
||||
@@ -123,7 +132,7 @@ class Room extends React.Component
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }) =>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user