Compare commits

...

2 Commits

Author SHA1 Message Date
Hristo Terezov
37c68b77f3 ref(web): startConference and initial GUM tracks management. 2024-06-14 11:25:34 +03:00
Hristo Terezov
e18ffa07f2 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.
2024-06-14 11:10:42 +03:00
10 changed files with 197 additions and 95 deletions

View File

@@ -83,6 +83,7 @@ import {
setAudioAvailable,
setAudioMuted,
setAudioUnmutePermissions,
setInitialGUMPromise,
setVideoAvailable,
setVideoMuted,
setVideoUnmutePermissions
@@ -591,7 +592,7 @@ export default {
const handleInitialTracks = (options, tracks) => {
let localTracks = tracks;
if (options.startWithAudioMuted || room?.isStartAudioMuted()) {
if (options.startWithAudioMuted) {
// Always add the track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted, i.e., if there is no local media capture.
if (browser.isWebKitBased()) {
@@ -600,58 +601,38 @@ export default {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
}
}
if (room?.isStartVideoMuted()) {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.VIDEO);
}
return localTracks;
};
if (isPrejoinPageVisible(state)) {
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
const localTracks = await tryCreateLocalTracks;
// Initialize device list a second time to ensure device labels get populated in case of an initial gUM
// acceptance; otherwise they may remain as empty strings.
this._initDeviceList(true);
if (isPrejoinPageVisible(state)) {
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
return APP.store.dispatch(initPrejoin(localTracks, errors));
}
logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
const tracks = handleInitialTracks(initialOptions, localTracks);
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
return this._setLocalAudioVideoStreams(tracks);
}
const { dispatch, getState } = APP.store;
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
return Promise.all([
tryCreateLocalTracks.then(tr => {
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
dispatch(setInitialGUMPromise(tryCreateLocalTracks.then(async tr => {
const tracks = handleInitialTracks(initialOptions, tr);
return tr;
}).then(tr => {
this._initDeviceList(true);
this._initDeviceList(true);
const filteredTracks = handleInitialTracks(initialOptions, tr);
if (isPrejoinPageVisible(getState())) {
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
dispatch(setInitialGUMPromise());
setGUMPendingStateOnFailedTracks(filteredTracks, APP.store.dispatch);
// Note: Not sure if initPrejoin needs to be async. But let's wait for it just to be sure the
// tracks are added.
await dispatch(initPrejoin(tracks, errors));
} else {
this._displayErrorsForCreateInitialLocalTracks(errors);
setGUMPendingStateOnFailedTracks(tracks);
}
return filteredTracks;
}),
APP.store.dispatch(connect())
]).then(([ tracks, _ ]) => {
this.startConference(tracks).catch(logger.error);
});
return {
tracks,
errors
};
})));
if (!isPrejoinPageVisible(getState())) {
dispatch(connect());
}
},
/**

View File

@@ -5,12 +5,12 @@ import { connect as reduxConnect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { IJitsiConference } from '../../../base/conference/reducer';
import { IConfig } from '../../../base/config/configType';
import { connect } from '../../../base/connection/actions.web';
import { toJid } from '../../../base/connection/functions';
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
@@ -134,9 +134,7 @@ class LoginDialog extends Component<IProps, IState> {
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(connect(jid, password));
}
}

View File

@@ -1,3 +1,5 @@
import { batch } from 'react-redux';
import { IStore } from '../app/types';
import { APP_WILL_NAVIGATE } from '../base/app/actionTypes';
import {
@@ -13,7 +15,9 @@ import {
JitsiConferenceErrors,
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import { gumPending, setInitialGUMPromise } from '../base/media/actions';
import { MEDIA_TYPE } from '../base/media/constants';
import { IGUMPendingState } from '../base/media/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { isLocalTrackMuted } from '../base/tracks/functions.any';
import { parseURIString } from '../base/util/uri';
@@ -143,7 +147,8 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_FAILED: {
const { error } = action;
const state = store.getState();
const { dispatch, getState } = store;
const state = getState();
const { jwt } = state['features/base/jwt'];
if (error
@@ -153,6 +158,11 @@ MiddlewareRegistry.register(store => next => action => {
error.recoverable = true;
_handleLogin(store);
} else {
batch(() => {
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
dispatch(setInitialGUMPromise());
});
}
break;
@@ -264,6 +274,11 @@ function _handleLogin({ dispatch, getState }: IStore) {
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
if (!room) {
batch(() => {
dispatch(setInitialGUMPromise());
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
});
logger.warn('Cannot handle login, room is undefined!');
return;
@@ -275,6 +290,8 @@ function _handleLogin({ dispatch, getState }: IStore) {
return;
}
dispatch(setInitialGUMPromise());
getTokenAuthUrl(
config,
locationURL,

View File

@@ -24,7 +24,7 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
import { iAmVisitor } from '../../visitors/functions';
import { overwriteConfig } from '../config/actions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
import { connect, connectionDisconnected, disconnect } from '../connection/actions';
import { connectionDisconnected, disconnect } from '../connection/actions';
import { validateJwt } from '../jwt/functions';
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
@@ -37,7 +37,6 @@ import {
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
import { getLocalTracks } from '../tracks/functions.any';
import {
CONFERENCE_FAILED,
@@ -208,17 +207,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(() => dispatch(conferenceWillLeave(conference)))
.then(() => conference.leave())
.then(() => dispatch(disconnect()))
.then(() => dispatch(connect()))
.then(() => {
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
const localTracks = getLocalTracks(getState()['features/base/tracks']);
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
APP.conference.startConference(jitsiTracks).catch(logger.error);
}
});
.then(() => dispatch(disconnect()));
}
break;

View File

@@ -4,8 +4,14 @@ import {
setPrejoinPageVisibility,
setSkipPrejoinOnReload
} from '../../prejoin/actions.web';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { iAmVisitor } from '../../visitors/functions';
import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED } from '../connection/actionTypes';
import { hangup } from '../connection/actions.web';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { gumPending, setInitialGUMPromise } from '../media/actions';
import { MEDIA_TYPE } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import {
@@ -131,6 +137,39 @@ MiddlewareRegistry.register(store => next => action => {
releaseScreenLock();
break;
case CONNECTION_DISCONNECTED: {
const { initialGUMPromise } = getState()['features/base/media'].common;
if (initialGUMPromise) {
store.dispatch(setInitialGUMPromise());
}
break;
}
case CONNECTION_ESTABLISHED: {
const state = getState();
if (!isPrejoinPageVisible(state)) {
const { initialGUMPromise = Promise.resolve({ tracks: [] }) } = state['features/base/media'].common;
initialGUMPromise.then(({ tracks }) => {
let tracksToUse = tracks ?? [];
if (iAmVisitor(getState())) {
tracksToUse = [];
tracks.forEach(track => track.dispose().catch(logger.error));
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
}
dispatch(setInitialGUMPromise());
return APP.conference.startConference(tracksToUse);
})
.catch(logger.error);
}
break;
}
}
return next(action);

View File

@@ -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.
*

View File

@@ -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<Array<Object>> | undefined} promise - The promise.
* @returns {{
* type: SET_INITIAL_GUM_PROMISE,
* promise: Promise
* }}
*/
export function setInitialGUMPromise(promise?: Promise<{ errors: any; tracks: Array<any>; }>) {
return {
type: SET_INITIAL_GUM_PROMISE,
promise
};
}
/**
* Action to set the muted state of the local screenshare.
*

View File

@@ -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,13 @@ interface IAudioState {
unmuteBlocked: boolean;
}
interface ICommonState {
initialGUMPromise?: Promise<{
errors: any;
tracks: Array<any>;
}>;
}
interface IScreenshareState {
available: boolean;
muted: number;
@@ -264,6 +296,7 @@ interface IVideoState {
export interface IMediaState {
audio: IAudioState;
common: ICommonState;
screenshare: IScreenshareState;
video: IVideoState;
}
@@ -280,6 +313,7 @@ export interface IMediaState {
*/
ReducerRegistry.register<IMediaState>('features/base/media', combineReducers({
audio: _audio,
common: _common,
screenshare: _screenshare,
video: _video
}));

View File

@@ -4,16 +4,13 @@ import { IStore } from '../app/types';
import { updateConfig } from '../base/config/actions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { connect } from '../base/connection/actions';
import { browser } from '../base/lib-jitsi-meet';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import { MEDIA_TYPE } from '../base/media/constants';
import { isVideoMutedByUser } from '../base/media/functions';
import { updateSettings } from '../base/settings/actions';
import { replaceLocalTrack, trackAdded } from '../base/tracks/actions';
import {
createLocalTracksF,
getLocalAudioTrack,
getLocalTracks,
getLocalVideoTrack
} from '../base/tracks/functions';
import { openURLInBrowser } from '../base/util/openURLInBrowser';
@@ -230,35 +227,7 @@ export function joinConference(options?: Object, ignoreJoiningInProgress = false
options && dispatch(updateConfig(options));
dispatch(connect(jid, password)).then(async () => {
// TODO keep this here till we move tracks and conference management from
// conference.js to react.
const state = getState();
let localTracks = getLocalTracks(state['features/base/tracks']);
// Do not signal audio/video tracks if the user joins muted.
for (const track of localTracks) {
// Always add the audio track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted.
if (track.muted && !(browser.isWebKitBased() && track.jitsiTrack
&& track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) {
try {
await dispatch(replaceLocalTrack(track.jitsiTrack, null));
} catch (error) {
logger.error(`Failed to replace local track (${track.jitsiTrack}) with null: ${error}`);
}
}
}
// Re-fetch the local tracks after muted tracks have been removed above.
// This is needed, because the tracks are effectively disposed by the replaceLocalTrack and should not be
// used anymore.
localTracks = getLocalTracks(getState()['features/base/tracks']);
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
APP.conference.startConference(jitsiTracks).catch(logger.error);
})
dispatch(connect(jid, password))
.catch(() => {
// There is nothing to do here. This is handled and dispatched in base/connection/actions.
});

View File

@@ -2,14 +2,19 @@ import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { browser } from '../base/lib-jitsi-meet';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media/actionTypes';
import { MEDIA_TYPE } from '../base/media/constants';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { updateSettings } from '../base/settings/actions';
import {
TRACK_ADDED,
TRACK_NO_DATA_FROM_SOURCE
} from '../base/tracks/actionTypes';
import { replaceLocalTrack } from '../base/tracks/actions.any';
import { getLocalTracks } from '../base/tracks/functions.any';
import { iAmVisitor } from '../visitors/functions';
import {
setDeviceStatusOk,
@@ -17,6 +22,7 @@ import {
setJoiningInProgress
} from './actions';
import { isPrejoinPageVisible } from './functions';
import logger from './logger';
/**
* The redux middleware for {@link PrejoinPage}.
@@ -26,6 +32,48 @@ import { isPrejoinPageVisible } from './functions';
*/
MiddlewareRegistry.register(store => next => async action => {
switch (action.type) {
case CONNECTION_ESTABLISHED: {
const { dispatch, getState } = store;
const result = next(action);
if (isPrejoinPageVisible(getState())) {
const { initialGUMPromise = Promise.resolve() } = getState()['features/base/media'].common;
initialGUMPromise.then(() => {
const state = getState();
let localTracks = getLocalTracks(state['features/base/tracks']);
const trackReplacePromises = [];
// Do not signal audio/video tracks if the user joins muted.
for (const track of localTracks) {
// Always add the audio track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted.
if ((track.muted && !(browser.isWebKitBased() && track.jitsiTrack
&& track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) || iAmVisitor(state)) {
trackReplacePromises.push(dispatch(replaceLocalTrack(track.jitsiTrack, null))
.catch((error: any) => {
logger.error(`Failed to replace local track (${track.jitsiTrack}) with null: ${error}`);
}));
}
}
Promise.allSettled(trackReplacePromises).then(() => {
// Re-fetch the local tracks after muted tracks have been removed above.
// This is needed, because the tracks are effectively disposed by the replaceLocalTrack and should
// not be used anymore.
localTracks = getLocalTracks(getState()['features/base/tracks']);
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
return APP.conference.startConference(jitsiTracks);
});
});
}
return result;
}
case SET_AUDIO_MUTED: {
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(updateSettings({