diff --git a/conference.js b/conference.js index 836015d1d..a98d5e610 100644 --- a/conference.js +++ b/conference.js @@ -115,7 +115,7 @@ import { submitFeedback } from './react/features/feedback'; 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 { initPrejoin, @@ -502,6 +502,11 @@ export default { ); } + JitsiMeetJS.mediaDevices.addEventListener( + JitsiMediaDevicesEvents.SLOW_GET_USER_MEDIA, + () => APP.store.dispatch(toggleSlowGUMOverlay(true)) + ); + let tryCreateLocalTracks; // 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({ devices: [ 'audio' ], - timeout - }, true) + timeout, + firePermissionPromptIsShownEvent: true, + fireSlowPromiseEvent: true + }) .then(([ audioStream ]) => [ desktopStream, audioStream ]) .catch(error => { @@ -536,8 +543,10 @@ export default { return requestedAudio ? createLocalTracksF({ devices: [ 'audio' ], - timeout - }, true) + timeout, + firePermissionPromptIsShownEvent: true, + fireSlowPromiseEvent: true + }) : []; }) .catch(error => { @@ -551,8 +560,10 @@ export default { } else { tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices, - timeout - }, true) + timeout, + firePermissionPromptIsShownEvent: true, + fireSlowPromiseEvent: true + }) .catch(err => { if (requestedAudio && requestedVideo) { @@ -574,8 +585,10 @@ export default { return ( createLocalTracksF({ devices: [ 'audio' ], - timeout - }, true)); + timeout, + firePermissionPromptIsShownEvent: true, + fireSlowPromiseEvent: true + })); } else if (requestedAudio && !requestedVideo) { errors.audioOnlyError = err; @@ -598,8 +611,9 @@ export default { return requestedVideo ? createLocalTracksF({ devices: [ 'video' ], - timeout - }, true) + firePermissionPromptIsShownEvent: true, + fireSlowPromiseEvent: true + }) : []; }) .catch(err => { @@ -619,6 +633,7 @@ export default { // the user inputs their credentials, but the dialog would be // overshadowed by the overlay. tryCreateLocalTracks.then(tracks => { + APP.store.dispatch(toggleSlowGUMOverlay(false)); APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false)); return tracks; @@ -882,7 +897,7 @@ export default { showUI && APP.store.dispatch(notifyMicError(error)); }; - createLocalTracksF({ devices: [ 'audio' ] }, false) + createLocalTracksF({ devices: [ 'audio' ] }) .then(([ audioTrack ]) => audioTrack) .catch(error => { maybeShowErrorDialog(error); @@ -996,7 +1011,7 @@ export default { // // FIXME when local track creation is moved to react/redux // it should take care of the use case described above - createLocalTracksF({ devices: [ 'video' ] }, false) + createLocalTracksF({ devices: [ 'video' ] }) .then(([ videoTrack ]) => videoTrack) .catch(error => { // FIXME should send some feedback to the API on error ? diff --git a/css/overlay/_overlay.scss b/css/overlay/_overlay.scss index 735c265e9..777c814d5 100644 --- a/css/overlay/_overlay.scss +++ b/css/overlay/_overlay.scss @@ -33,4 +33,12 @@ bottom: 24px; width: 100%; } + + &__spinner-container { + display: flex; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + } } diff --git a/package-lock.json b/package-lock.json index 5a933857a..89b25f048 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10343,8 +10343,8 @@ } }, "lib-jitsi-meet": { - "version": "github:jitsi/lib-jitsi-meet#7f919faaccb268ef307d619992260919a6535e95", - "from": "github:jitsi/lib-jitsi-meet#7f919faaccb268ef307d619992260919a6535e95", + "version": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056", + "from": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056", "requires": { "@jitsi/js-utils": "1.0.2", "@jitsi/sdp-interop": "1.0.3", diff --git a/package.json b/package.json index 316644c98..5a68fab0d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "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", "lodash": "4.17.19", "moment": "2.19.4", diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index bee9a0bc6..6fdb0d70e 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -127,7 +127,6 @@ export function createLocalTracksA(options = {}) { options.facingMode || CAMERA_FACING_MODE.USER, micDeviceId: options.micDeviceId }, - /* firePermissionPromptIsShownEvent */ false, store) .then( localTracks => { diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index 4a6c55006..31c45d061 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -63,16 +63,25 @@ export async function createLocalPresenterTrack(options, desktopHeight) { * @param {string|null} [options.micDeviceId] - Microphone device id or * {@code undefined} to use app's settings. * @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 * 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 * is to execute and from which state such as {@code config} is to be retrieved. * @returns {Promise} */ -export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) { +export function createLocalTracksF(options = {}, store) { let { cameraDeviceId, micDeviceId } = options; - const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options; + const { + desktopSharingSourceDevice, + desktopSharingSources, + firePermissionPromptIsShownEvent, + fireSlowPromiseEvent, + timeout + } = options; if (typeof APP !== 'undefined') { // 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), effects, firefox_fake_device, // eslint-disable-line camelcase + firePermissionPromptIsShownEvent, + fireSlowPromiseEvent, micDeviceId, resolution, timeout - }, - firePermissionPromptIsShownEvent) + }) .catch(err => { logger.error('Failed to create local tracks', options.devices, err); @@ -161,7 +171,10 @@ export function createPrejoinTracks() { // Resolve with no tracks tryCreateLocalTracks = Promise.resolve([]); } else { - tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true) + tryCreateLocalTracks = createLocalTracksF({ + devices: initialDevices, + firePermissionPromptIsShownEvent: true + }) .catch(err => { if (requestedAudio && requestedVideo) { @@ -169,7 +182,10 @@ export function createPrejoinTracks() { errors.audioAndVideoError = err; return ( - createLocalTracksF({ devices: [ 'audio' ] }, true)); + createLocalTracksF({ + devices: [ 'audio' ], + firePermissionPromptIsShownEvent: true + })); } else if (requestedAudio && !requestedVideo) { errors.audioOnlyError = err; @@ -190,7 +206,10 @@ export function createPrejoinTracks() { // Try video only... return requestedVideo - ? createLocalTracksF({ devices: [ 'video' ] }, true) + ? createLocalTracksF({ + devices: [ 'video' ], + firePermissionPromptIsShownEvent: true + }) : []; }) .catch(err => { diff --git a/react/features/overlay/actionTypes.js b/react/features/overlay/actionTypes.js index 8d90df09a..efbfb980f 100644 --- a/react/features/overlay/actionTypes.js +++ b/react/features/overlay/actionTypes.js @@ -14,6 +14,17 @@ export const 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 * action methods's description for more info about each of the fields. diff --git a/react/features/overlay/actions.js b/react/features/overlay/actions.js index 7ae3d5470..4be57f307 100644 --- a/react/features/overlay/actions.js +++ b/react/features/overlay/actions.js @@ -2,7 +2,8 @@ import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, - SET_FATAL_ERROR + SET_FATAL_ERROR, + TOGGLE_SLOW_GUM_OVERLAY } 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 * screen will be displayed or hidden. diff --git a/react/features/overlay/components/web/AbstractSlowGUMOverlay.js b/react/features/overlay/components/web/AbstractSlowGUMOverlay.js new file mode 100644 index 000000000..a6f35c333 --- /dev/null +++ b/react/features/overlay/components/web/AbstractSlowGUMOverlay.js @@ -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 { + /** + * 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; + } +} diff --git a/react/features/overlay/components/web/SlowGUMOverlay.js b/react/features/overlay/components/web/SlowGUMOverlay.js new file mode 100644 index 000000000..08baf953a --- /dev/null +++ b/react/features/overlay/components/web/SlowGUMOverlay.js @@ -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 ( + +
+ +
+
+ ); + } +} + +export default translate(SlowGUMOverlay); diff --git a/react/features/overlay/components/web/index.js b/react/features/overlay/components/web/index.js index f29b68547..0a96e7519 100644 --- a/react/features/overlay/components/web/index.js +++ b/react/features/overlay/components/web/index.js @@ -5,3 +5,4 @@ export { default as OverlayFrame } from './OverlayFrame'; export { default as PageReloadOverlay } from './PageReloadOverlay'; export { default as SuspendedOverlay } from './SuspendedOverlay'; export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay'; +export { default as SlowGUMOverlay } from './SlowGUMOverlay'; diff --git a/react/features/overlay/overlays.web.js b/react/features/overlay/overlays.web.js index 442681a65..f22029d62 100644 --- a/react/features/overlay/overlays.web.js +++ b/react/features/overlay/overlays.web.js @@ -2,6 +2,7 @@ import { PageReloadOverlay, + SlowGUMOverlay, SuspendedOverlay, UserMediaPermissionsOverlay } from './components/web'; @@ -17,6 +18,7 @@ export function getOverlays(): Array { return [ PageReloadOverlay, SuspendedOverlay, - UserMediaPermissionsOverlay + UserMediaPermissionsOverlay, + SlowGUMOverlay ]; } diff --git a/react/features/overlay/reducer.js b/react/features/overlay/reducer.js index 0b6370034..61d400508 100644 --- a/react/features/overlay/reducer.js +++ b/react/features/overlay/reducer.js @@ -5,7 +5,8 @@ import { assign, ReducerRegistry, set } from '../base/redux'; import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, - SET_FATAL_ERROR + SET_FATAL_ERROR, + TOGGLE_SLOW_GUM_OVERLAY } from './actionTypes'; /** @@ -28,6 +29,8 @@ ReducerRegistry.register('features/overlay', (state = { }, action) => { case SET_FATAL_ERROR: return _setFatalError(state, action); + case TOGGLE_SLOW_GUM_OVERLAY: + return _toggleSlowGUMOverlay(state, action); } 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. *