Merge branch 'feature-lastn' into develop
commit
c23522f978
|
|
@ -3,6 +3,7 @@ import * as mediasoupClient from 'mediasoup-client';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
import hark from 'hark';
|
import hark from 'hark';
|
||||||
import ScreenShare from './ScreenShare';
|
import ScreenShare from './ScreenShare';
|
||||||
|
import Spotlights from './Spotlights';
|
||||||
import { getSignalingUrl } from './urlFactory';
|
import { getSignalingUrl } from './urlFactory';
|
||||||
import * as cookiesManager from './cookiesManager';
|
import * as cookiesManager from './cookiesManager';
|
||||||
import * as requestActions from './redux/requestActions';
|
import * as requestActions from './redux/requestActions';
|
||||||
|
|
@ -19,7 +20,8 @@ const ROOM_OPTIONS =
|
||||||
{
|
{
|
||||||
requestTimeout : requestTimeout,
|
requestTimeout : requestTimeout,
|
||||||
transportOptions : transportOptions,
|
transportOptions : transportOptions,
|
||||||
turnServers : turnServers
|
turnServers : turnServers,
|
||||||
|
maxSpotlights : 4
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIDEO_CONSTRAINS =
|
const VIDEO_CONSTRAINS =
|
||||||
|
|
@ -63,6 +65,9 @@ export default class RoomClient
|
||||||
// My peer name.
|
// My peer name.
|
||||||
this._peerName = peerName;
|
this._peerName = peerName;
|
||||||
|
|
||||||
|
// Alert sound
|
||||||
|
this._soundAlert = new Audio('/resources/sounds/notify.mp3');
|
||||||
|
|
||||||
// Socket.io peer connection
|
// Socket.io peer connection
|
||||||
this._signalingSocket = io(signalingUrl);
|
this._signalingSocket = io(signalingUrl);
|
||||||
|
|
||||||
|
|
@ -70,6 +75,12 @@ export default class RoomClient
|
||||||
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
|
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
|
||||||
this._room.roomId = roomId;
|
this._room.roomId = roomId;
|
||||||
|
|
||||||
|
// Max spotlights
|
||||||
|
this._maxSpotlights = ROOM_OPTIONS.maxSpotlights;
|
||||||
|
|
||||||
|
// Manager of spotlight
|
||||||
|
this._spotlights = new Spotlights(this._maxSpotlights, this._room);
|
||||||
|
|
||||||
// Transport for sending.
|
// Transport for sending.
|
||||||
this._sendTransport = null;
|
this._sendTransport = null;
|
||||||
|
|
||||||
|
|
@ -280,7 +291,8 @@ export default class RoomClient
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
chatHistory,
|
chatHistory,
|
||||||
fileHistory
|
fileHistory,
|
||||||
|
lastN
|
||||||
} = await this.sendRequest('server-history');
|
} = await this.sendRequest('server-history');
|
||||||
|
|
||||||
if (chatHistory.length > 0)
|
if (chatHistory.length > 0)
|
||||||
|
|
@ -296,6 +308,18 @@ export default class RoomClient
|
||||||
|
|
||||||
this._dispatch(stateActions.addFileHistory(fileHistory));
|
this._dispatch(stateActions.addFileHistory(fileHistory));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastN.length > 0)
|
||||||
|
{
|
||||||
|
logger.debug('Got lastN');
|
||||||
|
|
||||||
|
// Remove our self from list
|
||||||
|
const index = lastN.indexOf(this._peerName);
|
||||||
|
|
||||||
|
lastN.splice(index, 1);
|
||||||
|
|
||||||
|
this._spotlights.addSpeakerList(lastN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
|
|
@ -319,6 +343,43 @@ export default class RoomClient
|
||||||
this._micProducer.resume();
|
this._micProducer.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated consumers based on spotlights
|
||||||
|
async updateSpotlights(spotlights)
|
||||||
|
{
|
||||||
|
logger.debug('updateSpotlights()');
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (spotlights.indexOf(peer.name) > -1) // Resume video for speaker
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.kind !== 'video' || !consumer.supported)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await consumer.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // Pause video for everybody else
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.kind !== 'video')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await consumer.pause('not-speaker');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
logger.error('updateSpotlights() failed: %o', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
installExtension()
|
installExtension()
|
||||||
{
|
{
|
||||||
logger.debug('installExtension()');
|
logger.debug('installExtension()');
|
||||||
|
|
@ -395,7 +456,6 @@ export default class RoomClient
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await this._updateWebcams();
|
|
||||||
await this._setWebcamProducer();
|
await this._setWebcamProducer();
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
|
|
@ -484,6 +544,8 @@ export default class RoomClient
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setProducerTrack(this._micProducer.id, newTrack));
|
stateActions.setProducerTrack(this._micProducer.id, newTrack));
|
||||||
|
|
||||||
|
cookiesManager.setAudioDevice({ audioDeviceId: deviceId });
|
||||||
|
|
||||||
await this._updateAudioDevices();
|
await this._updateAudioDevices();
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
|
|
@ -538,6 +600,8 @@ export default class RoomClient
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
|
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
|
||||||
|
|
||||||
|
cookiesManager.setVideoDevice({ videoDeviceId: deviceId });
|
||||||
|
|
||||||
await this._updateWebcams();
|
await this._updateWebcams();
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
|
|
@ -611,6 +675,16 @@ export default class RoomClient
|
||||||
stateActions.setWebcamInProgress(false));
|
stateActions.setWebcamInProgress(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedPeer(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('setSelectedPeer() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._spotlights.setPeerSpotlight(peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setSelectedPeer(peerName));
|
||||||
|
}
|
||||||
|
|
||||||
async mutePeerAudio(peerName)
|
async mutePeerAudio(peerName)
|
||||||
{
|
{
|
||||||
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName);
|
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName);
|
||||||
|
|
@ -972,6 +1046,9 @@ export default class RoomClient
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setRoomActiveSpeaker(peerName));
|
stateActions.setRoomActiveSpeaker(peerName));
|
||||||
|
|
||||||
|
if (peerName && peerName !== this._peerName)
|
||||||
|
this._spotlights.handleActiveSpeaker(peerName);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._signalingSocket.on('display-name-changed', (data) =>
|
this._signalingSocket.on('display-name-changed', (data) =>
|
||||||
|
|
@ -1038,6 +1115,23 @@ export default class RoomClient
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.addResponseMessage({ ...chatMessage, peerName }));
|
stateActions.addResponseMessage({ ...chatMessage, peerName }));
|
||||||
|
|
||||||
|
if (!this._getState().toolarea.toolAreaOpen ||
|
||||||
|
(this._getState().toolarea.toolAreaOpen &&
|
||||||
|
this._getState().toolarea.currentToolTab !== 'chat')) // Make sound
|
||||||
|
{
|
||||||
|
const alertPromise = this._soundAlert.play();
|
||||||
|
|
||||||
|
if (alertPromise !== undefined)
|
||||||
|
{
|
||||||
|
alertPromise
|
||||||
|
.then()
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('_soundAlert.play() | failed: %o', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._signalingSocket.on('file-receive', (data) =>
|
this._signalingSocket.on('file-receive', (data) =>
|
||||||
|
|
@ -1047,6 +1141,23 @@ export default class RoomClient
|
||||||
this._dispatch(stateActions.addFile(payload));
|
this._dispatch(stateActions.addFile(payload));
|
||||||
|
|
||||||
this.notify(`${payload.name} shared a file`);
|
this.notify(`${payload.name} shared a file`);
|
||||||
|
|
||||||
|
if (!this._getState().toolarea.toolAreaOpen ||
|
||||||
|
(this._getState().toolarea.toolAreaOpen &&
|
||||||
|
this._getState().toolarea.currentToolTab !== 'files')) // Make sound
|
||||||
|
{
|
||||||
|
const alertPromise = this._soundAlert.play();
|
||||||
|
|
||||||
|
if (alertPromise !== undefined)
|
||||||
|
{
|
||||||
|
alertPromise
|
||||||
|
.then()
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('_soundAlert.play() | failed: %o', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1099,6 +1210,18 @@ export default class RoomClient
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
||||||
|
|
||||||
|
const alertPromise = this._soundAlert.play();
|
||||||
|
|
||||||
|
if (alertPromise !== undefined)
|
||||||
|
{
|
||||||
|
alertPromise
|
||||||
|
.then()
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('_soundAlert.play() | failed: %o', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._handlePeer(peer);
|
this._handlePeer(peer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1139,31 +1262,20 @@ export default class RoomClient
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Don't produce if explicitely requested to not to do it.
|
// Don't produce if explicitely requested to not to do it.
|
||||||
if (!this._produce)
|
if (this._produce)
|
||||||
return;
|
{
|
||||||
|
if (this._room.canSend('audio'))
|
||||||
|
await this._setMicProducer();
|
||||||
|
|
||||||
// NOTE: Don't depend on this Promise to continue (so we don't do return).
|
|
||||||
Promise.resolve()
|
|
||||||
// Add our mic.
|
|
||||||
.then(() =>
|
|
||||||
{
|
|
||||||
if (!this._room.canSend('audio'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._setMicProducer()
|
|
||||||
.catch(() => {});
|
|
||||||
})
|
|
||||||
// Add our webcam (unless the cookie says no).
|
// Add our webcam (unless the cookie says no).
|
||||||
.then(() =>
|
if (this._room.canSend('video'))
|
||||||
{
|
{
|
||||||
if (!this._room.canSend('video'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const devicesCookie = cookiesManager.getDevices();
|
const devicesCookie = cookiesManager.getDevices();
|
||||||
|
|
||||||
if (!devicesCookie || devicesCookie.webcamEnabled)
|
if (!devicesCookie || devicesCookie.webcamEnabled)
|
||||||
this.enableWebcam();
|
await this.enableWebcam();
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._dispatch(stateActions.setRoomState('connected'));
|
this._dispatch(stateActions.setRoomState('connected'));
|
||||||
|
|
||||||
|
|
@ -1174,12 +1286,20 @@ export default class RoomClient
|
||||||
|
|
||||||
this.notify('You are in the room');
|
this.notify('You are in the room');
|
||||||
|
|
||||||
|
this._spotlights.on('spotlights-updated', (spotlights) =>
|
||||||
|
{
|
||||||
|
this._dispatch(stateActions.setSpotlights(spotlights));
|
||||||
|
this.updateSpotlights(spotlights);
|
||||||
|
});
|
||||||
|
|
||||||
const peers = this._room.peers;
|
const peers = this._room.peers;
|
||||||
|
|
||||||
for (const peer of peers)
|
for (const peer of peers)
|
||||||
{
|
{
|
||||||
this._handlePeer(peer, { notify: false });
|
this._handlePeer(peer, { notify: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._spotlights.start();
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
|
|
@ -1203,10 +1323,6 @@ export default class RoomClient
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
|
|
||||||
|
|
||||||
await this._updateAudioDevices();
|
|
||||||
|
|
||||||
logger.debug('_setMicProducer() | calling getUserMedia()');
|
logger.debug('_setMicProducer() | calling getUserMedia()');
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
@ -1233,6 +1349,10 @@ export default class RoomClient
|
||||||
codec : producer.rtpParameters.codecs[0].name
|
codec : producer.rtpParameters.codecs[0].name
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
|
||||||
|
|
||||||
|
await this._updateAudioDevices();
|
||||||
|
|
||||||
producer.on('close', (originator) =>
|
producer.on('close', (originator) =>
|
||||||
{
|
{
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -1268,9 +1388,12 @@ export default class RoomClient
|
||||||
logger.debug('mic Producer "unhandled" event');
|
logger.debug('mic Producer "unhandled" event');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!stream.getAudioTracks()[0])
|
const harkStream = new MediaStream;
|
||||||
|
|
||||||
|
harkStream.addTrack(producer.track);
|
||||||
|
if (!harkStream.getAudioTracks()[0])
|
||||||
throw new Error('_setMicProducer(): given stream has no audio track');
|
throw new Error('_setMicProducer(): given stream has no audio track');
|
||||||
producer.hark = hark(stream, { play: false });
|
producer.hark = hark(harkStream, { play: false });
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
producer.hark.on('volume_change', (dBs, threshold) =>
|
producer.hark.on('volume_change', (dBs, threshold) =>
|
||||||
|
|
@ -1423,18 +1546,12 @@ export default class RoomClient
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const { device } = this._webcam;
|
|
||||||
|
|
||||||
if (!device)
|
|
||||||
throw new Error('no webcam devices');
|
|
||||||
|
|
||||||
logger.debug('_setWebcamProducer() | calling getUserMedia()');
|
logger.debug('_setWebcamProducer() | calling getUserMedia()');
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia(
|
const stream = await navigator.mediaDevices.getUserMedia(
|
||||||
{
|
{
|
||||||
video :
|
video :
|
||||||
{
|
{
|
||||||
deviceId : { exact: device.deviceId },
|
|
||||||
...VIDEO_CONSTRAINS
|
...VIDEO_CONSTRAINS
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1456,14 +1573,15 @@ export default class RoomClient
|
||||||
{
|
{
|
||||||
id : producer.id,
|
id : producer.id,
|
||||||
source : 'webcam',
|
source : 'webcam',
|
||||||
deviceLabel : device.label,
|
|
||||||
type : this._getWebcamType(device),
|
|
||||||
locallyPaused : producer.locallyPaused,
|
locallyPaused : producer.locallyPaused,
|
||||||
remotelyPaused : producer.remotelyPaused,
|
remotelyPaused : producer.remotelyPaused,
|
||||||
track : producer.track,
|
track : producer.track,
|
||||||
codec : producer.rtpParameters.codecs[0].name
|
codec : producer.rtpParameters.codecs[0].name
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
logger.debug('_setWebcamProducer() | calling _updateWebcams()');
|
||||||
|
await this._updateWebcams();
|
||||||
|
|
||||||
producer.on('close', (originator) =>
|
producer.on('close', (originator) =>
|
||||||
{
|
{
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -1549,9 +1667,6 @@ export default class RoomClient
|
||||||
else if (!this._audioDevices.has(currentAudioDeviceId))
|
else if (!this._audioDevices.has(currentAudioDeviceId))
|
||||||
this._audioDevice.device = array[0];
|
this._audioDevice.device = array[0];
|
||||||
|
|
||||||
this._dispatch(
|
|
||||||
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setCanChangeAudioDevice(len >= 2));
|
stateActions.setCanChangeAudioDevice(len >= 2));
|
||||||
if (len >= 1)
|
if (len >= 1)
|
||||||
|
|
@ -1599,9 +1714,6 @@ export default class RoomClient
|
||||||
else if (!this._webcams.has(currentWebcamId))
|
else if (!this._webcams.has(currentWebcamId))
|
||||||
this._webcam.device = array[0];
|
this._webcam.device = array[0];
|
||||||
|
|
||||||
this._dispatch(
|
|
||||||
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setCanChangeWebcam(len >= 2));
|
stateActions.setCanChangeWebcam(len >= 2));
|
||||||
if (len >= 1)
|
if (len >= 1)
|
||||||
|
|
@ -1614,22 +1726,6 @@ export default class RoomClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getWebcamType(device)
|
|
||||||
{
|
|
||||||
if (/(back|rear)/i.test(device.label))
|
|
||||||
{
|
|
||||||
logger.debug('_getWebcamType() | it seems to be a back camera');
|
|
||||||
|
|
||||||
return 'back';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.debug('_getWebcamType() | it seems to be a front camera');
|
|
||||||
|
|
||||||
return 'front';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handlePeer(peer, { notify = true } = {})
|
_handlePeer(peer, { notify = true } = {})
|
||||||
{
|
{
|
||||||
const displayName = peer.appData.displayName;
|
const displayName = peer.appData.displayName;
|
||||||
|
|
@ -1772,9 +1868,13 @@ export default class RoomClient
|
||||||
// Receive the consumer (if we can).
|
// Receive the consumer (if we can).
|
||||||
if (consumer.supported)
|
if (consumer.supported)
|
||||||
{
|
{
|
||||||
// Pause it if video and we are in audio-only mode.
|
if (consumer.kind === 'video' &&
|
||||||
if (consumer.kind === 'video' && this._getState().me.audioOnly)
|
!this._spotlights.peerInSpotlights(consumer.peer.name))
|
||||||
consumer.pause('audio-only-mode');
|
{ // Start paused
|
||||||
|
logger.debug(
|
||||||
|
'consumer paused by default');
|
||||||
|
consumer.pause('not-speaker');
|
||||||
|
}
|
||||||
|
|
||||||
consumer.receive(this._recvTransport)
|
consumer.receive(this._recvTransport)
|
||||||
.then((track) =>
|
.then((track) =>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
autoFocus={autofocus}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
type='submit'
|
||||||
|
className='send'
|
||||||
|
value='Send'
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class MessageList extends Component
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='MessageList' id='messages'>
|
<div data-component='MessageList' id='messages'>
|
||||||
{
|
{ chatmessages.length > 0 ?
|
||||||
chatmessages.map((message, i) =>
|
chatmessages.map((message, i) =>
|
||||||
{
|
{
|
||||||
const messageTime = new Date(message.time);
|
const messageTime = new Date(message.time);
|
||||||
|
|
@ -61,6 +61,9 @@ class MessageList extends Component
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
:<div className='empty'>
|
||||||
|
<p>No one has said anything yet...</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,21 @@ class SharedFilesList extends Component
|
||||||
{
|
{
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
|
const { sharing } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='shared-files'>
|
<div className='shared-files'>
|
||||||
{this.props.sharing.map((entry, i) => (
|
{ sharing.length > 0 ?
|
||||||
<FileEntry
|
sharing.map((entry, i) => (
|
||||||
data={entry}
|
<FileEntry
|
||||||
key={i}
|
data={entry}
|
||||||
/>
|
key={i}
|
||||||
))}
|
/>
|
||||||
|
))
|
||||||
|
:<div className='empty'>
|
||||||
|
<p>No one has shared files yet...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import ResizeObserver from 'resize-observer-polyfill';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import * as stateActions from '../redux/stateActions';
|
import * as requestActions from '../redux/requestActions';
|
||||||
import Peer from './Peer';
|
import Peer from './Peer';
|
||||||
|
import HiddenPeers from './HiddenPeers';
|
||||||
|
|
||||||
class Filmstrip extends Component
|
class Filmstrip extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -113,7 +114,7 @@ class Filmstrip extends Component
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
const { peers, advancedMode } = this.props;
|
const { peers, advancedMode, spotlights, spotlightsLength } = this.props;
|
||||||
|
|
||||||
const activePeerName = this.getActivePeerName();
|
const activePeerName = this.getActivePeerName();
|
||||||
|
|
||||||
|
|
@ -138,25 +139,40 @@ class Filmstrip extends Component
|
||||||
|
|
||||||
<div className='filmstrip'>
|
<div className='filmstrip'>
|
||||||
<div className='filmstrip-content'>
|
<div className='filmstrip-content'>
|
||||||
{Object.keys(peers).map((peerName) => (
|
{
|
||||||
<div
|
Object.keys(peers).map((peerName) =>
|
||||||
key={peerName}
|
{
|
||||||
onClick={() => this.props.setSelectedPeer(peerName)}
|
return (
|
||||||
className={classnames('film', {
|
spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
|
||||||
selected : this.props.selectedPeerName === peerName,
|
<div
|
||||||
active : this.state.lastSpeaker === peerName
|
key={peerName}
|
||||||
})}
|
onClick={() => this.props.setSelectedPeer(peerName)}
|
||||||
>
|
className={classnames('film', {
|
||||||
<div className='film-content'>
|
selected : this.props.selectedPeerName === peerName,
|
||||||
<Peer
|
active : this.state.lastSpeaker === peerName
|
||||||
advancedMode={advancedMode}
|
})}
|
||||||
name={peerName}
|
>
|
||||||
/>
|
<div className='film-content'>
|
||||||
</div>
|
<Peer
|
||||||
</div>
|
advancedMode={advancedMode}
|
||||||
))}
|
name={peerName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='hidden-peer-container'>
|
||||||
|
{ (spotlightsLength<Object.keys(peers).length)?
|
||||||
|
<HiddenPeers
|
||||||
|
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||||
|
/>:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -169,19 +185,28 @@ Filmstrip.propTypes = {
|
||||||
consumers : PropTypes.object.isRequired,
|
consumers : PropTypes.object.isRequired,
|
||||||
myName : PropTypes.string.isRequired,
|
myName : PropTypes.string.isRequired,
|
||||||
selectedPeerName : PropTypes.string,
|
selectedPeerName : PropTypes.string,
|
||||||
setSelectedPeer : PropTypes.func.isRequired
|
setSelectedPeer : PropTypes.func.isRequired,
|
||||||
|
spotlightsLength : PropTypes.number,
|
||||||
|
spotlights : PropTypes.array.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) =>
|
||||||
activeSpeakerName : state.room.activeSpeakerName,
|
{
|
||||||
selectedPeerName : state.room.selectedPeerName,
|
const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
|
||||||
peers : state.peers,
|
|
||||||
consumers : state.consumers,
|
return {
|
||||||
myName : state.me.name
|
activeSpeakerName : state.room.activeSpeakerName,
|
||||||
});
|
selectedPeerName : state.room.selectedPeerName,
|
||||||
|
peers : state.peers,
|
||||||
|
consumers : state.consumers,
|
||||||
|
myName : state.me.name,
|
||||||
|
spotlights : state.room.spotlights,
|
||||||
|
spotlightsLength
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
setSelectedPeer : stateActions.setSelectedPeer
|
setSelectedPeer : requestActions.setSelectedPeer
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -10,12 +10,9 @@ const ListPeer = (props) =>
|
||||||
const {
|
const {
|
||||||
peer,
|
peer,
|
||||||
micConsumer,
|
micConsumer,
|
||||||
webcamConsumer,
|
|
||||||
screenConsumer,
|
screenConsumer,
|
||||||
onMuteMic,
|
onMuteMic,
|
||||||
onUnmuteMic,
|
onUnmuteMic,
|
||||||
onDisableWebcam,
|
|
||||||
onEnableWebcam,
|
|
||||||
onDisableScreen,
|
onDisableScreen,
|
||||||
onEnableScreen
|
onEnableScreen
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -26,12 +23,6 @@ const ListPeer = (props) =>
|
||||||
!micConsumer.remotelyPaused
|
!micConsumer.remotelyPaused
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoVisible = (
|
|
||||||
Boolean(webcamConsumer) &&
|
|
||||||
!webcamConsumer.locallyPaused &&
|
|
||||||
!webcamConsumer.remotelyPaused
|
|
||||||
);
|
|
||||||
|
|
||||||
const screenVisible = (
|
const screenVisible = (
|
||||||
Boolean(screenConsumer) &&
|
Boolean(screenConsumer) &&
|
||||||
!screenConsumer.locallyPaused &&
|
!screenConsumer.locallyPaused &&
|
||||||
|
|
@ -61,6 +52,9 @@ const ListPeer = (props) =>
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='volume-container'>
|
||||||
|
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||||
|
</div>
|
||||||
<div className='controls'>
|
<div className='controls'>
|
||||||
{ screenConsumer ?
|
{ screenConsumer ?
|
||||||
<div
|
<div
|
||||||
|
|
@ -84,28 +78,12 @@ const ListPeer = (props) =>
|
||||||
off : !micEnabled,
|
off : !micEnabled,
|
||||||
disabled : peer.peerAudioInProgress
|
disabled : peer.peerAudioInProgress
|
||||||
})}
|
})}
|
||||||
style={{ opacity : micEnabled && micConsumer ? (micConsumer.volume/10)
|
|
||||||
+ 0.2 :1 }}
|
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
{
|
{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
|
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
className={classnames('button', 'webcam', {
|
|
||||||
on : videoVisible,
|
|
||||||
off : !videoVisible,
|
|
||||||
disabled : peer.peerVideoInProgress
|
|
||||||
})}
|
|
||||||
onClick={(e) =>
|
|
||||||
{
|
|
||||||
e.stopPropagation();
|
|
||||||
videoVisible ?
|
|
||||||
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,73 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as appPropTypes from '../appPropTypes';
|
import * as appPropTypes from '../appPropTypes';
|
||||||
import * as stateActions from '../../redux/stateActions';
|
import * as requestActions from '../../redux/requestActions';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ListPeer from './ListPeer';
|
import ListPeer from './ListPeer';
|
||||||
import ListMe from './ListMe';
|
import ListMe from './ListMe';
|
||||||
|
|
||||||
const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerName }) => (
|
const ParticipantList =
|
||||||
<div data-component='ParticipantList'>
|
({
|
||||||
<ul className='list'>
|
advancedMode,
|
||||||
<ListMe />
|
peers,
|
||||||
|
setSelectedPeer,
|
||||||
|
selectedPeerName,
|
||||||
|
spotlights
|
||||||
|
}) => (
|
||||||
|
<div data-component='ParticipantList'>
|
||||||
|
<ul className='list'>
|
||||||
|
<li className='list-header'>Me:</li>
|
||||||
|
<ListMe />
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<ul className='list'>
|
||||||
|
<li className='list-header'>Participants in Spotlight:</li>
|
||||||
|
{peers.filter((peer) =>
|
||||||
|
{
|
||||||
|
return (spotlights.find((spotlight) =>
|
||||||
|
{ return (spotlight === peer.name); }));
|
||||||
|
}).map((peer) => (
|
||||||
|
<li
|
||||||
|
key={peer.name}
|
||||||
|
className={classNames('list-item', {
|
||||||
|
selected : peer.name === selectedPeerName
|
||||||
|
})}
|
||||||
|
onClick={() => setSelectedPeer(peer.name)}
|
||||||
|
>
|
||||||
|
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<ul className='list'>
|
||||||
|
<li className='list-header'>Passive Participants:</li>
|
||||||
|
{peers.filter((peer) =>
|
||||||
|
{
|
||||||
|
return !(spotlights.find((spotlight) =>
|
||||||
|
{ return (spotlight === peer.name); }));
|
||||||
|
}).map((peer) => (
|
||||||
|
<li
|
||||||
|
key={peer.name}
|
||||||
|
className={classNames('list-item', {
|
||||||
|
selected : peer.name === selectedPeerName
|
||||||
|
})}
|
||||||
|
onClick={() => setSelectedPeer(peer.name)}
|
||||||
|
>
|
||||||
|
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
{peers.map((peer) => (
|
</div>
|
||||||
<li
|
);
|
||||||
key={peer.name}
|
|
||||||
className={classNames('list-item', {
|
|
||||||
selected : peer.name === selectedPeerName
|
|
||||||
})}
|
|
||||||
onClick={() => setSelectedPeer(peer.name)}
|
|
||||||
>
|
|
||||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
ParticipantList.propTypes =
|
ParticipantList.propTypes =
|
||||||
{
|
{
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||||
setSelectedPeer : PropTypes.func.isRequired,
|
setSelectedPeer : PropTypes.func.isRequired,
|
||||||
selectedPeerName : PropTypes.string
|
selectedPeerName : PropTypes.string,
|
||||||
|
spotlights : PropTypes.array.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -41,12 +77,13 @@ const mapStateToProps = (state) =>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peers : peersArray,
|
peers : peersArray,
|
||||||
selectedPeerName : state.room.selectedPeerName
|
selectedPeerName : state.room.selectedPeerName,
|
||||||
|
spotlights : state.room.spotlights
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
setSelectedPeer : stateActions.setSelectedPeer
|
setSelectedPeer : requestActions.setSelectedPeer
|
||||||
};
|
};
|
||||||
|
|
||||||
const ParticipantListContainer = connect(
|
const ParticipantListContainer = connect(
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,6 @@ class Peer extends Component
|
||||||
screenConsumer,
|
screenConsumer,
|
||||||
onMuteMic,
|
onMuteMic,
|
||||||
onUnmuteMic,
|
onUnmuteMic,
|
||||||
onDisableWebcam,
|
|
||||||
onEnableWebcam,
|
|
||||||
onDisableScreen,
|
|
||||||
onEnableScreen,
|
|
||||||
toggleConsumerFullscreen,
|
toggleConsumerFullscreen,
|
||||||
style
|
style
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
@ -90,6 +86,13 @@ class Peer extends Component
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{!videoVisible ?
|
||||||
|
<div className='paused-video'>
|
||||||
|
<p>this video is paused</p>
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
<div className={classnames('view-container', 'webcam')} style={style}>
|
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||||
<div className='indicators'>
|
<div className='indicators'>
|
||||||
{peer.raiseHandState ?
|
{peer.raiseHandState ?
|
||||||
|
|
@ -123,20 +126,6 @@ class Peer extends Component
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
className={classnames('button', 'webcam', {
|
|
||||||
on : videoVisible,
|
|
||||||
off : !videoVisible,
|
|
||||||
disabled : peer.peerVideoInProgress
|
|
||||||
})}
|
|
||||||
onClick={(e) =>
|
|
||||||
{
|
|
||||||
e.stopPropagation();
|
|
||||||
videoVisible ?
|
|
||||||
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'fullscreen')}
|
className={classnames('button', 'fullscreen')}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
|
|
@ -146,10 +135,10 @@ class Peer extends Component
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PeerView
|
<PeerView
|
||||||
advancedMode={advancedMode}
|
advancedMode={advancedMode}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
audioTrack={micConsumer ? micConsumer.track : null}
|
|
||||||
volume={micConsumer ? micConsumer.volume : null}
|
volume={micConsumer ? micConsumer.volume : null}
|
||||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||||
videoVisible={videoVisible}
|
videoVisible={videoVisible}
|
||||||
|
|
@ -166,20 +155,6 @@ class Peer extends Component
|
||||||
visible : this.state.controlsVisible
|
visible : this.state.controlsVisible
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className={classnames('button', 'screen', {
|
|
||||||
on : screenVisible,
|
|
||||||
off : !screenVisible,
|
|
||||||
disabled : peer.peerScreenInProgress
|
|
||||||
})}
|
|
||||||
onClick={(e) =>
|
|
||||||
{
|
|
||||||
e.stopPropagation();
|
|
||||||
screenVisible ?
|
|
||||||
onDisableScreen(peer.name) : onEnableScreen(peer.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'fullscreen')}
|
className={classnames('button', 'fullscreen')}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
|
|
@ -213,12 +188,8 @@ Peer.propTypes =
|
||||||
screenConsumer : appPropTypes.Consumer,
|
screenConsumer : appPropTypes.Consumer,
|
||||||
onMuteMic : PropTypes.func.isRequired,
|
onMuteMic : PropTypes.func.isRequired,
|
||||||
onUnmuteMic : PropTypes.func.isRequired,
|
onUnmuteMic : PropTypes.func.isRequired,
|
||||||
onEnableWebcam : PropTypes.func.isRequired,
|
|
||||||
onDisableWebcam : PropTypes.func.isRequired,
|
|
||||||
streamDimensions : PropTypes.object,
|
streamDimensions : PropTypes.object,
|
||||||
style : PropTypes.object,
|
style : PropTypes.object,
|
||||||
onEnableScreen : PropTypes.func.isRequired,
|
|
||||||
onDisableScreen : PropTypes.func.isRequired,
|
|
||||||
toggleConsumerFullscreen : PropTypes.func.isRequired
|
toggleConsumerFullscreen : PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -253,23 +224,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
{
|
{
|
||||||
dispatch(requestActions.unmutePeerAudio(peerName));
|
dispatch(requestActions.unmutePeerAudio(peerName));
|
||||||
},
|
},
|
||||||
onEnableWebcam : (peerName) =>
|
|
||||||
{
|
|
||||||
|
|
||||||
dispatch(requestActions.resumePeerVideo(peerName));
|
|
||||||
},
|
|
||||||
onDisableWebcam : (peerName) =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.pausePeerVideo(peerName));
|
|
||||||
},
|
|
||||||
onEnableScreen : (peerName) =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.resumePeerScreen(peerName));
|
|
||||||
},
|
|
||||||
onDisableScreen : (peerName) =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.pausePeerScreen(peerName));
|
|
||||||
},
|
|
||||||
toggleConsumerFullscreen : (consumer) =>
|
toggleConsumerFullscreen : (consumer) =>
|
||||||
{
|
{
|
||||||
if (consumer)
|
if (consumer)
|
||||||
|
|
|
||||||
|
|
@ -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 * as appPropTypes from './appPropTypes';
|
||||||
import { Appear } from './transitions';
|
import { Appear } from './transitions';
|
||||||
import Peer from './Peer';
|
import Peer from './Peer';
|
||||||
|
import HiddenPeers from './HiddenPeers';
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
|
||||||
const RATIO = 1.334;
|
const RATIO = 1.334;
|
||||||
|
|
@ -91,7 +92,9 @@ class Peers extends React.Component
|
||||||
const {
|
const {
|
||||||
advancedMode,
|
advancedMode,
|
||||||
activeSpeakerName,
|
activeSpeakerName,
|
||||||
peers
|
peers,
|
||||||
|
spotlights,
|
||||||
|
spotlightsLength
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const style =
|
const style =
|
||||||
|
|
@ -106,22 +109,35 @@ class Peers extends React.Component
|
||||||
peers.map((peer) =>
|
peers.map((peer) =>
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Appear key={peer.name} duration={1000}>
|
(spotlights.find(function(spotlightsElement)
|
||||||
<div
|
{ return spotlightsElement == peer.name; }))?
|
||||||
className={classnames('peer-container', {
|
<Appear key={peer.name} duration={1000}>
|
||||||
'active-speaker' : peer.name === activeSpeakerName
|
<div
|
||||||
})}
|
className={classnames('peer-container', {
|
||||||
>
|
'selected' : this.props.selectedPeerName === peer.name,
|
||||||
<Peer
|
'active-speaker' : peer.name === activeSpeakerName
|
||||||
advancedMode={advancedMode}
|
})}
|
||||||
name={peer.name}
|
>
|
||||||
style={style}
|
<div className='peer-content'>
|
||||||
/>
|
<Peer
|
||||||
</div>
|
advancedMode={advancedMode}
|
||||||
</Appear>
|
name={peer.name}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Appear>
|
||||||
|
:null
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
<div className='hidden-peer-container'>
|
||||||
|
{ (spotlightsLength<peers.length)?
|
||||||
|
<HiddenPeers
|
||||||
|
hiddenPeersCount={peers.length-spotlightsLength}
|
||||||
|
/>:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -132,20 +148,27 @@ Peers.propTypes =
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||||
boxes : PropTypes.number,
|
boxes : PropTypes.number,
|
||||||
activeSpeakerName : PropTypes.string
|
activeSpeakerName : PropTypes.string,
|
||||||
|
selectedPeerName : PropTypes.string,
|
||||||
|
spotlightsLength : PropTypes.number,
|
||||||
|
spotlights : PropTypes.array.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
const peers = Object.values(state.peers);
|
const peers = Object.values(state.peers);
|
||||||
|
const spotlights = state.room.spotlights;
|
||||||
const boxes = peers.length + Object.values(state.consumers)
|
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
|
||||||
|
const boxes = spotlightsLength + Object.values(state.consumers)
|
||||||
.filter((consumer) => consumer.source === 'screen').length;
|
.filter((consumer) => consumer.source === 'screen').length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peers,
|
peers,
|
||||||
boxes,
|
boxes,
|
||||||
activeSpeakerName : state.room.activeSpeakerName
|
activeSpeakerName : state.room.activeSpeakerName,
|
||||||
|
selectedPeerName : state.room.selectedPeerName,
|
||||||
|
spotlights,
|
||||||
|
spotlightsLength
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ import ReactTooltip from 'react-tooltip';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
import CookieConsent from 'react-cookie-consent';
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
import * as requestActions from '../redux/requestActions';
|
import * as requestActions from '../redux/requestActions';
|
||||||
import * as stateActions from '../redux/stateActions';
|
import * as stateActions from '../redux/stateActions';
|
||||||
import { Appear } from './transitions';
|
import { Appear } from './transitions';
|
||||||
import Me from './Me';
|
import Me from './Me';
|
||||||
import Peers from './Peers';
|
import Peers from './Peers';
|
||||||
|
import AudioPeers from './PeerAudio/AudioPeers';
|
||||||
import Notifications from './Notifications';
|
import Notifications from './Notifications';
|
||||||
import ToolAreaButton from './ToolArea/ToolAreaButton';
|
import ToolAreaButton from './ToolArea/ToolAreaButton';
|
||||||
import ToolArea from './ToolArea/ToolArea';
|
import ToolArea from './ToolArea/ToolArea';
|
||||||
|
|
@ -80,9 +82,16 @@ class Room extends React.Component
|
||||||
|
|
||||||
<Appear duration={300}>
|
<Appear duration={300}>
|
||||||
<div data-component='Room'>
|
<div data-component='Room'>
|
||||||
|
<CookieConsent>
|
||||||
|
This website uses cookies to enhance the user experience.
|
||||||
|
</CookieConsent>
|
||||||
|
|
||||||
<FullScreenView advancedMode={room.advancedMode} />
|
<FullScreenView advancedMode={room.advancedMode} />
|
||||||
|
|
||||||
<div className='room-wrapper'>
|
<div className='room-wrapper'>
|
||||||
<div data-component='Logo' />
|
<div data-component='Logo' />
|
||||||
|
<AudioPeers />
|
||||||
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
|
||||||
<ToolAreaButton />
|
<ToolAreaButton />
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,13 @@ export function setDevices({ webcamEnabled })
|
||||||
{
|
{
|
||||||
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAudioDevice({ audioDeviceId })
|
||||||
|
{
|
||||||
|
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setVideoDevice({ videoDeviceId })
|
||||||
|
{
|
||||||
|
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ const initialState =
|
||||||
fullScreenConsumer : null, // ConsumerID
|
fullScreenConsumer : null, // ConsumerID
|
||||||
toolbarsVisible : true,
|
toolbarsVisible : true,
|
||||||
mode : 'democratic',
|
mode : 'democratic',
|
||||||
selectedPeerName : null
|
selectedPeerName : null,
|
||||||
|
spotlights : []
|
||||||
};
|
};
|
||||||
|
|
||||||
const room = (state = initialState, action) =>
|
const room = (state = initialState, action) =>
|
||||||
|
|
@ -83,6 +84,13 @@ const room = (state = initialState, action) =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_SPOTLIGHTS':
|
||||||
|
{
|
||||||
|
const { spotlights } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, spotlights };
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,22 @@ const toolarea = (state = initialState, action) =>
|
||||||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'OPEN_TOOL_AREA':
|
||||||
|
{
|
||||||
|
const toolAreaOpen = true;
|
||||||
|
const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||||
|
const unreadFiles = state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||||
|
|
||||||
|
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CLOSE_TOOL_AREA':
|
||||||
|
{
|
||||||
|
const toolAreaOpen = false;
|
||||||
|
|
||||||
|
return { ...state, toolAreaOpen };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_TOOL_TAB':
|
case 'SET_TOOL_TAB':
|
||||||
{
|
{
|
||||||
const { toolTab } = action.payload;
|
const { toolTab } = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,14 @@ export const sendFile = (file, name, picture) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setSelectedPeer = (selectedPeerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'REQUEST_SELECTED_PEER',
|
||||||
|
payload : { selectedPeerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// This returns a redux-thunk action (a function).
|
// This returns a redux-thunk action (a function).
|
||||||
export const notify = ({ type = 'info', text, timeout }) =>
|
export const notify = ({ type = 'info', text, timeout }) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,15 @@ export default ({ dispatch, getState }) => (next) =>
|
||||||
client.sendFile(action.payload);
|
client.sendFile(action.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'REQUEST_SELECTED_PEER':
|
||||||
|
{
|
||||||
|
const { selectedPeerName } = action.payload;
|
||||||
|
|
||||||
|
client.setSelectedPeer(selectedPeerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,20 @@ export const toggleToolArea = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openToolArea = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'OPEN_TOOL_AREA'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeToolArea = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'CLOSE_TOOL_AREA'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setToolTab = (toolTab) =>
|
export const setToolTab = (toolTab) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -474,7 +488,14 @@ export const loggedIn = () =>
|
||||||
type : 'LOGGED_IN'
|
type : 'LOGGED_IN'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setSelectedPeer = (selectedPeerName) => ({
|
export const setSelectedPeer = (selectedPeerName) =>
|
||||||
type : 'SET_SELECTED_PEER',
|
({
|
||||||
payload : { selectedPeerName }
|
type : 'SET_SELECTED_PEER',
|
||||||
});
|
payload : { selectedPeerName }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSpotlights = (spotlights) =>
|
||||||
|
({
|
||||||
|
type : 'SET_SPOTLIGHTS',
|
||||||
|
payload : { spotlights }
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"random-string": "^0.2.0",
|
"random-string": "^0.2.0",
|
||||||
"react": "^16.5.2",
|
"react": "^16.5.2",
|
||||||
|
"react-cookie-consent": "^1.9.0",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-dom": "^16.5.2",
|
"react-dom": "^16.5.2",
|
||||||
"react-draggable": "^3.0.5",
|
"react-draggable": "^3.0.5",
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
||||||
|
[data-component='AudioPeers'] {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component='MessageList'] {
|
[data-component='MessageList'] {
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
> .message {
|
> .message {
|
||||||
|
|
@ -49,25 +49,55 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 20vmin;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component='Sender'] {
|
[data-component='Sender'] {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: #fff;
|
||||||
padding: 1rem;
|
color: #000;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
|
||||||
> .new-message {
|
> .new-message {
|
||||||
width: 100%;
|
width: 80%;
|
||||||
|
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #FFF;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
margin-right: 1vmin;
|
||||||
|
border-radius: 0.5vmin;
|
||||||
|
padding-left: 1vmin;
|
||||||
|
color: #000;
|
||||||
|
|
||||||
&.focus {
|
&.focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .send {
|
||||||
|
width: 20%;
|
||||||
|
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
|
||||||
|
border: 0;
|
||||||
|
background-color: #aef;
|
||||||
|
color: #000;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 0.5vmin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,11 +7,10 @@
|
||||||
> .share-file {
|
> .share-file {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #252525;
|
background: #aef;
|
||||||
border: 1px solid #151515;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-bottom: 5px solid #151515;
|
border-radius: 1vmin;
|
||||||
border-radius: 3px 3px 0 0;
|
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
@ -21,7 +20,7 @@
|
||||||
|
|
||||||
> .shared-files {
|
> .shared-files {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
> .file-entry {
|
> .file-entry {
|
||||||
|
|
@ -76,6 +75,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 20vmin;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,78 @@
|
||||||
[data-component='Filmstrip'] {
|
[data-component='Filmstrip'] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
> .active-peer-container {
|
> .active-peer-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> .active-peer {
|
> .active-peer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1vmin;
|
padding: 1vmin;
|
||||||
|
|
||||||
> [data-component='Peer'] {
|
> [data-component='Peer'] {
|
||||||
border: 5px solid rgba(255, 255, 255, 0.15);
|
border: 5px solid rgba(255, 255, 255, 0.15);
|
||||||
box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
|
box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .filmstrip {
|
> .filmstrip {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(0, 0, 0 , 0.5);
|
background: rgba(0, 0, 0 , 0.5);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
height: 20vh;
|
height: 20vh;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> .filmstrip-content {
|
> .filmstrip-content {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> .film {
|
> .film {
|
||||||
height: 18vh;
|
height: 18vh;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding-left: 1vh;
|
padding-left: 1vh;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
padding-right: 1vh;
|
padding-right: 1vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .film-content {
|
> .film-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid rgba(255,255,255,0.15);
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
|
||||||
> [data-component='Peer'] {
|
> [data-component='Peer'] {
|
||||||
max-width: 18vh * (4 / 3);
|
max-width: 18vh * (4 / 3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.screen {
|
&.screen {
|
||||||
max-width: 18vh * (2 * 4 / 3);
|
max-width: 18vh * (2 * 4 / 3);
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
> .film-content {
|
> .film-content {
|
||||||
border-color: #FFF;
|
border-color: #FFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
> .film-content {
|
> .film-content {
|
||||||
border-color: #377EFF;
|
border-color: #377EFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
[data-component='ParticipantList'] {
|
[data-component='ParticipantList'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
> .list {
|
> .list {
|
||||||
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \
|
box-shadow: 0 2px 5px 2px rgba(0,0,0,0.2);
|
||||||
0 4px 20px 0 rgba(0,0,0,0.19);
|
background-color: #fff;
|
||||||
|
|
||||||
|
> .list-header {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
> .list-item {
|
> .list-item {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-bottom: 1px solid #CBCBCB;
|
border-bottom: 1px solid #CBCBCB;
|
||||||
|
|
@ -17,7 +23,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
border-bottom-color: #377EFF;
|
background-color: #377eff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +31,6 @@
|
||||||
|
|
||||||
[data-component='ListPeer'] {
|
[data-component='ListPeer'] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> .indicators {
|
> .indicators {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -75,6 +80,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> .volume-container {
|
||||||
|
float: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 1vmin;
|
||||||
|
position: relative;
|
||||||
|
background-size: 75%;
|
||||||
|
|
||||||
|
> .bar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0.3rem;
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
transition-property: opacity, background-color;
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
|
||||||
|
&.level0 { height: 0; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level1 { height: 0.2vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level2 { height: 0.4vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level3 { height: 0.6vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level4 { height: 0.8vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level5 { height: 1.0vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level6 { height: 1.2vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level7 { height: 1.4vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level8 { height: 1.6vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level9 { height: 1.8vh; background-color: rgba(#000, 0.8); }
|
||||||
|
&.level10 { height: 2.0vh; background-color: rgba(#000, 0.8); }
|
||||||
|
}
|
||||||
|
}
|
||||||
> .controls {
|
> .controls {
|
||||||
float: right;
|
float: right;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.paused-video {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 11;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 20px;
|
||||||
|
color: rgba(#fff, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.incompatible-video {
|
.incompatible-video {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
@ -210,7 +232,7 @@
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: 15px;
|
font-size: 20px;
|
||||||
color: rgba(#fff, 0.55);
|
color: rgba(#fff, 0.55);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@
|
||||||
&.active-speaker {
|
&.active-speaker {
|
||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
> .peer-content {
|
||||||
|
border: 1px solid #377eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+mobile() {
|
+mobile() {
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 8px 52px 8px 10px;
|
padding: 8px 52px 8px 10px;
|
||||||
transition: all 200ms ease;
|
transition: all 200ms ease;
|
||||||
|
box-shadow: 0vmin 0vmin 0.2vmin 0vmin rgba(17,17,17,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Dropdown-control:hover {
|
.Dropdown-control:hover {
|
||||||
|
|
|
||||||
|
|
@ -153,13 +153,13 @@
|
||||||
[data-component='ToolArea'] {
|
[data-component='ToolArea'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: #fff;
|
color: #000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 0;
|
width: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(50, 50, 50, 0.9);
|
background: #fff;
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
z-index: 1010;
|
z-index: 1010;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -168,7 +168,7 @@
|
||||||
|
|
||||||
> .tab-headers {
|
> .tab-headers {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: #ddd;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
> .tab-header {
|
> .tab-header {
|
||||||
|
|
@ -179,7 +179,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: #fff;
|
||||||
|
border-radius: 1vmin 1vmin 0vmin 0vmin;
|
||||||
|
box-shadow: 0.5vmin 0vmin 1vmin -0.5vmin #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .badge {
|
> .badge {
|
||||||
|
|
@ -200,5 +202,6 @@
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ body {
|
||||||
@import './components/Peers';
|
@import './components/Peers';
|
||||||
@import './components/Peer';
|
@import './components/Peer';
|
||||||
@import './components/PeerView';
|
@import './components/PeerView';
|
||||||
|
@import './components/HiddenPeersView';
|
||||||
@import './components/ScreenView';
|
@import './components/ScreenView';
|
||||||
@import './components/Notifications';
|
@import './components/Notifications';
|
||||||
@import './components/Chat';
|
@import './components/Chat';
|
||||||
|
|
@ -58,6 +59,7 @@ body {
|
||||||
@import './components/FullView';
|
@import './components/FullView';
|
||||||
@import './components/Filmstrip';
|
@import './components/Filmstrip';
|
||||||
@import './components/FileSharing';
|
@import './components/FileSharing';
|
||||||
|
@import './components/AudioPeers';
|
||||||
|
|
||||||
// Hack to detect in JS the current media query
|
// Hack to detect in JS the current media query
|
||||||
#multiparty-meeting-media-query-detector {
|
#multiparty-meeting-media-query-detector {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const EventEmitter = require('events').EventEmitter;
|
const EventEmitter = require('events').EventEmitter;
|
||||||
const WebTorrent = require('webtorrent-hybrid');
|
|
||||||
const Logger = require('./Logger');
|
const Logger = require('./Logger');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
@ -11,14 +10,6 @@ const BITRATE_FACTOR = 0.75;
|
||||||
|
|
||||||
const logger = new Logger('Room');
|
const logger = new Logger('Room');
|
||||||
|
|
||||||
const torrentClient = new WebTorrent({
|
|
||||||
tracker : {
|
|
||||||
rtcConfig : {
|
|
||||||
iceServers : config.turnServers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class Room extends EventEmitter
|
class Room extends EventEmitter
|
||||||
{
|
{
|
||||||
constructor(roomId, mediaServer, io)
|
constructor(roomId, mediaServer, io)
|
||||||
|
|
@ -38,6 +29,8 @@ class Room extends EventEmitter
|
||||||
|
|
||||||
this._fileHistory = [];
|
this._fileHistory = [];
|
||||||
|
|
||||||
|
this._lastN = [];
|
||||||
|
|
||||||
this._io = io;
|
this._io = io;
|
||||||
|
|
||||||
this._signalingPeers = new Map();
|
this._signalingPeers = new Map();
|
||||||
|
|
@ -79,7 +72,8 @@ class Room extends EventEmitter
|
||||||
if (this._signalingPeers)
|
if (this._signalingPeers)
|
||||||
for (let peer of this._signalingPeers)
|
for (let peer of this._signalingPeers)
|
||||||
{
|
{
|
||||||
peer.socket.disconnect();
|
if (peer.socket)
|
||||||
|
peer.socket.disconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
this._signalingPeers.clear();
|
this._signalingPeers.clear();
|
||||||
|
|
@ -123,9 +117,40 @@ class Room extends EventEmitter
|
||||||
|
|
||||||
const signalingPeer = { peerName : peerName, socket : socket };
|
const signalingPeer = { peerName : peerName, socket : socket };
|
||||||
|
|
||||||
|
const index = this._lastN.indexOf(peerName);
|
||||||
|
|
||||||
|
if (index === -1) // We don't have this peer, add to end
|
||||||
|
{
|
||||||
|
this._lastN.push(peerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._signalingPeers.set(peerName, signalingPeer);
|
||||||
|
|
||||||
this._handleSignalingPeer(signalingPeer);
|
this._handleSignalingPeer(signalingPeer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authCallback(data)
|
||||||
|
{
|
||||||
|
logger.debug('authCallback()');
|
||||||
|
|
||||||
|
const {
|
||||||
|
peerName,
|
||||||
|
name,
|
||||||
|
picture
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const signalingPeer = this._signalingPeers.get(peerName);
|
||||||
|
|
||||||
|
if (signalingPeer)
|
||||||
|
{
|
||||||
|
signalingPeer.socket.emit('auth',
|
||||||
|
{
|
||||||
|
name : name,
|
||||||
|
picture : picture
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_handleMediaRoom()
|
_handleMediaRoom()
|
||||||
{
|
{
|
||||||
logger.debug('_handleMediaRoom()');
|
logger.debug('_handleMediaRoom()');
|
||||||
|
|
@ -140,6 +165,14 @@ class Room extends EventEmitter
|
||||||
|
|
||||||
this._currentActiveSpeaker = activePeer;
|
this._currentActiveSpeaker = activePeer;
|
||||||
|
|
||||||
|
const index = this._lastN.indexOf(activePeer.name);
|
||||||
|
|
||||||
|
if (index > -1) // We have this speaker in the list, move to front
|
||||||
|
{
|
||||||
|
this._lastN.splice(index, 1);
|
||||||
|
this._lastN = [activePeer.name].concat(this._lastN);
|
||||||
|
}
|
||||||
|
|
||||||
const activeVideoProducer = activePeer.producers
|
const activeVideoProducer = activePeer.producers
|
||||||
.find((producer) => producer.kind === 'video');
|
.find((producer) => producer.kind === 'video');
|
||||||
|
|
||||||
|
|
@ -269,7 +302,8 @@ class Room extends EventEmitter
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
chatHistory : this._chatHistory,
|
chatHistory : this._chatHistory,
|
||||||
fileHistory : this._fileHistory
|
fileHistory : this._fileHistory,
|
||||||
|
lastN : this._lastN
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -279,15 +313,10 @@ class Room extends EventEmitter
|
||||||
// Return no error
|
// Return no error
|
||||||
cb(null);
|
cb(null);
|
||||||
|
|
||||||
const fileData = request.data.file;
|
const fileData = request.file;
|
||||||
|
|
||||||
this._fileHistory.push(fileData);
|
this._fileHistory.push(fileData);
|
||||||
|
|
||||||
if (!torrentClient.get(fileData.file.magnet))
|
|
||||||
{
|
|
||||||
torrentClient.add(fileData.file.magnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spread to others
|
// Spread to others
|
||||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||||
'file-receive',
|
'file-receive',
|
||||||
|
|
@ -302,10 +331,10 @@ class Room extends EventEmitter
|
||||||
// Return no error
|
// Return no error
|
||||||
cb(null);
|
cb(null);
|
||||||
|
|
||||||
const { raiseHandState } = request.data;
|
const { raiseHandState } = request;
|
||||||
const { mediaPeer } = signalingPeer;
|
const { mediaPeer } = signalingPeer;
|
||||||
|
|
||||||
mediaPeer.appData.raiseHandState = request.data.raiseHandState;
|
mediaPeer.appData.raiseHandState = request.raiseHandState;
|
||||||
// Spread to others
|
// Spread to others
|
||||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||||
'raisehand-message',
|
'raisehand-message',
|
||||||
|
|
@ -325,6 +354,13 @@ class Room extends EventEmitter
|
||||||
if (mediaPeer && !mediaPeer.closed)
|
if (mediaPeer && !mediaPeer.closed)
|
||||||
mediaPeer.close();
|
mediaPeer.close();
|
||||||
|
|
||||||
|
const index = this._lastN.indexOf(signalingPeer.peerName);
|
||||||
|
|
||||||
|
if (index > -1) // We have this peer in the list, remove
|
||||||
|
{
|
||||||
|
this._lastN.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// If this is the latest peer in the room, close the room.
|
// If this is the latest peer in the room, close the room.
|
||||||
// However wait a bit (for reconnections).
|
// However wait a bit (for reconnections).
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.3",
|
||||||
"mediasoup": "^2.1.0",
|
"mediasoup": "^2.1.0",
|
||||||
"passport-dataporten": "^1.3.0",
|
"passport-dataporten": "^1.3.0",
|
||||||
"webtorrent-hybrid": "^1.0.6",
|
|
||||||
"socket.io": "^2.1.1"
|
"socket.io": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,12 @@ const dataporten = new Dataporten.Setup(config.oauth2);
|
||||||
|
|
||||||
app.all('*', (req, res, next) =>
|
app.all('*', (req, res, next) =>
|
||||||
{
|
{
|
||||||
if(req.headers['x-forwarded-proto'] == 'http')
|
if(req.secure)
|
||||||
{
|
|
||||||
res.redirect('https://' + req.hostname + req.url);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.redirect('https://' + req.hostname + req.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(dataporten.passport.initialize());
|
app.use(dataporten.passport.initialize());
|
||||||
|
|
@ -72,26 +70,23 @@ dataporten.setupLogout(app, '/logout');
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/auth-callback',
|
'/auth-callback',
|
||||||
|
|
||||||
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
|
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
|
||||||
|
|
||||||
(req, res) =>
|
(req, res) =>
|
||||||
{
|
{
|
||||||
const state = JSON.parse(base64.decode(req.query.state));
|
const state = JSON.parse(base64.decode(req.query.state));
|
||||||
|
|
||||||
if (rooms.has(state.roomId))
|
if (rooms.has(state.roomId))
|
||||||
{
|
{
|
||||||
const room = rooms.get(state.roomId)._protooRoom;
|
const data =
|
||||||
|
|
||||||
if (room.hasPeer(state.peerName))
|
|
||||||
{
|
{
|
||||||
const peer = room.getPeer(state.peerName);
|
peerName : state.peerName,
|
||||||
|
name : req.user.data.displayName,
|
||||||
|
picture : req.user.data.photos[0]
|
||||||
|
};
|
||||||
|
|
||||||
peer.send('auth', {
|
const room = rooms.get(state.roomId);
|
||||||
name : req.user.data.displayName,
|
|
||||||
picture : req.user.data.photos[0]
|
room.authCallback(data);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send('');
|
res.send('');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue