Merge branch 'develop'

master
Håvar Aambø Fosstveit 2018-12-11 12:32:55 +01:00
commit 1d9668fa46
78 changed files with 3859 additions and 23137 deletions

9
CHANGELOG.md 100644
View File

@ -0,0 +1,9 @@
# Changelog
### 1.0
* Fixed toolarea button based on feedback from users
* Added possibility to move video to separate window
* Added SIP gateway
### RC1 1.0
* First stable release?

View File

@ -2,8 +2,16 @@
A WebRTC meeting service using [mediasoup](https://mediasoup.org) as its backend. A WebRTC meeting service using [mediasoup](https://mediasoup.org) as its backend.
Try it online at https://akademia.no. You can add /roomname to the URL for specifying a room. Try it online at https://letsmeet.no. You can add /roomname to the URL for specifying a room.
## Features
* Audio/Video
* Chat
* Screen sharing
* File sharing
* Different video layouts
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To test it, call: roomname@letsmeet.no.
## Installation ## Installation
@ -22,15 +30,6 @@ $ cp server/config.example.js server/config.js
* Copy `app/config.example.js` to `app/config.js` : * Copy `app/config.example.js` to `app/config.js` :
In addition, the server requires a screen to be installed for the server
to be able to seed shared torrent files. This is because the headless
Electron instance used by WebTorrent expects one.
See [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) for
more information about this.
* Copy `config.example.js` as `config.js` and customize it for your scenario:
```bash ```bash
$ cp app/config.example.js app/config.js $ cp app/config.example.js app/config.js
``` ```
@ -72,7 +71,7 @@ $ node server.js
## Deploy it in a server ## Deploy it in a server
* Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and dobbel check location path settings: * Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and check location path settings:
```bash ```bash
$ cp multiparty-meeting.service /etc/systemd/system/ $ cp multiparty-meeting.service /etc/systemd/system/
$ edit /etc/systemd/system/multiparty-meeting.service $ edit /etc/systemd/system/multiparty-meeting.service
@ -97,7 +96,6 @@ $ systemctl enable multiparty-meeting
* 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`) * 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`)
* If you want your service running at standard ports 80/443 you should: * If you want your service running at standard ports 80/443 you should:
* Make a redirect from HTTP port 80 to HTTPS (with Apache/NGINX)
* Configure a forwarding rule with iptables from port 443 to your configured service port (default 3443) * Configure a forwarding rule with iptables from port 443 to your configured service port (default 3443)

25
app/.babelrc 100644
View File

@ -0,0 +1,25 @@
{
"plugins":
[
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
],
"presets":
[
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"chrome >= 67",
"edge >= 17",
"firefox >= 60",
"safari >= 12"
]
}
}
],
"@babel/react"
]
}

View File

@ -9,7 +9,8 @@ module.exports =
plugins: plugins:
[ [
'import', 'import',
'react' 'react',
'jsx-control-statements'
], ],
extends: extends:
[ [
@ -21,13 +22,13 @@ module.exports =
react: react:
{ {
pragma: 'React', pragma: 'React',
version: '15' version: '16'
} }
}, },
parser: 'babel-eslint', parser: "babel-eslint",
parserOptions: parserOptions:
{ {
ecmaVersion: 9, ecmaVersion: 2018,
sourceType: 'module', sourceType: 'module',
ecmaFeatures: ecmaFeatures:
{ {
@ -177,6 +178,7 @@ module.exports =
'spaced-comment': [ 2, 'always' ], 'spaced-comment': [ 2, 'always' ],
'strict': 2, 'strict': 2,
'valid-typeof': 2, 'valid-typeof': 2,
'eol-last': 0,
'yoda': 2, 'yoda': 2,
// eslint-plugin-import options. // eslint-plugin-import options.
'import/extensions': 2, 'import/extensions': 2,
@ -197,7 +199,7 @@ module.exports =
'react/jsx-no-bind': 0, 'react/jsx-no-bind': 0,
'react/jsx-no-duplicate-props': 2, 'react/jsx-no-duplicate-props': 2,
'react/jsx-no-literals': 0, 'react/jsx-no-literals': 0,
'react/jsx-no-undef': 2, 'react/jsx-no-undef': 0,
'react/jsx-pascal-case': 2, 'react/jsx-pascal-case': 2,
'react/jsx-sort-prop-types': 0, 'react/jsx-sort-prop-types': 0,
'react/jsx-sort-props': 0, 'react/jsx-sort-props': 0,
@ -214,7 +216,7 @@ module.exports =
'react/no-string-refs': 0, 'react/no-string-refs': 0,
'react/no-unknown-property': 2, 'react/no-unknown-property': 2,
'react/prefer-es6-class': 2, 'react/prefer-es6-class': 2,
'react/prop-types': 2, 'react/prop-types': [ 2, { skipUndeclared: true } ],
'react/react-in-jsx-scope': 2, 'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2, 'react/self-closing-comp': 2,
'react/sort-comp': 0, 'react/sort-comp': 0,

1
app/.npmrc 100644
View File

@ -0,0 +1 @@
package-lock=false

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Multiparty Meeting</title>
</head>
<style>
body{
margin:auto;
padding:0.5vmin;
text-align:center;
position: fixed;
left: 50%;
top: 40%;
width: 90%;
transform: translate(-50%, 0%);
background-image: url('/resources/images/background.svg');
background-attachment: fixed;
background-position: center;
background-size: cover;
background-repeat: repeat;
}
input:hover {opacity:0.9;}
input[type=text]{
font-size: 1.5em;
padding: 1.5vmin;
background-color: rgba(0,0,0,0.3);
border: 0;
color: #fff;
margin: 0.8vmin;
width: 50%;
}
button:hover {background-color: #28bd7b;}
button{
font-size: 1.5em;
padding: 1.5vmin;
margin: 0.8vmin;
background-color: #38cd8b;
border-radius: 1.8vmin;
color: #fff;
border: 0;
}
img{
height: 15vmin;
}
</style>
<body>
<a>
<img src='/resources/images/logo.svg'></img><br>
</a>
<input id="room" type="text" onkeypress="checkEnter(event)" value="" placeholder="your room name">
<button onclick = "start(location.href)">Go to room</button>
</body>
<script>
var room = document.getElementById("room");
var stateObj = { foo: "bar" };
room.addEventListener("input", function(e) {
console.log(e.charCode);
history.replaceState(stateObj, "Multiparty Meeting", "/"+room.value);
},true);
room.focus();
function start(target){
location.href;history.replaceState(stateObj, "Multiparty Meeting", "/");
window.location = target;
}
function checkEnter(event){
var x = event.charCode || event.keyCode;
if (x == 13 ) {
start(location.href);
}
}
</script>
</html>

View File

@ -26,7 +26,7 @@ const touch = require('gulp-touch-cmd');
const browserify = require('browserify'); const browserify = require('browserify');
const watchify = require('watchify'); const watchify = require('watchify');
const envify = require('envify/custom'); const envify = require('envify/custom');
const uglify = require('gulp-uglify'); const uglify = require('gulp-uglify-es').default;
const source = require('vinyl-source-stream'); const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer'); const buffer = require('vinyl-buffer');
const del = require('del'); const del = require('del');
@ -77,10 +77,7 @@ function bundle(options)
// required to be true only for watchify. // required to be true only for watchify.
fullPaths : watch fullPaths : watch
}) })
.transform('babelify', .transform('babelify')
{
presets : [ 'env', 'react-app' ]
})
.transform(envify( .transform(envify(
{ {
NODE_ENV : process.env.NODE_ENV, NODE_ENV : process.env.NODE_ENV,
@ -140,7 +137,7 @@ gulp.task('lint', () =>
.pipe(eslint.format()); .pipe(eslint.format());
}); });
gulp.task('lint-fix', function() gulp.task('lint-fix', function()
{ {
return gulp.src(LINTING_FILES) return gulp.src(LINTING_FILES)
.pipe(plumber()) .pipe(plumber())
@ -171,7 +168,7 @@ gulp.task('css', () =>
gulp.task('html', () => gulp.task('html', () =>
{ {
return gulp.src('index.html') return gulp.src('*.html')
.pipe(change(changeHTML)) .pipe(change(changeHTML))
.pipe(gulp.dest(OUTPUT_DIR)); .pipe(gulp.dest(OUTPUT_DIR));
}); });
@ -244,7 +241,7 @@ gulp.task('browser', (done) =>
gulp.task('watch', (done) => gulp.task('watch', (done) =>
{ {
// Watch changes in HTML. // Watch changes in HTML.
gulp.watch([ 'index.html' ], gulp.series( gulp.watch([ '*.html' ], gulp.series(
'html' 'html'
)); ));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
import { EventEmitter } from 'events';
import Logger from './Logger';
const logger = new Logger('Spotlight');
export default class Spotlights extends EventEmitter
{
constructor(maxSpotlights, room)
{
super();
this._room = room;
this._maxSpotlights = maxSpotlights;
this._peerList = [];
this._selectedSpotlights = [];
this._currentSpotlights = [];
this._started = false;
}
start()
{
const peers = this._room.peers;
for (const peer of peers)
{
this._handlePeer(peer);
}
this._handleRoom();
this._started = true;
this._spotlightsUpdated();
}
peerInSpotlights(peerName)
{
if (this._started)
{
return this._currentSpotlights.indexOf(peerName) !== -1;
}
else
{
return false;
}
}
setPeerSpotlight(peerName)
{
logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName);
const index = this._selectedSpotlights.indexOf(peerName);
if (index !== -1)
{
this._selectedSpotlights = [];
}
else
{
this._selectedSpotlights = [ peerName ];
}
/*
if (index === -1) // We don't have this peer in the list, adding
{
this._selectedSpotlights.push(peerName);
}
else // We have this peer, remove
{
this._selectedSpotlights.splice(index, 1);
}
*/
if (this._started)
this._spotlightsUpdated();
}
_handleRoom()
{
this._room.on('newpeer', (peer) =>
{
logger.debug(
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
this._handlePeer(peer);
});
}
addSpeakerList(speakerList)
{
this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ];
if (this._started)
this._spotlightsUpdated();
}
_handlePeer(peer)
{
logger.debug('_handlePeer() [peerName:"%s"]', peer.name);
if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list
{
peer.on('close', () =>
{
let index = this._peerList.indexOf(peer.name);
if (index !== -1) // We have this peer in the list, remove
{
this._peerList.splice(index, 1);
}
index = this._selectedSpotlights.indexOf(peer.name);
if (index !== -1) // We have this peer in the list, remove
{
this._selectedSpotlights.splice(index, 1);
}
this._spotlightsUpdated();
});
logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name);
this._peerList.push(peer.name);
this._spotlightsUpdated();
}
}
handleActiveSpeaker(peerName)
{
logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName);
const index = this._peerList.indexOf(peerName);
if (index > -1)
{
this._peerList.splice(index, 1);
this._peerList = [ peerName ].concat(this._peerList);
this._spotlightsUpdated();
}
}
_spotlightsUpdated()
{
let spotlights;
if (this._selectedSpotlights.length > 0)
{
spotlights = [ ...new Set([ ...this._selectedSpotlights, ...this._peerList ]) ];
}
else
{
spotlights = this._peerList;
}
if (
!this._arraysEqual(
this._currentSpotlights, spotlights.slice(0, this._maxSpotlights)
)
)
{
logger.debug('_spotlightsUpdated() | spotlights updated, emitting');
this._currentSpotlights = spotlights.slice(0, this._maxSpotlights);
this.emit('spotlights-updated', this._currentSpotlights);
}
else
logger.debug('_spotlightsUpdated() | spotlights not updated');
}
_arraysEqual(arr1, arr2)
{
if (arr1.length !== arr2.length)
return false;
for (let i = arr1.length; i--;)
{
if (arr1[i] !== arr2[i])
return false;
}
return true;
}
}

View File

@ -34,6 +34,11 @@ class Chat extends Component
autoFocus={autofocus} autoFocus={autofocus}
autoComplete='off' autoComplete='off'
/> />
<input
type='submit'
className='send'
value='Send'
/>
</form> </form>
</div> </div>
); );
@ -53,7 +58,7 @@ Chat.propTypes =
Chat.defaultProps = Chat.defaultProps =
{ {
senderPlaceHolder : 'Type a message...', senderPlaceHolder : 'Type a message...',
autofocus : true, autofocus : false,
displayName : null displayName : null
}; };

View File

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

View File

@ -1,131 +0,0 @@
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 './Chat/MessageList';
class ChatWidget extends Component
{
componentWillReceiveProps(nextProps)
{
if (nextProps.chatmessages.length !== this.props.chatmessages.length)
if (!this.props.showChat)
this.props.increaseBadge();
}
render()
{
const {
senderPlaceHolder,
onSendMessage,
onToggleChat,
showChat,
disabledInput,
badge,
autofocus,
displayName
} = this.props;
return (
<div data-component='ChatWidget'>
{
showChat &&
<div data-component='Conversation'>
<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>
}
{
<div
className='launcher'
data-type='dark'
data-tip='Show room chat'
onClick={onToggleChat}
>
{
badge > 0 && <span className='badge'>{badge}</span>
}
</div>
}
</div>
);
}
}
ChatWidget.propTypes =
{
onToggleChat : PropTypes.func,
showChat : PropTypes.bool,
senderPlaceHolder : PropTypes.string,
onSendMessage : PropTypes.func,
disabledInput : PropTypes.bool,
badge : PropTypes.number,
autofocus : PropTypes.bool,
displayName : PropTypes.string,
chatmessages : PropTypes.arrayOf(PropTypes.object),
increaseBadge : PropTypes.func
};
ChatWidget.defaultProps =
{
senderPlaceHolder : 'Type a message...',
autofocus : true
};
const mapStateToProps = (state) =>
{
return {
showChat : state.chatbehavior.showChat,
disabledInput : state.chatbehavior.disabledInput,
displayName : state.me.displayName,
badge : state.chatbehavior.badge,
chatmessages : state.chatmessages
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onToggleChat : () =>
{
dispatch(stateActions.toggleChat());
},
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 = '';
},
increaseBadge : () =>
{
dispatch(stateActions.increaseBadge());
}
};
};
const ChatWidgetContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ChatWidget);
export default ChatWidgetContainer;

View File

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

View File

@ -2,9 +2,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import debounce from 'lodash/debounce';
import classnames from 'classnames'; import classnames from 'classnames';
import * as stateActions from '../redux/stateActions'; import * as requestActions from '../redux/requestActions';
import Peer from './Peer'; import Peer from './Peer';
import HiddenPeers from './HiddenPeers';
class Filmstrip extends Component class Filmstrip extends Component
{ {
@ -25,11 +27,11 @@ class Filmstrip extends Component
// person has spoken yet, the first peer in the list of peers. // person has spoken yet, the first peer in the list of peers.
getActivePeerName = () => getActivePeerName = () =>
{ {
if (this.props.selectedPeerName) if (this.props.selectedPeerName)
{ {
return this.props.selectedPeerName; return this.props.selectedPeerName;
} }
if (this.state.lastSpeaker) if (this.state.lastSpeaker)
{ {
return this.state.lastSpeaker; return this.state.lastSpeaker;
@ -51,7 +53,7 @@ class Filmstrip extends Component
{ {
let ratio = 4 / 3; let ratio = 4 / 3;
if (this.isSharingCamera(this.getActivePeerName())) if (this.isSharingCamera(this.getActivePeerName()))
{ {
ratio *= 2; ratio *= 2;
} }
@ -59,7 +61,7 @@ class Filmstrip extends Component
return ratio; return ratio;
}; };
updateDimensions = () => updateDimensions = debounce(() =>
{ {
const container = this.activePeerContainer.current; const container = this.activePeerContainer.current;
@ -69,16 +71,16 @@ class Filmstrip extends Component
let width = container.clientWidth; let width = container.clientWidth;
if (width / ratio > container.clientHeight) if (width / ratio > container.clientHeight)
{ {
width = container.clientHeight * ratio; width = container.clientHeight * ratio;
} }
this.setState({ this.setState({
width width
}); });
} }
}; }, 200);
componentDidMount() componentDidMount()
{ {
@ -112,7 +114,7 @@ class Filmstrip extends Component
render() render()
{ {
const { peers, advancedMode } = this.props; const { peers, advancedMode, spotlights, spotlightsLength } = this.props;
const activePeerName = this.getActivePeerName(); const activePeerName = this.getActivePeerName();
@ -137,25 +139,40 @@ class Filmstrip extends Component
<div className='filmstrip'> <div className='filmstrip'>
<div className='filmstrip-content'> <div className='filmstrip-content'>
{Object.keys(peers).map((peerName) => ( {
<div Object.keys(peers).map((peerName) =>
key={peerName} {
onClick={() => this.props.setSelectedPeer(peerName)} return (
className={classnames('film', { spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
selected : this.props.selectedPeerName === peerName, <div
active : this.state.lastSpeaker === peerName key={peerName}
})} onClick={() => this.props.setSelectedPeer(peerName)}
> className={classnames('film', {
<div className='film-content'> selected : this.props.selectedPeerName === peerName,
<Peer active : this.state.lastSpeaker === peerName
advancedMode={advancedMode} })}
name={peerName} >
/> <div className='film-content'>
</div> <Peer
</div> advancedMode={advancedMode}
))} name={peerName}
/>
</div>
</div>
:null
);
})
}
</div> </div>
</div> </div>
<div className='hidden-peer-container'>
{ (spotlightsLength<Object.keys(peers).length)?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>:null
}
</div>
</div> </div>
); );
} }
@ -168,19 +185,28 @@ Filmstrip.propTypes = {
consumers : PropTypes.object.isRequired, consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired, myName : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string, selectedPeerName : PropTypes.string,
setSelectedPeer : PropTypes.func.isRequired setSelectedPeer : PropTypes.func.isRequired,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) =>
activeSpeakerName : state.room.activeSpeakerName, {
selectedPeerName : state.room.selectedPeerName, const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
peers : state.peers,
consumers : state.consumers, return {
myName : state.me.name activeSpeakerName : state.room.activeSpeakerName,
}); selectedPeerName : state.room.selectedPeerName,
peers : state.peers,
consumers : state.consumers,
myName : state.me.name,
spotlights : state.room.spotlights,
spotlightsLength
};
};
const mapDispatchToProps = { const mapDispatchToProps = {
setSelectedPeer : stateActions.setSelectedPeer setSelectedPeer : requestActions.setSelectedPeer
}; };
export default connect( export default connect(

View File

@ -0,0 +1,107 @@
const key = {
fullscreenEnabled : 0,
fullscreenElement : 1,
requestFullscreen : 2,
exitFullscreen : 3,
fullscreenchange : 4,
fullscreenerror : 5
};
const webkit = [
'webkitFullscreenEnabled',
'webkitFullscreenElement',
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitfullscreenchange',
'webkitfullscreenerror'
];
const moz = [
'mozFullScreenEnabled',
'mozFullScreenElement',
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozfullscreenchange',
'mozfullscreenerror'
];
const ms = [
'msFullscreenEnabled',
'msFullscreenElement',
'msRequestFullscreen',
'msExitFullscreen',
'MSFullscreenChange',
'MSFullscreenError'
];
export default class FullScreen
{
constructor(document)
{
this.document = document;
this.vendor = (
('fullscreenEnabled' in this.document && Object.keys(key)) ||
(webkit[0] in this.document && webkit) ||
(moz[0] in this.document && moz) ||
(ms[0] in this.document && ms) ||
[]
);
}
requestFullscreen(element)
{
element[this.vendor[key.requestFullscreen]]();
}
requestFullscreenFunction(element)
{
element[this.vendor[key.requestFullscreen]];
}
addEventListener(type, handler)
{
this.document.addEventListener(this.vendor[key[type]], handler);
}
removeEventListener(type, handler)
{
this.document.removeEventListener(this.vendor[key[type]], handler);
}
get exitFullscreen()
{
return this.document[this.vendor[key.exitFullscreen]].bind(this.document);
}
get fullscreenEnabled()
{
return Boolean(this.document[this.vendor[key.fullscreenEnabled]]);
}
set fullscreenEnabled(val) {}
get fullscreenElement()
{
return this.document[this.vendor[key.fullscreenElement]];
}
set fullscreenElement(val) {}
get onfullscreenchange()
{
return this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()];
}
set onfullscreenchange(handler)
{
this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()] = handler;
}
get onfullscreenerror()
{
return this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()];
}
set onfullscreenerror(handler)
{
this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()] = handler;
}
}

View File

@ -40,7 +40,7 @@ const FullScreenView = (props) =>
<div className='controls'> <div className='controls'>
<div <div
className={classnames('button', 'fullscreen', 'room-controls', { className={classnames('button', 'exitfullscreen', 'room-controls', {
visible : toolbarsVisible visible : toolbarsVisible
})} })}
onClick={(e) => onClick={(e) =>
@ -56,7 +56,6 @@ const FullScreenView = (props) =>
videoTrack={consumer ? consumer.track : null} videoTrack={consumer ? consumer.track : null}
videoVisible={consumerVisible} videoVisible={consumerVisible}
videoProfile={consumerProfile} videoProfile={consumerProfile}
toggleFullscreen={() => toggleConsumerFullscreen(consumer)}
/> />
</div> </div>
); );

View File

@ -84,8 +84,7 @@ export default class FullView extends React.Component
FullView.propTypes = FullView.propTypes =
{ {
videoTrack : PropTypes.any, videoTrack : PropTypes.any,
videoVisible : PropTypes.bool, videoVisible : PropTypes.bool,
videoProfile : PropTypes.string, videoProfile : PropTypes.string
toggleFullscreen : PropTypes.func.isRequired
}; };

View File

@ -0,0 +1,85 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as stateActions from '../redux/stateActions';
class HiddenPeers extends Component
{
constructor(props)
{
super(props);
this.state = { className: '' };
}
componentDidUpdate(prevProps)
{
const { hiddenPeersCount } = this.props;
if (hiddenPeersCount !== prevProps.hiddenPeersCount)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ className: 'pulse' }, () =>
{
if (this.timeout)
{
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() =>
{
this.setState({ className: '' });
}, 2000);
});
}
}
render()
{
const {
hiddenPeersCount,
openUsersTab
} = this.props;
return (
<div
data-component='HiddenPeers'
className={this.state.className}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<div data-component='HiddenPeersView'>
<div className={classnames('view-container', this.state.className)} onClick={() => openUsersTab()}>
<p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount === 1) ? null : 's'}
</p>
</div>
</div>
</div>
);
}
}
HiddenPeers.propTypes =
{
hiddenPeersCount : PropTypes.number,
openUsersTab : PropTypes.func.isRequired
};
const mapDispatchToProps = (dispatch) =>
{
return {
openUsersTab : () =>
{
dispatch(stateActions.openToolArea());
dispatch(stateActions.setToolTab('users'));
}
};
};
const HiddenPeersContainer = connect(
null,
mapDispatchToProps
)(HiddenPeers);
export default HiddenPeersContainer;

View File

@ -15,14 +15,14 @@ class Me extends React.Component
controlsVisible : false controlsVisible : false
}; };
handleMouseOver = () => handleMouseOver = () =>
{ {
this.setState({ this.setState({
controlsVisible : true controlsVisible : true
}); });
}; };
handleMouseOut = () => handleMouseOut = () =>
{ {
this.setState({ this.setState({
controlsVisible : false controlsVisible : false
@ -108,23 +108,29 @@ class Me extends React.Component
> >
<div className={classnames('view-container', 'webcam')}> <div className={classnames('view-container', 'webcam')}>
{connected ? {connected ?
<div className={classnames('controls', { <div className={classnames('controls', 'visible')}>
visible : this.state.controlsVisible
})}
>
<div <div
data-tip='keyboard shortcut: &lsquo;m&lsquo;'
data-type='dark'
data-place='bottom'
data-for='me'
className={classnames('button', 'mic', micState, { className={classnames('button', 'mic', micState, {
disabled : me.audioInProgress disabled : me.audioInProgress,
visible : micState == 'off' || this.state.controlsVisible
})} })}
onClick={() => onClick={() =>
{ {
micState === 'on' ? onMuteMic() : onUnmuteMic(); micState === 'on' ? onMuteMic() : onUnmuteMic();
}} }}
/> />
<ReactTooltip
id='me'
effect='solid'
/>
<div <div
className={classnames('button', 'webcam', webcamState, { className={classnames('button', 'webcam', webcamState, {
disabled : me.webcamInProgress disabled : me.webcamInProgress,
visible : webcamState == 'off' || this.state.controlsVisible
})} })}
onClick={() => onClick={() =>
{ {
@ -161,15 +167,6 @@ class Me extends React.Component
</div> </div>
:null :null
} }
{this._tooltip ?
<ReactTooltip
effect='solid'
delayShow={100}
delayHide={100}
/>
:null
}
</div> </div>
); );
} }

View File

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

View File

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

View File

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

View File

@ -38,12 +38,10 @@ class Peer extends Component
screenConsumer, screenConsumer,
onMuteMic, onMuteMic,
onUnmuteMic, onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen,
toggleConsumerFullscreen, toggleConsumerFullscreen,
style toggleConsumerWindow,
style,
windowConsumer
} = this.props; } = this.props;
const micEnabled = ( const micEnabled = (
@ -90,6 +88,13 @@ class Peer extends Component
:null :null
} }
{!videoVisible ?
<div className='paused-video'>
<p>this video is paused</p>
</div>
:null
}
<div className={classnames('view-container', 'webcam')} style={style}> <div className={classnames('view-container', 'webcam')} style={style}>
<div className='indicators'> <div className='indicators'>
{peer.raiseHandState ? {peer.raiseHandState ?
@ -124,16 +129,14 @@ class Peer extends Component
/> />
<div <div
className={classnames('button', 'webcam', { className={classnames('button', 'newwindow', {
on : videoVisible, disabled : !videoVisible ||
off : !videoVisible, (windowConsumer === webcamConsumer.id)
disabled : peer.peerVideoInProgress
})} })}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
videoVisible ? toggleConsumerWindow(webcamConsumer);
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
}} }}
/> />
@ -146,10 +149,10 @@ class Peer extends Component
}} }}
/> />
</div> </div>
<PeerView <PeerView
advancedMode={advancedMode} advancedMode={advancedMode}
peer={peer} peer={peer}
audioTrack={micConsumer ? micConsumer.track : null}
volume={micConsumer ? micConsumer.volume : null} volume={micConsumer ? micConsumer.volume : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null} videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible} videoVisible={videoVisible}
@ -167,16 +170,11 @@ class Peer extends Component
})} })}
> >
<div <div
className={classnames('button', 'screen', { className={classnames('button', 'newwindow')}
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
})}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
screenVisible ? toggleConsumerWindow(screenConsumer);
onDisableScreen(peer.name) : onEnableScreen(peer.name);
}} }}
/> />
@ -211,15 +209,13 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number,
onMuteMic : PropTypes.func.isRequired, onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired, onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired,
streamDimensions : PropTypes.object, streamDimensions : PropTypes.object,
style : PropTypes.object, style : PropTypes.object,
onEnableScreen : PropTypes.func.isRequired, toggleConsumerFullscreen : PropTypes.func.isRequired,
onDisableScreen : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired
toggleConsumerFullscreen : PropTypes.func.isRequired
}; };
const mapStateToProps = (state, { name }) => const mapStateToProps = (state, { name }) =>
@ -238,7 +234,8 @@ const mapStateToProps = (state, { name }) =>
peer, peer,
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
screenConsumer screenConsumer,
windowConsumer : state.room.windowConsumer
}; };
}; };
@ -253,27 +250,15 @@ const mapDispatchToProps = (dispatch) =>
{ {
dispatch(requestActions.unmutePeerAudio(peerName)); dispatch(requestActions.unmutePeerAudio(peerName));
}, },
onEnableWebcam : (peerName) =>
{
dispatch(requestActions.resumePeerVideo(peerName));
},
onDisableWebcam : (peerName) =>
{
dispatch(requestActions.pausePeerVideo(peerName));
},
onEnableScreen : (peerName) =>
{
dispatch(requestActions.resumePeerScreen(peerName));
},
onDisableScreen : (peerName) =>
{
dispatch(requestActions.pausePeerScreen(peerName));
},
toggleConsumerFullscreen : (consumer) => toggleConsumerFullscreen : (consumer) =>
{ {
if (consumer) if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id)); dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
},
toggleConsumerWindow : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerWindow(consumer.id));
} }
}; };
}; };

View File

@ -0,0 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import PeerAudio from './PeerAudio';
const AudioPeer = ({ micConsumer }) =>
{
return (
<PeerAudio
audioTrack={micConsumer ? micConsumer.track : null}
/>
);
};
AudioPeer.propTypes =
{
micConsumer : appPropTypes.Consumer,
name : PropTypes.string
};
const mapStateToProps = (state, { name }) =>
{
const peer = state.peers[name];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const micConsumer =
consumersArray.find((consumer) => consumer.source === 'mic');
return {
micConsumer
};
};
const AudioPeerContainer = connect(
mapStateToProps
)(AudioPeer);
export default AudioPeerContainer;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import AudioPeer from './AudioPeer';
const AudioPeers = ({ peers }) =>
{
return (
<div data-component='AudioPeers'>
{
peers.map((peer) =>
{
return (
<AudioPeer
key={peer.name}
name={peer.name}
/>
);
})
}
</div>
);
};
AudioPeers.propTypes =
{
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
};
const mapStateToProps = (state) =>
{
const peers = Object.values(state.peers);
return {
peers
};
};
const AudioPeersContainer = connect(
mapStateToProps
)(AudioPeers);
export default AudioPeersContainer;

View File

@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class PeerAudio extends React.Component
{
constructor(props)
{
super(props);
// Latest received audio track.
// @type {MediaStreamTrack}
this._audioTrack = null;
}
render()
{
return (
<audio
ref='audio'
autoPlay
/>
);
}
componentDidMount()
{
const { audioTrack } = this.props;
this._setTrack(audioTrack);
}
componentWillReceiveProps(nextProps)
{
const { audioTrack } = nextProps;
this._setTrack(audioTrack);
}
_setTrack(audioTrack)
{
if (this._audioTrack === audioTrack)
return;
this._audioTrack = audioTrack;
const { audio } = this.refs;
if (audioTrack)
{
const stream = new MediaStream;
if (audioTrack)
stream.addTrack(audioTrack);
audio.srcObject = stream;
}
else
{
audio.srcObject = null;
}
}
}
PeerAudio.propTypes =
{
audioTrack : PropTypes.any
};

View File

@ -2,9 +2,11 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import debounce from 'lodash/debounce';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Peer from './Peer'; import Peer from './Peer';
import HiddenPeers from './HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334; const RATIO = 1.334;
@ -23,9 +25,9 @@ class Peers extends React.Component
this.peersRef = React.createRef(); this.peersRef = React.createRef();
} }
updateDimensions = () => updateDimensions = debounce(() =>
{ {
if (!this.peersRef.current) if (!this.peersRef.current)
{ {
return; return;
} }
@ -65,7 +67,7 @@ class Peers extends React.Component
peerHeight : 0.9 * y peerHeight : 0.9 * y
}); });
} }
}; }, 200);
componentDidMount() componentDidMount()
{ {
@ -90,7 +92,9 @@ class Peers extends React.Component
const { const {
advancedMode, advancedMode,
activeSpeakerName, activeSpeakerName,
peers peers,
spotlights,
spotlightsLength
} = this.props; } = this.props;
const style = const style =
@ -105,22 +109,35 @@ class Peers extends React.Component
peers.map((peer) => peers.map((peer) =>
{ {
return ( return (
<Appear key={peer.name} duration={1000}> (spotlights.find(function(spotlightsElement)
<div { return spotlightsElement == peer.name; }))?
className={classnames('peer-container', { <Appear key={peer.name} duration={1000}>
'active-speaker' : peer.name === activeSpeakerName <div
})} className={classnames('peer-container', {
> 'selected' : this.props.selectedPeerName === peer.name,
<Peer 'active-speaker' : peer.name === activeSpeakerName
advancedMode={advancedMode} })}
name={peer.name} >
style={style} <div className='peer-content'>
/> <Peer
</div> advancedMode={advancedMode}
</Appear> name={peer.name}
style={style}
/>
</div>
</div>
</Appear>
:null
); );
}) })
} }
<div className='hidden-peer-container'>
{ (spotlightsLength<peers.length)?
<HiddenPeers
hiddenPeersCount={peers.length-spotlightsLength}
/>:null
}
</div>
</div> </div>
); );
} }
@ -131,20 +148,27 @@ Peers.propTypes =
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
boxes : PropTypes.number, boxes : PropTypes.number,
activeSpeakerName : PropTypes.string activeSpeakerName : PropTypes.string,
selectedPeerName : PropTypes.string,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
const peers = Object.values(state.peers); const peers = Object.values(state.peers);
const spotlights = state.room.spotlights;
const boxes = peers.length + Object.values(state.consumers) const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
const boxes = spotlightsLength + Object.values(state.consumers)
.filter((consumer) => consumer.source === 'screen').length; .filter((consumer) => consumer.source === 'screen').length;
return { return {
peers, peers,
boxes, boxes,
activeSpeakerName : state.room.activeSpeakerName activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName,
spotlights,
spotlightsLength
}; };
}; };

View File

@ -4,16 +4,19 @@ import ReactTooltip from 'react-tooltip';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import CookieConsent from 'react-cookie-consent';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../redux/stateActions';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Me from './Me'; import Me from './Me';
import Peers from './Peers'; import Peers from './Peers';
import AudioPeers from './PeerAudio/AudioPeers';
import Notifications from './Notifications'; import Notifications from './Notifications';
import ToolAreaButton from './ToolArea/ToolAreaButton'; // import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea'; import ToolArea from './ToolArea/ToolArea';
import FullScreenView from './FullScreenView'; import FullScreenView from './FullScreenView';
import VideoWindow from './VideoWindow/VideoWindow';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { idle } from '../utils'; import { idle } from '../utils';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
@ -32,16 +35,16 @@ class Room extends React.Component
* given amount of time has passed since the * given amount of time has passed since the
* last time the cursor was moved. * last time the cursor was moved.
*/ */
waitForHide = idle(() => waitForHide = idle(() =>
{ {
this.props.setToolbarsVisible(false); this.props.setToolbarsVisible(false);
}, TIMEOUT); }, TIMEOUT);
handleMovement = () => handleMovement = () =>
{ {
// If the toolbars were hidden, show them again when // If the toolbars were hidden, show them again when
// the user moves their cursor. // the user moves their cursor.
if (!this.props.room.toolbarsVisible) if (!this.props.room.toolbarsVisible)
{ {
this.props.setToolbarsVisible(true); this.props.setToolbarsVisible(true);
} }
@ -65,7 +68,6 @@ class Room extends React.Component
{ {
const { const {
room, room,
toolAreaOpen,
amActiveSpeaker, amActiveSpeaker,
onRoomLinkCopy onRoomLinkCopy
} = this.props; } = this.props;
@ -81,11 +83,19 @@ class Room extends React.Component
<Appear duration={300}> <Appear duration={300}>
<div data-component='Room'> <div data-component='Room'>
<FullScreenView advancedMode={room.advancedMode} /> <CookieConsent>
<div className='room-wrapper'> This website uses cookies to enhance the user experience.
<Notifications /> </CookieConsent>
<ToolAreaButton /> <FullScreenView advancedMode={room.advancedMode} />
<VideoWindow advancedMode={room.advancedMode} />
<div className='room-wrapper'>
<div data-component='Logo' />
<AudioPeers />
<Notifications />
{room.advancedMode ? {room.advancedMode ?
<div className='state' data-tip='Server status'> <div className='state' data-tip='Server status'>
@ -94,7 +104,7 @@ class Room extends React.Component
</div> </div>
:null :null
} }
<div <div
className={classnames('room-link-wrapper room-controls', { className={classnames('room-link-wrapper room-controls', {
'visible' : this.props.room.toolbarsVisible 'visible' : this.props.room.toolbarsVisible
@ -123,7 +133,7 @@ class Room extends React.Component
{ {
return; return;
} }
event.preventDefault(); event.preventDefault();
}} }}
> >
@ -155,16 +165,8 @@ class Room extends React.Component
delayHide={100} delayHide={100}
/> />
</div> </div>
<div
className={classnames('toolarea-wrapper', { open: toolAreaOpen })} <ToolArea />
>
{toolAreaOpen ?
<ToolArea
advancedMode={room.advancedMode}
/>
:null
}
</div>
</div> </div>
</Appear> </Appear>
</Fragment> </Fragment>

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import Spinner from 'react-spinner'; import Spinner from 'react-spinner';
export default class PeerView extends React.Component export default class ScreenView extends React.Component
{ {
constructor(props) constructor(props)
{ {
@ -157,7 +157,7 @@ export default class PeerView extends React.Component
} }
} }
PeerView.propTypes = ScreenView.propTypes =
{ {
isMe : PropTypes.bool, isMe : PropTypes.bool,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,

View File

@ -5,6 +5,7 @@ import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../redux/stateActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Dropdown from 'react-dropdown'; import Dropdown from 'react-dropdown';
import ReactTooltip from 'react-tooltip';
const modes = [ { const modes = [ {
value : 'democratic', value : 'democratic',
@ -22,12 +23,6 @@ const Settings = ({
}) => }) =>
{ {
let webcams; let webcams;
let webcamText;
if (me.canChangeWebcam)
webcamText = 'Select camera';
else
webcamText = 'Unable to select camera';
if (me.webcamDevices) if (me.webcamDevices)
webcams = Array.from(me.webcamDevices.values()); webcams = Array.from(me.webcamDevices.values());
@ -51,13 +46,12 @@ const Settings = ({
<div data-component='Settings'> <div data-component='Settings'>
<div className='settings'> <div className='settings'>
<Dropdown <Dropdown
disabled={!me.canChangeWebcam}
options={webcams} options={webcams}
value={findOption(webcams, me.selectedWebcam)} value={findOption(webcams, me.selectedWebcam)}
onChange={(webcam) => handleChangeWebcam(webcam.value)} onChange={(webcam) => handleChangeWebcam(webcam.value)}
placeholder={webcamText} placeholder={'Select camera'}
/> />
<Dropdown <Dropdown
disabled={!me.canChangeAudioDevice} disabled={!me.canChangeAudioDevice}
options={audioDevices} options={audioDevices}
@ -65,20 +59,34 @@ const Settings = ({
onChange={(device) => handleChangeAudioDevice(device.value)} onChange={(device) => handleChangeAudioDevice(device.value)}
placeholder={audioDevicesText} placeholder={audioDevicesText}
/> />
<ReactTooltip
<input effect='solid'
id='room-mode'
type='checkbox'
checked={room.advancedMode}
onChange={onToggleAdvancedMode}
/> />
<label htmlFor='room-mode'>Advanced mode</label> <div
data-tip='keyboard shortcut: &lsquo;a&lsquo;'
data-type='dark'
data-place='left'
>
<input
id='room-mode'
type='checkbox'
checked={room.advancedMode}
onChange={onToggleAdvancedMode}
/>
<label htmlFor='room-mode'>Advanced mode</label>
</div>
<Dropdown <div
options={modes} data-tip='keyboard shortcut: type a digit'
value={findOption(modes, room.mode)} data-type='dark'
onChange={(mode) => handleChangeMode(mode.value)} data-place='left'
/> >
<Dropdown
options={modes}
value={findOption(modes, room.mode)}
onChange={(mode) => handleChangeMode(mode.value)}
/>
</div>
</div> </div>
</div> </div>
); );

View File

@ -4,46 +4,52 @@ import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import fscreen from 'fscreen'; import FullScreen from './FullScreen';
class Sidebar extends Component class Sidebar extends Component
{ {
state = { constructor(props)
fullscreen : false {
}; super(props);
this.fullscreen = new FullScreen(document);
this.state = {
fullscreen : false
};
}
handleToggleFullscreen = () => handleToggleFullscreen = () =>
{ {
if (fscreen.fullscreenElement) if (this.fullscreen.fullscreenElement)
{ {
fscreen.exitFullscreen(); this.fullscreen.exitFullscreen();
} }
else else
{ {
fscreen.requestFullscreen(document.documentElement); this.fullscreen.requestFullscreen(document.documentElement);
} }
}; };
handleFullscreenChange = () => handleFullscreenChange = () =>
{ {
this.setState({ this.setState({
fullscreen : fscreen.fullscreenElement !== null fullscreen : this.fullscreen.fullscreenElement !== null
}); });
}; };
componentDidMount() componentDidMount()
{ {
if (fscreen.fullscreenEnabled) if (this.fullscreen.fullscreenEnabled)
{ {
fscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
} }
} }
componentWillUnmount() componentWillUnmount()
{ {
if (fscreen.fullscreenEnabled) if (this.fullscreen.fullscreenEnabled)
{ {
fscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
} }
} }
@ -85,13 +91,14 @@ class Sidebar extends Component
})} })}
data-component='Sidebar' data-component='Sidebar'
> >
{fscreen.fullscreenEnabled && ( {this.fullscreen.fullscreenEnabled && (
<div <div
className={classnames('button', 'fullscreen', { className={classnames('button', 'fullscreen', {
on : this.state.fullscreen on : this.state.fullscreen
})} })}
onClick={this.handleToggleFullscreen} onClick={this.handleToggleFullscreen}
data-tip='Fullscreen' data-tip='Fullscreen'
data-place='right'
data-type='dark' data-type='dark'
/> />
)} )}
@ -99,6 +106,7 @@ class Sidebar extends Component
<div <div
className={classnames('button', 'screen', screenState)} className={classnames('button', 'screen', screenState)}
data-tip={screenTip} data-tip={screenTip}
data-place='right'
data-type='dark' data-type='dark'
onClick={() => onClick={() =>
{ {
@ -131,6 +139,7 @@ class Sidebar extends Component
<div <div
className='button logout' className='button logout'
data-tip='Logout' data-tip='Logout'
data-place='right'
data-type='dark' data-type='dark'
onClick={onLogout} onClick={onLogout}
> >
@ -140,6 +149,7 @@ class Sidebar extends Component
<div <div
className='button login off' className='button login off'
data-tip='Login' data-tip='Login'
data-place='right'
data-type='dark' data-type='dark'
onClick={onLogin} onClick={onLogin}
/> />
@ -150,6 +160,7 @@ class Sidebar extends Component
disabled : me.raiseHandInProgress disabled : me.raiseHandInProgress
})} })}
data-tip='Raise hand' data-tip='Raise hand'
data-place='right'
data-type='dark' data-type='dark'
onClick={() => onToggleHand(!me.raiseHand)} onClick={() => onToggleHand(!me.raiseHand)}
/> />
@ -157,6 +168,7 @@ class Sidebar extends Component
<div <div
className={classnames('button', 'leave-meeting')} className={classnames('button', 'leave-meeting')}
data-tip='Leave meeting' data-tip='Leave meeting'
data-place='right'
data-type='dark' data-type='dark'
onClick={() => onLeaveMeeting()} onClick={() => onLeaveMeeting()}
/> />

View File

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import * as stateActions from '../../redux/stateActions';
const TabHeader = ({ currentToolTab, setToolTab, id, name, badge }) => (
<div
className={classNames('tab-header', {
checked : currentToolTab === id
})}
onClick={() => setToolTab(id)}
>
{name}
{badge > 0 && (
<span className='badge'>{badge}</span>
)}
</div>
);
TabHeader.propTypes = {
currentToolTab : PropTypes.string.isRequired,
setToolTab : PropTypes.func.isRequired,
id : PropTypes.string.isRequired,
name : PropTypes.string.isRequired,
badge : PropTypes.number
};
const mapStateToProps = (state) => ({
currentToolTab : state.toolarea.currentToolTab
});
const mapDispatchToProps = {
setToolTab : stateActions.setToolTab
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TabHeader);

View File

@ -1,11 +1,13 @@
import React from 'react'; import React, { Fragment } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as toolTabActions from '../../redux/stateActions'; import classNames from 'classnames';
import * as stateActions from '../../redux/stateActions';
import ParticipantList from '../ParticipantList/ParticipantList'; import ParticipantList from '../ParticipantList/ParticipantList';
import Chat from '../Chat/Chat'; import Chat from '../Chat/Chat';
import Settings from '../Settings'; import Settings from '../Settings';
import FileSharing from '../FileSharing'; import FileSharing from '../FileSharing';
import TabHeader from './TabHeader';
class ToolArea extends React.Component class ToolArea extends React.Component
{ {
@ -18,88 +20,80 @@ class ToolArea extends React.Component
{ {
const { const {
currentToolTab, currentToolTab,
toolAreaOpen,
unreadMessages, unreadMessages,
unreadFiles, unreadFiles,
setToolTab toggleToolArea,
unread
} = this.props; } = this.props;
const VisibleTab = {
chat : Chat,
files : FileSharing,
users : ParticipantList,
settings : Settings
}[currentToolTab];
return ( return (
<div data-component='ToolArea'> <Fragment>
<div className='tabs'> <div
<input className={classNames('toolarea-shade', {
type='radio' open : toolAreaOpen
name='tabs' })}
id='tab-chat' onClick={toggleToolArea}
onChange={() => />
{
setToolTab('chat'); <div
}} data-component='ToolArea'
checked={currentToolTab === 'chat'} className={classNames({
/> open : toolAreaOpen
<label htmlFor='tab-chat'> })}
Chat >
<div
{unreadMessages > 0 && ( className='toolarea-button'
<span className='badge'>{unreadMessages}</span> onClick={toggleToolArea}
>
<span className='content'>
<div
className='toolarea-icon'
/>
<p>Toolbox</p>
</span>
{!toolAreaOpen && unread > 0 && (
<span className={classNames('badge', { long: unread >= 10 })}>
{unread}
</span>
)} )}
</label> </div>
<div className='tab-headers'>
<TabHeader
id='chat'
name='Chat'
badge={unreadMessages}
/>
<div className='tab'> <TabHeader
<Chat /> id='files'
name='Files'
badge={unreadFiles}
/>
<TabHeader
id='users'
name='Users'
/>
<TabHeader
id='settings'
name='Settings'
/>
</div> </div>
<input
type='radio'
name='tabs'
id='tab-files'
onChange={() => setToolTab('files')}
checked={currentToolTab === 'files'}
/>
<label htmlFor='tab-files'>
Files
{unreadFiles > 0 && (
<span className='badge'>{unreadFiles}</span>
)}
</label>
<div className='tab'> <div className='tab'>
<FileSharing /> <VisibleTab />
</div>
<input
type='radio'
name='tabs'
id='tab-users'
onChange={() =>
{
setToolTab('users');
}}
checked={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={currentToolTab === 'settings'}
/>
<label htmlFor='tab-settings'>Settings</label>
<div className='tab'>
<Settings />
</div> </div>
</div> </div>
</div> </Fragment>
); );
} }
} }
@ -110,17 +104,26 @@ ToolArea.propTypes =
currentToolTab : PropTypes.string.isRequired, currentToolTab : PropTypes.string.isRequired,
setToolTab : PropTypes.func.isRequired, setToolTab : PropTypes.func.isRequired,
unreadMessages : PropTypes.number.isRequired, unreadMessages : PropTypes.number.isRequired,
unreadFiles : PropTypes.number.isRequired unreadFiles : PropTypes.number.isRequired,
toolAreaOpen : PropTypes.bool,
toggleToolArea : PropTypes.func.isRequired,
closeToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
currentToolTab : state.toolarea.currentToolTab, currentToolTab : state.toolarea.currentToolTab,
unreadMessages : state.toolarea.unreadMessages, unreadMessages : state.toolarea.unreadMessages,
unreadFiles : state.toolarea.unreadFiles unreadFiles : state.toolarea.unreadFiles,
toolAreaOpen : state.toolarea.toolAreaOpen,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
setToolTab : toolTabActions.setToolTab setToolTab : stateActions.setToolTab,
toggleToolArea : stateActions.toggleToolArea,
closeToolArea : stateActions.closeToolArea
}; };
const ToolAreaContainer = connect( const ToolAreaContainer = connect(

View File

@ -11,19 +11,24 @@ class ToolAreaButton extends React.Component
const { const {
toolAreaOpen, toolAreaOpen,
toggleToolArea, toggleToolArea,
unread unread,
visible
} = this.props; } = this.props;
return ( return (
<div data-component='ToolAreaButton' className={classnames({ on: toolAreaOpen })}> <div
data-component='ToolAreaButton'
className={classnames('room-controls', {
on : toolAreaOpen,
visible
})}
>
<div <div
className={classnames('button toolarea-button room-controls', { className={classnames('button toolarea-button', {
on : toolAreaOpen, on : toolAreaOpen
visible : this.props.visible
})} })}
data-tip='Toggle tool area' data-tip='Open tools'
data-type='dark' data-type='dark'
data-for='globaltip'
onClick={() => toggleToolArea()} onClick={() => toggleToolArea()}
/> />

View File

@ -0,0 +1,286 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import FullScreen from '../FullScreen';
import classnames from 'classnames';
class NewWindow extends React.PureComponent
{
static defaultProps =
{
url : '',
name : '',
title : '',
features : { width: '800px', height: '600px' },
onBlock : null,
onUnload : null,
center : 'parent',
copyStyles : true
};
handleToggleFullscreen = () =>
{
if (this.fullscreen.fullscreenElement)
{
this.fullscreen.exitFullscreen();
}
else
{
this.fullscreen.requestFullscreen(this.window.document.documentElement);
}
};
handleFullscreenChange = () =>
{
this.setState({
fullscreen : this.fullscreen.fullscreenElement !== null
});
};
constructor(props)
{
super(props);
this.container = document.createElement('div');
this.window = null;
this.windowCheckerInterval = null;
this.released = false;
this.fullscreen = null;
this.state = {
mounted : false,
fullscreen : false
};
}
render()
{
if (!this.state.mounted)
return null;
return ReactDOM.createPortal([
<div key='newwindow' data-component='FullScreenView'>
<div className='controls'>
{this.fullscreen.fullscreenEnabled && (
<div
className={classnames('button', {
fullscreen : !this.state.fullscreen,
exitFullscreen : this.state.fullscreen
})}
onClick={this.handleToggleFullscreen}
data-tip='Fullscreen'
data-place='right'
data-type='dark'
/>
)}
</div>
{this.props.children}
</div>
], this.container);
}
componentDidMount()
{
this.openChild();
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({ mounted: true });
this.fullscreen = new FullScreen(this.window.document);
if (this.fullscreen.fullscreenEnabled)
{
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
}
}
openChild()
{
const {
url,
title,
name,
features,
onBlock,
center
} = this.props;
if (center === 'parent')
{
features.left =
(window.top.outerWidth / 2) + window.top.screenX - (features.width / 2);
features.top =
(window.top.outerHeight / 2) + window.top.screenY - (features.height / 2);
}
else if (center === 'screen')
{
const screenLeft =
window.screenLeft !== undefined ? window.screenLeft : screen.left;
const screenTop =
window.screenTop !== undefined ? window.screenTop : screen.top;
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width;
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height;
features.left = (width / 2) - (features.width / 2) + screenLeft;
features.top = (height / 2) - (features.height / 2) + screenTop;
}
this.window = window.open(url, name, toWindowFeatures(features));
this.windowCheckerInterval = setInterval(() =>
{
if (!this.window || this.window.closed)
{
this.release();
}
}, 50);
if (this.window)
{
this.window.document.title = title;
this.window.document.body.appendChild(this.container);
if (this.props.copyStyles)
{
setTimeout(() => copyStyles(document, this.window.document), 0);
}
this.window.addEventListener('beforeunload', () => this.release());
}
else if (typeof onBlock === 'function')
{
onBlock(null);
}
}
componentWillUnmount()
{
if (this.window)
{
if (this.fullscreen && this.fullscreen.fullscreenEnabled)
{
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
}
this.window.close();
}
}
release()
{
if (this.released)
{
return;
}
this.released = true;
clearInterval(this.windowCheckerInterval);
const { onUnload } = this.props;
if (typeof onUnload === 'function')
{
onUnload(null);
}
}
}
NewWindow.propTypes = {
children : PropTypes.node,
url : PropTypes.string,
name : PropTypes.string,
title : PropTypes.string,
features : PropTypes.object,
onUnload : PropTypes.func,
onBlock : PropTypes.func,
center : PropTypes.oneOf([ 'parent', 'screen' ]),
copyStyles : PropTypes.bool
};
function copyStyles(source, target)
{
Array.from(source.styleSheets).forEach((styleSheet) =>
{
let rules;
try
{
rules = styleSheet.cssRules;
}
catch (err) {}
if (rules)
{
const newStyleEl = source.createElement('style');
Array.from(styleSheet.cssRules).forEach((cssRule) =>
{
const { cssText, type } = cssRule;
let returnText = cssText;
if ([ 3, 5 ].includes(type))
{
returnText = cssText
.split('url(')
.map((line) =>
{
if (line[1] === '/')
{
return `${line.slice(0, 1)}${
window.location.origin
}${line.slice(1)}`;
}
return line;
})
.join('url(');
}
newStyleEl.appendChild(source.createTextNode(returnText));
});
target.head.appendChild(newStyleEl);
}
else if (styleSheet.href)
{
const newLinkEl = source.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
target.head.appendChild(newLinkEl);
}
});
}
function toWindowFeatures(obj)
{
return Object.keys(obj)
.reduce((features, name) =>
{
const value = obj[name];
if (typeof value === 'boolean')
{
features.push(`${name}=${value ? 'yes' : 'no'}`);
}
else
{
features.push(`${name}=${value}`);
}
return features;
}, [])
.join(',');
}
export default NewWindow;

View File

@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import NewWindow from './NewWindow';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../redux/stateActions';
import FullView from '../FullView';
const VideoWindow = (props) =>
{
const {
advancedMode,
consumer,
toggleConsumerWindow
} = props;
if (!consumer)
return null;
const consumerVisible = (
Boolean(consumer) &&
!consumer.locallyPaused &&
!consumer.remotelyPaused
);
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return (
<NewWindow onUnload={toggleConsumerWindow}>
<FullView
advancedMode={advancedMode}
videoTrack={consumer ? consumer.track : null}
videoVisible={consumerVisible}
videoProfile={consumerProfile}
/>
</NewWindow>
);
};
VideoWindow.propTypes =
{
advancedMode : PropTypes.bool,
consumer : appPropTypes.Consumer,
toggleConsumerWindow : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
consumer : state.consumers[state.room.windowConsumer]
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleConsumerWindow : () =>
{
dispatch(stateActions.toggleConsumerWindow());
}
};
};
const VideoWindowContainer = connect(
mapStateToProps,
mapDispatchToProps
)(VideoWindow);
export default VideoWindowContainer;

View File

@ -23,7 +23,6 @@ export const Me = PropTypes.shape(
device : Device.isRequired, device : Device.isRequired,
canSendMic : PropTypes.bool.isRequired, canSendMic : PropTypes.bool.isRequired,
canSendWebcam : PropTypes.bool.isRequired, canSendWebcam : PropTypes.bool.isRequired,
canChangeWebcam : PropTypes.bool.isRequired,
webcamInProgress : PropTypes.bool.isRequired, webcamInProgress : PropTypes.bool.isRequired,
audioOnly : PropTypes.bool.isRequired, audioOnly : PropTypes.bool.isRequired,
audioOnlyInProgress : PropTypes.bool.isRequired, audioOnlyInProgress : PropTypes.bool.isRequired,

View File

@ -22,3 +22,13 @@ export function setDevices({ webcamEnabled })
{ {
jsCookie.set(DEVICES_COOKIE, { webcamEnabled }); jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
} }
export function setAudioDevice({ audioDeviceId })
{
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
}
export function setVideoDevice({ videoDeviceId })
{
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
}

View File

@ -32,7 +32,7 @@ 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.pathname).substr(1) let roomId = (urlParser.pathname).substr(1)
? (urlParser.pathname).substr(1) : urlParser.query.roomId; ? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
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';

View File

@ -18,7 +18,6 @@
device : { flag: 'firefox', name: 'Firefox', version: '61' }, device : { flag: 'firefox', name: 'Firefox', version: '61' },
canSendMic : true, canSendMic : true,
canSendWebcam : true, canSendWebcam : true,
canChangeWebcam : false,
webcamInProgress : false, webcamInProgress : false,
audioOnly : false, audioOnly : false,
audioOnlyInProgress : false, audioOnlyInProgress : false,

View File

@ -6,9 +6,11 @@ const initialState =
showSettings : false, showSettings : false,
advancedMode : false, advancedMode : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
windowConsumer : null, // ConsumerID
toolbarsVisible : true, toolbarsVisible : true,
mode : 'democratic', mode : 'democratic',
selectedPeerName : null selectedPeerName : null,
spotlights : []
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -61,6 +63,17 @@ const room = (state = initialState, action) =>
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId }; return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
} }
case 'TOGGLE_WINDOW_CONSUMER':
{
const { consumerId } = action.payload;
const currentConsumer = state.windowConsumer;
if (currentConsumer === consumerId)
return { ...state, windowConsumer: null };
else
return { ...state, windowConsumer: consumerId };
}
case 'SET_TOOLBARS_VISIBLE': case 'SET_TOOLBARS_VISIBLE':
{ {
const { toolbarsVisible } = action.payload; const { toolbarsVisible } = action.payload;
@ -83,6 +96,13 @@ const room = (state = initialState, action) =>
}; };
} }
case 'SET_SPOTLIGHTS':
{
const { spotlights } = action.payload;
return { ...state, spotlights };
}
default: default:
return state; return state;
} }

View File

@ -19,6 +19,22 @@ const toolarea = (state = initialState, action) =>
return { ...state, toolAreaOpen, unreadMessages, unreadFiles }; return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
} }
case 'OPEN_TOOL_AREA':
{
const toolAreaOpen = true;
const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
const unreadFiles = state.currentToolTab === 'files' ? 0 : state.unreadFiles;
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
}
case 'CLOSE_TOOL_AREA':
{
const toolAreaOpen = false;
return { ...state, toolAreaOpen };
}
case 'SET_TOOL_TAB': case 'SET_TOOL_TAB':
{ {
const { toolTab } = action.payload; const { toolTab } = action.payload;
@ -30,7 +46,7 @@ const toolarea = (state = initialState, action) =>
case 'ADD_NEW_RESPONSE_MESSAGE': case 'ADD_NEW_RESPONSE_MESSAGE':
{ {
if (state.toolAreaOpen && state.currentToolTab === 'chat') if (state.toolAreaOpen && state.currentToolTab === 'chat')
{ {
return state; return state;
} }

View File

@ -221,6 +221,14 @@ export const sendFile = (file, name, picture) =>
}; };
}; };
export const setSelectedPeer = (selectedPeerName) =>
{
return {
type : 'REQUEST_SELECTED_PEER',
payload : { selectedPeerName }
};
};
// This returns a redux-thunk action (a function). // This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) => export const notify = ({ type = 'info', text, timeout }) =>
{ {

View File

@ -237,6 +237,15 @@ export default ({ dispatch, getState }) => (next) =>
client.sendFile(action.payload); client.sendFile(action.payload);
break; break;
} }
case 'REQUEST_SELECTED_PEER':
{
const { selectedPeerName } = action.payload;
client.setSelectedPeer(selectedPeerName);
break;
}
} }
return next(action); return next(action);

View File

@ -62,14 +62,6 @@ export const setAudioDevices = (devices) =>
}; };
}; };
export const setCanChangeWebcam = (flag) =>
{
return {
type : 'SET_CAN_CHANGE_WEBCAM',
payload : flag
};
};
export const setWebcamDevices = (devices) => export const setWebcamDevices = (devices) =>
{ {
return { return {
@ -161,6 +153,20 @@ export const toggleToolArea = () =>
}; };
}; };
export const openToolArea = () =>
{
return {
type : 'OPEN_TOOL_AREA'
};
};
export const closeToolArea = () =>
{
return {
type : 'CLOSE_TOOL_AREA'
};
};
export const setToolTab = (toolTab) => export const setToolTab = (toolTab) =>
{ {
return { return {
@ -383,6 +389,14 @@ export const toggleConsumerFullscreen = (consumerId) =>
}; };
}; };
export const toggleConsumerWindow = (consumerId) =>
{
return {
type : 'TOGGLE_WINDOW_CONSUMER',
payload : { consumerId }
};
};
export const setToolbarsVisible = (toolbarsVisible) => ({ export const setToolbarsVisible = (toolbarsVisible) => ({
type : 'SET_TOOLBARS_VISIBLE', type : 'SET_TOOLBARS_VISIBLE',
payload : { toolbarsVisible } payload : { toolbarsVisible }
@ -474,7 +488,14 @@ export const loggedIn = () =>
type : 'LOGGED_IN' type : 'LOGGED_IN'
}); });
export const setSelectedPeer = (selectedPeerName) => ({ export const setSelectedPeer = (selectedPeerName) =>
type : 'SET_SELECTED_PEER', ({
payload : { selectedPeerName } type : 'SET_SELECTED_PEER',
}); payload : { selectedPeerName }
});
export const setSpotlights = (spotlights) =>
({
type : 'SET_SPOTLIGHTS',
payload : { spotlights }
});

View File

@ -18,6 +18,9 @@ if (process.env.NODE_ENV === 'development')
{ {
const reduxLogger = createLogger( const reduxLogger = createLogger(
{ {
// filter VOLUME level actions from log
predicate : (getState, action) => ! (action.type == 'SET_PRODUCER_VOLUME'
|| action.type == 'SET_CONSUMER_VOLUME'),
duration : true, duration : true,
timestamp : false, timestamp : false,
level : 'log', level : 'log',
@ -43,4 +46,4 @@ export const store = createStore(
reducers, reducers,
undefined, undefined,
enhancer enhancer
); );

View File

@ -1,4 +1,4 @@
export function getProtooUrl(peerName, roomId) export function getSignalingUrl(peerName, roomId)
{ {
const hostname = window.location.hostname; const hostname = window.location.hostname;
const port = window.location.port; const port = window.location.port;

12527
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,54 +7,56 @@
"license": "MIT", "license": "MIT",
"main": "lib/index.jsx", "main": "lib/index.jsx",
"dependencies": { "dependencies": {
"babel-runtime": "^6.26.0", "@babel/runtime": "^7.1.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"create-torrent": "^3.32.1", "create-torrent": "^3.32.1",
"debug": "^3.1.0", "debug": "^4.1.0",
"domready": "^1.0.8", "domready": "^1.0.8",
"drag-drop": "^4.2.0", "drag-drop": "^4.2.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"fscreen": "^1.0.2",
"hark": "^1.2.2", "hark": "^1.2.2",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"magnet-uri": "^5.2.3", "magnet-uri": "^5.2.3",
"marked": "^0.4.0", "marked": "^0.5.1",
"mediasoup-client": "^2.1.1", "mediasoup-client": "^2.3.2",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"protoo-client": "^3.0.0",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^16.4.1", "react": "^16.5.2",
"react-cookie-consent": "^1.9.0",
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.4.1", "react-dom": "^16.5.2",
"react-draggable": "^3.0.5", "react-draggable": "^3.0.5",
"react-dropdown": "^1.5.0", "react-dropdown": "^1.5.0",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"react-spinner": "^0.2.7", "react-spinner": "^0.2.7",
"react-tooltip": "^3.6.1", "react-tooltip": "^3.9.0",
"react-transition-group": "^2.4.0", "react-transition-group": "^2.5.0",
"redux": "^4.0.0", "redux": "^4.0.1",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"resize-observer-polyfill": "^1.5.0", "resize-observer-polyfill": "^1.5.0",
"riek": "^1.1.0", "riek": "^1.1.0",
"url-parse": "^1.4.1", "socket.io-client": "^2.1.1",
"webtorrent": "^0.101.0" "url-parse": "^1.4.3",
"webtorrent": "^0.102.4"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.3", "@babel/core": "^7.1.2",
"babel-eslint": "^8.2.6", "@babel/plugin-proposal-class-properties": "^7.1.0",
"babel-preset-env": "^1.7.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"babel-preset-es2015": "^6.24.1", "@babel/plugin-transform-runtime": "^7.1.0",
"babel-preset-react-app": "^3.1.2", "@babel/preset-env": "^7.1.0",
"babel-preset-stage-0": "^6.24.1", "@babel/preset-react": "^7.0.0",
"babelify": "^8.0.0", "babel-eslint": "^10.0.1",
"browser-sync": "^2.24.6", "babelify": "^10.0.0",
"browserify": "^16.2.2", "browser-sync": "^2.26.3",
"browserify": "^16.2.3",
"del": "^3.0.0", "del": "^3.0.0",
"envify": "^4.1.0", "envify": "^4.1.0",
"eslint": "^5.2.0", "eslint": "^5.7.0",
"eslint-plugin-import": "^2.13.0", "eslint-plugin-import": "^2.14.0",
"eslint-plugin-react": "^7.10.0", "eslint-plugin-jsx-control-statements": "^2.2.1",
"eslint-plugin-react": "^7.11.1",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-change": "^1.0.0", "gulp-change": "^1.0.0",
"gulp-css-base64": "^1.3.4", "gulp-css-base64": "^1.3.4",
@ -65,13 +67,13 @@
"gulp-rename": "^1.4.0", "gulp-rename": "^1.4.0",
"gulp-stylus": "^2.7.0", "gulp-stylus": "^2.7.0",
"gulp-touch-cmd": "0.0.1", "gulp-touch-cmd": "0.0.1",
"gulp-uglify": "^3.0.0", "gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nib": "^1.1.2", "nib": "^1.1.2",
"supports-color": "^5.4.0", "supports-color": "^5.5.0",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"watchify": "^3.11.0" "watchify": "^3.11.0"

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 256 256" height="256px" id="Layer_1" version="1.1" viewBox="0 0 256 256" width="256px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M76.8,217.6c0-1.637,0.625-3.274,1.875-4.524L163.75,128L78.675,42.925c-2.5-2.5-2.5-6.55,0-9.05s6.55-2.5,9.05,0 l89.601,89.6c2.5,2.5,2.5,6.551,0,9.051l-89.601,89.6c-2.5,2.5-6.55,2.5-9.05,0C77.425,220.875,76.8,219.237,76.8,217.6z"/></svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.2" width="67.32mm" height="67.32mm" viewBox="4682 4809 6732 6732" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" xmlns:ooo="http://xml.openoffice.org/svg/export" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:presentation="http://sun.com/xmlns/staroffice/presentation" xmlns:smil="http://www.w3.org/2001/SMIL20/" xmlns:anim="urn:oasis:names:tc:opendocument:xmlns:animation:1.0" xml:space="preserve">
<defs class="ClipPathGroup">
<clipPath id="presentation_clip_path" clipPathUnits="userSpaceOnUse">
<rect x="4682" y="4809" width="6732" height="6732"/>
</clipPath>
<clipPath id="presentation_clip_path_shrink" clipPathUnits="userSpaceOnUse">
<rect x="4688" y="4815" width="6719" height="6719"/>
</clipPath>
</defs>
<defs class="TextShapeIndex">
<g ooo:slide="id1" ooo:id-list="id3 id4 id5"/>
</defs>
<defs class="EmbeddedBulletChars">
<g id="bullet-char-template(57356)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 580,1141 L 1163,571 580,0 -4,571 580,1141 Z"/>
</g>
<g id="bullet-char-template(57354)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 8,1128 L 1137,1128 1137,0 8,0 8,1128 Z"/>
</g>
<g id="bullet-char-template(10146)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 174,0 L 602,739 174,1481 1456,739 174,0 Z M 1358,739 L 309,1346 659,739 1358,739 Z"/>
</g>
<g id="bullet-char-template(10132)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 2015,739 L 1276,0 717,0 1260,543 174,543 174,936 1260,936 717,1481 1274,1481 2015,739 Z"/>
</g>
<g id="bullet-char-template(10007)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 0,-2 C -7,14 -16,27 -25,37 L 356,567 C 262,823 215,952 215,954 215,979 228,992 255,992 264,992 276,990 289,987 310,991 331,999 354,1012 L 381,999 492,748 772,1049 836,1024 860,1049 C 881,1039 901,1025 922,1006 886,937 835,863 770,784 769,783 710,716 594,584 L 774,223 C 774,196 753,168 711,139 L 727,119 C 717,90 699,76 672,76 641,76 570,178 457,381 L 164,-76 C 142,-110 111,-127 72,-127 30,-127 9,-110 8,-76 1,-67 -2,-52 -2,-32 -2,-23 -1,-13 0,-2 Z"/>
</g>
<g id="bullet-char-template(10004)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 285,-33 C 182,-33 111,30 74,156 52,228 41,333 41,471 41,549 55,616 82,672 116,743 169,778 240,778 293,778 328,747 346,684 L 369,508 C 377,444 397,411 428,410 L 1163,1116 C 1174,1127 1196,1133 1229,1133 1271,1133 1292,1118 1292,1087 L 1292,965 C 1292,929 1282,901 1262,881 L 442,47 C 390,-6 338,-33 285,-33 Z"/>
</g>
<g id="bullet-char-template(9679)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 813,0 C 632,0 489,54 383,161 276,268 223,411 223,592 223,773 276,916 383,1023 489,1130 632,1184 813,1184 992,1184 1136,1130 1245,1023 1353,916 1407,772 1407,592 1407,412 1353,268 1245,161 1136,54 992,0 813,0 Z"/>
</g>
<g id="bullet-char-template(8226)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 346,457 C 273,457 209,483 155,535 101,586 74,649 74,723 74,796 101,859 155,911 209,963 273,989 346,989 419,989 480,963 531,910 582,859 608,796 608,723 608,648 583,586 532,535 482,483 420,457 346,457 Z"/>
</g>
<g id="bullet-char-template(8211)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M -4,459 L 1135,459 1135,606 -4,606 -4,459 Z"/>
</g>
<g id="bullet-char-template(61548)" transform="scale(0.00048828125,-0.00048828125)">
<path d="M 173,740 C 173,903 231,1043 346,1159 462,1274 601,1332 765,1332 928,1332 1067,1274 1183,1159 1299,1043 1357,903 1357,740 1357,577 1299,437 1183,322 1067,206 928,148 765,148 601,148 462,206 346,322 231,437 173,577 173,740 Z"/>
</g>
</defs>
<defs class="TextEmbeddedBitmaps"/>
<g class="SlideGroup">
<g>
<g id="container-id1">
<g id="id1" class="Slide" clip-path="url(#presentation_clip_path)">
<g class="Page">
<g class="com.sun.star.drawing.LineShape">
<g id="id3">
<rect class="BoundingBox" stroke="none" fill="none" x="6253" y="6380" width="3525" height="3525"/>
<path fill="none" stroke="rgb(204,0,0)" stroke-width="1016" stroke-linejoin="round" stroke-linecap="round" d="M 6761,9395 L 9268,6888"/>
</g>
</g>
<g class="com.sun.star.drawing.LineShape">
<g id="id4">
<rect class="BoundingBox" stroke="none" fill="none" x="6253" y="6398" width="3525" height="3526"/>
<path fill="none" stroke="rgb(204,0,0)" stroke-width="1016" stroke-linejoin="round" stroke-linecap="round" d="M 9269,9414 L 6762,6907"/>
</g>
</g>
<g class="com.sun.star.drawing.CustomShape">
<g id="id5">
<rect class="BoundingBox" stroke="none" fill="none" x="4681" y="4808" width="6734" height="6734"/>
<path fill="none" stroke="rgb(204,0,0)" stroke-width="508" stroke-linejoin="round" d="M 8047,5063 C 9811,5063 11159,6410 11159,8174 11159,9938 9811,11286 8047,11286 6283,11286 4936,9938 4936,8174 4936,6410 6283,5063 8047,5063 Z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g>
<path
d="M23.78700473 7.7610694C23.78700473 7.7610694 19.32738673 2.3867608 19.32738673 2.3867608C19.13984773 2.1607585 18.91713373 2.0882379 18.63693973 2.2176245C18.36703673 2.3422016 18.22715573 2.5841415 18.22715573 2.9368427C18.22715573 2.9368427 18.22715573 5.6735307 18.22715573 5.6735307C16.60026573 6.0262319 15.09099273 6.6909889 13.72054573 7.6906705C12.03598073 8.9196554 10.69300173 10.4408914 9.74082913 12.3174694C9.56878473 12.6565904 9.61472063 13.0060384 9.85793833 13.2765524C10.07145773 13.5142954 10.41981173 13.5447094 10.65379573 13.3611024C10.66553573 13.3469524 10.66553573 13.3469524 10.67727573 13.3469524C10.84110373 13.2202534 11.00363973 13.1043994 11.15725573 13.0083964C11.31983073 12.9069714 11.66067273 12.7122784 12.18733073 12.4444524C12.71395073 12.1762954 13.21736773 11.9365244 13.72058873 11.7532424C14.22400573 11.5696774 14.85607473 11.3852164 15.59343573 11.2311224C16.34257473 11.0749524 17.07997473 10.9913044 17.79417273 10.9913044C17.79417273 10.9913044 18.22715973 10.9913044 18.22715973 10.9913044C18.22715973 10.9913044 18.22715973 13.6574994 18.22715973 13.6574994C18.22715973 14.0102004 18.36723673 14.2509614 18.63694373 14.3767174C18.73010373 14.4203774 18.81250973 14.4332974 18.87092773 14.4332974C19.04645473 14.4332974 19.19826973 14.3622874 19.32739073 14.2076254C19.32739073 14.2076254 23.78708673 8.8613254 23.78708673 8.8613254C24.03351273 8.5660564 24.03300473 8.0574684 23.78700873 7.7610674C23.78700873 7.7610674 23.78700873 7.7610674 23.78700873 7.7610674M19.51465573 11.7673384C19.51465573 11.7673384 19.51465573 10.2720364 19.51465573 10.2720364C19.51465573 9.8630814 19.26897373 9.5103804 18.94108373 9.4962344C18.68362273 9.4537944 18.29731573 9.4396544 17.79417273 9.4396544C16.12032873 9.4396544 14.43478673 9.7782104 12.76090373 10.4694194C14.54011773 8.6497074 16.61200773 7.5365769 18.96455973 7.1263967C19.28071173 7.0713227 19.51473473 6.7454039 19.51473473 6.3364497C19.51473473 6.3364497 19.51473473 4.8128558 19.51473473 4.8128558C19.51473473 4.8128558 22.41761773 8.3111524 22.41761773 8.3111524C22.41761773 8.3111524 19.51465573 11.7673414 19.51465573 11.7673414" />
<path
d="M17.17360373 20.0049884C17.17360373 20.0615284 17.10341373 20.1461164 17.04499073 20.1461164C17.04499073 20.1461164 1.44180703 20.1461164 1.44180703 20.1461164C1.35991303 20.1461164 1.32469783 20.1036764 1.32469783 20.0049884C1.32469783 20.0049884 1.32469783 6.3082043 1.32469783 6.3082043C1.32469783 6.2095143 1.35991283 6.1670295 1.44180703 6.1670295C1.44180703 6.1670295 14.34107173 6.1670295 14.34107173 6.1670295C14.34107173 6.1670295 14.34107173 4.5871826 14.34107173 4.5871826C14.34107173 4.5871826 1.44180703 4.5871826 1.44180703 4.5871826C0.70440653 4.5871826 0.10735711 5.2784393 0.02542373 6.139068C0.02542373 6.139068 0.02542373 6.3082043 0.02542373 6.3082043C0.02542373 6.3082043 0.02542373 20.0050354 0.02542373 20.0050354C0.02542373 20.0050354 0.02542373 20.1744544 0.02542373 20.1744544C0.10731799 21.0348004 0.70440653 21.7118644 1.44180703 21.7118644C1.44180703 21.7118644 17.04499073 21.7118644 17.04499073 21.7118644C17.44280173 21.7118644 17.78516873 21.5460284 18.05162873 21.2180354C18.32380073 20.8830634 18.46141273 20.4705254 18.46141273 20.0049884C18.46141273 20.0049884 18.46141273 15.3501814 18.46141273 15.3501814C18.46141273 15.3501814 17.17364273 15.3501814 17.17364273 15.3501814C17.17364273 15.3501814 17.17364273 20.0049884 17.17364273 20.0049884C17.17364273 20.0049884 17.17360373 20.0049884 17.17360373 20.0049884" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

View File

@ -0,0 +1,6 @@
[data-component='AudioPeers'] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
}

View File

@ -1,76 +1,22 @@
[data-component='ChatWidget'] {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
margin: 0 10px 10px 0;
max-width: 300px;
right: 0;
width: 90vw;
z-index: 100;
> .launcher {
align-self: flex-end;
margin-top: 10px;
background-position: center;
background-size: 70%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
background-image: url('/resources/images/chat-icon.svg');
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
height: 45px;
width: 45px;
position: relative;
&.focus {
outline: none;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.disabled {
pointer-events: none;
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;
}
}
}
[data-component='Conversation'] {
border-radius: 5px;
box-shadow: 0px 2px 10px 1px #000;
}
[data-component='Chat'] { [data-component='Chat'] {
height: 100%; height: 100%;
display: flex;
flex-grow: 1;
flex-direction: column;
} }
[data-component='MessageList'] { [data-component='MessageList'] {
background-color: rgba(#000, 0.1); overflow-y: auto;
height: 91vmin; flex-grow: 1;
overflow-y: scroll;
padding-top: 5px;
border-radius: 5px 5px 0px 0px;
> .message { > .message {
margin: 5px;
display: flex; display: flex;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all;
&:not(:first-child) {
margin-top: 0.5rem;
}
> .client { > .client {
margin-left: auto; margin-left: auto;
@ -79,10 +25,10 @@
> .client, > .response { > .client, > .response {
background-color: rgba(#000, 0.1); background-color: rgba(#000, 0.1);
border-radius: 5px; border-radius: 5px;
max-width: 215px; max-width: 85%;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 6px; padding: 0.5rem;
> .message-avatar { > .message-avatar {
height: 2rem; height: 2rem;
@ -90,41 +36,68 @@
} }
> .message-content { > .message-content {
padding-left: 6px; padding-left: 0.5rem;
> .message-text { > .message-text {
font-size: 1.3vmin; font-size: 1rem;
} }
> .message-time { > .message-time {
font-size: 1vmin; font-size: 0.8rem;
opacity: 0.8; opacity: 0.8;
} }
} }
} }
} }
> .empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 20vmin;
> p {
padding: 6px 12px;
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 20px;
color: #000;
}
}
} }
[data-component='Sender'] { [data-component='Sender'] {
align-items: center;
display: flex; display: flex;
background-color: rgba(#000, 0.1); background-color: #fff;
height: 6vmin; color: #000;
padding: 0.5vmin; flex-shrink: 0;
border-radius: 0 0 5px 5px; margin-top: 0.5rem;
height: 3rem;
> .new-message { > .new-message {
width: 100%; width: 80%;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
border: 0; border: 0;
border-radius: 5px; font-size: 1rem;
background-color: rgba(#000, 0.1); margin-right: 1vmin;
color: #fff; border-radius: 0.5vmin;
height: 30px; padding-left: 1vmin;
padding-left: 10px; color: #000;
font-size: 1.4vmin;
&.focus { &.focus {
outline: none; outline: none;
} }
} }
> .send {
width: 20%;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
border: 0;
background-color: #aef;
color: #000;
font-size: 1rem;
border-radius: 0.5vmin;
}
} }

View File

@ -1,17 +1,16 @@
[data-component='FileSharing'] { [data-component='FileSharing'] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; flex-grow: 1;
> .sharing-toolbar { > .sharing-toolbar {
> .share-file { > .share-file {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
background: #252525; background: #aef;
border: 1px solid #151515;
padding: 1rem; padding: 1rem;
border-bottom: 5px solid #151515; border-radius: 1vmin;
border-radius: 3px 3px 0 0; box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
@ -21,7 +20,8 @@
> .shared-files { > .shared-files {
flex-grow: 1; flex-grow: 1;
overflow-y: scroll; overflow-y: auto;
margin-top: 0.5rem;
> .file-entry { > .file-entry {
background-color: rgba(0,0,0,0.1); background-color: rgba(0,0,0,0.1);
@ -29,7 +29,10 @@
width: 100%; width: 100%;
padding: 0.5rem; padding: 0.5rem;
display: flex; display: flex;
margin-top: 0.5rem;
&:not(:first-child) {
margin-top: 0.5rem;
}
&:last-child { &:last-child {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
@ -72,6 +75,23 @@
} }
} }
} }
> .empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 20vmin;
> p {
padding: 6px 12px;
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 20px;
color: #000;
}
}
} }
} }
@ -92,4 +112,4 @@
justify-content: center; justify-content: center;
font-size: 2rem; font-size: 2rem;
z-index: 3000; z-index: 3000;
} }

View File

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

View File

@ -44,10 +44,15 @@
height: 5vmin; height: 5vmin;
} }
&.fullscreen { &.exitfullscreen {
background-image: url('/resources/images/icon_fullscreen_exit_black.svg'); background-image: url('/resources/images/icon_fullscreen_exit_black.svg');
background-color: rgba(#fff, 0.7); background-color: rgba(#fff, 0.7);
} }
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }

View File

@ -0,0 +1,93 @@
[data-component='HiddenPeersView'] {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
> .info {
$backgroundTint = #000;
position: absolute;
z-index: 10;
top: 0.6vmin;
left: 0.6vmin;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
> .view-container {
width: 12vmin;
height: 9vmin;
position: absolute;
bottom: 3%;
right: 3%;
color: #aaa;
cursor: pointer;
background-image: url('/resources/images/buddy.svg');
background-color: rgba(#2a4b58, 1);
background-position: bottom;
background-size: auto 85%;
background-repeat: no-repeat;
text-align: center;
vertical-align: middle;
line-height: 1.8vmin;
font-size: 1.7vmin;
font-weight: bolder;
animation: none;
&.pulse {
animation: pulse 2s;
}
}
.view-container>p{
transform: translate(0%,50%);
}
.view-container,
.view-container::before,
.view-container::after {
/* Add shadow to distinguish sheets from one another */
box-shadow: 2px 1px 1px rgba(0,0,0,0.15);
}
.view-container::before,
.view-container::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: #2a4b58;
}
/* Second sheet of paper */
.view-container::before {
left: .7vmin;
top: .7vmin;
z-index: -1;
}
/* Third sheet of paper */
.view-container::after {
left: 1.4vmin;
top: 1.4vmin;
z-index: -2;
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 1.0);
}
70% {
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}

View File

@ -0,0 +1,19 @@
[data-component='Logo'] {
position: absolute;
height: 4%;
width: 8%;
top: 1%;
left: 1%;
z-index: 20;
background-position: left;
background-size: 100%;
background-repeat: no-repeat;
+desktop() {
opacity: 1;
}
+mobile() {
}
background-image: url('/resources/images/logo.svg');
background-size: contain;
}

View File

@ -33,7 +33,7 @@
&.visible { &.visible {
opacity: 1; opacity: 1;
} }
> .button { > .button {
flex: 0 0 auto; flex: 0 0 auto;
margin: 0.2vmin; margin: 0.2vmin;
@ -43,13 +43,17 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: rgba(#000, 0.5); background-color: rgba(#000, 0.5);
cursor: pointer; cursor: pointer;
opacity: 0;
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
&.visible {
opacity: 0.85;
}
+desktop() { +desktop() {
width: 24px; width: 24px;
height: 24px; height: 24px;
opacity: 0.85;
&:hover { &:hover {
opacity: 1; opacity: 1;

View File

@ -1,12 +1,18 @@
[data-component='ParticipantList'] { [data-component='ParticipantList'] {
width: 100%; width: 100%;
overflow-y: auto;
padding: 6px;
> .list { > .list {
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \ box-shadow: 0 2px 5px 2px rgba(0,0,0,0.2);
0 4px 20px 0 rgba(0,0,0,0.19); background-color: #fff;
> .list-header {
padding: 0.5rem;
font-weight: bolder;
}
> .list-item { > .list-item {
padding: 0.5vmin; padding: 0.5rem;
border-bottom: 1px solid #CBCBCB; border-bottom: 1px solid #CBCBCB;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
@ -17,7 +23,7 @@
} }
&.selected { &.selected {
border-bottom-color: #377EFF; background-color: #377eff;
} }
} }
} }
@ -25,21 +31,19 @@
[data-component='ListPeer'] { [data-component='ListPeer'] {
display: flex; display: flex;
align-items: center;
> .indicators { > .indicators {
left: 0; left: 0;
top: 0; top: 0;
display: flex; display: flex;
flex-direction:; row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding: 0.4vmin;
transition: opacity 0.3s; transition: opacity 0.3s;
> .icon { > .icon {
flex: 0 0 auto; flex: 0 0 auto;
margin: 0.2vmin; margin: 0.3rem;
border-radius: 2px; border-radius: 2px;
background-position: center; background-position: center;
background-size: 75%; background-size: 75%;
@ -76,6 +80,41 @@
} }
} }
} }
> .volume-container {
float: right;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 1vmin;
position: relative;
background-size: 75%;
> .bar {
flex: 0 0 auto;
margin: 0.3rem;
background-size: 75%;
background-repeat: no-repeat;
cursor: pointer;
transition-property: opacity, background-color;
width: 3px;
border-radius: 6px;
transition-duration: 0.25s;
position: absolute;
bottom: 0px;
&.level0 { height: 0; background-color: rgba(#000, 0.8); }
&.level1 { height: 0.2vh; background-color: rgba(#000, 0.8); }
&.level2 { height: 0.4vh; background-color: rgba(#000, 0.8); }
&.level3 { height: 0.6vh; background-color: rgba(#000, 0.8); }
&.level4 { height: 0.8vh; background-color: rgba(#000, 0.8); }
&.level5 { height: 1.0vh; background-color: rgba(#000, 0.8); }
&.level6 { height: 1.2vh; background-color: rgba(#000, 0.8); }
&.level7 { height: 1.4vh; background-color: rgba(#000, 0.8); }
&.level8 { height: 1.6vh; background-color: rgba(#000, 0.8); }
&.level9 { height: 1.8vh; background-color: rgba(#000, 0.8); }
&.level10 { height: 2.0vh; background-color: rgba(#000, 0.8); }
}
}
> .controls { > .controls {
float: right; float: right;
display: flex; display: flex;
@ -85,7 +124,7 @@
> .button { > .button {
flex: 0 0 auto; flex: 0 0 auto;
margin: 0.2vmin; margin: 0.3rem;
border-radius: 2px; border-radius: 2px;
background-position: center; background-position: center;
background-size: 75%; background-size: 75%;
@ -176,10 +215,10 @@
} }
> .peer-info { > .peer-info {
font-size: 1.4vmin; font-size: 1rem;
border: none; border: none;
display: flex; display: flex;
padding: 1vmin; padding-left: 0.5rem;
flex-grow: 1; flex-grow: 1;
align-items: center; align-items: center;
} }

View File

@ -189,10 +189,37 @@
background-image: url('/resources/images/icon_fullscreen_black.svg'); background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7); background-color: rgba(#fff, 0.7);
} }
&.newwindow {
background-image: url('/resources/images/icon_new_window_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }
} }
.paused-video {
position: absolute;
z-index: 11;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> p {
padding: 6px 12px;
border-radius: 6px;
user-select: none;
pointer-events: none;
font-size: 20px;
color: rgba(#fff, 0.55);
}
}
.incompatible-video { .incompatible-video {
position: absolute; position: absolute;
z-index: 10; z-index: 10;
@ -210,7 +237,7 @@
border-radius: 6px; border-radius: 6px;
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
font-size: 15px; font-size: 20px;
color: rgba(#fff, 0.55); color: rgba(#fff, 0.55);
} }
} }

View File

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

View File

@ -19,15 +19,15 @@
background-color: rgba(#fff, 0.2); background-color: rgba(#fff, 0.2);
+desktop() { +desktop() {
top: 20px; bottom: 1%;
left: 20px; left: 1%;
width: 124px; width: 10%;
} }
+mobile() { +mobile() {
top: 10px; bottom: 1%;
left: 10px; left: 1%;
width: 110px; width: 10%;
} }
> .icon { > .icon {
@ -152,33 +152,18 @@
} }
+desktop() { +desktop() {
top: 20px; top: 6%;
left: 20px; left:1%;
border: 1px solid rgba(#fff, 0.15); border: 1px solid rgba(#fff, 0.15);
} }
+mobile() { +mobile() {
top: 10px; top: 6%;
left: 10px; left: 1%;
border: 1px solid rgba(#fff, 0.25); border: 1px solid rgba(#fff, 0.25);
} }
} }
} }
> .toolarea-wrapper {
position: fixed;
width: 0;
top: 0;
right: 0;
height: 100%;
background-color: rgba(50, 50, 50, 0.9);
transition: width 0.3s;
z-index: 1000;
&.open {
width: 25%;
}
}
} }
.room-controls { .room-controls {
@ -210,6 +195,7 @@
outline: none; outline: none;
padding: 8px 52px 8px 10px; padding: 8px 52px 8px 10px;
transition: all 200ms ease; transition: all 200ms ease;
box-shadow: 0vmin 0vmin 0.2vmin 0vmin rgba(17,17,17,0.5);
} }
.Dropdown-control:hover { .Dropdown-control:hover {

View File

@ -9,13 +9,13 @@
align-items: center; align-items: center;
+desktop() { +desktop() {
left: 20px; left: 1.0em;
width: 36px; width: 2.6em;
} }
+mobile() { +mobile() {
left: 10px; left: 0.5em;
width: 32px; width: 2.6em;
} }
> .button { > .button {
@ -34,13 +34,13 @@
justify-content: center; justify-content: center;
+desktop() { +desktop() {
height: 36px; height: 2.5em;
width: 36px; width: 2.5em;
} }
+mobile() { +mobile() {
height: 32px; height: 2.5em;
width: 32px; width: 2.5em;
} }
&.on { &.on {
@ -110,7 +110,7 @@
} }
&.leave-meeting { &.leave-meeting {
background-image: url('/resources/images/leave-meeting.svg'); background-image: url('/resources/images/cancel.svg');
} }
} }
} }

View File

@ -1,20 +1,212 @@
.toolarea-shade {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
&.open {
display: block;
}
+desktop() {
&.open {
background: rgba(0, 0, 0, 0.3);
display: block;
}
}
}
[data-component='ToolAreaButton'] {
&.on {
right: 80%;
}
}
[data-component='ToolArea'] {
&.open {
width: 80%;
}
.toolarea-shade.open {
display: block;
}
> .button {
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
background-color: rgba(#aef);
cursor: pointer;
border-radius: 15%;
padding: 1px;
+desktop() {
height: 36px;
width: 18px;
}
+mobile() {
height: 32px;
width: 16px;
}
&.toolarea-close-button {
background-image: url('/resources/images/arrow_right.svg');
position: absolute;
top: 50%;
left: -22px;
display: none;
&.on {
display: block;
}
}
}
> .toolarea-button {
text-align: center;
writing-mode: vertical-rl;
text-orientation: mixed;
list-style: none;
height: 115px;
width: 35px;
left: -35px;
top: 50%;
transform: translate(0, -50%);
position: absolute;
cursor: pointer;
> .badge {
border-radius: 50%;
writing-mode: horizontal-tb;
font-size: 1rem;
background: #b12525;
color: #fff;
text-align: center;
margin-top: -10px;
line-height: 1rem;
margin-left: -0px;
position: absolute;
padding: 0.2rem 0.4rem;
top: 0;
left: 0;
&.long {
border-radius: 25% / 50%;
margin-top: -13px;
margin-left: -4px;
}
}
> .content {
border: 1px solid #AAA;
width: 100%;
height: 100%;
display: flex;
border-bottom-left-radius: 6px;
border-top-left-radius: 6px;
background: #FFF;
color: #333;
z-index: 2;
border-right-color: #FFF;
&:before, &:after {
border: 1px solid #AAA;
position: absolute;
width: 6px;
height: 6px;
content: "";
}
&:before {
top: -6px;
right: 0;
border-bottom-right-radius: 6px;
border-width: 0px 1px 1px 0px;
box-shadow: 0px 3px 0 #FFF;
}
&:after {
bottom: -6px;
right: 0;
border-top-right-radius: 6px;
border-width: 1px 1px 0px 0px;
box-shadow: 0px -3px 0 #FFF;
}
> .toolarea-icon {
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
border-radius: 100%;
height: 32px;
width: 32px;
background-image: url('/resources/images/icon_tool_area_black.svg');
}
> p {
padding: 9px;
}
}
}
}
@media (min-width: 600px) {
[data-component='ToolAreaButton'] {
&.on {
right: 60%;
}
}
[data-component='ToolArea'] {
&.open {
width: 60%;
}
}
}
@media (min-width: 900px) {
[data-component='ToolAreaButton'] {
&.on {
right: 40%;
}
}
[data-component='ToolArea'] {
&.open {
width: 40%;
}
}
}
@media (min-width: 1500px) {
[data-component='ToolAreaButton'] {
&.on {
right: 25%;
}
}
[data-component='ToolArea'] {
&.open {
width: 25%;
}
}
}
[data-component='ToolAreaButton'] { [data-component='ToolAreaButton'] {
position: absolute; position: absolute;
z-index: 1000; z-index: 1020;
right: 0; right: 0;
height: 36px; height: 36px;
width: 36px; width: 36px;
padding: 2rem; margin: 2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition: right 0.3s; transition: right 0.3s;
&.on {
right: 25%;
}
> .button { > .button {
flex: 0 0 auto; flex: 0 0 auto;
margin: 4px 0; margin: 4px 0;
@ -78,59 +270,55 @@
[data-component='ToolArea'] { [data-component='ToolArea'] {
width: 100%; width: 100%;
height: 100%; height: 100%;
color: #fff; color: #000;
position: fixed;
width: 0;
top: 0;
right: 0;
height: 100%;
background: #fff;
transition: width 0.3s;
z-index: 1010;
display: flex;
flex-direction: column;
border-left: 1px solid #222;
> .tabs { > .tab-headers {
display: flex; display: flex;
flex-wrap: wrap; background: #ddd;
height: 100%; flex-shrink: 0;
> label { > .tab-header {
order: 1; flex-grow: 1;
display: block;
padding: 1vmin 0 0.8vmin 0;
cursor: pointer; cursor: pointer;
background: rgba(0,0,0,0.3); padding: 1rem;
font-weight: bold; font-size: 1.2rem;
transition: background ease 0.2s;
text-align: center; text-align: center;
width: 25%;
font-size: 1.3vmin; &.checked {
height: 3vmin; background: #fff;
border-radius: 1vmin 1vmin 0vmin 0vmin;
box-shadow: 0.5vmin 0vmin 1vmin -0.5vmin #aaa;
}
> .badge { > .badge {
padding: 0.1vmin 1vmin; padding: 0.2rem 0.6rem;
text-align: center; text-align: center;
font-weight: 300; font-weight: 300;
font-size: 1.2vmin; font-size: 1rem;
color: #fff; color: #fff;
background-color: #b12525; background-color: #b12525;
border-radius: 2px; border-radius: 2px;
margin-left: 1vmin; margin-left: 1vmin;
} }
} }
}
> .tab { > .tab {
order: 99; flex-grow: 1;
flex-grow: 1; padding: 0.5rem;
width: 100%; display: flex;
height: 100%; flex-direction: column;
display: none; min-height: 0;
padding: 1vmin;
background: rgba(0,0,0,0.1);
}
> input[type="radio"] {
display: none;
}
> input[type="radio"]:checked + label {
background: rgba(0,0,0,0.1);
}
> input[type="radio"]:checked + label + .tab {
display: block;
background: rgba(0,0,0,0.1);
}
} }
} }

View File

@ -9,18 +9,10 @@ global-reset();
html { html {
height: 100%; height: 100%;
box-sizing: border-box;
background-image: url('/resources/images/background.svg');
background-attachment: fixed;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
font-family: 'Roboto'; font-family: 'Roboto';
font-weight: 300; font-weight: 300;
overflow-x: hidden;
+desktop() { overflow-y: hidden;
font-size: 16px;
}
+mobile() { +mobile() {
font-size: 12px; font-size: 12px;
@ -30,6 +22,17 @@ html {
body { body {
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden;
background-color: #333;
+desktop() {
font-size: 16px;
background-image: url('/resources/images/background.svg');
background-attachment: fixed;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
} }
#multiparty-meeting { #multiparty-meeting {
@ -39,11 +42,13 @@ body {
// Components // Components
@import './components/Room'; @import './components/Room';
@import './components/Logo';
@import './components/Sidebar'; @import './components/Sidebar';
@import './components/Me'; @import './components/Me';
@import './components/Peers'; @import './components/Peers';
@import './components/Peer'; @import './components/Peer';
@import './components/PeerView'; @import './components/PeerView';
@import './components/HiddenPeersView';
@import './components/ScreenView'; @import './components/ScreenView';
@import './components/Notifications'; @import './components/Notifications';
@import './components/Chat'; @import './components/Chat';
@ -54,6 +59,7 @@ body {
@import './components/FullView'; @import './components/FullView';
@import './components/Filmstrip'; @import './components/Filmstrip';
@import './components/FileSharing'; @import './components/FileSharing';
@import './components/AudioPeers';
// Hack to detect in JS the current media query // Hack to detect in JS the current media query
#multiparty-meeting-media-query-detector { #multiparty-meeting-media-query-detector {

View File

@ -3,13 +3,14 @@ Description=multiparty-meeting is a audio / video meeting service running in the
After=network.target After=network.target
[Service] [Service]
ExecStart=/usr/local/src/multiparty-meeting/server.js ExecStart=/usr/local/src/multiparty-meeting/server/server.js
Restart=always Restart=always
User=nobody User=nobody
Group=nogroup Group=nogroup
Environment=PATH=/usr/bin:/usr/local/bin Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production Environment=NODE_ENV=production
WorkingDirectory=/usr/local/src/multiparty-meeting WorkingDirectory=/usr/local/src/multiparty-meeting/server
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

1
server/.npmrc 100644
View File

@ -0,0 +1 @@
package-lock=false

View File

@ -15,7 +15,11 @@ module.exports =
key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem` key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem`
}, },
// Listening port for https server. // Listening port for https server.
listeningPort : 3443, listeningPort : 443,
// Any http request is redirected to https.
// Listening port for http server.
listeningRedirectPort : 80,
// STUN/TURN
turnServers : [ turnServers : [
{ {
urls : [ urls : [
@ -59,20 +63,22 @@ module.exports =
useinbandfec : 1 useinbandfec : 1
} }
}, },
{
kind : 'video',
name : 'VP8',
clockRate : 90000
}
// { // {
// kind : 'video', // kind : 'video',
// name : 'H264', // name : 'VP8',
// clockRate : 90000, // clockRate : 90000
// parameters :
// {
// 'packetization-mode' : 1
// }
// } // }
{
kind : 'video',
name : 'H264',
clockRate : 90000,
parameters :
{
'packetization-mode' : 1,
'profile-level-id' : '42e01f',
'level-asymmetry-allowed' : 1
}
}
], ],
// mediasoup per Peer max sending bitrate (in bps). // mediasoup per Peer max sending bitrate (in bps).
maxBitrate : 500000 maxBitrate : 500000

View File

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

View File

@ -1,8 +1,6 @@
'use strict'; 'use strict';
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const protooServer = require('protoo-server');
const WebTorrent = require('webtorrent-hybrid');
const Logger = require('./Logger'); const Logger = require('./Logger');
const config = require('../config'); const config = require('../config');
@ -12,17 +10,9 @@ const BITRATE_FACTOR = 0.75;
const logger = new Logger('Room'); const logger = new Logger('Room');
const torrentClient = new WebTorrent({
tracker : {
rtcConfig : {
iceServers : config.turnServers
}
}
});
class Room extends EventEmitter class Room extends EventEmitter
{ {
constructor(roomId, mediaServer) constructor(roomId, mediaServer, io)
{ {
logger.info('constructor() [roomId:"%s"]', roomId); logger.info('constructor() [roomId:"%s"]', roomId);
@ -39,11 +29,14 @@ class Room extends EventEmitter
this._fileHistory = []; this._fileHistory = [];
this._lastN = [];
this._io = io;
this._signalingPeers = new Map();
try try
{ {
// Protoo Room instance.
this._protooRoom = new protooServer.Room();
// mediasoup Room instance. // mediasoup Room instance.
this._mediaRoom = mediaServer.Room(config.mediasoup.mediaCodecs); this._mediaRoom = mediaServer.Room(config.mediasoup.mediaCodecs);
} }
@ -75,9 +68,15 @@ class Room extends EventEmitter
this._closed = true; this._closed = true;
// Close the protoo Room. // Close the signalingPeers
if (this._protooRoom) if (this._signalingPeers)
this._protooRoom.close(); for (let peer of this._signalingPeers)
{
if (peer.socket)
peer.socket.disconnect();
};
this._signalingPeers.clear();
// Close the mediasoup Room. // Close the mediasoup Room.
if (this._mediaRoom) if (this._mediaRoom)
@ -93,31 +92,63 @@ class Room extends EventEmitter
return; return;
logger.info( logger.info(
'logStatus() [room id:"%s", protoo peers:%s, mediasoup peers:%s]', 'logStatus() [room id:"%s", peers:%s, mediasoup peers:%s]',
this._roomId, this._roomId,
this._protooRoom.peers.length, this._signalingPeers.length,
this._mediaRoom.peers.length); this._mediaRoom.peers.length);
} }
handleConnection(peerName, transport) handleConnection(peerName, socket)
{ {
logger.info('handleConnection() [peerName:"%s"]', peerName); logger.info('handleConnection() [peerName:"%s"]', peerName);
if (this._protooRoom.hasPeer(peerName)) if (this._signalingPeers.has(peerName))
{ {
logger.warn( logger.warn(
'handleConnection() | there is already a peer with same peerName, ' + 'handleConnection() | there is already a peer with same peerName, ' +
'closing the previous one [peerName:"%s"]', 'closing the previous one [peerName:"%s"]',
peerName); peerName);
const protooPeer = this._protooRoom.getPeer(peerName); const signalingPeer = this._signalingPeers.get(peerName);
protooPeer.close(); signalingPeer.socket.disconnect();
this._signalingPeers.delete(peerName);
} }
const protooPeer = this._protooRoom.createPeer(peerName, transport); const signalingPeer = { peerName : peerName, socket : socket };
this._handleProtooPeer(protooPeer); const index = this._lastN.indexOf(peerName);
if (index === -1) // We don't have this peer, add to end
{
this._lastN.push(peerName);
}
this._signalingPeers.set(peerName, signalingPeer);
this._handleSignalingPeer(signalingPeer);
}
authCallback(data)
{
logger.debug('authCallback()');
const {
peerName,
name,
picture
} = data;
const signalingPeer = this._signalingPeers.get(peerName);
if (signalingPeer)
{
signalingPeer.socket.emit('auth',
{
name : name,
picture : picture
});
}
} }
_handleMediaRoom() _handleMediaRoom()
@ -134,6 +165,14 @@ class Room extends EventEmitter
this._currentActiveSpeaker = activePeer; this._currentActiveSpeaker = activePeer;
const index = this._lastN.indexOf(activePeer.name);
if (index > -1) // We have this speaker in the list, move to front
{
this._lastN.splice(index, 1);
this._lastN = [activePeer.name].concat(this._lastN);
}
const activeVideoProducer = activePeer.producers const activeVideoProducer = activePeer.producers
.find((producer) => producer.kind === 'video'); .find((producer) => producer.kind === 'video');
@ -173,185 +212,175 @@ class Room extends EventEmitter
} }
} }
// Spread to others via protoo. this._io.to(this._roomId).emit('active-speaker', {
this._protooRoom.spread(
'active-speaker',
{
peerName : activePeer ? activePeer.name : null peerName : activePeer ? activePeer.name : null
}); });
}); });
} }
_handleProtooPeer(protooPeer) _handleSignalingPeer(signalingPeer)
{ {
logger.debug('_handleProtooPeer() [peer:"%s"]', protooPeer.id); logger.debug('_handleSignalingPeer() [peer:"%s"]', signalingPeer.id);
protooPeer.on('request', (request, accept, reject) => signalingPeer.socket.on('mediasoup-request', (request, cb) =>
{ {
logger.debug( const mediasoupRequest = request;
'protoo "request" event [method:%s, peer:"%s"]',
request.method, protooPeer.id);
switch (request.method) this._handleMediasoupClientRequest(
{ signalingPeer, mediasoupRequest, cb);
case 'mediasoup-request':
{
const mediasoupRequest = request.data;
this._handleMediasoupClientRequest(
protooPeer, mediasoupRequest, accept, reject);
break;
}
case 'mediasoup-notification':
{
accept();
const mediasoupNotification = request.data;
this._handleMediasoupClientNotification(
protooPeer, mediasoupNotification);
break;
}
case 'change-display-name':
{
accept();
const { displayName } = request.data;
const { mediaPeer } = protooPeer.data;
const oldDisplayName = mediaPeer.appData.displayName;
mediaPeer.appData.displayName = displayName;
// Spread to others via protoo.
this._protooRoom.spread(
'display-name-changed',
{
peerName : protooPeer.id,
displayName : displayName,
oldDisplayName : oldDisplayName
},
[ protooPeer ]);
break;
}
case 'change-profile-picture':
{
accept();
this._protooRoom.spread('profile-picture-changed', {
peerName : protooPeer.id,
picture : request.data.picture
}, [ protooPeer ]);
break;
}
case 'chat-message':
{
accept();
const { chatMessage } = request.data;
this._chatHistory.push(chatMessage);
// Spread to others via protoo.
this._protooRoom.spread(
'chat-message-receive',
{
peerName : protooPeer.id,
chatMessage : chatMessage
},
[ protooPeer ]);
break;
}
case 'chat-history':
{
accept();
protooPeer.send(
'chat-history-receive',
{ chatHistory: this._chatHistory }
);
break;
}
case 'send-file':
{
accept();
const fileData = request.data.file;
this._fileHistory.push(fileData);
if (!torrentClient.get(fileData.file.magnet))
{
torrentClient.add(fileData.file.magnet);
}
this._protooRoom.spread('file-receive', {
file : fileData
}, [ protooPeer ]);
break;
}
case 'file-history':
{
accept();
protooPeer.send('file-history-receive', {
fileHistory : this._fileHistory
});
break;
}
case 'raisehand-message':
{
accept();
const { raiseHandState } = request.data;
const { mediaPeer } = protooPeer.data;
mediaPeer.appData.raiseHandState = request.data.raiseHandState;
// Spread to others via protoo.
this._protooRoom.spread(
'raisehand-message',
{
peerName : protooPeer.id,
raiseHandState : raiseHandState
},
[ protooPeer ]);
break;
}
default:
{
logger.error('unknown request.method "%s"', request.method);
reject(400, `unknown request.method "${request.method}"`);
}
}
}); });
protooPeer.on('close', () => signalingPeer.socket.on('mediasoup-notification', (request, cb) =>
{ {
logger.debug('protoo Peer "close" event [peer:"%s"]', protooPeer.id); // Return no error
cb(null);
const { mediaPeer } = protooPeer.data; const mediasoupNotification = request;
this._handleMediasoupClientNotification(
signalingPeer, mediasoupNotification);
});
signalingPeer.socket.on('change-display-name', (request, cb) =>
{
// Return no error
cb(null);
const { displayName } = request;
const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName);
const oldDisplayName = mediaPeer.appData.displayName;
mediaPeer.appData.displayName = displayName;
signalingPeer.socket.broadcast.to(this._roomId).emit(
'display-name-changed',
{
peerName : signalingPeer.peerName,
displayName : displayName,
oldDisplayName : oldDisplayName
}
);
});
signalingPeer.socket.on('change-profile-picture', (request, cb) =>
{
// Return no error
cb(null);
signalingPeer.socket.broadcast.to(this._roomId).emit(
'profile-picture-changed',
{
peerName : signalingPeer.peerName,
picture : request.picture
}
);
});
signalingPeer.socket.on('chat-message', (request, cb) =>
{
// Return no error
cb(null);
const { chatMessage } = request;
this._chatHistory.push(chatMessage);
// Spread to others
signalingPeer.socket.broadcast.to(this._roomId).emit(
'chat-message-receive',
{
peerName : signalingPeer.peerName,
chatMessage : chatMessage
}
);
});
signalingPeer.socket.on('server-history', (request, cb) =>
{
// Return to sender
cb(
null,
{
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastN : this._lastN
}
);
});
signalingPeer.socket.on('send-file', (request, cb) =>
{
// Return no error
cb(null);
const fileData = request.file;
this._fileHistory.push(fileData);
// Spread to others
signalingPeer.socket.broadcast.to(this._roomId).emit(
'file-receive',
{
peerName : signalingPeer.peerName,
file : fileData
}
);
});
signalingPeer.socket.on('raisehand-message', (request, cb) =>
{
// Return no error
cb(null);
const { raiseHandState } = request;
const { mediaPeer } = signalingPeer;
mediaPeer.appData.raiseHandState = raiseHandState;
// Spread to others
signalingPeer.socket.broadcast.to(this._roomId).emit(
'raisehand-message',
{
peerName : signalingPeer.peerName,
raiseHandState : raiseHandState
},
);
});
signalingPeer.socket.on('request-consumer-keyframe', (request, cb) =>
{
cb(null);
const { consumerId } = request;
const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName);
const consumer = mediaPeer.consumers
.find((_consumer) => _consumer.id === consumerId);
if (!consumer)
{
logger.warn('consumer with id "%s" not found', consumerId);
return;
}
consumer.requestKeyFrame();
});
signalingPeer.socket.on('disconnect', () =>
{
logger.debug('Peer "close" event [peer:"%s"]', signalingPeer.peerName);
const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName);
if (mediaPeer && !mediaPeer.closed) if (mediaPeer && !mediaPeer.closed)
mediaPeer.close(); mediaPeer.close();
const index = this._lastN.indexOf(signalingPeer.peerName);
if (index > -1) // We have this peer in the list, remove
{
this._lastN.splice(index, 1);
}
// If this is the latest peer in the room, close the room. // If this is the latest peer in the room, close the room.
// However wait a bit (for reconnections). // However wait a bit (for reconnections).
setTimeout(() => setTimeout(() =>
@ -371,12 +400,11 @@ class Room extends EventEmitter
}); });
} }
_handleMediaPeer(protooPeer, mediaPeer) _handleMediaPeer(signalingPeer, mediaPeer)
{ {
mediaPeer.on('notify', (notification) => mediaPeer.on('notify', (notification) =>
{ {
protooPeer.send('mediasoup-notification', notification) signalingPeer.socket.emit('mediasoup-notification', notification);
.catch(() => {});
}); });
mediaPeer.on('newtransport', (transport) => mediaPeer.on('newtransport', (transport) =>
@ -424,12 +452,11 @@ class Room extends EventEmitter
// Notify about the existing active speaker. // Notify about the existing active speaker.
if (this._currentActiveSpeaker) if (this._currentActiveSpeaker)
{ {
protooPeer.send( signalingPeer.socket.emit(
'active-speaker', 'active-speaker',
{ {
peerName : this._currentActiveSpeaker.name peerName : this._currentActiveSpeaker.name
}) });
.catch(() => {});
} }
} }
@ -495,19 +522,19 @@ class Room extends EventEmitter
consumer.setPreferredProfile('low'); consumer.setPreferredProfile('low');
} }
_handleMediasoupClientRequest(protooPeer, request, accept, reject) _handleMediasoupClientRequest(signalingPeer, request, cb)
{ {
logger.debug( logger.debug(
'mediasoup-client request [method:%s, peer:"%s"]', 'mediasoup-client request [method:%s, peer:"%s"]',
request.method, protooPeer.id); request.method, signalingPeer.peerName);
switch (request.method) switch (request.method)
{ {
case 'queryRoom': case 'queryRoom':
{ {
this._mediaRoom.receiveRequest(request) this._mediaRoom.receiveRequest(request)
.then((response) => accept(response)) .then((response) => cb(null, response))
.catch((error) => reject(500, error.toString())); .catch((error) => cb(error.toString()));
break; break;
} }
@ -517,15 +544,15 @@ class Room extends EventEmitter
// TODO: Handle appData. Yes? // TODO: Handle appData. Yes?
const { peerName } = request; const { peerName } = request;
if (peerName !== protooPeer.id) if (peerName !== signalingPeer.peerName)
{ {
reject(403, 'that is not your corresponding mediasoup Peer name'); cb('that is not your corresponding mediasoup Peer name');
break; break;
} }
else if (protooPeer.data.mediaPeer) else if (signalingPeer.mediaPeer)
{ {
reject(500, 'already have a mediasoup Peer'); cb('already have a mediasoup Peer');
break; break;
} }
@ -533,18 +560,18 @@ class Room extends EventEmitter
this._mediaRoom.receiveRequest(request) this._mediaRoom.receiveRequest(request)
.then((response) => .then((response) =>
{ {
accept(response); cb(null, response);
// Get the newly created mediasoup Peer. // Get the newly created mediasoup Peer.
const mediaPeer = this._mediaRoom.getPeerByName(peerName); const mediaPeer = this._mediaRoom.getPeerByName(peerName);
protooPeer.data.mediaPeer = mediaPeer; signalingPeer.mediaPeer = mediaPeer;
this._handleMediaPeer(protooPeer, mediaPeer); this._handleMediaPeer(signalingPeer, mediaPeer);
}) })
.catch((error) => .catch((error) =>
{ {
reject(500, error.toString()); cb(error.toString());
}); });
break; break;
@ -552,7 +579,7 @@ class Room extends EventEmitter
default: default:
{ {
const { mediaPeer } = protooPeer.data; const { mediaPeer } = signalingPeer;
if (!mediaPeer) if (!mediaPeer)
{ {
@ -560,25 +587,25 @@ class Room extends EventEmitter
'cannot handle mediasoup request, no mediasoup Peer [method:"%s"]', 'cannot handle mediasoup request, no mediasoup Peer [method:"%s"]',
request.method); request.method);
reject(400, 'no mediasoup Peer'); cb('no mediasoup Peer');
} }
mediaPeer.receiveRequest(request) mediaPeer.receiveRequest(request)
.then((response) => accept(response)) .then((response) => cb(null, response))
.catch((error) => reject(500, error.toString())); .catch((error) => cb(error.toString()));
} }
} }
} }
_handleMediasoupClientNotification(protooPeer, notification) _handleMediasoupClientNotification(signalingPeer, notification)
{ {
logger.debug( logger.debug(
'mediasoup-client notification [method:%s, peer:"%s"]', 'mediasoup-client notification [method:%s, peer:"%s"]',
notification.method, protooPeer.id); notification.method, signalingPeer.peerName);
// NOTE: mediasoup-client just sends notifications with target 'peer', // NOTE: mediasoup-client just sends notifications with target 'peer',
// so first of all, get the mediasoup Peer. // so first of all, get the mediasoup Peer.
const { mediaPeer } = protooPeer.data; const { mediaPeer } = signalingPeer;
if (!mediaPeer) if (!mediaPeer)
{ {

View File

@ -155,17 +155,20 @@ function handleTransport(transport, baseEvent, stream)
const statsInterval = setInterval(() => const statsInterval = setInterval(() =>
{ {
transport.getStats() if (typeof transport.getStats === 'function')
.then((stats) => {
{ transport.getStats()
emit( .then((stats) =>
Object.assign({}, baseEvent, {
{ emit(
event : 'transport.stats', Object.assign({}, baseEvent,
stats : stats {
}), event : 'transport.stats',
stream); stats : stats
}); }),
stream);
});
}
}, STATS_INTERVAL); }, STATS_INTERVAL);
transport.on('close', (originator) => transport.on('close', (originator) =>

View File

@ -8,7 +8,7 @@ const config = require('./config');
// mediasoup server. // mediasoup server.
const mediaServer = mediasoup.Server( const mediaServer = mediasoup.Server(
{ {
numWorkers : 1, numWorkers : null,
logLevel : config.mediasoup.logLevel, logLevel : config.mediasoup.logLevel,
logTags : config.mediasoup.logTags, logTags : config.mediasoup.logTags,
rtcIPv4 : config.mediasoup.rtcIPv4, rtcIPv4 : config.mediasoup.rtcIPv4,
@ -318,4 +318,4 @@ function stdinError(msg)
console.error(colors.red.bold('ERROR: ') + colors.red(msg)); console.error(colors.red.bold('ERROR: ') + colors.red(msg));
} }
module.exports = mediaServer; module.exports = mediaServer;

8318
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,16 +9,16 @@
"dependencies": { "dependencies": {
"base-64": "^0.1.0", "base-64": "^0.1.0",
"colors": "^1.1.2", "colors": "^1.1.2",
"debug": "^3.1.0", "compression": "^1.7.3",
"debug": "^4.1.0",
"express": "^4.16.3", "express": "^4.16.3",
"mediasoup": "^2.1.0", "mediasoup": "^2.4.3",
"passport-dataporten": "^1.3.0", "passport-dataporten": "^1.3.0",
"protoo-server": "^2.0.7", "socket.io": "^2.1.1"
"webtorrent-hybrid": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-eslint": "^4.0.2", "gulp-eslint": "^5.0.0",
"gulp-plumber": "^1.2.0" "gulp-plumber": "^1.2.0"
} }
} }

View File

@ -7,9 +7,10 @@ process.title = 'multiparty-meeting-server';
const config = require('./config'); const config = require('./config');
const fs = require('fs'); const fs = require('fs');
const https = require('https'); const https = require('https');
const http = require('http');
const express = require('express'); const express = require('express');
const compression = require('compression');
const url = require('url'); const url = require('url');
const protooServer = require('protoo-server');
const Logger = require('./lib/Logger'); const Logger = require('./lib/Logger');
const Room = require('./lib/Room'); const Room = require('./lib/Room');
const Dataporten = require('passport-dataporten'); const Dataporten = require('passport-dataporten');
@ -39,12 +40,24 @@ const tls =
const app = express(); const app = express();
app.use(compression());
const dataporten = new Dataporten.Setup(config.oauth2); const dataporten = new Dataporten.Setup(config.oauth2);
app.all('*', (req, res, next) =>
{
if(req.secure)
{
return next();
}
res.redirect('https://' + req.hostname + req.url);
});
app.use(dataporten.passport.initialize()); app.use(dataporten.passport.initialize());
app.use(dataporten.passport.session()); app.use(dataporten.passport.session());
app.get('/login', (req, res, next) => app.get('/login', (req, res, next) =>
{ {
dataporten.passport.authenticate('dataporten', { dataporten.passport.authenticate('dataporten', {
state : base64.encode(JSON.stringify({ state : base64.encode(JSON.stringify({
@ -52,34 +65,36 @@ app.get('/login', (req, res, next) =>
peerName : req.query.peerName, peerName : req.query.peerName,
code : utils.random(10) code : utils.random(10)
})) }))
})(req, res, next); })(req, res, next);
}); });
dataporten.setupLogout(app, '/logout'); dataporten.setupLogout(app, '/logout');
app.get('/', function (req, res) {
console.log(req.url);
res.sendFile(`${__dirname}/public/chooseRoom.html`);
})
app.get( app.get(
'/auth-callback', '/auth-callback',
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }), dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
(req, res) => (req, res) =>
{ {
const state = JSON.parse(base64.decode(req.query.state)); const state = JSON.parse(base64.decode(req.query.state));
if (rooms.has(state.roomId)) if (rooms.has(state.roomId))
{ {
const room = rooms.get(state.roomId)._protooRoom; const data =
if (room.hasPeer(state.peerName))
{ {
const peer = room.getPeer(state.peerName); peerName : state.peerName,
name : req.user.data.displayName,
picture : req.user.data.photos[0]
};
peer.send('auth', { const room = rooms.get(state.roomId);
name : req.user.data.displayName,
picture : req.user.data.photos[0] room.authCallback(data);
});
}
} }
res.send(''); res.send('');
@ -98,29 +113,25 @@ httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
logger.info('Server running on port: ', config.listeningPort); logger.info('Server running on port: ', config.listeningPort);
}); });
// Protoo WebSocket server listens to same webserver so everything is available const httpServer = http.createServer(app);
// via same port
const webSocketServer = new protooServer.WebSocketServer(httpsServer, httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
{ {
maxReceivedFrameSize : 960000, // 960 KBytes. logger.info('Server redirecting port: ', config.listeningRedirectPort);
maxReceivedMessageSize : 960000, });
fragmentOutgoingMessages : true,
fragmentationThreshold : 960000 const io = require('socket.io')(httpsServer);
});
// Handle connections from clients. // Handle connections from clients.
webSocketServer.on('connectionrequest', (info, accept, reject) => io.on('connection', (socket) =>
{ {
// The client indicates the roomId and peerId in the URL query. const { roomId, peerName } = socket.handshake.query;
const u = url.parse(info.request.url, true);
const roomId = u.query['roomId'];
const peerName = u.query['peerName'];
if (!roomId || !peerName) if (!roomId || !peerName)
{ {
logger.warn('connection request without roomId and/or peerName'); logger.warn('connection request without roomId and/or peerName');
reject(400, 'Connection request without roomId and/or peerName'); socket.disconnect(true);
return; return;
} }
@ -137,7 +148,7 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
try try
{ {
room = new Room(roomId, mediaServer); room = new Room(roomId, mediaServer, io);
global.APP_ROOM = room; global.APP_ROOM = room;
} }
@ -145,7 +156,7 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
{ {
logger.error('error creating a new Room: %s', error); logger.error('error creating a new Room: %s', error);
reject(error); socket.disconnect(true);
return; return;
} }
@ -168,7 +179,8 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
room = rooms.get(roomId); room = rooms.get(roomId);
} }
const transport = accept(); socket.join(roomId);
socket.room = roomId;
room.handleConnection(peerName, transport); room.handleConnection(peerName, socket);
}); });