Merge branch 'develop'
commit
1d9668fa46
|
|
@ -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?
|
||||
22
README.md
22
README.md
|
|
@ -2,8 +2,16 @@
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -22,15 +30,6 @@ $ cp server/config.example.js server/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
|
||||
$ cp app/config.example.js app/config.js
|
||||
```
|
||||
|
|
@ -72,7 +71,7 @@ $ node server.js
|
|||
|
||||
## 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
|
||||
$ cp multiparty-meeting.service /etc/systemd/system/
|
||||
$ 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`)
|
||||
|
||||
* 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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ module.exports =
|
|||
plugins:
|
||||
[
|
||||
'import',
|
||||
'react'
|
||||
'react',
|
||||
'jsx-control-statements'
|
||||
],
|
||||
extends:
|
||||
[
|
||||
|
|
@ -21,13 +22,13 @@ module.exports =
|
|||
react:
|
||||
{
|
||||
pragma: 'React',
|
||||
version: '15'
|
||||
version: '16'
|
||||
}
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
parser: "babel-eslint",
|
||||
parserOptions:
|
||||
{
|
||||
ecmaVersion: 9,
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures:
|
||||
{
|
||||
|
|
@ -177,6 +178,7 @@ module.exports =
|
|||
'spaced-comment': [ 2, 'always' ],
|
||||
'strict': 2,
|
||||
'valid-typeof': 2,
|
||||
'eol-last': 0,
|
||||
'yoda': 2,
|
||||
// eslint-plugin-import options.
|
||||
'import/extensions': 2,
|
||||
|
|
@ -197,7 +199,7 @@ module.exports =
|
|||
'react/jsx-no-bind': 0,
|
||||
'react/jsx-no-duplicate-props': 2,
|
||||
'react/jsx-no-literals': 0,
|
||||
'react/jsx-no-undef': 2,
|
||||
'react/jsx-no-undef': 0,
|
||||
'react/jsx-pascal-case': 2,
|
||||
'react/jsx-sort-prop-types': 0,
|
||||
'react/jsx-sort-props': 0,
|
||||
|
|
@ -214,7 +216,7 @@ module.exports =
|
|||
'react/no-string-refs': 0,
|
||||
'react/no-unknown-property': 2,
|
||||
'react/prefer-es6-class': 2,
|
||||
'react/prop-types': 2,
|
||||
'react/prop-types': [ 2, { skipUndeclared: true } ],
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
||||
|
|
@ -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>
|
||||
|
|
@ -26,7 +26,7 @@ const touch = require('gulp-touch-cmd');
|
|||
const browserify = require('browserify');
|
||||
const watchify = require('watchify');
|
||||
const envify = require('envify/custom');
|
||||
const uglify = require('gulp-uglify');
|
||||
const uglify = require('gulp-uglify-es').default;
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const del = require('del');
|
||||
|
|
@ -77,10 +77,7 @@ function bundle(options)
|
|||
// required to be true only for watchify.
|
||||
fullPaths : watch
|
||||
})
|
||||
.transform('babelify',
|
||||
{
|
||||
presets : [ 'env', 'react-app' ]
|
||||
})
|
||||
.transform('babelify')
|
||||
.transform(envify(
|
||||
{
|
||||
NODE_ENV : process.env.NODE_ENV,
|
||||
|
|
@ -171,7 +168,7 @@ gulp.task('css', () =>
|
|||
|
||||
gulp.task('html', () =>
|
||||
{
|
||||
return gulp.src('index.html')
|
||||
return gulp.src('*.html')
|
||||
.pipe(change(changeHTML))
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
});
|
||||
|
|
@ -244,7 +241,7 @@ gulp.task('browser', (done) =>
|
|||
gulp.task('watch', (done) =>
|
||||
{
|
||||
// Watch changes in HTML.
|
||||
gulp.watch([ 'index.html' ], gulp.series(
|
||||
gulp.watch([ '*.html' ], gulp.series(
|
||||
'html'
|
||||
));
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,184 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import Logger from './Logger';
|
||||
|
||||
const logger = new Logger('Spotlight');
|
||||
|
||||
export default class Spotlights extends EventEmitter
|
||||
{
|
||||
constructor(maxSpotlights, room)
|
||||
{
|
||||
super();
|
||||
|
||||
this._room = room;
|
||||
this._maxSpotlights = maxSpotlights;
|
||||
this._peerList = [];
|
||||
this._selectedSpotlights = [];
|
||||
this._currentSpotlights = [];
|
||||
this._started = false;
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
const peers = this._room.peers;
|
||||
|
||||
for (const peer of peers)
|
||||
{
|
||||
this._handlePeer(peer);
|
||||
}
|
||||
|
||||
this._handleRoom();
|
||||
|
||||
this._started = true;
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
peerInSpotlights(peerName)
|
||||
{
|
||||
if (this._started)
|
||||
{
|
||||
return this._currentSpotlights.indexOf(peerName) !== -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setPeerSpotlight(peerName)
|
||||
{
|
||||
logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName);
|
||||
|
||||
const index = this._selectedSpotlights.indexOf(peerName);
|
||||
|
||||
if (index !== -1)
|
||||
{
|
||||
this._selectedSpotlights = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this._selectedSpotlights = [ peerName ];
|
||||
}
|
||||
|
||||
/*
|
||||
if (index === -1) // We don't have this peer in the list, adding
|
||||
{
|
||||
this._selectedSpotlights.push(peerName);
|
||||
}
|
||||
else // We have this peer, remove
|
||||
{
|
||||
this._selectedSpotlights.splice(index, 1);
|
||||
}
|
||||
*/
|
||||
|
||||
if (this._started)
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
_handleRoom()
|
||||
{
|
||||
this._room.on('newpeer', (peer) =>
|
||||
{
|
||||
logger.debug(
|
||||
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
||||
this._handlePeer(peer);
|
||||
});
|
||||
}
|
||||
|
||||
addSpeakerList(speakerList)
|
||||
{
|
||||
this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ];
|
||||
|
||||
if (this._started)
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
_handlePeer(peer)
|
||||
{
|
||||
logger.debug('_handlePeer() [peerName:"%s"]', peer.name);
|
||||
|
||||
if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list
|
||||
{
|
||||
peer.on('close', () =>
|
||||
{
|
||||
let index = this._peerList.indexOf(peer.name);
|
||||
|
||||
if (index !== -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._peerList.splice(index, 1);
|
||||
}
|
||||
|
||||
index = this._selectedSpotlights.indexOf(peer.name);
|
||||
|
||||
if (index !== -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._selectedSpotlights.splice(index, 1);
|
||||
}
|
||||
|
||||
this._spotlightsUpdated();
|
||||
});
|
||||
|
||||
logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name);
|
||||
|
||||
this._peerList.push(peer.name);
|
||||
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
handleActiveSpeaker(peerName)
|
||||
{
|
||||
logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName);
|
||||
|
||||
const index = this._peerList.indexOf(peerName);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this._peerList.splice(index, 1);
|
||||
this._peerList = [ peerName ].concat(this._peerList);
|
||||
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
_spotlightsUpdated()
|
||||
{
|
||||
let spotlights;
|
||||
|
||||
if (this._selectedSpotlights.length > 0)
|
||||
{
|
||||
spotlights = [ ...new Set([ ...this._selectedSpotlights, ...this._peerList ]) ];
|
||||
}
|
||||
else
|
||||
{
|
||||
spotlights = this._peerList;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._arraysEqual(
|
||||
this._currentSpotlights, spotlights.slice(0, this._maxSpotlights)
|
||||
)
|
||||
)
|
||||
{
|
||||
logger.debug('_spotlightsUpdated() | spotlights updated, emitting');
|
||||
|
||||
this._currentSpotlights = spotlights.slice(0, this._maxSpotlights);
|
||||
this.emit('spotlights-updated', this._currentSpotlights);
|
||||
}
|
||||
else
|
||||
logger.debug('_spotlightsUpdated() | spotlights not updated');
|
||||
}
|
||||
|
||||
_arraysEqual(arr1, arr2)
|
||||
{
|
||||
if (arr1.length !== arr2.length)
|
||||
return false;
|
||||
|
||||
for (let i = arr1.length; i--;)
|
||||
{
|
||||
if (arr1[i] !== arr2[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,11 @@ class Chat extends Component
|
|||
autoFocus={autofocus}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
className='send'
|
||||
value='Send'
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -53,7 +58,7 @@ Chat.propTypes =
|
|||
Chat.defaultProps =
|
||||
{
|
||||
senderPlaceHolder : 'Type a message...',
|
||||
autofocus : true,
|
||||
autofocus : false,
|
||||
displayName : null
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class MessageList extends Component
|
|||
|
||||
return (
|
||||
<div data-component='MessageList' id='messages'>
|
||||
{
|
||||
{ chatmessages.length > 0 ?
|
||||
chatmessages.map((message, i) =>
|
||||
{
|
||||
const messageTime = new Date(message.time);
|
||||
|
|
@ -61,6 +61,9 @@ class MessageList extends Component
|
|||
</div>
|
||||
);
|
||||
})
|
||||
:<div className='empty'>
|
||||
<p>No one has said anything yet...</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -13,14 +13,21 @@ class SharedFilesList extends Component
|
|||
{
|
||||
render()
|
||||
{
|
||||
const { sharing } = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
{this.props.sharing.map((entry, i) => (
|
||||
{ sharing.length > 0 ?
|
||||
sharing.map((entry, i) => (
|
||||
<FileEntry
|
||||
data={entry}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
:<div className='empty'>
|
||||
<p>No one has shared files yet...</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import React, { Component } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { connect } from 'react-redux';
|
||||
import debounce from 'lodash/debounce';
|
||||
import classnames from 'classnames';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import Peer from './Peer';
|
||||
import HiddenPeers from './HiddenPeers';
|
||||
|
||||
class Filmstrip extends Component
|
||||
{
|
||||
|
|
@ -59,7 +61,7 @@ class Filmstrip extends Component
|
|||
return ratio;
|
||||
};
|
||||
|
||||
updateDimensions = () =>
|
||||
updateDimensions = debounce(() =>
|
||||
{
|
||||
const container = this.activePeerContainer.current;
|
||||
|
||||
|
|
@ -78,7 +80,7 @@ class Filmstrip extends Component
|
|||
width
|
||||
});
|
||||
}
|
||||
};
|
||||
}, 200);
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
|
|
@ -112,7 +114,7 @@ class Filmstrip extends Component
|
|||
|
||||
render()
|
||||
{
|
||||
const { peers, advancedMode } = this.props;
|
||||
const { peers, advancedMode, spotlights, spotlightsLength } = this.props;
|
||||
|
||||
const activePeerName = this.getActivePeerName();
|
||||
|
||||
|
|
@ -137,7 +139,11 @@ class Filmstrip extends Component
|
|||
|
||||
<div className='filmstrip'>
|
||||
<div className='filmstrip-content'>
|
||||
{Object.keys(peers).map((peerName) => (
|
||||
{
|
||||
Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
return (
|
||||
spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
|
||||
<div
|
||||
key={peerName}
|
||||
onClick={() => this.props.setSelectedPeer(peerName)}
|
||||
|
|
@ -153,9 +159,20 @@ class Filmstrip extends Component
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
:null
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden-peer-container'>
|
||||
{ (spotlightsLength<Object.keys(peers).length)?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||
/>:null
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -168,19 +185,28 @@ Filmstrip.propTypes = {
|
|||
consumers : PropTypes.object.isRequired,
|
||||
myName : PropTypes.string.isRequired,
|
||||
selectedPeerName : PropTypes.string,
|
||||
setSelectedPeer : PropTypes.func.isRequired
|
||||
setSelectedPeer : PropTypes.func.isRequired,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
|
||||
|
||||
return {
|
||||
activeSpeakerName : state.room.activeSpeakerName,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
peers : state.peers,
|
||||
consumers : state.consumers,
|
||||
myName : state.me.name
|
||||
});
|
||||
myName : state.me.name,
|
||||
spotlights : state.room.spotlights,
|
||||
spotlightsLength
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setSelectedPeer : stateActions.setSelectedPeer
|
||||
setSelectedPeer : requestActions.setSelectedPeer
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ const FullScreenView = (props) =>
|
|||
|
||||
<div className='controls'>
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', 'room-controls', {
|
||||
className={classnames('button', 'exitfullscreen', 'room-controls', {
|
||||
visible : toolbarsVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
|
|
@ -56,7 +56,6 @@ const FullScreenView = (props) =>
|
|||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
toggleFullscreen={() => toggleConsumerFullscreen(consumer)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -86,6 +86,5 @@ FullView.propTypes =
|
|||
{
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool,
|
||||
videoProfile : PropTypes.string,
|
||||
toggleFullscreen : PropTypes.func.isRequired
|
||||
videoProfile : PropTypes.string
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -108,23 +108,29 @@ class Me extends React.Component
|
|||
>
|
||||
<div className={classnames('view-container', 'webcam')}>
|
||||
{connected ?
|
||||
<div className={classnames('controls', {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div className={classnames('controls', 'visible')}>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘m‘'
|
||||
data-type='dark'
|
||||
data-place='bottom'
|
||||
data-for='me'
|
||||
className={classnames('button', 'mic', micState, {
|
||||
disabled : me.audioInProgress
|
||||
disabled : me.audioInProgress,
|
||||
visible : micState == 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
micState === 'on' ? onMuteMic() : onUnmuteMic();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ReactTooltip
|
||||
id='me'
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
className={classnames('button', 'webcam', webcamState, {
|
||||
disabled : me.webcamInProgress
|
||||
disabled : me.webcamInProgress,
|
||||
visible : webcamState == 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
|
|
@ -161,15 +167,6 @@ class Me extends React.Component
|
|||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
{this._tooltip ?
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,9 @@ const ListPeer = (props) =>
|
|||
const {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
onMuteMic,
|
||||
onUnmuteMic,
|
||||
onDisableWebcam,
|
||||
onEnableWebcam,
|
||||
onDisableScreen,
|
||||
onEnableScreen
|
||||
} = props;
|
||||
|
|
@ -26,12 +23,6 @@ const ListPeer = (props) =>
|
|||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
|
|
@ -61,6 +52,9 @@ const ListPeer = (props) =>
|
|||
:null
|
||||
}
|
||||
</div>
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||
</div>
|
||||
<div className='controls'>
|
||||
{ screenConsumer ?
|
||||
<div
|
||||
|
|
@ -84,28 +78,12 @@ const ListPeer = (props) =>
|
|||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
style={{ opacity : micEnabled && micConsumer ? (micConsumer.volume/10)
|
||||
+ 0.2 :1 }}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'webcam', {
|
||||
on : videoVisible,
|
||||
off : !videoVisible,
|
||||
disabled : peer.peerVideoInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
videoVisible ?
|
||||
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,17 +2,32 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import * as requestActions from '../../redux/requestActions';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListPeer from './ListPeer';
|
||||
import ListMe from './ListMe';
|
||||
|
||||
const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerName }) => (
|
||||
const ParticipantList =
|
||||
({
|
||||
advancedMode,
|
||||
peers,
|
||||
setSelectedPeer,
|
||||
selectedPeerName,
|
||||
spotlights
|
||||
}) => (
|
||||
<div data-component='ParticipantList'>
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Me:</li>
|
||||
<ListMe />
|
||||
|
||||
{peers.map((peer) => (
|
||||
</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', {
|
||||
|
|
@ -24,6 +39,26 @@ const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerNam
|
|||
</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>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -32,7 +67,8 @@ ParticipantList.propTypes =
|
|||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
setSelectedPeer : PropTypes.func.isRequired,
|
||||
selectedPeerName : PropTypes.string
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
|
|
@ -41,12 +77,13 @@ const mapStateToProps = (state) =>
|
|||
|
||||
return {
|
||||
peers : peersArray,
|
||||
selectedPeerName : state.room.selectedPeerName
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights : state.room.spotlights
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setSelectedPeer : stateActions.setSelectedPeer
|
||||
setSelectedPeer : requestActions.setSelectedPeer
|
||||
};
|
||||
|
||||
const ParticipantListContainer = connect(
|
||||
|
|
|
|||
|
|
@ -38,12 +38,10 @@ class Peer extends Component
|
|||
screenConsumer,
|
||||
onMuteMic,
|
||||
onUnmuteMic,
|
||||
onDisableWebcam,
|
||||
onEnableWebcam,
|
||||
onDisableScreen,
|
||||
onEnableScreen,
|
||||
toggleConsumerFullscreen,
|
||||
style
|
||||
toggleConsumerWindow,
|
||||
style,
|
||||
windowConsumer
|
||||
} = this.props;
|
||||
|
||||
const micEnabled = (
|
||||
|
|
@ -90,6 +88,13 @@ class Peer extends Component
|
|||
:null
|
||||
}
|
||||
|
||||
{!videoVisible ?
|
||||
<div className='paused-video'>
|
||||
<p>this video is paused</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||
<div className='indicators'>
|
||||
{peer.raiseHandState ?
|
||||
|
|
@ -124,16 +129,14 @@ class Peer extends Component
|
|||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'webcam', {
|
||||
on : videoVisible,
|
||||
off : !videoVisible,
|
||||
disabled : peer.peerVideoInProgress
|
||||
className={classnames('button', 'newwindow', {
|
||||
disabled : !videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
videoVisible ?
|
||||
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
@ -146,10 +149,10 @@ class Peer extends Component
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PeerView
|
||||
advancedMode={advancedMode}
|
||||
peer={peer}
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
volume={micConsumer ? micConsumer.volume : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
|
|
@ -167,16 +170,11 @@ class Peer extends Component
|
|||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('button', 'screen', {
|
||||
on : screenVisible,
|
||||
off : !screenVisible,
|
||||
disabled : peer.peerScreenInProgress
|
||||
})}
|
||||
className={classnames('button', 'newwindow')}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
screenVisible ?
|
||||
onDisableScreen(peer.name) : onEnableScreen(peer.name);
|
||||
toggleConsumerWindow(screenConsumer);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
@ -211,15 +209,13 @@ Peer.propTypes =
|
|||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
windowConsumer : PropTypes.number,
|
||||
onMuteMic : PropTypes.func.isRequired,
|
||||
onUnmuteMic : PropTypes.func.isRequired,
|
||||
onEnableWebcam : PropTypes.func.isRequired,
|
||||
onDisableWebcam : PropTypes.func.isRequired,
|
||||
streamDimensions : PropTypes.object,
|
||||
style : PropTypes.object,
|
||||
onEnableScreen : PropTypes.func.isRequired,
|
||||
onDisableScreen : PropTypes.func.isRequired,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
|
|
@ -238,7 +234,8 @@ const mapStateToProps = (state, { name }) =>
|
|||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer
|
||||
screenConsumer,
|
||||
windowConsumer : state.room.windowConsumer
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -253,27 +250,15 @@ const mapDispatchToProps = (dispatch) =>
|
|||
{
|
||||
dispatch(requestActions.unmutePeerAudio(peerName));
|
||||
},
|
||||
onEnableWebcam : (peerName) =>
|
||||
{
|
||||
|
||||
dispatch(requestActions.resumePeerVideo(peerName));
|
||||
},
|
||||
onDisableWebcam : (peerName) =>
|
||||
{
|
||||
dispatch(requestActions.pausePeerVideo(peerName));
|
||||
},
|
||||
onEnableScreen : (peerName) =>
|
||||
{
|
||||
dispatch(requestActions.resumePeerScreen(peerName));
|
||||
},
|
||||
onDisableScreen : (peerName) =>
|
||||
{
|
||||
dispatch(requestActions.pausePeerScreen(peerName));
|
||||
},
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||
},
|
||||
toggleConsumerWindow : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerWindow(consumer.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import PeerAudio from './PeerAudio';
|
||||
|
||||
const AudioPeer = ({ micConsumer }) =>
|
||||
{
|
||||
return (
|
||||
<PeerAudio
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AudioPeer.propTypes =
|
||||
{
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
name : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
|
||||
return {
|
||||
micConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const AudioPeerContainer = connect(
|
||||
mapStateToProps
|
||||
)(AudioPeer);
|
||||
|
||||
export default AudioPeerContainer;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import AudioPeer from './AudioPeer';
|
||||
|
||||
const AudioPeers = ({ peers }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='AudioPeers'>
|
||||
{
|
||||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<AudioPeer
|
||||
key={peer.name}
|
||||
name={peer.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AudioPeers.propTypes =
|
||||
{
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peers = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers
|
||||
};
|
||||
};
|
||||
|
||||
const AudioPeersContainer = connect(
|
||||
mapStateToProps
|
||||
)(AudioPeers);
|
||||
|
||||
export default AudioPeersContainer;
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class PeerAudio extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
// Latest received audio track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<audio
|
||||
ref='audio'
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack } = this.props;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack } = nextProps;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
}
|
||||
|
||||
_setTrack(audioTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
|
||||
const { audio } = this.refs;
|
||||
|
||||
if (audioTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
audio.srcObject = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.srcObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PeerAudio.propTypes =
|
||||
{
|
||||
audioTrack : PropTypes.any
|
||||
};
|
||||
|
|
@ -2,9 +2,11 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import debounce from 'lodash/debounce';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import { Appear } from './transitions';
|
||||
import Peer from './Peer';
|
||||
import HiddenPeers from './HiddenPeers';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const RATIO = 1.334;
|
||||
|
|
@ -23,7 +25,7 @@ class Peers extends React.Component
|
|||
this.peersRef = React.createRef();
|
||||
}
|
||||
|
||||
updateDimensions = () =>
|
||||
updateDimensions = debounce(() =>
|
||||
{
|
||||
if (!this.peersRef.current)
|
||||
{
|
||||
|
|
@ -65,7 +67,7 @@ class Peers extends React.Component
|
|||
peerHeight : 0.9 * y
|
||||
});
|
||||
}
|
||||
};
|
||||
}, 200);
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
|
|
@ -90,7 +92,9 @@ class Peers extends React.Component
|
|||
const {
|
||||
advancedMode,
|
||||
activeSpeakerName,
|
||||
peers
|
||||
peers,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
} = this.props;
|
||||
|
||||
const style =
|
||||
|
|
@ -105,22 +109,35 @@ class Peers extends React.Component
|
|||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
(spotlights.find(function(spotlightsElement)
|
||||
{ return spotlightsElement == peer.name; }))?
|
||||
<Appear key={peer.name} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'selected' : this.props.selectedPeerName === peer.name,
|
||||
'active-speaker' : peer.name === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<div className='peer-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peer.name}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
:null
|
||||
);
|
||||
})
|
||||
}
|
||||
<div className='hidden-peer-container'>
|
||||
{ (spotlightsLength<peers.length)?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={peers.length-spotlightsLength}
|
||||
/>:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -131,20 +148,27 @@ Peers.propTypes =
|
|||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
boxes : PropTypes.number,
|
||||
activeSpeakerName : PropTypes.string
|
||||
activeSpeakerName : PropTypes.string,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peers = Object.values(state.peers);
|
||||
|
||||
const boxes = peers.length + Object.values(state.consumers)
|
||||
const spotlights = state.room.spotlights;
|
||||
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
|
||||
const boxes = spotlightsLength + Object.values(state.consumers)
|
||||
.filter((consumer) => consumer.source === 'screen').length;
|
||||
|
||||
return {
|
||||
peers,
|
||||
boxes,
|
||||
activeSpeakerName : state.room.activeSpeakerName
|
||||
activeSpeakerName : state.room.activeSpeakerName,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,19 @@ import ReactTooltip from 'react-tooltip';
|
|||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import CookieConsent from 'react-cookie-consent';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import { Appear } from './transitions';
|
||||
import Me from './Me';
|
||||
import Peers from './Peers';
|
||||
import AudioPeers from './PeerAudio/AudioPeers';
|
||||
import Notifications from './Notifications';
|
||||
import ToolAreaButton from './ToolArea/ToolAreaButton';
|
||||
// import ToolAreaButton from './ToolArea/ToolAreaButton';
|
||||
import ToolArea from './ToolArea/ToolArea';
|
||||
import FullScreenView from './FullScreenView';
|
||||
import VideoWindow from './VideoWindow/VideoWindow';
|
||||
import Draggable from 'react-draggable';
|
||||
import { idle } from '../utils';
|
||||
import Sidebar from './Sidebar';
|
||||
|
|
@ -65,7 +68,6 @@ class Room extends React.Component
|
|||
{
|
||||
const {
|
||||
room,
|
||||
toolAreaOpen,
|
||||
amActiveSpeaker,
|
||||
onRoomLinkCopy
|
||||
} = this.props;
|
||||
|
|
@ -81,11 +83,19 @@ class Room extends React.Component
|
|||
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<FullScreenView advancedMode={room.advancedMode} />
|
||||
<div className='room-wrapper'>
|
||||
<Notifications />
|
||||
<CookieConsent>
|
||||
This website uses cookies to enhance the user experience.
|
||||
</CookieConsent>
|
||||
|
||||
<ToolAreaButton />
|
||||
<FullScreenView advancedMode={room.advancedMode} />
|
||||
|
||||
<VideoWindow advancedMode={room.advancedMode} />
|
||||
|
||||
<div className='room-wrapper'>
|
||||
<div data-component='Logo' />
|
||||
<AudioPeers />
|
||||
|
||||
<Notifications />
|
||||
|
||||
{room.advancedMode ?
|
||||
<div className='state' data-tip='Server status'>
|
||||
|
|
@ -155,16 +165,8 @@ class Room extends React.Component
|
|||
delayHide={100}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('toolarea-wrapper', { open: toolAreaOpen })}
|
||||
>
|
||||
{toolAreaOpen ?
|
||||
<ToolArea
|
||||
advancedMode={room.advancedMode}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
|
||||
<ToolArea />
|
||||
</div>
|
||||
</Appear>
|
||||
</Fragment>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import classnames from 'classnames';
|
||||
import Spinner from 'react-spinner';
|
||||
|
||||
export default class PeerView extends React.Component
|
||||
export default class ScreenView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
|
|
@ -157,7 +157,7 @@ export default class PeerView extends React.Component
|
|||
}
|
||||
}
|
||||
|
||||
PeerView.propTypes =
|
||||
ScreenView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
advancedMode : PropTypes.bool,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as requestActions from '../redux/requestActions';
|
|||
import * as stateActions from '../redux/stateActions';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropdown from 'react-dropdown';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
const modes = [ {
|
||||
value : 'democratic',
|
||||
|
|
@ -22,12 +23,6 @@ const Settings = ({
|
|||
}) =>
|
||||
{
|
||||
let webcams;
|
||||
let webcamText;
|
||||
|
||||
if (me.canChangeWebcam)
|
||||
webcamText = 'Select camera';
|
||||
else
|
||||
webcamText = 'Unable to select camera';
|
||||
|
||||
if (me.webcamDevices)
|
||||
webcams = Array.from(me.webcamDevices.values());
|
||||
|
|
@ -51,11 +46,10 @@ const Settings = ({
|
|||
<div data-component='Settings'>
|
||||
<div className='settings'>
|
||||
<Dropdown
|
||||
disabled={!me.canChangeWebcam}
|
||||
options={webcams}
|
||||
value={findOption(webcams, me.selectedWebcam)}
|
||||
onChange={(webcam) => handleChangeWebcam(webcam.value)}
|
||||
placeholder={webcamText}
|
||||
placeholder={'Select camera'}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
|
|
@ -65,7 +59,14 @@ const Settings = ({
|
|||
onChange={(device) => handleChangeAudioDevice(device.value)}
|
||||
placeholder={audioDevicesText}
|
||||
/>
|
||||
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘a‘'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<input
|
||||
id='room-mode'
|
||||
type='checkbox'
|
||||
|
|
@ -73,7 +74,13 @@ const Settings = ({
|
|||
onChange={onToggleAdvancedMode}
|
||||
/>
|
||||
<label htmlFor='room-mode'>Advanced mode</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tip='keyboard shortcut: type a digit'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<Dropdown
|
||||
options={modes}
|
||||
value={findOption(modes, room.mode)}
|
||||
|
|
@ -81,6 +88,7 @@ const Settings = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,46 +4,52 @@ import { connect } from 'react-redux';
|
|||
import classnames from 'classnames';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import fscreen from 'fscreen';
|
||||
import FullScreen from './FullScreen';
|
||||
|
||||
class Sidebar extends Component
|
||||
{
|
||||
state = {
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.fullscreen = new FullScreen(document);
|
||||
this.state = {
|
||||
fullscreen : false
|
||||
};
|
||||
}
|
||||
|
||||
handleToggleFullscreen = () =>
|
||||
{
|
||||
if (fscreen.fullscreenElement)
|
||||
if (this.fullscreen.fullscreenElement)
|
||||
{
|
||||
fscreen.exitFullscreen();
|
||||
this.fullscreen.exitFullscreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
fscreen.requestFullscreen(document.documentElement);
|
||||
this.fullscreen.requestFullscreen(document.documentElement);
|
||||
}
|
||||
};
|
||||
|
||||
handleFullscreenChange = () =>
|
||||
{
|
||||
this.setState({
|
||||
fullscreen : fscreen.fullscreenElement !== null
|
||||
fullscreen : this.fullscreen.fullscreenElement !== null
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
if (fscreen.fullscreenEnabled)
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
fscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
>
|
||||
{fscreen.fullscreenEnabled && (
|
||||
{this.fullscreen.fullscreenEnabled && (
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
on : this.state.fullscreen
|
||||
})}
|
||||
onClick={this.handleToggleFullscreen}
|
||||
data-tip='Fullscreen'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
/>
|
||||
)}
|
||||
|
|
@ -99,6 +106,7 @@ class Sidebar extends Component
|
|||
<div
|
||||
className={classnames('button', 'screen', screenState)}
|
||||
data-tip={screenTip}
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() =>
|
||||
{
|
||||
|
|
@ -131,6 +139,7 @@ class Sidebar extends Component
|
|||
<div
|
||||
className='button logout'
|
||||
data-tip='Logout'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={onLogout}
|
||||
>
|
||||
|
|
@ -140,6 +149,7 @@ class Sidebar extends Component
|
|||
<div
|
||||
className='button login off'
|
||||
data-tip='Login'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={onLogin}
|
||||
/>
|
||||
|
|
@ -150,6 +160,7 @@ class Sidebar extends Component
|
|||
disabled : me.raiseHandInProgress
|
||||
})}
|
||||
data-tip='Raise hand'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => onToggleHand(!me.raiseHand)}
|
||||
/>
|
||||
|
|
@ -157,6 +168,7 @@ class Sidebar extends Component
|
|||
<div
|
||||
className={classnames('button', 'leave-meeting')}
|
||||
data-tip='Leave meeting'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => onLeaveMeeting()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
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 Chat from '../Chat/Chat';
|
||||
import Settings from '../Settings';
|
||||
import FileSharing from '../FileSharing';
|
||||
import TabHeader from './TabHeader';
|
||||
|
||||
class ToolArea extends React.Component
|
||||
{
|
||||
|
|
@ -18,88 +20,80 @@ class ToolArea extends React.Component
|
|||
{
|
||||
const {
|
||||
currentToolTab,
|
||||
toolAreaOpen,
|
||||
unreadMessages,
|
||||
unreadFiles,
|
||||
setToolTab
|
||||
toggleToolArea,
|
||||
unread
|
||||
} = this.props;
|
||||
|
||||
const VisibleTab = {
|
||||
chat : Chat,
|
||||
files : FileSharing,
|
||||
users : ParticipantList,
|
||||
settings : Settings
|
||||
}[currentToolTab];
|
||||
|
||||
return (
|
||||
<div data-component='ToolArea'>
|
||||
<div className='tabs'>
|
||||
<input
|
||||
type='radio'
|
||||
name='tabs'
|
||||
id='tab-chat'
|
||||
onChange={() =>
|
||||
{
|
||||
setToolTab('chat');
|
||||
}}
|
||||
checked={currentToolTab === 'chat'}
|
||||
<Fragment>
|
||||
<div
|
||||
className={classNames('toolarea-shade', {
|
||||
open : toolAreaOpen
|
||||
})}
|
||||
onClick={toggleToolArea}
|
||||
/>
|
||||
<label htmlFor='tab-chat'>
|
||||
Chat
|
||||
|
||||
{unreadMessages > 0 && (
|
||||
<span className='badge'>{unreadMessages}</span>
|
||||
<div
|
||||
data-component='ToolArea'
|
||||
className={classNames({
|
||||
open : toolAreaOpen
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className='toolarea-button'
|
||||
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 className='tab'>
|
||||
<Chat />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type='radio'
|
||||
name='tabs'
|
||||
id='tab-files'
|
||||
onChange={() => setToolTab('files')}
|
||||
checked={currentToolTab === 'files'}
|
||||
<div className='tab-headers'>
|
||||
<TabHeader
|
||||
id='chat'
|
||||
name='Chat'
|
||||
badge={unreadMessages}
|
||||
/>
|
||||
<label htmlFor='tab-files'>
|
||||
Files
|
||||
|
||||
{unreadFiles > 0 && (
|
||||
<span className='badge'>{unreadFiles}</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
<div className='tab'>
|
||||
<FileSharing />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type='radio'
|
||||
name='tabs'
|
||||
id='tab-users'
|
||||
onChange={() =>
|
||||
{
|
||||
setToolTab('users');
|
||||
}}
|
||||
checked={currentToolTab === 'users'}
|
||||
<TabHeader
|
||||
id='files'
|
||||
name='Files'
|
||||
badge={unreadFiles}
|
||||
/>
|
||||
<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'}
|
||||
<TabHeader
|
||||
id='users'
|
||||
name='Users'
|
||||
/>
|
||||
<label htmlFor='tab-settings'>Settings</label>
|
||||
|
||||
<TabHeader
|
||||
id='settings'
|
||||
name='Settings'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='tab'>
|
||||
<Settings />
|
||||
</div>
|
||||
<VisibleTab />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,17 +104,26 @@ ToolArea.propTypes =
|
|||
currentToolTab : PropTypes.string.isRequired,
|
||||
setToolTab : PropTypes.func.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) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab,
|
||||
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 = {
|
||||
setToolTab : toolTabActions.setToolTab
|
||||
setToolTab : stateActions.setToolTab,
|
||||
toggleToolArea : stateActions.toggleToolArea,
|
||||
closeToolArea : stateActions.closeToolArea
|
||||
};
|
||||
|
||||
const ToolAreaContainer = connect(
|
||||
|
|
|
|||
|
|
@ -11,19 +11,24 @@ class ToolAreaButton extends React.Component
|
|||
const {
|
||||
toolAreaOpen,
|
||||
toggleToolArea,
|
||||
unread
|
||||
unread,
|
||||
visible
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='ToolAreaButton' className={classnames({ on: toolAreaOpen })}>
|
||||
<div
|
||||
className={classnames('button toolarea-button room-controls', {
|
||||
data-component='ToolAreaButton'
|
||||
className={classnames('room-controls', {
|
||||
on : toolAreaOpen,
|
||||
visible : this.props.visible
|
||||
visible
|
||||
})}
|
||||
data-tip='Toggle tool area'
|
||||
>
|
||||
<div
|
||||
className={classnames('button toolarea-button', {
|
||||
on : toolAreaOpen
|
||||
})}
|
||||
data-tip='Open tools'
|
||||
data-type='dark'
|
||||
data-for='globaltip'
|
||||
onClick={() => toggleToolArea()}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -23,7 +23,6 @@ export const Me = PropTypes.shape(
|
|||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
canChangeWebcam : PropTypes.bool.isRequired,
|
||||
webcamInProgress : PropTypes.bool.isRequired,
|
||||
audioOnly : PropTypes.bool.isRequired,
|
||||
audioOnlyInProgress : PropTypes.bool.isRequired,
|
||||
|
|
|
|||
|
|
@ -22,3 +22,13 @@ export function setDevices({ webcamEnabled })
|
|||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
||||
}
|
||||
|
||||
export function setAudioDevice({ audioDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
|
||||
}
|
||||
|
||||
export function setVideoDevice({ videoDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function run()
|
|||
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new UrlParse(window.location.href, true);
|
||||
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';
|
||||
let displayName = urlParser.query.displayName;
|
||||
const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
device : { flag: 'firefox', name: 'Firefox', version: '61' },
|
||||
canSendMic : true,
|
||||
canSendWebcam : true,
|
||||
canChangeWebcam : false,
|
||||
webcamInProgress : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ const initialState =
|
|||
showSettings : false,
|
||||
advancedMode : false,
|
||||
fullScreenConsumer : null, // ConsumerID
|
||||
windowConsumer : null, // ConsumerID
|
||||
toolbarsVisible : true,
|
||||
mode : 'democratic',
|
||||
selectedPeerName : null
|
||||
selectedPeerName : null,
|
||||
spotlights : []
|
||||
};
|
||||
|
||||
const room = (state = initialState, action) =>
|
||||
|
|
@ -61,6 +63,17 @@ const room = (state = initialState, action) =>
|
|||
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':
|
||||
{
|
||||
const { toolbarsVisible } = action.payload;
|
||||
|
|
@ -83,6 +96,13 @@ const room = (state = initialState, action) =>
|
|||
};
|
||||
}
|
||||
|
||||
case 'SET_SPOTLIGHTS':
|
||||
{
|
||||
const { spotlights } = action.payload;
|
||||
|
||||
return { ...state, spotlights };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,22 @@ const toolarea = (state = initialState, action) =>
|
|||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
||||
case 'OPEN_TOOL_AREA':
|
||||
{
|
||||
const toolAreaOpen = true;
|
||||
const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
||||
case 'CLOSE_TOOL_AREA':
|
||||
{
|
||||
const toolAreaOpen = false;
|
||||
|
||||
return { ...state, toolAreaOpen };
|
||||
}
|
||||
|
||||
case 'SET_TOOL_TAB':
|
||||
{
|
||||
const { toolTab } = action.payload;
|
||||
|
|
|
|||
|
|
@ -221,6 +221,14 @@ export const sendFile = (file, name, picture) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setSelectedPeer = (selectedPeerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REQUEST_SELECTED_PEER',
|
||||
payload : { selectedPeerName }
|
||||
};
|
||||
};
|
||||
|
||||
// This returns a redux-thunk action (a function).
|
||||
export const notify = ({ type = 'info', text, timeout }) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -237,6 +237,15 @@ export default ({ dispatch, getState }) => (next) =>
|
|||
client.sendFile(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'REQUEST_SELECTED_PEER':
|
||||
{
|
||||
const { selectedPeerName } = action.payload;
|
||||
|
||||
client.setSelectedPeer(selectedPeerName);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
|
|
|||
|
|
@ -62,14 +62,6 @@ export const setAudioDevices = (devices) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const setCanChangeWebcam = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CAN_CHANGE_WEBCAM',
|
||||
payload : flag
|
||||
};
|
||||
};
|
||||
|
||||
export const setWebcamDevices = (devices) =>
|
||||
{
|
||||
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) =>
|
||||
{
|
||||
return {
|
||||
|
|
@ -383,6 +389,14 @@ export const toggleConsumerFullscreen = (consumerId) =>
|
|||
};
|
||||
};
|
||||
|
||||
export const toggleConsumerWindow = (consumerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_WINDOW_CONSUMER',
|
||||
payload : { consumerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setToolbarsVisible = (toolbarsVisible) => ({
|
||||
type : 'SET_TOOLBARS_VISIBLE',
|
||||
payload : { toolbarsVisible }
|
||||
|
|
@ -474,7 +488,14 @@ export const loggedIn = () =>
|
|||
type : 'LOGGED_IN'
|
||||
});
|
||||
|
||||
export const setSelectedPeer = (selectedPeerName) => ({
|
||||
export const setSelectedPeer = (selectedPeerName) =>
|
||||
({
|
||||
type : 'SET_SELECTED_PEER',
|
||||
payload : { selectedPeerName }
|
||||
});
|
||||
|
||||
export const setSpotlights = (spotlights) =>
|
||||
({
|
||||
type : 'SET_SPOTLIGHTS',
|
||||
payload : { spotlights }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ if (process.env.NODE_ENV === 'development')
|
|||
{
|
||||
const reduxLogger = createLogger(
|
||||
{
|
||||
// filter VOLUME level actions from log
|
||||
predicate : (getState, action) => ! (action.type == 'SET_PRODUCER_VOLUME'
|
||||
|| action.type == 'SET_CONSUMER_VOLUME'),
|
||||
duration : true,
|
||||
timestamp : false,
|
||||
level : 'log',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export function getProtooUrl(peerName, roomId)
|
||||
export function getSignalingUrl(peerName, roomId)
|
||||
{
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,54 +7,56 @@
|
|||
"license": "MIT",
|
||||
"main": "lib/index.jsx",
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"classnames": "^2.2.6",
|
||||
"create-torrent": "^3.32.1",
|
||||
"debug": "^3.1.0",
|
||||
"debug": "^4.1.0",
|
||||
"domready": "^1.0.8",
|
||||
"drag-drop": "^4.2.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"fscreen": "^1.0.2",
|
||||
"hark": "^1.2.2",
|
||||
"js-cookie": "^2.2.0",
|
||||
"magnet-uri": "^5.2.3",
|
||||
"marked": "^0.4.0",
|
||||
"mediasoup-client": "^2.1.1",
|
||||
"marked": "^0.5.1",
|
||||
"mediasoup-client": "^2.3.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"protoo-client": "^3.0.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-dom": "^16.4.1",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-draggable": "^3.0.5",
|
||||
"react-dropdown": "^1.5.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-spinner": "^0.2.7",
|
||||
"react-tooltip": "^3.6.1",
|
||||
"react-transition-group": "^2.4.0",
|
||||
"redux": "^4.0.0",
|
||||
"react-tooltip": "^3.9.0",
|
||||
"react-transition-group": "^2.5.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"riek": "^1.1.0",
|
||||
"url-parse": "^1.4.1",
|
||||
"webtorrent": "^0.101.0"
|
||||
"socket.io-client": "^2.1.1",
|
||||
"url-parse": "^1.4.3",
|
||||
"webtorrent": "^0.102.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.6",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react-app": "^3.1.2",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"babelify": "^8.0.0",
|
||||
"browser-sync": "^2.24.6",
|
||||
"browserify": "^16.2.2",
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"browser-sync": "^2.26.3",
|
||||
"browserify": "^16.2.3",
|
||||
"del": "^3.0.0",
|
||||
"envify": "^4.1.0",
|
||||
"eslint": "^5.2.0",
|
||||
"eslint-plugin-import": "^2.13.0",
|
||||
"eslint-plugin-react": "^7.10.0",
|
||||
"eslint": "^5.7.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-control-statements": "^2.2.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-change": "^1.0.0",
|
||||
"gulp-css-base64": "^1.3.4",
|
||||
|
|
@ -65,13 +67,13 @@
|
|||
"gulp-rename": "^1.4.0",
|
||||
"gulp-stylus": "^2.7.0",
|
||||
"gulp-touch-cmd": "0.0.1",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-uglify-es": "^1.0.4",
|
||||
"gulp-util": "^3.0.8",
|
||||
"lodash": "^4.17.10",
|
||||
"mkdirp": "^0.5.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nib": "^1.1.2",
|
||||
"supports-color": "^5.4.0",
|
||||
"supports-color": "^5.5.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"watchify": "^3.11.0"
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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.
|
|
@ -0,0 +1,6 @@
|
|||
[data-component='AudioPeers'] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
@ -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'] {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-component='MessageList'] {
|
||||
background-color: rgba(#000, 0.1);
|
||||
height: 91vmin;
|
||||
overflow-y: scroll;
|
||||
padding-top: 5px;
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
> .message {
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
> .client {
|
||||
margin-left: auto;
|
||||
|
|
@ -79,10 +25,10 @@
|
|||
> .client, > .response {
|
||||
background-color: rgba(#000, 0.1);
|
||||
border-radius: 5px;
|
||||
max-width: 215px;
|
||||
max-width: 85%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
padding: 0.5rem;
|
||||
|
||||
> .message-avatar {
|
||||
height: 2rem;
|
||||
|
|
@ -90,41 +36,68 @@
|
|||
}
|
||||
|
||||
> .message-content {
|
||||
padding-left: 6px;
|
||||
padding-left: 0.5rem;
|
||||
|
||||
> .message-text {
|
||||
font-size: 1.3vmin;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
> .message-time {
|
||||
font-size: 1vmin;
|
||||
font-size: 0.8rem;
|
||||
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'] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
background-color: rgba(#000, 0.1);
|
||||
height: 6vmin;
|
||||
padding: 0.5vmin;
|
||||
border-radius: 0 0 5px 5px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.5rem;
|
||||
height: 3rem;
|
||||
|
||||
> .new-message {
|
||||
width: 100%;
|
||||
width: 80%;
|
||||
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(#000, 0.1);
|
||||
color: #fff;
|
||||
height: 30px;
|
||||
padding-left: 10px;
|
||||
font-size: 1.4vmin;
|
||||
font-size: 1rem;
|
||||
margin-right: 1vmin;
|
||||
border-radius: 0.5vmin;
|
||||
padding-left: 1vmin;
|
||||
color: #000;
|
||||
|
||||
&.focus {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
[data-component='FileSharing'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
> .sharing-toolbar {
|
||||
> .share-file {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background: #252525;
|
||||
border: 1px solid #151515;
|
||||
background: #aef;
|
||||
padding: 1rem;
|
||||
border-bottom: 5px solid #151515;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-radius: 1vmin;
|
||||
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
|
|
@ -21,7 +20,8 @@
|
|||
|
||||
> .shared-files {
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
> .file-entry {
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
|
|
@ -29,7 +29,10 @@
|
|||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,10 +44,15 @@
|
|||
height: 5vmin;
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
&.exitfullscreen {
|
||||
background-image: url('/resources/images/icon_fullscreen_exit_black.svg');
|
||||
background-color: rgba(#fff, 0.7);
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
background-image: url('/resources/images/icon_fullscreen_black.svg');
|
||||
background-color: rgba(#fff, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -43,13 +43,17 @@
|
|||
background-repeat: no-repeat;
|
||||
background-color: rgba(#000, 0.5);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition-property: opacity, background-color;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
&.visible {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
+desktop() {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
[data-component='ParticipantList'] {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
|
||||
> .list {
|
||||
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \
|
||||
0 4px 20px 0 rgba(0,0,0,0.19);
|
||||
box-shadow: 0 2px 5px 2px rgba(0,0,0,0.2);
|
||||
background-color: #fff;
|
||||
|
||||
> .list-header {
|
||||
padding: 0.5rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
> .list-item {
|
||||
padding: 0.5vmin;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #CBCBCB;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
|
@ -17,7 +23,7 @@
|
|||
}
|
||||
|
||||
&.selected {
|
||||
border-bottom-color: #377EFF;
|
||||
background-color: #377eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,21 +31,19 @@
|
|||
|
||||
[data-component='ListPeer'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .indicators {
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction:; row;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 0.4vmin;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
> .icon {
|
||||
flex: 0 0 auto;
|
||||
margin: 0.2vmin;
|
||||
margin: 0.3rem;
|
||||
border-radius: 2px;
|
||||
background-position: center;
|
||||
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 {
|
||||
float: right;
|
||||
display: flex;
|
||||
|
|
@ -85,7 +124,7 @@
|
|||
|
||||
> .button {
|
||||
flex: 0 0 auto;
|
||||
margin: 0.2vmin;
|
||||
margin: 0.3rem;
|
||||
border-radius: 2px;
|
||||
background-position: center;
|
||||
background-size: 75%;
|
||||
|
|
@ -176,10 +215,10 @@
|
|||
}
|
||||
|
||||
> .peer-info {
|
||||
font-size: 1.4vmin;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
display: flex;
|
||||
padding: 1vmin;
|
||||
padding-left: 0.5rem;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,9 +189,36 @@
|
|||
background-image: url('/resources/images/icon_fullscreen_black.svg');
|
||||
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 {
|
||||
position: absolute;
|
||||
|
|
@ -210,7 +237,7 @@
|
|||
border-radius: 6px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
font-size: 15px;
|
||||
font-size: 20px;
|
||||
color: rgba(#fff, 0.55);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@
|
|||
&.active-speaker {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
> .peer-content {
|
||||
border: 1px solid #377eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@
|
|||
background-color: rgba(#fff, 0.2);
|
||||
|
||||
+desktop() {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 124px;
|
||||
bottom: 1%;
|
||||
left: 1%;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 110px;
|
||||
bottom: 1%;
|
||||
left: 1%;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
|
|
@ -152,33 +152,18 @@
|
|||
}
|
||||
|
||||
+desktop() {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
top: 6%;
|
||||
left:1%;
|
||||
border: 1px solid rgba(#fff, 0.15);
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
top: 6%;
|
||||
left: 1%;
|
||||
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 {
|
||||
|
|
@ -210,6 +195,7 @@
|
|||
outline: none;
|
||||
padding: 8px 52px 8px 10px;
|
||||
transition: all 200ms ease;
|
||||
box-shadow: 0vmin 0vmin 0.2vmin 0vmin rgba(17,17,17,0.5);
|
||||
}
|
||||
|
||||
.Dropdown-control:hover {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@
|
|||
align-items: center;
|
||||
|
||||
+desktop() {
|
||||
left: 20px;
|
||||
width: 36px;
|
||||
left: 1.0em;
|
||||
width: 2.6em;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
left: 10px;
|
||||
width: 32px;
|
||||
left: 0.5em;
|
||||
width: 2.6em;
|
||||
}
|
||||
|
||||
> .button {
|
||||
|
|
@ -34,13 +34,13 @@
|
|||
justify-content: center;
|
||||
|
||||
+desktop() {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
height: 2.5em;
|
||||
width: 2.5em;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
height: 2.5em;
|
||||
width: 2.5em;
|
||||
}
|
||||
|
||||
&.on {
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
}
|
||||
|
||||
&.leave-meeting {
|
||||
background-image: url('/resources/images/leave-meeting.svg');
|
||||
background-image: url('/resources/images/cancel.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'] {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
z-index: 1020;
|
||||
right: 0;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
padding: 2rem;
|
||||
margin: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: right 0.3s;
|
||||
|
||||
&.on {
|
||||
right: 25%;
|
||||
}
|
||||
|
||||
> .button {
|
||||
flex: 0 0 auto;
|
||||
margin: 4px 0;
|
||||
|
|
@ -78,59 +270,55 @@
|
|||
[data-component='ToolArea'] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
|
||||
> .tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
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;
|
||||
|
||||
> label {
|
||||
order: 1;
|
||||
display: block;
|
||||
padding: 1vmin 0 0.8vmin 0;
|
||||
> .tab-headers {
|
||||
display: flex;
|
||||
background: #ddd;
|
||||
flex-shrink: 0;
|
||||
|
||||
> .tab-header {
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
background: rgba(0,0,0,0.3);
|
||||
font-weight: bold;
|
||||
transition: background ease 0.2s;
|
||||
padding: 1rem;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
width: 25%;
|
||||
font-size: 1.3vmin;
|
||||
height: 3vmin;
|
||||
|
||||
&.checked {
|
||||
background: #fff;
|
||||
border-radius: 1vmin 1vmin 0vmin 0vmin;
|
||||
box-shadow: 0.5vmin 0vmin 1vmin -0.5vmin #aaa;
|
||||
}
|
||||
|
||||
> .badge {
|
||||
padding: 0.1vmin 1vmin;
|
||||
padding: 0.2rem 0.6rem;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 1.2vmin;
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
background-color: #b12525;
|
||||
border-radius: 2px;
|
||||
margin-left: 1vmin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .tab {
|
||||
order: 99;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
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);
|
||||
}
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,10 @@ global-reset();
|
|||
|
||||
html {
|
||||
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-weight: 300;
|
||||
|
||||
+desktop() {
|
||||
font-size: 16px;
|
||||
}
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
|
||||
+mobile() {
|
||||
font-size: 12px;
|
||||
|
|
@ -30,6 +22,17 @@ html {
|
|||
body {
|
||||
height: 100%;
|
||||
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 {
|
||||
|
|
@ -39,11 +42,13 @@ body {
|
|||
|
||||
// Components
|
||||
@import './components/Room';
|
||||
@import './components/Logo';
|
||||
@import './components/Sidebar';
|
||||
@import './components/Me';
|
||||
@import './components/Peers';
|
||||
@import './components/Peer';
|
||||
@import './components/PeerView';
|
||||
@import './components/HiddenPeersView';
|
||||
@import './components/ScreenView';
|
||||
@import './components/Notifications';
|
||||
@import './components/Chat';
|
||||
|
|
@ -54,6 +59,7 @@ body {
|
|||
@import './components/FullView';
|
||||
@import './components/Filmstrip';
|
||||
@import './components/FileSharing';
|
||||
@import './components/AudioPeers';
|
||||
|
||||
// Hack to detect in JS the current media query
|
||||
#multiparty-meeting-media-query-detector {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ Description=multiparty-meeting is a audio / video meeting service running in the
|
|||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/src/multiparty-meeting/server.js
|
||||
ExecStart=/usr/local/src/multiparty-meeting/server/server.js
|
||||
Restart=always
|
||||
User=nobody
|
||||
Group=nogroup
|
||||
Environment=PATH=/usr/bin:/usr/local/bin
|
||||
Environment=NODE_ENV=production
|
||||
WorkingDirectory=/usr/local/src/multiparty-meeting
|
||||
WorkingDirectory=/usr/local/src/multiparty-meeting/server
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
||||
|
|
@ -15,7 +15,11 @@ module.exports =
|
|||
key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem`
|
||||
},
|
||||
// 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 : [
|
||||
{
|
||||
urls : [
|
||||
|
|
@ -59,20 +63,22 @@ module.exports =
|
|||
useinbandfec : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
kind : 'video',
|
||||
name : 'VP8',
|
||||
clockRate : 90000
|
||||
}
|
||||
// {
|
||||
// kind : 'video',
|
||||
// name : 'H264',
|
||||
// clockRate : 90000,
|
||||
// parameters :
|
||||
// {
|
||||
// 'packetization-mode' : 1
|
||||
// }
|
||||
// name : 'VP8',
|
||||
// clockRate : 90000
|
||||
// }
|
||||
{
|
||||
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).
|
||||
maxBitrate : 500000
|
||||
|
|
|
|||
|
|
@ -1,30 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
const headers = {
|
||||
"access-control-allow-origin": "*",
|
||||
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"access-control-allow-headers": "content-type, accept",
|
||||
"access-control-max-age": 10,
|
||||
"Content-Type": "application/json"
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'access-control-allow-headers': 'content-type, accept',
|
||||
'access-control-max-age': 10,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
exports.prepareResponse = function(req, cb) {
|
||||
var data = "";
|
||||
req.on('data', function(chunk) { data += chunk; });
|
||||
req.on('end', function() { cb(data); });
|
||||
exports.prepareResponse = (req, cb) =>
|
||||
{
|
||||
let 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);
|
||||
res.end(data);
|
||||
};
|
||||
|
||||
exports.send404 = function(res) {
|
||||
exports.send404 = (res) =>
|
||||
{
|
||||
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 });
|
||||
res.end();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const protooServer = require('protoo-server');
|
||||
const WebTorrent = require('webtorrent-hybrid');
|
||||
const Logger = require('./Logger');
|
||||
const config = require('../config');
|
||||
|
||||
|
|
@ -12,17 +10,9 @@ const BITRATE_FACTOR = 0.75;
|
|||
|
||||
const logger = new Logger('Room');
|
||||
|
||||
const torrentClient = new WebTorrent({
|
||||
tracker : {
|
||||
rtcConfig : {
|
||||
iceServers : config.turnServers
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class Room extends EventEmitter
|
||||
{
|
||||
constructor(roomId, mediaServer)
|
||||
constructor(roomId, mediaServer, io)
|
||||
{
|
||||
logger.info('constructor() [roomId:"%s"]', roomId);
|
||||
|
||||
|
|
@ -39,11 +29,14 @@ class Room extends EventEmitter
|
|||
|
||||
this._fileHistory = [];
|
||||
|
||||
this._lastN = [];
|
||||
|
||||
this._io = io;
|
||||
|
||||
this._signalingPeers = new Map();
|
||||
|
||||
try
|
||||
{
|
||||
// Protoo Room instance.
|
||||
this._protooRoom = new protooServer.Room();
|
||||
|
||||
// mediasoup Room instance.
|
||||
this._mediaRoom = mediaServer.Room(config.mediasoup.mediaCodecs);
|
||||
}
|
||||
|
|
@ -75,9 +68,15 @@ class Room extends EventEmitter
|
|||
|
||||
this._closed = true;
|
||||
|
||||
// Close the protoo Room.
|
||||
if (this._protooRoom)
|
||||
this._protooRoom.close();
|
||||
// Close the signalingPeers
|
||||
if (this._signalingPeers)
|
||||
for (let peer of this._signalingPeers)
|
||||
{
|
||||
if (peer.socket)
|
||||
peer.socket.disconnect();
|
||||
};
|
||||
|
||||
this._signalingPeers.clear();
|
||||
|
||||
// Close the mediasoup Room.
|
||||
if (this._mediaRoom)
|
||||
|
|
@ -93,31 +92,63 @@ class Room extends EventEmitter
|
|||
return;
|
||||
|
||||
logger.info(
|
||||
'logStatus() [room id:"%s", protoo peers:%s, mediasoup peers:%s]',
|
||||
'logStatus() [room id:"%s", peers:%s, mediasoup peers:%s]',
|
||||
this._roomId,
|
||||
this._protooRoom.peers.length,
|
||||
this._signalingPeers.length,
|
||||
this._mediaRoom.peers.length);
|
||||
}
|
||||
|
||||
handleConnection(peerName, transport)
|
||||
handleConnection(peerName, socket)
|
||||
{
|
||||
logger.info('handleConnection() [peerName:"%s"]', peerName);
|
||||
|
||||
if (this._protooRoom.hasPeer(peerName))
|
||||
if (this._signalingPeers.has(peerName))
|
||||
{
|
||||
logger.warn(
|
||||
'handleConnection() | there is already a peer with same peerName, ' +
|
||||
'closing the previous one [peerName:"%s"]',
|
||||
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()
|
||||
|
|
@ -134,6 +165,14 @@ class Room extends EventEmitter
|
|||
|
||||
this._currentActiveSpeaker = activePeer;
|
||||
|
||||
const index = this._lastN.indexOf(activePeer.name);
|
||||
|
||||
if (index > -1) // We have this speaker in the list, move to front
|
||||
{
|
||||
this._lastN.splice(index, 1);
|
||||
this._lastN = [activePeer.name].concat(this._lastN);
|
||||
}
|
||||
|
||||
const activeVideoProducer = activePeer.producers
|
||||
.find((producer) => producer.kind === 'video');
|
||||
|
||||
|
|
@ -173,185 +212,175 @@ class Room extends EventEmitter
|
|||
}
|
||||
}
|
||||
|
||||
// Spread to others via protoo.
|
||||
this._protooRoom.spread(
|
||||
'active-speaker',
|
||||
{
|
||||
this._io.to(this._roomId).emit('active-speaker', {
|
||||
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(
|
||||
'protoo "request" event [method:%s, peer:"%s"]',
|
||||
request.method, protooPeer.id);
|
||||
|
||||
switch (request.method)
|
||||
{
|
||||
case 'mediasoup-request':
|
||||
{
|
||||
const mediasoupRequest = request.data;
|
||||
const mediasoupRequest = request;
|
||||
|
||||
this._handleMediasoupClientRequest(
|
||||
protooPeer, mediasoupRequest, accept, reject);
|
||||
signalingPeer, mediasoupRequest, cb);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'mediasoup-notification':
|
||||
signalingPeer.socket.on('mediasoup-notification', (request, cb) =>
|
||||
{
|
||||
accept();
|
||||
// Return no error
|
||||
cb(null);
|
||||
|
||||
const mediasoupNotification = request.data;
|
||||
const mediasoupNotification = request;
|
||||
|
||||
this._handleMediasoupClientNotification(
|
||||
protooPeer, mediasoupNotification);
|
||||
signalingPeer, mediasoupNotification);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'change-display-name':
|
||||
signalingPeer.socket.on('change-display-name', (request, cb) =>
|
||||
{
|
||||
accept();
|
||||
// Return no error
|
||||
cb(null);
|
||||
|
||||
const { displayName } = request.data;
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
const { displayName } = request;
|
||||
const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName);
|
||||
const oldDisplayName = mediaPeer.appData.displayName;
|
||||
|
||||
mediaPeer.appData.displayName = displayName;
|
||||
|
||||
// Spread to others via protoo.
|
||||
this._protooRoom.spread(
|
||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||
'display-name-changed',
|
||||
{
|
||||
peerName : protooPeer.id,
|
||||
peerName : signalingPeer.peerName,
|
||||
displayName : displayName,
|
||||
oldDisplayName : oldDisplayName
|
||||
},
|
||||
[ protooPeer ]);
|
||||
|
||||
break;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
case 'change-profile-picture':
|
||||
signalingPeer.socket.on('change-profile-picture', (request, cb) =>
|
||||
{
|
||||
accept();
|
||||
// Return no error
|
||||
cb(null);
|
||||
|
||||
this._protooRoom.spread('profile-picture-changed', {
|
||||
peerName : protooPeer.id,
|
||||
picture : request.data.picture
|
||||
}, [ protooPeer ]);
|
||||
|
||||
break;
|
||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||
'profile-picture-changed',
|
||||
{
|
||||
peerName : signalingPeer.peerName,
|
||||
picture : request.picture
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
case 'chat-message':
|
||||
signalingPeer.socket.on('chat-message', (request, cb) =>
|
||||
{
|
||||
accept();
|
||||
// Return no error
|
||||
cb(null);
|
||||
|
||||
const { chatMessage } = request.data;
|
||||
const { chatMessage } = request;
|
||||
|
||||
this._chatHistory.push(chatMessage);
|
||||
|
||||
// Spread to others via protoo.
|
||||
this._protooRoom.spread(
|
||||
// Spread to others
|
||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||
'chat-message-receive',
|
||||
{
|
||||
peerName : protooPeer.id,
|
||||
peerName : signalingPeer.peerName,
|
||||
chatMessage : chatMessage
|
||||
},
|
||||
[ protooPeer ]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'chat-history':
|
||||
{
|
||||
accept();
|
||||
|
||||
protooPeer.send(
|
||||
'chat-history-receive',
|
||||
{ chatHistory: this._chatHistory }
|
||||
);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'send-file':
|
||||
signalingPeer.socket.on('server-history', (request, cb) =>
|
||||
{
|
||||
accept();
|
||||
// Return to sender
|
||||
cb(
|
||||
null,
|
||||
{
|
||||
chatHistory : this._chatHistory,
|
||||
fileHistory : this._fileHistory,
|
||||
lastN : this._lastN
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const fileData = request.data.file;
|
||||
signalingPeer.socket.on('send-file', (request, cb) =>
|
||||
{
|
||||
// Return no error
|
||||
cb(null);
|
||||
|
||||
const fileData = request.file;
|
||||
|
||||
this._fileHistory.push(fileData);
|
||||
|
||||
if (!torrentClient.get(fileData.file.magnet))
|
||||
// Spread to others
|
||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||
'file-receive',
|
||||
{
|
||||
torrentClient.add(fileData.file.magnet);
|
||||
}
|
||||
|
||||
this._protooRoom.spread('file-receive', {
|
||||
peerName : signalingPeer.peerName,
|
||||
file : fileData
|
||||
}, [ protooPeer ]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'file-history':
|
||||
{
|
||||
accept();
|
||||
|
||||
protooPeer.send('file-history-receive', {
|
||||
fileHistory : this._fileHistory
|
||||
);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'raisehand-message':
|
||||
signalingPeer.socket.on('raisehand-message', (request, cb) =>
|
||||
{
|
||||
accept();
|
||||
// Return no error
|
||||
cb(null);
|
||||
|
||||
const { raiseHandState } = request.data;
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
const { raiseHandState } = request;
|
||||
const { mediaPeer } = signalingPeer;
|
||||
|
||||
mediaPeer.appData.raiseHandState = request.data.raiseHandState;
|
||||
// Spread to others via protoo.
|
||||
this._protooRoom.spread(
|
||||
mediaPeer.appData.raiseHandState = raiseHandState;
|
||||
// Spread to others
|
||||
signalingPeer.socket.broadcast.to(this._roomId).emit(
|
||||
'raisehand-message',
|
||||
{
|
||||
peerName : protooPeer.id,
|
||||
peerName : signalingPeer.peerName,
|
||||
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('request-consumer-keyframe', (request, cb) =>
|
||||
{
|
||||
logger.debug('protoo Peer "close" event [peer:"%s"]', protooPeer.id);
|
||||
cb(null);
|
||||
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
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)
|
||||
mediaPeer.close();
|
||||
|
||||
const index = this._lastN.indexOf(signalingPeer.peerName);
|
||||
|
||||
if (index > -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._lastN.splice(index, 1);
|
||||
}
|
||||
|
||||
// If this is the latest peer in the room, close the room.
|
||||
// However wait a bit (for reconnections).
|
||||
setTimeout(() =>
|
||||
|
|
@ -371,12 +400,11 @@ class Room extends EventEmitter
|
|||
});
|
||||
}
|
||||
|
||||
_handleMediaPeer(protooPeer, mediaPeer)
|
||||
_handleMediaPeer(signalingPeer, mediaPeer)
|
||||
{
|
||||
mediaPeer.on('notify', (notification) =>
|
||||
{
|
||||
protooPeer.send('mediasoup-notification', notification)
|
||||
.catch(() => {});
|
||||
signalingPeer.socket.emit('mediasoup-notification', notification);
|
||||
});
|
||||
|
||||
mediaPeer.on('newtransport', (transport) =>
|
||||
|
|
@ -424,12 +452,11 @@ class Room extends EventEmitter
|
|||
// Notify about the existing active speaker.
|
||||
if (this._currentActiveSpeaker)
|
||||
{
|
||||
protooPeer.send(
|
||||
signalingPeer.socket.emit(
|
||||
'active-speaker',
|
||||
{
|
||||
peerName : this._currentActiveSpeaker.name
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,19 +522,19 @@ class Room extends EventEmitter
|
|||
consumer.setPreferredProfile('low');
|
||||
}
|
||||
|
||||
_handleMediasoupClientRequest(protooPeer, request, accept, reject)
|
||||
_handleMediasoupClientRequest(signalingPeer, request, cb)
|
||||
{
|
||||
logger.debug(
|
||||
'mediasoup-client request [method:%s, peer:"%s"]',
|
||||
request.method, protooPeer.id);
|
||||
request.method, signalingPeer.peerName);
|
||||
|
||||
switch (request.method)
|
||||
{
|
||||
case 'queryRoom':
|
||||
{
|
||||
this._mediaRoom.receiveRequest(request)
|
||||
.then((response) => accept(response))
|
||||
.catch((error) => reject(500, error.toString()));
|
||||
.then((response) => cb(null, response))
|
||||
.catch((error) => cb(error.toString()));
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -517,15 +544,15 @@ class Room extends EventEmitter
|
|||
// TODO: Handle appData. Yes?
|
||||
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;
|
||||
}
|
||||
else if (protooPeer.data.mediaPeer)
|
||||
else if (signalingPeer.mediaPeer)
|
||||
{
|
||||
reject(500, 'already have a mediasoup Peer');
|
||||
cb('already have a mediasoup Peer');
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -533,18 +560,18 @@ class Room extends EventEmitter
|
|||
this._mediaRoom.receiveRequest(request)
|
||||
.then((response) =>
|
||||
{
|
||||
accept(response);
|
||||
cb(null, response);
|
||||
|
||||
// Get the newly created mediasoup Peer.
|
||||
const mediaPeer = this._mediaRoom.getPeerByName(peerName);
|
||||
|
||||
protooPeer.data.mediaPeer = mediaPeer;
|
||||
signalingPeer.mediaPeer = mediaPeer;
|
||||
|
||||
this._handleMediaPeer(protooPeer, mediaPeer);
|
||||
this._handleMediaPeer(signalingPeer, mediaPeer);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
reject(500, error.toString());
|
||||
cb(error.toString());
|
||||
});
|
||||
|
||||
break;
|
||||
|
|
@ -552,7 +579,7 @@ class Room extends EventEmitter
|
|||
|
||||
default:
|
||||
{
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
const { mediaPeer } = signalingPeer;
|
||||
|
||||
if (!mediaPeer)
|
||||
{
|
||||
|
|
@ -560,25 +587,25 @@ class Room extends EventEmitter
|
|||
'cannot handle mediasoup request, no mediasoup Peer [method:"%s"]',
|
||||
request.method);
|
||||
|
||||
reject(400, 'no mediasoup Peer');
|
||||
cb('no mediasoup Peer');
|
||||
}
|
||||
|
||||
mediaPeer.receiveRequest(request)
|
||||
.then((response) => accept(response))
|
||||
.catch((error) => reject(500, error.toString()));
|
||||
.then((response) => cb(null, response))
|
||||
.catch((error) => cb(error.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleMediasoupClientNotification(protooPeer, notification)
|
||||
_handleMediasoupClientNotification(signalingPeer, notification)
|
||||
{
|
||||
logger.debug(
|
||||
'mediasoup-client notification [method:%s, peer:"%s"]',
|
||||
notification.method, protooPeer.id);
|
||||
notification.method, signalingPeer.peerName);
|
||||
|
||||
// NOTE: mediasoup-client just sends notifications with target 'peer',
|
||||
// so first of all, get the mediasoup Peer.
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
const { mediaPeer } = signalingPeer;
|
||||
|
||||
if (!mediaPeer)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ function handleTransport(transport, baseEvent, stream)
|
|||
});
|
||||
|
||||
const statsInterval = setInterval(() =>
|
||||
{
|
||||
if (typeof transport.getStats === 'function')
|
||||
{
|
||||
transport.getStats()
|
||||
.then((stats) =>
|
||||
|
|
@ -166,6 +168,7 @@ function handleTransport(transport, baseEvent, stream)
|
|||
}),
|
||||
stream);
|
||||
});
|
||||
}
|
||||
}, STATS_INTERVAL);
|
||||
|
||||
transport.on('close', (originator) =>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const config = require('./config');
|
|||
// mediasoup server.
|
||||
const mediaServer = mediasoup.Server(
|
||||
{
|
||||
numWorkers : 1,
|
||||
numWorkers : null,
|
||||
logLevel : config.mediasoup.logLevel,
|
||||
logTags : config.mediasoup.logTags,
|
||||
rtcIPv4 : config.mediasoup.rtcIPv4,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,16 +9,16 @@
|
|||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"colors": "^1.1.2",
|
||||
"debug": "^3.1.0",
|
||||
"compression": "^1.7.3",
|
||||
"debug": "^4.1.0",
|
||||
"express": "^4.16.3",
|
||||
"mediasoup": "^2.1.0",
|
||||
"mediasoup": "^2.4.3",
|
||||
"passport-dataporten": "^1.3.0",
|
||||
"protoo-server": "^2.0.7",
|
||||
"webtorrent-hybrid": "^1.0.6"
|
||||
"socket.io": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-eslint": "^4.0.2",
|
||||
"gulp-eslint": "^5.0.0",
|
||||
"gulp-plumber": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ process.title = 'multiparty-meeting-server';
|
|||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const url = require('url');
|
||||
const protooServer = require('protoo-server');
|
||||
const Logger = require('./lib/Logger');
|
||||
const Room = require('./lib/Room');
|
||||
const Dataporten = require('passport-dataporten');
|
||||
|
|
@ -39,8 +40,20 @@ const tls =
|
|||
|
||||
const app = express();
|
||||
|
||||
app.use(compression());
|
||||
|
||||
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.session());
|
||||
|
||||
|
|
@ -58,28 +71,30 @@ app.get('/login', (req, res, next) =>
|
|||
|
||||
dataporten.setupLogout(app, '/logout');
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
console.log(req.url);
|
||||
res.sendFile(`${__dirname}/public/chooseRoom.html`);
|
||||
})
|
||||
|
||||
app.get(
|
||||
'/auth-callback',
|
||||
|
||||
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
|
||||
|
||||
(req, res) =>
|
||||
{
|
||||
const state = JSON.parse(base64.decode(req.query.state));
|
||||
|
||||
if (rooms.has(state.roomId))
|
||||
{
|
||||
const room = rooms.get(state.roomId)._protooRoom;
|
||||
|
||||
if (room.hasPeer(state.peerName))
|
||||
const data =
|
||||
{
|
||||
const peer = room.getPeer(state.peerName);
|
||||
|
||||
peer.send('auth', {
|
||||
peerName : state.peerName,
|
||||
name : req.user.data.displayName,
|
||||
picture : req.user.data.photos[0]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const room = rooms.get(state.roomId);
|
||||
|
||||
room.authCallback(data);
|
||||
}
|
||||
|
||||
res.send('');
|
||||
|
|
@ -98,29 +113,25 @@ httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
|
|||
logger.info('Server running on port: ', config.listeningPort);
|
||||
});
|
||||
|
||||
// Protoo WebSocket server listens to same webserver so everything is available
|
||||
// via same port
|
||||
const webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
||||
const httpServer = http.createServer(app);
|
||||
|
||||
httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
|
||||
{
|
||||
maxReceivedFrameSize : 960000, // 960 KBytes.
|
||||
maxReceivedMessageSize : 960000,
|
||||
fragmentOutgoingMessages : true,
|
||||
fragmentationThreshold : 960000
|
||||
logger.info('Server redirecting port: ', config.listeningRedirectPort);
|
||||
});
|
||||
|
||||
const io = require('socket.io')(httpsServer);
|
||||
|
||||
// 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 u = url.parse(info.request.url, true);
|
||||
const roomId = u.query['roomId'];
|
||||
const peerName = u.query['peerName'];
|
||||
const { roomId, peerName } = socket.handshake.query;
|
||||
|
||||
if (!roomId || !peerName)
|
||||
{
|
||||
logger.warn('connection request without roomId and/or peerName');
|
||||
|
||||
reject(400, 'Connection request without roomId and/or peerName');
|
||||
socket.disconnect(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -137,7 +148,7 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
|||
|
||||
try
|
||||
{
|
||||
room = new Room(roomId, mediaServer);
|
||||
room = new Room(roomId, mediaServer, io);
|
||||
|
||||
global.APP_ROOM = room;
|
||||
}
|
||||
|
|
@ -145,7 +156,7 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
|||
{
|
||||
logger.error('error creating a new Room: %s', error);
|
||||
|
||||
reject(error);
|
||||
socket.disconnect(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -168,7 +179,8 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
|||
room = rooms.get(roomId);
|
||||
}
|
||||
|
||||
const transport = accept();
|
||||
socket.join(roomId);
|
||||
socket.room = roomId;
|
||||
|
||||
room.handleConnection(peerName, transport);
|
||||
room.handleConnection(peerName, socket);
|
||||
});
|
||||
Loading…
Reference in New Issue