Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(premeeting): pre-call connection test #15151

Merged
merged 4 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,11 @@ var config = {
// hideDisplayName: false,
// // List of buttons to hide from the extra join options dropdown.
// hideExtraJoinButtons: ['no-audio', 'by-phone'],
// // Configuration for pre-call test
// // By setting preCallTestEnabled, you enable the pre-call test in the prejoin page.
// // ICE server credentials need to be provided over the preCallTestICEUrl
// preCallTestEnabled: false,
// preCallTestICEUrl: ''
// },

// When 'true', the user cannot edit the display name.
Expand Down
5 changes: 4 additions & 1 deletion lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -922,9 +922,11 @@
"configuringDevices": "Configuring devices...",
"connectedWithAudioQ": "You’re connected with audio?",
"connection": {
"failed": "Connection test failed!",
"good": "Your internet connection looks good!",
"nonOptimal": "Your internet connection is not optimal",
"poor": "You have a poor internet connection"
"poor": "You have a poor internet connection",
"running": "Running connection test..."
},
"connectionDetails": {
"audioClipping": "We expect your audio to be clipped.",
Expand All @@ -933,6 +935,7 @@
"goodQuality": "Awesome! Your media quality is going to be great.",
"noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
"noVideo": "We expect that your video will be terrible.",
"testFailed": "The connection test encountered unexpected issues, but this might not impact your experience.",
"undetectable": "If you still can not make calls in browser, we recommend that you make sure your speakers, microphone and camera are properly set up, that you have granted your browser rights to use your microphone and camera, and that your browser version is up-to-date. If you still have trouble calling, you should contact the web application developer.",
"veryPoorConnection": "We expect your call quality to be really terrible.",
"videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
Expand Down
2 changes: 2 additions & 0 deletions react/features/base/config/configType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ export interface IConfig {
enabled?: boolean;
hideDisplayName?: boolean;
hideExtraJoinButtons?: Array<string>;
preCallTestEnabled?: boolean;
preCallTestICEUrl?: string;
};
prejoinPageEnabled?: boolean;
raisedHands?: {
Expand Down
8 changes: 7 additions & 1 deletion react/features/base/premeeting/actionTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@

/**
* Action type to set the precall test data.
*/
export const SET_PRECALL_TEST_RESULTS = 'SET_PRECALL_TEST_RESULTS';

/**
* Type for setting the user's consent for unsafe room joining.
*
Expand All @@ -6,4 +12,4 @@
* consent: boolean
* }
*/
export const SET_UNSAFE_ROOM_CONSENT = 'SET_UNSAFE_ROOM_CONSENT'
export const SET_UNSAFE_ROOM_CONSENT = 'SET_UNSAFE_ROOM_CONSENT'
52 changes: 51 additions & 1 deletion react/features/base/premeeting/actions.web.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
import { IStore } from '../../app/types';
import JitsiMeetJS from '../lib-jitsi-meet';

import { SET_PRECALL_TEST_RESULTS, SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
import { getPreCallICEUrl } from './functions';
import logger from './logger';
import { IPreCallResult, IPreCallTestState, PreCallTestStatus } from './types';

/**
* Sets the consent of the user for joining the unsafe room.
Expand All @@ -15,3 +21,47 @@ export function setUnsafeRoomConsent(consent: boolean) {
consent
};
}

/**
* Initializes the 'precallTest' and executes one test, storing the results.
*
* @returns {Function}
*/
export function runPreCallTest() {
return async function(dispatch: Function, getState: IStore['getState']) {
try {

dispatch(setPreCallTestResults({ status: PreCallTestStatus.RUNNING }));

const turnCredentialsUrl = getPreCallICEUrl(getState());

saghul marked this conversation as resolved.
Show resolved Hide resolved
if (!turnCredentialsUrl) {
throw new Error('No TURN credentials URL provided in config');
}

const turnCredentials = await fetch(turnCredentialsUrl);
const { iceServers } = await turnCredentials.json();
saghul marked this conversation as resolved.
Show resolved Hide resolved
const result: IPreCallResult = await JitsiMeetJS.runPreCallTest(iceServers);

dispatch(setPreCallTestResults({ status: PreCallTestStatus.FINISHED,
result }));
} catch (error) {
logger.error('Failed to run pre-call test', error);

dispatch(setPreCallTestResults({ status: PreCallTestStatus.FAILED }));
}
};
}

/**
* Action used to set data from precall test.
*
* @param {IPreCallTestState} value - The precall test results.
* @returns {Object}
*/
export function setPreCallTestResults(value: IPreCallTestState) {
return {
type: SET_PRECALL_TEST_RESULTS,
value
};
}
83 changes: 46 additions & 37 deletions react/features/base/premeeting/components/web/ConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import React, { useCallback, useState } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';

import { translate } from '../../../i18n/functions';
import Icon from '../../../icons/components/Icon';
import { IconArrowDown, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
import { IconArrowDown, IconCloseCircle, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
import Spinner from '../../../ui/components/web/Spinner';
import { runPreCallTest } from '../../actions.web';
import { CONNECTION_TYPE } from '../../constants';
import { getConnectionData } from '../../functions';

interface IProps extends WithTranslation {

/**
* List of strings with details about the connection.
*/
connectionDetails?: string[];

/**
* The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
*/
connectionType?: string;
}

const useStyles = makeStyles()(theme => {
return {
connectionStatus: {
Expand Down Expand Up @@ -68,6 +56,10 @@ const useStyles = makeStyles()(theme => {
background: '#31B76A'
},

'& .con-status--failed': {
background: '#E12D2D'
},

'& .con-status--poor': {
background: '#E12D2D'
},
Expand Down Expand Up @@ -122,6 +114,11 @@ const CONNECTION_TYPE_MAP: {
icon: Function;
};
} = {
[CONNECTION_TYPE.FAILED]: {
connectionClass: 'con-status--failed',
icon: IconCloseCircle,
connectionText: 'prejoin.connection.failed'
},
[CONNECTION_TYPE.POOR]: {
connectionClass: 'con-status--poor',
icon: IconWifi1Bar,
Expand All @@ -145,10 +142,17 @@ const CONNECTION_TYPE_MAP: {
* @param {IProps} props - The props of the component.
* @returns {ReactElement}
*/
function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
const ConnectionStatus = () => {
const { classes } = useStyles();

const dispatch = useDispatch();
const { t } = useTranslation();
const { connectionType, connectionDetails } = useSelector(getConnectionData);
const [ showDetails, toggleDetails ] = useState(false);

useEffect(() => {
dispatch(runPreCallTest());
}, []);

const arrowClassName = showDetails
? 'con-status-arrow con-status-arrow--up'
: 'con-status-arrow';
Expand All @@ -173,6 +177,26 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
return null;
}

if (connectionType === CONNECTION_TYPE.RUNNING) {
return (
<div className = { classes.connectionStatus }>
<div
aria-level = { 1 }
className = 'con-status-header'
role = 'heading'>
<div className = 'con-status-circle'>
<Spinner
color = { 'green' }
size = 'medium' />
</div>
<span
className = 'con-status-text'
id = 'connection-status-description'>{t('prejoin.connection.running')}</span>
</div>
</div>
);
}

const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];

return (
Expand Down Expand Up @@ -208,21 +232,6 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
{detailsText}</div>
</div>
);
}

/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps() {
const { connectionDetails, connectionType } = getConnectionData();

return {
connectionDetails,
connectionType
};
}
};

export default translate(connect(mapStateToProps)(ConnectionStatus));
export default ConnectionStatus;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { isButtonEnabled } from '../../../../toolbox/functions.web';
import { getConferenceName } from '../../../conference/functions';
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { isPreCallTestEnabled } from '../../functions';

import ConnectionStatus from './ConnectionStatus';
import Preview from './Preview';
Expand All @@ -24,6 +25,11 @@ interface IProps {
*/
_buttons: Array<string>;

/**
* Determine if pre call test is enabled.
*/
_isPreCallTestEnabled?: boolean;

/**
* The branding background of the premeeting screen(lobby/prejoin).
*/
Expand Down Expand Up @@ -169,6 +175,7 @@ const useStyles = makeStyles()(theme => {

const PreMeetingScreen = ({
_buttons,
_isPreCallTestEnabled,
_premeetingBackground,
_roomName,
children,
Expand All @@ -188,11 +195,13 @@ const PreMeetingScreen = ({
backgroundSize: 'cover'
} : {};

console.log('Rendering premeeting....');

return (
<div className = { clsx('premeeting-screen', classes.container, className) }>
<div style = { style }>
<div className = { classes.content }>
<ConnectionStatus />
{_isPreCallTestEnabled && <ConnectionStatus />}

<div className = { classes.contentControls }>
<h1 className = { classes.title }>
Expand Down Expand Up @@ -245,6 +254,7 @@ function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
_buttons: hiddenPremeetingButtons
? premeetingButtons
: premeetingButtons.filter(b => isButtonEnabled(b, toolbarButtons)),
_isPreCallTestEnabled: isPreCallTestEnabled(state),
_premeetingBackground: premeetingBackground,
_roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
};
Expand Down
4 changes: 3 additions & 1 deletion react/features/base/premeeting/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const CONNECTION_TYPE = {
FAILED: 'failed',
GOOD: 'good',
NON_OPTIMAL: 'nonOptimal',
NONE: 'none',
POOR: 'poor'
POOR: 'poor',
RUNNING: 'running'
};
Loading
Loading