From 27ab4e555eb372fa93a123ec91dc090600f0ce78 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Mon, 12 Feb 2024 18:35:51 -0600 Subject: [PATCH] fix(prosody-auth): Don't loose initial tracks. During the prosody login cycle the initial GUM tracks were lost causing the user to start the call without local media and audio/video mute buttons staying forever in pending state. --- conference.js | 35 +++++++++++-------- react/features/authentication/actions.web.ts | 24 +++++++++++++ .../components/web/LoginDialog.tsx | 8 ++--- react/features/authentication/middleware.ts | 7 ++++ react/features/base/media/actionTypes.ts | 10 ++++++ react/features/base/media/actions.ts | 17 +++++++++ react/features/base/media/reducer.ts | 31 ++++++++++++++++ 7 files changed, 113 insertions(+), 19 deletions(-) diff --git a/conference.js b/conference.js index af135e1cfce0a..b2c23ee16fe9d 100644 --- a/conference.js +++ b/conference.js @@ -86,6 +86,7 @@ import { setAudioAvailable, setAudioMuted, setAudioUnmutePermissions, + setInitialGUMPromise, setVideoAvailable, setVideoMuted, setVideoUnmutePermissions @@ -715,6 +716,7 @@ export default { return localTracks; }; + const { dispatch } = APP.store; if (isPrejoinPageVisible(state)) { const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions); @@ -725,9 +727,9 @@ export default { this._initDeviceList(true); if (isPrejoinPageVisible(state)) { - APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE)); + dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE)); - return APP.store.dispatch(initPrejoin(localTracks, errors)); + return dispatch(initPrejoin(localTracks, errors)); } logger.debug('Prejoin screen no longer displayed at the time when tracks were created'); @@ -742,23 +744,28 @@ export default { } const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions); + const gumPromise = tryCreateLocalTracks.then(tr => { + this._displayErrorsForCreateInitialLocalTracks(errors); - return Promise.all([ - tryCreateLocalTracks.then(tr => { - this._displayErrorsForCreateInitialLocalTracks(errors); + return tr; + }).then(tr => { + this._initDeviceList(true); - return tr; - }).then(tr => { - this._initDeviceList(true); + const filteredTracks = handleInitialTracks(initialOptions, tr); - const filteredTracks = handleInitialTracks(initialOptions, tr); + setGUMPendingStateOnFailedTracks(filteredTracks); - setGUMPendingStateOnFailedTracks(filteredTracks); + return filteredTracks; + }); - return filteredTracks; - }), - APP.store.dispatch(connect()) - ]).then(([ tracks, _ ]) => { + return Promise.all([ + gumPromise, + dispatch(connect()) + ]).catch(e => { + dispatch(setInitialGUMPromise(gumPromise)); + throw e; + }) + .then(([ tracks, _ ]) => { this.startConference(tracks).catch(logger.error); }); }, diff --git a/react/features/authentication/actions.web.ts b/react/features/authentication/actions.web.ts index 17c3221138f06..69e272f64b1cf 100644 --- a/react/features/authentication/actions.web.ts +++ b/react/features/authentication/actions.web.ts @@ -1,10 +1,13 @@ import { maybeRedirectToWelcomePage } from '../app/actions.web'; import { IStore } from '../app/types'; +import { connect } from '../base/connection/actions.web'; import { openDialog } from '../base/dialog/actions'; import { browser } from '../base/lib-jitsi-meet'; +import { setInitialGUMPromise } from '../base/media/actions'; import { CANCEL_LOGIN } from './actionTypes'; import LoginQuestionDialog from './components/web/LoginQuestionDialog'; +import logger from './logger'; export * from './actions.any'; @@ -76,3 +79,24 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any { } }; } + +/** + * Executes connect with the passed credentials and then continues the flow to start a conference. + * + * @param {string} jid - The jid for the connection. + * @param {string} password - The password for the connection. + * @returns {Function} + */ +export function sumbitConnectionCredentials(jid?: string, password?: string) { + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + const { initialGUMPromise } = getState()['features/base/media'].common; + + dispatch(connect(jid, password)) + .then(() => initialGUMPromise ?? []) + .then((tracks: Array = []) => { + // clear the initial GUM promise since we don't need it anymore. + dispatch(setInitialGUMPromise()); + APP.conference.startConference(tracks).catch(logger.error); + }); + }; +} diff --git a/react/features/authentication/components/web/LoginDialog.tsx b/react/features/authentication/components/web/LoginDialog.tsx index a6bc3425fbe68..26e634cefcbb1 100644 --- a/react/features/authentication/components/web/LoginDialog.tsx +++ b/react/features/authentication/components/web/LoginDialog.tsx @@ -10,10 +10,10 @@ import { translate, translateToHTML } from '../../../base/i18n/functions'; import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet'; import Dialog from '../../../base/ui/components/web/Dialog'; import Input from '../../../base/ui/components/web/Input'; -import { joinConference } from '../../../prejoin/actions.web'; import { authenticateAndUpgradeRole, - cancelLogin + cancelLogin, + sumbitConnectionCredentials } from '../../actions.web'; /** @@ -134,9 +134,7 @@ class LoginDialog extends Component { if (conference) { dispatch(authenticateAndUpgradeRole(jid, password, conference)); } else { - // dispatch(connect(jid, password)); - // FIXME: Workaround for the web version. To be removed once we get rid of conference.js - dispatch(joinConference(undefined, false, jid, password)); + dispatch(sumbitConnectionCredentials(jid, password)); } } diff --git a/react/features/authentication/middleware.ts b/react/features/authentication/middleware.ts index 75a4597d5c7a3..9fb1a834c4659 100644 --- a/react/features/authentication/middleware.ts +++ b/react/features/authentication/middleware.ts @@ -13,6 +13,7 @@ import { JitsiConferenceErrors, JitsiConnectionErrors } from '../base/lib-jitsi-meet'; +import { setInitialGUMPromise } from '../base/media/actions'; import { MEDIA_TYPE } from '../base/media/constants'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { isLocalTrackMuted } from '../base/tracks/functions.any'; @@ -153,6 +154,8 @@ MiddlewareRegistry.register(store => next => action => { error.recoverable = true; _handleLogin(store); + } else { + store.dispatch(setInitialGUMPromise()); } break; @@ -264,6 +267,8 @@ function _handleLogin({ dispatch, getState }: IStore) { const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO); if (!room) { + dispatch(setInitialGUMPromise()); + logger.warn('Cannot handle login, room is undefined!'); return; @@ -275,6 +280,8 @@ function _handleLogin({ dispatch, getState }: IStore) { return; } + dispatch(setInitialGUMPromise()); + getTokenAuthUrl( config, locationURL, diff --git a/react/features/base/media/actionTypes.ts b/react/features/base/media/actionTypes.ts index bf5ebd82b4c2d..af5e6ec84cb73 100644 --- a/react/features/base/media/actionTypes.ts +++ b/react/features/base/media/actionTypes.ts @@ -51,6 +51,16 @@ export const SET_AUDIO_UNMUTE_PERMISSIONS = 'SET_AUDIO_UNMUTE_PERMISSIONS'; */ export const SET_CAMERA_FACING_MODE = 'SET_CAMERA_FACING_MODE'; +/** + * Sets the initial GUM promise. + * + * { + * type: SET_INITIAL_GUM_PROMISE, + * promise: Promise + * }} + */ +export const SET_INITIAL_GUM_PROMISE = 'SET_INITIAL_GUM_PROMISE'; + /** * The type of (redux) action to set the muted state of the local screenshare. * diff --git a/react/features/base/media/actions.ts b/react/features/base/media/actions.ts index b53dff117827b..7dee484f858fc 100644 --- a/react/features/base/media/actions.ts +++ b/react/features/base/media/actions.ts @@ -9,6 +9,7 @@ import { SET_AUDIO_MUTED, SET_AUDIO_UNMUTE_PERMISSIONS, SET_CAMERA_FACING_MODE, + SET_INITIAL_GUM_PROMISE, SET_SCREENSHARE_MUTED, SET_VIDEO_AVAILABLE, SET_VIDEO_MUTED, @@ -93,6 +94,22 @@ export function setCameraFacingMode(cameraFacingMode: string) { }; } +/** + * Sets the initial GUM promise. + * + * @param {Promise> | undefined} promise - The promise. + * @returns {{ + * type: SET_INITIAL_GUM_PROMISE, + * promise: Promise + * }} + */ +export function setInitialGUMPromise(promise?: Promise>) { + return { + type: SET_INITIAL_GUM_PROMISE, + promise + }; +} + /** * Action to set the muted state of the local screenshare. * diff --git a/react/features/base/media/reducer.ts b/react/features/base/media/reducer.ts index b0a5296df8023..99cd6e73184e8 100644 --- a/react/features/base/media/reducer.ts +++ b/react/features/base/media/reducer.ts @@ -10,6 +10,7 @@ import { SET_AUDIO_MUTED, SET_AUDIO_UNMUTE_PERMISSIONS, SET_CAMERA_FACING_MODE, + SET_INITIAL_GUM_PROMISE, SET_SCREENSHARE_MUTED, SET_VIDEO_AVAILABLE, SET_VIDEO_MUTED, @@ -87,6 +88,30 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi } } +/** + * The initial common media state. + */ +const _COMMON_INITIAL_STATE = {}; + +/** + * Reducer fot the common properties in media state. + * + * @param {ICommonState} state - Common media state. + * @param {Object} action - Action object. + * @param {string} action.type - Type of action. + * @returns {ICommonState} + */ +function _common(state: ICommonState = _COMMON_INITIAL_STATE, action: AnyAction) { + if (action.type === SET_INITIAL_GUM_PROMISE) { + return { + ...state, + initialGUMPromise: action.promise + }; + } + + return state; +} + /** * Media state object for local screenshare. * @@ -247,6 +272,10 @@ interface IAudioState { unmuteBlocked: boolean; } +interface ICommonState { + initialGUMPromise?: Promise>; +} + interface IScreenshareState { available: boolean; muted: number; @@ -264,6 +293,7 @@ interface IVideoState { export interface IMediaState { audio: IAudioState; + common: ICommonState; screenshare: IScreenshareState; video: IVideoState; } @@ -280,6 +310,7 @@ export interface IMediaState { */ ReducerRegistry.register('features/base/media', combineReducers({ audio: _audio, + common: _common, screenshare: _screenshare, video: _video }));