Merge branch 'develop'
|
|
@ -1,5 +1,7 @@
|
|||
node_modules/
|
||||
|
||||
/app/config.*
|
||||
!/app/config.example.js
|
||||
/server/config.*
|
||||
!/server/config.example.js
|
||||
/server/public/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
module.exports =
|
||||
{
|
||||
chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'
|
||||
};
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
```js
|
||||
{
|
||||
peerWidth : 200,
|
||||
peerHeight : 150,
|
||||
room :
|
||||
{
|
||||
url : 'https://example.io/?&roomId=d0el8y34',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||