Massive change. Changed to react-scripts (webpack) for building. Changed to material-ui where applicable. Changed to CSS-in-JS.

This commit is contained in:
Håvar Aambø Fosstveit
2019-03-29 14:12:13 +01:00
parent 36944a9ea7
commit 0449d6ff20
185 changed files with 4564 additions and 6442 deletions
@@ -0,0 +1,124 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../../actions/stateActions';
import BuddyImage from '../../images/buddy.svg';
const styles = () =>
({
root :
{
width : '12vmin',
height : '9vmin',
position : 'absolute',
bottom : '3%',
right : '3%',
color : 'rgba(170, 170, 170, 1)',
cursor : 'pointer',
backgroundImage : `url(${BuddyImage})`,
backgroundColor : 'rgba(42, 75, 88, 1)',
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat',
border : 'var(--peer-border)',
boxShadow : 'var(--peer-shadow)',
textAlign : 'center',
verticalAlign : 'middle',
lineHeight : '1.8vmin',
fontSize : '1.7vmin',
fontWeight : 'bolder',
animation : 'none',
'&.pulse' :
{
animation : 'pulse 0.5s'
}
},
'@keyframes pulse' :
{
'0%' :
{
transform : 'scale3d(1, 1, 1)'
},
'50%' :
{
transform : 'scale3d(1.2, 1.2, 1.2)'
},
'100%' :
{
transform : 'scale3d(1, 1, 1)'
}
}
});
class HiddenPeers extends Component
{
constructor(props)
{
super(props);
this.state = { className: '' };
}
componentDidUpdate(prevProps)
{
const { hiddenPeersCount } = this.props;
if (hiddenPeersCount !== prevProps.hiddenPeersCount)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ className: 'pulse' }, () =>
{
if (this.timeout)
{
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() =>
{
this.setState({ className: '' });
}, 500);
});
}
}
render()
{
const {
hiddenPeersCount,
openUsersTab,
classes
} = this.props;
return (
<div className={classnames(classes.root, this.state.className)} onClick={() => openUsersTab()}>
<p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount === 1) ? null : 's'}
</p>
</div>
);
}
}
HiddenPeers.propTypes =
{
hiddenPeersCount : PropTypes.number,
openUsersTab : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapDispatchToProps = (dispatch) =>
{
return {
openUsersTab : () =>
{
dispatch(stateActions.openToolArea());
dispatch(stateActions.setToolTab('users'));
}
};
};
export default connect(
null,
mapDispatchToProps
)(withStyles(styles)(HiddenPeers));
+332
View File
@@ -0,0 +1,332 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import ReactTooltip from 'react-tooltip';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { getDeviceInfo } from 'mediasoup-client';
import * as appPropTypes from '../appPropTypes';
import PeerView from '../VideoContainers/PeerView';
import ScreenView from '../VideoContainers/ScreenView';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VideoIcon from '@material-ui/icons/Videocam';
import VideoOffIcon from '@material-ui/icons/VideocamOff';
const styles = () =>
({
root :
{
display : 'flex',
flexDirection : 'row',
flex : '100 100 auto',
position : 'relative'
},
viewContainer :
{
position : 'relative',
width : 'var(--me-width)',
height : 'var(--me-height)',
'&.webcam' :
{
order : 2
},
'&.screen' :
{
order : 1
}
},
controls :
{
position : 'absolute',
right : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
padding : '0.4vmin',
zIndex : 20,
opacity : 0,
transition : 'opacity 0.3s',
'&.visible' :
{
opacity : 1
}
},
button :
{
flex : '0 0 auto',
margin : '0.2vmin',
borderRadius : 2,
opacity : 0.85,
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
backgroundColor : 'var(--media-control-button-color)',
'&:hover' :
{
opacity : 1
},
'&.unsupported' :
{
pointerEvents : 'none'
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.on' :
{
backgroundColor : 'var(--media-control-botton-on)'
},
'&.off' :
{
backgroundColor : 'var(--media-control-botton-off)'
}
}
});
class Me extends React.Component
{
state = {
controlsVisible : false
};
handleMouseOver = () =>
{
this.setState({
controlsVisible : true
});
};
handleMouseOut = () =>
{
this.setState({
controlsVisible : false
});
};
constructor(props)
{
super(props);
this._mounted = false;
this._rootNode = null;
this._tooltip = true;
// TODO: Issue when using react-tooltip in Edge:
// https://github.com/wwayne/react-tooltip/issues/328
if (getDeviceInfo().flag === 'msedge')
this._tooltip = false;
}
render()
{
const {
roomClient,
connected,
me,
advancedMode,
micProducer,
webcamProducer,
screenProducer,
classes
} = this.props;
let micState;
if (!me.canSendMic)
micState = 'unsupported';
else if (!micProducer)
micState = 'unsupported';
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
micState = 'on';
else
micState = 'off';
let webcamState;
if (!me.canSendWebcam)
webcamState = 'unsupported';
else if (webcamProducer)
webcamState = 'on';
else
webcamState = 'off';
const videoVisible = (
Boolean(webcamProducer) &&
!webcamProducer.locallyPaused &&
!webcamProducer.remotelyPaused
);
const screenVisible = (
Boolean(screenProducer) &&
!screenProducer.locallyPaused &&
!screenProducer.remotelyPaused
);
let tip;
if (!me.displayNameSet)
tip = 'Click on your name to change it';
return (
<div
className={classes.root}
ref={(node) => (this._rootNode = node)}
data-tip={tip}
data-tip-disable={!tip}
data-type='dark'
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<div className={classnames(classes.viewContainer, 'webcam')}>
{ connected ?
<div className={classnames(classes.controls, 'visible')}>
<div
data-tip='keyboard shortcut: &lsquo;m&lsquo;'
data-type='dark'
data-place='bottom'
data-for='me'
className={classnames(classes.button, 'mic', micState, {
disabled : me.audioInProgress,
visible : micState === 'off' || this.state.controlsVisible
})}
onClick={() =>
{
micState === 'on' ?
roomClient.muteMic() :
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</div>
<ReactTooltip
id='me'
effect='solid'
/>
<div
className={classnames(classes.button, 'webcam', webcamState, {
disabled : me.webcamInProgress,
visible : webcamState === 'off' || this.state.controlsVisible
})}
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</div>
</div>
:null
}
<PeerView
isMe
advancedMode={advancedMode}
peer={me}
audioTrack={micProducer ? micProducer.track : null}
volume={micProducer ? micProducer.volume : null}
videoTrack={webcamProducer ? webcamProducer.track : null}
videoVisible={videoVisible}
audioCodec={micProducer ? micProducer.codec : null}
videoCodec={webcamProducer ? webcamProducer.codec : null}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
/>
</div>
{ screenProducer ?
<div className={classnames(classes.viewContainer, 'screen')}>
<ScreenView
isMe
advancedMode={advancedMode}
screenTrack={screenProducer ? screenProducer.track : null}
screenVisible={screenVisible}
screenCodec={screenProducer ? screenProducer.codec : null}
/>
</div>
:null
}
</div>
);
}
componentDidMount()
{
this._mounted = true;
if (this._tooltip)
{
setTimeout(() =>
{
if (!this._mounted || this.props.me.displayNameSet)
return;
ReactTooltip.show(this._rootNode);
}, 4000);
}
}
componentWillUnmount()
{
this._mounted = false;
}
componentWillReceiveProps(nextProps)
{
if (this._tooltip)
{
if (nextProps.me.displayNameSet)
ReactTooltip.hide(this._rootNode);
}
}
}
Me.propTypes =
{
roomClient : PropTypes.any.isRequired,
connected : PropTypes.bool.isRequired,
advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
const producersArray = Object.values(state.producers);
const micProducer =
producersArray.find((producer) => producer.source === 'mic');
const webcamProducer =
producersArray.find((producer) => producer.source === 'webcam');
const screenProducer =
producersArray.find((producer) => producer.source === 'screen');
return {
connected : state.room.state === 'connected',
me : state.me,
micProducer : micProducer,
webcamProducer : webcamProducer,
screenProducer : screenProducer
};
};
export default withRoomContext(connect(
mapStateToProps,
null
)(withStyles(styles)(Me)));
+397
View File
@@ -0,0 +1,397 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../../actions/stateActions';
import PeerView from '../VideoContainers/PeerView';
import ScreenView from '../VideoContainers/ScreenView';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import NewWindowIcon from '@material-ui/icons/OpenInNew';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
const styles = () =>
({
root :
{
width : '100%',
height : '100%',
display : 'flex',
flexDirection : 'row',
flex : '100 100 auto',
position : 'relative'
},
viewContainer :
{
position : 'relative',
flexGrow : 1,
'&.webcam' :
{
order : 2
},
'&.screen' :
{
order : 1,
maxWidth : '50%'
}
},
controls :
{
position : 'absolute',
right : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
padding : '0.4vmin',
zIndex : 20,
opacity : 0,
transition : 'opacity 0.3s',
'&.visible' :
{
opacity : 1
}
},
button :
{
flex : '0 0 auto',
margin : '0.2vmin',
borderRadius : 2,
opacity : 0.85,
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
backgroundColor : 'var(--media-control-button-color)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
'&:hover' :
{
opacity : 1
},
'&.unsupported' :
{
pointerEvents : 'none'
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.on' :
{
backgroundColor : 'var(--media-control-botton-on)'
},
'&.off' :
{
backgroundColor : 'var(--media-control-botton-off)'
}
},
pausedVideo :
{
position : 'absolute',
zIndex : 11,
top : 0,
bottom : 0,
left : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
'& p' :
{
padding : '6px 12px',
borderRadius : 6,
userSelect : 'none',
pointerEvents : 'none',
fontSize : 20,
color : 'rgba(255, 255, 255, 0.55)'
}
},
incompatibleVideo :
{
position : 'absolute',
zIndex : 10,
top : 0,
bottom : 0,
left : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
'& p' :
{
padding : '6px 12px',
borderRadius : 6,
userSelect : 'none',
pointerEvents : 'none',
fontSize : 20,
color : 'rgba(255, 255, 255, 0.55)'
}
}
});
class Peer extends Component
{
state = {
controlsVisible : false
};
handleMouseOver = () =>
{
this.setState({
controlsVisible : true
});
};
handleMouseOut = () =>
{
this.setState({
controlsVisible : false
});
};
render()
{
const {
roomClient,
advancedMode,
peer,
micConsumer,
webcamConsumer,
screenConsumer,
toggleConsumerFullscreen,
toggleConsumerWindow,
style,
windowConsumer,
classes
} = this.props;
const micEnabled = (
Boolean(micConsumer) &&
!micConsumer.locallyPaused &&
!micConsumer.remotelyPaused
);
const videoVisible = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!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
className={classnames(classes.root, {
screen : screenConsumer
})}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
{ videoVisible && !webcamConsumer.supported ?
<div className={classes.incompatibleVideo}>
<p>incompatible video</p>
</div>
:null
}
{ !videoVisible ?
<div className={classes.pausedVideo}>
<p>this video is paused</p>
</div>
:null
}
<div className={classnames(classes.viewContainer, 'webcam')} style={style}>
<div
className={classnames(classes.controls, {
visible : this.state.controlsVisible
})}
>
<div
className={classnames(classes.button, {
on : micEnabled,
off : !micEnabled,
disabled : peer.peerAudioInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}}
>
{ micEnabled ?
<MicIcon />
:
<MicOffIcon />
}
</div>
<div
className={classnames(classes.button, {
disabled : !videoVisible ||
(windowConsumer === webcamConsumer.id)
})}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</div>
<div
className={classnames(classes.button, 'fullscreen', {
disabled : !videoVisible
})}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</div>
</div>
<PeerView
advancedMode={advancedMode}
peer={peer}
volume={micConsumer ? micConsumer.volume : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible}
videoProfile={videoProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
/>
</div>
{ screenConsumer ?
<div className={classnames(classes.viewContainer, 'screen')} style={style}>
<div
className={classnames(classes.controls, {
visible : this.state.controlsVisible
})}
>
<div
className={classnames(classes.button, 'newwindow', {
disabled : !screenVisible ||
(windowConsumer === screenConsumer.id)
})}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</div>
<div
className={classnames(classes.button, 'fullscreen', {
disabled : !screenVisible
})}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</div>
</div>
<ScreenView
advancedMode={advancedMode}
screenTrack={screenConsumer ? screenConsumer.track : null}
screenVisible={screenVisible}
screenProfile={screenProfile}
screenCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
:null
}
</div>
);
}
}
Peer.propTypes =
{
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number,
streamDimensions : PropTypes.object,
style : PropTypes.object,
toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired,
classes : PropTypes.object
};
const mapStateToProps = (state, { name }) =>
{
const peer = state.peers[name];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const micConsumer =
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,
screenConsumer,
windowConsumer : state.room.windowConsumer
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleConsumerFullscreen : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
},
toggleConsumerWindow : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerWindow(consumer.id));
}
};
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Peer)));
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { RIEInput } from 'riek';
export default class EditableInput extends React.Component
{
render()
{
const {
value,
propName,
className,
classLoading,
classInvalid,
editProps,
onChange
} = this.props;
return (
<RIEInput
value={value}
propName={propName}
className={className}
classLoading={classLoading}
classInvalid={classInvalid}
shouldBlockWhileLoading
editProps={editProps}
change={(data) => onChange(data)}
/>
);
}
shouldComponentUpdate(nextProps)
{
if (nextProps.value === this.props.value)
return false;
return true;
}
}
EditableInput.propTypes =
{
value : PropTypes.string,
propName : PropTypes.string.isRequired,
className : PropTypes.string,
classLoading : PropTypes.string,
classInvalid : PropTypes.string,
editProps : PropTypes.any,
onChange : PropTypes.func.isRequired
};
+190
View File
@@ -0,0 +1,190 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import Fab from '@material-ui/core/Fab';
import Avatar from '@material-ui/core/Avatar';
import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExtensionIcon from '@material-ui/icons/Extension';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import HandOff from '../../images/icon-hand-black.svg';
import HandOn from '../../images/icon-hand-white.svg';
import LeaveIcon from '@material-ui/icons/Cancel';
const styles = (theme) =>
({
root :
{
position : 'fixed',
zIndex : 500,
top : '50%',
transform : 'translate(0%, -50%)',
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
left : '1.0em',
width : '2.6em'
},
fab :
{
margin: theme.spacing.unit
}
});
class Sidebar extends Component
{
render()
{
const {
roomClient,
toolbarsVisible,
me,
screenProducer,
locked,
classes
} = this.props;
let screenState;
if (me.needExtension)
{
screenState = 'need-extension';
}
else if (!me.canShareScreen)
{
screenState = 'unsupported';
}
else if (screenProducer)
{
screenState = 'on';
}
else
{
screenState = 'off';
}
return (
<div
className={classnames(classes.root, 'room-controls', {
'visible' : toolbarsVisible
})}
>
<Fab
aria-label='Share screen'
className={classes.fab}
disabled={!me.canShareScreen || me.screenShareInProgress}
color={screenState === 'on' ? 'secondary' : 'default'}
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
case 'need-extension':
{
roomClient.installExtension();
break;
}
default:
{
break;
}
}
}}
>
{ screenState === 'on' || screenState === 'unsupported' ?
<ScreenOffIcon/>
:null
}
{ screenState === 'off' ?
<ScreenIcon/>
:null
}
{ screenState === 'need-extension' ?
<ExtensionIcon/>
:null
}
</Fab>
<Fab
aria-label='Room lock'
className={classes.fab}
color={locked ? 'secondary' : 'default'}
onClick={() =>
{
if (locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ locked ?
<LockIcon />
:
<LockOpenIcon />
}
</Fab>
<Fab
aria-label='Raise hand'
className={classes.fab}
disabled={me.raiseHandInProgress}
color={me.raiseHand ? 'secondary' : 'default'}
onClick={() => roomClient.sendRaiseHandState(!me.raiseHand)}
>
<Avatar alt='Hand' src={me.raiseHand ? HandOn : HandOff} />
</Fab>
<Fab
aria-label='Leave meeting'
className={classes.fab}
color='secondary'
onClick={() => roomClient.close()}
>
<LeaveIcon />
</Fab>
</div>
);
}
}
Sidebar.propTypes = {
roomClient : PropTypes.any.isRequired,
toolbarsVisible : PropTypes.bool.isRequired,
me : appPropTypes.Me.isRequired,
screenProducer : appPropTypes.Producer,
locked : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
toolbarsVisible : state.room.toolbarsVisible,
screenProducer : Object.values(state.producers)
.find((producer) => producer.source === 'screen'),
me : state.me,
locked : state.room.locked
});
export default withRoomContext(connect(
mapStateToProps
)(withStyles(styles)(Sidebar)));
+108
View File
@@ -0,0 +1,108 @@
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)
{
// eslint-disable-next-line
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;
}
}
+5
View File
@@ -0,0 +1,5 @@
.LoadingView
{
height : 100%;
width : 100%;
}
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import './LoadingView.css';
const LoadingView = () =>
{
return (
<div className='LoadingView' />
);
};
export default LoadingView;
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import MessageList from './MessageList';
import ChatInput from './ChatInput';
const styles = () =>
({
root :
{
display : 'flex',
flexDirection : 'column',
width : '100%',
height : '100%'
}
});
const Chat = (props) =>
{
const {
classes
} = props;
return (
<Paper className={classes.root}>
<MessageList />
<ChatInput />
</Paper>
);
};
Chat.propTypes =
{
classes : PropTypes.object.isRequired
};
export default withStyles(styles)(Chat);
@@ -0,0 +1,131 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
import IconButton from '@material-ui/core/IconButton';
import SendIcon from '@material-ui/icons/Send';
const styles = (theme) =>
({
root :
{
padding : theme.spacing.unit,
display : 'flex',
alignItems : 'center',
borderRadius : 0
},
input :
{
marginLeft : 8,
flex : 1
},
iconButton :
{
padding : 10
}
});
class ChatInput extends Component
{
constructor(props)
{
super(props);
this.state =
{
message : ''
};
}
createNewMessage = (text, sender, name, picture) =>
({
type : 'message',
text,
time : Date.now(),
name,
sender,
picture
});
handleChange = (e) =>
{
this.setState({ message: e.target.value });
}
render()
{
const {
roomClient,
displayName,
picture,
classes
} = this.props;
return (
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder='Enter chat message...'
value={this.state.message || ''}
onChange={this.handleChange}
onKeyPress={(ev) =>
{
if (ev.key === 'Enter')
{
ev.preventDefault();
if (this.state.message && this.state.message !== '')
{
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
roomClient.sendChatMessage(message);
this.setState({ message: '' });
}
}
}}
autoFocus
/>
<IconButton
color='primary'
className={classes.iconButton}
aria-label='Send'
onClick={() =>
{
if (this.state.message && this.state.message !== '')
{
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
roomClient.sendChatMessage(message);
this.setState({ message: '' });
}
}}
>
<SendIcon />
</IconButton>
</Paper>
);
}
}
ChatInput.propTypes =
{
roomClient : PropTypes.object.isRequired,
displayName : PropTypes.string,
picture : PropTypes.string,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
displayName : state.me.displayName,
picture : state.me.picture
});
export default withRoomContext(
connect(mapStateToProps)(withStyles(styles)(ChatInput))
);
@@ -0,0 +1,99 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import marked from 'marked';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
const linkRenderer = new marked.Renderer();
linkRenderer.link = (href, title, text) =>
{
title = title ? title : href;
text = text ? text : href;
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
};
const styles = (theme) =>
({
root :
{
display : 'flex',
marginBottom : theme.spacing.unit,
padding : theme.spacing.unit
},
selfMessage :
{
marginLeft : 'auto',
},
remoteMessage :
{
marginRight : 'auto'
},
text :
{
'& p' :
{
margin : 0
}
},
content :
{
marginLeft : theme.spacing.unit
},
avatar :
{
borderRadius : '50%',
height : '2rem',
alignSelf : 'center'
},
});
const Message = (props) =>
{
const {
self,
picture,
text,
time,
name,
classes
} = props;
return (
<Paper
className={classnames(
classes.root,
self ? classes.selfMessage : classes.remoteMessage
)}
>
<img alt='Avatar' className={classes.avatar} src={picture} />
<div className={classes.content}>
<Typography
className={classes.text}
variant='subtitle1'
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html : marked.parse(
text,
{ sanitize: true, renderer: linkRenderer }
) }}
/>
<Typography variant='caption'>{self ? 'Me' : name} - {time}</Typography>
</div>
</Paper>
);
};
Message.propTypes =
{
self : PropTypes.bool,
picture : PropTypes.string,
text : PropTypes.string,
time : PropTypes.string,
name : PropTypes.string,
classes : PropTypes.object.isRequired
};
export default withStyles(styles)(Message);
@@ -0,0 +1,92 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Message from './Message';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
const styles = (theme) =>
({
root :
{
height : '100%',
display : 'flex',
flexDirection : 'column',
alignItems : 'center',
overflowY : 'auto',
padding : theme.spacing.unit
}
});
class MessageList extends React.Component
{
componentDidMount()
{
this.node.scrollTop = this.node.scrollHeight;
}
getSnapshotBeforeUpdate()
{
return this.node.scrollTop
+ this.node.offsetHeight === this.node.scrollHeight;
}
componentDidUpdate(prevProps, prevState, shouldScroll)
{
if (shouldScroll)
{
this.node.scrollTop = this.node.scrollHeight;
}
}
getTimeString(time)
{
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
}
render()
{
const {
chatmessages,
classes
} = this.props;
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
{
chatmessages.map((message, index) =>
{
const messageTime = new Date(message.time);
const picture = (message.sender === 'response' ?
message.picture : this.props.myPicture) || EmptyAvatar;
return (
<Message
key={index}
self={message.sender === 'client'}
picture={picture}
text={message.text}
time={this.getTimeString(messageTime)}
name={message.name}
/>
);
})
}
</div>
);
}
}
MessageList.propTypes =
{
chatmessages : PropTypes.array,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
chatmessages : state.chatmessages
});
export default connect(mapStateToProps, null)(withStyles(styles)(MessageList));
@@ -0,0 +1,164 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import magnet from 'magnet-uri';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
const styles = (theme) =>
({
root :
{
display : 'flex',
alignItems : 'center',
width : '100%',
padding : theme.spacing.unit,
boxShadow : '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
'&:not(:last-child)' :
{
marginBottom : theme.spacing.unit
}
},
avatar :
{
borderRadius : '50%',
height : '2rem'
},
text :
{
margin : 0,
padding : theme.spacing.unit
},
fileContent :
{
display : 'flex',
alignItems : 'center'
},
fileInfo :
{
display : 'flex',
alignItems : 'center',
padding : theme.spacing.unit
},
button :
{
marginRight : 'auto'
}
});
class File extends Component
{
render()
{
const {
roomClient,
torrentSupport,
file,
classes
} = this.props;
return (
<div className={classes.root}>
<img alt='Peer avatar' className={classes.avatar} src={file.picture || EmptyAvatar} />
<div className={classes.fileContent}>
{ file.files ?
<Fragment>
<Typography className={classes.text}>
File finished downloading
</Typography>
{ file.files.map((sharedFile, i) => (
<div className={classes.fileInfo} key={i}>
<Typography className={classes.text}>
{sharedFile.name}
</Typography>
<Button
variant='contained'
component='span'
className={classes.button}
onClick={() =>
{
roomClient.saveFile(sharedFile);
}}
>
Save
</Button>
</div>
))}
</Fragment>
:null
}
<Typography className={classes.text}>
{ file.me ?
'You shared a file'
:
`${file.displayName} shared a file`
}
</Typography>
{ !file.active && !file.files ?
<div className={classes.fileInfo}>
<Typography className={classes.text}>
{magnet.decode(file.magnetUri).dn}
</Typography>
{ torrentSupport ?
<Button
variant='contained'
component='span'
className={classes.button}
onClick={() =>
{
roomClient.handleDownload(file.magnetUri);
}}
>
Download
</Button>
:
<Typography className={classes.text}>
Your browser does not support downloading files using WebTorrent.
</Typography>
}
</div>
:null
}
{ file.timeout ?
<Typography className={classes.text}>
If this process takes a long time, there might not be anyone seeding
this torrent. Try asking someone to reupload the file that you want.
</Typography>
:null
}
{ file.active ?
<progress value={file.progress} />
:null
}
</div>
</div>
);
}
}
File.propTypes = {
roomClient : PropTypes.object.isRequired,
torrentSupport : PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state, { magnetUri }) =>
{
return {
file : state.files[magnetUri],
torrentSupport : state.room.torrentSupport
};
};
export default withRoomContext(connect(
mapStateToProps
)(withStyles(styles)(File)));
@@ -0,0 +1,71 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import File from './File';
const styles = (theme) =>
({
root :
{
height : '100%',
display : 'flex',
flexDirection : 'column',
alignItems : 'center',
overflowY : 'auto',
padding : theme.spacing.unit
}
});
class FileList extends Component
{
componentDidMount()
{
this.node.scrollTop = this.node.scrollHeight;
}
getSnapshotBeforeUpdate()
{
return this.node.scrollTop
+ this.node.offsetHeight === this.node.scrollHeight;
}
componentDidUpdate(prevProps, prevState, shouldScroll)
{
if (shouldScroll)
{
this.node.scrollTop = this.node.scrollHeight;
}
}
render()
{
const {
files,
classes
} = this.props;
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
{ Object.keys(files).map((magnetUri) =>
<File key={magnetUri} magnetUri={magnetUri} />
)}
</div>
);
}
}
FileList.propTypes =
{
files : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
files : state.files
};
};
export default connect(mapStateToProps)(withStyles(styles)(FileList));
@@ -0,0 +1,99 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import FileList from './FileList';
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
root :
{
display : 'flex',
flexDirection : 'column',
width : '100%',
height : '100%'
},
input :
{
display : 'none'
},
button :
{
margin : theme.spacing.unit
}
});
class FileSharing extends Component
{
constructor(props)
{
super(props);
this._fileInput = React.createRef();
}
handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
this.props.roomClient.shareFiles(event.target.files);
}
};
render()
{
const {
torrentSupport,
classes
} = this.props;
const buttonDescription = torrentSupport ?
'Share file' : 'File sharing not supported';
return (
<Paper className={classes.root}>
<input
ref={this._fileInput}
className={classes.input}
type='file'
onChange={this.handleFileChange}
id='share-files-button'
/>
<label htmlFor='share-files-button'>
<Button
variant='contained'
component='span'
className={classes.button}
disabled={!torrentSupport}
>
{buttonDescription}
</Button>
</label>
<FileList />
</Paper>
);
}
}
FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired,
torrentSupport : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
torrentSupport : state.room.torrentSupport,
tabOpen : state.toolarea.currentToolTab === 'files'
};
};
export default withRoomContext(connect(
mapStateToProps
)(withStyles(styles)(FileSharing)));
@@ -0,0 +1,108 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../../actions/stateActions';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Badge from '@material-ui/core/Badge';
import Chat from './Chat/Chat';
import FileSharing from './FileSharing/FileSharing';
import ParticipantList from './ParticipantList/ParticipantList';
const tabs =
[
'chat',
'files',
'users'
];
const styles = (theme) =>
({
root :
{
display : 'flex',
flexDirection : 'column',
width : '100%',
height : '100%',
backgroundColor : theme.palette.background.paper
}
});
class MeetingDrawer extends React.Component
{
handleChange = (event, value) =>
{
this.props.setToolTab(tabs[value]);
};
render()
{
const {
currentToolTab,
unreadMessages,
unreadFiles,
classes
} = this.props;
return (
<div className={classes.root}>
<AppBar position='static' color='default'>
<Tabs
value={tabs.indexOf(currentToolTab)}
onChange={this.handleChange}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
>
<Tab
label=
{
<Badge color='secondary' badgeContent={unreadMessages}>
Chat
</Badge>
}
/>
<Tab
label=
{
<Badge color='secondary' badgeContent={unreadFiles}>
File sharing
</Badge>
}
/>
<Tab label='Participants' />
</Tabs>
</AppBar>
{currentToolTab === 'chat' && <Chat />}
{currentToolTab === 'files' && <FileSharing />}
{currentToolTab === 'users' && <ParticipantList />}
</div>
);
}
}
MeetingDrawer.propTypes =
{
currentToolTab : PropTypes.string.isRequired,
setToolTab : PropTypes.func.isRequired,
unreadMessages : PropTypes.number.isRequired,
unreadFiles : PropTypes.number.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
currentToolTab : state.toolarea.currentToolTab,
unreadMessages : state.toolarea.unreadMessages,
unreadFiles : state.toolarea.unreadFiles
});
const mapDispatchToProps = {
setToolTab : stateActions.setToolTab
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(MeetingDrawer));
@@ -0,0 +1,115 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = () =>
({
root :
{
padding : '0.5rem',
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
listPeer :
{
display : 'flex'
},
avatar :
{
borderRadius : '50%',
height : '2rem'
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : '0.5rem',
flexGrow : 1,
alignItems : 'center'
},
indicators :
{
left : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
},
icon :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`,
opacity : 1
}
},
});
const ListMe = (props) =>
{
const {
me,
classes
} = props;
const picture = me.picture || EmptyAvatar;
return (
<li className={classes.root}>
<div className={classes.listPeer}>
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}>
{me.displayName}
</div>
<div className={classes.indicators}>
{ me.raisedHand ?
<div className={classnames(classes.icon, 'raise-hand')} />
:null
}
</div>
</div>
</li>
);
};
ListMe.propTypes =
{
me : appPropTypes.Me.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
me : state.me
});
export default connect(
mapStateToProps
)(withStyles(styles)(ListMe));
@@ -0,0 +1,294 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = () =>
({
root :
{
padding : '0.5rem',
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
listPeer :
{
display : 'flex'
},
avatar :
{
borderRadius : '50%',
height : '2rem'
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : '0.5rem',
flexGrow : 1,
alignItems : 'center'
},
indicators :
{
left : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
},
icon :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.on' :
{
opacity : 1
},
'&.off' :
{
opacity : 0.2
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`
}
},
volumeContainer :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
width : '1vmin',
position : 'relative',
backgroundSize : '75%'
},
bar :
{
flex : '0 0 auto',
margin : '0.3rem',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 1)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
width : 3,
borderRadius : 6,
transitionDuration : '0.25s',
position : 'absolute',
bottom : 0,
'&.level0' : { height: 0 },
'&.level1' : { height: '0.2vh' },
'&.level2' : { height: '0.4vh' },
'&.level3' : { height: '0.6vh' },
'&.level4' : { height: '0.8vh' },
'&.level5' : { height: '1.0vh' },
'&.level6' : { height: '1.2vh' },
'&.level7' : { height: '1.4vh' },
'&.level8' : { height: '1.6vh' },
'&.level9' : { height: '1.8vh' },
'&.level10' : { height: '2.0vh' }
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
},
button :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.5)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.unsupported' :
{
pointerEvents : 'none'
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.on' :
{
backgroundColor : 'var(--media-control-botton-on)'
},
'&.off' :
{
backgroundColor : 'var(--media-control-botton-off)'
}
}
});
const ListPeer = (props) =>
{
const {
roomClient,
peer,
micConsumer,
screenConsumer,
classes
} = props;
const micEnabled = (
Boolean(micConsumer) &&
!micConsumer.locallyPaused &&
!micConsumer.remotelyPaused
);
const screenVisible = (
Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
!screenConsumer.remotelyPaused
);
const picture = peer.picture || EmptyAvatar;
return (
<div className={classes.root}>
<img alt='Peer avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}>
{peer.displayName}
</div>
<div className={classes.indicators}>
{ peer.raiseHandState ?
<div className={
classnames(
classes.icon, 'raise-hand', {
on : peer.raiseHandState,
off : !peer.raiseHandState
}
)
}
/>
:null
}
</div>
<div className={classes.volumeContainer}>
<div className={classnames(classes.bar, `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
</div>
<div className={classes.controls}>
{ screenConsumer ?
<div
className={classnames(classes.button, 'screen', {
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
}}
>
{ screenVisible ?
<ScreenIcon />
:
<ScreenOffIcon />
}
</div>
:null
}
<div
className={classnames(classes.button, 'mic', {
on : micEnabled,
off : !micEnabled,
disabled : peer.peerAudioInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}}
>
{ micEnabled ?
<MicIcon />
:
<MicOffIcon />
}
</div>
</div>
</div>
);
};
ListPeer.propTypes =
{
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state, { name }) =>
{
const peer = state.peers[name];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const micConsumer =
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,
screenConsumer
};
};
export default withRoomContext(connect(
mapStateToProps
)(withStyles(styles)(ListPeer)));
@@ -0,0 +1,154 @@
import React from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types';
import ListPeer from './ListPeer';
import ListMe from './ListMe';
const styles = (theme) =>
({
root :
{
width : '100%',
overflowY : 'auto',
padding : 6
},
list :
{
listStyleType : 'none',
padding : theme.spacing.unit,
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)'
},
listheader :
{
padding : '0.5rem',
fontWeight : 'bolder'
},
listItem :
{
padding : '0.5rem',
width : '100%',
overflow : 'hidden',
cursor : 'pointer',
'&.selected' :
{
backgroundColor: 'rgba(55, 126, 255, 1)'
},
'&:not(:last-child)' :
{
borderBottom : '1px solid #CBCBCB'
}
}
});
class ParticipantList extends React.Component
{
componentDidMount()
{
this.node.scrollTop = this.node.scrollHeight;
}
getSnapshotBeforeUpdate()
{
return this.node.scrollTop
+ this.node.offsetHeight === this.node.scrollHeight;
}
componentDidUpdate(prevProps, prevState, shouldScroll)
{
if (shouldScroll)
{
this.node.scrollTop = this.node.scrollHeight;
}
}
render()
{
const {
roomClient,
advancedMode,
peers,
selectedPeerName,
spotlights,
classes
} = this.props;
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
<ul className={classes.list}>
<li className={classes.listheader}>Me:</li>
<ListMe />
</ul>
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Participants in Spotlight:</li>
{peers.filter((peer) =>
{
return (spotlights.find((spotlight) =>
{ return (spotlight === peer.name); }));
}).map((peer) => (
<li
key={peer.name}
className={classNames(classes.listItem, {
selected : peer.name === selectedPeerName
})}
onClick={() => roomClient.setSelectedPeer(peer.name)}
>
<ListPeer name={peer.name} advancedMode={advancedMode} />
</li>
))}
</ul>
<br />
<ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li>
{peers.filter((peer) =>
{
return !(spotlights.find((spotlight) =>
{ return (spotlight === peer.name); }));
}).map((peer) => (
<li
key={peer.name}
className={classNames(classes.listItem, {
selected : peer.name === selectedPeerName
})}
onClick={() => roomClient.setSelectedPeer(peer.name)}
>
<ListPeer name={peer.name} advancedMode={advancedMode} />
</li>
))}
</ul>
</div>
);
}
}
ParticipantList.propTypes =
{
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
selectedPeerName : PropTypes.string,
spotlights : PropTypes.array.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
const peersArray = Object.values(state.peers);
return {
peers : peersArray,
selectedPeerName : state.room.selectedPeerName,
spotlights : state.room.spotlights
};
};
const ParticipantListContainer = withRoomContext(connect(
mapStateToProps
)(withStyles(styles)(ParticipantList)));
export default ParticipantListContainer;
@@ -0,0 +1,210 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import debounce from 'lodash/debounce';
import { withStyles } from '@material-ui/core/styles';
import Peer from '../Containers/Peer';
import HiddenPeers from '../Containers/HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334;
const PADDING = 100;
const styles = (theme) =>
({
root :
{
width : '100%',
height : '100%',
display : 'flex',
flexDirection : 'row',
flexWrap : 'wrap',
justifyContent : 'center',
alignItems : 'center',
alignContent : 'center',
paddingTop : 70,
paddingBottom : 30
},
peerContainer :
{
overflow : 'hidden',
flex : '0 0 auto',
margin : 6,
boxShadow : 'var(--peer-shadow)',
border : 'var(--peer-border)',
transitionProperty : 'border-color',
'&.active-speaker' :
{
borderColor : 'var(--active-speaker-border-color)'
},
'&.selected' :
{
borderColor : 'var(--selected-peer-border-color)'
}
}
});
class Democratic extends React.Component
{
constructor(props)
{
super(props);
this.state = {
peerWidth : 400,
peerHeight : 300
};
this.peersRef = React.createRef();
}
updateDimensions = debounce(() =>
{
if (!this.peersRef.current)
{
return;
}
const n = this.props.boxes;
if (n === 0)
{
return;
}
const width = this.peersRef.current.clientWidth;
const height = this.peersRef.current.clientHeight - PADDING;
let x, y, space;
for (let rows = 1; rows < 100; rows = rows + 1)
{
x = width / Math.ceil(n / rows);
y = x / RATIO;
if (height < (y * rows))
{
y = height / rows;
x = RATIO * y;
break;
}
space = height - (y * (rows));
if (space < y)
{
break;
}
}
if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x))
{
this.setState({
peerWidth : 0.9 * x,
peerHeight : 0.9 * y
});
}
}, 200);
componentDidMount()
{
window.addEventListener('resize', this.updateDimensions);
const observer = new ResizeObserver(this.updateDimensions);
observer.observe(this.peersRef.current);
}
componentWillUnmount()
{
window.removeEventListener('resize', this.updateDimensions);
}
componentDidUpdate()
{
this.updateDimensions();
}
render()
{
const {
advancedMode,
activeSpeakerName,
peers,
spotlights,
spotlightsLength,
classes
} = this.props;
const style =
{
'width' : this.state.peerWidth,
'height' : this.state.peerHeight
};
return (
<div className={classes.root} ref={this.peersRef}>
{ Object.keys(peers).map((peerName) =>
{
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
{
return (
<div
key={peerName}
className={classnames(classes.peerContainer, {
'selected' : this.props.selectedPeerName === peerName,
'active-speaker' : peerName === activeSpeakerName
})}
>
<Peer
advancedMode={advancedMode}
name={peerName}
style={style}
/>
</div>
);
}
else
{
return('');
}
})}
{ spotlightsLength < Object.keys(peers).length ?
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length - spotlightsLength}
/>
:null
}
</div>
);
}
}
Democratic.propTypes =
{
advancedMode : PropTypes.bool,
peers : PropTypes.object.isRequired,
boxes : PropTypes.number,
activeSpeakerName : PropTypes.string,
selectedPeerName : PropTypes.string,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
const spotlights = state.room.spotlights;
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
const boxes = spotlightsLength + Object.values(state.consumers)
.filter((consumer) => consumer.source === 'screen').length;
return {
peers : state.peers,
boxes,
activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName,
spotlights,
spotlightsLength
};
};
export default connect(
mapStateToProps
)(withStyles(styles)(Democratic));
@@ -0,0 +1,85 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withSnackbar } from 'notistack';
import * as stateActions from '../../actions/stateActions';
class Notifications extends Component
{
displayed = [];
storeDisplayed = (id) =>
{
this.displayed = [ ...this.displayed, id ];
};
shouldComponentUpdate({ notifications: newNotifications = [] })
{
const { notifications: currentNotifications } = this.props;
let notExists = false;
for (let i = 0; i < newNotifications.length; i += 1)
{
if (notExists) continue;
notExists = notExists ||
!currentNotifications.filter(({ id }) => newNotifications[i].id === id).length;
}
return notExists;
}
componentDidUpdate()
{
const { notifications = [] } = this.props;
notifications.forEach((notification) =>
{
// Do nothing if snackbar is already displayed
if (this.displayed.includes(notification.id)) return;
// Display snackbar using notistack
this.props.enqueueSnackbar(notification.text,
{
variant : notification.type,
autoHideDuration : notification.timeout,
anchorOrigin : {
vertical : 'bottom',
horizontal : 'left'
}
}
);
// Keep track of snackbars that we've displayed
this.storeDisplayed(notification.id);
// Dispatch action to remove snackbar from redux store
this.props.removeNotification(notification.id);
});
}
render()
{
return null;
}
}
Notifications.propTypes =
{
notifications : PropTypes.array.isRequired,
enqueueSnackbar : PropTypes.func.isRequired,
removeNotification : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
({
notifications : state.notifications
});
const mapDispatchToProps = (dispatch) =>
({
removeNotification : (notificationId) =>
dispatch(stateActions.removeNotification({ notificationId }))
});
export default withSnackbar(
connect(mapStateToProps, mapDispatchToProps)(Notifications)
);
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes';
import PeerAudio from './PeerAudio';
const AudioPeer = ({ micConsumer }) =>
{
return (
<PeerAudio
audioTrack={micConsumer ? micConsumer.track : null}
/>
);
};
AudioPeer.propTypes =
{
micConsumer : appPropTypes.Consumer,
name : PropTypes.string
};
const mapStateToProps = (state, { name }) =>
{
const peer = state.peers[name];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const micConsumer =
consumersArray.find((consumer) => consumer.source === 'mic');
return {
micConsumer
};
};
const AudioPeerContainer = connect(
mapStateToProps
)(AudioPeer);
export default AudioPeerContainer;
@@ -0,0 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import AudioPeer from './AudioPeer';
const AudioPeers = ({ peers }) =>
{
return (
<div data-component='AudioPeers'>
{
Object.values(peers).map((peer) =>
{
return (
<AudioPeer
key={peer.name}
name={peer.name}
/>
);
})
}
</div>
);
};
AudioPeers.propTypes =
{
peers : PropTypes.object
};
const mapStateToProps = (state) =>
({
peers : state.peers
});
const AudioPeersContainer = connect(
mapStateToProps
)(AudioPeers);
export default AudioPeersContainer;
+67
View File
@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class PeerAudio extends React.Component
{
constructor(props)
{
super(props);
// Latest received audio track.
// @type {MediaStreamTrack}
this._audioTrack = null;
}
render()
{
return (
<audio
ref='audio'
autoPlay
/>
);
}
componentDidMount()
{
const { audioTrack } = this.props;
this._setTrack(audioTrack);
}
componentWillReceiveProps(nextProps)
{
const { audioTrack } = nextProps;
this._setTrack(audioTrack);
}
_setTrack(audioTrack)
{
if (this._audioTrack === audioTrack)
return;
this._audioTrack = audioTrack;
const { audio } = this.refs;
if (audioTrack)
{
const stream = new MediaStream();
if (audioTrack)
stream.addTrack(audioTrack);
audio.srcObject = stream;
}
else
{
audio.srcObject = null;
}
}
}
PeerAudio.propTypes =
{
audioTrack : PropTypes.any
};
+406
View File
@@ -0,0 +1,406 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from './appPropTypes';
import classnames from 'classnames';
import { withRoomContext } from '../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import * as stateActions from '../actions/stateActions';
import Draggable from 'react-draggable';
import { idle } from '../utils';
import FullScreen from './FullScreen';
import CookieConsent from 'react-cookie-consent';
import CssBaseline from '@material-ui/core/CssBaseline';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
import Hidden from '@material-ui/core/Hidden';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Badge from '@material-ui/core/Badge';
import AccountCircle from '@material-ui/icons/AccountCircle';
import Notifications from './Notifications/Notifications';
import MeetingDrawer from './MeetingDrawer/MeetingDrawer';
import Democratic from './MeetingViews/Democratic';
import Me from './Containers/Me';
import AudioPeers from './PeerAudio/AudioPeers';
import FullScreenView from './VideoContainers/FullScreenView';
import VideoWindow from './VideoWindow/VideoWindow';
import Sidebar from './Controls/Sidebar';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import SettingsIcon from '@material-ui/icons/Settings';
import Settings from './Settings/Settings';
const TIMEOUT = 10 * 1000;
const styles = (theme) =>
({
root :
{
display : 'flex',
width : '100%',
height : '100%'
},
message :
{
position : 'fixed',
top : '50%',
left : '50%',
transform : 'translateX(-50%) translateY(-50%)',
width : '30vw',
padding : 'theme.unit.spacing * 2'
},
menuButton :
{
margin : 0,
padding : 0
},
toolbar : theme.mixins.toolbar,
drawerPaper :
{
width : '30vw',
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
}
},
grow :
{
flexGrow : 1
},
title :
{
display : 'none',
marginLeft : 20,
[theme.breakpoints.up('sm')] :
{
display : 'block'
}
},
actionButtons :
{
display : 'flex'
},
meContainer :
{
position : 'fixed',
zIndex : 110,
overflow : 'hidden',
boxShadow : 'var(--me-shadow)',
transitionProperty : 'border-color',
transitionDuration : '0.15s',
top : '8%',
left : '1%',
border : 'var(--me-border)',
'&.active-speaker' :
{
borderColor : 'var(--active-speaker-border-color)'
}
}
});
class Room extends Component
{
constructor(props)
{
super(props);
this.fullscreen = new FullScreen(document);
this.state = {
drawerOpen : false,
fullscreen : false
};
}
waitForHide = idle(() =>
{
this.props.setToolbarsVisible(false);
}, TIMEOUT);
handleMovement = () =>
{
// If the toolbars were hidden, show them again when
// the user moves their cursor.
if (!this.props.room.toolbarsVisible)
{
this.props.setToolbarsVisible(true);
}
this.waitForHide();
}
componentDidMount()
{
if (this.fullscreen.fullscreenEnabled)
{
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
}
window.addEventListener('mousemove', this.handleMovement);
window.addEventListener('touchstart', this.handleMovement);
}
componentWillUnmount()
{
if (this.fullscreen.fullscreenEnabled)
{
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
}
window.removeEventListener('mousemove', this.handleMovement);
window.removeEventListener('touchstart', this.handleMovement);
}
handleToggleFullscreen = () =>
{
if (this.fullscreen.fullscreenElement)
{
this.fullscreen.exitFullscreen();
}
else
{
this.fullscreen.requestFullscreen(document.documentElement);
}
};
handleFullscreenChange = () =>
{
this.setState({
fullscreen : this.fullscreen.fullscreenElement !== null
});
};
render()
{
const {
roomClient,
room,
me,
amActiveSpeaker,
setSettingsOpen,
toolAreaOpen,
toggleToolArea,
unread,
classes,
theme
} = this.props;
if (room.audioSuspended)
{
return (
<div className={classes.root}>
<Paper className={classes.message}>
<Typography>This webpage required sound and video to play, please click to allow.</Typography>
<Button
variant='contained'
onClick={() =>
{
roomClient.notify('Joining.');
roomClient.resumeAudio();
}}
>
Allow
</Button>
</Paper>
</div>
);
}
else if (room.lockedOut)
{
return (
<div className={classes.root}>
<Paper className={classes.message}>
<Typography>This room is locked at the moment, try again later.</Typography>
</Paper>
</div>
);
}
else
{
return (
<div className={classes.root}>
<CookieConsent>
This website uses cookies to enhance the user experience.
</CookieConsent>
<FullScreenView advancedMode={room.advancedMode} />
<VideoWindow advancedMode={room.advancedMode} />
<AudioPeers />
<Notifications />
<CssBaseline />
<AppBar
position='fixed'
>
<Toolbar>
<Badge
color='secondary'
badgeContent={unread}
>
<IconButton
color='inherit'
aria-label='Open drawer'
onClick={() => toggleToolArea()}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</Badge>
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
Multiparty meeting
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
{ this.fullscreen.fullscreenEnabled ?
<IconButton
aria-label='Fullscreen'
color='inherit'
onClick={this.handleToggleFullscreen}
>
{ this.state.fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
:null
}
<IconButton
aria-label='Settings'
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
{ me.loginEnabled ?
<IconButton
aria-label='Account'
color='inherit'
onClick={() => me.loggedIn ? roomClient.logout() : roomClient.login() }
>
<AccountCircle />
</IconButton>
:null
}
</div>
</Toolbar>
</AppBar>
<nav>
<Hidden implementation='css'>
<SwipeableDrawer
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={toolAreaOpen}
onClose={() => toggleToolArea()}
onOpen={() => toggleToolArea()}
classes={{
paper : classes.drawerPaper
}}
>
<MeetingDrawer />
</SwipeableDrawer>
</Hidden>
</nav>
<Democratic advancedMode={room.advancedMode} />
<Draggable handle='.me-handle' bounds='body' cancel='.display-name'>
<div
className={classnames(classes.meContainer, 'me-handle', {
'active-speaker' : amActiveSpeaker
})}
>
<Me
advancedMode={room.advancedMode}
/>
</div>
</Draggable>
<Sidebar />
<Settings />
</div>
);
}
}
}
Room.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
screenProducer : appPropTypes.Producer,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.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,
screenProducer : screenProducer,
toolAreaOpen : state.toolarea.toolAreaOpen,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
setToolbarsVisible : (visible) =>
{
dispatch(stateActions.setToolbarsVisible(visible));
},
setSettingsOpen : (settingsOpen) =>
{
dispatch(stateActions.setSettingsOpen({ settingsOpen }));
},
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
}
};
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles, { withTheme: true })(Room)));
+152
View File
@@ -0,0 +1,152 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as stateActions from '../../actions/stateActions';
import PropTypes from 'prop-types';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
const styles = (theme) =>
({
root :
{
},
device :
{
width : '20vw',
padding : theme.spacing.unit * 2
},
formControl :
{
display : 'flex',
}
});
const Settings = ({
roomClient,
room,
me,
onToggleAdvancedMode,
handleChangeMode,
handleCloseSettings,
classes
}) =>
{
let webcams;
if (me.webcamDevices)
webcams = Array.from(me.webcamDevices.values());
else
webcams = [];
let audioDevices;
if (me.audioDevices)
audioDevices = Array.from(me.audioDevices.values());
else
audioDevices = [];
return (
<Dialog
className={classes.root}
open={room.settingsOpen}
onClose={() => handleCloseSettings({ settingsOpen: false })}
>
<DialogTitle id="form-dialog-title">Settings</DialogTitle>
<form className={classes.device} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={me.selectedWebcam || ''}
onChange={(event) => event.target.value ? roomClient.changeWebcam(event.target.value) : null }
displayEmpty
name='Camera'
autoWidth
className={classes.selectEmpty}
// disabled={!me.canChangeWebcam}
>
<MenuItem value='' />
{ webcams.map((webcam, index) =>
{
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
Select camera device
</FormHelperText>
</FormControl>
</form>
<form className={classes.device} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={me.selectedAudioDevice || ''}
onChange={(event) => event.target.value ? roomClient.changeAudioDevice(event.target.value) : null }
displayEmpty
name='Audio device'
autoWidth
className={classes.selectEmpty}
disabled={!me.canChangeAudioDevice}
>
<MenuItem value='' />
{ audioDevices.map((audio, index) =>
{
return (
<MenuItem key={index} value={audio.deviceId}>{audio.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ me.canChangeAudioDevice ?
'Select audio device'
:
'Unable to select audio device'
}
</FormHelperText>
</FormControl>
</form>
<DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
Close
</Button>
</DialogActions>
</Dialog>
);
};
Settings.propTypes =
{
roomClient : PropTypes.any.isRequired,
me : appPropTypes.Me.isRequired,
room : appPropTypes.Room.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
room : state.room
};
};
const mapDispatchToProps = {
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode,
handleCloseSettings : stateActions.setSettingsOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Settings)));
@@ -0,0 +1,163 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../actions/stateActions';
import FullView from './FullView';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
const styles = () =>
({
root :
{
position : 'absolute',
top : 0,
left : 0,
height : '100%',
width : '100%',
zIndex : 20000
},
controls :
{
position : 'absolute',
zIndex : 20020,
right : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
padding : '0.4vmin'
},
button :
{
flex : '0 0 auto',
margin : '0.2vmin',
borderRadius : 2,
backgroundColor : 'rgba(255, 255, 255, 0.7)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : '5vmin',
height : '5vmin'
},
icon :
{
fontSize : '5vmin'
},
incompatibleVideo :
{
position : 'absolute',
zIndex : 20010,
top : 0,
bottom : 0,
left : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
'& p' :
{
padding : '6px 12px',
borderRadius : 6,
userSelect : 'none',
pointerEvents : 'none',
fontSize : 15,
color : 'rgba(255, 255, 255, 0.55)'
}
}
});
const FullScreenView = (props) =>
{
const {
advancedMode,
consumer,
toggleConsumerFullscreen,
toolbarsVisible,
classes
} = props;
if (!consumer)
return null;
const consumerVisible = (
Boolean(consumer) &&
!consumer.locallyPaused &&
!consumer.remotelyPaused
);
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return (
<div className={classes.root}>
{ consumerVisible && !consumer.supported ?
<div className={classes.incompatibleVideo}>
<p>incompatible video</p>
</div>
:null
}
<div className={classes.controls}>
<div
className={classnames(classes.button, 'room-controls', {
visible : toolbarsVisible
})}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenExitIcon className={classes.icon} />
</div>
</div>
<FullView
advancedMode={advancedMode}
videoTrack={consumer ? consumer.track : null}
videoVisible={consumerVisible}
videoProfile={consumerProfile}
/>
</div>
);
};
FullScreenView.propTypes =
{
advancedMode : PropTypes.bool,
consumer : appPropTypes.Consumer,
toggleConsumerFullscreen : PropTypes.func.isRequired,
toolbarsVisible : PropTypes.bool,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
consumer : state.consumers[state.room.fullScreenConsumer],
toolbarsVisible : state.room.toolbarsVisible
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleConsumerFullscreen : (consumer) =>
{
if (consumer)
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(FullScreenView));
@@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
const styles = () =>
({
root :
{
position : 'relative',
flex : '100 100 auto',
height : '100%',
width : '100%',
display : 'flex',
flexDirection : 'column',
overflow : 'hidden'
},
video :
{
flex : '100 100 auto',
height : '100%',
width : '100%',
objectFit : 'contain',
userSelect : 'none',
transitionProperty : 'opacity',
transitionDuration : '.15s',
backgroundColor : 'rgba(0, 0, 0, 1)',
'&.hidden' :
{
opacity : 0,
transitionDuration : '0s'
},
'&.loading' :
{
filter : 'blur(5px)'
}
}
});
class FullView extends React.Component
{
constructor(props)
{
super(props);
// Latest received video track.
// @type {MediaStreamTrack}
this._videoTrack = null;
this.video = React.createRef();
}
render()
{
const {
videoVisible,
videoProfile,
classes
} = this.props;
return (
<div className={classes.root}>
<video
ref={this.video}
className={classnames(classes.video, {
hidden : !videoVisible,
loading : videoProfile === 'none'
})}
autoPlay
playsInline
muted={Boolean(true)}
/>
</div>
);
}
componentDidMount()
{
const { videoTrack } = this.props;
this._setTracks(videoTrack);
}
componentDidUpdate()
{
const { videoTrack } = this.props;
this._setTracks(videoTrack);
}
_setTracks(videoTrack)
{
if (this._videoTrack === videoTrack)
return;
this._videoTrack = videoTrack;
const video = this.video.current;
if (videoTrack)
{
const stream = new MediaStream();
stream.addTrack(videoTrack);
video.srcObject = stream;
}
else
{
video.srcObject = null;
}
}
}
FullView.propTypes =
{
videoTrack : PropTypes.any,
videoVisible : PropTypes.bool,
videoProfile : PropTypes.string,
classes : PropTypes.object.isRequired
};
export default withStyles(styles)(FullView);
@@ -0,0 +1,431 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import EditableInput from '../Controls/EditableInput';
const styles = (theme) =>
({
root:
{
position : 'relative',
flex : '100 100 auto',
height : '100%',
width : '100%',
display : 'flex',
flexDirection : 'column',
overflow : 'hidden',
backgroundColor : 'var(--peer-bg-color)',
backgroundImage : 'var(--peer-empty-avatar)',
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat'
},
video :
{
flex : '100 100 auto',
height : '100%',
width : '100%',
objectFit : 'cover',
userSelect : 'none',
transitionProperty : 'opacity',
transitionDuration : '.15s',
backgroundColor : 'var(--peer-video-bg-color)',
'&.is-me' :
{
transform : 'scaleX(-1)'
},
'&.hidden' :
{
opacity : 0,
transitionDuration : '0s'
},
'&.loading' :
{
filter : 'blur(5px)'
}
},
info :
{
position : 'absolute',
zIndex : 10,
top : '0.6vmin',
left : '0.6vmin',
bottom : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'space-between'
},
media :
{
flex : '0 0 auto',
display : 'flex',
flexDirection : 'row'
},
box :
{
padding : '0.4vmin',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.25)',
'& p' :
{
userSelect : 'none',
pointerEvents : 'none',
margin : 0,
color : 'rgba(255, 255, 255, 0.7)',
fontSize : 10,
'&:last-child' :
{
marginBottom : 0
}
}
},
peer :
{
flex : '0 0 auto',
display : 'flex',
flexDirection : 'column',
justifyContent : 'flex-end',
position : 'absolute',
bottom : '0.6vmin',
left : 0,
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.25)',
padding : '0.5vmin',
alignItems : 'flex-start'
},
displayNameEdit :
{
fontSize : 14,
fontWeight : 400,
color : 'rgba(255, 255, 255, 0.85)',
border : 'none',
borderBottom : '1px solid #aeff00',
backgroundColor : 'transparent'
},
displayNameStatic :
{
userSelect : 'none',
cursor : 'text',
fontSize : 14,
fontWeight : 400,
color : 'rgba(255, 255, 255, 0.85)',
'&:hover' :
{
backgroundColor: 'rgb(174, 255, 0, 0.25)'
}
},
deviceInfo :
{
marginTop : '0.4vmin',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'flex-end',
'& span' :
{
userSelect : 'none',
pointerEvents : 'none',
fontSize : 11,
color : 'rgba(255, 255, 255, 0.55)'
}
},
volume :
{
position : 'absolute',
top : 0,
bottom : 0,
right : 2,
width : 10,
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
},
volumeBar :
{
width : 6,
borderRadius : 6,
background : 'rgba(yellow, 0.65)',
transitionProperty : 'height background-color',
transitionDuration : '0.25s',
'&.level0' :
{
height : 0,
backgroundColor : 'rgba(255, 255, 0, 0.65)'
},
'&.level1' :
{
height : '10%',
backgroundColor : 'rgba(255, 255, 0, 0.65)'
},
'&.level2' :
{
height : '20%',
backgroundColor : 'rgba(255, 255, 0, 0.65)'
},
'&.level3' :
{
height : '30%',
backgroundColor : 'rgba(255, 255, 0, 0.65)'
},
'&.level4' :
{
height : '40%',
backgroundColor : 'rgba(255, 165, 0, 0.65)'
},
'&.level5' :
{
height : '50%',
backgroundColor : 'rgba(255, 165, 0, 0.65)'
},
'&.level6' :
{
height : '60%',
backgroundColor : 'rgba(255, 0, 0, 0.65)'
},
'&.level7' :
{
height : '70%',
backgroundColor : 'rgba(255, 0, 0, 0.65)'
},
'&.level8' :
{
height : '80%',
backgroundColor : 'rgba(0, 0, 0, 0.65)'
},
'&.level9' :
{
height : '90%',
backgroundColor : 'rgba(0, 0, 0, 0.65)'
},
'&.level10' :
{
height : '100%',
backgroundColor : 'rgba(0, 0, 0, 0.65)'
}
}
});
class PeerView extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
volume : 0, // Integer from 0 to 10.,
videoWidth : null,
videoHeight : null
};
// Latest received video track.
// @type {MediaStreamTrack}
this._audioTrack = null;
// Latest received video track.
// @type {MediaStreamTrack}
this._videoTrack = null;
// Periodic timer for showing video resolution.
this._videoResolutionTimer = null;
}
render()
{
const {
isMe,
peer,
volume,
advancedMode,
videoVisible,
videoProfile,
audioCodec,
videoCodec,
onChangeDisplayName,
classes
} = this.props;
const {
videoWidth,
videoHeight
} = this.state;
return (
<div className={classes.root}>
<div className={classes.info}>
{ advancedMode ?
<div className={classes.media}>
<div className={classes.box}>
{ audioCodec ?
<p>{audioCodec}</p>
:null
}
{ videoCodec ?
<p>{videoCodec} {videoProfile}</p>
:null
}
{ (videoVisible && videoWidth !== null) ?
<p>{videoWidth}x{videoHeight}</p>
:null
}
</div>
</div>
:null
}
<div className={classes.peer}>
{ isMe ?
<EditableInput
value={peer.displayName}
propName='displayName'
className={classnames(classes.displayNameEdit, 'display-name')}
classLoading='loading'
classInvalid='invalid'
shouldBlockWhileLoading
editProps={{
maxLength : 30,
autoCorrect : false,
spellCheck : false
}}
onChange={({ displayName }) => onChangeDisplayName(displayName)}
/>
:
<span className={classes.displayNameStatic}>
{peer.displayName}
</span>
}
{ advancedMode ?
<div className={classes.deviceInfo}>
<span>
{peer.device.name} {Math.floor(peer.device.version) || null}
</span>
</div>
:null
}
</div>
</div>
<video
ref='video'
className={classnames(classes.video, {
hidden : !videoVisible,
'is-me' : isMe,
loading : videoProfile === 'none'
})}
autoPlay
playsInline
muted={isMe}
/>
<div className={classes.volume}>
<div className={classnames(classes.volumeBar, `level${volume}`)} />
</div>
</div>
);
}
componentDidMount()
{
const { audioTrack, videoTrack } = this.props;
this._setTracks(audioTrack, videoTrack);
}
componentWillUnmount()
{
clearInterval(this._videoResolutionTimer);
}
componentWillReceiveProps(nextProps)
{
const { audioTrack, videoTrack } = nextProps;
this._setTracks(audioTrack, videoTrack);
}
_setTracks(audioTrack, videoTrack)
{
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
return;
this._audioTrack = audioTrack;
this._videoTrack = videoTrack;
clearInterval(this._videoResolutionTimer);
this._hideVideoResolution();
const { video } = this.refs;
if (audioTrack || videoTrack)
{
const stream = new MediaStream();
if (audioTrack)
stream.addTrack(audioTrack);
if (videoTrack)
stream.addTrack(videoTrack);
video.srcObject = stream;
if (videoTrack)
this._showVideoResolution();
}
else
{
video.srcObject = null;
}
}
_showVideoResolution()
{
this._videoResolutionTimer = setInterval(() =>
{
const { videoWidth, videoHeight } = this.state;
const { video } = this.refs;
// Don't re-render if nothing changed.
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
return;
this.setState(
{
videoWidth : video.videoWidth,
videoHeight : video.videoHeight
});
}, 1000);
}
_hideVideoResolution()
{
this.setState({ videoWidth: null, videoHeight: null });
}
}
PeerView.propTypes =
{
isMe : PropTypes.bool,
peer : PropTypes.oneOfType(
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
advancedMode : PropTypes.bool,
audioTrack : PropTypes.any,
volume : PropTypes.number,
videoTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired,
videoProfile : PropTypes.string,
audioCodec : PropTypes.string,
videoCodec : PropTypes.string,
onChangeDisplayName : PropTypes.func,
classes : PropTypes.object.isRequired
};
export default withStyles(styles)(PeerView);
@@ -0,0 +1,246 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
const styles = () =>
({
root:
{
position : 'relative',
flex : '100 100 auto',
height : '100%',
width : '100%',
display : 'flex',
flexDirection : 'column',
overflow : 'hidden',
backgroundColor : 'var(--peer-bg-color)',
backgroundImage : 'var(--peer-empty-avatar)',
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat'
},
video :
{
flex : '100 100 auto',
height : '100%',
width : '100%',
objectFit : 'cover',
userSelect : 'none',
transitionProperty : 'opacity',
transitionDuration : '.15s',
backgroundColor : 'var(--peer-video-bg-color)',
'&.is-me' :
{
transform : 'scaleX(-1)'
},
'&.hidden' :
{
opacity : 0,
transitionDuration : '0s'
},
'&.loading' :
{
filter : 'blur(5px)'
}
},
info :
{
position : 'absolute',
zIndex : 10,
top : '0.6vmin',
left : '0.6vmin',
bottom : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'space-between'
},
media :
{
flex : '0 0 auto',
display : 'flex',
flexDirection : 'row'
},
box :
{
padding : '0.4vmin',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.25)',
'& p' :
{
userSelect : 'none',
pointerEvents : 'none',
margin : 0,
color : 'rgba(255, 255, 255, 0.7)',
fontSize : 10,
'&:last-child' :
{
marginBottom : 0
}
}
}
});
class ScreenView extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
screenWidth : null,
screenHeight : null
};
// Latest received screen track.
// @type {MediaStreamTrack}
this._screenTrack = null;
// Periodic timer for showing video resolution.
this._screenResolutionTimer = null;
}
render()
{
const {
isMe,
advancedMode,
screenVisible,
screenProfile,
screenCodec,
classes
} = this.props;
const {
screenWidth,
screenHeight
} = this.state;
return (
<div className={classes.root}>
<div className={classes.info}>
{ advancedMode ?
<div className={classnames(classes.media, { 'is-me': isMe })}>
{ screenVisible ?
<div className={classes.box}>
{ screenCodec ?
<p>{screenCodec} {screenProfile}</p>
:null
}
{ (screenVisible && screenWidth !== null) ?
<p>{screenWidth}x{screenHeight}</p>
:null
}
</div>
:null
}
</div>
:null
}
</div>
<video
ref='video'
className={classnames(classes.video, {
hidden : !screenVisible,
'is-me' : isMe,
loading : screenProfile === 'none'
})}
autoPlay
playsInline
muted={Boolean(true)}
/>
</div>
);
}
componentDidMount()
{
const { screenTrack } = this.props;
this._setTracks(screenTrack);
}
componentWillUnmount()
{
clearInterval(this._screenResolutionTimer);
}
componentWillReceiveProps(nextProps)
{
const { screenTrack } = nextProps;
this._setTracks(screenTrack);
}
_setTracks(screenTrack)
{
if (this._screenTrack === screenTrack)
return;
this._screenTrack = screenTrack;
clearInterval(this._screenResolutionTimer);
this._hideScreenResolution();
const { video } = this.refs;
if (screenTrack)
{
const stream = new MediaStream();
if (screenTrack)
stream.addTrack(screenTrack);
video.srcObject = stream;
if (screenTrack)
this._showScreenResolution();
}
else
{
video.srcObject = null;
}
}
_showScreenResolution()
{
this._screenResolutionTimer = setInterval(() =>
{
const { screenWidth, screenHeight } = this.state;
const { video } = this.refs;
// Don't re-render if nothing changed.
if (video.videoWidth === screenWidth && video.videoHeight === screenHeight)
return;
this.setState(
{
screenWidth : video.videoWidth,
screenHeight : video.videoHeight
});
}, 1000);
}
_hideScreenResolution()
{
this.setState({ screenWidth: null, screenHeight: null });
}
}
ScreenView.propTypes =
{
isMe : PropTypes.bool,
advancedMode : PropTypes.bool,
screenTrack : PropTypes.any,
screenVisible : PropTypes.bool,
screenProfile : PropTypes.string,
screenCodec : PropTypes.string,
classes : PropTypes.object.isRequired
};
export default withStyles(styles)(ScreenView);
+359
View File
@@ -0,0 +1,359 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import FullScreen from '../FullScreen';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
const styles = () =>
({
root :
{
position : 'absolute',
top : 0,
left : 0,
height : '100%',
width : '100%',
zIndex : 20000
},
controls :
{
position : 'absolute',
zIndex : 20020,
right : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
padding : '0.4vmin'
},
button :
{
flex : '0 0 auto',
margin : '0.2vmin',
borderRadius : 2,
backgroundColor : 'rgba(255, 255, 255, 0.7)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : '5vmin',
height : '5vmin'
},
icon :
{
fontSize : '5vmin'
},
incompatibleVideo :
{
position : 'absolute',
zIndex : 20010,
top : 0,
bottom : 0,
left : 0,
right : 0,
display : 'flex',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
'& p' :
{
padding : '6px 12px',
borderRadius : 6,
userSelect : 'none',
pointerEvents : 'none',
fontSize : 15,
color : 'rgba(255, 255, 255, 0.55)'
}
}
});
class NewWindow extends React.PureComponent
{
static defaultProps =
{
url : '',
name : 'Multiparty Meeting',
title : 'Multiparty Meeting',
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()
{
const {
classes
} = this.props;
if (!this.state.mounted)
return null;
return ReactDOM.createPortal([
<div key='newwindow' className={classes.root}>
<div className={classes.controls}>
{this.fullscreen.fullscreenEnabled && (
<div
className={classes.button}
onClick={this.handleToggleFullscreen}
data-tip='Fullscreen'
data-place='right'
data-type='dark'
>
{ this.state.fullscreen ?
<FullScreenExitIcon className={classes.icon} />
:
<FullScreenIcon className={classes.icon} />
}
</div>
)}
</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 : window.screen.left;
const screenTop =
window.screenTop !== undefined ? window.screenTop : window.screen.top;
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: window.screen.width;
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: window.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 && 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,
classes : PropTypes.object.isRequired
};
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 withStyles(styles)(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 '../../actions/stateActions';
import FullView from '../VideoContainers/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;
+91
View File
@@ -0,0 +1,91 @@
import PropTypes from 'prop-types';
export const Room = PropTypes.shape(
{
url : PropTypes.string.isRequired,
state : PropTypes.oneOf(
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
activeSpeakerName : PropTypes.string
});
export const Device = PropTypes.shape(
{
flag : PropTypes.string.isRequired,
name : PropTypes.string.isRequired,
version : PropTypes.string
});
export const Me = PropTypes.shape(
{
name : PropTypes.string.isRequired,
displayName : PropTypes.string,
displayNameSet : PropTypes.bool.isRequired,
device : Device.isRequired,
canSendMic : PropTypes.bool.isRequired,
canSendWebcam : PropTypes.bool.isRequired,
webcamInProgress : PropTypes.bool.isRequired,
audioOnly : PropTypes.bool.isRequired,
audioOnlyInProgress : PropTypes.bool.isRequired,
restartIceInProgress : PropTypes.bool.isRequired
});
export const Producer = PropTypes.shape(
{
id : PropTypes.number.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
track : PropTypes.any,
codec : PropTypes.string.isRequired
});
export const Peer = PropTypes.shape(
{
name : PropTypes.string.isRequired,
displayName : PropTypes.string,
device : Device.isRequired,
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
});
export const Consumer = PropTypes.shape(
{
id : PropTypes.number.isRequired,
peerName : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
supported : PropTypes.bool.isRequired,
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
track : PropTypes.any,
codec : PropTypes.string
});
export const Notification = PropTypes.shape(
{
id : PropTypes.string.isRequired,
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
timeout : PropTypes.number
});
export const Message = PropTypes.shape(
{
type : PropTypes.string,
component : PropTypes.string,
text : PropTypes.string,
sender : PropTypes.string
});
export const FileEntryProps = PropTypes.shape(
{
data : PropTypes.shape({
name : PropTypes.string.isRequired,
picture : PropTypes.string,
file : PropTypes.shape({
magnet : PropTypes.string.isRequired
}).isRequired,
me : PropTypes.bool
}).isRequired,
notify : PropTypes.func.isRequired
});