feat(conference): Enable forced reload of client on bridge failure.

* feat(conference): Enable forced reload of client on bridge failure.
Force the client to reload when the bridge that is handling the media goes down.
This mitigates issues seen on the bridge because of a client re-joining the call with the same endpointId, BWE issues, etc.
This behavior is configurable through 'enableForcedReload' setting in config.js.
The client skips the pre-join page when the page reloads.

* squash: refactor the restart logic.

* squash: fix description

* squash: dispatch conferenceWillLeave action before reload.
This commit is contained in:
Jaya Allamsetty 2021-02-04 12:33:18 -05:00 committed by GitHub
parent 16d88a288f
commit 0138f23755
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 44 deletions

View File

@ -325,6 +325,11 @@ var config = {
// TCC sequence numbers starting from 0.
// enableIceRestart: false,
// Enables forced reload of the client when the call is migrated as a result of
// the bridge going down. Currently enabled by default as call migration through
// session-terminate is causing siganling issues when Octo is enabled.
// enableForcedReload: true,
// Use TURN/UDP servers for the jitsi-videobridge connection (by default
// we filter out TURN/UDP because it is usually not needed since the
// bridge itself is reachable via UDP)
@ -732,6 +737,7 @@ var config = {
// 'dialog.reservationError',
// 'dialog.serviceUnavailable', // shown when server is not reachable
// 'dialog.sessTerminated', // shown when there is a failed conference session
// 'dialog.sessionRestarted', // show when a client reload is initiated because of bridge migration
// 'dialog.tokenAuthFailed', // show when an invalid jwt is used
// 'dialog.transcribing', // transcribing notifications (pending, off)
// 'dialOut.statusMessage', // shown when dial out status is updated.

View File

@ -280,6 +280,7 @@
"sendPrivateMessageTitle": "Send privately?",
"serviceUnavailable": "Service unavailable",
"sessTerminated": "Call terminated",
"sessionRestarted": "Call restarted by the bridge",
"Share": "Share",
"shareVideoLinkError": "Please provide a correct youtube link.",
"shareVideoTitle": "Share a video",

View File

@ -42,16 +42,6 @@ export const CONFERENCE_JOINED = 'CONFERENCE_JOINED';
*/
export const CONFERENCE_LEFT = 'CONFERENCE_LEFT';
/**
* The type of (redux) action which signals that an uuid for a conference has been set.
*
* {
* type: CONFERENCE_UNIQUE_ID_SET,
* conference: JitsiConference
* }
*/
export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET';
/**
* The type of (redux) action, which indicates conference subject changes.
*
@ -72,6 +62,16 @@ export const CONFERENCE_SUBJECT_CHANGED = 'CONFERENCE_SUBJECT_CHANGED';
*/
export const CONFERENCE_TIMESTAMP_CHANGED = 'CONFERENCE_TIMESTAMP_CHANGED';
/**
* The type of (redux) action which signals that an uuid for a conference has been set.
*
* {
* type: CONFERENCE_UNIQUE_ID_SET,
* conference: JitsiConference
* }
*/
export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET';
/**
* The type of (redux) action which signals that a specific conference will be
* joined.

View File

@ -296,22 +296,6 @@ export function conferenceLeft(conference: Object) {
};
}
/**
* Signals that the unique identifier for conference has been set.
*
* @param {JitsiConference} conference - The JitsiConference instance, where the uuid has been set.
* @returns {{
* type: CONFERENCE_UNIQUE_ID_SET,
* conference: JitsiConference,
* }}
*/
export function conferenceUniqueIdSet(conference: Object) {
return {
type: CONFERENCE_UNIQUE_ID_SET,
conference
};
}
/**
* Signals that the conference subject has been changed.
*
@ -344,6 +328,22 @@ export function conferenceTimestampChanged(conferenceTimestamp: number) {
};
}
/**
* Signals that the unique identifier for conference has been set.
*
* @param {JitsiConference} conference - The JitsiConference instance, where the uuid has been set.
* @returns {{
* type: CONFERENCE_UNIQUE_ID_SET,
* conference: JitsiConference,
* }}
*/
export function conferenceUniqueIdSet(conference: Object) {
return {
type: CONFERENCE_UNIQUE_ID_SET,
conference
};
}
/**
* Adds any existing local tracks to a specific conference before the conference
* is joined. Then signals the intention of the application to have the local

View File

@ -7,6 +7,7 @@ import {
createPinnedEvent,
sendAnalytics
} from '../../analytics';
import { reloadNow } from '../../app/actions';
import { openDisplayNamePrompt } from '../../display-name';
import { showErrorNotification } from '../../notifications';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
@ -117,6 +118,7 @@ MiddlewareRegistry.register(store => next => action => {
function _conferenceFailed({ dispatch, getState }, next, action) {
const result = next(action);
const { conference, error } = action;
const { enableForcedReload } = getState()['features/base/config'];
// Handle specific failure reasons.
switch (error.name) {
@ -130,6 +132,16 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
break;
}
case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
if (enableForcedReload) {
dispatch(showErrorNotification({
description: 'Restart initiated because of a bridge failure',
titleKey: 'dialog.sessionRestarted'
}));
}
break;
}
case JitsiConferenceErrors.CONNECTION_ERROR: {
const [ msg ] = error.params;
@ -147,26 +159,26 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
break;
}
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
if (typeof APP !== 'undefined') {
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
return result;
}
// XXX After next(action), it is clear whether the error is recoverable.
!error.recoverable
if (typeof APP === 'undefined') {
!error.recoverable
&& conference
&& conference.leave().catch(reason => {
// Even though we don't care too much about the failure, it may be
// good to know that it happen, so log it (on the info level).
logger.info('JitsiConference.leave() rejected with:', reason);
});
} else if (typeof beforeUnloadHandler !== 'undefined') {
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
dispatch(conferenceWillLeave(conference));
dispatch(reloadNow());
}
return result;
}

View File

@ -1,15 +1,35 @@
// @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { setPrejoinPageVisibility, setSkipPrejoinOnReload } from '../../prejoin';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from './actionTypes';
import './middleware.any';
declare var APP: Object;
MiddlewareRegistry.register((/* store */) => next => action => {
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { enableForcedReload } = getState()['features/base/config'];
switch (action.type) {
case CONFERENCE_JOINED: {
if (enableForcedReload) {
dispatch(setPrejoinPageVisibility(false));
dispatch(setSkipPrejoinOnReload(false));
}
break;
}
case CONFERENCE_FAILED: {
enableForcedReload
&& action.error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED
&& dispatch(setSkipPrejoinOnReload(true));
break;
}
case TOGGLE_SCREENSHARING: {
if (typeof APP === 'object') {
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);

View File

@ -19,6 +19,11 @@ export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
*/
export const SET_SKIP_PREJOIN = 'SET_SKIP_PREJOIN';
/**
* Action type to set the visiblity of the prejoin page when client is forcefully reloaded.
*/
export const SET_SKIP_PREJOIN_RELOAD = 'SET_SKIP_PREJOIN_RELOAD';
/**
* Action type used to set the mandatory stance of the prejoin display name.
*/

View File

@ -26,6 +26,7 @@ import {
SET_DIALOUT_STATUS,
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
SET_SKIP_PREJOIN,
SET_SKIP_PREJOIN_RELOAD,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PRECALL_TEST_RESULTS,
SET_PREJOIN_DEVICE_ERRORS,
@ -418,6 +419,20 @@ export function setSkipPrejoin(value: boolean) {
};
}
/**
* Sets the visibility of the prejoin page when a client reload
* is triggered as a result of call migration initiated by Jicofo.
*
* @param {boolean} value - The visibility value.
* @returns {Object}
*/
export function setSkipPrejoinOnReload(value: boolean) {
return {
type: SET_SKIP_PREJOIN_RELOAD,
value
};
}
/**
* Action used to set the visiblitiy of the 'JoinByPhoneDialog'.
*

View File

@ -149,7 +149,8 @@ export function isJoinByPhoneDialogVisible(state: Object): boolean {
export function isPrejoinPageEnabled(state: Object): boolean {
return navigator.product !== 'ReactNative'
&& state['features/base/config'].prejoinPageEnabled
&& !state['features/base/settings'].userSelectedSkipPrejoin;
&& !state['features/base/settings'].userSelectedSkipPrejoin
&& !(state['features/base/config'].enableForcedReload && state['features/prejoin'].skipPrejoinOnReload);
}
/**

View File

@ -1,4 +1,4 @@
import { ReducerRegistry } from '../base/redux';
import { PersistenceRegistry, ReducerRegistry } from '../base/redux';
import {
SET_DEVICE_STATUS,
@ -10,7 +10,8 @@ import {
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
SET_PREJOIN_PAGE_VISIBILITY,
SET_SKIP_PREJOIN
SET_SKIP_PREJOIN,
SET_SKIP_PREJOIN_RELOAD
} from './actionTypes';
const DEFAULT_STATE = {
@ -28,10 +29,24 @@ const DEFAULT_STATE = {
name: '',
rawError: '',
showPrejoin: true,
skipPrejoinOnReload: false,
showJoinByPhoneDialog: false,
userSelectedSkipPrejoin: false
};
/**
* The name of the redux store/state property which is the root of the redux
* state of the feature {@code prejoin}.
*/
const STORE_NAME = 'features/prejoin';
/**
* Sets up the persistence of the feature {@code prejoin}.
*/
PersistenceRegistry.register(STORE_NAME, {
skipPrejoinOnReload: true
}, DEFAULT_STATE);
/**
* Listen for actions that mutate the prejoin state
*/
@ -46,6 +61,13 @@ ReducerRegistry.register(
};
}
case SET_SKIP_PREJOIN_RELOAD: {
return {
...state,
skipPrejoinOnReload: action.value
};
}
case SET_PRECALL_TEST_RESULTS:
return {
...state,