Added filmstrip and settings back. Still some issues with layout if a peer shares screen in filmstrip.
parent
0449d6ff20
commit
6a9ac30a8b
|
|
@ -1,5 +1,6 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
/app/build/
|
||||||
/app/public/config.js
|
/app/public/config.js
|
||||||
/server/config/
|
/server/config/
|
||||||
!/server/config/config.example.js
|
!/server/config/config.example.js
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const styles = (theme) =>
|
||||||
},
|
},
|
||||||
fab :
|
fab :
|
||||||
{
|
{
|
||||||
margin: theme.spacing.unit
|
margin : theme.spacing.unit
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { withRoomContext } from '../../RoomContext';
|
||||||
|
import Peer from '../Containers/Peer';
|
||||||
|
import HiddenPeers from '../Containers/HiddenPeers';
|
||||||
|
|
||||||
|
const styles = (theme) =>
|
||||||
|
({
|
||||||
|
root :
|
||||||
|
{
|
||||||
|
display : 'flex',
|
||||||
|
flexDirection : 'column',
|
||||||
|
alignItems : 'center',
|
||||||
|
height : '100%',
|
||||||
|
width : '100%'
|
||||||
|
},
|
||||||
|
activePeerContainer :
|
||||||
|
{
|
||||||
|
width : '100%',
|
||||||
|
height : '80vh',
|
||||||
|
display : 'flex',
|
||||||
|
justifyContent : 'center',
|
||||||
|
alignItems : 'center'
|
||||||
|
},
|
||||||
|
activePeer :
|
||||||
|
{
|
||||||
|
width : '100%',
|
||||||
|
border : '5px solid rgba(255, 255, 255, 0.15)',
|
||||||
|
boxShadow : '0px 5px 12px 2px rgba(17, 17, 17, 0.5)',
|
||||||
|
marginTop : 60
|
||||||
|
},
|
||||||
|
filmStrip :
|
||||||
|
{
|
||||||
|
display : 'flex',
|
||||||
|
background : 'rgba(0, 0, 0 , 0.5)',
|
||||||
|
width : '100%',
|
||||||
|
overflowX : 'auto',
|
||||||
|
height : '20vh',
|
||||||
|
alignItems : 'center'
|
||||||
|
},
|
||||||
|
filmStripContent :
|
||||||
|
{
|
||||||
|
margin : '0 auto',
|
||||||
|
display : 'flex',
|
||||||
|
height : '100%',
|
||||||
|
alignItems : 'center'
|
||||||
|
},
|
||||||
|
film :
|
||||||
|
{
|
||||||
|
height : '18vh',
|
||||||
|
flexShrink : 0,
|
||||||
|
paddingLeft : '1vh',
|
||||||
|
'& .active' :
|
||||||
|
{
|
||||||
|
borderColor : 'var(--active-speaker-border-color)'
|
||||||
|
},
|
||||||
|
'&.selected' :
|
||||||
|
{
|
||||||
|
borderColor : 'var(--selected-peer-border-color)'
|
||||||
|
},
|
||||||
|
'&:last-child' :
|
||||||
|
{
|
||||||
|
paddingRight : '1vh'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filmContent :
|
||||||
|
{
|
||||||
|
height : '100%',
|
||||||
|
width : '100%',
|
||||||
|
border : '1px solid rgba(255,255,255,0.15)',
|
||||||
|
maxWidth : 'calc(18vh * (4 / 3))',
|
||||||
|
cursor : 'pointer',
|
||||||
|
'& .screen' :
|
||||||
|
{
|
||||||
|
maxWidth : 'calc(18vh * (2 * 4 / 3))',
|
||||||
|
border : 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hiddenPeers :
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class Filmstrip extends Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.activePeerContainer = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
lastSpeaker : null,
|
||||||
|
width : 400
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the name of the peer which is currently speaking. This is either
|
||||||
|
// the latest active speaker, or the manually selected peer, or, if no
|
||||||
|
// person has spoken yet, the first peer in the list of peers.
|
||||||
|
getActivePeerName = () =>
|
||||||
|
{
|
||||||
|
if (this.props.selectedPeerName)
|
||||||
|
{
|
||||||
|
return this.props.selectedPeerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.lastSpeaker)
|
||||||
|
{
|
||||||
|
return this.state.lastSpeaker;
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerNames = Object.keys(this.props.peers);
|
||||||
|
|
||||||
|
if (peerNames.length > 0)
|
||||||
|
{
|
||||||
|
return peerNames[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isSharingCamera = (peerName) => this.props.peers[peerName] &&
|
||||||
|
this.props.peers[peerName].consumers.some((consumer) =>
|
||||||
|
this.props.consumers[consumer].source === 'screen');
|
||||||
|
|
||||||
|
getRatio = () =>
|
||||||
|
{
|
||||||
|
let ratio = 4 / 3;
|
||||||
|
|
||||||
|
if (this.isSharingCamera(this.getActivePeerName()))
|
||||||
|
{
|
||||||
|
ratio *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratio;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDimensions = debounce(() =>
|
||||||
|
{
|
||||||
|
const container = this.activePeerContainer.current;
|
||||||
|
|
||||||
|
if (container)
|
||||||
|
{
|
||||||
|
const ratio = this.getRatio();
|
||||||
|
|
||||||
|
let width = container.clientWidth;
|
||||||
|
|
||||||
|
if (width / ratio > (container.clientHeight - 100))
|
||||||
|
{
|
||||||
|
width = (container.clientHeight - 100) * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
window.addEventListener('resize', this.updateDimensions);
|
||||||
|
const observer = new ResizeObserver(this.updateDimensions);
|
||||||
|
|
||||||
|
observer.observe(this.activePeerContainer.current);
|
||||||
|
this.updateDimensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount()
|
||||||
|
{
|
||||||
|
window.removeEventListener('resize', this.updateDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps)
|
||||||
|
{
|
||||||
|
if (prevProps !== this.props)
|
||||||
|
{
|
||||||
|
this.updateDimensions();
|
||||||
|
|
||||||
|
if (this.props.activeSpeakerName !== this.props.myName)
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
|
this.setState({
|
||||||
|
lastSpeaker : this.props.activeSpeakerName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
roomClient,
|
||||||
|
peers,
|
||||||
|
advancedMode,
|
||||||
|
spotlights,
|
||||||
|
spotlightsLength,
|
||||||
|
classes
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const activePeerName = this.getActivePeerName();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<div className={classes.activePeerContainer} ref={this.activePeerContainer}>
|
||||||
|
{ peers[activePeerName] ?
|
||||||
|
<div
|
||||||
|
className={classes.activePeer}
|
||||||
|
style={{
|
||||||
|
width : this.state.width,
|
||||||
|
height : this.state.width / this.getRatio()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Peer
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
name={activePeerName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.filmStrip}>
|
||||||
|
<div className={classes.filmStripContent}>
|
||||||
|
{ Object.keys(peers).map((peerName) =>
|
||||||
|
{
|
||||||
|
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={peerName}
|
||||||
|
onClick={() => roomClient.setSelectedPeer(peerName)}
|
||||||
|
className={classnames(classes.film, {
|
||||||
|
selected : this.props.selectedPeerName === peerName,
|
||||||
|
active : this.state.lastSpeaker === peerName
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={classes.filmContent}>
|
||||||
|
<Peer
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
name={peerName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ('');
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.hiddenPeers}>
|
||||||
|
{ spotlightsLength<Object.keys(peers).length ?
|
||||||
|
<HiddenPeers
|
||||||
|
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||||
|
/>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Filmstrip.propTypes = {
|
||||||
|
roomClient : PropTypes.any.isRequired,
|
||||||
|
activeSpeakerName : PropTypes.string,
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
peers : PropTypes.object.isRequired,
|
||||||
|
consumers : PropTypes.object.isRequired,
|
||||||
|
myName : PropTypes.string.isRequired,
|
||||||
|
selectedPeerName : PropTypes.string,
|
||||||
|
spotlightsLength : PropTypes.number,
|
||||||
|
spotlights : PropTypes.array.isRequired,
|
||||||
|
classes : PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeSpeakerName : state.room.activeSpeakerName,
|
||||||
|
selectedPeerName : state.room.selectedPeerName,
|
||||||
|
peers : state.peers,
|
||||||
|
consumers : state.consumers,
|
||||||
|
myName : state.me.name,
|
||||||
|
spotlights : state.room.spotlights,
|
||||||
|
spotlightsLength
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRoomContext(connect(
|
||||||
|
mapStateToProps,
|
||||||
|
undefined
|
||||||
|
)(withStyles(styles)(Filmstrip)));
|
||||||
|
|
@ -25,6 +25,7 @@ import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||||
import Notifications from './Notifications/Notifications';
|
import Notifications from './Notifications/Notifications';
|
||||||
import MeetingDrawer from './MeetingDrawer/MeetingDrawer';
|
import MeetingDrawer from './MeetingDrawer/MeetingDrawer';
|
||||||
import Democratic from './MeetingViews/Democratic';
|
import Democratic from './MeetingViews/Democratic';
|
||||||
|
import Filmstrip from './MeetingViews/Filmstrip';
|
||||||
import Me from './Containers/Me';
|
import Me from './Containers/Me';
|
||||||
import AudioPeers from './PeerAudio/AudioPeers';
|
import AudioPeers from './PeerAudio/AudioPeers';
|
||||||
import FullScreenView from './VideoContainers/FullScreenView';
|
import FullScreenView from './VideoContainers/FullScreenView';
|
||||||
|
|
@ -123,9 +124,10 @@ class Room extends Component
|
||||||
|
|
||||||
this.fullscreen = new FullScreen(document);
|
this.fullscreen = new FullScreen(document);
|
||||||
|
|
||||||
this.state = {
|
this.state =
|
||||||
drawerOpen : false,
|
{
|
||||||
fullscreen : false
|
drawerOpen : false,
|
||||||
|
fullscreen : false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,12 +204,20 @@ class Room extends Component
|
||||||
theme
|
theme
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const View =
|
||||||
|
{
|
||||||
|
filmstrip : Filmstrip,
|
||||||
|
democratic : Democratic
|
||||||
|
}[room.mode];
|
||||||
|
|
||||||
if (room.audioSuspended)
|
if (room.audioSuspended)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<Paper className={classes.message}>
|
<Paper className={classes.message}>
|
||||||
<Typography>This webpage required sound and video to play, please click to allow.</Typography>
|
<Typography>
|
||||||
|
This webpage required sound and video to play, please click to allow.
|
||||||
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -302,7 +312,9 @@ class Room extends Component
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Account'
|
aria-label='Account'
|
||||||
color='inherit'
|
color='inherit'
|
||||||
onClick={() => me.loggedIn ? roomClient.logout() : roomClient.login() }
|
onClick={() => {
|
||||||
|
me.loggedIn ? roomClient.logout() : roomClient.login();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AccountCircle />
|
<AccountCircle />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@ -327,7 +339,9 @@ class Room extends Component
|
||||||
</SwipeableDrawer>
|
</SwipeableDrawer>
|
||||||
</Hidden>
|
</Hidden>
|
||||||
</nav>
|
</nav>
|
||||||
<Democratic advancedMode={room.advancedMode} />
|
|
||||||
|
<View advancedMode={room.advancedMode} />
|
||||||
|
|
||||||
<Draggable handle='.me-handle' bounds='body' cancel='.display-name'>
|
<Draggable handle='.me-handle' bounds='body' cancel='.display-name'>
|
||||||
<div
|
<div
|
||||||
className={classnames(classes.meContainer, 'me-handle', {
|
className={classnames(classes.meContainer, 'me-handle', {
|
||||||
|
|
|
||||||
|
|
@ -12,24 +12,53 @@ import Button from '@material-ui/core/Button';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||||
import FormControl from '@material-ui/core/FormControl';
|
import FormControl from '@material-ui/core/FormControl';
|
||||||
|
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||||
import Select from '@material-ui/core/Select';
|
import Select from '@material-ui/core/Select';
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
|
|
||||||
const styles = (theme) =>
|
const styles = (theme) =>
|
||||||
({
|
({
|
||||||
root :
|
root :
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
device :
|
dialogPaper :
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setting :
|
||||||
{
|
{
|
||||||
width : '20vw',
|
|
||||||
padding : theme.spacing.unit * 2
|
padding : theme.spacing.unit * 2
|
||||||
},
|
},
|
||||||
formControl :
|
formControl :
|
||||||
{
|
{
|
||||||
display : 'flex',
|
display : 'flex'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modes = [ {
|
||||||
|
value : 'democratic',
|
||||||
|
label : 'Democratic view'
|
||||||
|
}, {
|
||||||
|
value : 'filmstrip',
|
||||||
|
label : 'Filmstrip view'
|
||||||
|
} ];
|
||||||
|
|
||||||
const Settings = ({
|
const Settings = ({
|
||||||
roomClient,
|
roomClient,
|
||||||
room,
|
room,
|
||||||
|
|
@ -59,13 +88,20 @@ const Settings = ({
|
||||||
className={classes.root}
|
className={classes.root}
|
||||||
open={room.settingsOpen}
|
open={room.settingsOpen}
|
||||||
onClose={() => handleCloseSettings({ settingsOpen: false })}
|
onClose={() => handleCloseSettings({ settingsOpen: false })}
|
||||||
|
classes={{
|
||||||
|
paper : classes.dialogPaper
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle id="form-dialog-title">Settings</DialogTitle>
|
<DialogTitle id='form-dialog-title'>Settings</DialogTitle>
|
||||||
<form className={classes.device} autoComplete='off'>
|
<form className={classes.setting} autoComplete='off'>
|
||||||
<FormControl className={classes.formControl}>
|
<FormControl className={classes.formControl}>
|
||||||
<Select
|
<Select
|
||||||
value={me.selectedWebcam || ''}
|
value={me.selectedWebcam || ''}
|
||||||
onChange={(event) => event.target.value ? roomClient.changeWebcam(event.target.value) : null }
|
onChange={(event) =>
|
||||||
|
{
|
||||||
|
if (event.target.value)
|
||||||
|
roomClient.changeWebcam(event.target.value);
|
||||||
|
}}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
name='Camera'
|
name='Camera'
|
||||||
autoWidth
|
autoWidth
|
||||||
|
|
@ -85,11 +121,15 @@ const Settings = ({
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</form>
|
</form>
|
||||||
<form className={classes.device} autoComplete='off'>
|
<form className={classes.setting} autoComplete='off'>
|
||||||
<FormControl className={classes.formControl}>
|
<FormControl className={classes.formControl}>
|
||||||
<Select
|
<Select
|
||||||
value={me.selectedAudioDevice || ''}
|
value={me.selectedAudioDevice || ''}
|
||||||
onChange={(event) => event.target.value ? roomClient.changeAudioDevice(event.target.value) : null }
|
onChange={(event) =>
|
||||||
|
{
|
||||||
|
if (event.target.value)
|
||||||
|
roomClient.changeAudioDevice(event.target.value);
|
||||||
|
}}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
name='Audio device'
|
name='Audio device'
|
||||||
autoWidth
|
autoWidth
|
||||||
|
|
@ -113,11 +153,37 @@ const Settings = ({
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</form>
|
</form>
|
||||||
|
<FormControlLabel
|
||||||
|
className={classes.setting}
|
||||||
|
control={<Checkbox checked={room.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
|
||||||
|
label='Advanced mode'
|
||||||
|
/>
|
||||||
|
<form className={classes.setting} autoComplete='off'>
|
||||||
|
<FormControl className={classes.formControl}>
|
||||||
|
<Select
|
||||||
|
value={room.mode || ''}
|
||||||
|
onChange={(event) => handleChangeMode(event.target.value)}
|
||||||
|
name='Room mode'
|
||||||
|
autoWidth
|
||||||
|
className={classes.selectEmpty}
|
||||||
|
>
|
||||||
|
{ modes.map((mode, index) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} value={mode.value}>{mode.label}</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
<FormHelperText>
|
||||||
|
Select room layout
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</form>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
|
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -129,6 +195,7 @@ Settings.propTypes =
|
||||||
room : appPropTypes.Room.isRequired,
|
room : appPropTypes.Room.isRequired,
|
||||||
onToggleAdvancedMode : PropTypes.func.isRequired,
|
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||||
handleChangeMode : PropTypes.func.isRequired,
|
handleChangeMode : PropTypes.func.isRequired,
|
||||||
|
handleCloseSettings : PropTypes.func.isRequired,
|
||||||
classes : PropTypes.object.isRequired
|
classes : PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue