Merge branch 'RC1-1.0' into develop

master
Håvar Aambø Fosstveit 2018-12-11 12:31:16 +01:00
commit fa91d9b9e2
23 changed files with 402 additions and 82 deletions

View File

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

View File

@ -11,6 +11,8 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci
* 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
* Clone the project:

View File

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

View File

@ -168,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));
});
@ -241,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'
));

View File

@ -179,6 +179,13 @@ export default class RoomClient
this.notify('Changed layout to filmstrip view.');
break;
}
case 'm': // Toggle microphone
{
this.toggleMic();
this.notify('Muted/unmuted your microphone.');
break;
}
}
}
});
@ -391,6 +398,16 @@ export default class RoomClient
}
}
toggleMic()
{
logger.debug('toggleMic()');
if (this._micProducer.locallyPaused)
this.unmuteMic();
else
this.muteMic();
}
muteMic()
{
logger.debug('muteMic()');
@ -1349,7 +1366,7 @@ export default class RoomClient
if (this._produce)
{
if (this._room.canSend('audio'))
await this._setMicProducer();
this._setMicProducer();
// Add our webcam (unless the cookie says no).
if (this._room.canSend('video'))
@ -1357,7 +1374,7 @@ export default class RoomClient
const devicesCookie = cookiesManager.getDevices();
if (!devicesCookie || devicesCookie.webcamEnabled)
await this.enableWebcam();
this.enableWebcam();
}
}

View File

@ -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: &lsquo;m&lsquo;'
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>
);
}

View File

@ -40,7 +40,8 @@ class Peer extends Component
onUnmuteMic,
toggleConsumerFullscreen,
toggleConsumerWindow,
style
style,
windowConsumer
} = this.props;
const micEnabled = (
@ -128,7 +129,10 @@ class Peer extends Component
/>
<div
className={classnames('button', 'newwindow')}
className={classnames('button', 'newwindow', {
disabled : !videoVisible ||
(windowConsumer === webcamConsumer.id)
})}
onClick={(e) =>
{
e.stopPropagation();
@ -205,6 +209,7 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number,
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
streamDimensions : PropTypes.object,
@ -229,7 +234,8 @@ const mapStateToProps = (state, { name }) =>
peer,
micConsumer,
webcamConsumer,
screenConsumer
screenConsumer,
windowConsumer : state.room.windowConsumer
};
};

View File

@ -13,7 +13,7 @@ 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';
@ -97,8 +97,6 @@ class Room extends React.Component
<Notifications />
<ToolAreaButton />
{room.advancedMode ?
<div className='state' data-tip='Server status'>
<div className={classnames('icon', room.state)} />

View File

@ -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',
@ -58,7 +59,14 @@ const Settings = ({
onChange={(device) => handleChangeAudioDevice(device.value)}
placeholder={audioDevicesText}
/>
<ReactTooltip
effect='solid'
/>
<div
data-tip='keyboard shortcut: &lsquo;a&lsquo;'
data-type='dark'
data-place='left'
>
<input
id='room-mode'
type='checkbox'
@ -66,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)}
@ -74,6 +88,7 @@ const Settings = ({
/>
</div>
</div>
</div>
);
};

View File

@ -24,7 +24,7 @@ class ToolArea extends React.Component
unreadMessages,
unreadFiles,
toggleToolArea,
closeToolArea
unread
} = this.props;
const VisibleTab = {
@ -50,11 +50,21 @@ class ToolArea extends React.Component
})}
>
<div
className={classNames('toolarea-close-button button', {
on : toolAreaOpen
})}
onClick={closeToolArea}
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>
)}
</div>
<div className='tab-headers'>
<TabHeader
id='chat'
@ -97,14 +107,17 @@ ToolArea.propTypes =
unreadFiles : PropTypes.number.isRequired,
toolAreaOpen : PropTypes.bool,
toggleToolArea : PropTypes.func.isRequired,
closeToolArea : 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,
toolAreaOpen : state.toolarea.toolAreaOpen
toolAreaOpen : state.toolarea.toolAreaOpen,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles
});
const mapDispatchToProps = {

View File

@ -27,7 +27,7 @@ class ToolAreaButton extends React.Component
className={classnames('button toolarea-button', {
on : toolAreaOpen
})}
data-tip='Open tool box'
data-tip='Open tools'
data-type='dark'
onClick={() => toggleToolArea()}
/>

View File

@ -165,7 +165,7 @@ class NewWindow extends React.PureComponent
{
if (this.window)
{
if (this.fullscreen.fullscreenEnabled)
if (this.fullscreen && this.fullscreen.fullscreenEnabled)
{
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
}

View File

@ -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';

View File

@ -18,7 +18,9 @@ if (process.env.NODE_ENV === 'development')
{
const reduxLogger = createLogger(
{
predicate : (getState, action) => action.type !== 'SET_PRODUCER_VOLUME',
// 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',

View File

@ -36,6 +36,7 @@
"redux-thunk": "^2.3.0",
"resize-observer-polyfill": "^1.5.0",
"riek": "^1.1.0",
"socket.io-client": "^2.1.1",
"url-parse": "^1.4.3",
"webtorrent": "^0.102.4"
},

View File

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

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -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;

View File

@ -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');
}
}
}

View File

@ -63,6 +63,93 @@
}
}
> .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) {
@ -158,10 +245,6 @@
background-image: url('/resources/images/icon_tool_area_black.svg');
}
}
&.toolarea-close-button {
background-image: url('/resources/images/arrow_right.svg');
}
}
> .badge {

View File

@ -346,6 +346,25 @@ class Room extends EventEmitter
);
});
signalingPeer.socket.on('request-consumer-keyframe', (request, cb) =>
{
cb(null);
const { consumerId } = request;
const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName);
const consumer = mediaPeer.consumers
.find((_consumer) => _consumer.id === consumerId);
if (!consumer)
{
logger.warn('consumer with id "%s" not found', consumerId);
return;
}
consumer.requestKeyFrame();
});
signalingPeer.socket.on('disconnect', () =>
{
logger.debug('Peer "close" event [peer:"%s"]', signalingPeer.peerName);

View File

@ -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) =>

View File

@ -12,7 +12,7 @@
"compression": "^1.7.3",
"debug": "^4.1.0",
"express": "^4.16.3",
"mediasoup": "^2.3.3",
"mediasoup": "^2.4.3",
"passport-dataporten": "^1.3.0",
"socket.io": "^2.1.1"
},

View File

@ -71,6 +71,11 @@ 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' }),