Przeglądaj źródła

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

* enable precall test

* minor fixes

* update lang sort

* code review
factor2
Andrei Gavrilescu 7 miesięcy temu
rodzic
commit
dd859d2a26
No account linked to committer's email address

+ 5
- 0
config.js Wyświetl plik

@@ -758,6 +758,11 @@ var config = {
758 758
     //     hideDisplayName: false,
759 759
     //     // List of buttons to hide from the extra join options dropdown.
760 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 768
     // When 'true', the user cannot edit the display name.

+ 4
- 1
lang/main.json Wyświetl plik

@@ -922,9 +922,11 @@
922 922
         "configuringDevices": "Configuring devices...",
923 923
         "connectedWithAudioQ": "You’re connected with audio?",
924 924
         "connection": {
925
+            "failed": "Connection test failed!",
925 926
             "good": "Your internet connection looks good!",
926 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 931
         "connectionDetails": {
930 932
             "audioClipping": "We expect your audio to be clipped.",
@@ -933,6 +935,7 @@
933 935
             "goodQuality": "Awesome! Your media quality is going to be great.",
934 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 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 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 940
             "veryPoorConnection": "We expect your call quality to be really terrible.",
938 941
             "videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",

+ 2
- 0
react/features/base/config/configType.ts Wyświetl plik

@@ -484,6 +484,8 @@ export interface IConfig {
484 484
         enabled?: boolean;
485 485
         hideDisplayName?: boolean;
486 486
         hideExtraJoinButtons?: Array<string>;
487
+        preCallTestEnabled?: boolean;
488
+        preCallTestICEUrl?: string;
487 489
     };
488 490
     prejoinPageEnabled?: boolean;
489 491
     raisedHands?: {

+ 7
- 1
react/features/base/premeeting/actionTypes.ts Wyświetl plik

@@ -1,3 +1,9 @@
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 8
  * Type for setting the user's consent for unsafe room joining.
3 9
  *
@@ -6,4 +12,4 @@
6 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 Wyświetl plik

@@ -1,4 +1,10 @@
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 10
  * Sets the consent of the user for joining the unsafe room.
@@ -15,3 +21,47 @@ export function setUnsafeRoomConsent(consent: boolean) {
15 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 Wyświetl plik

@@ -1,29 +1,17 @@
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 4
 import { makeStyles } from 'tss-react/mui';
5 5
 
6
-import { translate } from '../../../i18n/functions';
7 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 8
 import { withPixelLineHeight } from '../../../styles/functions.web';
10 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 12
 import { CONNECTION_TYPE } from '../../constants';
12 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 15
 const useStyles = makeStyles()(theme => {
28 16
     return {
29 17
         connectionStatus: {
@@ -68,6 +56,10 @@ const useStyles = makeStyles()(theme => {
68 56
                 background: '#31B76A'
69 57
             },
70 58
 
59
+            '& .con-status--failed': {
60
+                background: '#E12D2D'
61
+            },
62
+
71 63
             '& .con-status--poor': {
72 64
                 background: '#E12D2D'
73 65
             },
@@ -122,6 +114,11 @@ const CONNECTION_TYPE_MAP: {
122 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 122
     [CONNECTION_TYPE.POOR]: {
126 123
         connectionClass: 'con-status--poor',
127 124
         icon: IconWifi1Bar,
@@ -145,10 +142,17 @@ const CONNECTION_TYPE_MAP: {
145 142
  * @param {IProps} props - The props of the component.
146 143
  * @returns {ReactElement}
147 144
  */
148
-function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
145
+const ConnectionStatus = () => {
149 146
     const { classes } = useStyles();
150
-
147
+    const dispatch = useDispatch();
148
+    const { t } = useTranslation();
149
+    const { connectionType, connectionDetails } = useSelector(getConnectionData);
151 150
     const [ showDetails, toggleDetails ] = useState(false);
151
+
152
+    useEffect(() => {
153
+        dispatch(runPreCallTest());
154
+    }, []);
155
+
152 156
     const arrowClassName = showDetails
153 157
         ? 'con-status-arrow con-status-arrow--up'
154 158
         : 'con-status-arrow';
@@ -173,6 +177,26 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
173 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 200
     const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];
177 201
 
178 202
     return (
@@ -208,21 +232,6 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
208 232
                 {detailsText}</div>
209 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 Wyświetl plik

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

+ 3
- 1
react/features/base/premeeting/constants.ts Wyświetl plik

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

+ 193
- 8
react/features/base/premeeting/functions.ts Wyświetl plik

@@ -1,4 +1,10 @@
1
+import { findIndex } from 'lodash-es';
2
+
3
+import { IReduxState } from '../../app/types';
4
+
1 5
 import { CONNECTION_TYPE } from './constants';
6
+import logger from './logger';
7
+import { IPreCallResult, PreCallTestStatus } from './types';
2 8
 
3 9
 
4 10
 /**
@@ -31,6 +37,14 @@ const defaultMarginTop = '10%';
31 37
  */
32 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 49
  * Calculates avatar dimensions based on window height and position.
36 50
  *
@@ -71,16 +85,187 @@ export function calculateAvatarDimensions(height: number) {
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 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 Wyświetl plik

@@ -1,10 +1,13 @@
1 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 7
 const DEFAULT_STATE: IPreMeetingState = {
8
+    preCallTestState: {
9
+        status: PreCallTestStatus.INITIAL
10
+    },
8 11
     unsafeRoomConsent: false
9 12
 };
10 13
 
@@ -20,6 +23,12 @@ ReducerRegistry.register<IPreMeetingState>(
20 23
     'features/base/premeeting',
21 24
     (state = DEFAULT_STATE, action): IPreMeetingState => {
22 25
         switch (action.type) {
26
+        case SET_PRECALL_TEST_RESULTS:
27
+            return {
28
+                ...state,
29
+                preCallTestState: action.value
30
+            };
31
+
23 32
         case SET_UNSAFE_ROOM_CONSENT: {
24 33
             return {
25 34
                 ...state,

+ 22
- 0
react/features/base/premeeting/types.ts Wyświetl plik

@@ -1,3 +1,25 @@
1
+
2
+export enum PreCallTestStatus {
3
+    FAILED = 'FAILED',
4
+    FINISHED = 'FINISHED',
5
+    INITIAL = 'INITIAL',
6
+    RUNNING = 'RUNNING'
7
+}
8
+
1 9
 export interface IPreMeetingState {
10
+    preCallTestState: IPreCallTestState;
2 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
+}

Ładowanie…
Anuluj
Zapisz