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,