Selaa lähdekoodia

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

* enable precall test

* minor fixes

* update lang sort

* code review
factor2
Andrei Gavrilescu 11 kuukautta sitten
vanhempi
commit
dd859d2a26
No account linked to committer's email address

+ 5
- 0
config.js Näytä tiedosto

758
     //     hideDisplayName: false,
758
     //     hideDisplayName: false,
759
     //     // List of buttons to hide from the extra join options dropdown.
759
     //     // List of buttons to hide from the extra join options dropdown.
760
     //     hideExtraJoinButtons: ['no-audio', 'by-phone'],
760
     //     hideExtraJoinButtons: ['no-audio', 'by-phone'],
761
+    //     // Configuration for pre-call test
762
+    //     // By setting preCallTestEnabled, you enable the pre-call test in the prejoin page.
763
+    //     // ICE server credentials need to be provided over the preCallTestICEUrl
764
+    //     preCallTestEnabled: false,
765
+    //     preCallTestICEUrl: ''
761
     // },
766
     // },
762
 
767
 
763
     // When 'true', the user cannot edit the display name.
768
     // When 'true', the user cannot edit the display name.

+ 4
- 1
lang/main.json Näytä tiedosto

922
         "configuringDevices": "Configuring devices...",
922
         "configuringDevices": "Configuring devices...",
923
         "connectedWithAudioQ": "You’re connected with audio?",
923
         "connectedWithAudioQ": "You’re connected with audio?",
924
         "connection": {
924
         "connection": {
925
+            "failed": "Connection test failed!",
925
             "good": "Your internet connection looks good!",
926
             "good": "Your internet connection looks good!",
926
             "nonOptimal": "Your internet connection is not optimal",
927
             "nonOptimal": "Your internet connection is not optimal",
927
-            "poor": "You have a poor internet connection"
928
+            "poor": "You have a poor internet connection",
929
+            "running": "Running connection test..."
928
         },
930
         },
929
         "connectionDetails": {
931
         "connectionDetails": {
930
             "audioClipping": "We expect your audio to be clipped.",
932
             "audioClipping": "We expect your audio to be clipped.",
933
             "goodQuality": "Awesome! Your media quality is going to be great.",
935
             "goodQuality": "Awesome! Your media quality is going to be great.",
934
             "noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
936
             "noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
935
             "noVideo": "We expect that your video will be terrible.",
937
             "noVideo": "We expect that your video will be terrible.",
938
+            "testFailed": "The connection test encountered unexpected issues, but this might not impact your experience.",
936
             "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.",
939
             "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.",
937
             "veryPoorConnection": "We expect your call quality to be really terrible.",
940
             "veryPoorConnection": "We expect your call quality to be really terrible.",
938
             "videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
941
             "videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",

+ 2
- 0
react/features/base/config/configType.ts Näytä tiedosto

484
         enabled?: boolean;
484
         enabled?: boolean;
485
         hideDisplayName?: boolean;
485
         hideDisplayName?: boolean;
486
         hideExtraJoinButtons?: Array<string>;
486
         hideExtraJoinButtons?: Array<string>;
487
+        preCallTestEnabled?: boolean;
488
+        preCallTestICEUrl?: string;
487
     };
489
     };
488
     prejoinPageEnabled?: boolean;
490
     prejoinPageEnabled?: boolean;
489
     raisedHands?: {
491
     raisedHands?: {

+ 7
- 1
react/features/base/premeeting/actionTypes.ts Näytä tiedosto

1
+
2
+/**
3
+ * Action type to set the precall test data.
4
+ */
5
+export const SET_PRECALL_TEST_RESULTS = 'SET_PRECALL_TEST_RESULTS';
6
+
1
 /**
7
 /**
2
  * Type for setting the user's consent for unsafe room joining.
8
  * Type for setting the user's consent for unsafe room joining.
3
  *
9
  *
6
  *     consent: boolean
12
  *     consent: boolean
7
  * }
13
  * }
8
  */
14
  */
9
-export const SET_UNSAFE_ROOM_CONSENT = 'SET_UNSAFE_ROOM_CONSENT'
15
+export const SET_UNSAFE_ROOM_CONSENT = 'SET_UNSAFE_ROOM_CONSENT'

+ 51
- 1
react/features/base/premeeting/actions.web.ts Näytä tiedosto

1
-import { SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
1
+import { IStore } from '../../app/types';
2
+import JitsiMeetJS from '../lib-jitsi-meet';
3
+
4
+import { SET_PRECALL_TEST_RESULTS, SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
5
+import { getPreCallICEUrl } from './functions';
6
+import logger from './logger';
7
+import { IPreCallResult, IPreCallTestState, PreCallTestStatus } from './types';
2
 
8
 
3
 /**
9
 /**
4
  * Sets the consent of the user for joining the unsafe room.
10
  * Sets the consent of the user for joining the unsafe room.
15
         consent
21
         consent
16
     };
22
     };
17
 }
23
 }
24
+
25
+/**
26
+ * Initializes the 'precallTest' and executes one test, storing the results.
27
+ *
28
+ * @returns {Function}
29
+ */
30
+export function runPreCallTest() {
31
+    return async function(dispatch: Function, getState: IStore['getState']) {
32
+        try {
33
+
34
+            dispatch(setPreCallTestResults({ status: PreCallTestStatus.RUNNING }));
35
+
36
+            const turnCredentialsUrl = getPreCallICEUrl(getState());
37
+
38
+            if (!turnCredentialsUrl) {
39
+                throw new Error('No TURN credentials URL provided in config');
40
+            }
41
+
42
+            const turnCredentials = await fetch(turnCredentialsUrl);
43
+            const { iceServers } = await turnCredentials.json();
44
+            const result: IPreCallResult = await JitsiMeetJS.runPreCallTest(iceServers);
45
+
46
+            dispatch(setPreCallTestResults({ status: PreCallTestStatus.FINISHED,
47
+                result }));
48
+        } catch (error) {
49
+            logger.error('Failed to run pre-call test', error);
50
+
51
+            dispatch(setPreCallTestResults({ status: PreCallTestStatus.FAILED }));
52
+        }
53
+    };
54
+}
55
+
56
+/**
57
+ * Action used to set data from precall test.
58
+ *
59
+ * @param {IPreCallTestState} value - The precall test results.
60
+ * @returns {Object}
61
+ */
62
+export function setPreCallTestResults(value: IPreCallTestState) {
63
+    return {
64
+        type: SET_PRECALL_TEST_RESULTS,
65
+        value
66
+    };
67
+}

+ 46
- 37
react/features/base/premeeting/components/web/ConnectionStatus.tsx Näytä tiedosto

1
-import React, { useCallback, useState } from 'react';
2
-import { WithTranslation } from 'react-i18next';
3
-import { connect } from 'react-redux';
1
+import React, { useCallback, useEffect, useState } from 'react';
2
+import { useTranslation } from 'react-i18next';
3
+import { useDispatch, useSelector } from 'react-redux';
4
 import { makeStyles } from 'tss-react/mui';
4
 import { makeStyles } from 'tss-react/mui';
5
 
5
 
6
-import { translate } from '../../../i18n/functions';
7
 import Icon from '../../../icons/components/Icon';
6
 import Icon from '../../../icons/components/Icon';
8
-import { IconArrowDown, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
7
+import { IconArrowDown, IconCloseCircle, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
9
 import { withPixelLineHeight } from '../../../styles/functions.web';
8
 import { withPixelLineHeight } from '../../../styles/functions.web';
10
 import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
9
 import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
10
+import Spinner from '../../../ui/components/web/Spinner';
11
+import { runPreCallTest } from '../../actions.web';
11
 import { CONNECTION_TYPE } from '../../constants';
12
 import { CONNECTION_TYPE } from '../../constants';
12
 import { getConnectionData } from '../../functions';
13
 import { getConnectionData } from '../../functions';
13
 
14
 
14
-interface IProps extends WithTranslation {
15
-
16
-    /**
17
-     * List of strings with details about the connection.
18
-     */
19
-    connectionDetails?: string[];
20
-
21
-    /**
22
-     * The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
23
-     */
24
-    connectionType?: string;
25
-}
26
-
27
 const useStyles = makeStyles()(theme => {
15
 const useStyles = makeStyles()(theme => {
28
     return {
16
     return {
29
         connectionStatus: {
17
         connectionStatus: {
68
                 background: '#31B76A'
56
                 background: '#31B76A'
69
             },
57
             },
70
 
58
 
59
+            '& .con-status--failed': {
60
+                background: '#E12D2D'
61
+            },
62
+
71
             '& .con-status--poor': {
63
             '& .con-status--poor': {
72
                 background: '#E12D2D'
64
                 background: '#E12D2D'
73
             },
65
             },
122
         icon: Function;
114
         icon: Function;
123
     };
115
     };
124
 } = {
116
 } = {
117
+    [CONNECTION_TYPE.FAILED]: {
118
+        connectionClass: 'con-status--failed',
119
+        icon: IconCloseCircle,
120
+        connectionText: 'prejoin.connection.failed'
121
+    },
125
     [CONNECTION_TYPE.POOR]: {
122
     [CONNECTION_TYPE.POOR]: {
126
         connectionClass: 'con-status--poor',
123
         connectionClass: 'con-status--poor',
127
         icon: IconWifi1Bar,
124
         icon: IconWifi1Bar,
145
  * @param {IProps} props - The props of the component.
142
  * @param {IProps} props - The props of the component.
146
  * @returns {ReactElement}
143
  * @returns {ReactElement}
147
  */
144
  */
148
-function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
145
+const ConnectionStatus = () => {
149
     const { classes } = useStyles();
146
     const { classes } = useStyles();
150
-
147
+    const dispatch = useDispatch();
148
+    const { t } = useTranslation();
149
+    const { connectionType, connectionDetails } = useSelector(getConnectionData);
151
     const [ showDetails, toggleDetails ] = useState(false);
150
     const [ showDetails, toggleDetails ] = useState(false);
151
+
152
+    useEffect(() => {
153
+        dispatch(runPreCallTest());
154
+    }, []);
155
+
152
     const arrowClassName = showDetails
156
     const arrowClassName = showDetails
153
         ? 'con-status-arrow con-status-arrow--up'
157
         ? 'con-status-arrow con-status-arrow--up'
154
         : 'con-status-arrow';
158
         : 'con-status-arrow';
173
         return null;
177
         return null;
174
     }
178
     }
175
 
179
 
180
+    if (connectionType === CONNECTION_TYPE.RUNNING) {
181
+        return (
182
+            <div className = { classes.connectionStatus }>
183
+                <div
184
+                    aria-level = { 1 }
185
+                    className = 'con-status-header'
186
+                    role = 'heading'>
187
+                    <div className = 'con-status-circle'>
188
+                        <Spinner
189
+                            color = { 'green' }
190
+                            size = 'medium' />
191
+                    </div>
192
+                    <span
193
+                        className = 'con-status-text'
194
+                        id = 'connection-status-description'>{t('prejoin.connection.running')}</span>
195
+                </div>
196
+            </div>
197
+        );
198
+    }
199
+
176
     const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];
200
     const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];
177
 
201
 
178
     return (
202
     return (
208
                 {detailsText}</div>
232
                 {detailsText}</div>
209
         </div>
233
         </div>
210
     );
234
     );
211
-}
212
-
213
-/**
214
- * Maps (parts of) the redux state to the React {@code Component} props.
215
- *
216
- * @param {Object} state - The redux state.
217
- * @returns {Object}
218
- */
219
-function mapStateToProps() {
220
-    const { connectionDetails, connectionType } = getConnectionData();
221
-
222
-    return {
223
-        connectionDetails,
224
-        connectionType
225
-    };
226
-}
235
+};
227
 
236
 
228
-export default translate(connect(mapStateToProps)(ConnectionStatus));
237
+export default ConnectionStatus;

+ 11
- 1
react/features/base/premeeting/components/web/PreMeetingScreen.tsx Näytä tiedosto

11
 import { getConferenceName } from '../../../conference/functions';
11
 import { getConferenceName } from '../../../conference/functions';
12
 import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
12
 import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
13
 import { withPixelLineHeight } from '../../../styles/functions.web';
13
 import { withPixelLineHeight } from '../../../styles/functions.web';
14
+import { isPreCallTestEnabled } from '../../functions';
14
 
15
 
15
 import ConnectionStatus from './ConnectionStatus';
16
 import ConnectionStatus from './ConnectionStatus';
16
 import Preview from './Preview';
17
 import Preview from './Preview';
24
      */
25
      */
25
     _buttons: Array<string>;
26
     _buttons: Array<string>;
26
 
27
 
28
+    /**
29
+     * Determine if pre call test is enabled.
30
+     */
31
+    _isPreCallTestEnabled?: boolean;
32
+
27
     /**
33
     /**
28
      * The branding background of the premeeting screen(lobby/prejoin).
34
      * The branding background of the premeeting screen(lobby/prejoin).
29
      */
35
      */
169
 
175
 
170
 const PreMeetingScreen = ({
176
 const PreMeetingScreen = ({
171
     _buttons,
177
     _buttons,
178
+    _isPreCallTestEnabled,
172
     _premeetingBackground,
179
     _premeetingBackground,
173
     _roomName,
180
     _roomName,
174
     children,
181
     children,
188
         backgroundSize: 'cover'
195
         backgroundSize: 'cover'
189
     } : {};
196
     } : {};
190
 
197
 
198
+    console.log('Rendering premeeting....');
199
+
191
     return (
200
     return (
192
         <div className = { clsx('premeeting-screen', classes.container, className) }>
201
         <div className = { clsx('premeeting-screen', classes.container, className) }>
193
             <div style = { style }>
202
             <div style = { style }>
194
                 <div className = { classes.content }>
203
                 <div className = { classes.content }>
195
-                    <ConnectionStatus />
204
+                    {_isPreCallTestEnabled && <ConnectionStatus />}
196
 
205
 
197
                     <div className = { classes.contentControls }>
206
                     <div className = { classes.contentControls }>
198
                         <h1 className = { classes.title }>
207
                         <h1 className = { classes.title }>
245
         _buttons: hiddenPremeetingButtons
254
         _buttons: hiddenPremeetingButtons
246
             ? premeetingButtons
255
             ? premeetingButtons
247
             : premeetingButtons.filter(b => isButtonEnabled(b, toolbarButtons)),
256
             : premeetingButtons.filter(b => isButtonEnabled(b, toolbarButtons)),
257
+        _isPreCallTestEnabled: isPreCallTestEnabled(state),
248
         _premeetingBackground: premeetingBackground,
258
         _premeetingBackground: premeetingBackground,
249
         _roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
259
         _roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
250
     };
260
     };

+ 3
- 1
react/features/base/premeeting/constants.ts Näytä tiedosto

1
 export const CONNECTION_TYPE = {
1
 export const CONNECTION_TYPE = {
2
+    FAILED: 'failed',
2
     GOOD: 'good',
3
     GOOD: 'good',
3
     NON_OPTIMAL: 'nonOptimal',
4
     NON_OPTIMAL: 'nonOptimal',
4
     NONE: 'none',
5
     NONE: 'none',
5
-    POOR: 'poor'
6
+    POOR: 'poor',
7
+    RUNNING: 'running'
6
 };
8
 };

+ 193
- 8
react/features/base/premeeting/functions.ts Näytä tiedosto

1
+import { findIndex } from 'lodash-es';
2
+
3
+import { IReduxState } from '../../app/types';
4
+
1
 import { CONNECTION_TYPE } from './constants';
5
 import { CONNECTION_TYPE } from './constants';
6
+import logger from './logger';
7
+import { IPreCallResult, PreCallTestStatus } from './types';
2
 
8
 
3
 
9
 
4
 /**
10
 /**
31
  */
37
  */
32
 const smallMarginTop = '5%';
38
 const smallMarginTop = '5%';
33
 
39
 
40
+// loss in percentage overall the test duration
41
+const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
42
+const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
43
+
44
+// throughput in kbps
45
+const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
46
+const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
47
+
34
 /**
48
 /**
35
  * Calculates avatar dimensions based on window height and position.
49
  * Calculates avatar dimensions based on window height and position.
36
  *
50
  *
71
 }
85
 }
72
 
86
 
73
 /**
87
 /**
74
- * Selector for determining the connection type & details.
88
+ * Returns the level based on a list of thresholds.
75
  *
89
  *
76
- * @returns {{
77
- *   connectionType: string,
78
- *   connectionDetails: string[]
79
- * }}
90
+ * @param {number[]} thresholds - The thresholds array.
91
+ * @param {number} value - The value against which the level is calculated.
92
+ * @param {boolean} descending - The order based on which the level is calculated.
93
+ *
94
+ * @returns {number}
80
  */
95
  */
81
-export function getConnectionData() {
96
+function _getLevel(thresholds: number[], value: number, descending = true) {
97
+    let predicate;
98
+
99
+    if (descending) {
100
+        predicate = function(threshold: number) {
101
+            return value > threshold;
102
+        };
103
+    } else {
104
+        predicate = function(threshold: number) {
105
+            return value < threshold;
106
+        };
107
+    }
108
+
109
+    const i = findIndex(thresholds, predicate);
110
+
111
+    if (i === -1) {
112
+        return thresholds.length;
113
+    }
114
+
115
+    return i;
116
+}
117
+
118
+/**
119
+ * Returns the connection details from the test results.
120
+ *
121
+ * @param {number} testResults.fractionalLoss - Factional loss.
122
+ * @param {number} testResults.throughput - Throughput.
123
+ *
124
+ * @returns {{
125
+*   connectionType: string,
126
+*   connectionDetails: string[]
127
+* }}
128
+*/
129
+function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t, mediaConnectivity }: IPreCallResult) {
130
+    let connectionType = CONNECTION_TYPE.FAILED;
131
+    const connectionDetails: Array<string> = [];
132
+
133
+    if (!mediaConnectivity) {
134
+        connectionType = CONNECTION_TYPE.POOR;
135
+        connectionDetails.push('prejoin.connectionDetails.noMediaConnectivity');
136
+
137
+        return {
138
+            connectionType,
139
+            connectionDetails
140
+        };
141
+    }
142
+
143
+    const loss = {
144
+        audioQuality: _getLevel(LOSS_AUDIO_THRESHOLDS, l),
145
+        videoQuality: _getLevel(LOSS_VIDEO_THRESHOLDS, l)
146
+    };
147
+    const throughput = {
148
+        audioQuality: _getLevel(THROUGHPUT_AUDIO_THRESHOLDS, t, false),
149
+        videoQuality: _getLevel(THROUGHPUT_VIDEO_THRESHOLDS, t, false)
150
+    };
151
+
152
+    if (throughput.audioQuality === 0 || loss.audioQuality === 0) {
153
+        // Calls are impossible.
154
+        connectionType = CONNECTION_TYPE.POOR;
155
+        connectionDetails.push('prejoin.connectionDetails.veryPoorConnection');
156
+    } else if (
157
+        throughput.audioQuality === 2
158
+       && throughput.videoQuality === 2
159
+       && loss.audioQuality === 2
160
+       && loss.videoQuality === 3
161
+    ) {
162
+        // Ideal conditions for both audio and video. Show only one message.
163
+        connectionType = CONNECTION_TYPE.GOOD;
164
+        connectionDetails.push('prejoin.connectionDetails.goodQuality');
165
+    } else {
166
+        connectionType = CONNECTION_TYPE.NON_OPTIMAL;
167
+
168
+        if (throughput.audioQuality === 1) {
169
+            // Minimum requirements for a call are met.
170
+            connectionDetails.push('prejoin.connectionDetails.audioLowNoVideo');
171
+        } else {
172
+            // There are two paragraphs: one saying something about audio and the other about video.
173
+            if (loss.audioQuality === 1) {
174
+                connectionDetails.push('prejoin.connectionDetails.audioClipping');
175
+            } else {
176
+                connectionDetails.push('prejoin.connectionDetails.audioHighQuality');
177
+            }
178
+
179
+            if (throughput.videoQuality === 0 || loss.videoQuality === 0) {
180
+                connectionDetails.push('prejoin.connectionDetails.noVideo');
181
+            } else if (throughput.videoQuality === 1) {
182
+                connectionDetails.push('prejoin.connectionDetails.videoLowQuality');
183
+            } else if (loss.videoQuality === 1) {
184
+                connectionDetails.push('prejoin.connectionDetails.videoFreezing');
185
+            } else if (loss.videoQuality === 2) {
186
+                connectionDetails.push('prejoin.connectionDetails.videoTearing');
187
+            } else {
188
+                connectionDetails.push('prejoin.connectionDetails.videoHighQuality');
189
+            }
190
+        }
191
+        connectionDetails.push('prejoin.connectionDetails.undetectable');
192
+    }
193
+
82
     return {
194
     return {
83
-        connectionType: CONNECTION_TYPE.NONE,
84
-        connectionDetails: []
195
+        connectionType,
196
+        connectionDetails
85
     };
197
     };
86
 }
198
 }
199
+
200
+/**
201
+ * Selector for determining the connection type & details.
202
+ *
203
+ * @param {Object} state - The state of the app.
204
+ * @returns {{
205
+*   connectionType: string,
206
+*   connectionDetails: string[]
207
+* }}
208
+*/
209
+export function getConnectionData(state: IReduxState) {
210
+    const { preCallTestState: { status, result } } = state['features/base/premeeting'];
211
+
212
+    switch (status) {
213
+    case PreCallTestStatus.INITIAL:
214
+        return {
215
+            connectionType: CONNECTION_TYPE.NONE,
216
+            connectionDetails: []
217
+        };
218
+    case PreCallTestStatus.RUNNING:
219
+        return {
220
+            connectionType: CONNECTION_TYPE.RUNNING,
221
+            connectionDetails: []
222
+        };
223
+    case PreCallTestStatus.FAILED:
224
+        // A failed test means that something went wrong with our business logic and not necessarily
225
+        // that the connection is bad. For instance, the endpoint providing the ICE credentials could be down.
226
+        return {
227
+            connectionType: CONNECTION_TYPE.FAILED,
228
+            connectionDetails: [ 'prejoin.connectionDetails.testFailed' ]
229
+        };
230
+    case PreCallTestStatus.FINISHED:
231
+        if (result) {
232
+            return _getConnectionDataFromTestResults(result);
233
+        }
234
+
235
+        logger.error('Pre-call test finished but no test results were available');
236
+
237
+        return {
238
+            connectionType: CONNECTION_TYPE.FAILED,
239
+            connectionDetails: [ 'prejoin.connectionDetails.testFailed' ]
240
+        };
241
+    default:
242
+        return {
243
+            connectionType: CONNECTION_TYPE.NONE,
244
+            connectionDetails: []
245
+        };
246
+    }
247
+}
248
+
249
+/**
250
+ * Selector for determining if the pre-call test is enabled.
251
+ *
252
+ * @param {Object} state - The state of the app.
253
+ * @returns {boolean}
254
+ */
255
+export function isPreCallTestEnabled(state: IReduxState): boolean {
256
+    const { prejoinConfig } = state['features/base/config'];
257
+
258
+    return prejoinConfig?.preCallTestEnabled ?? false;
259
+}
260
+
261
+/**
262
+ * Selector for retrieving the pre-call test ICE URL.
263
+ *
264
+ * @param {Object} state - The state of the app.
265
+ * @returns {string | undefined}
266
+ */
267
+export function getPreCallICEUrl(state: IReduxState): string | undefined {
268
+    const { prejoinConfig } = state['features/base/config'];
269
+
270
+    return prejoinConfig?.preCallTestICEUrl;
271
+}

+ 11
- 2
react/features/base/premeeting/reducer.web.ts Näytä tiedosto

1
 import ReducerRegistry from '../redux/ReducerRegistry';
1
 import ReducerRegistry from '../redux/ReducerRegistry';
2
 
2
 
3
-import { SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
4
-import { IPreMeetingState } from './types';
3
+import { SET_PRECALL_TEST_RESULTS, SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
4
+import { IPreMeetingState, PreCallTestStatus } from './types';
5
 
5
 
6
 
6
 
7
 const DEFAULT_STATE: IPreMeetingState = {
7
 const DEFAULT_STATE: IPreMeetingState = {
8
+    preCallTestState: {
9
+        status: PreCallTestStatus.INITIAL
10
+    },
8
     unsafeRoomConsent: false
11
     unsafeRoomConsent: false
9
 };
12
 };
10
 
13
 
20
     'features/base/premeeting',
23
     'features/base/premeeting',
21
     (state = DEFAULT_STATE, action): IPreMeetingState => {
24
     (state = DEFAULT_STATE, action): IPreMeetingState => {
22
         switch (action.type) {
25
         switch (action.type) {
26
+        case SET_PRECALL_TEST_RESULTS:
27
+            return {
28
+                ...state,
29
+                preCallTestState: action.value
30
+            };
31
+
23
         case SET_UNSAFE_ROOM_CONSENT: {
32
         case SET_UNSAFE_ROOM_CONSENT: {
24
             return {
33
             return {
25
                 ...state,
34
                 ...state,

+ 22
- 0
react/features/base/premeeting/types.ts Näytä tiedosto

1
+
2
+export enum PreCallTestStatus {
3
+    FAILED = 'FAILED',
4
+    FINISHED = 'FINISHED',
5
+    INITIAL = 'INITIAL',
6
+    RUNNING = 'RUNNING'
7
+}
8
+
1
 export interface IPreMeetingState {
9
 export interface IPreMeetingState {
10
+    preCallTestState: IPreCallTestState;
2
     unsafeRoomConsent?: boolean;
11
     unsafeRoomConsent?: boolean;
3
 }
12
 }
13
+
14
+export interface IPreCallTestState {
15
+    result?: IPreCallResult;
16
+    status: PreCallTestStatus;
17
+}
18
+
19
+export interface IPreCallResult {
20
+    fractionalLoss: number;
21
+    jitter: number;
22
+    mediaConnectivity: boolean;
23
+    rtt: number;
24
+    throughput: number;
25
+}

Loading…
Peruuta
Tallenna