fix(gum) add event handling for SLOW_GET_USER_MEDIA

Show an overlay with a spinner when slow gUM is fired
This commit is contained in:
Tudor-Ovidiu Avram 2020-11-18 14:38:00 +02:00
parent 43761fc398
commit f50fd7b7bd
13 changed files with 192 additions and 28 deletions

View File

@ -115,7 +115,7 @@ import {
submitFeedback submitFeedback
} from './react/features/feedback'; } from './react/features/feedback';
import { showNotification } from './react/features/notifications'; import { showNotification } from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay'; import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor'; import { suspendDetected } from './react/features/power-monitor';
import { import {
initPrejoin, initPrejoin,
@ -502,6 +502,11 @@ export default {
); );
} }
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.SLOW_GET_USER_MEDIA,
() => APP.store.dispatch(toggleSlowGUMOverlay(true))
);
let tryCreateLocalTracks; let tryCreateLocalTracks;
// On Electron there is no permission prompt for granting permissions. That's why we don't need to // On Electron there is no permission prompt for granting permissions. That's why we don't need to
@ -519,8 +524,10 @@ export default {
return createLocalTracksF({ return createLocalTracksF({
devices: [ 'audio' ], devices: [ 'audio' ],
timeout timeout,
}, true) firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
.then(([ audioStream ]) => .then(([ audioStream ]) =>
[ desktopStream, audioStream ]) [ desktopStream, audioStream ])
.catch(error => { .catch(error => {
@ -536,8 +543,10 @@ export default {
return requestedAudio return requestedAudio
? createLocalTracksF({ ? createLocalTracksF({
devices: [ 'audio' ], devices: [ 'audio' ],
timeout timeout,
}, true) firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
: []; : [];
}) })
.catch(error => { .catch(error => {
@ -551,8 +560,10 @@ export default {
} else { } else {
tryCreateLocalTracks = createLocalTracksF({ tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices, devices: initialDevices,
timeout timeout,
}, true) firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
.catch(err => { .catch(err => {
if (requestedAudio && requestedVideo) { if (requestedAudio && requestedVideo) {
@ -574,8 +585,10 @@ export default {
return ( return (
createLocalTracksF({ createLocalTracksF({
devices: [ 'audio' ], devices: [ 'audio' ],
timeout timeout,
}, true)); firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
}));
} else if (requestedAudio && !requestedVideo) { } else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err; errors.audioOnlyError = err;
@ -598,8 +611,9 @@ export default {
return requestedVideo return requestedVideo
? createLocalTracksF({ ? createLocalTracksF({
devices: [ 'video' ], devices: [ 'video' ],
timeout firePermissionPromptIsShownEvent: true,
}, true) fireSlowPromiseEvent: true
})
: []; : [];
}) })
.catch(err => { .catch(err => {
@ -619,6 +633,7 @@ export default {
// the user inputs their credentials, but the dialog would be // the user inputs their credentials, but the dialog would be
// overshadowed by the overlay. // overshadowed by the overlay.
tryCreateLocalTracks.then(tracks => { tryCreateLocalTracks.then(tracks => {
APP.store.dispatch(toggleSlowGUMOverlay(false));
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false)); APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
return tracks; return tracks;
@ -882,7 +897,7 @@ export default {
showUI && APP.store.dispatch(notifyMicError(error)); showUI && APP.store.dispatch(notifyMicError(error));
}; };
createLocalTracksF({ devices: [ 'audio' ] }, false) createLocalTracksF({ devices: [ 'audio' ] })
.then(([ audioTrack ]) => audioTrack) .then(([ audioTrack ]) => audioTrack)
.catch(error => { .catch(error => {
maybeShowErrorDialog(error); maybeShowErrorDialog(error);
@ -996,7 +1011,7 @@ export default {
// //
// FIXME when local track creation is moved to react/redux // FIXME when local track creation is moved to react/redux
// it should take care of the use case described above // it should take care of the use case described above
createLocalTracksF({ devices: [ 'video' ] }, false) createLocalTracksF({ devices: [ 'video' ] })
.then(([ videoTrack ]) => videoTrack) .then(([ videoTrack ]) => videoTrack)
.catch(error => { .catch(error => {
// FIXME should send some feedback to the API on error ? // FIXME should send some feedback to the API on error ?

View File

@ -33,4 +33,12 @@
bottom: 24px; bottom: 24px;
width: 100%; width: 100%;
} }
&__spinner-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
} }

4
package-lock.json generated
View File

@ -10343,8 +10343,8 @@
} }
}, },
"lib-jitsi-meet": { "lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#7f919faaccb268ef307d619992260919a6535e95", "version": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
"from": "github:jitsi/lib-jitsi-meet#7f919faaccb268ef307d619992260919a6535e95", "from": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
"requires": { "requires": {
"@jitsi/js-utils": "1.0.2", "@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3", "@jitsi/sdp-interop": "1.0.3",

View File

@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1", "jquery-i18next": "1.2.1",
"js-md5": "0.6.1", "js-md5": "0.6.1",
"jwt-decode": "2.2.0", "jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#7f919faaccb268ef307d619992260919a6535e95", "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19", "lodash": "4.17.19",
"moment": "2.19.4", "moment": "2.19.4",

View File

@ -127,7 +127,6 @@ export function createLocalTracksA(options = {}) {
options.facingMode || CAMERA_FACING_MODE.USER, options.facingMode || CAMERA_FACING_MODE.USER,
micDeviceId: options.micDeviceId micDeviceId: options.micDeviceId
}, },
/* firePermissionPromptIsShownEvent */ false,
store) store)
.then( .then(
localTracks => { localTracks => {

View File

@ -63,16 +63,25 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
* @param {string|null} [options.micDeviceId] - Microphone device id or * @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings. * {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks. * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet * @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a * should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event. * corresponding event.
* @param {boolean} [options.fireSlowPromiseEvent] - Whether lib-jitsi-meet
* should check for a slow {@code getUserMedia} request and fire a
* corresponding event.
* @param {Object} store - The redux store in the context of which the function * @param {Object} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved. * is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>} * @returns {Promise<JitsiLocalTrack[]>}
*/ */
export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) { export function createLocalTracksF(options = {}, store) {
let { cameraDeviceId, micDeviceId } = options; let { cameraDeviceId, micDeviceId } = options;
const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options; const {
desktopSharingSourceDevice,
desktopSharingSources,
firePermissionPromptIsShownEvent,
fireSlowPromiseEvent,
timeout
} = options;
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the // TODO The app's settings should go in the redux store and then the
@ -114,11 +123,12 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
devices: options.devices.slice(0), devices: options.devices.slice(0),
effects, effects,
firefox_fake_device, // eslint-disable-line camelcase firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
fireSlowPromiseEvent,
micDeviceId, micDeviceId,
resolution, resolution,
timeout timeout
}, })
firePermissionPromptIsShownEvent)
.catch(err => { .catch(err => {
logger.error('Failed to create local tracks', options.devices, err); logger.error('Failed to create local tracks', options.devices, err);
@ -161,7 +171,10 @@ export function createPrejoinTracks() {
// Resolve with no tracks // Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]); tryCreateLocalTracks = Promise.resolve([]);
} else { } else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true) tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
})
.catch(err => { .catch(err => {
if (requestedAudio && requestedVideo) { if (requestedAudio && requestedVideo) {
@ -169,7 +182,10 @@ export function createPrejoinTracks() {
errors.audioAndVideoError = err; errors.audioAndVideoError = err;
return ( return (
createLocalTracksF({ devices: [ 'audio' ] }, true)); createLocalTracksF({
devices: [ 'audio' ],
firePermissionPromptIsShownEvent: true
}));
} else if (requestedAudio && !requestedVideo) { } else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err; errors.audioOnlyError = err;
@ -190,7 +206,10 @@ export function createPrejoinTracks() {
// Try video only... // Try video only...
return requestedVideo return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true) ? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true
})
: []; : [];
}) })
.catch(err => { .catch(err => {

View File

@ -14,6 +14,17 @@
export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED
= 'MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED'; = 'MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED';
/**
* The type of the Redux action which signals that the overlay for slow gUM is visible or not.
*
* {
* type: TOGGLE_SLOW_GUM_OVERLAY,
* isVisible: {boolean},
* }
* @public
*/
export const TOGGLE_SLOW_GUM_OVERLAY = 'TOGGLE_SLOW_GUM_OVERLAY';
/** /**
* Adjust the state of the fatal error which shows/hides the reload screen. See * Adjust the state of the fatal error which shows/hides the reload screen. See
* action methods's description for more info about each of the fields. * action methods's description for more info about each of the fields.

View File

@ -2,7 +2,8 @@
import { import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR SET_FATAL_ERROR,
TOGGLE_SLOW_GUM_OVERLAY
} from './actionTypes'; } from './actionTypes';
/** /**
@ -26,6 +27,24 @@ export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, brows
}; };
} }
/**
* Signals that the prompt for media permission is visible or not.
*
* @param {boolean} isVisible - If the value is true - the prompt for media
* permission is visible otherwise the value is false/undefined.
* @public
* @returns {{
* type: SLOW_GET_USER_MEDIA_OVERLAY,
* isVisible: {boolean}
* }}
*/
export function toggleSlowGUMOverlay(isVisible: boolean) {
return {
type: TOGGLE_SLOW_GUM_OVERLAY,
isVisible
};
}
/** /**
* The action indicates that an unrecoverable error has occurred and the reload * The action indicates that an unrecoverable error has occurred and the reload
* screen will be displayed or hidden. * screen will be displayed or hidden.

View File

@ -0,0 +1,33 @@
// @flow
import { Component } from 'react';
/**
* The type of the React {@code Component} props of
* {@link AbstractSlowGUMOverlay}.
*/
type Props = {
/**
* The function to translate human-readable text.
*/
t: Function
};
/**
* Implements a React {@link Component} for slow gUM overlay. Shown when
* a slow gUM promise resolution is detected
*/
export default class AbstractSlowGUMOverlay extends Component<Props> {
/**
* Determines whether this overlay needs to be rendered (according to a
* specific redux state). Called by {@link OverlayContainer}.
*
* @param {Object} state - The redux state.
* @returns {boolean} - If this overlay needs to be rendered, {@code true};
* {@code false}, otherwise.
*/
static needsRender(state: Object) {
return state['features/overlay'].isSlowGUMOverlayVisible;
}
}

View File

@ -0,0 +1,36 @@
// @flow
import Spinner from '@atlaskit/spinner';
import React from 'react';
import { translate } from '../../../base/i18n';
import AbstractSlowGUMOverlay from './AbstractSlowGUMOverlay';
import OverlayFrame from './OverlayFrame';
/**
* Implements a React {@link Component} for slow gUM overlay. Shown when
* a slow gUM promise resolution is detected
*/
class SlowGUMOverlay extends AbstractSlowGUMOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// const { t } = this.props;
return (
<OverlayFrame>
<div className = { 'overlay__spinner-container' }>
<Spinner
invertColor = { true }
size = { 'large' } />
</div>
</OverlayFrame>
);
}
}
export default translate(SlowGUMOverlay);

View File

@ -5,3 +5,4 @@ export { default as OverlayFrame } from './OverlayFrame';
export { default as PageReloadOverlay } from './PageReloadOverlay'; export { default as PageReloadOverlay } from './PageReloadOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay'; export { default as SuspendedOverlay } from './SuspendedOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay'; export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';
export { default as SlowGUMOverlay } from './SlowGUMOverlay';

View File

@ -2,6 +2,7 @@
import { import {
PageReloadOverlay, PageReloadOverlay,
SlowGUMOverlay,
SuspendedOverlay, SuspendedOverlay,
UserMediaPermissionsOverlay UserMediaPermissionsOverlay
} from './components/web'; } from './components/web';
@ -17,6 +18,7 @@ export function getOverlays(): Array<Object> {
return [ return [
PageReloadOverlay, PageReloadOverlay,
SuspendedOverlay, SuspendedOverlay,
UserMediaPermissionsOverlay UserMediaPermissionsOverlay,
SlowGUMOverlay
]; ];
} }

View File

@ -5,7 +5,8 @@ import { assign, ReducerRegistry, set } from '../base/redux';
import { import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR SET_FATAL_ERROR,
TOGGLE_SLOW_GUM_OVERLAY
} from './actionTypes'; } from './actionTypes';
/** /**
@ -28,6 +29,8 @@ ReducerRegistry.register('features/overlay', (state = { }, action) => {
case SET_FATAL_ERROR: case SET_FATAL_ERROR:
return _setFatalError(state, action); return _setFatalError(state, action);
case TOGGLE_SLOW_GUM_OVERLAY:
return _toggleSlowGUMOverlay(state, action);
} }
return state; return state;
@ -52,6 +55,24 @@ function _mediaPermissionPromptVisibilityChanged(
}); });
} }
/**
* Reduces a specific redux action TOGGLE_SLOW_GUM_OVERLAY of
* the feature overlay.
*
* @param {Object} state - The redux state of the feature overlay.
* @param {Action} action - The redux action to reduce.
* @private
* @returns {Object} The new state of the feature overlay after the reduction of
* the specified action.
*/
function _toggleSlowGUMOverlay(
state,
{ isVisible }) {
return assign(state, {
isSlowGUMOverlayVisible: isVisible
});
}
/** /**
* Sets the {@code LoadConfigOverlay} overlay visible or not. * Sets the {@code LoadConfigOverlay} overlay visible or not.
* *