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. 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 ## Installation
@ -22,15 +28,6 @@ $ cp server/config.example.js server/config.js
* Copy `app/config.example.js` to `app/config.js` : * Copy `app/config.example.js` to `app/config.js` :
In addition, the server requires a screen to be installed for the server
to be able to seed shared torrent files. This is because the headless
Electron instance used by WebTorrent expects one.
See [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) for
more information about this.
* Copy `config.example.js` as `config.js` and customize it for your scenario:
```bash ```bash
$ cp app/config.example.js app/config.js $ cp app/config.example.js app/config.js
``` ```
@ -72,7 +69,7 @@ $ node server.js
## Deploy it in a server ## Deploy it in a server
* Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and dobbel check location path settings: * Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and check location path settings:
```bash ```bash
$ cp multiparty-meeting.service /etc/systemd/system/ $ cp multiparty-meeting.service /etc/systemd/system/
$ edit /etc/systemd/system/multiparty-meeting.service $ edit /etc/systemd/system/multiparty-meeting.service
@ -97,7 +94,6 @@ $ systemctl enable multiparty-meeting
* 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`) * 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`)
* If you want your service running at standard ports 80/443 you should: * If you want your service running at standard ports 80/443 you should:
* Make a redirect from HTTP port 80 to HTTPS (with Apache/NGINX)
* Configure a forwarding rule with iptables from port 443 to your configured service port (default 3443) * Configure a forwarding rule with iptables from port 443 to your configured service port (default 3443)

View File

@ -16,7 +16,7 @@ import {
const logger = new Logger('RoomClient'); const logger = new Logger('RoomClient');
const ROOM_OPTIONS = let ROOM_OPTIONS =
{ {
requestTimeout : requestTimeout, requestTimeout : requestTimeout,
transportOptions : transportOptions, transportOptions : transportOptions,
@ -71,6 +71,9 @@ export default class RoomClient
// Socket.io peer connection // Socket.io peer connection
this._signalingSocket = io(signalingUrl); this._signalingSocket = io(signalingUrl);
if (this._device.flag === 'firefox')
ROOM_OPTIONS = Object.assign({ iceTransportPolicy: 'relay' }, ROOM_OPTIONS);
// mediasoup-client Room instance. // mediasoup-client Room instance.
this._room = new mediasoupClient.Room(ROOM_OPTIONS); this._room = new mediasoupClient.Room(ROOM_OPTIONS);
this._room.roomId = roomId; this._room.roomId = roomId;
@ -115,6 +118,8 @@ export default class RoomClient
this._screenSharingProducer = null; this._screenSharingProducer = null;
this._startKeyListener();
this._join({ displayName, device }); this._join({ displayName, device });
} }
@ -137,6 +142,48 @@ export default class RoomClient
this._dispatch(stateActions.setRoomState('closed')); 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() login()
{ {
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
@ -154,6 +201,21 @@ export default class RoomClient
this._loginWindow.close(); 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) notify(text)
{ {
this._dispatch(requestActions.notify({ text: text })); this._dispatch(requestActions.notify({ text: text }));
@ -169,9 +231,9 @@ export default class RoomClient
if (called) if (called)
return; return;
called = true; called = true;
callback(new Error('Callback timeout')); callback(new Error('Request timeout.'));
}, },
5000 ROOM_OPTIONS.requestTimeout
); );
return (...args) => return (...args) =>
@ -223,13 +285,13 @@ export default class RoomClient
this._dispatch(stateActions.setDisplayName(displayName)); this._dispatch(stateActions.setDisplayName(displayName));
this.notify('Display name changed'); this.notify(`Your display name changed to ${displayName}.`);
} }
catch (error) catch (error)
{ {
logger.error('changeDisplayName() | failed: %o', 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 // We need to refresh the component for it to render the previous
// displayName again. // displayName again.
@ -263,7 +325,7 @@ export default class RoomClient
{ {
logger.error('sendChatMessage() | failed: %o', error); 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); 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); 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 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(); track.stop();
this._dispatch( this._dispatch(
@ -965,7 +1055,7 @@ export default class RoomClient
{ {
logger.error('sendRaiseHandState() | failed: %o', error); 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 // We need to refresh the component for it to render changed state
this._dispatch(stateActions.setMyRaiseHandState(!state)); this._dispatch(stateActions.setMyRaiseHandState(!state));
@ -1014,7 +1104,7 @@ export default class RoomClient
{ {
logger.warn('signaling Peer "disconnect" event'); logger.warn('signaling Peer "disconnect" event');
this.notify('WebSocket disconnected'); this.notify('You are disconnected.');
// Leave Room. // Leave Room.
try { this._room.remoteClose({ cause: 'signaling disconnected' }); } try { this._room.remoteClose({ cause: 'signaling disconnected' }); }
@ -1054,7 +1144,7 @@ export default class RoomClient
this._signalingSocket.on('display-name-changed', (data) => this._signalingSocket.on('display-name-changed', (data) =>
{ {
// eslint-disable-next-line no-shadow // 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. // NOTE: Hack, we shouldn't do this, but this is just a demo.
const peer = this._room.getPeerByName(peerName); const peer = this._room.getPeerByName(peerName);
@ -1066,12 +1156,14 @@ export default class RoomClient
return; return;
} }
const oldDisplayName = peer.appData.displayName;
peer.appData.displayName = displayName; peer.appData.displayName = displayName;
this._dispatch( this._dispatch(
stateActions.setPeerDisplayName(displayName, peerName)); 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) => this._signalingSocket.on('profile-picture-changed', (data) =>
@ -1092,7 +1184,7 @@ export default class RoomClient
this._dispatch(stateActions.setPicture(data.picture)); this._dispatch(stateActions.setPicture(data.picture));
this._dispatch(stateActions.loggedIn()); this._dispatch(stateActions.loggedIn());
this.notify(`Authenticated successfully: ${data}`); this.notify('You are logged in.');
this.closeLoginWindow(); this.closeLoginWindow();
}); });
@ -1103,6 +1195,18 @@ export default class RoomClient
logger.debug('Got raiseHandState from "%s"', peerName); 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( this._dispatch(
stateActions.setPeerRaiseHandState(peerName, raiseHandState)); stateActions.setPeerRaiseHandState(peerName, raiseHandState));
}); });
@ -1120,43 +1224,33 @@ export default class RoomClient
(this._getState().toolarea.toolAreaOpen && (this._getState().toolarea.toolAreaOpen &&
this._getState().toolarea.currentToolTab !== 'chat')) // Make sound this._getState().toolarea.currentToolTab !== 'chat')) // Make sound
{ {
const alertPromise = this._soundAlert.play(); this._soundNotification();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
} }
}); });
this._signalingSocket.on('file-receive', (data) => 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 || if (!this._getState().toolarea.toolAreaOpen ||
(this._getState().toolarea.toolAreaOpen && (this._getState().toolarea.toolAreaOpen &&
this._getState().toolarea.currentToolTab !== 'files')) // Make sound this._getState().toolarea.currentToolTab !== 'files')) // Make sound
{ {
const alertPromise = this._soundAlert.play(); this._soundNotification();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
} }
}); });
} }
@ -1210,17 +1304,7 @@ export default class RoomClient
logger.debug( logger.debug(
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
const alertPromise = this._soundAlert.play(); this._soundNotification();
if (alertPromise !== undefined)
{
alertPromise
.then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
this._handlePeer(peer); this._handlePeer(peer);
}); });
@ -1284,7 +1368,7 @@ export default class RoomClient
this.getServerHistory(); this.getServerHistory();
this.notify('You are in the room'); this.notify('You have joined the room.');
this._spotlights.on('spotlights-updated', (spotlights) => this._spotlights.on('spotlights-updated', (spotlights) =>
{ {
@ -1305,7 +1389,7 @@ export default class RoomClient
{ {
logger.error('_joinRoom() failed:%o', error); 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(); this.close();
} }
@ -1419,7 +1503,7 @@ export default class RoomClient
{ {
logger.error('_setMicProducer() failed:%o', error); 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) if (producer)
producer.close(); producer.close();
@ -1525,7 +1609,14 @@ export default class RoomClient
{ {
logger.error('_setScreenShareProducer() failed:%o', error); 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) if (producer)
producer.close(); producer.close();
@ -1623,7 +1714,7 @@ export default class RoomClient
{ {
logger.error('_setWebcamProducer() failed:%o', error); 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) if (producer)
producer.close(); producer.close();
@ -1714,8 +1805,6 @@ export default class RoomClient
else if (!this._webcams.has(currentWebcamId)) else if (!this._webcams.has(currentWebcamId))
this._webcam.device = array[0]; this._webcam.device = array[0];
this._dispatch(
stateActions.setCanChangeWebcam(len >= 2));
if (len >= 1) if (len >= 1)
this._dispatch( this._dispatch(
stateActions.setWebcamDevices(this._webcams)); stateActions.setWebcamDevices(this._webcams));
@ -1741,7 +1830,7 @@ export default class RoomClient
if (notify) if (notify)
{ {
this.notify(`${displayName} joined the room`); this.notify(`${displayName} joined the room.`);
} }
for (const consumer of peer.consumers) for (const consumer of peer.consumers)
@ -1759,7 +1848,7 @@ export default class RoomClient
if (this._room.joined) 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 = Chat.defaultProps =
{ {
senderPlaceHolder : 'Type a message...', senderPlaceHolder : 'Type a message...',
autofocus : true, autofocus : false,
displayName : null 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='controls'>
<div <div
className={classnames('button', 'fullscreen', 'room-controls', { className={classnames('button', 'exitfullscreen', 'room-controls', {
visible : toolbarsVisible visible : toolbarsVisible
})} })}
onClick={(e) => onClick={(e) =>
@ -56,7 +56,6 @@ const FullScreenView = (props) =>
videoTrack={consumer ? consumer.track : null} videoTrack={consumer ? consumer.track : null}
videoVisible={consumerVisible} videoVisible={consumerVisible}
videoProfile={consumerProfile} videoProfile={consumerProfile}
toggleFullscreen={() => toggleConsumerFullscreen(consumer)}
/> />
</div> </div>
); );

View File

@ -86,6 +86,5 @@ FullView.propTypes =
{ {
videoTrack : PropTypes.any, videoTrack : PropTypes.any,
videoVisible : PropTypes.bool, videoVisible : PropTypes.bool,
videoProfile : PropTypes.string, videoProfile : PropTypes.string
toggleFullscreen : PropTypes.func.isRequired
}; };

View File

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

View File

@ -16,6 +16,7 @@ import Notifications from './Notifications';
import ToolAreaButton from './ToolArea/ToolAreaButton'; import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea'; import ToolArea from './ToolArea/ToolArea';
import FullScreenView from './FullScreenView'; import FullScreenView from './FullScreenView';
import VideoWindow from './VideoWindow/VideoWindow';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { idle } from '../utils'; import { idle } from '../utils';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
@ -88,6 +89,8 @@ class Room extends React.Component
<FullScreenView advancedMode={room.advancedMode} /> <FullScreenView advancedMode={room.advancedMode} />
<VideoWindow advancedMode={room.advancedMode} />
<div className='room-wrapper'> <div className='room-wrapper'>
<div data-component='Logo' /> <div data-component='Logo' />
<AudioPeers /> <AudioPeers />

View File

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

View File

@ -22,12 +22,6 @@ const Settings = ({
}) => }) =>
{ {
let webcams; let webcams;
let webcamText;
if (me.canChangeWebcam)
webcamText = 'Select camera';
else
webcamText = 'Unable to select camera';
if (me.webcamDevices) if (me.webcamDevices)
webcams = Array.from(me.webcamDevices.values()); webcams = Array.from(me.webcamDevices.values());
@ -51,11 +45,10 @@ const Settings = ({
<div data-component='Settings'> <div data-component='Settings'>
<div className='settings'> <div className='settings'>
<Dropdown <Dropdown
disabled={!me.canChangeWebcam}
options={webcams} options={webcams}
value={findOption(webcams, me.selectedWebcam)} value={findOption(webcams, me.selectedWebcam)}
onChange={(webcam) => handleChangeWebcam(webcam.value)} onChange={(webcam) => handleChangeWebcam(webcam.value)}
placeholder={webcamText} placeholder={'Select camera'}
/> />
<Dropdown <Dropdown

View File

@ -4,46 +4,52 @@ import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import fscreen from 'fscreen'; import FullScreen from './FullScreen';
class Sidebar extends Component class Sidebar extends Component
{ {
state = { constructor(props)
{
super(props);
this.fullscreen = new FullScreen(document);
this.state = {
fullscreen : false fullscreen : false
}; };
}
handleToggleFullscreen = () => handleToggleFullscreen = () =>
{ {
if (fscreen.fullscreenElement) if (this.fullscreen.fullscreenElement)
{ {
fscreen.exitFullscreen(); this.fullscreen.exitFullscreen();
} }
else else
{ {
fscreen.requestFullscreen(document.documentElement); this.fullscreen.requestFullscreen(document.documentElement);
} }
}; };
handleFullscreenChange = () => handleFullscreenChange = () =>
{ {
this.setState({ this.setState({
fullscreen : fscreen.fullscreenElement !== null fullscreen : this.fullscreen.fullscreenElement !== null
}); });
}; };
componentDidMount() componentDidMount()
{ {
if (fscreen.fullscreenEnabled) if (this.fullscreen.fullscreenEnabled)
{ {
fscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
} }
} }
componentWillUnmount() componentWillUnmount()
{ {
if (fscreen.fullscreenEnabled) if (this.fullscreen.fullscreenEnabled)
{ {
fscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
} }
} }
@ -85,7 +91,7 @@ class Sidebar extends Component
})} })}
data-component='Sidebar' data-component='Sidebar'
> >
{fscreen.fullscreenEnabled && ( {this.fullscreen.fullscreenEnabled && (
<div <div
className={classnames('button', 'fullscreen', { className={classnames('button', 'fullscreen', {
on : this.state.fullscreen on : this.state.fullscreen

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ const initialState =
showSettings : false, showSettings : false,
advancedMode : false, advancedMode : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
windowConsumer : null, // ConsumerID
toolbarsVisible : true, toolbarsVisible : true,
mode : 'democratic', mode : 'democratic',
selectedPeerName : null, selectedPeerName : null,
@ -62,6 +63,17 @@ const room = (state = initialState, action) =>
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId }; return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
} }
case 'TOGGLE_WINDOW_CONSUMER':
{
const { consumerId } = action.payload;
const currentConsumer = state.windowConsumer;
if (currentConsumer === consumerId)
return { ...state, windowConsumer: null };
else
return { ...state, windowConsumer: consumerId };
}
case 'SET_TOOLBARS_VISIBLE': case 'SET_TOOLBARS_VISIBLE':
{ {
const { toolbarsVisible } = action.payload; const { toolbarsVisible } = action.payload;

View File

@ -62,14 +62,6 @@ export const setAudioDevices = (devices) =>
}; };
}; };
export const setCanChangeWebcam = (flag) =>
{
return {
type : 'SET_CAN_CHANGE_WEBCAM',
payload : flag
};
};
export const setWebcamDevices = (devices) => export const setWebcamDevices = (devices) =>
{ {
return { return {
@ -397,6 +389,14 @@ export const toggleConsumerFullscreen = (consumerId) =>
}; };
}; };
export const toggleConsumerWindow = (consumerId) =>
{
return {
type : 'TOGGLE_WINDOW_CONSUMER',
payload : { consumerId }
};
};
export const setToolbarsVisible = (toolbarsVisible) => ({ export const setToolbarsVisible = (toolbarsVisible) => ({
type : 'SET_TOOLBARS_VISIBLE', type : 'SET_TOOLBARS_VISIBLE',
payload : { toolbarsVisible } payload : { toolbarsVisible }

View File

@ -18,6 +18,7 @@ if (process.env.NODE_ENV === 'development')
{ {
const reduxLogger = createLogger( const reduxLogger = createLogger(
{ {
predicate : (getState, action) => action.type !== 'SET_PRODUCER_VOLUME',
duration : true, duration : true,
timestamp : false, timestamp : false,
level : 'log', level : 'log',

View File

@ -14,12 +14,11 @@
"domready": "^1.0.8", "domready": "^1.0.8",
"drag-drop": "^4.2.0", "drag-drop": "^4.2.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"fscreen": "^1.0.2",
"hark": "^1.2.2", "hark": "^1.2.2",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"magnet-uri": "^5.2.3", "magnet-uri": "^5.2.3",
"marked": "^0.5.1", "marked": "^0.5.1",
"mediasoup-client": "^2.1.1", "mediasoup-client": "^2.3.2",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^16.5.2", "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; height: 5vmin;
} }
&.fullscreen { &.exitfullscreen {
background-image: url('/resources/images/icon_fullscreen_exit_black.svg'); background-image: url('/resources/images/icon_fullscreen_exit_black.svg');
background-color: rgba(#fff, 0.7); background-color: rgba(#fff, 0.7);
} }
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }

View File

@ -189,6 +189,11 @@
background-image: url('/resources/images/icon_fullscreen_black.svg'); background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7); background-color: rgba(#fff, 0.7);
} }
&.newwindow {
background-image: url('/resources/images/icon_new_window_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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