Merge branch 'develop'

master
Håvar Aambø Fosstveit 2018-06-29 11:25:49 +02:00
commit a40f0569ac
72 changed files with 6467 additions and 2850 deletions

View File

@ -1,4 +1,19 @@
module.exports = module.exports =
{ {
chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi' chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi',
loginEnabled : false,
turnServers : [
{
urls : [
'turn:example.com:443?transport=tcp'
],
username : 'example',
credential : 'example'
}
],
requestTimeout : 10000,
transportOptions :
{
tcp : true
}
}; };

View File

@ -206,6 +206,7 @@ gulp.task('livebrowser', (done) =>
{ {
open : 'external', open : 'external',
host : config.domain, host : config.domain,
port : 3000,
server : server :
{ {
baseDir : OUTPUT_DIR baseDir : OUTPUT_DIR
@ -226,6 +227,7 @@ gulp.task('browser', (done) =>
{ {
open : 'external', open : 'external',
host : config.domain, host : config.domain,
port : 3000,
server : server :
{ {
baseDir : OUTPUT_DIR baseDir : OUTPUT_DIR

View File

@ -6,23 +6,25 @@ import { getProtooUrl } from './urlFactory';
import * as cookiesManager from './cookiesManager'; import * as cookiesManager from './cookiesManager';
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 {
turnServers,
requestTimeout,
transportOptions
} from '../config';
const logger = new Logger('RoomClient'); const logger = new Logger('RoomClient');
const ROOM_OPTIONS = const ROOM_OPTIONS =
{ {
requestTimeout : 10000, requestTimeout : requestTimeout,
transportOptions : transportOptions : transportOptions,
{ turnServers : turnServers
tcp : true
}
}; };
const VIDEO_CONSTRAINS = const VIDEO_CONSTRAINS =
{ {
qvga : { width: { ideal: 320 }, height: { ideal: 240 } }, width : { ideal: 1280 },
vga : { width: { ideal: 640 }, height: { ideal: 480 } }, aspectRatio : 1.334
hd : { width: { ideal: 1280 }, height: { ideal: 720 } }
}; };
export default class RoomClient export default class RoomClient
@ -37,6 +39,9 @@ export default class RoomClient
const protooUrl = getProtooUrl(peerName, roomId); const protooUrl = getProtooUrl(peerName, roomId);
const protooTransport = new protooClient.WebSocketTransport(protooUrl); const protooTransport = new protooClient.WebSocketTransport(protooUrl);
// window element to external login site
this._loginWindow;
// Closed flag. // Closed flag.
this._closed = false; this._closed = false;
@ -52,6 +57,9 @@ export default class RoomClient
// Redux store getState function. // Redux store getState function.
this._getState = getState; this._getState = getState;
// This device
this._device = device;
// My peer name. // My peer name.
this._peerName = peerName; this._peerName = peerName;
@ -60,6 +68,7 @@ export default class RoomClient
// mediasoup-client Room instance. // mediasoup-client Room instance.
this._room = new mediasoupClient.Room(ROOM_OPTIONS); this._room = new mediasoupClient.Room(ROOM_OPTIONS);
this._room.roomId = roomId;
// Transport for sending. // Transport for sending.
this._sendTransport = null; this._sendTransport = null;
@ -77,6 +86,8 @@ export default class RoomClient
// @type {Map<String, MediaDeviceInfos>} // @type {Map<String, MediaDeviceInfos>}
this._webcams = new Map(); this._webcams = new Map();
this._audioDevices = new Map();
// Local Webcam. Object with: // Local Webcam. Object with:
// - {MediaDeviceInfo} [device] // - {MediaDeviceInfo} [device]
// - {String} [resolution] - 'qvga' / 'vga' / 'hd'. // - {String} [resolution] - 'qvga' / 'vga' / 'hd'.
@ -85,6 +96,10 @@ export default class RoomClient
resolution : 'hd' resolution : 'hd'
}; };
this._audioDevice = {
device : null
};
this._screenSharing = ScreenShare.create(); this._screenSharing = ScreenShare.create();
this._screenSharingProducer = null; this._screenSharingProducer = null;
@ -111,6 +126,20 @@ export default class RoomClient
this._dispatch(stateActions.setRoomState('closed')); this._dispatch(stateActions.setRoomState('closed'));
} }
login()
{
this._dispatch(stateActions.setLoginInProgress(true));
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
this._loginWindow = window.open(url, 'loginWindow');
}
closeLoginWindow()
{
this._loginWindow.close();
}
changeDisplayName(displayName) changeDisplayName(displayName)
{ {
logger.debug('changeDisplayName() [displayName:"%s"]', displayName); logger.debug('changeDisplayName() [displayName:"%s"]', displayName);
@ -350,9 +379,75 @@ export default class RoomClient
}); });
} }
changeWebcam() changeAudioDevice(deviceId)
{ {
logger.debug('changeWebcam()'); logger.debug('changeAudioDevice() [deviceId: %s]', deviceId);
this._dispatch(
stateActions.setAudioInProgress(true));
return Promise.resolve()
.then(() =>
{
this._audioDevice.device = this._audioDevices.get(deviceId);
logger.debug(
'changeAudioDevice() | new selected webcam [device:%o]',
this._audioDevice.device);
})
.then(() =>
{
const { device } = this._audioDevice;
if (!device)
throw new Error('no audio devices');
logger.debug('changeAudioDevice() | calling getUserMedia()');
return navigator.mediaDevices.getUserMedia(
{
audio :
{
deviceId : { exact: device.deviceId }
}
});
})
.then((stream) =>
{
const track = stream.getAudioTracks()[0];
return this._micProducer.replaceTrack(track)
.then((newTrack) =>
{
track.stop();
return newTrack;
});
})
.then((newTrack) =>
{
this._dispatch(
stateActions.setProducerTrack(this._micProducer.id, newTrack));
return this._updateAudioDevices();
})
.then(() =>
{
this._dispatch(
stateActions.setAudioInProgress(false));
})
.catch((error) =>
{
logger.error('changeAudioDevice() failed: %o', error);
this._dispatch(
stateActions.setAudioInProgress(false));
});
}
changeWebcam(deviceId)
{
logger.debug('changeWebcam() [deviceId: %s]', deviceId);
this._dispatch( this._dispatch(
stateActions.setWebcamInProgress(true)); stateActions.setWebcamInProgress(true));
@ -360,22 +455,7 @@ export default class RoomClient
return Promise.resolve() return Promise.resolve()
.then(() => .then(() =>
{ {
return this._updateWebcams(); this._webcam.device = this._webcams.get(deviceId);
})
.then(() =>
{
const array = Array.from(this._webcams.keys());
const len = array.length;
const deviceId =
this._webcam.device ? this._webcam.device.deviceId : undefined;
let idx = array.indexOf(deviceId);
if (idx < len - 1)
idx++;
else
idx = 0;
this._webcam.device = this._webcams.get(array[idx]);
logger.debug( logger.debug(
'changeWebcam() | new selected webcam [device:%o]', 'changeWebcam() | new selected webcam [device:%o]',
@ -386,7 +466,7 @@ export default class RoomClient
}) })
.then(() => .then(() =>
{ {
const { device, resolution } = this._webcam; const { device } = this._webcam;
if (!device) if (!device)
throw new Error('no webcam devices'); throw new Error('no webcam devices');
@ -398,7 +478,7 @@ export default class RoomClient
video : video :
{ {
deviceId : { exact: device.deviceId }, deviceId : { exact: device.deviceId },
...VIDEO_CONSTRAINS[resolution] ...VIDEO_CONSTRAINS
} }
}); });
}) })
@ -419,6 +499,10 @@ export default class RoomClient
this._dispatch( this._dispatch(
stateActions.setProducerTrack(this._webcamProducer.id, newTrack)); stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
return this._updateWebcams();
})
.then(() =>
{
this._dispatch( this._dispatch(
stateActions.setWebcamInProgress(false)); stateActions.setWebcamInProgress(false));
}) })
@ -463,7 +547,7 @@ export default class RoomClient
}) })
.then(() => .then(() =>
{ {
const { device, resolution } = this._webcam; const { device } = this._webcam;
logger.debug('changeWebcamResolution() | calling getUserMedia()'); logger.debug('changeWebcamResolution() | calling getUserMedia()');
@ -472,7 +556,7 @@ export default class RoomClient
video : video :
{ {
deviceId : { exact: device.deviceId }, deviceId : { exact: device.deviceId },
...VIDEO_CONSTRAINS[resolution] ...VIDEO_CONSTRAINS
} }
}); });
}) })
@ -507,6 +591,222 @@ export default class RoomClient
}); });
} }
mutePeerAudio(peerName)
{
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerAudioInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'mic')
continue;
consumer.pause('mute-audio');
}
}
}
this._dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('mutePeerAudio() failed: %o', error);
this._dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
});
}
unmutePeerAudio(peerName)
{
logger.debug('unmutePeerAudio() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerAudioInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'mic' || !consumer.supported)
continue;
consumer.resume();
}
}
}
this._dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('unmutePeerAudio() failed: %o', error);
this._dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
});
}
pausePeerVideo(peerName)
{
logger.debug('pausePeerVideo() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerVideoInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'webcam')
continue;
consumer.pause('pause-video');
}
}
}
this._dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('pausePeerVideo() failed: %o', error);
this._dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
});
}
resumePeerVideo(peerName)
{
logger.debug('resumePeerVideo() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerVideoInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'webcam' || !consumer.supported)
continue;
consumer.resume();
}
}
}
this._dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('resumePeerVideo() failed: %o', error);
this._dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
});
}
pausePeerScreen(peerName)
{
logger.debug('pausePeerScreen() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'screen')
continue;
consumer.pause('pause-screen');
}
}
}
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('pausePeerScreen() failed: %o', error);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
});
}
resumePeerScreen(peerName)
{
logger.debug('resumePeerScreen() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'screen' || !consumer.supported)
continue;
consumer.resume();
}
}
}
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('resumePeerScreen() failed: %o', error);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
});
}
enableAudioOnly() enableAudioOnly()
{ {
logger.debug('enableAudioOnly()'); logger.debug('enableAudioOnly()');
@ -587,6 +887,43 @@ export default class RoomClient
}); });
} }
sendRaiseHandState(state)
{
logger.debug('sendRaiseHandState: ', state);
this._dispatch(
stateActions.setMyRaiseHandStateInProgress(true));
return this._protoo.send('raisehand-message', { raiseHandState: state })
.then(() =>
{
this._dispatch(
stateActions.setMyRaiseHandState(state));
this._dispatch(requestActions.notify(
{
text : 'raiseHand state changed'
}));
this._dispatch(
stateActions.setMyRaiseHandStateInProgress(false));
})
.catch((error) =>
{
logger.error('sendRaiseHandState() | failed: %o', error);
this._dispatch(requestActions.notify(
{
type : 'error',
text : `Could not change raise hand state: ${error}`
}));
// We need to refresh the component for it to render changed state
this._dispatch(stateActions.setMyRaiseHandState(!state));
this._dispatch(
stateActions.setMyRaiseHandStateInProgress(false));
});
}
restartIce() restartIce()
{ {
logger.debug('restartIce()'); logger.debug('restartIce()');
@ -714,6 +1051,48 @@ export default class RoomClient
break; break;
} }
// This means: server wants to change MY displayName
case 'auth':
{
logger.debug('got auth event from server', request.data);
accept();
if (request.data.verified == true)
{
this.changeDisplayName(request.data.name);
this._dispatch(requestActions.notify(
{
text : `Authenticated successfully: ${request.data}`
}
));
}
else
{
this._dispatch(requestActions.notify(
{
text : `Authentication failed: ${request.data}`
}
));
}
this.closeLoginWindow();
this._dispatch(stateActions.setLoginInProgress(false));
break;
}
case 'raisehand-message':
{
accept();
const { peerName, raiseHandState } = request.data;
logger.debug('Got raiseHandState from "%s"', peerName);
this._dispatch(
stateActions.setPeerRaiseHandState(peerName, raiseHandState));
break;
}
case 'chat-message-receive': case 'chat-message-receive':
{ {
accept(); accept();
@ -926,6 +1305,12 @@ export default class RoomClient
let producer; let producer;
return Promise.resolve() return Promise.resolve()
.then(() =>
{
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
return this._updateAudioDevices();
})
.then(() => .then(() =>
{ {
logger.debug('_setMicProducer() | calling getUserMedia()'); logger.debug('_setMicProducer() | calling getUserMedia()');
@ -1078,6 +1463,14 @@ export default class RoomClient
this._dispatch(stateActions.removeProducer(producer.id)); this._dispatch(stateActions.removeProducer(producer.id));
}); });
producer.on('trackended', (originator) =>
{
logger.debug(
'webcam Producer "trackended" event [originator:%s]', originator);
this.disableScreenSharing();
});
producer.on('pause', (originator) => producer.on('pause', (originator) =>
{ {
logger.debug( logger.debug(
@ -1143,7 +1536,7 @@ export default class RoomClient
return Promise.resolve() return Promise.resolve()
.then(() => .then(() =>
{ {
const { device, resolution } = this._webcam; const { device } = this._webcam;
if (!device) if (!device)
throw new Error('no webcam devices'); throw new Error('no webcam devices');
@ -1155,7 +1548,7 @@ export default class RoomClient
video : video :
{ {
deviceId : { exact: device.deviceId }, deviceId : { exact: device.deviceId },
...VIDEO_CONSTRAINS[resolution] ...VIDEO_CONSTRAINS
} }
}); });
}) })
@ -1245,6 +1638,57 @@ export default class RoomClient
}); });
} }
_updateAudioDevices()
{
logger.debug('_updateAudioDevices()');
// Reset the list.
this._audioDevices = new Map();
return Promise.resolve()
.then(() =>
{
logger.debug('_updateAudioDevices() | calling enumerateDevices()');
return navigator.mediaDevices.enumerateDevices();
})
.then((devices) =>
{
for (const device of devices)
{
if (device.kind !== 'audioinput')
continue;
device.value = device.deviceId;
this._audioDevices.set(device.deviceId, device);
}
})
.then(() =>
{
const array = Array.from(this._audioDevices.values());
const len = array.length;
const currentAudioDeviceId =
this._audioDevice.device ? this._audioDevice.device.deviceId : undefined;
logger.debug('_updateAudioDevices() [audiodevices:%o]', array);
if (len === 0)
this._audioDevice.device = null;
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)
this._dispatch(
stateActions.setAudioDevices(this._audioDevices));
});
}
_updateWebcams() _updateWebcams()
{ {
logger.debug('_updateWebcams()'); logger.debug('_updateWebcams()');
@ -1266,6 +1710,8 @@ export default class RoomClient
if (device.kind !== 'videoinput') if (device.kind !== 'videoinput')
continue; continue;
device.value = device.deviceId;
this._webcams.set(device.deviceId, device); this._webcams.set(device.deviceId, device);
} }
}) })
@ -1285,6 +1731,12 @@ export default class RoomClient
this._dispatch( this._dispatch(
stateActions.setCanChangeWebcam(this._webcams.size >= 2)); stateActions.setCanChangeWebcam(this._webcams.size >= 2));
this._dispatch(
stateActions.setCanChangeWebcam(len >= 2));
if (len >= 1)
this._dispatch(
stateActions.setWebcamDevices(this._webcams));
}); });
} }

View File

@ -189,6 +189,70 @@ class FirefoxScreenShare
} }
} }
class EdgeScreenShare
{
constructor()
{
this._stream = null;
}
start(options = {})
{
const constraints = this._toConstraints(options);
return navigator.getDisplayMedia(constraints)
.then((stream) =>
{
this._stream = stream;
return Promise.resolve(stream);
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
return true;
}
needExtension()
{
return false;
}
_toConstraints()
{
const constraints = {
video : true
};
return constraints;
}
}
class DefaultScreenShare
{
isScreenShareAvailable()
{
return false;
}
needExtension()
{
return false;
}
}
export default class ScreenShare export default class ScreenShare
{ {
static create() static create()
@ -203,9 +267,13 @@ export default class ScreenShare
{ {
return new ChromeScreenShare(); return new ChromeScreenShare();
} }
case 'edge':
{
return new EdgeScreenShare();
}
default: default:
{ {
return null; return new DefaultScreenShare();
} }
} }
} }

View File

@ -0,0 +1,89 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as stateActions from '../../redux/stateActions';
import * as requestActions from '../../redux/requestActions';
import MessageList from './MessageList';
class Chat extends Component
{
render()
{
const {
senderPlaceHolder,
onSendMessage,
disabledInput,
autofocus,
displayName
} = this.props;
return (
<div data-component='Chat'>
<MessageList />
<form
data-component='Sender'
onSubmit={(e) => { onSendMessage(e, displayName); }}
>
<input
type='text'
className='new-message'
name='message'
placeholder={senderPlaceHolder}
disabled={disabledInput}
autoFocus={autofocus}
autoComplete='off'
/>
</form>
</div>
);
}
}
Chat.propTypes =
{
senderPlaceHolder : PropTypes.string,
onSendMessage : PropTypes.func,
disabledInput : PropTypes.bool,
autofocus : PropTypes.bool,
displayName : PropTypes.string
};
Chat.defaultProps =
{
senderPlaceHolder : 'Type a message...',
autofocus : true,
displayName : null
};
const mapStateToProps = (state) =>
{
return {
disabledInput : state.chatbehavior.disabledInput,
displayName : state.me.displayName
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onSendMessage : (event, displayName) =>
{
event.preventDefault();
const userInput = event.target.message.value;
if (userInput)
{
dispatch(stateActions.addUserMessage(userInput));
dispatch(requestActions.sendChatMessage(userInput, displayName));
}
event.target.message.value = '';
}
};
};
const ChatContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Chat);
export default ChatContainer;

View File

@ -7,6 +7,13 @@ import MessageList from './Chat/MessageList';
class ChatWidget extends Component class ChatWidget extends Component
{ {
componentWillReceiveProps(nextProps)
{
if (nextProps.chatmessages.length !== this.props.chatmessages.length)
if (!this.props.showChat)
this.props.increaseBadge();
}
render() render()
{ {
const { const {
@ -68,15 +75,15 @@ ChatWidget.propTypes =
disabledInput : PropTypes.bool, disabledInput : PropTypes.bool,
badge : PropTypes.number, badge : PropTypes.number,
autofocus : PropTypes.bool, autofocus : PropTypes.bool,
displayName : PropTypes.string displayName : PropTypes.string,
chatmessages : PropTypes.arrayOf(PropTypes.object),
increaseBadge : PropTypes.func
}; };
ChatWidget.defaultProps = ChatWidget.defaultProps =
{ {
senderPlaceHolder : 'Type a message...', senderPlaceHolder : 'Type a message...',
badge : 0, autofocus : true
autofocus : true,
displayName : null
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -84,7 +91,9 @@ const mapStateToProps = (state) =>
return { return {
showChat : state.chatbehavior.showChat, showChat : state.chatbehavior.showChat,
disabledInput : state.chatbehavior.disabledInput, disabledInput : state.chatbehavior.disabledInput,
displayName : state.me.displayName displayName : state.me.displayName,
badge : state.chatbehavior.badge,
chatmessages : state.chatmessages
}; };
}; };
@ -106,6 +115,10 @@ const mapDispatchToProps = (dispatch) =>
dispatch(requestActions.sendChatMessage(userInput, displayName)); dispatch(requestActions.sendChatMessage(userInput, displayName));
} }
event.target.message.value = ''; event.target.message.value = '';
},
increaseBadge : () =>
{
dispatch(stateActions.increaseBadge());
} }
}; };
}; };

View File

@ -0,0 +1,91 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from './appPropTypes';
import * as stateActions from '../redux/stateActions';
import FullView from './FullView';
const FullScreenView = (props) =>
{
const {
advancedMode,
consumer,
toggleConsumerFullscreen
} = props;
if (!consumer)
return null;
const consumerVisible = (
Boolean(consumer) &&
!consumer.locallyPaused &&
!consumer.remotelyPaused
);
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return (
<div data-component='FullScreenView'>
{consumerVisible && !consumer.supported ?
<div className='incompatible-video'>
<p>incompatible video</p>
</div>
:null
}
<div className='controls'>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(consumer);
}}
/>
</div>
<FullView
advancedMode={advancedMode}
videoTrack={consumer ? consumer.track : null}
videoVisible={consumerVisible}
videoProfile={consumerProfile}
/>
</div>
);
};
FullScreenView.propTypes =
{
advancedMode : PropTypes.bool,
consumer : appPropTypes.Consumer,
toggleConsumerFullscreen : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
consumer : state.consumers[state.room.fullScreenConsumer]
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleConsumerFullscreen : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
}
};
};
const FullScreenViewContainer = connect(
mapStateToProps,
mapDispatchToProps
)(FullScreenView);
export default FullScreenViewContainer;

View File

@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Spinner from 'react-spinner';
export default class FullView extends React.Component
{
constructor(props)
{
super(props);
// Latest received video track.
// @type {MediaStreamTrack}
this._videoTrack = null;
}
render()
{
const {
videoVisible,
videoProfile
} = this.props;
return (
<div data-component='FullView'>
<video
ref='video'
className={classnames({
hidden : !videoVisible,
loading : videoProfile === 'none'
})}
autoPlay
muted={Boolean(true)}
/>
{videoProfile === 'none' ?
<div className='spinner-container'>
<Spinner />
</div>
:null
}
</div>
);
}
componentDidMount()
{
const { videoTrack } = this.props;
this._setTracks(videoTrack);
}
componentWillReceiveProps(nextProps)
{
const { videoTrack } = nextProps;
this._setTracks(videoTrack);
}
_setTracks(videoTrack)
{
if (this._videoTrack === videoTrack)
return;
this._videoTrack = videoTrack;
const { video } = this.refs;
if (videoTrack)
{
const stream = new MediaStream;
if (videoTrack)
stream.addTrack(videoTrack);
video.srcObject = stream;
}
else
{
video.srcObject = null;
}
}
}
FullView.propTypes =
{
videoTrack : PropTypes.any,
videoVisible : PropTypes.bool,
videoProfile : PropTypes.string
};

View File

@ -7,6 +7,7 @@ import { getDeviceInfo } from 'mediasoup-client';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import PeerView from './PeerView'; import PeerView from './PeerView';
import ScreenView from './ScreenView';
class Me extends React.Component class Me extends React.Component
{ {
@ -29,14 +30,15 @@ class Me extends React.Component
const { const {
connected, connected,
me, me,
advancedMode,
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer,
onChangeDisplayName, onChangeDisplayName,
onMuteMic, onMuteMic,
onUnmuteMic, onUnmuteMic,
onEnableWebcam, onEnableWebcam,
onDisableWebcam, onDisableWebcam
onChangeWebcam
} = this.props; } = this.props;
let micState; let micState;
@ -59,19 +61,18 @@ class Me extends React.Component
else else
webcamState = 'off'; webcamState = 'off';
let changeWebcamState;
if (Boolean(webcamProducer) && me.canChangeWebcam)
changeWebcamState = 'on';
else
changeWebcamState = 'unsupported';
const videoVisible = ( const videoVisible = (
Boolean(webcamProducer) && Boolean(webcamProducer) &&
!webcamProducer.locallyPaused && !webcamProducer.locallyPaused &&
!webcamProducer.remotelyPaused !webcamProducer.remotelyPaused
); );
const screenVisible = (
Boolean(screenProducer) &&
!screenProducer.locallyPaused &&
!screenProducer.remotelyPaused
);
let tip; let tip;
if (!me.displayNameSet) if (!me.displayNameSet)
@ -85,47 +86,58 @@ class Me extends React.Component
data-tip-disable={!tip} data-tip-disable={!tip}
data-type='dark' data-type='dark'
> >
{connected ? <div className={classnames('view-container', 'webcam')}>
<div className='controls'> {connected ?
<div <div className='controls'>
className={classnames('button', 'mic', micState)} <div
onClick={() => className={classnames('button', 'mic', micState, {
{ disabled : me.audioInProgress
micState === 'on' ? onMuteMic() : onUnmuteMic(); })}
}} onClick={() =>
/> {
micState === 'on' ? onMuteMic() : onUnmuteMic();
}}
/>
<div <div
className={classnames('button', 'webcam', webcamState, { className={classnames('button', 'webcam', webcamState, {
disabled : me.webcamInProgress disabled : me.webcamInProgress
})} })}
onClick={() => onClick={() =>
{ {
webcamState === 'on' ? onDisableWebcam() : onEnableWebcam(); webcamState === 'on' ? onDisableWebcam() : onEnableWebcam();
}} }}
/> />
</div>
:null
}
<div <PeerView
className={classnames('button', 'change-webcam', changeWebcamState, { isMe
disabled : me.webcamInProgress advancedMode={advancedMode}
})} peer={me}
onClick={() => onChangeWebcam()} audioTrack={micProducer ? micProducer.track : null}
videoTrack={webcamProducer ? webcamProducer.track : null}
videoVisible={videoVisible}
audioCodec={micProducer ? micProducer.codec : null}
videoCodec={webcamProducer ? webcamProducer.codec : null}
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)}
/>
</div>
{screenProducer ?
<div className={classnames('view-container', 'screen')}>
<ScreenView
isMe
advancedMode={advancedMode}
screenTrack={screenProducer ? screenProducer.track : null}
screenVisible={screenVisible}
screenCodec={screenProducer ? screenProducer.codec : null}
/> />
</div> </div>
:null :null
} }
<PeerView
isMe
peer={me}
audioTrack={micProducer ? micProducer.track : null}
videoTrack={webcamProducer ? webcamProducer.track : null}
videoVisible={videoVisible}
audioCodec={micProducer ? micProducer.codec : null}
videoCodec={webcamProducer ? webcamProducer.codec : null}
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)}
/>
{this._tooltip ? {this._tooltip ?
<ReactTooltip <ReactTooltip
effect='solid' effect='solid'
@ -172,15 +184,16 @@ class Me extends React.Component
Me.propTypes = Me.propTypes =
{ {
connected : PropTypes.bool.isRequired, connected : PropTypes.bool.isRequired,
advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
micProducer : appPropTypes.Producer, micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
onChangeDisplayName : PropTypes.func.isRequired, onChangeDisplayName : PropTypes.func.isRequired,
onMuteMic : PropTypes.func.isRequired, onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired, onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired, onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired, onDisableWebcam : PropTypes.func.isRequired
onChangeWebcam : PropTypes.func.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -190,12 +203,15 @@ const mapStateToProps = (state) =>
producersArray.find((producer) => producer.source === 'mic'); producersArray.find((producer) => producer.source === 'mic');
const webcamProducer = const webcamProducer =
producersArray.find((producer) => producer.source === 'webcam'); producersArray.find((producer) => producer.source === 'webcam');
const screenProducer =
producersArray.find((producer) => producer.source === 'screen');
return { return {
connected : state.room.state === 'connected', connected : state.room.state === 'connected',
me : state.me, me : state.me,
micProducer : micProducer, micProducer : micProducer,
webcamProducer : webcamProducer webcamProducer : webcamProducer,
screenProducer : screenProducer
}; };
}; };
@ -209,8 +225,7 @@ const mapDispatchToProps = (dispatch) =>
onMuteMic : () => dispatch(requestActions.muteMic()), onMuteMic : () => dispatch(requestActions.muteMic()),
onUnmuteMic : () => dispatch(requestActions.unmuteMic()), onUnmuteMic : () => dispatch(requestActions.unmuteMic()),
onEnableWebcam : () => dispatch(requestActions.enableWebcam()), onEnableWebcam : () => dispatch(requestActions.enableWebcam()),
onDisableWebcam : () => dispatch(requestActions.disableWebcam()), onDisableWebcam : () => dispatch(requestActions.disableWebcam())
onChangeWebcam : () => dispatch(requestActions.changeWebcam())
}; };
}; };

View File

@ -0,0 +1,166 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../../redux/requestActions';
const ListPeer = (props) =>
{
const {
peer,
micConsumer,
webcamConsumer,
screenConsumer,
onMuteMic,
onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen
} = props;
const micEnabled = (
Boolean(micConsumer) &&
!micConsumer.locallyPaused &&
!micConsumer.remotelyPaused
);
const videoVisible = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!webcamConsumer.remotelyPaused
);
const screenVisible = (
Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
!screenConsumer.remotelyPaused
);
return (
<div data-component='ListPeer'>
<img className='avatar' />
<div className='peer-info'>
{peer.displayName}
</div>
<div className='controls'>
{ screenConsumer ?
<div
className={classnames('button', 'screen', {
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
onDisableScreen(peer.name) : onEnableScreen(peer.name);
}}
/>
:null
}
<div
className={classnames('button', 'mic', {
on : micEnabled,
off : !micEnabled,
disabled : peer.peerAudioInProgress
})}
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>
);
};
ListPeer.propTypes =
{
advancedMode : PropTypes.bool,
peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired,
onEnableScreen : PropTypes.func.isRequired,
onDisableScreen : PropTypes.func.isRequired
};
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');
const webcamConsumer =
consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen');
return {
peer,
micConsumer,
webcamConsumer,
screenConsumer
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onMuteMic : (peerName) =>
{
dispatch(requestActions.mutePeerAudio(peerName));
},
onUnmuteMic : (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));
}
};
};
const ListPeerContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ListPeer);
export default ListPeerContainer;

View File

@ -0,0 +1,80 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../../redux/requestActions';
import * as stateActions from '../../redux/stateActions';
import PropTypes from 'prop-types';
import ListPeer from './ListPeer';
class ParticipantList extends React.Component
{
constructor(props)
{
super(props);
}
render()
{
const {
advancedMode,
peers
} = this.props;
return (
<div data-component='ParticipantList'>
<ul className='list'>
{
peers.map((peer) =>
{
return (
<li key={peer.name} className='list-item'>
<ListPeer name={peer.name} advancedMode={advancedMode} />
</li>
);
})
}
</ul>
</div>
);
}
}
ParticipantList.propTypes =
{
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
};
const mapStateToProps = (state) =>
{
const peersArray = Object.values(state.peers);
return {
peers : peersArray
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
handleChangeWebcam : (device) =>
{
dispatch(requestActions.changeWebcam(device.value));
},
handleChangeAudioDevice : (device) =>
{
dispatch(requestActions.changeAudioDevice(device.value));
},
onToggleAdvancedMode : () =>
{
dispatch(stateActions.toggleAdvancedMode());
}
};
};
const ParticipantListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ParticipantList);
export default ParticipantListContainer;

View File

@ -1,15 +1,29 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions';
import PeerView from './PeerView'; import PeerView from './PeerView';
import ScreenView from './ScreenView';
const Peer = (props) => const Peer = (props) =>
{ {
const { const {
advancedMode,
peer, peer,
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
screenConsumer screenConsumer,
onMuteMic,
onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen,
toggleConsumerFullscreen,
style
} = props; } = props;
const micEnabled = ( const micEnabled = (
@ -41,18 +55,12 @@ const Peer = (props) =>
screenProfile = screenConsumer.profile; screenProfile = screenConsumer.profile;
return ( return (
<div data-component='Peer'> <div
<div className='indicators'> data-component='Peer'
{!micEnabled ? className={classnames({
<div className='icon mic-off' /> screen : screenConsumer
:null })}
} >
{!videoVisible ?
<div className='icon webcam-off' />
:null
}
</div>
{videoVisible && !webcamConsumer.supported ? {videoVisible && !webcamConsumer.supported ?
<div className='incompatible-video'> <div className='incompatible-video'>
<p>incompatible video</p> <p>incompatible video</p>
@ -60,29 +68,112 @@ const Peer = (props) =>
:null :null
} }
<PeerView <div className={classnames('view-container', 'webcam')} style={style}>
peer={peer} <div className='controls'>
audioTrack={micConsumer ? micConsumer.track : null} <div
videoTrack={webcamConsumer ? webcamConsumer.track : null} className={classnames('button', 'mic', {
screenTrack={screenConsumer ? screenConsumer.track : null} on : micEnabled,
videoVisible={videoVisible} off : !micEnabled,
videoProfile={videoProfile} disabled : peer.peerAudioInProgress
screenVisible={screenVisible} })}
screenProfile={screenProfile} onClick={(e) =>
audioCodec={micConsumer ? micConsumer.codec : null} {
videoCodec={webcamConsumer ? webcamConsumer.codec : null} e.stopPropagation();
screenCodec={screenConsumer ? screenConsumer.codec : null} 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
className={classnames('button', 'fullscreen')}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(webcamConsumer);
}}
/>
</div>
<PeerView
advancedMode={advancedMode}
peer={peer}
audioTrack={micConsumer ? micConsumer.track : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible}
videoProfile={videoProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
/>
</div>
{screenConsumer ?
<div className={classnames('view-container', 'screen')} style={style}>
<div className='controls'>
<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) =>
{
e.stopPropagation();
toggleConsumerFullscreen(screenConsumer);
}}
/>
</div>
<ScreenView
advancedMode={advancedMode}
screenTrack={screenConsumer ? screenConsumer.track : null}
screenVisible={screenVisible}
screenProfile={screenProfile}
screenCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
:null
}
</div> </div>
); );
}; };
Peer.propTypes = Peer.propTypes =
{ {
peer : appPropTypes.Peer.isRequired, advancedMode : PropTypes.bool,
micConsumer : appPropTypes.Consumer, peer : appPropTypes.Peer.isRequired,
webcamConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer webcamConsumer : appPropTypes.Consumer,
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
}; };
const mapStateToProps = (state, { name }) => const mapStateToProps = (state, { name }) =>
@ -105,6 +196,45 @@ const mapStateToProps = (state, { name }) =>
}; };
}; };
const PeerContainer = connect(mapStateToProps)(Peer); const mapDispatchToProps = (dispatch) =>
{
return {
onMuteMic : (peerName) =>
{
dispatch(requestActions.mutePeerAudio(peerName));
},
onUnmuteMic : (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) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
}
};
};
const PeerContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Peer);
export default PeerContainer; export default PeerContainer;

View File

@ -14,11 +14,9 @@ export default class PeerView extends React.Component
this.state = this.state =
{ {
volume : 0, // Integer from 0 to 10., volume : 0, // Integer from 0 to 10.,
videoWidth : null, videoWidth : null,
videoHeight : null, videoHeight : null
screenWidth : null,
screenHeight : null
}; };
// Latest received video track. // Latest received video track.
@ -29,10 +27,6 @@ export default class PeerView extends React.Component
// @type {MediaStreamTrack} // @type {MediaStreamTrack}
this._videoTrack = null; this._videoTrack = null;
// Latest received screen track.
// @type {MediaStreamTrack}
this._screenTrack = null;
// Hark instance. // Hark instance.
// @type {Object} // @type {Object}
this._hark = null; this._hark = null;
@ -46,51 +40,31 @@ export default class PeerView extends React.Component
const { const {
isMe, isMe,
peer, peer,
advancedMode,
videoVisible, videoVisible,
videoProfile, videoProfile,
screenVisible,
screenProfile,
audioCodec, audioCodec,
videoCodec, videoCodec,
screenCodec,
onChangeDisplayName onChangeDisplayName
} = this.props; } = this.props;
const { const {
volume, volume,
videoWidth, videoWidth,
videoHeight, videoHeight
screenWidth,
screenHeight
} = this.state; } = this.state;
return ( return (
<div data-component='PeerView'> <div data-component='PeerView'>
<div className='info'> <div className='info'>
<div className={classnames('media', { 'is-me': isMe })}> {advancedMode ?
{screenVisible ? <div className={classnames('media', { 'is-me': isMe })}>
<div className='box'> <div className='box'>
{audioCodec ? {audioCodec ?
<p className='codec'>{audioCodec}</p> <p className='codec'>{audioCodec}</p>
:null :null
} }
{screenCodec ?
<p className='codec'>{screenCodec} {screenProfile}</p>
:null
}
{(screenVisible && screenWidth !== null) ?
<p className='resolution'>{screenWidth}x{screenHeight}</p>
:null
}
</div>
:<div className='box'>
{audioCodec ?
<p className='codec'>{audioCodec}</p>
:null
}
{videoCodec ? {videoCodec ?
<p className='codec'>{videoCodec} {videoProfile}</p> <p className='codec'>{videoCodec} {videoProfile}</p>
:null :null
@ -101,8 +75,9 @@ export default class PeerView extends React.Component
:null :null
} }
</div> </div>
} </div>
</div> :null
}
<div className={classnames('peer', { 'is-me': isMe })}> <div className={classnames('peer', { 'is-me': isMe })}>
{isMe ? {isMe ?
@ -126,49 +101,36 @@ export default class PeerView extends React.Component
</span> </span>
} }
<div className='row'> {advancedMode ?
<span <div className='row'>
className={classnames('device-icon', peer.device.flag)} <span
/> className={classnames('device-icon', peer.device.flag)}
<span className='device-version'> />
{peer.device.name} {Math.floor(peer.device.version) || null} <span className='device-version'>
</span> {peer.device.name} {Math.floor(peer.device.version) || null}
</div> </span>
</div>
:null
}
</div> </div>
</div> </div>
<video <video
ref='video' ref='video'
className={classnames({ className={classnames({
hidden : !videoVisible && !screenVisible, hidden : !videoVisible,
'is-me' : isMe, 'is-me' : isMe,
loading : videoProfile === 'none' && screenProfile === 'none' loading : videoProfile === 'none'
})} })}
autoPlay autoPlay
muted={isMe} muted={isMe}
/> />
{screenVisible ?
<div className='minivideo'>
<video
ref='minivideo'
className={classnames({
hidden : !videoVisible,
'is-me' : isMe,
loading : videoProfile === 'none'
})}
autoPlay
muted={isMe}
/>
</div>
:null
}
<div className='volume-container'> <div className='volume-container'>
<div className={classnames('bar', `level${volume}`)} /> <div className={classnames('bar', `level${volume}`)} />
</div> </div>
{videoProfile === 'none' && screenProfile === 'none' ? {videoProfile === 'none' ?
<div className='spinner-container'> <div className='spinner-container'>
<Spinner /> <Spinner />
</div> </div>
@ -180,9 +142,9 @@ export default class PeerView extends React.Component
componentDidMount() componentDidMount()
{ {
const { audioTrack, videoTrack, screenTrack } = this.props; const { audioTrack, videoTrack } = this.props;
this._setTracks(audioTrack, videoTrack, screenTrack); this._setTracks(audioTrack, videoTrack);
} }
componentWillUnmount() componentWillUnmount()
@ -195,21 +157,18 @@ export default class PeerView extends React.Component
componentWillReceiveProps(nextProps) componentWillReceiveProps(nextProps)
{ {
const { audioTrack, videoTrack, screenTrack } = nextProps; const { audioTrack, videoTrack } = nextProps;
this._setTracks(audioTrack, videoTrack, screenTrack); this._setTracks(audioTrack, videoTrack);
} }
_setTracks(audioTrack, videoTrack, screenTrack) _setTracks(audioTrack, videoTrack)
{ {
if (this._audioTrack === audioTrack && if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
this._videoTrack === videoTrack &&
this._screenTrack === screenTrack)
return; return;
this._audioTrack = audioTrack; this._audioTrack = audioTrack;
this._videoTrack = videoTrack; this._videoTrack = videoTrack;
this._screenTrack = screenTrack;
if (this._hark) if (this._hark)
this._hark.stop(); this._hark.stop();
@ -217,9 +176,9 @@ export default class PeerView extends React.Component
clearInterval(this._videoResolutionTimer); clearInterval(this._videoResolutionTimer);
this._hideVideoResolution(); this._hideVideoResolution();
const { video, minivideo } = this.refs; const { video } = this.refs;
if (audioTrack || videoTrack || screenTrack) if (audioTrack || videoTrack)
{ {
const stream = new MediaStream; const stream = new MediaStream;
@ -229,19 +188,7 @@ export default class PeerView extends React.Component
if (videoTrack) if (videoTrack)
stream.addTrack(videoTrack); stream.addTrack(videoTrack);
if (screenTrack) video.srcObject = stream;
{
const screenStream = new MediaStream;
screenStream.addTrack(screenTrack);
video.srcObject = screenStream;
minivideo.srcObject = stream;
}
else
{
video.srcObject = stream;
}
if (audioTrack) if (audioTrack)
this._runHark(stream); this._runHark(stream);
@ -310,15 +257,12 @@ PeerView.propTypes =
isMe : PropTypes.bool, isMe : PropTypes.bool,
peer : PropTypes.oneOfType( peer : PropTypes.oneOfType(
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired, [ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
advancedMode : PropTypes.bool,
audioTrack : PropTypes.any, audioTrack : PropTypes.any,
videoTrack : PropTypes.any, videoTrack : PropTypes.any,
screenTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired, videoVisible : PropTypes.bool.isRequired,
videoProfile : PropTypes.string, videoProfile : PropTypes.string,
screenVisible : PropTypes.bool.isRequired,
screenProfile : PropTypes.string,
audioCodec : PropTypes.string, audioCodec : PropTypes.string,
videoCodec : PropTypes.string, videoCodec : PropTypes.string,
screenCodec : PropTypes.string,
onChangeDisplayName : PropTypes.func onChangeDisplayName : PropTypes.func
}; };

View File

@ -3,23 +3,29 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
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 { Appear } from './transitions'; import { Appear } from './transitions';
import Peer from './Peer'; import Peer from './Peer';
class Peers extends React.Component class Peers extends React.Component
{ {
constructor() constructor(props)
{ {
super(); super(props);
this.state = { this.state = {
ratio : 4 / 3 peerWidth : 400,
peerHeight : 300,
ratio : 1.334
}; };
} }
updateDimensions()
resizeUpdate()
{ {
const n = this.props.peers.length; this.updateDimensions();
}
updateDimensions(props = this.props)
{
const n = props.videoStreams ? props.videoStreams : 0;
if (n == 0) if (n == 0)
{ {
@ -47,38 +53,44 @@ class Peers extends React.Component
break; break;
} }
} }
if (Math.ceil(this.props.peerWidth) !== Math.ceil(0.9 * x)) if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x))
{ {
this.props.onComponentResize(0.9 * x, 0.9 * y); this.setState({
peerWidth : 0.9 * x,
peerHeight : 0.9 * y
});
} }
} }
componentDidMount() componentDidMount()
{ {
window.addEventListener('resize', this.updateDimensions.bind(this)); window.addEventListener('resize', this.resizeUpdate.bind(this));
} }
componentWillUnmount() componentWillUnmount()
{ {
window.removeEventListener('resize', this.updateDimensions.bind(this)); window.removeEventListener('resize', this.resizeUpdate.bind(this));
}
componentWillReceiveProps(nextProps)
{
this.updateDimensions(nextProps);
} }
render() render()
{ {
const { const {
advancedMode,
activeSpeakerName, activeSpeakerName,
peers, peers,
peerWidth, toolAreaOpen
peerHeight
} = this.props; } = this.props;
const style = const style =
{ {
'width' : peerWidth, 'width' : this.state.peerWidth,
'height' : peerHeight 'height' : this.state.peerHeight
}; };
this.updateDimensions();
return ( return (
<div data-component='Peers' ref='peers'> <div data-component='Peers' ref='peers'>
@ -90,9 +102,14 @@ class Peers extends React.Component
<div <div
className={classnames('peer-container', { className={classnames('peer-container', {
'active-speaker' : peer.name === activeSpeakerName 'active-speaker' : peer.name === activeSpeakerName
})} style={style} })}
> >
<Peer name={peer.name} /> <Peer
advancedMode={advancedMode}
name={peer.name}
style={style}
toolAreaOpen={toolAreaOpen}
/>
</div> </div>
</Appear> </Appear>
); );
@ -105,40 +122,33 @@ class Peers extends React.Component
Peers.propTypes = Peers.propTypes =
{ {
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
videoStreams : PropTypes.any,
activeSpeakerName : PropTypes.string, activeSpeakerName : PropTypes.string,
peerHeight : PropTypes.number, toolAreaOpen : PropTypes.bool
peerWidth : PropTypes.number,
onComponentResize : PropTypes.func.isRequired
};
const mapDispatchToProps = (dispatch) =>
{
return {
onComponentResize : (peerWidth, peerHeight) =>
{
dispatch(stateActions.onComponentResize(peerWidth, peerHeight));
}
};
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
// TODO: This is not OK since it's creating a new array every time, so triggering a
// component rendering.
const peersArray = Object.values(state.peers); const peersArray = Object.values(state.peers);
const videoStreamsArray = Object.values(state.consumers);
const videoStreams =
videoStreamsArray.filter((consumer) =>
{
return (consumer.source === 'webcam' || consumer.source === 'screen');
}).length;
return { return {
peers : peersArray, peers : peersArray,
videoStreams : videoStreams,
activeSpeakerName : state.room.activeSpeakerName, activeSpeakerName : state.room.activeSpeakerName,
peerHeight : state.room.peerHeight, toolAreaOpen : state.toolarea.toolAreaOpen
peerWidth : state.room.peerWidth
}; };
}; };
const PeersContainer = connect( const PeersContainer = connect(
mapStateToProps, mapStateToProps
mapDispatchToProps
)(Peers); )(Peers);
export default PeersContainer; export default PeersContainer;

View File

@ -10,7 +10,10 @@ import { Appear } from './transitions';
import Me from './Me'; import Me from './Me';
import Peers from './Peers'; import Peers from './Peers';
import Notifications from './Notifications'; import Notifications from './Notifications';
import ChatWidget from './ChatWidget'; import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea';
import FullScreenView from './FullScreenView';
import Draggable from 'react-draggable';
class Room extends React.Component class Room extends React.Component
{ {
@ -19,15 +22,15 @@ class Room extends React.Component
const { const {
room, room,
me, me,
toolAreaOpen,
amActiveSpeaker, amActiveSpeaker,
screenProducer, screenProducer,
onRoomLinkCopy, onRoomLinkCopy,
onSetAudioMode, onLogin,
onRestartIce,
onLeaveMeeting,
onShareScreen, onShareScreen,
onUnShareScreen, onUnShareScreen,
onNeedExtension onNeedExtension,
onLeaveMeeting
} = this.props; } = this.props;
let screenState; let screenState;
@ -57,119 +60,142 @@ class Room extends React.Component
return ( return (
<Appear duration={300}> <Appear duration={300}>
<div data-component='Room'> <div data-component='Room'>
<Notifications /> <FullScreenView advancedMode={room.advancedMode} />
<ChatWidget />
<div className='state' data-tip='Server status'>
<div className={classnames('icon', room.state)} />
<p className={classnames('text', room.state)}>{room.state}</p>
</div>
<div className='room-link-wrapper'>
<div className='room-link'>
<ClipboardButton
component='a'
className='link'
button-href={room.url}
button-target='_blank'
data-tip='Click to copy room link'
data-clipboard-text={room.url}
onSuccess={onRoomLinkCopy}
onClick={(event) =>
{
// If this is a 'Open in new window/tab' don't prevent
// click default action.
if (
event.ctrlKey || event.shiftKey || event.metaKey ||
// Middle click (IE > 9 and everyone else).
(event.button && event.button === 1)
)
{
return;
}
event.preventDefault();
}}
>
invitation link
</ClipboardButton>
</div>
</div>
<Peers />
<div <div
className={classnames('me-container', { className='room-wrapper'
'active-speaker' : amActiveSpeaker style={{
})} width : toolAreaOpen ? '80%' : '100%'
}}
> >
<Me /> <Notifications />
</div> <ToolAreaButton />
<div className='sidebar'> {room.advancedMode ?
<div <div className='state' data-tip='Server status'>
className={classnames('button', 'screen', screenState)} <div className={classnames('icon', room.state)} />
data-tip={screenTip} <p className={classnames('text', room.state)}>{room.state}</p>
data-type='dark' </div>
onClick={() => :null
{ }
switch (screenState)
<div className='room-link-wrapper'>
<div className='room-link'>
<ClipboardButton
component='a'
className='link'
button-href={room.url}
button-target='_blank'
data-tip='Click to copy room link'
data-clipboard-text={room.url}
onSuccess={onRoomLinkCopy}
onClick={(event) =>
{
// If this is a 'Open in new window/tab' don't prevent
// click default action.
if (
event.ctrlKey || event.shiftKey || event.metaKey ||
// Middle click (IE > 9 and everyone else).
(event.button && event.button === 1)
)
{
return;
}
event.preventDefault();
}}
>
invitation link
</ClipboardButton>
</div>
</div>
<Peers
advancedMode={room.advancedMode}
/>
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
<div
className={classnames('me-container', {
'active-speaker' : amActiveSpeaker
})}
>
<Me
advancedMode={room.advancedMode}
/>
</div>
</Draggable>
<div className='sidebar'>
<div
className={classnames('button', 'screen', screenState)}
data-tip={screenTip}
data-type='dark'
onClick={() =>
{ {
case 'on': switch (screenState)
{ {
onUnShareScreen(); case 'on':
break; {
onUnShareScreen();
break;
}
case 'off':
{
onShareScreen();
break;
}
case 'need-extension':
{
onNeedExtension();
break;
}
default:
{
break;
}
} }
case 'off': }}
{ />
onShareScreen();
break;
}
case 'need-extension':
{
onNeedExtension();
break;
}
default:
{
break;
}
}
}}
/>
<div {me.loginEnabled ?
className={classnames('button', 'audio-only', { <div
on : me.audioOnly, className={classnames('button', 'login', 'off', {
disabled : me.audioOnlyInProgress disabled : me.loginInProgress
})} })}
data-tip='Toggle audio only mode' data-tip='Login'
data-type='dark' data-type='dark'
onClick={() => onSetAudioMode(!me.audioOnly)} onClick={() => onLogin()}
/> />
:null
}
<div <div
className={classnames('button', 'restart-ice', { className={classnames('button', 'leave-meeting')}
disabled : me.restartIceInProgress data-tip='Leave meeting'
})} data-type='dark'
data-tip='Restart ICE' onClick={() => onLeaveMeeting()}
data-type='dark' />
onClick={() => onRestartIce()} </div>
/>
<div <ReactTooltip
className={classnames('button', 'leave-meeting')} effect='solid'
data-tip='Leave meeting' delayShow={100}
data-type='dark' delayHide={100}
onClick={() => onLeaveMeeting()}
/> />
</div> </div>
<div
<ReactTooltip className='toolarea-wrapper'
effect='solid' style={{
delayShow={100} width : toolAreaOpen ? '20%' : '0%'
delayHide={100} }}
/> >
{toolAreaOpen ?
<ToolArea
advancedMode={room.advancedMode}
/>
:null
}
</div>
</div> </div>
</Appear> </Appear>
); );
@ -181,14 +207,14 @@ Room.propTypes =
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired, amActiveSpeaker : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer,
onRoomLinkCopy : PropTypes.func.isRequired, onRoomLinkCopy : PropTypes.func.isRequired,
onSetAudioMode : PropTypes.func.isRequired,
onRestartIce : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired,
onShareScreen : PropTypes.func.isRequired, onShareScreen : PropTypes.func.isRequired,
onUnShareScreen : PropTypes.func.isRequired, onUnShareScreen : PropTypes.func.isRequired,
onNeedExtension : PropTypes.func.isRequired onNeedExtension : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired,
onLogin : PropTypes.func.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -200,6 +226,7 @@ const mapStateToProps = (state) =>
return { return {
room : state.room, room : state.room,
me : state.me, me : state.me,
toolAreaOpen : state.toolarea.toolAreaOpen,
amActiveSpeaker : state.me.name === state.room.activeSpeakerName, amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
screenProducer : screenProducer screenProducer : screenProducer
}; };
@ -215,17 +242,6 @@ const mapDispatchToProps = (dispatch) =>
text : 'Room link copied to the clipboard' text : 'Room link copied to the clipboard'
})); }));
}, },
onSetAudioMode : (enable) =>
{
if (enable)
dispatch(requestActions.enableAudioOnly());
else
dispatch(requestActions.disableAudioOnly());
},
onRestartIce : () =>
{
dispatch(requestActions.restartIce());
},
onLeaveMeeting : () => onLeaveMeeting : () =>
{ {
dispatch(requestActions.leaveRoom()); dispatch(requestActions.leaveRoom());
@ -241,6 +257,10 @@ const mapDispatchToProps = (dispatch) =>
onNeedExtension : () => onNeedExtension : () =>
{ {
dispatch(requestActions.installExtension()); dispatch(requestActions.installExtension());
},
onLogin : () =>
{
dispatch(requestActions.userLogin());
} }
}; };
}; };

View File

@ -0,0 +1,168 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Spinner from 'react-spinner';
export default class PeerView extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
screenWidth : null,
screenHeight : null
};
// Latest received screen track.
// @type {MediaStreamTrack}
this._screenTrack = null;
// Periodic timer for showing video resolution.
this._screenResolutionTimer = null;
}
render()
{
const {
isMe,
advancedMode,
screenVisible,
screenProfile,
screenCodec
} = this.props;
const {
screenWidth,
screenHeight
} = this.state;
return (
<div data-component='ScreenView'>
<div className='info'>
{advancedMode ?
<div className={classnames('media', { 'is-me': isMe })}>
{screenVisible ?
<div className='box'>
{screenCodec ?
<p className='codec'>{screenCodec} {screenProfile}</p>
:null
}
{(screenVisible && screenWidth !== null) ?
<p className='resolution'>{screenWidth}x{screenHeight}</p>
:null
}
</div>
:null
}
</div>
:null
}
</div>
<video
ref='video'
className={classnames({
hidden : !screenVisible,
'is-me' : isMe,
loading : screenProfile === 'none'
})}
autoPlay
muted={Boolean(true)}
/>
{screenProfile === 'none' ?
<div className='spinner-container'>
<Spinner />
</div>
:null
}
</div>
);
}
componentDidMount()
{
const { screenTrack } = this.props;
this._setTracks(screenTrack);
}
componentWillUnmount()
{
clearInterval(this._screenResolutionTimer);
}
componentWillReceiveProps(nextProps)
{
const { screenTrack } = nextProps;
this._setTracks(screenTrack);
}
_setTracks(screenTrack)
{
if (this._screenTrack === screenTrack)
return;
this._screenTrack = screenTrack;
clearInterval(this._screenResolutionTimer);
this._hideScreenResolution();
const { video } = this.refs;
if (screenTrack)
{
const stream = new MediaStream;
if (screenTrack)
stream.addTrack(screenTrack);
video.srcObject = stream;
if (screenTrack)
this._showScreenResolution();
}
else
{
video.srcObject = null;
}
}
_showScreenResolution()
{
this._screenResolutionTimer = setInterval(() =>
{
const { screenWidth, screenHeight } = this.state;
const { video } = this.refs;
// Don't re-render if nothing changed.
if (video.videoWidth === screenWidth && video.videoHeight === screenHeight)
return;
this.setState(
{
screenWidth : video.videoWidth,
screenHeight : video.videoHeight
});
}, 1000);
}
_hideScreenResolution()
{
this.setState({ screenWidth: null, screenHeight: null });
}
}
PeerView.propTypes =
{
isMe : PropTypes.bool,
advancedMode : PropTypes.bool,
screenTrack : PropTypes.any,
screenVisible : PropTypes.bool,
screenProfile : PropTypes.string,
screenCodec : PropTypes.string
};

View File

@ -0,0 +1,119 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions';
import PropTypes from 'prop-types';
import Dropdown from 'react-dropdown';
class Settings extends React.Component
{
constructor(props)
{
super(props);
}
render()
{
const {
room,
me,
handleChangeWebcam,
handleChangeAudioDevice,
onToggleAdvancedMode
} = this.props;
let webcams;
let webcamText;
if (me.canChangeWebcam)
webcamText = 'Select camera';
else
webcamText = 'Unable to select camera';
if (me.webcamDevices)
webcams = Array.from(me.webcamDevices.values());
else
webcams = [];
let audioDevices;
let audioDevicesText;
if (me.canChangeAudioDevice)
audioDevicesText = 'Select audio input device';
else
audioDevicesText = 'Unable to select audio input device';
if (me.audioDevices)
audioDevices = Array.from(me.audioDevices.values());
else
audioDevices = [];
return (
<div data-component='Settings'>
<div className='settings'>
<Dropdown
disabled={!me.canChangeWebcam}
options={webcams}
onChange={handleChangeWebcam}
placeholder={webcamText}
/>
<Dropdown
disabled={!me.canChangeAudioDevice}
options={audioDevices}
onChange={handleChangeAudioDevice}
placeholder={audioDevicesText}
/>
<input
type='checkbox'
defaultChecked={room.advancedMode}
onChange={onToggleAdvancedMode}
/>
<span>Advanced mode</span>
</div>
</div>
);
}
}
Settings.propTypes =
{
me : appPropTypes.Me.isRequired,
room : appPropTypes.Room.isRequired,
handleChangeWebcam : PropTypes.func.isRequired,
handleChangeAudioDevice : PropTypes.func.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
room : state.room
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
handleChangeWebcam : (device) =>
{
dispatch(requestActions.changeWebcam(device.value));
},
handleChangeAudioDevice : (device) =>
{
dispatch(requestActions.changeAudioDevice(device.value));
},
onToggleAdvancedMode : () =>
{
dispatch(stateActions.toggleAdvancedMode());
}
};
};
const SettingsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Settings);
export default SettingsContainer;

View File

@ -0,0 +1,108 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as stateActions from '../../redux/stateActions';
import ParticipantList from '../ParticipantList/ParticipantList';
import Chat from '../Chat/Chat';
import Settings from '../Settings';
class ToolArea extends React.Component
{
constructor(props)
{
super(props);
}
render()
{
const {
toolarea,
setToolTab
} = this.props;
return (
<div data-component='ToolArea'>
<div className='tabs'>
<input
type='radio'
name='tabs'
id='tab-chat'
onChange={() =>
{
setToolTab('chat');
}}
checked={toolarea.currentToolTab === 'chat'}
/>
<label htmlFor='tab-chat'>Chat</label>
<div className='tab'>
<Chat />
</div>
<input
type='radio'
name='tabs'
id='tab-users'
onChange={() =>
{
setToolTab('users');
}}
checked={toolarea.currentToolTab === 'users'}
/>
<label htmlFor='tab-users'>Users</label>
<div className='tab'>
<ParticipantList />
</div>
<input
type='radio'
name='tabs'
id='tab-settings'
onChange={() =>
{
setToolTab('settings');
}}
checked={toolarea.currentToolTab === 'settings'}
/>
<label htmlFor='tab-settings'>Settings</label>
<div className='tab'>
<Settings />
</div>
</div>
</div>
);
}
}
ToolArea.propTypes =
{
advancedMode : PropTypes.bool,
toolarea : PropTypes.object.isRequired,
setToolTab : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
toolarea : state.toolarea
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
setToolTab : (toolTab) =>
{
dispatch(stateActions.setToolTab(toolTab));
}
};
};
const ToolAreaContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ToolArea);
export default ToolAreaContainer;

View File

@ -0,0 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as stateActions from '../../redux/stateActions';
class ToolAreaButton extends React.Component
{
render()
{
const {
toolAreaOpen,
toggleToolArea
} = this.props;
return (
<div data-component='ToolAreaButton'>
<div
className={classnames('button', 'toolarea-button', {
on : toolAreaOpen
})}
data-tip='Toggle tool area'
data-type='dark'
data-for='globaltip'
onClick={() => toggleToolArea()}
/>
</div>
);
}
}
ToolAreaButton.propTypes =
{
toolAreaOpen : PropTypes.bool.isRequired,
toggleToolArea : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
toolAreaOpen : state.toolarea.toolAreaOpen
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
}
};
};
const ToolAreaButtonContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ToolAreaButton);
export default ToolAreaButtonContainer;

View File

@ -5,7 +5,8 @@ import { render } from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { import {
applyMiddleware as applyReduxMiddleware, applyMiddleware as applyReduxMiddleware,
createStore as createReduxStore createStore as createReduxStore,
compose as composeRedux
} from 'redux'; } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { createLogger as createReduxLogger } from 'redux-logger'; import { createLogger as createReduxLogger } from 'redux-logger';
@ -19,6 +20,7 @@ import * as stateActions from './redux/stateActions';
import reducers from './redux/reducers'; import reducers from './redux/reducers';
import roomClientMiddleware from './redux/roomClientMiddleware'; import roomClientMiddleware from './redux/roomClientMiddleware';
import Room from './components/Room'; import Room from './components/Room';
import { loginEnabled } from '../config';
const logger = new Logger(); const logger = new Logger();
const reduxMiddlewares = const reduxMiddlewares =
@ -40,10 +42,22 @@ if (process.env.NODE_ENV === 'development')
reduxMiddlewares.push(reduxLogger); reduxMiddlewares.push(reduxLogger);
} }
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extensions options like name, actionsBlacklist, actionsCreators, serialize...
}) : composeRedux;
const enhancer = composeEnhancers(
applyReduxMiddleware(...reduxMiddlewares)
// other store enhancers if any
);
const store = createReduxStore( const store = createReduxStore(
reducers, reducers,
undefined, undefined,
applyReduxMiddleware(...reduxMiddlewares) enhancer
); );
domready(() => domready(() =>
@ -61,11 +75,12 @@ function run()
const peerName = randomString({ length: 8 }).toLowerCase(); const peerName = randomString({ length: 8 }).toLowerCase();
const urlParser = new UrlParse(window.location.href, true); const urlParser = new UrlParse(window.location.href, true);
let roomId = urlParser.query.roomId; let roomId = (urlParser.pathname).substr(1)
? (urlParser.pathname).substr(1) : urlParser.query.roomId;
const produce = urlParser.query.produce !== 'false'; const produce = urlParser.query.produce !== 'false';
let displayName = urlParser.query.displayName; let displayName = urlParser.query.displayName;
const isSipEndpoint = urlParser.query.sipEndpoint === 'true'; const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
const useSimulcast = urlParser.query.simulcast !== 'false'; const useSimulcast = urlParser.query.simulcast === 'true';
if (!roomId) if (!roomId)
{ {
@ -128,7 +143,7 @@ function run()
// NOTE: I don't like this. // NOTE: I don't like this.
store.dispatch( store.dispatch(
stateActions.setMe({ peerName, displayName, displayNameSet, device })); stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled }));
// NOTE: I don't like this. // NOTE: I don't like this.
store.dispatch( store.dispatch(

View File

@ -51,10 +51,11 @@
{ {
'alice' : 'alice' :
{ {
name : 'alice', name : 'alice',
displayName : 'Alice Thomsom', displayName : 'Alice Thomsom',
device : { flag: 'chrome', name: 'Chrome', version: '58' }, raiseHandState : false,
consumers : [ 5551, 5552 ] device : { flag: 'chrome', name: 'Chrome', version: '58' },
consumers : [ 5551, 5552 ]
} }
}, },
consumers : consumers :

View File

@ -12,8 +12,9 @@ const chatbehavior = (state = initialState, action) =>
case 'TOGGLE_CHAT': case 'TOGGLE_CHAT':
{ {
const showChat = !state.showChat; const showChat = !state.showChat;
const badge = 0;
return { ...state, showChat }; return { ...state, showChat, badge };
} }
case 'TOGGLE_INPUT_DISABLED': case 'TOGGLE_INPUT_DISABLED':
@ -23,6 +24,10 @@ const chatbehavior = (state = initialState, action) =>
return { ...state, disabledInput }; return { ...state, disabledInput };
} }
case 'INCREASE_BADGE':
{
return { ...state, badge: state.badge + (state.showChat ? 0 : 1) };
}
default: default:
return state; return state;
} }

View File

@ -7,6 +7,7 @@ import consumers from './consumers';
import notifications from './notifications'; import notifications from './notifications';
import chatmessages from './chatmessages'; import chatmessages from './chatmessages';
import chatbehavior from './chatbehavior'; import chatbehavior from './chatbehavior';
import toolarea from './toolarea';
const reducers = combineReducers( const reducers = combineReducers(
{ {
@ -17,7 +18,8 @@ const reducers = combineReducers(
consumers, consumers,
notifications, notifications,
chatmessages, chatmessages,
chatbehavior chatbehavior,
toolarea
}); });
export default reducers; export default reducers;

View File

@ -8,11 +8,19 @@ const initialState =
canSendWebcam : false, canSendWebcam : false,
canShareScreen : false, canShareScreen : false,
needExtension : false, needExtension : false,
canChangeAudioDevice : false,
audioDevices : null,
canChangeWebcam : false, canChangeWebcam : false,
webcamDevices : null,
webcamInProgress : false, webcamInProgress : false,
audioInProgress : false,
screenShareInProgress : false, screenShareInProgress : false,
loginInProgress : false,
loginEnabled : false,
audioOnly : false, audioOnly : false,
audioOnlyInProgress : false, audioOnlyInProgress : false,
raiseHand : false,
raiseHandInProgress : false,
restartIceInProgress : false restartIceInProgress : false
}; };
@ -22,9 +30,22 @@ const me = (state = initialState, action) =>
{ {
case 'SET_ME': case 'SET_ME':
{ {
const { peerName, displayName, displayNameSet, device } = action.payload; const {
peerName,
displayName,
displayNameSet,
device,
loginEnabled
} = action.payload;
return { ...state, name: peerName, displayName, displayNameSet, device }; return {
...state,
name : peerName,
displayName,
displayNameSet,
device,
loginEnabled
};
} }
case 'SET_MEDIA_CAPABILITIES': case 'SET_MEDIA_CAPABILITIES':
@ -41,6 +62,20 @@ const me = (state = initialState, action) =>
return { ...state, canShareScreen, needExtension }; return { ...state, canShareScreen, needExtension };
} }
case 'SET_CAN_CHANGE_AUDIO_DEVICE':
{
const canChangeAudioDevice = action.payload;
return { ...state, canChangeAudioDevice };
}
case 'SET_AUDIO_DEVICES':
{
const { devices } = action.payload;
return { ...state, audioDevices: devices };
}
case 'SET_CAN_CHANGE_WEBCAM': case 'SET_CAN_CHANGE_WEBCAM':
{ {
const canChangeWebcam = action.payload; const canChangeWebcam = action.payload;
@ -48,6 +83,20 @@ const me = (state = initialState, action) =>
return { ...state, canChangeWebcam }; return { ...state, canChangeWebcam };
} }
case 'SET_WEBCAM_DEVICES':
{
const { devices } = action.payload;
return { ...state, webcamDevices: devices };
}
case 'SET_AUDIO_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, audioInProgress: flag };
}
case 'SET_WEBCAM_IN_PROGRESS': case 'SET_WEBCAM_IN_PROGRESS':
{ {
const { flag } = action.payload; const { flag } = action.payload;
@ -62,6 +111,13 @@ const me = (state = initialState, action) =>
return { ...state, screenShareInProgress: flag }; return { ...state, screenShareInProgress: flag };
} }
case 'SET_LOGIN_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, loginInProgress: flag };
}
case 'SET_DISPLAY_NAME': case 'SET_DISPLAY_NAME':
{ {
let { displayName } = action.payload; let { displayName } = action.payload;
@ -87,6 +143,20 @@ const me = (state = initialState, action) =>
return { ...state, audioOnlyInProgress: flag }; return { ...state, audioOnlyInProgress: flag };
} }
case 'SET_MY_RAISE_HAND_STATE':
{
const { flag } = action.payload;
return { ...state, raiseHand: flag };
}
case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, raiseHandInProgress: flag };
}
case 'SET_RESTART_ICE_IN_PROGRESS': case 'SET_RESTART_ICE_IN_PROGRESS':
{ {
const { flag } = action.payload; const { flag } = action.payload;

View File

@ -34,6 +34,58 @@ const peers = (state = initialState, action) =>
return { ...state, [newPeer.name]: newPeer }; return { ...state, [newPeer.name]: newPeer };
} }
case 'SET_PEER_VIDEO_IN_PROGRESS':
{
const { peerName, flag } = action.payload;
const peer = state[peerName];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, peerVideoInProgress: flag };
return { ...state, [newPeer.name]: newPeer };
}
case 'SET_PEER_AUDIO_IN_PROGRESS':
{
const { peerName, flag } = action.payload;
const peer = state[peerName];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, peerAudioInProgress: flag };
return { ...state, [newPeer.name]: newPeer };
}
case 'SET_PEER_SCREEN_IN_PROGRESS':
{
const { peerName, flag } = action.payload;
const peer = state[peerName];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, peerScreenInProgress: flag };
return { ...state, [newPeer.name]: newPeer };
}
case 'SET_PEER_RAISE_HAND_STATE':
{
const { peerName, raiseHandState } = action.payload;
const peer = state[peerName];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, raiseHandState };
return { ...state, [newPeer.name]: newPeer };
}
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
{ {
const { consumer, peerName } = action.payload; const { consumer, peerName } = action.payload;

View File

@ -1,10 +1,11 @@
const initialState = const initialState =
{ {
url : null, url : null,
state : 'new', // new/connecting/connected/disconnected/closed, state : 'new', // new/connecting/connected/disconnected/closed,
activeSpeakerName : null, activeSpeakerName : null,
peerHeight : 300, showSettings : false,
peerWidth : 400 advancedMode : false,
fullScreenConsumer : null // ConsumerID
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -35,11 +36,26 @@ const room = (state = initialState, action) =>
return { ...state, activeSpeakerName: peerName }; return { ...state, activeSpeakerName: peerName };
} }
case 'SET_COMPONENT_SIZE': case 'TOGGLE_SETTINGS':
{ {
const { peerWidth, peerHeight } = action.payload; const showSettings = !state.showSettings;
return { ...state, peerWidth: peerWidth, peerHeight: peerHeight }; return { ...state, showSettings };
}
case 'TOGGLE_ADVANCED_MODE':
{
const advancedMode = !state.advancedMode;
return { ...state, advancedMode };
}
case 'TOGGLE_FULLSCREEN_CONSUMER':
{
const { consumerId } = action.payload;
const currentConsumer = state.fullScreenConsumer;
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
} }
default: default:

View File

@ -0,0 +1,30 @@
const initialState =
{
toolAreaOpen : false,
currentToolTab : 'chat' // chat, settings, users
};
const toolarea = (state = initialState, action) =>
{
switch (action.type)
{
case 'TOGGLE_TOOL_AREA':
{
const toolAreaOpen = !state.toolAreaOpen;
return { ...state, toolAreaOpen };
}
case 'SET_TOOL_TAB':
{
const { toolTab } = action.payload;
return { ...state, currentToolTab: toolTab };
}
default:
return state;
}
};
export default toolarea;

View File

@ -57,10 +57,19 @@ export const disableWebcam = () =>
}; };
}; };
export const changeWebcam = () => export const changeWebcam = (deviceId) =>
{ {
return { return {
type : 'CHANGE_WEBCAM' type : 'CHANGE_WEBCAM',
payload : { deviceId }
};
};
export const changeAudioDevice = (deviceId) =>
{
return {
type : 'CHANGE_AUDIO_DEVICE',
payload : { deviceId }
}; };
}; };
@ -78,6 +87,75 @@ export const disableAudioOnly = () =>
}; };
}; };
export const mutePeerAudio = (peerName) =>
{
return {
type : 'MUTE_PEER_AUDIO',
payload : { peerName }
};
};
export const unmutePeerAudio = (peerName) =>
{
return {
type : 'UNMUTE_PEER_AUDIO',
payload : { peerName }
};
};
export const pausePeerVideo = (peerName) =>
{
return {
type : 'PAUSE_PEER_VIDEO',
payload : { peerName }
};
};
export const resumePeerVideo = (peerName) =>
{
return {
type : 'RESUME_PEER_VIDEO',
payload : { peerName }
};
};
export const pausePeerScreen = (peerName) =>
{
return {
type : 'PAUSE_PEER_SCREEN',
payload : { peerName }
};
};
export const resumePeerScreen = (peerName) =>
{
return {
type : 'RESUME_PEER_SCREEN',
payload : { peerName }
};
};
export const userLogin = () =>
{
return {
type : 'USER_LOGIN'
};
};
export const raiseHand = () =>
{
return {
type : 'RAISE_HAND'
};
};
export const lowerHand = () =>
{
return {
type : 'LOWER_HAND'
};
};
export const restartIce = () => export const restartIce = () =>
{ {
return { return {

View File

@ -83,7 +83,18 @@ export default ({ dispatch, getState }) => (next) =>
case 'CHANGE_WEBCAM': case 'CHANGE_WEBCAM':
{ {
client.changeWebcam(); const { deviceId } = action.payload;
client.changeWebcam(deviceId);
break;
}
case 'CHANGE_AUDIO_DEVICE':
{
const { deviceId } = action.payload;
client.changeAudioDevice(deviceId);
break; break;
} }
@ -102,6 +113,81 @@ export default ({ dispatch, getState }) => (next) =>
break; break;
} }
case 'MUTE_PEER_AUDIO':
{
const { peerName } = action.payload;
client.mutePeerAudio(peerName);
break;
}
case 'UNMUTE_PEER_AUDIO':
{
const { peerName } = action.payload;
client.unmutePeerAudio(peerName);
break;
}
case 'PAUSE_PEER_VIDEO':
{
const { peerName } = action.payload;
client.pausePeerVideo(peerName);
break;
}
case 'RESUME_PEER_VIDEO':
{
const { peerName } = action.payload;
client.resumePeerVideo(peerName);
break;
}
case 'PAUSE_PEER_SCREEN':
{
const { peerName } = action.payload;
client.pausePeerScreen(peerName);
break;
}
case 'RESUME_PEER_SCREEN':
{
const { peerName } = action.payload;
client.resumePeerScreen(peerName);
break;
}
case 'RAISE_HAND':
{
client.sendRaiseHandState(true);
break;
}
case 'USER_LOGIN':
{
client.login();
break;
}
case 'LOWER_HAND':
{
client.sendRaiseHandState(false);
break;
}
case 'RESTART_ICE': case 'RESTART_ICE':
{ {
client.restartIce(); client.restartIce();

View File

@ -22,19 +22,11 @@ export const setRoomActiveSpeaker = (peerName) =>
}; };
}; };
export const onComponentResize = (peerWidth, peerHeight) => export const setMe = ({ peerName, displayName, displayNameSet, device, loginEnabled }) =>
{
return {
type : 'SET_COMPONENT_SIZE',
payload : { peerWidth, peerHeight }
};
};
export const setMe = ({ peerName, displayName, displayNameSet, device }) =>
{ {
return { return {
type : 'SET_ME', type : 'SET_ME',
payload : { peerName, displayName, displayNameSet, device } payload : { peerName, displayName, displayNameSet, device, loginEnabled }
}; };
}; };
@ -54,6 +46,22 @@ export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
}; };
}; };
export const setCanChangeAudioDevice = (flag) =>
{
return {
type : 'SET_CAN_CHANGE_AUDIO_DEVICE',
payload : flag
};
};
export const setAudioDevices = (devices) =>
{
return {
type : 'SET_AUDIO_DEVICES',
payload : { devices }
};
};
export const setCanChangeWebcam = (flag) => export const setCanChangeWebcam = (flag) =>
{ {
return { return {
@ -62,6 +70,14 @@ export const setCanChangeWebcam = (flag) =>
}; };
}; };
export const setWebcamDevices = (devices) =>
{
return {
type : 'SET_WEBCAM_DEVICES',
payload : { devices }
};
};
export const setDisplayName = (displayName) => export const setDisplayName = (displayName) =>
{ {
return { return {
@ -70,6 +86,13 @@ export const setDisplayName = (displayName) =>
}; };
}; };
export const toggleAdvancedMode = () =>
{
return {
type : 'TOGGLE_ADVANCED_MODE'
};
};
export const setAudioOnlyState = (enabled) => export const setAudioOnlyState = (enabled) =>
{ {
return { return {
@ -86,6 +109,84 @@ export const setAudioOnlyInProgress = (flag) =>
}; };
}; };
export const setPeerVideoInProgress = (peerName, flag) =>
{
return {
type : 'SET_PEER_VIDEO_IN_PROGRESS',
payload : { peerName, flag }
};
};
export const setPeerAudioInProgress = (peerName, flag) =>
{
return {
type : 'SET_PEER_AUDIO_IN_PROGRESS',
payload : { peerName, flag }
};
};
export const setPeerScreenInProgress = (peerName, flag) =>
{
return {
type : 'SET_PEER_SCREEN_IN_PROGRESS',
payload : { peerName, flag }
};
};
export const setMyRaiseHandState = (flag) =>
{
return {
type : 'SET_MY_RAISE_HAND_STATE',
payload : { flag }
};
};
export const setLoginInProgress = (flag) =>
{
return {
type : 'SET_LOGIN_IN_PROGRESS',
payload : { flag }
};
};
export const toggleSettings = () =>
{
return {
type : 'TOGGLE_SETTINGS'
};
};
export const toggleToolArea = () =>
{
return {
type : 'TOGGLE_TOOL_AREA'
};
};
export const setToolTab = (toolTab) =>
{
return {
type : 'SET_TOOL_TAB',
payload : { toolTab }
};
};
export const setMyRaiseHandStateInProgress = (flag) =>
{
return {
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS',
payload : { flag }
};
};
export const setPeerRaiseHandState = (peerName, raiseHandState) =>
{
return {
type : 'SET_PEER_RAISE_HAND_STATE',
payload : { peerName, raiseHandState }
};
};
export const setRestartIceInProgress = (flag) => export const setRestartIceInProgress = (flag) =>
{ {
return { return {
@ -134,6 +235,14 @@ export const setProducerTrack = (producerId, track) =>
}; };
}; };
export const setAudioInProgress = (flag) =>
{
return {
type : 'SET_AUDIO_IN_PROGRESS',
payload : { flag }
};
};
export const setWebcamInProgress = (flag) => export const setWebcamInProgress = (flag) =>
{ {
return { return {
@ -252,6 +361,21 @@ export const toggleChat = () =>
}; };
}; };
export const toggleConsumerFullscreen = (consumerId) =>
{
return {
type : 'TOGGLE_FULLSCREEN_CONSUMER',
payload : { consumerId }
};
};
export const increaseBadge = () =>
{
return {
type : 'INCREASE_BADGE'
};
};
export const toggleInputDisabled = () => export const toggleInputDisabled = () =>
{ {
return { return {

View File

@ -35,5 +35,11 @@ export function getBrowserType()
return 'chrome'; return 'chrome';
} }
// MSEdge
if (ua.indexOf('edge') !== -1)
{
return 'edge';
}
return 'N/A'; return 'N/A';
} }

4371
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,13 +14,15 @@
"hark": "^1.1.6", "hark": "^1.1.6",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"marked": "^0.3.17", "marked": "^0.3.17",
"mediasoup-client": "^2.0.14", "mediasoup-client": "^2.1.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"protoo-client": "^2.0.7", "protoo-client": "^2.0.7",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^16.2.0", "react": "^16.2.0",
"react-clipboard.js": "^1.1.3", "react-clipboard.js": "^1.1.3",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"react-draggable": "^3.0.5",
"react-dropdown": "^1.5.0",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-spinner": "^0.2.7", "react-spinner": "^0.2.7",
"react-tooltip": "^3.4.0", "react-tooltip": "^3.4.0",
@ -38,6 +40,7 @@
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babelify": "^8.0.0", "babelify": "^8.0.0",
"browser-sync": "^2.23.6", "browser-sync": "^2.23.6",
"browserify": "^16.1.0", "browserify": "^16.1.0",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 313 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;"
xml:space="preserve">
<metadata
id="metadata11"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
<defs
id="defs9" />
<path
style="fill:#000000;stroke-width:0.40677965"
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
id="path3710"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;"
xml:space="preserve">
<metadata
id="metadata11"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
<defs
id="defs9" />
<path
style="fill:#ffffff;stroke-width:0.40677965"
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
id="path3710"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,4 +1,4 @@
<svg fill="#FFFFFF" fill-opacity="0.65" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg"> <svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/> <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 352 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@ -1,4 +1,4 @@
<svg fill="#FFFFFF" fill-opacity="0.85" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg"> <svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/> <path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 534 B

View File

@ -1,4 +1,4 @@
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg"> <svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/> <path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 534 B

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;"
xml:space="preserve">
<metadata
id="metadata11"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
<defs
id="defs9" />
<path
style="fill:#ffffff;stroke-width:0.40677965"
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
id="path3710"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,4 +1,4 @@
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg"> <svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/> <path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 335 B

View File

@ -1,4 +1,4 @@
<svg fill="#FFFFFF" fill-opacity="0.65" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none"/>
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/> <path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1,4 @@
<svg fill="#000000" height="48" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill="none" d="M0 0h20v20H0V0z"/>
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 790 B

View File

@ -0,0 +1,3 @@
<svg fill="#FFFFFF" height="24" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 0 24 24" width="48">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@ -1,4 +1,4 @@
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg"> <svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none"/>
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/> <path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 266 B

View File

@ -1,13 +1,13 @@
[data-component='ChatWidget'] { [data-component='ChatWidget'] {
position: absolute;
bottom: 0; bottom: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 10px 10px 0; margin: 0 10px 10px 0;
max-width: 300px; max-width: 300px;
position: fixed;
right: 0; right: 0;
width: 90vw; width: 90vw;
z-index: 9999; z-index: 100;
> .launcher { > .launcher {
align-self: flex-end; align-self: flex-end;
@ -23,6 +23,7 @@
border-radius: 100%; border-radius: 100%;
height: 45px; height: 45px;
width: 45px; width: 45px;
position: relative;
&.focus { &.focus {
outline: none; outline: none;
@ -36,6 +37,17 @@
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
} }
> .badge{
border-radius: 50%;
padding: 0.7vmin;
top: -1vmin;
font-size: 1.5vmin;
left: -1vmin;
background: rgba(255,0,0,0.9);
color: #fff;
font-weight: bold;
position: absolute;
}
} }
} }
@ -44,10 +56,13 @@
box-shadow: 0px 2px 10px 1px #000; box-shadow: 0px 2px 10px 1px #000;
} }
[data-component='Chat'] {
height: 100%;
}
[data-component='MessageList'] { [data-component='MessageList'] {
background-color: rgba(#fff, 0.9); background-color: rgba(#fff, 0.9);
height: 50vh; height: 91vmin;
max-height: 350px;
overflow-y: scroll; overflow-y: scroll;
padding-top: 5px; padding-top: 5px;
border-radius: 5px 5px 0px 0px; border-radius: 5px 5px 0px 0px;
@ -102,8 +117,8 @@
align-items: center; align-items: center;
display: flex; display: flex;
background-color: rgba(#fff, 0.9); background-color: rgba(#fff, 0.9);
height: 35px; height: 6vmin;
padding: 5px; padding: 0.5vmin;
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
> .new-message { > .new-message {
@ -121,4 +136,3 @@
} }
} }
} }

View File

@ -0,0 +1,75 @@
[data-component='FullScreenView'] {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 200;
> .controls {
position: absolute;
z-index: 201;
right: 0;
top: 0;
display: flex;
flex-direction:; row;
justify-content: flex-start;
align-items: center;
padding: 0.4vmin;
> .button {
flex: 0 0 auto;
margin: 0.2vmin;
border-radius: 2px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
+desktop() {
width: 5vmin;
height: 5vmin;
opacity: 0.85;
&:hover {
opacity: 1;
}
}
+mobile() {
width: 5vmin;
height: 5vmin;
}
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_exit_black.svg');
background-color: rgba(#fff, 0.7);
}
}
}
.incompatible-video {
position: absolute;
z-index: 2
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: 15px;
color: rgba(#fff, 0.55);
}
}
}

View File

@ -0,0 +1,105 @@
[data-component='FullView'] {
position: relative;
flex: 100 100 auto;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: rgba(#2a4b58, 0.9);
background-image: url('/resources/images/buddy.svg');
background-position: bottom;
background-size: auto 85%;
background-repeat: no-repeat;
> .info {
$backgroundTint = #000;
position: absolute;
z-index: 5
top: 0.6vmin;
left: 0.6vmin;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
> .media {
flex: 0 0 auto;
display: flex;
flex-direction: row;
> .box {
padding: 0.4vmin;
border-radius: 2px;
background-color: rgba(#000, 0.25);
> p {
user-select: none;
pointer-events: none;
margin-bottom: 2px;
color: rgba(#fff, 0.7);
font-size: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
> video {
flex: 100 100 auto;
height: 100%;
width: 100%;
object-fit: contain;
user-select: none;
transition-property: opacity;
transition-duration: .15s;
background-color: rgba(#000, 0.75);
&.hidden {
opacity: 0;
transition-duration: 0s;
}
&.loading {
filter: blur(5px);
}
}
> .spinner-container {
position: absolute;
top: 0
bottom: 0;
left: 0;
right: 0;
background-color: rgba(#000, 0.75);
.react-spinner {
position: relative;
width: 48px;
height: 48px;
top: 50%;
left: 50%;
.react-spinner_bar {
position: absolute;
width: 20%;
height: 7.8%;
top: -3.9%;
left: -10%;
animation: PeerView-spinner 1.2s linear infinite;
border-radius: 5px;
background-color: rgba(#fff, 0.5);
}
}
}
}
@keyframes FullView-spinner {
0% { opacity: 1; }
100% { opacity: 0.15; }
}

View File

@ -1,96 +1,121 @@
[data-component='Me'] { [data-component='Me'] {
flex: 100 100 auto;
position: relative; position: relative;
height: 100%; flex-direction: row;
width: 100%; display: flex;
> .controls { > .view-container {
position: absolute; position: relative;
z-index: 10 width: 20vmin;
top: 0; height: 15vmin;
left: 0;
right: 0;
display: flex;
flex-direction:; row;
justify-content: flex-end;
align-items: center;
> .button { &.webcam {
flex: 0 0 auto; order: 2;
margin: 4px; }
margin-left: 0;
border-radius: 2px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
+desktop() { &.screen {
width: 28px; order: 1;
height: 28px; }
opacity: 0.85;
&:hover { > .controls {
opacity: 1; position: absolute;
} z-index: 10;
} right: 0;
top: 0;
display: flex;
flex-direction:; row;
justify-content: flex-start;
align-items: center;
padding: 0.4vmin;
+mobile() { > .button {
width: 26px; flex: 0 0 auto;
height: 26px; margin: 0.2vmin;
} border-radius: 2px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
&.unsupported { +desktop() {
pointer-events: none; width: 24px;
} height: 24px;
opacity: 0.85;
&.disabled { &:hover {
pointer-events: none; opacity: 1;
opacity: 0.5; }
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.mic {
&.on {
background-image: url('/resources/images/icon_mic_black_on.svg');
} }
&.off { +mobile() {
background-image: url('/resources/images/icon_mic_white_off.svg'); width: 22px;
background-color: rgba(#d42241, 0.7); height: 22px;
} }
&.unsupported { &.unsupported {
background-image: url('/resources/images/icon_mic_white_unsupported.svg'); pointer-events: none;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
} }
}
&.webcam {
&.on { &.on {
background-image: url('/resources/images/icon_webcam_black_on.svg'); background-color: rgba(#fff, 0.7);
} }
&.off { &.mic {
background-image: url('/resources/images/icon_webcam_white_on.svg'); &.on {
background-image: url('/resources/images/icon_mic_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
}
} }
&.unsupported { &.webcam {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg'); &.on {
} background-image: url('/resources/images/icon_webcam_black_on.svg');
} }
&.change-webcam { &.off {
&.on { background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-image: url('/resources/images/icon_change_webcam_black.svg'); background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
}
} }
&.unsupported { &.screen {
background-image: url('/resources/images/icon_change_webcam_white_unsupported.svg'); &.on {
background-image: url('/resources/images/share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
}
}
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
} }
} }
} }

View File

@ -1,9 +1,9 @@
[data-component='Notifications'] { [data-component='Notifications'] {
position: fixed; position: absolute;
z-index: 9999; z-index: 9999;
pointer-events: none; pointer-events: none;
top: 0; top: 0;
right: 0; right: 65px;
bottom: 0; bottom: 0;
padding: 20px; padding: 20px;
display: flex; display: flex;

View File

@ -0,0 +1,132 @@
[data-component='ParticipantList'] {
width: 100%;
> .list {
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \
0 4px 20px 0 rgba(0,0,0,0.19);
> .list-item {
padding: 0.5vmin;
border-bottom: 1px solid #ddd;
width: 100%;
overflow: hidden;
}
}
}
[data-component='ListPeer'] {
> .controls {
float: right;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
> .button {
flex: 0 0 auto;
margin: 0.2vmin;
border-radius: 2px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
+desktop() {
width: 24px;
height: 24px;
opacity: 0.85;
&:hover {
opacity: 1;
}
}
+mobile() {
width: 22px;
height: 22px;
}
&.unsupported {
pointer-events: none;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.mic {
&.on {
background-image: url('/resources/images/icon_mic_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
}
}
&.webcam {
&.on {
background-image: url('/resources/images/icon_webcam_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
}
}
&.screen {
&.on {
background-image: url('/resources/images/share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
}
}
}
}
> .avatar {
padding: 8px 16px;
float: left;
width: auto;
border: none;
display: block;
outline: 0;
border-radius: 50%;
vertical-align: middle;
}
> .peer-info {
font-size: 1.4vmin;
float: left;
width: auto;
border: none;
display: block;
outline: 0;
padding: 0.6vmin;
}
}

View File

@ -3,6 +3,15 @@
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
flex-direction: row;
display: flex;
&.screen {
border: 5px solid rgba(#fff, 0.4);
}
&:not(.screen) {
}
+mobile() { +mobile() {
display: flex; display: flex;
@ -11,39 +20,117 @@
align-items: center; align-items: center;
} }
> .indicators { > .view-container {
position: absolute; position: relative;
z-index: 10
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction:; row;
justify-content: flex-end;
align-items: center;
> .icon { &.webcam {
flex: 0 0 auto; order: 2;
margin: 4px; }
margin-left: 0;
width: 32px;
height: 32px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
transition-property: opacity;
transition-duration: 0.15s;
+desktop() { &.screen {
opacity: 0.85; order: 1;
} }
&.mic-off { > .controls {
background-image: url('/resources/images/icon_remote_mic_white_off.svg'); position: absolute;
} z-index: 10;
right: 0;
top: 0;
display: flex;
flex-direction:; row;
justify-content: flex-start;
align-items: center;
padding: 0.4vmin;
&.webcam-off { > .button {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); flex: 0 0 auto;
margin: 0.2vmin;
border-radius: 2px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
+desktop() {
width: 24px;
height: 24px;
opacity: 0.85;
&:hover {
opacity: 1;
}
}
+mobile() {
width: 22px;
height: 22px;
}
&.unsupported {
pointer-events: none;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.mic {
&.on {
background-image: url('/resources/images/icon_mic_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
}
}
&.webcam {
&.on {
background-image: url('/resources/images/icon_webcam_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
}
}
&.screen {
&.on {
background-image: url('/resources/images/share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
}
}
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }
} }

View File

@ -17,9 +17,9 @@
position: absolute; position: absolute;
z-index: 5 z-index: 5
top: 0; top: 0.6vmin;
left: 0.6vmin;
bottom: 0; bottom: 0;
left: 0;
right: 0; right: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -31,8 +31,7 @@
flex-direction: row; flex-direction: row;
> .box { > .box {
margin: 4px; padding: 0.4vmin;
padding: 2px 4px;
border-radius: 2px; border-radius: 2px;
background-color: rgba(#000, 0.25); background-color: rgba(#000, 0.25);
@ -55,27 +54,32 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
position: absolute;
bottom: 0.6vmin;
left: 0;
border-radius: 2px;
background-color: rgba(#000, 0.25);
+desktop() { +desktop() {
&.is-me { &.is-me {
padding: 10px; padding: 1vmin;
align-items: flex-start; align-items: flex-start;
} }
&:not(.is-me) { &:not(.is-me) {
padding: 20px; padding: 1vmin;
align-items: flex-start; align-items: flex-start;
} }
} }
+mobile() { +mobile() {
&.is-me { &.is-me {
padding: 10px; padding: 1vmin;
align-items: flex-start; align-items: flex-start;
} }
&:not(.is-me) { &:not(.is-me) {
padding: 10px; padding: 1vmin;
align-items: flex-end; align-items: flex-end;
} }
} }
@ -114,15 +118,15 @@
} }
> .row { > .row {
margin-top: 4px; margin-top: 0.4vmin;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-end; align-items: flex-end;
> .device-icon { > .device-icon {
height: 18px; height: 12px;
width: 18px; width: 12px;
margin-right: 3px; margin-right: 3px;
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
@ -190,39 +194,6 @@
} }
} }
> .minivideo {
height: 15%;
width: 15%;
bottom: 1%;
right: 1%;
position: absolute;
overflow: hidden;
> video {
flex: 100 100 auto;
height: 100%;
width: 100%;
object-fit: cover;
user-select: none;
transition-property: opacity;
transition-duration: .15s;
background-color: rgba(#000, 0.75);
&.is-me {
transform: scaleX(-1);
}
&.hidden {
opacity: 0;
transition-duration: 0s;
}
&.loading {
filter: blur(5px);
}
}
}
> .volume-container { > .volume-container {
position: absolute; position: absolute;
top: 0 top: 0

View File

@ -1,261 +1,430 @@
[data-component='Room'] { [data-component='Room'] {
position: relative;
height: 100%;
width: 100%;
AppearFadeIn(300ms); AppearFadeIn(300ms);
> .state { > .room-wrapper {
position: fixed;
z-index: 100;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-radius: 25px;
background-color: rgba(#fff, 0.2);
+desktop() {
top: 20px;
left: 20px;
width: 124px;
}
+mobile() {
top: 10px;
left: 10px;
width: 110px;
}
> .icon {
flex: 0 0 auto;
border-radius: 100%;
+desktop() {
margin: 5px;
margin-right: 0;
height: 20px;
width: 20px;
}
+mobile() {
margin: 4px;
margin-right: 0;
height: 16px;
width: 16px;
}
&.new, &.closed {
background-color: rgba(#aaa, 0.5);
}
&.connecting {
animation: Room-info-state-connecting .75s infinite linear;
}
&.connected {
background-color: rgba(#30bd18, 0.75);
+mobile() {
display: none;
}
}
}
> .text {
flex: 100 0 auto;
user-select: none;
pointer-events: none;
text-align: center;
text-transform: uppercase;
font-family: 'Roboto';
font-weight: 400;
color: rgba(#fff, 0.75);
+desktop() {
font-size: 12px;
}
+mobile() {
font-size: 10px;
}
&.connected {
+mobile() {
display: none;
}
}
}
}
> .room-link-wrapper {
pointer-events: none;
position: absolute; position: absolute;
z-index: 1;
top: 0; top: 0;
left: 0; left: 0;
right: 0; height: 100%;
display: flex; width: 100%;
flex-direction: row; transition: width 0.3s;
justify-content: center;
> .room-link { > .state {
width: auto; position: fixed;
background-color: rgba(#fff, 0.75); z-index: 100;
border-bottom-right-radius: 4px; display: flex;
border-bottom-left-radius: 4px; flex-direction: row;
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4); justify-content: center;
align-items: center;
border-radius: 25px;
background-color: rgba(#fff, 0.2);
> a.link { +desktop() {
display: block;; top: 20px;
user-select: none; left: 20px;
pointer-events: auto; width: 124px;
color: #104758; }
font-weight: 400;
cursor: pointer; +mobile() {
text-decoration: none; top: 10px;
transition-property: opacity; left: 10px;
transition-duration: 0.25s; width: 110px;
opacity: 0.8; }
> .icon {
flex: 0 0 auto;
border-radius: 100%;
+desktop() { +desktop() {
padding: 10px 20px; margin: 5px;
font-size: 16px; margin-right: 0;
height: 20px;
width: 20px;
} }
+mobile() { +mobile() {
padding: 6px 10px; margin: 4px;
font-size: 14px; margin-right: 0;
height: 16px;
width: 16px;
} }
&:hover { &.new, &.closed {
opacity: 1; background-color: rgba(#aaa, 0.5);
text-decoration: underline; }
&.connecting {
animation: Room-info-state-connecting .75s infinite linear;
}
&.connected {
background-color: rgba(#30bd18, 0.75);
+mobile() {
display: none;
}
}
}
> .text {
flex: 100 0 auto;
user-select: none;
pointer-events: none;
text-align: center;
text-transform: uppercase;
font-family: 'Roboto';
font-weight: 400;
color: rgba(#fff, 0.75);
+desktop() {
font-size: 12px;
}
+mobile() {
font-size: 10px;
}
&.connected {
+mobile() {
display: none;
}
} }
} }
} }
}
> .me-container { > .room-link-wrapper {
position: fixed; pointer-events: none;
z-index: 100; position: absolute;
overflow: hidden; z-index: 1;
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5); top: 0;
transition-property: border-color; left: 0;
transition-duration: 0.15s; right: 0;
display: flex;
flex-direction: row;
justify-content: center;
&.active-speaker { > .room-link {
border-color: #fff; width: auto;
background-color: rgba(#fff, 0.75);
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4);
> a.link {
display: block;;
user-select: none;
pointer-events: auto;
color: #104758;
font-weight: 400;
cursor: pointer;
text-decoration: none;
transition-property: opacity;
transition-duration: 0.25s;
opacity: 0.8;
+desktop() {
padding: 10px 20px;
font-size: 16px;
}
+mobile() {
padding: 6px 10px;
font-size: 14px;
}
&:hover {
opacity: 1;
text-decoration: underline;
}
}
}
} }
+desktop() { > .me-container {
height: 200px; position: fixed;
width: 235px; z-index: 100;
bottom: 20px; overflow: hidden;
left: 20px; box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
border: 1px solid rgba(#fff, 0.15); transition-property: border-color;
}
+mobile() {
height: 175px;
width: 150px;
bottom: 10px;
left: 10px;
border: 1px solid rgba(#fff, 0.25);
}
}
> .sidebar {
position: fixed;
z-index: 101;
top: calc(50% - 60px);
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+desktop() {
left: 20px;
width: 36px;
}
+mobile() {
left: 10px;
width: 32px;
}
> .button {
flex: 0 0 auto;
margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
border-radius: 100%;
&.active-speaker {
border-color: #fff;
}
+desktop() { +desktop() {
height: 36px; bottom: 20px;
left: 20px;
border: 1px solid rgba(#fff, 0.15);
}
+mobile() {
height: 175px;
width: 150px;
bottom: 10px;
left: 10px;
border: 1px solid rgba(#fff, 0.25);
}
}
> .sidebar {
position: fixed;
z-index: 101;
top: calc(50% - 60px);
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+desktop() {
left: 20px;
width: 36px; width: 36px;
} }
+mobile() { +mobile() {
height: 32px; left: 10px;
width: 32px; width: 32px;
} }
&.on { > .button {
background-color: rgba(#fff, 0.7); flex: 0 0 auto;
} margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
&.disabled { +desktop() {
pointer-events: none; height: 36px;
opacity: 0.5; width: 36px;
} }
&.audio-only { +mobile() {
background-image: url('/resources/images/icon_audio_only_white.svg'); height: 32px;
width: 32px;
}
&.on { &.on {
background-image: url('/resources/images/icon_audio_only_black.svg'); background-color: rgba(#fff, 0.7);
}
}
&.restart-ice {
background-image: url('/resources/images/icon_restart_ice_white.svg');
&.on {
background-image: url('/resources/images/icon_restart_ice__black.svg');
}
}
&.screen {
&.on {
background-image: url('/resources/images/no-share-screen-black.svg');
} }
&.off { &.disabled {
background-image: url('/resources/images/share-screen-white.svg'); pointer-events: none;
opacity: 0.5;
} }
&.unsupported { &.login {
background-image: url('/resources/images/no-share-screen-white.svg'); &.off {
background-color: rgba(#d42241, 0.7); background-image: url('/resources/images/icon_login_white.svg');
}
} }
&.need-extension { &.settings {
background-image: url('/resources/images/share-screen-extension.svg'); &.off {
} background-image: url('/resources/images/icon_settings_white.svg');
} }
&.leave-meeting { &.on {
background-image: url('/resources/images/leave-meeting.svg'); background-image: url('/resources/images/icon_settings_black.svg');
}
}
&.screen {
&.on {
background-image: url('/resources/images/no-share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/share-screen-white.svg');
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.need-extension {
background-image: url('/resources/images/share-screen-extension.svg');
}
}
&.raise-hand {
background-image: url('/resources/images/icon-hand-white.svg');
&.on {
background-image: url('/resources/images/icon-hand-black.svg');
}
}
&.leave-meeting {
background-image: url('/resources/images/leave-meeting.svg');
}
} }
} }
} }
> .toolarea-wrapper {
position: fixed;
top: 0;
right: 0;
width: 20%;
height: 100%;
background-color: #FFF;
transition: width 0.3s;
}
}
.Dropdown-root {
position: relative;
padding: 0.3vmin;
}
.Dropdown-control {
position: relative;
overflow: hidden;
background-color: white;
border: 1px solid #ccc;
border-radius: 2px;
box-sizing: border-box;
color: #333;
cursor: default;
outline: none;
padding: 8px 52px 8px 10px;
transition: all 200ms ease;
}
.Dropdown-control:hover {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}
.Dropdown-arrow {
border-color: #999 transparent transparent;
border-style: solid;
border-width: 5px 5px 0;
content: ' ';
display: block;
height: 0;
margin-top: -ceil(2.5);
position: absolute;
right: 10px;
top: 14px;
width: 0
}
.is-open .Dropdown-arrow {
border-color: transparent transparent #999;
border-width: 0 5px 5px;
}
.Dropdown-menu {
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
box-sizing: border-box;
margin-top: -1px;
max-height: 200px;
overflow-y: auto;
position: absolute;
top: 100%;
width: 100%;
z-index: 1000;
-webkit-overflow-scrolling: touch;
}
.Dropdown-menu .Dropdown-group > .Dropdown-title {
padding: 8px 10px;
color: rgba(51, 51, 51, 1.2);
font-weight: bold;
text-transform: capitalize;
}
.Dropdown-option {
box-sizing: border-box;
color: rgba(51, 51, 51, 0.8);
cursor: pointer;
display: block;
padding: 8px 10px;
}
.Dropdown-option:last-child {
border-bottom-right-radius: 2px;
border-bottom-left-radius: 2px;
}
.Dropdown-option:hover {
background-color: #f2f9fc;
color: #333;
}
.Dropdown-option.is-selected {
background-color: #f2f9fc;
color: #333;
}
.Dropdown-noresults {
box-sizing: border-box;
color: #ccc;
cursor: default;
display: block;
padding: 8px 10px;
}
.react-tabs__tab-list {
border-bottom: 1px solid #aaa;
margin: 0 0 10px;
padding: 0;
}
.react-tabs__tab {
display: inline-block;
border: 1px solid transparent;
border-bottom: none;
bottom: -1px;
position: relative;
list-style: none;
padding: 6px 12px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.react-tabs__tab--selected {
background: #fff;
border-color: #aaa;
color: black;
border-radius: 5px 5px 0 0;
}
.react-tabs__tab--disabled {
color: GrayText;
cursor: default;
}
.react-tabs__tab:focus {
box-shadow: 0 0 5px hsl(208, 99%, 50%);
border-color: hsl(208, 99%, 50%);
outline: none;
}
.react-tabs__tab:focus:after {
content: "";
position: absolute;
height: 5px;
left: -4px;
right: -4px;
bottom: -5px;
background: #fff;
}
.react-tabs__tab-panel {
display: none;
}
.react-tabs__tab-panel--selected {
display: block;
} }
@keyframes Room-info-state-connecting { @keyframes Room-info-state-connecting {

View File

@ -0,0 +1,109 @@
[data-component='ScreenView'] {
position: relative;
flex: 100 100 auto;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: rgba(#2a4b58, 0.9);
background-image: url('/resources/images/buddy.svg');
background-position: bottom;
background-size: auto 85%;
background-repeat: no-repeat;
> .info {
$backgroundTint = #000;
position: absolute;
z-index: 5
top: 0.6vmin;
left: 0.6vmin;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
> .media {
flex: 0 0 auto;
display: flex;
flex-direction: row;
> .box {
padding: 0.4vmin;
border-radius: 2px;
background-color: rgba(#000, 0.25);
> p {
user-select: none;
pointer-events: none;
margin-bottom: 2px;
color: rgba(#fff, 0.7);
font-size: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
> video {
flex: 100 100 auto;
height: 100%;
width: 100%;
object-fit: contain;
user-select: none;
transition-property: opacity;
transition-duration: .15s;
background-color: rgba(#000, 0.75);
&.is-me {
transform: scaleX(-1);
}
&.hidden {
opacity: 0;
transition-duration: 0s;
}
&.loading {
filter: blur(5px);
}
}
> .spinner-container {
position: absolute;
top: 0
bottom: 0;
left: 0;
right: 0;
background-color: rgba(#000, 0.75);
.react-spinner {
position: relative;
width: 48px;
height: 48px;
top: 50%;
left: 50%;
.react-spinner_bar {
position: absolute;
width: 20%;
height: 7.8%;
top: -3.9%;
left: -10%;
animation: PeerView-spinner 1.2s linear infinite;
border-radius: 5px;
background-color: rgba(#fff, 0.5);
}
}
}
}
@keyframes ScreenView-spinner {
0% { opacity: 1; }
100% { opacity: 0.15; }
}

View File

@ -0,0 +1,3 @@
[data-component='Settings'] {
}

View File

@ -0,0 +1,99 @@
[data-component='ToolAreaButton'] {
position: absolute;
z-index: 101;
top: 20px;
right: 20px;
height: 36px;
width: 36px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .button {
flex: 0 0 auto;
margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
+desktop() {
height: 36px;
width: 36px;
}
+mobile() {
height: 32px;
width: 32px;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.toolarea-button {
background-image: url('/resources/images/icon_tool_area_white.svg');
&.on {
background-image: url('/resources/images/icon_tool_area_black.svg');
}
}
}
}
[data-component='ToolArea'] {
width: 100%;
height: 100%;
> .tabs {
display: flex;
flex-wrap: wrap;
height: 100%;
> label {
order: 1;
display: block;
padding: 1vmin 0 1vmin 0;
cursor: pointer;
background: rgba(#000, 0.3);
font-weight: bold;
transition: background ease 0.2s;
text-align: center;
width: 33.33%;
font-size: 1.3vmin;
height: 3vmin;
}
> .tab {
order: 99;
flex-grow: 1;
width: 100%;
height: 100%;
display: none;
padding: 1vmin;
background: #fff;
}
> input[type="radio"] {
display: none;
}
> input[type="radio"]:checked + label {
background: #fff;
}
> input[type="radio"]:checked + label + .tab {
display: block;
}
}
}

View File

@ -40,15 +40,21 @@ body {
@import './components/Peers'; @import './components/Peers';
@import './components/Peer'; @import './components/Peer';
@import './components/PeerView'; @import './components/PeerView';
@import './components/ScreenView';
@import './components/Notifications'; @import './components/Notifications';
@import './components/Chat'; @import './components/Chat';
@import './components/Settings';
@import './components/ToolArea';
@import './components/ParticipantList';
@import './components/FullScreenView';
@import './components/FullView';
} }
// 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 {
position: relative; position: absolute;
z-index: -1000; z-index: -1000;
bottom: 1px; bottom: 0;
left: 0; left: 0;
height: 1px; height: 1px;
width: 1px; width: 1px;

View File

@ -0,0 +1,15 @@
[Unit]
Description=multiparty-meeting is a audio / video meeting service running in the browser and powered by webRTC
After=network.target
[Service]
ExecStart=/usr/local/src/multiparty-meeting/server.js
Restart=always
User=nobody
Group=nogroup
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/usr/local/src/multiparty-meeting
[Install]
WantedBy=multi-user.target

View File

@ -1,5 +1,18 @@
module.exports = module.exports =
{ {
// oAuth2 conf
oauth2 :
{
client_id : '',
client_secret : '',
providerID : '',
redirect_uri : 'https://mYDomainName:port/auth-callback',
authorization_endpoint : '',
userinfo_endpoint : '',
token_endpoint : '',
scopes : { request : [ 'openid', 'userid','profile'] },
response_type : 'code'
},
// Listening hostname for `gulp live|open`. // Listening hostname for `gulp live|open`.
domain : 'localhost', domain : 'localhost',
tls : tls :
@ -7,6 +20,8 @@ module.exports =
cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`, cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`,
key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem` key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem`
}, },
// Listening port for https server.
listeningPort : 3443,
mediasoup : mediasoup :
{ {
// mediasoup Server settings. // mediasoup Server settings.

View File

@ -0,0 +1,31 @@
'use strict';
const headers = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-headers": "content-type, accept",
"access-control-max-age": 10,
"Content-Type": "application/json"
};
exports.prepareResponse = function(req, cb) {
var data = "";
req.on('data', function(chunk) { data += chunk; });
req.on('end', function() { cb(data); });
};
exports.respond = function(res, data, status) {
status = status || 200;
res.writeHead(status, headers);
res.end(data);
};
exports.send404 = function(res) {
exports.respond(res, 'Not Found', 404);
};
exports.redirector = function(res, loc, status) {
status = status || 302;
res.writeHead(status, { Location: loc });
res.end();
};

View File

@ -254,12 +254,32 @@ class Room extends EventEmitter
protooPeer.send( protooPeer.send(
'chat-history-receive', 'chat-history-receive',
{ chatHistory : this._chatHistory } { chatHistory: this._chatHistory }
); );
break; break;
} }
case 'raisehand-message':
{
accept();
const { raiseHandState } = request.data;
const { mediaPeer } = protooPeer.data;
mediaPeer.appData.raiseHand = request.data.raiseHandState;
// Spread to others via protoo.
this._protooRoom.spread(
'raisehand-message',
{
peerName : protooPeer.id,
raiseHandState : raiseHandState
},
[ protooPeer ]);
break;
}
default: default:
{ {
logger.error('unknown request.method "%s"', request.method); logger.error('unknown request.method "%s"', request.method);

View File

@ -10,7 +10,7 @@
"colors": "^1.1.2", "colors": "^1.1.2",
"debug": "^3.1.0", "debug": "^3.1.0",
"express": "^4.16.2", "express": "^4.16.2",
"mediasoup": "^2.0.14", "mediasoup": "^2.1.0",
"protoo-server": "^2.0.7" "protoo-server": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {

194
server/router.js 100644
View File

@ -0,0 +1,194 @@
'use strict';
const EventEmitter = require( 'events' );
const eventEmitter = new EventEmitter();
const path = require('path');
const url = require('url');
const httpHelpers = require('./http-helpers');
const fs = require('fs');
const config = require('./config');
const utils = require('./util');
const querystring = require('querystring');
const https = require('https')
const Logger = require('./lib/Logger');
const logger = new Logger();
let authRequests = {}; // ongoing auth requests :
/*
{
state:
{
peerName:'peerName'
code:'oauth2 code',
roomId: 'romid',
}
}
*/
const actions = {
'GET': function(req, res) {
var parsedUrl = url.parse(req.url,true);
if ( parsedUrl.pathname === '/auth-callback' )
{
if ( typeof(authRequests[parsedUrl.query.state]) != 'undefined' )
{
console.log('got authorization code for access token: ',parsedUrl.query,authRequests[parsedUrl.query.state]);
const auth = "Basic " + new Buffer(config.oauth2.client_id + ":" + config.oauth2.client_secret).toString("base64");
const postUrl = url.parse(config.oauth2.token_endpoint);
let postData = querystring.stringify({
"grant_type":"authorization_code",
"code":parsedUrl.query.code,
"redirect_uri":config.oauth2.redirect_uri
});
let request = https.request( {
host : postUrl.hostname,
path : postUrl.pathname,
port : postUrl.port,
method : 'POST',
headers :
{
'Content-Type' : 'application/x-www-form-urlencoded',
'Authorization' : auth,
'Content-Length': Buffer.byteLength(postData)
}
}, function(res)
{
res.setEncoding("utf8");
let body = "";
res.on("data", data => {
body += data;
});
res.on("end", () => {
if ( res.statusCode == 200 )
{
console.log('We\'ve got an access token!', body);
body = JSON.parse(body);
authRequests[parsedUrl.query.state].access_token =
body.access_token;
const auth = "Bearer " + body.access_token;
const getUrl = url.parse(config.oauth2.userinfo_endpoint);
let request = https.request( {
host : getUrl.hostname,
path : getUrl.pathname,
port : getUrl.port,
method : 'GET',
headers :
{
'Authorization' : auth,
}
}, function(res)
{
res.setEncoding("utf8");
let body = '';
res.on("data", data => {
body += data;
});
res.on("end", () => {
// we don't need this any longer:
delete authRequests[parsedUrl.query.state].access_token;
body = JSON.parse(body);
console.log(body);
if ( res.statusCode == 200 )
{
authRequests[parsedUrl.query.state].verified = true;
if ( typeof(body.sub) != 'undefined')
{
authRequests[parsedUrl.query.state].sub = body.sub;
}
if ( typeof(body.name) != 'undefined')
{
authRequests[parsedUrl.query.state].name = body.name;
}
if ( typeof(body.picture) != 'undefined')
{
authRequests[parsedUrl.query.state].picture = body.picture;
}
} else {
{
authRequests[parsedUrl.query.state].verified = false;
}
}
eventEmitter.emit('auth',
authRequests[parsedUrl.query.state]);
delete authRequests[parsedUrl.query.state];
});
});
request.write(' ');
request.end;
}
else
{
console.log('access_token denied',body);
authRequests[parsedUrl.query.state].verified = false;
delete authRequests[parsedUrl.query.state].access_token;
eventEmitter.emit('auth',
authRequests[parsedUrl.query.state]);
}
});
});
request.write(postData);
request.end;
}
else
{
logger.warn('Got authorization_code for unseen state:', parsedUrl)
}
}
else if (parsedUrl.pathname === '/login') {
const state = utils.random(10);
httpHelpers.redirector(res, config.oauth2.authorization_endpoint
+ '?client_id=' + config.oauth2.client_id
+ '&redirect_uri=' + config.oauth2.redirect_uri
+ '&state=' + state
+ '&scopes=' + config.oauth2.scopes.request.join('+')
+ '&response_type=' + config.oauth2.response_type);
authRequests[state] =
{
'roomId' : parsedUrl.query.roomId,
'peerName' : parsedUrl.query.peerName
};
console.log('Started authorization process: ', parsedUrl.query);
}
else
{
console.log('requested url:', parsedUrl.pathname);
var resolvedBase = path.resolve('./public');
var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, '');
var fileLoc = path.join(resolvedBase, safeSuffix);
var stream = fs.createReadStream(fileLoc);
// Handle non-existent file -> delivering index.html
stream.on('error', function(error) {
stream = fs.createReadStream(path.resolve('./public/index.html'));
res.statusCode = 200;
stream.pipe(res);
});
// File exists, stream it to user
res.statusCode = 200;
stream.pipe(res);
}
},
'POST': function(req, res) {
httpHelpers.prepareResponse(req, function(data) {
// Do something with the data that was just collected by the helper
// e.g., validate and save to db
// either redirect or respond
// should be based on result of the operation performed in response to the POST request intent
// e.g., if user wants to save, and save fails, throw error
httpHelpers.redirector(res, /* redirect path , optional status code - defaults to 302 */);
});
}
};
module.exports = eventEmitter;
module.exports.handleRequest = function(req, res) {
var action = actions[req.method];
action ? action(req, res) : httpHelpers.send404(res);
};

View File

@ -14,7 +14,9 @@ console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
const fs = require('fs'); const fs = require('fs');
const https = require('https'); const https = require('https');
const router = require('./router');
const url = require('url'); const url = require('url');
const path = require('path');
const protooServer = require('protoo-server'); const protooServer = require('protoo-server');
const mediasoup = require('mediasoup'); const mediasoup = require('mediasoup');
const readline = require('readline'); const readline = require('readline');
@ -77,25 +79,34 @@ mediaServer.on('newroom', (room) =>
}); });
}); });
// HTTPS server for the protoo WebSocket server. // HTTPS server
const tls = const tls =
{ {
cert : fs.readFileSync(config.tls.cert), cert : fs.readFileSync(config.tls.cert),
key : fs.readFileSync(config.tls.key) key : fs.readFileSync(config.tls.key)
}; };
const httpsServer = https.createServer(tls, (req, res) => const httpsServer = https.createServer(tls, router.handleRequest);
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
{ {
res.writeHead(404, 'Not Here'); logger.info('Server running, port: ',config.listeningPort);
res.end();
}); });
httpsServer.listen(3443, '0.0.0.0', () => router.on('auth',function(event){
{ console.log('router: Got an event: ',event)
logger.info('protoo WebSocket server running'); if ( rooms.has(event.roomId) )
}); {
const room = rooms.get(event.roomId)._protooRoom;
if ( room.hasPeer(event.peerName) )
{
const peer = room.getPeer(event.peerName);
peer.send('auth', event)
}
}
})
// Protoo WebSocket server. // Protoo WebSocket server listens to same webserver so everythink is available
// via same port
const webSocketServer = new protooServer.WebSocketServer(httpsServer, const webSocketServer = new protooServer.WebSocketServer(httpsServer,
{ {
maxReceivedFrameSize : 960000, // 960 KBytes. maxReceivedFrameSize : 960000, // 960 KBytes.

18
server/util.js 100644
View File

@ -0,0 +1,18 @@
'use strict';
var crypto = require('crypto');
exports.random = function (howMany, chars) {
chars = chars
|| "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789";
var rnd = crypto.randomBytes(howMany)
, value = new Array(howMany)
, len = len = Math.min(256, chars.length)
, d = 256 / len
for (var i = 0; i < howMany; i++) {
value[i] = chars[Math.floor(rnd[i] / d)]
};
return value.join('');
}