diff --git a/conference.js b/conference.js index 92c54c5ce..a09bc5bc7 100644 --- a/conference.js +++ b/conference.js @@ -101,6 +101,7 @@ import { createLocalTracksF, destroyLocalTracks, isLocalTrackMuted, + isUserInteractionRequiredForUnmute, replaceLocalTrack, trackAdded, trackRemoved @@ -663,8 +664,11 @@ export default { options.roomName, { startAudioOnly: config.startAudioOnly, startScreenSharing: config.startScreenSharing, - startWithAudioMuted: config.startWithAudioMuted || config.startSilent, + startWithAudioMuted: config.startWithAudioMuted + || config.startSilent + || isUserInteractionRequiredForUnmute(APP.store.getState()), startWithVideoMuted: config.startWithVideoMuted + || isUserInteractionRequiredForUnmute(APP.store.getState()) })) .then(([ tracks, con ]) => { tracks.forEach(track => { @@ -765,6 +769,13 @@ export default { * dialogs in case of media permissions error. */ muteAudio(mute, showUI = true) { + if (!mute + && isUserInteractionRequiredForUnmute(APP.store.getState())) { + logger.error('Unmuting audio requires user interaction'); + + return; + } + // Not ready to modify track's state yet if (!this._localTracksInitialized) { // This will only modify base/media.audio.muted which is then synced @@ -828,6 +839,13 @@ export default { * dialogs in case of media permissions error. */ muteVideo(mute, showUI = true) { + if (!mute + && isUserInteractionRequiredForUnmute(APP.store.getState())) { + logger.error('Unmuting video requires user interaction'); + + return; + } + // If not ready to modify track's state yet adjust the base/media if (!this._localTracksInitialized) { // This will only modify base/media.video.muted which is then synced diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index dc3034829..a1320eecc 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -4,6 +4,7 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme'; import React from 'react'; import { DialogContainer } from '../../base/dialog'; +import '../../base/user-interaction'; import '../../base/responsive-ui'; import '../../chat'; import '../../external-api'; diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index da437b823..ea4784d6c 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -228,6 +228,25 @@ export function isRemoteTrackMuted(tracks, mediaType, participantId) { return !track || track.muted; } +/** + * Returns whether or not the current environment needs a user interaction with + * the page before any unmute can occur. + * + * @param {Object} state - The redux state. + * @returns {boolean} + */ +export function isUserInteractionRequiredForUnmute(state) { + const { browser } = JitsiMeetJS.util; + + return !browser.isReactNative() + && !browser.isChrome() + && !browser.isChromiumBased() + && !browser.isElectron() + && window + && window.self !== window.top + && !state['features/base/user-interaction'].interacted; +} + /** * Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of * the specified {@code track} is already in accord with the specified diff --git a/react/features/base/tracks/middleware.js b/react/features/base/tracks/middleware.js index ad2b27be9..8dfa8adc2 100644 --- a/react/features/base/tracks/middleware.js +++ b/react/features/base/tracks/middleware.js @@ -24,7 +24,12 @@ import { TRACK_REMOVED, TRACK_UPDATED } from './actionTypes'; -import { getLocalTrack, getTrackByJitsiTrack, setTrackMuted } from './functions'; +import { + getLocalTrack, + getTrackByJitsiTrack, + isUserInteractionRequiredForUnmute, + setTrackMuted +} from './functions'; declare var APP: Object; @@ -50,6 +55,11 @@ MiddlewareRegistry.register(store => next => action => { break; } case SET_AUDIO_MUTED: + if (!action.muted + && isUserInteractionRequiredForUnmute(store.getState())) { + return; + } + _setMuted(store, action, MEDIA_TYPE.AUDIO); break; @@ -74,6 +84,11 @@ MiddlewareRegistry.register(store => next => action => { } case SET_VIDEO_MUTED: + if (!action.muted + && isUserInteractionRequiredForUnmute(store.getState())) { + return; + } + _setMuted(store, action, MEDIA_TYPE.VIDEO); break; diff --git a/react/features/base/user-interaction/actionTypes.js b/react/features/base/user-interaction/actionTypes.js new file mode 100644 index 000000000..29cb8bca4 --- /dev/null +++ b/react/features/base/user-interaction/actionTypes.js @@ -0,0 +1,20 @@ +/** + * The type of (redux) action which signals that an event listener has been + * added to listen for any user interaction with the page. + * + * { + * type: SET_USER_INTERACTION_LISTENER, + * userInteractionListener: Function + * } + */ +export const SET_USER_INTERACTION_LISTENER = 'SET_USER_INTERACTION_LISTENER'; + +/** + * The type of (redux) action which signals the user has interacted with the + * page. + * + * { + * type: USER_INTERACTION_RECEIVED, + * } + */ +export const USER_INTERACTION_RECEIVED = 'USER_INTERACTION_RECEIVED'; diff --git a/react/features/base/user-interaction/index.js b/react/features/base/user-interaction/index.js new file mode 100644 index 000000000..200d25492 --- /dev/null +++ b/react/features/base/user-interaction/index.js @@ -0,0 +1,2 @@ +import './middleware'; +import './reducer'; diff --git a/react/features/base/user-interaction/middleware.js b/react/features/base/user-interaction/middleware.js new file mode 100644 index 000000000..3080cc81d --- /dev/null +++ b/react/features/base/user-interaction/middleware.js @@ -0,0 +1,79 @@ +// @flow + +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { MiddlewareRegistry } from '../redux'; + +import { + SET_USER_INTERACTION_LISTENER, + USER_INTERACTION_RECEIVED +} from './actionTypes'; + +/** + * Implements the entry point of the middleware of the feature base/user-interaction. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case APP_WILL_MOUNT: + _startListeningForUserInteraction(store); + break; + + case APP_WILL_UNMOUNT: + case USER_INTERACTION_RECEIVED: + _stopListeningForUserInteraction(store); + break; + } + + return next(action); +}); + +/** + * Registers listeners to notify redux of any user interaction with the page. + * + * @param {Object} store - The redux store. + * @private + * @returns {void} + */ +function _startListeningForUserInteraction(store) { + const userInteractionListener = event => { + if (event.isTrusted) { + store.dispatch({ + type: USER_INTERACTION_RECEIVED + }); + + _stopListeningForUserInteraction(store); + } + }; + + window.addEventListener('mousedown', userInteractionListener); + window.addEventListener('keydown', userInteractionListener); + + store.dispatch({ + type: SET_USER_INTERACTION_LISTENER, + userInteractionListener + }); +} + +/** + * Un-registers listeners intended to notify when the user has interacted with + * the page. + * + * @param {Object} store - The redux store. + * @private + * @returns {void} + */ +function _stopListeningForUserInteraction({ getState, dispatch }) { + const { userInteractionListener } = getState()['features/base/app']; + + if (userInteractionListener) { + window.removeEventListener('mousedown', userInteractionListener); + window.removeEventListener('keydown', userInteractionListener); + + dispatch({ + type: SET_USER_INTERACTION_LISTENER, + userInteractionListener: undefined + }); + } +} diff --git a/react/features/base/user-interaction/reducer.js b/react/features/base/user-interaction/reducer.js new file mode 100644 index 000000000..4409907b9 --- /dev/null +++ b/react/features/base/user-interaction/reducer.js @@ -0,0 +1,34 @@ +// @flow + +import { ReducerRegistry } from '../redux'; + +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { + SET_USER_INTERACTION_LISTENER, + USER_INTERACTION_RECEIVED +} from './actionTypes'; + +ReducerRegistry.register('features/base/user-interaction', (state = {}, action) => { + switch (action.type) { + case APP_WILL_MOUNT: + case APP_WILL_UNMOUNT: { + return { + ...state, + interacted: false + }; + } + case SET_USER_INTERACTION_LISTENER: + return { + ...state, + userInteractionListener: action.userInteractionListener + }; + + case USER_INTERACTION_RECEIVED: + return { + ...state, + interacted: true + }; + } + + return state; +});