Merge branch 'develop'

master
Håvar Aambø Fosstveit 2018-04-13 10:21:08 +02:00
commit b54878cc01
31 changed files with 1122 additions and 94 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
node_modules/
/app/config.*
!/app/config.example.js
/server/config.*
!/server/config.example.js
/server/public/

View File

@ -0,0 +1,4 @@
module.exports =
{
chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'
};

View File

@ -20,6 +20,7 @@ const gulpif = require('gulp-if');
const gutil = require('gulp-util');
const plumber = require('gulp-plumber');
const rename = require('gulp-rename');
const change = require('gulp-change');
const header = require('gulp-header');
const touch = require('gulp-touch-cmd');
const browserify = require('browserify');
@ -45,6 +46,7 @@ const BANNER_OPTIONS =
currentYear : (new Date()).getFullYear()
};
const OUTPUT_DIR = '../server/public';
const appOptions = require('./config');
// Set Node 'development' environment (unless externally set).
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
@ -123,6 +125,11 @@ function bundle(options)
return rebundle();
}
function changeHTML(content)
{
return content.replace(/chromeExtension/g, appOptions.chromeExtension);
}
gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
gulp.task('lint', () =>
@ -163,6 +170,7 @@ gulp.task('css', () =>
gulp.task('html', () =>
{
return gulp.src('index.html')
.pipe(change(changeHTML))
.pipe(gulp.dest(OUTPUT_DIR));
});

View File

@ -8,6 +8,7 @@
<meta name='description' content='multiparty meeting - Cutting Edge WebRTC Video Conferencing'>
<link rel='stylesheet' href='/multiparty-meeting.css'>
<link rel="chrome-webstore-item" href="chromeExtension">
<script src='/resources/js/antiglobal.js'></script>
<script>
@ -15,7 +16,7 @@
if (window.antiglobal)
{
window.antiglobal('___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__');
window.antiglobal('__multipartyMeetingScreenShareExtensionAvailable__', '___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__');
setInterval(window.antiglobal, 180000);
}
</script>

View File

@ -1,6 +1,7 @@
import protooClient from 'protoo-client';
import * as mediasoupClient from 'mediasoup-client';
import Logger from './Logger';
import ScreenShare from './ScreenShare';
import { getProtooUrl } from './urlFactory';
import * as cookiesManager from './cookiesManager';
import * as requestActions from './redux/requestActions';
@ -79,12 +80,15 @@ export default class RoomClient
// Local Webcam. Object with:
// - {MediaDeviceInfo} [device]
// - {String} [resolution] - 'qvga' / 'vga' / 'hd'.
this._webcam =
{
this._webcam = {
device : null,
resolution : 'hd'
};
this._screenSharing = ScreenShare.create();
this._screenSharingProducer = null;
this._join({ displayName, device });
}
@ -189,6 +193,79 @@ export default class RoomClient
this._micProducer.resume();
}
installExtension()
{
logger.debug('installExtension()');
return new Promise((resolve, reject) =>
{
window.addEventListener('message', _onExtensionMessage, false);
// eslint-disable-next-line no-undef
chrome.webstore.install(null, _successfulInstall, _failedInstall);
function _onExtensionMessage({ data })
{
if (data.type === 'ScreenShareInjected')
{
logger.debug('installExtension() | installation succeeded');
return resolve();
}
}
function _failedInstall(reason)
{
window.removeEventListener('message', _onExtensionMessage);
return reject(
new Error('Failed to install extension: %s', reason));
}
function _successfulInstall()
{
logger.debug('installExtension() | installation accepted');
}
})
.then(() =>
{
// This should be handled better
this._dispatch(stateActions.setScreenCapabilities(
{
canShareScreen : this._room.canSend('video'),
needExtension : false
}));
})
.catch((error) =>
{
logger.error('installExtension() | failed: %o', error);
});
}
enableScreenSharing()
{
logger.debug('enableScreenSharing()');
this._dispatch(
stateActions.setScreenShareInProgress(true));
return Promise.resolve()
.then(() =>
{
return this._setScreenShareProducer();
})
.then(() =>
{
this._dispatch(
stateActions.setScreenShareInProgress(false));
})
.catch((error) =>
{
logger.error('enableScreenSharing() | failed: %o', error);
this._dispatch(
stateActions.setScreenShareInProgress(false));
});
}
enableWebcam()
{
logger.debug('enableWebcam()');
@ -222,6 +299,30 @@ export default class RoomClient
});
}
disableScreenSharing()
{
logger.debug('disableScreenSharing()');
this._dispatch(
stateActions.setScreenShareInProgress(true));
return Promise.resolve()
.then(() =>
{
this._screenSharingProducer.close();
this._dispatch(
stateActions.setScreenShareInProgress(false));
})
.catch((error) =>
{
logger.error('disableScreenSharing() | failed: %o', error);
this._dispatch(
stateActions.setScreenShareInProgress(false));
});
}
disableWebcam()
{
logger.debug('disableWebcam()');
@ -736,6 +837,12 @@ export default class RoomClient
canSendMic : this._room.canSend('audio'),
canSendWebcam : this._room.canSend('video')
}));
this._dispatch(stateActions.setScreenCapabilities(
{
canShareScreen : this._room.canSend('video') &&
this._screenSharing.isScreenShareAvailable(),
needExtension : this._screenSharing.needExtension()
}));
})
.then(() =>
{
@ -906,6 +1013,117 @@ export default class RoomClient
});
}
_setScreenShareProducer()
{
if (!this._room.canSend('video'))
{
return Promise.reject(
new Error('cannot send screen'));
}
let producer;
return Promise.resolve()
.then(() =>
{
const available = this._screenSharing.isScreenShareAvailable() &&
!this._screenSharing.needExtension();
if (!available)
throw new Error('screen sharing not available');
logger.debug('_setScreenShareProducer() | calling getUserMedia()');
return this._screenSharing.start({
width : 1280,
height : 720,
frameRate : 3
});
})
.then((stream) =>
{
const track = stream.getVideoTracks()[0];
producer = this._room.createProducer(
track, { simulcast: false }, { source: 'screen' });
// No need to keep original track.
track.stop();
// Send it.
return producer.send(this._sendTransport);
})
.then(() =>
{
this._screenSharingProducer = producer;
this._dispatch(stateActions.addProducer(
{
id : producer.id,
source : 'screen',
deviceLabel : 'screen',
type : 'screen',
locallyPaused : producer.locallyPaused,
remotelyPaused : producer.remotelyPaused,
track : producer.track,
codec : producer.rtpParameters.codecs[0].name
}));
producer.on('close', (originator) =>
{
logger.debug(
'webcam Producer "close" event [originator:%s]', originator);
this._screenSharingProducer = null;
this._dispatch(stateActions.removeProducer(producer.id));
});
producer.on('pause', (originator) =>
{
logger.debug(
'webcam Producer "pause" event [originator:%s]', originator);
this._dispatch(stateActions.setProducerPaused(producer.id, originator));
});
producer.on('resume', (originator) =>
{
logger.debug(
'webcam Producer "resume" event [originator:%s]', originator);
this._dispatch(stateActions.setProducerResumed(producer.id, originator));
});
producer.on('handled', () =>
{
logger.debug('webcam Producer "handled" event');
});
producer.on('unhandled', () =>
{
logger.debug('webcam Producer "unhandled" event');
});
})
.then(() =>
{
logger.debug('_setScreenShareProducer() succeeded');
})
.catch((error) =>
{
logger.error('_setScreenShareProducer() failed:%o', error);
this._dispatch(requestActions.notify(
{
text : `Screen share producer failed: ${error.name}:${error.message}`
}));
if (producer)
producer.close();
throw error;
});
}
_setWebcamProducer()
{
if (!this._room.canSend('video'))

View File

@ -0,0 +1,212 @@
import { getBrowserType } from './utils';
class ChromeScreenShare
{
constructor()
{
this._stream = null;
}
start(options = { })
{
const state = this;
return new Promise((resolve, reject) =>
{
window.addEventListener('message', _onExtensionMessage, false);
window.postMessage({ type: 'getStreamId' }, '*');
function _onExtensionMessage({ data })
{
if (data.type !== 'gotStreamId')
{
return;
}
const constraints = state._toConstraints(options, data.streamId);
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) =>
{
window.removeEventListener('message', _onExtensionMessage);
state._stream = stream;
resolve(stream);
})
.catch((err) =>
{
window.removeEventListener('message', _onExtensionMessage);
reject(err);
});
}
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
{
return true;
}
return false;
}
needExtension()
{
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
{
return false;
}
return true;
}
_toConstraints(options, streamId)
{
const constraints = {
video : {
mandatory : {
chromeMediaSource : 'desktop',
chromeMediaSourceId : streamId
},
optional : [ {
googTemporalLayeredScreencast : true
} ]
},
audio : false
};
if (isFinite(options.width))
{
constraints.video.mandatory.maxWidth = options.width;
constraints.video.mandatory.minWidth = options.width;
}
if (isFinite(options.height))
{
constraints.video.mandatory.maxHeight = options.height;
constraints.video.mandatory.minHeight = options.height;
}
if (isFinite(options.frameRate))
{
constraints.video.mandatory.maxFrameRate = options.frameRate;
constraints.video.mandatory.minFrameRate = options.frameRate;
}
return constraints;
}
}
class FirefoxScreenShare
{
constructor()
{
this._stream = null;
}
start(options = {})
{
const constraints = this._toConstraints(options);
return navigator.mediaDevices.getUserMedia(constraints)
.then((stream) =>
{
this._stream = stream;
return Promise.resolve(stream);
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
return true;
}
needExtension()
{
return false;
}
_toConstraints(options)
{
const constraints = {
video : {
mediaSource : 'window'
},
audio : false
};
if ('mediaSource' in options)
{
constraints.video.mediaSource = options.mediaSource;
}
if (isFinite(options.width))
{
constraints.video.width = {
min : options.width,
max : options.width
};
}
if (isFinite(options.height))
{
constraints.video.height = {
min : options.height,
max : options.height
};
}
if (isFinite(options.frameRate))
{
constraints.video.frameRate = {
min : options.frameRate,
max : options.frameRate
};
}
return constraints;
}
}
export default class ScreenShare
{
static create()
{
switch (getBrowserType())
{
case 'firefox':
{
return new FirefoxScreenShare();
}
case 'chrome':
{
return new ChromeScreenShare();
}
default:
{
return null;
}
}
}
}

View File

@ -8,7 +8,8 @@ const Peer = (props) =>
const {
peer,
micConsumer,
webcamConsumer
webcamConsumer,
screenConsumer
} = props;
const micEnabled = (
@ -23,11 +24,22 @@ const Peer = (props) =>
!webcamConsumer.remotelyPaused
);
const screenVisible = (
Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
!screenConsumer.remotelyPaused
);
let videoProfile;
if (webcamConsumer)
videoProfile = webcamConsumer.profile;
let screenProfile;
if (screenConsumer)
screenProfile = screenConsumer.profile;
return (
<div data-component='Peer'>
<div className='indicators'>
@ -52,10 +64,14 @@ const Peer = (props) =>
peer={peer}
audioTrack={micConsumer ? micConsumer.track : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null}
screenTrack={screenConsumer ? screenConsumer.track : null}
videoVisible={videoVisible}
videoProfile={videoProfile}
screenVisible={screenVisible}
screenProfile={screenProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
screenCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
);
@ -65,7 +81,8 @@ Peer.propTypes =
{
peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer
};
const mapStateToProps = (state, { name }) =>
@ -77,11 +94,14 @@ const mapStateToProps = (state, { name }) =>
consumersArray.find((consumer) => consumer.source === 'mic');
const webcamConsumer =
consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen');
return {
peer,
micConsumer,
webcamConsumer
webcamConsumer,
screenConsumer
};
};

View File

@ -16,7 +16,9 @@ export default class PeerView extends React.Component
{
volume : 0, // Integer from 0 to 10.,
videoWidth : null,
videoHeight : null
videoHeight : null,
screenWidth : null,
screenHeight : null
};
// Latest received video track.
@ -27,6 +29,10 @@ export default class PeerView extends React.Component
// @type {MediaStreamTrack}
this._videoTrack = null;
// Latest received screen track.
// @type {MediaStreamTrack}
this._screenTrack = null;
// Hark instance.
// @type {Object}
this._hark = null;
@ -42,27 +48,49 @@ export default class PeerView extends React.Component
peer,
videoVisible,
videoProfile,
screenVisible,
screenProfile,
audioCodec,
videoCodec,
screenCodec,
onChangeDisplayName
} = this.props;
const {
volume,
videoWidth,
videoHeight
videoHeight,
screenWidth,
screenHeight
} = this.state;
return (
<div data-component='PeerView'>
<div className='info'>
<div className={classnames('media', { 'is-me': isMe })}>
{screenVisible ?
<div className='box'>
{audioCodec ?
<p className='codec'>{audioCodec}</p>
:null
}
{screenCodec ?
<p className='codec'>{screenCodec} {screenProfile}</p>
:null
}
{(screenVisible && screenWidth !== null) ?
<p className='resolution'>{screenWidth}x{screenHeight}</p>
:null
}
</div>
:<div className='box'>
{audioCodec ?
<p className='codec'>{audioCodec}</p>
:null
}
{videoCodec ?
<p className='codec'>{videoCodec} {videoProfile}</p>
:null
@ -73,6 +101,7 @@ export default class PeerView extends React.Component
:null
}
</div>
}
</div>
<div className={classnames('peer', { 'is-me': isMe })}>
@ -110,6 +139,19 @@ export default class PeerView extends React.Component
<video
ref='video'
className={classnames({
hidden : !videoVisible && !screenVisible,
'is-me' : isMe,
loading : videoProfile === 'none' && screenProfile === 'none'
})}
autoPlay
muted={isMe}
/>
{screenVisible ?
<div className='minivideo'>
<video
ref='minivideo'
className={classnames({
hidden : !videoVisible,
'is-me' : isMe,
@ -118,12 +160,15 @@ export default class PeerView extends React.Component
autoPlay
muted={isMe}
/>
</div>
:null
}
<div className='volume-container'>
<div className={classnames('bar', `level${volume}`)} />
</div>
{videoProfile === 'none' ?
{videoProfile === 'none' && screenProfile === 'none' ?
<div className='spinner-container'>
<Spinner />
</div>
@ -135,9 +180,9 @@ export default class PeerView extends React.Component
componentDidMount()
{
const { audioTrack, videoTrack } = this.props;
const { audioTrack, videoTrack, screenTrack } = this.props;
this._setTracks(audioTrack, videoTrack);
this._setTracks(audioTrack, videoTrack, screenTrack);
}
componentWillUnmount()
@ -150,18 +195,21 @@ export default class PeerView extends React.Component
componentWillReceiveProps(nextProps)
{
const { audioTrack, videoTrack } = nextProps;
const { audioTrack, videoTrack, screenTrack } = nextProps;
this._setTracks(audioTrack, videoTrack);
this._setTracks(audioTrack, videoTrack, screenTrack);
}
_setTracks(audioTrack, videoTrack)
_setTracks(audioTrack, videoTrack, screenTrack)
{
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
if (this._audioTrack === audioTrack &&
this._videoTrack === videoTrack &&
this._screenTrack === screenTrack)
return;
this._audioTrack = audioTrack;
this._videoTrack = videoTrack;
this._screenTrack = screenTrack;
if (this._hark)
this._hark.stop();
@ -169,9 +217,9 @@ export default class PeerView extends React.Component
clearInterval(this._videoResolutionTimer);
this._hideVideoResolution();
const { video } = this.refs;
const { video, minivideo } = this.refs;
if (audioTrack || videoTrack)
if (audioTrack || videoTrack || screenTrack)
{
const stream = new MediaStream;
@ -181,7 +229,19 @@ export default class PeerView extends React.Component
if (videoTrack)
stream.addTrack(videoTrack);
if (screenTrack)
{
const screenStream = new MediaStream;
screenStream.addTrack(screenTrack);
video.srcObject = screenStream;
minivideo.srcObject = stream;
}
else
{
video.srcObject = stream;
}
if (audioTrack)
this._runHark(stream);
@ -252,9 +312,13 @@ PeerView.propTypes =
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
audioTrack : PropTypes.any,
videoTrack : PropTypes.any,
screenTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired,
videoProfile : PropTypes.string,
screenVisible : PropTypes.bool.isRequired,
screenProfile : PropTypes.string,
audioCodec : PropTypes.string,
videoCodec : PropTypes.string,
screenCodec : PropTypes.string,
onChangeDisplayName : PropTypes.func
};

View File

@ -3,13 +3,85 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from './appPropTypes';
import * as stateActions from '../redux/stateActions';
import { Appear } from './transitions';
import Peer from './Peer';
const Peers = ({ peers, activeSpeakerName }) =>
class Peers extends React.Component
{
constructor()
{
super();
this.state = {
ratio : 4 / 3
};
}
updateDimensions()
{
const n = this.props.peers.length;
if (n == 0)
{
return;
}
const width = this.refs.peers.clientWidth;
const height = this.refs.peers.clientHeight;
let x, y, space;
for (let rows = 1; rows < 100; rows = rows + 1)
{
x = width / Math.ceil(n / rows);
y = x / this.state.ratio;
if (height < (y * rows))
{
y = height / rows;
x = this.state.ratio * y;
break;
}
space = height - (y * (rows));
if (space < y)
{
break;
}
}
if (Math.ceil(this.props.peerWidth) !== Math.ceil(0.9 * x))
{
this.props.onComponentResize(0.9 * x, 0.9 * y);
}
}
componentDidMount()
{
window.addEventListener('resize', this.updateDimensions.bind(this));
}
componentWillUnmount()
{
window.removeEventListener('resize', this.updateDimensions.bind(this));
}
render()
{
const {
activeSpeakerName,
peers,
peerWidth,
peerHeight
} = this.props;
const style =
{
'width' : peerWidth,
'height' : peerHeight
};
this.updateDimensions();
return (
<div data-component='Peers'>
<div data-component='Peers' ref='peers'>
{
peers.map((peer) =>
{
@ -18,7 +90,7 @@ const Peers = ({ peers, activeSpeakerName }) =>
<div
className={classnames('peer-container', {
'active-speaker' : peer.name === activeSpeakerName
})}
})} style={style}
>
<Peer name={peer.name} />
</div>
@ -28,12 +100,26 @@ const Peers = ({ peers, activeSpeakerName }) =>
}
</div>
);
};
}
}
Peers.propTypes =
{
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
activeSpeakerName : PropTypes.string
activeSpeakerName : PropTypes.string,
peerHeight : PropTypes.number,
peerWidth : PropTypes.number,
onComponentResize : PropTypes.func.isRequired
};
const mapDispatchToProps = (dispatch) =>
{
return {
onComponentResize : (peerWidth, peerHeight) =>
{
dispatch(stateActions.onComponentResize(peerWidth, peerHeight));
}
};
};
const mapStateToProps = (state) =>
@ -44,10 +130,15 @@ const mapStateToProps = (state) =>
return {
peers : peersArray,
activeSpeakerName : state.room.activeSpeakerName
activeSpeakerName : state.room.activeSpeakerName,
peerHeight : state.room.peerHeight,
peerWidth : state.room.peerWidth
};
};
const PeersContainer = connect(mapStateToProps)(Peers);
const PeersContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Peers);
export default PeersContainer;

View File

@ -20,12 +20,40 @@ class Room extends React.Component
room,
me,
amActiveSpeaker,
screenProducer,
onRoomLinkCopy,
onSetAudioMode,
onRestartIce,
onLeaveMeeting
onLeaveMeeting,
onShareScreen,
onUnShareScreen,
onNeedExtension
} = this.props;
let screenState;
let screenTip;
if (me.needExtension)
{
screenState = 'need-extension';
screenTip = 'Install screen sharing extension';
}
else if (!me.canShareScreen)
{
screenState = 'unsupported';
screenTip = 'Screen sharing not supported';
}
else if (screenProducer)
{
screenState = 'on';
screenTip = 'Stop screen sharing';
}
else
{
screenState = 'off';
screenTip = 'Start screen sharing';
}
return (
<Appear duration={300}>
<div data-component='Room'>
@ -79,6 +107,37 @@ class Room extends React.Component
</div>
<div className='sidebar'>
<div
className={classnames('button', 'screen', screenState)}
data-tip={screenTip}
data-type='dark'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
onUnShareScreen();
break;
}
case 'off':
{
onShareScreen();
break;
}
case 'need-extension':
{
onNeedExtension();
break;
}
default:
{
break;
}
}
}}
/>
<div
className={classnames('button', 'audio-only', {
on : me.audioOnly,
@ -122,18 +181,27 @@ Room.propTypes =
room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired,
screenProducer : appPropTypes.Producer,
onRoomLinkCopy : PropTypes.func.isRequired,
onSetAudioMode : PropTypes.func.isRequired,
onRestartIce : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired
onLeaveMeeting : PropTypes.func.isRequired,
onShareScreen : PropTypes.func.isRequired,
onUnShareScreen : PropTypes.func.isRequired,
onNeedExtension : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
const producersArray = Object.values(state.producers);
const screenProducer =
producersArray.find((producer) => producer.source === 'screen');
return {
room : state.room,
me : state.me,
amActiveSpeaker : state.me.name === state.room.activeSpeakerName
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
screenProducer : screenProducer
};
};
@ -161,6 +229,18 @@ const mapDispatchToProps = (dispatch) =>
onLeaveMeeting : () =>
{
dispatch(requestActions.leaveRoom());
},
onShareScreen : () =>
{
dispatch(requestActions.enableScreenSharing());
},
onUnShareScreen : () =>
{
dispatch(requestActions.disableScreenSharing());
},
onNeedExtension : () =>
{
dispatch(requestActions.installExtension());
}
};
};

View File

@ -33,9 +33,9 @@ export const Me = PropTypes.shape(
export const Producer = PropTypes.shape(
{
id : PropTypes.number.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam' ]).isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back' ]),
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
track : PropTypes.any,
@ -54,7 +54,7 @@ export const Consumer = PropTypes.shape(
{
id : PropTypes.number.isRequired,
peerName : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam' ]).isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
supported : PropTypes.bool.isRequired,
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,

View File

@ -2,6 +2,8 @@
```js
{
peerWidth : 200,
peerHeight : 150,
room :
{
url : 'https://example.io/?&roomId=d0el8y34',

View File

@ -6,8 +6,11 @@ const initialState =
device : null,
canSendMic : false,
canSendWebcam : false,
canShareScreen : false,
needExtension : false,
canChangeWebcam : false,
webcamInProgress : false,
screenShareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
restartIceInProgress : false
@ -31,6 +34,13 @@ const me = (state = initialState, action) =>
return { ...state, canSendMic, canSendWebcam };
}
case 'SET_SCREEN_CAPABILITIES':
{
const { canShareScreen, needExtension } = action.payload;
return { ...state, canShareScreen, needExtension };
}
case 'SET_CAN_CHANGE_WEBCAM':
{
const canChangeWebcam = action.payload;
@ -45,6 +55,13 @@ const me = (state = initialState, action) =>
return { ...state, webcamInProgress: flag };
}
case 'SET_SCREEN_SHARE_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, screenShareInProgress: flag };
}
case 'SET_DISPLAY_NAME':
{
let { displayName } = action.payload;

View File

@ -2,7 +2,9 @@ const initialState =
{
url : null,
state : 'new', // new/connecting/connected/disconnected/closed,
activeSpeakerName : null
activeSpeakerName : null,
peerHeight : 300,
peerWidth : 400
};
const room = (state = initialState, action) =>
@ -33,6 +35,13 @@ const room = (state = initialState, action) =>
return { ...state, activeSpeakerName: peerName };
}
case 'SET_COMPONENT_SIZE':
{
const { peerWidth, peerHeight } = action.payload;
return { ...state, peerWidth: peerWidth, peerHeight: peerHeight };
}
default:
return state;
}

View File

@ -85,6 +85,27 @@ export const restartIce = () =>
};
};
export const enableScreenSharing = () =>
{
return {
type : 'ENABLE_SCREEN_SHARING'
};
};
export const disableScreenSharing = () =>
{
return {
type : 'DISABLE_SCREEN_SHARING'
};
};
export const installExtension = () =>
{
return {
type : 'INSTALL_EXTENSION'
};
};
export const sendChatMessage = (text, name) =>
{
const message = createNewMessage(text, 'response', name);

View File

@ -109,6 +109,27 @@ export default ({ dispatch, getState }) => (next) =>
break;
}
case 'ENABLE_SCREEN_SHARING':
{
client.enableScreenSharing();
break;
}
case 'DISABLE_SCREEN_SHARING':
{
client.disableScreenSharing();
break;
}
case 'INSTALL_EXTENSION':
{
client.installExtension();
break;
}
case 'SEND_CHAT_MESSAGE':
{
const { message } = action.payload;

View File

@ -22,6 +22,14 @@ export const setRoomActiveSpeaker = (peerName) =>
};
};
export const onComponentResize = (peerWidth, peerHeight) =>
{
return {
type : 'SET_COMPONENT_SIZE',
payload : { peerWidth, peerHeight }
};
};
export const setMe = ({ peerName, displayName, displayNameSet, device }) =>
{
return {
@ -38,6 +46,14 @@ export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) =>
};
};
export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
{
return {
type : 'SET_SCREEN_CAPABILITIES',
payload : { canShareScreen, needExtension }
};
};
export const setCanChangeWebcam = (flag) =>
{
return {
@ -126,6 +142,14 @@ export const setWebcamInProgress = (flag) =>
};
};
export const setScreenShareInProgress = (flag) =>
{
return {
type : 'SET_SCREEN_SHARE_IN_PROGRESS',
payload : { flag }
};
};
export const addPeer = (peer) =>
{
return {

View File

@ -18,3 +18,22 @@ export function isMobile()
{
return !mediaQueryDetectorElem.offsetParent;
}
export function getBrowserType()
{
const ua = navigator.userAgent.toLowerCase();
// Firefox
if (ua.indexOf('firefox') !== -1)
{
return 'firefox';
}
// Chrome
if (ua.indexOf('chrome') !== -1 && ua.indexOf('edge') === -1)
{
return 'chrome';
}
return 'N/A';
}

77
app/package-lock.json generated
View File

@ -2570,6 +2570,12 @@
"resolved": "https://registry.npmjs.org/domready/-/domready-1.0.8.tgz",
"integrity": "sha1-kfJS5Ze2Wvd+dFriTdAYXV4m1Yw="
},
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@ -3219,6 +3225,21 @@
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"dev": true
},
"event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
"dev": true,
"requires": {
"duplexer": "0.1.1",
"from": "0.1.7",
"map-stream": "0.1.0",
"pause-stream": "0.0.11",
"split": "0.3.3",
"stream-combiner": "0.0.4",
"through": "2.3.8"
}
},
"eventemitter3": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz",
@ -3773,6 +3794,12 @@
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"dev": true
},
"from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
"dev": true
},
"fs-extra": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
@ -5246,6 +5273,15 @@
}
}
},
"gulp-change": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gulp-change/-/gulp-change-1.0.0.tgz",
"integrity": "sha1-inWf4bviU0TtFk50DpkxOxXM5jk=",
"dev": true,
"requires": {
"event-stream": "3.3.4"
}
},
"gulp-css-base64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/gulp-css-base64/-/gulp-css-base64-1.3.4.tgz",
@ -7008,6 +7044,12 @@
"integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
"dev": true
},
"map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true
},
"map-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
@ -8150,6 +8192,15 @@
"pinkie-promise": "2.0.1"
}
},
"pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
"dev": true,
"requires": {
"through": "2.3.8"
}
},
"pbkdf2": {
"version": "3.0.14",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz",
@ -8971,14 +9022,14 @@
"resolved": "https://registry.npmjs.org/riek/-/riek-1.1.0.tgz",
"integrity": "sha1-6oVNtKTtCWIw/wQ4JQjW374pWZQ=",
"requires": {
"debug": "2.6.8",
"debug": "2.6.9",
"prop-types": "15.6.0"
},
"dependencies": {
"debug": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
@ -9581,6 +9632,15 @@
"integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
"dev": true
},
"split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
"dev": true,
"requires": {
"through": "2.3.8"
}
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -9701,6 +9761,15 @@
"readable-stream": "2.3.3"
}
},
"stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
"dev": true,
"requires": {
"duplexer": "0.1.1"
}
},
"stream-combiner2": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",

View File

@ -49,6 +49,7 @@
"gulp": "^4.0.0",
"gulp-css-base64": "^1.3.4",
"gulp-eslint": "^4.0.2",
"gulp-change": "^1.0.0",
"gulp-header": "^2.0.1",
"gulp-if": "^2.0.2",
"gulp-plumber": "^1.2.0",

View File

@ -2,7 +2,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 511.626 511.626" style="enable-background:new 0 0 511.626 511.626;" xml:space="preserve">
<g>
<path d="M477.371,127.44c-22.843-28.074-53.871-50.249-93.076-66.523c-39.204-16.272-82.035-24.41-128.478-24.41 c-34.643,0-67.762,4.805-99.357,14.417c-31.595,9.611-58.812,22.602-81.653,38.97c-22.845,16.37-41.018,35.832-54.534,58.385 C6.757,170.833,0,194.484,0,219.228c0,28.549,8.61,55.3,25.837,80.234c17.227,24.931,40.778,45.871,70.664,62.811 c-2.096,7.611-4.57,14.846-7.426,21.693c-2.855,6.852-5.424,12.474-7.708,16.851c-2.286,4.377-5.376,9.233-9.281,14.562 c-3.899,5.328-6.849,9.089-8.848,11.275c-1.997,2.19-5.28,5.812-9.851,10.849c-4.565,5.048-7.517,8.329-8.848,9.855 c-0.193,0.089-0.953,0.952-2.285,2.567c-1.331,1.615-1.999,2.423-1.999,2.423l-1.713,2.566c-0.953,1.431-1.381,2.334-1.287,2.707 c0.096,0.373-0.094,1.331-0.57,2.851c-0.477,1.526-0.428,2.669,0.142,3.433v0.284c0.765,3.429,2.43,6.187,4.998,8.277 c2.568,2.092,5.474,2.95,8.708,2.563c12.375-1.522,23.223-3.606,32.548-6.276c49.87-12.758,93.649-35.782,131.334-69.097 c14.272,1.522,28.072,2.286,41.396,2.286c46.442,0,89.271-8.138,128.479-24.417c39.208-16.272,70.233-38.448,93.072-66.517 c22.843-28.062,34.263-58.663,34.263-91.781C511.626,186.108,500.207,155.509,477.371,127.44z" fill="#bababa"/>
<path d="M477.371,127.44c-22.843-28.074-53.871-50.249-93.076-66.523c-39.204-16.272-82.035-24.41-128.478-24.41 c-34.643,0-67.762,4.805-99.357,14.417c-31.595,9.611-58.812,22.602-81.653,38.97c-22.845,16.37-41.018,35.832-54.534,58.385 C6.757,170.833,0,194.484,0,219.228c0,28.549,8.61,55.3,25.837,80.234c17.227,24.931,40.778,45.871,70.664,62.811 c-2.096,7.611-4.57,14.846-7.426,21.693c-2.855,6.852-5.424,12.474-7.708,16.851c-2.286,4.377-5.376,9.233-9.281,14.562 c-3.899,5.328-6.849,9.089-8.848,11.275c-1.997,2.19-5.28,5.812-9.851,10.849c-4.565,5.048-7.517,8.329-8.848,9.855 c-0.193,0.089-0.953,0.952-2.285,2.567c-1.331,1.615-1.999,2.423-1.999,2.423l-1.713,2.566c-0.953,1.431-1.381,2.334-1.287,2.707 c0.096,0.373-0.094,1.331-0.57,2.851c-0.477,1.526-0.428,2.669,0.142,3.433v0.284c0.765,3.429,2.43,6.187,4.998,8.277 c2.568,2.092,5.474,2.95,8.708,2.563c12.375-1.522,23.223-3.606,32.548-6.276c49.87-12.758,93.649-35.782,131.334-69.097 c14.272,1.522,28.072,2.286,41.396,2.286c46.442,0,89.271-8.138,128.479-24.417c39.208-16.272,70.233-38.448,93.072-66.517 c22.843-28.062,34.263-58.663,34.263-91.781C511.626,186.108,500.207,155.509,477.371,127.44z" fill="#FFFFFF"/>
</g>
<g>
</g>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<g id="XMLID_2_">
<path id="XMLID_8_" class="st0" d="M69.1,61.3l4.6,4.6h1.8v-4.6H69.1z M70.9,56.7l0-22.9c0-2.5-2.1-4.6-4.6-4.6H37l12,12
c0.4-0.1,0.8-0.2,1.3-0.2v-4.9l9.2,8.5L55.8,48l12.7,12.7C69.9,59.9,70.9,58.4,70.9,56.7z M26,23.9L23,26.8l3.5,3.5
c-0.9,0.8-1.5,2-1.5,3.4v22.9c0,2.5,2,4.6,4.6,4.6h-9.2v4.6H62l6.2,6.2l2.9-2.9L26,23.9z M36.5,54.4c0.7-3.4,2.1-6.8,4.7-9.3
l3.6,3.6C41.4,49.6,38.7,51.4,36.5,54.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="XMLID_2_">
<path id="XMLID_8_" class="st0" d="M69.1,61.3l4.6,4.6h1.8v-4.6H69.1z M70.9,56.7l0-22.9c0-2.5-2.1-4.6-4.6-4.6H37l12,12
c0.4-0.1,0.8-0.2,1.3-0.2v-4.9l9.2,8.5L55.8,48l12.7,12.7C69.9,59.9,70.9,58.4,70.9,56.7z M26,23.9L23,26.8l3.5,3.5
c-0.9,0.8-1.5,2-1.5,3.4v22.9c0,2.5,2,4.6,4.6,4.6h-9.2v4.6H62l6.2,6.2l2.9-2.9L26,23.9z M36.5,54.4c0.7-3.4,2.1-6.8,4.7-9.3
l3.6,3.6C41.4,49.6,38.7,51.4,36.5,54.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<g id="XMLID_2_">
<path id="XMLID_6_" class="st0" d="M66.3,61.8c2.5,0,4.6-2.1,4.6-4.6l0-22.9c0-2.5-2.1-4.6-4.6-4.6H29.7c-2.5,0-4.6,2-4.6,4.6v22.9
c0,2.5,2,4.6,4.6,4.6h-9.2v4.6h55v-4.6H66.3z M50.3,53.7v-5c-6.4,0-10.6,1.9-13.8,6.2c1.3-6.1,4.8-12.2,13.8-13.5v-4.9l9.2,8.5
L50.3,53.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 83.208 83.208" style="enable-background:new 0 0 83.208 83.208;" xml:space="preserve">
<g>
<g>
<g>
<polygon style="fill:#D80027;" points="25.052,69.154 17.894,76.312 53.683,76.312 46.525,69.154 "/>
</g>
<g>
<path style="fill:#D80027;" d="M64.419,6.896c-7.831,0-14.537,4.814-17.357,11.631H0v46.525h71.577v-22.01
c6.814-2.82,11.631-9.53,11.631-17.357C83.208,15.325,74.78,6.896,64.419,6.896z M64.419,42.685c-9.373,0-17-7.627-17-17
s7.627-17,17-17s17,7.627,17,17S73.792,42.685,64.419,42.685z"/>
</g>
<g>
<polygon style="fill:#D80027;" points="66.338,29.372 67.068,14.258 61.764,14.258 62.533,29.372 "/>
</g>
<g>
<path style="fill:#D80027;" d="M64.434,31.039c-1.764,0-3.003,1.267-3.003,3.035c0,1.732,1.199,3.035,3.003,3.035
c1.804,0,2.97-1.303,2.97-3.035C67.368,32.306,66.202,31.039,64.434,31.039z"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="XMLID_2_">
<path id="XMLID_6_" class="st0" d="M66.3,61.8c2.5,0,4.6-2.1,4.6-4.6l0-22.9c0-2.5-2.1-4.6-4.6-4.6H29.7c-2.5,0-4.6,2-4.6,4.6v22.9
c0,2.5,2,4.6,4.6,4.6h-9.2v4.6h55v-4.6H66.3z M50.3,53.7v-5c-6.4,0-10.6,1.9-13.8,6.2c1.3-6.1,4.8-12.2,13.8-13.5v-4.9l9.2,8.5
L50.3,53.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -15,7 +15,7 @@
background-position: center;
background-size: 70%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
background-color: rgba(#fff, 0.3);
background-image: url('/resources/images/chat-icon.svg');
cursor: pointer;
transition-property: opacity, background-color;

View File

@ -24,11 +24,6 @@
display: flex;
flex-direction: column;
justify-content: space-between;
background: linear-gradient(to bottom,
rgba($backgroundTint, 0) 0%,
rgba($backgroundTint, 0) 60%,
rgba($backgroundTint, 0.1) 70%,
rgba($backgroundTint, 0.8) 100%);
> .media {
flex: 0 0 auto;
@ -195,6 +190,39 @@
}
}
> .minivideo {
height: 15%;
width: 15%;
bottom: 1%;
right: 1%;
position: absolute;
overflow: hidden;
> video {
flex: 100 100 auto;
height: 100%;
width: 100%;
object-fit: cover;
user-select: none;
transition-property: opacity;
transition-duration: .15s;
background-color: rgba(#000, 0.75);
&.is-me {
transform: scaleX(-1);
}
&.hidden {
opacity: 0;
transition-duration: 0s;
}
&.loading {
filter: blur(5px);
}
}
}
> .volume-container {
position: absolute;
top: 0

View File

@ -1,10 +1,11 @@
[data-component='Peers'] {
min-height: 100%;
width: 100%;
height: 100%;
+desktop() {
width: 100%;
padding: 40px 0 140px 0;
padding: 0px 0 0px 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@ -29,8 +30,6 @@
+desktop() {
flex: 0 0 auto;
height: 382px;
width: 450px;
margin: 6px;
border: 1px solid rgba(#fff, 0.15);
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);

View File

@ -232,6 +232,25 @@
}
}
&.screen {
&.on {
background-image: url('/resources/images/no-share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/share-screen-white.svg');
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.need-extension {
background-image: url('/resources/images/share-screen-extension.svg');
}
}
&.leave-meeting {
background-image: url('/resources/images/leave-meeting.svg');
}

View File

@ -48,7 +48,7 @@ body {
#multiparty-meeting-media-query-detector {
position: relative;
z-index: -1000;
bottom: 0;
bottom: 1px;
left: 0;
height: 1px;
width: 1px;