appIntMeet code

This commit is contained in:
astagor 2023-12-21 09:29:47 +01:00
parent e26c8a5158
commit 550a76bfb0
13 changed files with 444 additions and 72 deletions

View file

@ -43,7 +43,7 @@
"is-electron": "^2.2.0",
"marked": "^0.8.0",
"material-ui-popup-state": "^1.8.0",
"mediasoup-client": "^3.6.55",
"mediasoup-client": "^3.7.1",
"notistack": "^0.9.5",
"prop-types": "^15.7.2",
"random-string": "^0.2.0",
@ -51,6 +51,7 @@
"react-cookie-consent": "^2.5.0",
"react-dom": "^16.10.2",
"react-flip-toolkit": "^7.0.9",
"react-iframe": "1.8.5",
"react-image-file-resizer": "^0.3.8",
"react-images-upload": "^1.2.0",
"react-intl": "^3.4.0",
@ -66,7 +67,7 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"riek": "^1.1.0",
"socket.io-client": "^2.4.0",
"socket.io-client": "^4.5.4",
"source-map-explorer": "^2.1.0",
"streamsaver": "^2.0.5",
"typescript": "^4.2.4",

View file

@ -2110,6 +2110,24 @@ export default class RoomClient
peerActions.setStopPeerScreenSharingInProgress(peerId, false));
}
async toggleIframe(iframeUrl)
{
store.dispatch(
roomActions.setToggleIframeInProgress(true));
try
{
await this.sendRequest('toggleIframe', { iframeUrl });
}
catch (error)
{
logger.error('toggleIframe() [error:"%o"]', error);
}
store.dispatch(
roomActions.setToggleIframeInProgress(false));
}
async muteAllPeers()
{
logger.debug('muteAllPeers()');
@ -3194,6 +3212,24 @@ export default class RoomClient
break;
}
case 'showIframe':
{
const { iframeUrl } = notification.data;
store.dispatch(
roomActions.openIframe(iframeUrl));
break;
}
case 'closeIframe':
{
store.dispatch(
roomActions.closeIframe());
break;
}
case 'moderator:clearChat':
{
store.dispatch(chatActions.clearChat());
@ -3917,6 +3953,7 @@ export default class RoomClient
chatHistory,
fileHistory,
lastNHistory,
iframeHistory,
locked,
lobbyPeers,
accessCode
@ -3984,6 +4021,9 @@ export default class RoomClient
(fileHistory.length > 0) && store.dispatch(
fileActions.addFileHistory(fileHistory));
(iframeHistory) && store.dispatch(
roomActions.openIframe(iframeHistory));
locked ?
store.dispatch(roomActions.setRoomLocked()) :
store.dispatch(roomActions.setRoomUnLocked());

View file

@ -3,7 +3,8 @@ import { connect } from 'react-redux';
import {
meProducersSelector,
makePermissionSelector,
recordingConsentsPeersSelector
recordingConsentsPeersSelector,
showIframeSelect
} from '../../store/selectors';
import { permissions } from '../../permissions';
import { withRoomContext } from '../../RoomContext';
@ -60,6 +61,10 @@ const styles = (theme) =>
'&.screen' :
{
order : 2
},
'&.iframe' :
{
order : 3
}
},
viewContainer :
@ -156,6 +161,7 @@ const Me = (props) =>
roomClient,
me,
settings,
iframeUrl,
activeSpeaker,
spacing,
style,
@ -175,7 +181,7 @@ const Me = (props) =>
localRecordingState
} = props;
// const width = style.width;
const width = style.width;
const height = style.height;
@ -1018,6 +1024,19 @@ const Me = (props) =>
</div>
</div>
}
{iframeUrl &&
<div className={classnames(classes.root, 'iframe')} style={spacingStyle}>
<div className={classes.viewContainer} style={style}>
<VideoView
isMe
isIframe
iframeUrl={`${iframeUrl }`}
videoVisible
/>
</div>
</div>
}
</React.Fragment>
);
};
@ -1028,6 +1047,7 @@ Me.propTypes =
advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object,
iframeUrl : PropTypes.string.isRequired,
activeSpeaker : PropTypes.bool,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
@ -1073,6 +1093,7 @@ const makeMapStateToProps = () =>
me : state.me,
...meProducersSelector(state),
settings : state.settings,
iframeUrl : showIframeSelect(state),
activeSpeaker : state.me.id === state.room.activeSpeakerId,
hasAudioPermission : canShareAudio(state),
hasVideoPermission : canShareVideo(state),

View file

@ -303,24 +303,27 @@ const JoinDialog = ({
};
// TODO: prefix with the Edumeet server HTTP endpoint
fetch('/auth/check_login_status', {
credentials : 'include',
method : 'GET',
cache : 'no-cache',
redirect : 'follow',
referrerPolicy : 'no-referrer' })
.then((response) => response.json())
.then((json) =>
{
if (json.loggedIn)
if (config.loginEnabled)
{
fetch('/auth/check_login_status', {
credentials : 'include',
method : 'GET',
cache : 'no-cache',
redirect : 'follow',
referrerPolicy : 'no-referrer' })
.then((response) => response.json())
.then((json) =>
{
roomClient.setLoggedIn(json.loggedIn);
}
})
.catch((error) =>
{
logger.error('Error checking login status', error);
});
if (json.loggedIn)
{
roomClient.setLoggedIn(json.loggedIn);
}
})
.catch((error) =>
{
logger.error('Error checking login status', error);
});
}
return (
<div className={classes.root}>

View file

@ -1,19 +1,41 @@
import React from 'react';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import { useIntl } from 'react-intl';
import { useIntl, FormattedMessage } from 'react-intl';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import PanIcon from '@material-ui/icons/PanTool';
import Button from '@material-ui/core/Button';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import TextField from '@material-ui/core/TextField';
import { showIframeSelect, makePermissionSelector } from '../../../store/selectors';
import { permissions } from '../../../permissions';
import { config } from '../../../config';
const urlPattern = new RegExp(
'^(https?:\\/\\/)?' +
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
'((\\d{1,3}\\.){3}\\d{1,3}))' +
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
'(\\?[;&a-z\\d%_.~+=-]*)?' +
'(\\#[-a-z\\d_]*)?$',
'i'
);
const styles = (theme) =>
({
root :
{
display : 'flex',
flexDirection : 'column',
width : '100%',
overflowY : 'auto'
},
me :
{
width : '100%',
overflow : 'hidden',
@ -48,70 +70,163 @@ const styles = (theme) =>
const ListMe = (props) =>
{
const intl = useIntl();
const {
roomClient,
me,
iframeUrl,
toggleIframeInProgress,
hasScreenPermission,
settings,
classes
} = props;
const intl = useIntl();
const [ currentUrl, setCurrentUrl ] = useState('');
const validateUrl = () =>
{
if (currentUrl === '')
return false;
if (!urlPattern.test(currentUrl))
return false;
if (!currentUrl.toLowerCase().startsWith('https://'))
return false;
return true;
};
const isValidUrl = validateUrl();
const picture = me.picture || EmptyAvatar;
return (
<div className={classes.root}>
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}>
{settings.displayName}
</div>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
<div className={classes.me}>
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}>
{settings.displayName}
</div>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
className={
classnames(me.raisedHand ? classes.green : null, classes.buttons)
}
disabled={me.raisedHandInProgress}
color='primary'
onClick={(e) =>
{
e.stopPropagation();
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
className={
classnames(me.raisedHand ? classes.green : null, classes.buttons)
}
disabled={me.raisedHandInProgress}
color='primary'
onClick={(e) =>
{
e.stopPropagation();
roomClient.setRaisedHand(!me.raisedHand);
}}
>
<PanIcon />
</IconButton>
</Tooltip>
</div>
roomClient.setRaisedHand(!me.raisedHand);
{hasScreenPermission &&
<React.Fragment>
<TextField
id='displayname'
label={intl.formatMessage({
id : 'label.iframeAppUrl',
defaultMessage : 'External app URL to open for all (https only)'
})}
value={iframeUrl ?? currentUrl}
variant='outlined'
margin='normal'
disabled={iframeUrl}
onChange={(event) => setCurrentUrl(event.target.value.trim())}
fullWidth
/>
{iframeUrl &&
<Button
aria-label={intl.formatMessage({
id : 'room.hideIframe',
defaultMessage : 'Hide external app'
})}
className={classes.button}
variant='contained'
color='secondary'
disabled={toggleIframeInProgress}
onClick={() => roomClient.toggleIframe(null)}
>
<FormattedMessage
id='room.hideIframe'
defaultMessage='Hide external app'
/>
</Button>
}
{!iframeUrl &&
<Button
aria-label={intl.formatMessage({
id : 'room.showIframe',
defaultMessage : 'Show external app'
})}
className={classes.button}
variant='contained'
color='secondary'
disabled={toggleIframeInProgress || !isValidUrl}
onClick={() =>
{
roomClient.toggleIframe(currentUrl);
setCurrentUrl('');
}}
>
<PanIcon />
</IconButton>
</Tooltip>
<FormattedMessage
id='room.showIframe'
defaultMessage='Show external app'
/>
</Button>
}
</React.Fragment>
}
</div>
);
};
ListMe.propTypes =
{
roomClient : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
roomClient : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
iframeUrl : PropTypes.string,
toggleIframeInProgress : PropTypes.bool,
hasScreenPermission : PropTypes.bool.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
me : state.me,
settings : state.settings
});
const makeMapStateToProps = () =>
{
const canShareScreen =
makePermissionSelector(permissions.SHARE_SCREEN);
const mapStateToProps = (state) => ({
me : state.me,
iframeUrl : showIframeSelect(state),
toggleIframeInProgress : state.room.toggleIframeInProgress,
hasScreenPermission : canShareScreen(state),
settings : state.settings
});
return mapStateToProps;
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
@ -119,6 +234,8 @@ export default withRoomContext(connect(
{
return (
prev.me === next.me &&
prev.room.iframeUrl === next.room.iframeUrl &&
prev.room.toggleIframeInProgress === next.room.toggleIframeInProgress &&
prev.settings === next.settings
);
}

View file

@ -243,6 +243,7 @@ export default connect(
prev.room.spotlights === next.room.spotlights &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.room.hideSelfView === next.room.hideSelfView &&
prev.room.iframeUrl === next.room.iframeUrl &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.buttonControlBar === next.settings.buttonControlBar &&
prev.settings.aspectRatio === next.settings.aspectRatio &&

View file

@ -407,6 +407,7 @@ export default withRoomContext(connect(
prev.room.selectedPeers === next.room.selectedPeers &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.room.hideSelfView === next.room.hideSelfView &&
prev.room.iframeUrl === next.room.iframeUrl &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.aspectRatio === next.settings.aspectRatio &&

View file

@ -10,6 +10,10 @@ import SignalCellular0BarIcon from '@material-ui/icons/SignalCellular0Bar';
import SignalCellular1BarIcon from '@material-ui/icons/SignalCellular1Bar';
import SignalCellular2BarIcon from '@material-ui/icons/SignalCellular2Bar';
import SignalCellular3BarIcon from '@material-ui/icons/SignalCellular3Bar';
import Iframe from 'react-iframe';
import IconButton from '@material-ui/core/IconButton';
import FullscreenIcon from '@material-ui/icons/Fullscreen';
import ReplayIcon from '@material-ui/icons/Replay';
import { AudioAnalyzer } from './AudioAnalyzer';
const logger = new Logger('VideoView');
@ -172,6 +176,27 @@ const styles = (theme) =>
{
backgroundColor : 'rgb(174, 255, 0, 0.25)'
}
},
iframeContainer :
{
display : 'flex',
flexFlow : 'column',
height : '100%',
background : 'white'
},
iframeMenu :
{
background : 'white'
},
iframeMain :
{
flex : 1,
overflow : 'auto',
border : 0
},
iframeButtons :
{
padding : theme.spacing(1)
}
});
@ -206,6 +231,8 @@ class VideoView extends React.PureComponent
{
const {
isMe,
isIframe,
iframeUrl,
isMirrored,
isScreen,
isExtraVideo,
@ -305,6 +332,7 @@ class VideoView extends React.PureComponent
return (
<div className={classes.root}>
{ !isIframe &&
<div className={classes.info}>
<div className={classes.media}>
{(audioCodec || videoCodec) &&
@ -448,7 +476,8 @@ class VideoView extends React.PureComponent
</div>
}
</div>
}
{!isIframe &&
<video
ref='videoElement'
className={classnames(classes.video, {
@ -473,7 +502,60 @@ class VideoView extends React.PureComponent
muted
controls={false}
/>
}
{isIframe &&
<div className={classnames(classes.video, {
contain : videoContain
})}
>
<div className={classes.iframeContainer}>
<div className={classes.iframeMenu}>
<IconButton
className={classes.iframeButtons}
onClick={() =>
{
const elem = document.getElementById('iframe_iframe');
if (elem.requestFullscreen)
{
elem.requestFullscreen();
}
else if (elem.mozRequestFullScreen)
{ /* Firefox */
elem.mozRequestFullScreen();
}
else if (elem.webkitRequestFullscreen)
{ /* Chrome, Safari and Opera */
elem.webkitRequestFullscreen();
}
else if (elem.msRequestFullscreen)
{ /* IE/Edge */
elem.msRequestFullscreen();
}
}}
>
<FullscreenIcon />
</IconButton>
<IconButton
className={classes.iframeButtons}
onClick={() =>
{
const elem = document.getElementById('iframe_iframe');
elem.src = iframeUrl;
}}
>
<ReplayIcon />
</IconButton>
</div>
<Iframe
url={iframeUrl}
id='iframe_iframe'
className={classes.iframeMain}
/>
</div>
</div>
}
{children}
</div>
);
@ -481,6 +563,8 @@ class VideoView extends React.PureComponent
componentDidMount()
{
if (this.props.isIframe) return;
const { videoTrack, audioTrack, showAudioAnalyzer } = this.props;
this._setTracks(videoTrack);
@ -499,6 +583,8 @@ class VideoView extends React.PureComponent
componentWillUnmount()
{
if (this.props.isIframe) return;
clearInterval(this._videoResolutionTimer);
const { videoElement } = this.refs;
@ -519,6 +605,8 @@ class VideoView extends React.PureComponent
componentDidUpdate(prevProps)
{
if (this.props.isIframe) return;
if (prevProps !== this.props)
{
const { videoTrack, audioTrack, showAudioAnalyzer } = this.props;
@ -636,6 +724,8 @@ VideoView.propTypes =
isMe : PropTypes.bool,
isMirrored : PropTypes.bool,
isScreen : PropTypes.bool,
isIframe : PropTypes.bool,
iframeUrl : PropTypes.string,
isExtraVideo : PropTypes.bool,
showQuality : PropTypes.bool,
showAudioAnalyzer : PropTypes.bool,

View file

@ -142,6 +142,23 @@ export const addSelectedPeer = (peerId) =>
payload : { peerId }
});
export const openIframe = (iframeUrl) =>
({
type : 'OPEN_IFRAME',
payload : { iframeUrl }
});
export const setToggleIframeInProgress = (flag) =>
({
type : 'SET_TOGGLE_IFRAME_IN_PROGRESS',
payload : { flag }
});
export const closeIframe = () =>
({
type : 'CLOSE_IFRAME'
});
export const removeSelectedPeer = (peerId) =>
({
type : 'REMOVE_SELECTED_PEER',

View file

@ -26,6 +26,8 @@ const initialState =
settingsOpen : false,
extraVideoOpen : false,
hideSelfView : false,
iframeUrl : null,
toggleIframeInProgress : false,
rolesManagerOpen : false,
helpOpen : false,
aboutOpen : false,
@ -309,6 +311,23 @@ const room = (state = initialState, action) =>
return { ...state, hideSelfView };
}
case 'OPEN_IFRAME':
{
const { iframeUrl } = action.payload;
return { ...state, iframeUrl };
}
case 'CLOSE_IFRAME':
{
const iframeUrl = null;
return { ...state, iframeUrl };
}
case 'SET_TOGGLE_IFRAME_IN_PROGRESS':
return { ...state, toggleIframeInProgress: action.payload.flag };
default:
return state;
}

View file

@ -20,6 +20,8 @@ const peersKeySelector = createSelector(
(peers) => Object.keys(peers)
);
export const showIframeSelect = (state) => state.room.iframeUrl;
export const peersValueSelector = createSelector(
peersSelector,
(peers) => Object.values(peers)
@ -186,6 +188,7 @@ export const raisedHandsSelector = createSelector(
export const videoBoxesSelector = createSelector(
isHiddenSelect,
showIframeSelect,
spotlightsLengthSelector,
screenProducersSelector,
spotlightScreenConsumerSelector,
@ -193,6 +196,7 @@ export const videoBoxesSelector = createSelector(
spotlightExtraVideoConsumerSelector,
(
isHidden,
iframeUrl,
spotlightsLength,
screenProducers,
screenConsumers,
@ -202,7 +206,8 @@ export const videoBoxesSelector = createSelector(
{
return spotlightsLength + (isHidden ? 0 : 1) +
(isHidden ? 0 : screenProducers.length) + screenConsumers.length +
(isHidden ? 0 : extraVideoProducers.length) + extraVideoConsumers.length;
(isHidden ? 0 : extraVideoProducers.length) + extraVideoConsumers.length +
(isHidden ? 0 : (iframeUrl ? 1 : 0));
}
);

View file

@ -262,6 +262,8 @@ class Room extends EventEmitter
this._fileHistory = [];
this._iframeHistory = null;
this._lastN = [];
this._peers = {};
@ -310,6 +312,8 @@ class Room extends EventEmitter
this._fileHistory = null;
this._iframeHistory = null;
this._lobby.close();
this._lobby = null;
@ -923,6 +927,7 @@ class Room extends EventEmitter
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
iframeHistory : this._iframeHistory,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
@ -1770,6 +1775,57 @@ class Room extends EventEmitter
break;
}
case 'toggleIframe':
{
if (!peer.joined)
throw new Error('Peer not yet joined');
if (!this._hasPermission(peer, SHARE_SCREEN))
throw new Error('Peer not authorized to toggle external app');
const { iframeUrl } = request.data;
if (iframeUrl && this._iframeHistory)
throw new Error('External app already opened');
if (!iframeUrl)
{
this._iframeHistory = null;
// Spread to others and self
this._notification(peer.socket, 'closeIframe', null, true, true);
}
else
{
const pattern = new RegExp(
'^(https?:\\/\\/)?' +
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
'((\\d{1,3}\\.){3}\\d{1,3}))' +
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
'(\\?[;&a-z\\d%_.~+=-]*)?' +
'(\\#[-a-z\\d_]*)?$',
'i'
);
if (!pattern.test(iframeUrl))
throw new Error('Not a valid url for external app');
if (!iframeUrl.toLowerCase().startsWith('https://'))
throw new Error('Only https allowed for external apps');
this._iframeHistory = iframeUrl;
// Spread to others and self
this._notification(peer.socket, 'showIframe',
{ iframeUrl }, true, true);
}
// Return no error
cb();
break;
}
case 'moderator:stopAllScreenSharing':
{
if (!this._hasPermission(peer, MODERATE_ROOM))

View file

@ -27,24 +27,25 @@
"axios": "^0.21.1",
"base-64": "^0.1.0",
"bcrypt": "^5.0.1",
"body-parser": "^1.19.0",
"body-parser": "^1.20.1",
"colors": "^1.4.0",
"compression": "^1.7.4",
"connect-redis": "^4.0.3",
"convict": "^6.1.0",
"convict": "^6.2.3",
"convict-format-with-validator": "^6.0.1",
"cookie-parser": "^1.4.4",
"debug": "^4.3.1",
"express": "^4.17.1",
"express-session": "^1.17.0",
"express": "^4.18.2",
"express-session": "^1.17.3",
"express-socket.io-session": "^1.3.5",
"fast-stats": "^0.0.6",
"helmet": "^3.21.2",
"ims-lti": "^3.0.2",
"json5": "^2.2.0",
"jsonwebtoken": "^8.5.1",
"mediasoup": "3.10.5",
"openid-client": "^3.7.3",
"json5": "^2.2.2",
"jsonwebtoken": "^9.0.0",
"mediasoup": "3.12.12",
"openid-client": "^5.3.1",
"os-utils": "^0.0.14",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"passport-lti": "0.0.7",
@ -52,7 +53,7 @@
"pidusage": "^2.0.21",
"prom-client": "^13.1.0",
"redis": "v3",
"socket.io": "^2.4.0",
"socket.io": "^4.5.4",
"spdy": "^4.0.2",
"toml": "^3.0.0",
"uuid": "^7.0.2",
@ -75,7 +76,7 @@
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "6.8.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-import": "^2.26.0",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
}