commit
8d997c14fe
|
|
@ -0,0 +1,4 @@
|
|||
# Changelog
|
||||
|
||||
### RC1 1.0
|
||||
* First stable release?
|
||||
20
README.md
20
README.md
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Chat.propTypes =
|
|||
Chat.defaultProps =
|
||||
{
|
||||
senderPlaceHolder : 'Type a message...',
|
||||
autofocus : true,
|
||||
autofocus : false,
|
||||
displayName : null
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
const key = {
|
||||
fullscreenEnabled : 0,
|
||||
fullscreenElement : 1,
|
||||
requestFullscreen : 2,
|
||||
exitFullscreen : 3,
|
||||
fullscreenchange : 4,
|
||||
fullscreenerror : 5
|
||||
};
|
||||
|
||||
const webkit = [
|
||||
'webkitFullscreenEnabled',
|
||||
'webkitFullscreenElement',
|
||||
'webkitRequestFullscreen',
|
||||
'webkitExitFullscreen',
|
||||
'webkitfullscreenchange',
|
||||
'webkitfullscreenerror'
|
||||
];
|
||||
|
||||
const moz = [
|
||||
'mozFullScreenEnabled',
|
||||
'mozFullScreenElement',
|
||||
'mozRequestFullScreen',
|
||||
'mozCancelFullScreen',
|
||||
'mozfullscreenchange',
|
||||
'mozfullscreenerror'
|
||||
];
|
||||
|
||||
const ms = [
|
||||
'msFullscreenEnabled',
|
||||
'msFullscreenElement',
|
||||
'msRequestFullscreen',
|
||||
'msExitFullscreen',
|
||||
'MSFullscreenChange',
|
||||
'MSFullscreenError'
|
||||
];
|
||||
|
||||
export default class FullScreen
|
||||
{
|
||||
constructor(document)
|
||||
{
|
||||
this.document = document;
|
||||
this.vendor = (
|
||||
('fullscreenEnabled' in this.document && Object.keys(key)) ||
|
||||
(webkit[0] in this.document && webkit) ||
|
||||
(moz[0] in this.document && moz) ||
|
||||
(ms[0] in this.document && ms) ||
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
requestFullscreen(element)
|
||||
{
|
||||
element[this.vendor[key.requestFullscreen]]();
|
||||
}
|
||||
|
||||
requestFullscreenFunction(element)
|
||||
{
|
||||
element[this.vendor[key.requestFullscreen]];
|
||||
}
|
||||
|
||||
addEventListener(type, handler)
|
||||
{
|
||||
this.document.addEventListener(this.vendor[key[type]], handler);
|
||||
}
|
||||
|
||||
removeEventListener(type, handler)
|
||||
{
|
||||
this.document.removeEventListener(this.vendor[key[type]], handler);
|
||||
}
|
||||
|
||||
get exitFullscreen()
|
||||
{
|
||||
return this.document[this.vendor[key.exitFullscreen]].bind(this.document);
|
||||
}
|
||||
|
||||
get fullscreenEnabled()
|
||||
{
|
||||
return Boolean(this.document[this.vendor[key.fullscreenEnabled]]);
|
||||
}
|
||||
set fullscreenEnabled(val) {}
|
||||
|
||||
get fullscreenElement()
|
||||
{
|
||||
return this.document[this.vendor[key.fullscreenElement]];
|
||||
}
|
||||
set fullscreenElement(val) {}
|
||||
|
||||
get onfullscreenchange()
|
||||
{
|
||||
return this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()];
|
||||
}
|
||||
|
||||
set onfullscreenchange(handler)
|
||||
{
|
||||
this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()] = handler;
|
||||
}
|
||||
|
||||
get onfullscreenerror()
|
||||
{
|
||||
return this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()];
|
||||
}
|
||||
|
||||
set onfullscreenerror(handler)
|
||||
{
|
||||
this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()] = handler;
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ const FullScreenView = (props) =>
|
|||
|
||||
<div className='controls'>
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', 'room-controls', {
|
||||
className={classnames('button', 'exitfullscreen', 'room-controls', {
|
||||
visible : toolbarsVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
|
|
@ -56,7 +56,6 @@ const FullScreenView = (props) =>
|
|||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
toggleFullscreen={() => toggleConsumerFullscreen(consumer)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,11 +45,10 @@ const Settings = ({
|
|||
<div data-component='Settings'>
|
||||
<div className='settings'>
|
||||
<Dropdown
|
||||
disabled={!me.canChangeWebcam}
|
||||
options={webcams}
|
||||
value={findOption(webcams, me.selectedWebcam)}
|
||||
onChange={(webcam) => handleChangeWebcam(webcam.value)}
|
||||
placeholder={webcamText}
|
||||
placeholder={'Select camera'}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ class ToolArea extends React.Component
|
|||
toolAreaOpen,
|
||||
unreadMessages,
|
||||
unreadFiles,
|
||||
toggleToolArea
|
||||
toggleToolArea,
|
||||
closeToolArea
|
||||
} = this.props;
|
||||
|
||||
const VisibleTab = {
|
||||
|
|
@ -48,6 +49,12 @@ class ToolArea extends React.Component
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import NewWindow from './NewWindow';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import FullView from '../FullView';
|
||||
|
||||
const VideoWindow = (props) =>
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
consumer,
|
||||
toggleConsumerWindow
|
||||
} = props;
|
||||
|
||||
if (!consumer)
|
||||
return null;
|
||||
|
||||
const consumerVisible = (
|
||||
Boolean(consumer) &&
|
||||
!consumer.locallyPaused &&
|
||||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<NewWindow onUnload={toggleConsumerWindow}>
|
||||
<FullView
|
||||
advancedMode={advancedMode}
|
||||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
/>
|
||||
</NewWindow>
|
||||
);
|
||||
};
|
||||
|
||||
VideoWindow.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
consumer : appPropTypes.Consumer,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
consumer : state.consumers[state.room.windowConsumer]
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerWindow : () =>
|
||||
{
|
||||
dispatch(stateActions.toggleConsumerWindow());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const VideoWindowContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(VideoWindow);
|
||||
|
||||
export default VideoWindowContainer;
|
||||
|
|
@ -23,7 +23,6 @@ export const Me = PropTypes.shape(
|
|||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
canChangeWebcam : PropTypes.bool.isRequired,
|
||||
webcamInProgress : PropTypes.bool.isRequired,
|
||||
audioOnly : PropTypes.bool.isRequired,
|
||||
audioOnlyInProgress : PropTypes.bool.isRequired,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
device : { flag: 'firefox', name: 'Firefox', version: '61' },
|
||||
canSendMic : true,
|
||||
canSendWebcam : true,
|
||||
canChangeWebcam : false,
|
||||
webcamInProgress : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 256 256" height="256px" id="Layer_1" version="1.1" viewBox="0 0 256 256" width="256px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M76.8,217.6c0-1.637,0.625-3.274,1.875-4.524L163.75,128L78.675,42.925c-2.5-2.5-2.5-6.55,0-9.05s6.55-2.5,9.05,0 l89.601,89.6c2.5,2.5,2.5,6.551,0,9.051l-89.601,89.6c-2.5,2.5-6.55,2.5-9.05,0C77.425,220.875,76.8,219.237,76.8,217.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 585 B |
|
|
@ -0,0 +1,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 |
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue