Add the ability for a peer to have several video producers in a room.

auto_join_3.3
Håvar Aambø Fosstveit 2020-05-04 00:16:23 +02:00
parent a0cd4416d8
commit 08e2c425c6
27 changed files with 962 additions and 191 deletions

View File

@ -233,6 +233,9 @@ export default class RoomClient
// Local webcam mediasoup Producer.
this._webcamProducer = null;
// Extra videos being produced
this._extraVideoProducers = new Map();
// Map of webcam MediaDeviceInfos indexed by deviceId.
// @type {Map<String, MediaDeviceInfos>}
this._webcams = {};
@ -3005,6 +3008,159 @@ export default class RoomClient
}
}
async addExtraVideo(videoDeviceId)
{
logger.debug(
'addExtraVideo() [videoDeviceId:"%s"]',
videoDeviceId
);
store.dispatch(
roomActions.setExtraVideoOpen(false));
if (!this._mediasoupDevice.canProduce('video'))
{
logger.error('enableWebcam() | cannot produce video');
return;
}
let track;
store.dispatch(
meActions.setWebcamInProgress(true));
try
{
const device = this._webcams[videoDeviceId];
const resolution = store.getState().settings.resolution;
if (!device)
throw new Error('no webcam devices');
logger.debug(
'addExtraVideo() | new selected webcam [device:%o]',
device);
logger.debug('_setWebcamProducer() | calling getUserMedia()');
const stream = await navigator.mediaDevices.getUserMedia(
{
video :
{
deviceId : { ideal: videoDeviceId },
...VIDEO_CONSTRAINS[resolution]
}
});
track = stream.getVideoTracks()[0];
let producer;
if (this._useSimulcast)
{
// If VP9 is the only available video codec then use SVC.
const firstVideoCodec = this._mediasoupDevice
.rtpCapabilities
.codecs
.find((c) => c.kind === 'video');
let encodings;
if (firstVideoCodec.mimeType.toLowerCase() === 'video/vp9')
encodings = VIDEO_KSVC_ENCODINGS;
else if ('simulcastEncodings' in window.config)
encodings = window.config.simulcastEncodings;
else
encodings = VIDEO_SIMULCAST_ENCODINGS;
producer = await this._sendTransport.produce(
{
track,
encodings,
codecOptions :
{
videoGoogleStartBitrate : 1000
},
appData :
{
source : 'extravideo'
}
});
}
else
{
producer = await this._sendTransport.produce({
track,
appData :
{
source : 'extravideo'
}
});
}
this._extraVideoProducers.set(producer.id, producer);
store.dispatch(producerActions.addProducer(
{
id : producer.id,
deviceLabel : device.label,
source : 'extravideo',
paused : producer.paused,
track : producer.track,
rtpParameters : producer.rtpParameters,
codec : producer.rtpParameters.codecs[0].mimeType.split('/')[1]
}));
// store.dispatch(settingsActions.setSelectedWebcamDevice(deviceId));
await this._updateWebcams();
producer.on('transportclose', () =>
{
this._extraVideoProducers.delete(producer.id);
producer = null;
});
producer.on('trackended', () =>
{
store.dispatch(requestActions.notify(
{
type : 'error',
text : intl.formatMessage({
id : 'devices.cameraDisconnected',
defaultMessage : 'Camera disconnected'
})
}));
this.disableExtraVideo(producer.id)
.catch(() => {});
});
logger.debug('addExtraVideo() succeeded');
}
catch (error)
{
logger.error('addExtraVideo() failed:%o', error);
store.dispatch(requestActions.notify(
{
type : 'error',
text : intl.formatMessage({
id : 'devices.cameraError',
defaultMessage : 'An error occurred while accessing your camera'
})
}));
if (track)
track.stop();
}
store.dispatch(
meActions.setWebcamInProgress(false));
}
async enableMic()
{
if (this._micProducer)
@ -3505,6 +3661,37 @@ export default class RoomClient
meActions.setWebcamInProgress(false));
}
async disableExtraVideo(id)
{
logger.debug('disableExtraVideo()');
const producer = this._extraVideoProducers.get(id);
if (!producer)
return;
store.dispatch(meActions.setWebcamInProgress(true));
producer.close();
store.dispatch(
producerActions.removeProducer(id));
try
{
await this.sendRequest(
'closeProducer', { producerId: id });
}
catch (error)
{
logger.error('disableWebcam() [error:"%o"]', error);
}
this._extraVideoProducers.delete(id);
store.dispatch(meActions.setWebcamInProgress(false));
}
async disableWebcam()
{
logger.debug('disableWebcam()');

View File

@ -58,6 +58,12 @@ export const setSettingsOpen = (settingsOpen) =>
payload : { settingsOpen }
});
export const setExtraVideoOpen = (extraVideoOpen) =>
({
type : 'SET_EXTRA_VIDEO_OPEN',
payload : { extraVideoOpen }
});
export const setSettingsTab = (tab) =>
({
type : 'SET_SETTINGS_TAB',

View File

@ -150,6 +150,7 @@ const Me = (props) =>
micProducer,
webcamProducer,
screenProducer,
extraVideoProducers,
canShareScreen,
classes
} = props;
@ -467,6 +468,112 @@ const Me = (props) =>
</VideoView>
</div>
</div>
{ extraVideoProducers.map((producer) =>
{
return (
<div key={producer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={spacingStyle}
>
<div className={classes.viewContainer} style={style}>
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p className={hover ? 'hover' : null}>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<Tooltip title={webcamTip} placement='left'>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
roomClient.disableExtraVideo(producer.id);
}}
>
<VideoIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
isMe
advancedMode={advancedMode}
peer={me}
displayName={settings.displayName}
showPeerInfo
videoTrack={producer && producer.track}
videoVisible={videoVisible}
videoCodec={producer && producer.codec}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
/>
</div>
</div>
);
})}
{ screenProducer &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -545,20 +652,21 @@ const Me = (props) =>
Me.propTypes =
{
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object,
activeSpeaker : PropTypes.bool,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
spacing : PropTypes.number,
style : PropTypes.object,
smallButtons : PropTypes.bool,
canShareScreen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object,
activeSpeaker : PropTypes.bool,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer),
spacing : PropTypes.number,
style : PropTypes.object,
smallButtons : PropTypes.bool,
canShareScreen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>

View File

@ -125,6 +125,7 @@ const Peer = (props) =>
micConsumer,
webcamConsumer,
screenConsumer,
extraVideoConsumers,
toggleConsumerFullscreen,
toggleConsumerWindow,
spacing,
@ -351,6 +352,161 @@ const Peer = (props) =>
</div>
</div>
{ extraVideoConsumers.map((consumer) =>
{
return (
<div key={consumer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
{ browser.platform !== 'mobile' &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
showPeerInfo
consumerSpatialLayers={consumer ? consumer.spatialLayers : null}
consumerTemporalLayers={consumer ? consumer.temporalLayers : null}
consumerCurrentSpatialLayer={
consumer ? consumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
consumer ? consumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
consumer ? consumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
consumer ? consumer.preferredTemporalLayer : null
}
videoMultiLayer={consumer && consumer.type !== 'simple'}
videoTrack={consumer && consumer.track}
videoVisible={videoVisible}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/>
</div>
</div>
);
})}
{ screenConsumer &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -508,6 +664,7 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool,
browser : PropTypes.object.isRequired,

View File

@ -0,0 +1,167 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
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) =>
({
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 :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const ExtraVideo = ({
roomClient,
extraVideoOpen,
webcamDevices,
handleCloseExtraVideo,
classes
}) =>
{
const intl = useIntl();
const [ videoDevice, setVideoDevice ] = React.useState('');
const handleChange = (event) =>
{
setVideoDevice(event.target.value);
};
let videoDevices;
if (webcamDevices)
videoDevices = Object.values(webcamDevices);
else
videoDevices = [];
return (
<Dialog
open={extraVideoOpen}
onClose={() => handleCloseExtraVideo(false)}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.extraVideo'
defaultMessage='Extra video'
/>
</DialogTitle>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={videoDevice}
displayEmpty
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={videoDevices.length === 0}
onChange={handleChange}
>
{ videoDevices.map((webcam, index) =>
{
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ videoDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
</form>
<DialogActions>
<Button onClick={() => roomClient.addExtraVideo(videoDevice)} color='primary'>
<FormattedMessage
id='label.addVideo'
defaultMessage='Add video'
/>
</Button>
</DialogActions>
</Dialog>
);
};
ExtraVideo.propTypes =
{
roomClient : PropTypes.object.isRequired,
extraVideoOpen : PropTypes.bool.isRequired,
webcamDevices : PropTypes.object,
handleCloseExtraVideo : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
webcamDevices : state.me.webcamDevices,
extraVideoOpen : state.room.extraVideoOpen
});
const mapDispatchToProps = {
handleCloseExtraVideo : roomActions.setExtraVideoOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.webcamDevices === next.me.webcamDevices &&
prev.room.extraVideoOpen === next.room.extraVideoOpen
);
}
}
)(withStyles(styles)(ExtraVideo)));

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
@ -14,11 +14,14 @@ import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge';
import ExtensionIcon from '@material-ui/icons/Extension';
import AccountCircle from '@material-ui/icons/AccountCircle';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
@ -27,6 +30,7 @@ import SecurityIcon from '@material-ui/icons/Security';
import PeopleIcon from '@material-ui/icons/People';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
@ -89,6 +93,10 @@ const styles = (theme) =>
green :
{
color : 'rgba(0, 153, 0, 1)'
},
moreAction :
{
margin : theme.spacing(0, 0, 0, 1)
}
});
@ -127,6 +135,18 @@ const TopBar = (props) =>
{
const intl = useIntl();
const [ moreActionsElement, setMoreActionsElement ] = useState(null);
const handleMoreActionsOpen = (event) =>
{
setMoreActionsElement(event.currentTarget);
};
const handleMoreActionsClose = () =>
{
setMoreActionsElement(null);
};
const {
roomClient,
room,
@ -140,6 +160,7 @@ const TopBar = (props) =>
fullscreen,
onFullscreen,
setSettingsOpen,
setExtraVideoOpen,
setLockDialogOpen,
toggleToolArea,
openUsersTab,
@ -149,6 +170,8 @@ const TopBar = (props) =>
classes
} = props;
const isMoreActionsMenuOpen = Boolean(moreActionsElement);
const lockTooltip = room.locked ?
intl.formatMessage({
id : 'tooltip.unLockRoom',
@ -183,196 +206,230 @@ const TopBar = (props) =>
});
return (
<AppBar
position='fixed'
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
onClick={() => toggleToolArea()}
>
<IconButton
<React.Fragment>
<AppBar
position='fixed'
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
onClick={() => toggleToolArea()}
>
<IconButton
color='inherit'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography
className={classes.title}
variant='h6'
color='inherit'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
{ window.config.title ? window.config.title : 'Multiparty meeting' }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
noWrap
>
{ window.config.title ? window.config.title : 'Multiparty meeting' }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
aria-haspopup='true'
onClick={handleMoreActionsOpen}
color='inherit'
onClick={() => openUsersTab()}
>
<Badge
color='primary'
badgeContent={peersLength + 1}
>
<PeopleIcon />
</Badge>
<ExtensionIcon />
</IconButton>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
</Tooltip>
<Tooltip title={lockTooltip}>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
disabled={!canLock}
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</span>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<span className={classes.disabledButton}>
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
disabled={!canPromote}
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
onClick={onFullscreen}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</span>
</Tooltip>
}
{ loginEnabled &&
<Tooltip title={loginTooltip}>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
color='inherit'
onClick={() => openUsersTab()}
>
<Badge
color='primary'
badgeContent={peersLength + 1}
>
<PeopleIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
<SettingsIcon />
</IconButton>
</Tooltip>
}
<div className={classes.divider} />
<Button
<Tooltip title={lockTooltip}>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
disabled={!canLock}
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</span>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
className={classes.actionButton}
color='inherit'
disabled={!canPromote}
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</span>
</Tooltip>
}
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
</IconButton>
</Tooltip>
}
<div className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'label.leave',
defaultMessage : 'Leave'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
<FormattedMessage
id='label.leave'
defaultMessage='Leave'
/>
</Button>
</div>
</Toolbar>
</AppBar>
<Menu
anchorEl={moreActionsElement}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
open={isMoreActionsMenuOpen}
onClose={handleMoreActionsClose}
getContentAnchorEl={null}
>
<MenuItem
dense
onClick={() =>
{
handleMoreActionsClose();
setExtraVideoOpen(!room.extraVideoOpen);
}}
>
<VideoCallIcon
aria-label={intl.formatMessage({
id : 'label.leave',
defaultMessage : 'Leave'
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
<FormattedMessage
id='label.leave'
defaultMessage='Leave'
/>
</Button>
</div>
</Toolbar>
</AppBar>
/>
<p className={classes.moreAction}>Add video</p>
</MenuItem>
</Menu>
</React.Fragment>
);
};
@ -391,6 +448,7 @@ TopBar.propTypes =
onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
setExtraVideoOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired,
@ -430,6 +488,10 @@ const mapDispatchToProps = (dispatch) =>
{
dispatch(roomActions.setSettingsOpen(settingsOpen));
},
setExtraVideoOpen : (extraVideoOpen) =>
{
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
},
setLockDialogOpen : (lockDialogOpen) =>
{
dispatch(roomActions.setLockDialogOpen(lockDialogOpen));

View File

@ -24,6 +24,7 @@ import LockDialog from './AccessControl/LockDialog/LockDialog';
import Settings from './Settings/Settings';
import TopBar from './Controls/TopBar';
import WakeLock from 'react-wakelock-react16';
import ExtraVideo from './Controls/ExtraVideo';
const TIMEOUT = 5 * 1000;
@ -217,6 +218,10 @@ class Room extends React.PureComponent
{ room.settingsOpen &&
<Settings />
}
{ room.extraVideoOpen &&
<ExtraVideo />
}
</div>
);
}

View File

@ -37,6 +37,11 @@ export const screenProducersSelector = createSelector(
(producers) => Object.values(producers).filter((producer) => producer.source === 'screen')
);
export const extraVideoProducersSelector = createSelector(
producersSelect,
(producers) => Object.values(producers).filter((producer) => producer.source === 'extravideo')
);
export const micProducerSelector = createSelector(
producersSelect,
(producers) => Object.values(producers).find((producer) => producer.source === 'mic')
@ -67,6 +72,24 @@ export const screenConsumerSelector = createSelector(
(consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen')
);
export const spotlightScreenConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'screen' && spotlights.includes(consumer.peerId)
)
);
export const spotlightExtraVideoConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'extravideo' && spotlights.includes(consumer.peerId)
)
);
export const passiveMicConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
@ -114,21 +137,33 @@ export const raisedHandsSelector = createSelector(
export const videoBoxesSelector = createSelector(
spotlightsLengthSelector,
screenProducersSelector,
screenConsumerSelector,
(spotlightsLength, screenProducers, screenConsumers) =>
spotlightsLength + 1 + screenProducers.length + screenConsumers.length
spotlightScreenConsumerSelector,
extraVideoProducersSelector,
spotlightExtraVideoConsumerSelector,
(
spotlightsLength,
screenProducers,
screenConsumers,
extraVideoProducers,
extraVideoConsumers
) =>
spotlightsLength + 1 + screenProducers.length +
screenConsumers.length + extraVideoProducers.length +
extraVideoConsumers.length
);
export const meProducersSelector = createSelector(
micProducerSelector,
webcamProducerSelector,
screenProducerSelector,
(micProducer, webcamProducer, screenProducer) =>
extraVideoProducersSelector,
(micProducer, webcamProducer, screenProducer, extraVideoProducers) =>
{
return {
micProducer,
webcamProducer,
screenProducer
screenProducer,
extraVideoProducers
};
}
);
@ -151,8 +186,10 @@ export const makePeerConsumerSelector = () =>
consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen');
const extraVideoConsumers =
consumersArray.filter((consumer) => consumer.source === 'extravideo');
return { micConsumer, webcamConsumer, screenConsumer };
return { micConsumer, webcamConsumer, screenConsumer, extraVideoConsumers };
}
);
};

View File

@ -18,9 +18,9 @@ export const Me = PropTypes.shape(
export const Producer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
type : PropTypes.oneOf([ 'front', 'back', 'screen', 'extravideo' ]),
paused : PropTypes.bool.isRequired,
track : PropTypes.any,
codec : PropTypes.string.isRequired
@ -37,7 +37,7 @@ export const Consumer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
peerId : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),

View File

@ -20,6 +20,7 @@ const initialState =
selectedPeerId : null,
spotlights : [],
settingsOpen : false,
extraVideoOpen : false,
currentSettingsTab : 'media', // media, appearence, advanced
lockDialogOpen : false,
joined : false,
@ -114,6 +115,13 @@ const room = (state = initialState, action) =>
return { ...state, settingsOpen };
}
case 'SET_EXTRA_VIDEO_OPEN':
{
const { extraVideoOpen } = action.payload;
return { ...state, extraVideoOpen };
}
case 'SET_SETTINGS_TAB':
{
const { tab } = action.payload;

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "设置",
"settings.camera": "视频设备",

View File

@ -57,6 +57,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -103,6 +104,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Nastavení",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen",
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Einstellungen",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Indstillinger",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ρυθμίσεις",
"settings.camera": "Κάμερα",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": "Moderator actions",
"room.raisedHand": "{displayName} raised their hand",
"room.loweredHand": "{displayName} put their hand down",
"room.extraVideo": "Extra video",
"me.mutedPTT": "You are muted, hold down SPACE-BAR to talk",
@ -104,6 +105,7 @@
"label.media": "Media",
"label.appearence": "Appearence",
"label.advanced": "Advanced",
"label.addVideo": "Add video",
"settings.settings": "Settings",
"settings.camera": "Camera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ajustes",
"settings.camera": "Cámara",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Paramètres",
"settings.camera": "Caméra",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor",
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Postavke",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Beállítások",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -103,6 +104,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Impostazioni",
"settings.camera": "Videocamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": "Moderatorhandlinger",
"room.raisedHand": "{displayName} rakk opp hånden",
"room.loweredHand": "{displayName} tok ned hånden",
"room.extraVideo": "Ekstra video",
"me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke",
@ -104,6 +105,7 @@
"label.media": "Media",
"label.appearence": "Utseende",
"label.advanced": "Avansert",
"label.addVideo": "Legg til video",
"settings.settings": "Innstillinger",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ustawienia",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Definições",
"settings.camera": "Camera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Setări",
"settings.camera": "Cameră video",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ayarlar",
"settings.camera": "Kamera",

View File

@ -58,6 +58,7 @@
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
@ -104,6 +105,7 @@
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Налаштування",
"settings.camera": "Камера",