From 0138f23755876e1a58967fd0a643eb2fef6e2556 Mon Sep 17 00:00:00 2001 From: Jaya Allamsetty <54324652+jallamsetty1@users.noreply.github.com> Date: Thu, 4 Feb 2021 12:33:18 -0500 Subject: [PATCH] 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. --- config.js | 6 +++ lang/main.json | 1 + react/features/base/conference/actionTypes.js | 20 +++++----- react/features/base/conference/actions.js | 32 +++++++-------- .../base/conference/middleware.any.js | 40 ++++++++++++------- .../base/conference/middleware.web.js | 22 +++++++++- react/features/prejoin/actionTypes.js | 5 +++ react/features/prejoin/actions.js | 15 +++++++ react/features/prejoin/functions.js | 3 +- react/features/prejoin/reducer.js | 26 +++++++++++- 10 files changed, 126 insertions(+), 44 deletions(-) diff --git a/config.js b/config.js index 18a6e8e0c..8611ffdea 100644 --- a/config.js +++ b/config.js @@ -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. diff --git a/lang/main.json b/lang/main.json index 0846d3281..ff8e9858d 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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", diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js index 39c79a664..db198e5e4 100644 --- a/react/features/base/conference/actionTypes.js +++ b/react/features/base/conference/actionTypes.js @@ -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. diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index a56022208..3a2e5a863 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -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 diff --git a/react/features/base/conference/middleware.any.js b/react/features/base/conference/middleware.any.js index 1ba019c02..d77325ac5 100644 --- a/react/features/base/conference/middleware.any.js +++ b/react/features/base/conference/middleware.any.js @@ -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; } diff --git a/react/features/base/conference/middleware.web.js b/react/features/base/conference/middleware.web.js index dd083b606..e696e85a8 100644 --- a/react/features/base/conference/middleware.web.js +++ b/react/features/base/conference/middleware.web.js @@ -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); diff --git a/react/features/prejoin/actionTypes.js b/react/features/prejoin/actionTypes.js index 5d66041ad..df4428af9 100644 --- a/react/features/prejoin/actionTypes.js +++ b/react/features/prejoin/actionTypes.js @@ -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. */ diff --git a/react/features/prejoin/actions.js b/react/features/prejoin/actions.js index 1d9a2945f..df76f08db 100644 --- a/react/features/prejoin/actions.js +++ b/react/features/prejoin/actions.js @@ -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'. * diff --git a/react/features/prejoin/functions.js b/react/features/prejoin/functions.js index 85b6770c0..fa39ff342 100644 --- a/react/features/prejoin/functions.js +++ b/react/features/prejoin/functions.js @@ -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); } /** diff --git a/react/features/prejoin/reducer.js b/react/features/prejoin/reducer.js index f4702f74e..35c235b85 100644 --- a/react/features/prejoin/reducer.js +++ b/react/features/prejoin/reducer.js @@ -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,