Merge pull request #76 from havfo/RC1-1.0

Rc1 1.0
master
Håvar Aambø Fosstveit 2018-11-15 22:57:06 +01:00 committed by GitHub
commit 8d997c14fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 804 additions and 142 deletions

4
CHANGELOG.md 100644
View File

@ -0,0 +1,4 @@
# Changelog
### RC1 1.0
* First stable release?

View File

@ -2,8 +2,14 @@
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
## Installation
@ -22,15 +28,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 +69,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 +94,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)

View File

@ -16,7 +16,7 @@ import {
const logger = new Logger('RoomClient');
const ROOM_OPTIONS =
let ROOM_OPTIONS =
{
requestTimeout : requestTimeout,
transportOptions : transportOptions,
@ -71,6 +71,9 @@ export default class RoomClient
// Socket.io peer connection
this._signalingSocket = io(signalingUrl);
if (this._device.flag === 'firefox')
ROOM_OPTIONS = Object.assign({ iceTransportPolicy: 'relay' }, ROOM_OPTIONS);
// mediasoup-client Room instance.
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
this._room.roomId = roomId;
@ -115,6 +118,8 @@ export default class RoomClient
this._screenSharingProducer = null;
this._startKeyListener();
this._join({ displayName, device });
}
@ -137,6 +142,48 @@ export default class RoomClient
this._dispatch(stateActions.setRoomState('closed'));
}
_startKeyListener()
{
// Add keypress event listner on document
document.addEventListener('keypress', (event) =>
{
const key = String.fromCharCode(event.which);
const source = event.target;
const exclude = [ 'input', 'textarea' ];
if (exclude.indexOf(source.tagName.toLowerCase()) === -1)
{
logger.debug('keyPress() [key:"%s"]', key);
switch (key)
{
case 'a': // Activate advanced mode
{
this._dispatch(stateActions.toggleAdvancedMode());
this.notify('Toggled advanced mode.');
break;
}
case '1': // Set democratic view
{
this._dispatch(stateActions.setDisplayMode('democratic'));
this.notify('Changed layout to democratic view.');
break;
}
case '2': // Set filmstrip view
{
this._dispatch(stateActions.setDisplayMode('filmstrip'));
this.notify('Changed layout to filmstrip view.');
break;
}
}
}
});
}
login()
{
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
@ -154,6 +201,21 @@ export default class RoomClient
this._loginWindow.close();
}
_soundNotification()
{
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
}
notify(text)
{
this._dispatch(requestActions.notify({ text: text }));
@ -169,9 +231,9 @@ export default class RoomClient
if (called)
return;
called = true;
callback(new Error('Callback timeout'));
callback(new Error('Request timeout.'));
},
5000
ROOM_OPTIONS.requestTimeout
);
return (...args) =>
@ -223,13 +285,13 @@ export default class RoomClient
this._dispatch(stateActions.setDisplayName(displayName));
this.notify('Display name changed');
this.notify(`Your display name changed to ${displayName}.`);
}
catch (error)
{
logger.error('changeDisplayName() | failed: %o', error);
this.notify(`Could not change display name: ${error}`);
this.notify('An error occured while changing your display name.');
// We need to refresh the component for it to render the previous
// displayName again.
@ -263,7 +325,7 @@ export default class RoomClient
{
logger.error('sendChatMessage() | failed: %o', error);
this.notify(`Could not send chat: ${error}`);
this.notify('An error occured while sending chat message.');
}
}
@ -279,7 +341,7 @@ export default class RoomClient
{
logger.error('sendFile() | failed: %o', error);
this.notify('An error occurred while sharing a file');
this.notify('An error occurred while sharing file.');
}
}
@ -325,7 +387,7 @@ export default class RoomClient
{
logger.error('getServerHistory() | failed: %o', error);
this.notify(`Could not get chat history: ${error}`);
this.notify('An error occured while getting server history.');
}
}
@ -539,6 +601,34 @@ export default class RoomClient
const newTrack = await this._micProducer.replaceTrack(track);
const harkStream = new MediaStream;
harkStream.addTrack(newTrack);
if (!harkStream.getAudioTracks()[0])
throw new Error('changeAudioDevice(): given stream has no audio track');
if (this._micProducer.hark != null) this._micProducer.hark.stop();
this._micProducer.hark = hark(harkStream, { play: false });
// eslint-disable-next-line no-unused-vars
this._micProducer.hark.on('volume_change', (dBs, threshold) =>
{
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
// Math.pow(10, dBs / 20)
// However it does not produce a visually useful output, so let exagerate
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
// minimize component renderings.
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
if (volume === 1)
volume = 0;
if (volume !== this._micProducer.volume)
{
this._micProducer.volume = volume;
this._dispatch(stateActions.setProducerVolume(this._micProducer.id, volume));
}
});
track.stop();
this._dispatch(
@ -965,7 +1055,7 @@ export default class RoomClient
{
logger.error('sendRaiseHandState() | failed: %o', error);
this.notify(`Could not change raise hand state: ${error}`);
this.notify(`An error occured while ${state ? 'raising' : 'lowering'} hand.`);
// We need to refresh the component for it to render changed state
this._dispatch(stateActions.setMyRaiseHandState(!state));
@ -1014,7 +1104,7 @@ export default class RoomClient
{
logger.warn('signaling Peer "disconnect" event');
this.notify('WebSocket disconnected');
this.notify('You are disconnected.');
// Leave Room.
try { this._room.remoteClose({ cause: 'signaling disconnected' }); }
@ -1054,7 +1144,7 @@ export default class RoomClient
this._signalingSocket.on('display-name-changed', (data) =>
{
// eslint-disable-next-line no-shadow
const { peerName, displayName, oldDisplayName } = data;
const { peerName, displayName } = data;
// NOTE: Hack, we shouldn't do this, but this is just a demo.
const peer = this._room.getPeerByName(peerName);
@ -1066,12 +1156,14 @@ export default class RoomClient
return;
}
const oldDisplayName = peer.appData.displayName;
peer.appData.displayName = displayName;
this._dispatch(
stateActions.setPeerDisplayName(displayName, peerName));
this.notify(`${oldDisplayName} is now ${displayName}`);
this.notify(`${oldDisplayName} changed their display name to ${displayName}.`);
});
this._signalingSocket.on('profile-picture-changed', (data) =>
@ -1092,7 +1184,7 @@ export default class RoomClient
this._dispatch(stateActions.setPicture(data.picture));
this._dispatch(stateActions.loggedIn());
this.notify(`Authenticated successfully: ${data}`);
this.notify('You are logged in.');
this.closeLoginWindow();
});
@ -1103,6 +1195,18 @@ export default class RoomClient
logger.debug('Got raiseHandState from "%s"', peerName);
// NOTE: Hack, we shouldn't do this, but this is just a demo.
const peer = this._room.getPeerByName(peerName);
if (!peer)
{
logger.error('peer not found');
return;
}
this.notify(`${peer.appData.displayName} ${raiseHandState ? 'raised' : 'lowered'} their hand.`);
this._dispatch(
stateActions.setPeerRaiseHandState(peerName, raiseHandState));
});
@ -1120,43 +1224,33 @@ export default class RoomClient
(this._getState().toolarea.toolAreaOpen &&
this._getState().toolarea.currentToolTab !== 'chat')) // Make sound
{
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
this._soundNotification();
}
});
this._signalingSocket.on('file-receive', (data) =>
{
const payload = data.file;
const { peerName, file } = data;
this._dispatch(stateActions.addFile(payload));
// NOTE: Hack, we shouldn't do this, but this is just a demo.
const peer = this._room.getPeerByName(peerName);
this.notify(`${payload.name} shared a file`);
if (!peer)
{
logger.error('peer not found');
return;
}
this._dispatch(stateActions.addFile(file));
this.notify(`${peer.appData.displayName} shared a file.`);
if (!this._getState().toolarea.toolAreaOpen ||
(this._getState().toolarea.toolAreaOpen &&
this._getState().toolarea.currentToolTab !== 'files')) // Make sound
{
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
this._soundNotification();
}
});
}
@ -1210,17 +1304,7 @@ export default class RoomClient
logger.debug(
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
const alertPromise = this._soundAlert.play();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
this._soundNotification();
this._handlePeer(peer);
});
@ -1284,7 +1368,7 @@ export default class RoomClient
this.getServerHistory();
this.notify('You are in the room');
this.notify('You have joined the room.');
this._spotlights.on('spotlights-updated', (spotlights) =>
{
@ -1305,7 +1389,7 @@ export default class RoomClient
{
logger.error('_joinRoom() failed:%o', error);
this.notify(`Could not join the room: ${error.toString()}`);
this.notify('An error occured while joining the room.');
this.close();
}
@ -1419,7 +1503,7 @@ export default class RoomClient
{
logger.error('_setMicProducer() failed:%o', error);
this.notify(`Mic producer failed: ${error.name}:${error.message}`);
this.notify('An error occured while accessing your microphone.');
if (producer)
producer.close();
@ -1525,7 +1609,14 @@ export default class RoomClient
{
logger.error('_setScreenShareProducer() failed:%o', error);
this.notify(`Screen share producer failed: ${error.name}:${error.message}`);
if (error.name === 'NotAllowedError') // Request to share denied by user
{
this.notify('Request to start sharing your screen was denied.');
}
else // Some other error
{
this.notify('An error occured while starting to share your screen.');
}
if (producer)
producer.close();
@ -1623,7 +1714,7 @@ export default class RoomClient
{
logger.error('_setWebcamProducer() failed:%o', error);
this.notify(`Webcam producer failed: ${error.name}:${error.message}`);
this.notify('An error occured while accessing your camera.');
if (producer)
producer.close();
@ -1714,8 +1805,6 @@ export default class RoomClient
else if (!this._webcams.has(currentWebcamId))
this._webcam.device = array[0];
this._dispatch(
stateActions.setCanChangeWebcam(len >= 2));
if (len >= 1)
this._dispatch(
stateActions.setWebcamDevices(this._webcams));
@ -1741,7 +1830,7 @@ export default class RoomClient
if (notify)
{
this.notify(`${displayName} joined the room`);
this.notify(`${displayName} joined the room.`);
}
for (const consumer of peer.consumers)
@ -1759,7 +1848,7 @@ export default class RoomClient
if (this._room.joined)
{
this.notify(`${peer.appData.displayName} left the room`);
this.notify(`${displayName} left the room.`);
}
});

View File

@ -58,7 +58,7 @@ Chat.propTypes =
Chat.defaultProps =
{
senderPlaceHolder : 'Type a message...',
autofocus : true,
autofocus : false,
displayName : null
};

View File

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

View File

@ -40,7 +40,7 @@ const FullScreenView = (props) =>
<div className='controls'>
<div
className={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>
);

View File

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

View File

@ -39,6 +39,7 @@ class Peer extends Component
onMuteMic,
onUnmuteMic,
toggleConsumerFullscreen,
toggleConsumerWindow,
style
} = this.props;
@ -126,6 +127,15 @@ class Peer extends Component
}}
/>
<div
className={classnames('button', 'newwindow')}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerWindow(webcamConsumer);
}}
/>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
@ -155,6 +165,15 @@ class Peer extends Component
visible : this.state.controlsVisible
})}
>
<div
className={classnames('button', 'newwindow')}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerWindow(screenConsumer);
}}
/>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
@ -190,7 +209,8 @@ Peer.propTypes =
onUnmuteMic : PropTypes.func.isRequired,
streamDimensions : PropTypes.object,
style : PropTypes.object,
toggleConsumerFullscreen : PropTypes.func.isRequired
toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired
};
const mapStateToProps = (state, { name }) =>
@ -228,6 +248,11 @@ const mapDispatchToProps = (dispatch) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
},
toggleConsumerWindow : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerWindow(consumer.id));
}
};
};

View File

@ -16,6 +16,7 @@ import Notifications from './Notifications';
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';
@ -88,6 +89,8 @@ class Room extends React.Component
<FullScreenView advancedMode={room.advancedMode} />
<VideoWindow advancedMode={room.advancedMode} />
<div className='room-wrapper'>
<div data-component='Logo' />
<AudioPeers />

View File

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

View File

@ -22,12 +22,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,13 +45,12 @@ 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
disabled={!me.canChangeAudioDevice}
options={audioDevices}

View File

@ -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 = {
fullscreen : false
};
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,7 +91,7 @@ class Sidebar extends Component
})}
data-component='Sidebar'
>
{fscreen.fullscreenEnabled && (
{this.fullscreen.fullscreenEnabled && (
<div
className={classnames('button', 'fullscreen', {
on : this.state.fullscreen

View File

@ -23,7 +23,8 @@ class ToolArea extends React.Component
toolAreaOpen,
unreadMessages,
unreadFiles,
toggleToolArea
toggleToolArea,
closeToolArea
} = this.props;
const VisibleTab = {
@ -43,11 +44,17 @@ class ToolArea extends React.Component
/>
<div
data-component='ToolArea'
data-component='ToolArea'
className={classNames({
open : toolAreaOpen
})}
>
<div
className={classNames('toolarea-close-button button', {
on : toolAreaOpen
})}
onClick={closeToolArea}
/>
<div className='tab-headers'>
<TabHeader
id='chat'
@ -89,7 +96,8 @@ ToolArea.propTypes =
unreadMessages : PropTypes.number.isRequired,
unreadFiles : PropTypes.number.isRequired,
toolAreaOpen : PropTypes.bool,
toggleToolArea : PropTypes.func.isRequired
toggleToolArea : PropTypes.func.isRequired,
closeToolArea : PropTypes.func.isRequired
};
const mapStateToProps = (state) => ({
@ -101,7 +109,8 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = {
setToolTab : stateActions.setToolTab,
toggleToolArea : stateActions.toggleToolArea
toggleToolArea : stateActions.toggleToolArea,
closeToolArea : stateActions.closeToolArea
};
const ToolAreaContainer = connect(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ const initialState =
showSettings : false,
advancedMode : false,
fullScreenConsumer : null, // ConsumerID
windowConsumer : null, // ConsumerID
toolbarsVisible : true,
mode : 'democratic',
selectedPeerName : null,
@ -62,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;

View File

@ -62,14 +62,6 @@ export const setAudioDevices = (devices) =>
};
};
export const setCanChangeWebcam = (flag) =>
{
return {
type : 'SET_CAN_CHANGE_WEBCAM',
payload : flag
};
};
export const setWebcamDevices = (devices) =>
{
return {
@ -397,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 }

View File

@ -18,6 +18,7 @@ if (process.env.NODE_ENV === 'development')
{
const reduxLogger = createLogger(
{
predicate : (getState, action) => action.type !== 'SET_PRODUCER_VOLUME',
duration : true,
timestamp : false,
level : 'log',
@ -43,4 +44,4 @@ export const store = createStore(
reducers,
undefined,
enhancer
);
);

View File

@ -14,12 +14,11 @@
"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.5.1",
"mediasoup-client": "^2.1.1",
"mediasoup-client": "^2.3.2",
"prop-types": "^15.6.2",
"random-string": "^0.2.0",
"react": "^16.5.2",

View File

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

After

Width:  |  Height:  |  Size: 585 B

View File

@ -0,0 +1,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

View File

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

View File

@ -189,6 +189,11 @@
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);
}
}
}
}

View File

@ -5,7 +5,6 @@
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: none;
&.open {
@ -14,7 +13,8 @@
+desktop() {
&.open {
display: none;
background: rgba(0, 0, 0, 0.3);
display: block;
}
}
}
@ -33,6 +33,36 @@
.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;
}
}
}
}
@media (min-width: 600px) {
@ -128,6 +158,10 @@
background-image: url('/resources/images/icon_tool_area_black.svg');
}
}
&.toolarea-close-button {
background-image: url('/resources/images/arrow_right.svg');
}
}
> .badge {

View File

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

View File

@ -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
// }
// kind : 'video',
// 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

View File

@ -321,7 +321,8 @@ class Room extends EventEmitter
signalingPeer.socket.broadcast.to(this._roomId).emit(
'file-receive',
{
file : fileData
peerName : signalingPeer.peerName,
file : fileData
}
);
});
@ -334,7 +335,7 @@ class Room extends EventEmitter
const { raiseHandState } = request;
const { mediaPeer } = signalingPeer;
mediaPeer.appData.raiseHandState = request.raiseHandState;
mediaPeer.appData.raiseHandState = raiseHandState;
// Spread to others
signalingPeer.socket.broadcast.to(this._roomId).emit(
'raisehand-message',

View File

@ -9,9 +9,10 @@
"dependencies": {
"base-64": "^0.1.0",
"colors": "^1.1.2",
"compression": "^1.7.3",
"debug": "^4.1.0",
"express": "^4.16.3",
"mediasoup": "^2.1.0",
"mediasoup": "^2.3.3",
"passport-dataporten": "^1.3.0",
"socket.io": "^2.1.1"
},

View File

@ -9,6 +9,7 @@ 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 Logger = require('./lib/Logger');
const Room = require('./lib/Room');
@ -39,6 +40,8 @@ const tls =
const app = express();
app.use(compression());
const dataporten = new Dataporten.Setup(config.oauth2);
app.all('*', (req, res, next) =>