diff --git a/conference.js b/conference.js index 5f7d71f7f..cd80083df 100644 --- a/conference.js +++ b/conference.js @@ -1345,8 +1345,7 @@ export default { * @returns {boolean} */ isAudioOnly() { - return Boolean( - APP.store.getState()['features/base/conference'].audioOnly); + return Boolean(APP.store.getState()['features/base/audio-only'].enabled); }, videoSwitchInProgress: false, diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index b81461caf..fc236c6bb 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -7,8 +7,8 @@ import { toURLString } from '../../base/util'; import '../../follow-me'; import { OverlayContainer } from '../../overlay'; -// Enable rejoin analytics -import '../../rejoin'; +import '../../base/lastn'; // Register lastN middleware +import '../../rejoin'; // Enable rejoin analytics import { appNavigate } from '../actions'; import { getDefaultURL } from '../functions'; diff --git a/react/features/base/audio-only/actionTypes.js b/react/features/base/audio-only/actionTypes.js new file mode 100644 index 000000000..15c578979 --- /dev/null +++ b/react/features/base/audio-only/actionTypes.js @@ -0,0 +1,12 @@ +// @flow + +/** + * The type of (redux) action which sets the audio-only flag for the current + * conference. + * + * { + * type: SET_AUDIO_ONLY, + * audioOnly: boolean + * } + */ +export const SET_AUDIO_ONLY = 'SET_AUDIO_ONLY'; diff --git a/react/features/base/audio-only/actions.js b/react/features/base/audio-only/actions.js new file mode 100644 index 000000000..17a4bb57d --- /dev/null +++ b/react/features/base/audio-only/actions.js @@ -0,0 +1,64 @@ +// @flow + +import { getLogger } from 'jitsi-meet-logger'; + +import UIEvents from '../../../../service/UI/UIEvents'; + +import { createAudioOnlyChangedEvent, sendAnalytics } from '../../analytics'; + +import { SET_AUDIO_ONLY } from './actionTypes'; + +import type { Dispatch } from 'redux'; + +declare var APP: Object; +const logger = getLogger('features/base/audio-only'); + + +/** + * Sets the audio-only flag for the current JitsiConference. + * + * @param {boolean} audioOnly - True if the conference should be audio only; + * false, otherwise. + * @param {boolean} ensureVideoTrack - Define if conference should ensure + * to create a video track. + * @returns {{ + * type: SET_AUDIO_ONLY, + * audioOnly: boolean, + * ensureVideoTrack: boolean + * }} + */ +export function setAudioOnly(audioOnly: boolean, ensureVideoTrack: boolean = false) { + return (dispatch: Dispatch, getState: Function) => { + const { enabled: oldValue } = getState()['features/base/audio-only']; + + if (oldValue !== audioOnly) { + sendAnalytics(createAudioOnlyChangedEvent(audioOnly)); + logger.log(`Audio-only ${audioOnly ? 'enabled' : 'disabled'}`); + + dispatch({ + type: SET_AUDIO_ONLY, + audioOnly, + ensureVideoTrack + }); + + if (typeof APP !== 'undefined') { + // TODO This should be a temporary solution that lasts only until video + // tracks and all ui is moved into react/redux on the web. + APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly); + } + } + }; +} + +/** + * Toggles the audio-only flag for the current JitsiConference. + * + * @returns {Function} + */ +export function toggleAudioOnly() { + return (dispatch: Dispatch, getState: Function) => { + const { enabled } = getState()['features/base/audio-only']; + + return dispatch(setAudioOnly(!enabled, true)); + }; +} diff --git a/react/features/base/audio-only/index.js b/react/features/base/audio-only/index.js new file mode 100644 index 000000000..f6aa63cd5 --- /dev/null +++ b/react/features/base/audio-only/index.js @@ -0,0 +1,6 @@ +// @flow + +export * from './actions'; +export * from './actionTypes'; + +import './reducer'; diff --git a/react/features/base/audio-only/reducer.js b/react/features/base/audio-only/reducer.js new file mode 100644 index 000000000..28b1a289c --- /dev/null +++ b/react/features/base/audio-only/reducer.js @@ -0,0 +1,23 @@ +// @flow + +import { ReducerRegistry } from '../redux'; + +import { SET_AUDIO_ONLY } from './actionTypes'; + + +const DEFAULT_STATE = { + enabled: false +}; + + +ReducerRegistry.register('features/base/audio-only', (state = DEFAULT_STATE, action) => { + switch (action.type) { + case SET_AUDIO_ONLY: + return { + ...state, + enabled: action.audioOnly + }; + default: + return state; + } +}); diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js index 2237a8c2c..37d09726c 100644 --- a/react/features/base/conference/actionTypes.js +++ b/react/features/base/conference/actionTypes.js @@ -118,17 +118,6 @@ export const LOCK_STATE_CHANGED = 'LOCK_STATE_CHANGED'; */ export const P2P_STATUS_CHANGED = 'P2P_STATUS_CHANGED'; -/** - * The type of (redux) action which sets the audio-only flag for the current - * conference. - * - * { - * type: SET_AUDIO_ONLY, - * audioOnly: boolean - * } - */ -export const SET_AUDIO_ONLY = 'SET_AUDIO_ONLY'; - /** * The type of (redux) action which sets the desktop sharing enabled flag for * the current conference. @@ -152,16 +141,6 @@ export const SET_DESKTOP_SHARING_ENABLED */ export const SET_FOLLOW_ME = 'SET_FOLLOW_ME'; -/** - * The type of (redux) action which sets the video channel's lastN (value). - * - * { - * type: SET_LASTN, - * lastN: number - * } - */ -export const SET_LASTN = 'SET_LASTN'; - /** * The type of (redux) action which sets the maximum video height that should be * received from remote participants, even if the user prefers a larger video diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 3503f6abb..71cecfd73 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -35,10 +35,8 @@ import { KICKED_OUT, LOCK_STATE_CHANGED, P2P_STATUS_CHANGED, - SET_AUDIO_ONLY, SET_DESKTOP_SHARING_ENABLED, SET_FOLLOW_ME, - SET_LASTN, SET_MAX_RECEIVER_VIDEO_QUALITY, SET_PASSWORD, SET_PASSWORD_FAILED, @@ -522,29 +520,6 @@ export function p2pStatusChanged(p2p: boolean) { }; } -/** - * Sets the audio-only flag for the current JitsiConference. - * - * @param {boolean} audioOnly - True if the conference should be audio only; - * false, otherwise. - * @param {boolean} ensureVideoTrack - Define if conference should ensure - * to create a video track. - * @returns {{ - * type: SET_AUDIO_ONLY, - * audioOnly: boolean, - * ensureVideoTrack: boolean - * }} - */ -export function setAudioOnly( - audioOnly: boolean, - ensureVideoTrack: boolean = false) { - return { - type: SET_AUDIO_ONLY, - audioOnly, - ensureVideoTrack - }; -} - /** * Sets the flag for indicating if desktop sharing is enabled. * @@ -577,35 +552,6 @@ export function setFollowMe(enabled: boolean) { }; } -/** - * Sets the video channel's last N (value) of the current conference. A value of - * undefined shall be used to reset it to the default value. - * - * @param {(number|undefined)} lastN - The last N value to be set. - * @returns {Function} - */ -export function setLastN(lastN: ?number) { - return (dispatch: Dispatch, getState: Function) => { - if (typeof lastN === 'undefined') { - const config = getState()['features/base/config']; - - /* eslint-disable no-param-reassign */ - - lastN = config.channelLastN; - if (typeof lastN === 'undefined') { - lastN = -1; - } - - /* eslint-enable no-param-reassign */ - } - - dispatch({ - type: SET_LASTN, - lastN - }); - }; -} - /** * Sets the max frame height that should be received from remote videos. * @@ -754,19 +700,6 @@ export function setStartMutedPolicy( }; } -/** - * Toggles the audio-only flag for the current JitsiConference. - * - * @returns {Function} - */ -export function toggleAudioOnly() { - return (dispatch: Dispatch, getState: Function) => { - const { audioOnly } = getState()['features/base/conference']; - - return dispatch(setAudioOnly(!audioOnly, true)); - }; -} - /** * Changing conference subject. * diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index 0b285672d..9c8b8a2d0 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -4,7 +4,6 @@ import { reloadNow } from '../../app'; import { ACTION_PINNED, ACTION_UNPINNED, - createAudioOnlyChangedEvent, createConnectionEvent, createOfferAnswerFailedEvent, createPinnedEvent, @@ -12,7 +11,6 @@ import { } from '../../analytics'; import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection'; import { JitsiConferenceErrors } from '../lib-jitsi-meet'; -import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media'; import { getParticipantById, getPinnedParticipant, @@ -20,14 +18,12 @@ import { PIN_PARTICIPANT } from '../participants'; import { MiddlewareRegistry, StateListenerRegistry } from '../redux'; -import UIEvents from '../../../../service/UI/UIEvents'; import { TRACK_ADDED, TRACK_REMOVED } from '../tracks'; import { conferenceFailed, conferenceWillLeave, createConference, - setLastN, setSubject } from './actions'; import { @@ -36,16 +32,14 @@ import { CONFERENCE_SUBJECT_CHANGED, CONFERENCE_WILL_LEAVE, DATA_CHANNEL_OPENED, - SET_AUDIO_ONLY, - SET_LASTN, SET_PENDING_SUBJECT_CHANGE, SET_ROOM } from './actionTypes'; import { _addLocalTracksToConference, + _removeLocalTracksFromConference, forEachConference, - getCurrentConference, - _removeLocalTracksFromConference + getCurrentConference } from './functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -93,12 +87,6 @@ MiddlewareRegistry.register(store => next => action => { case PIN_PARTICIPANT: return _pinParticipant(store, next, action); - case SET_AUDIO_ONLY: - return _setAudioOnly(store, next, action); - - case SET_LASTN: - return _setLastN(store, next, action); - case SET_ROOM: return _setRoom(store, next, action); @@ -197,21 +185,10 @@ function _conferenceFailed(store, next, action) { */ function _conferenceJoined({ dispatch, getState }, next, action) { const result = next(action); + const { conference } = action; + const { pendingSubjectChange } = getState()['features/base/conference']; - const { - audioOnly, - conference, - pendingSubjectChange - } = getState()['features/base/conference']; - - if (pendingSubjectChange) { - dispatch(setSubject(pendingSubjectChange)); - } - - // FIXME On Web the audio only mode for "start audio only" is toggled before - // conference is added to the redux store ("on conference joined" action) - // and the LastN value needs to be synchronized here. - audioOnly && conference.getLastN() !== 0 && dispatch(setLastN(0)); + pendingSubjectChange && dispatch(setSubject(pendingSubjectChange)); // FIXME: Very dirty solution. This will work on web only. // When the user closes the window or quits the browser, lib-jitsi-meet @@ -460,79 +437,6 @@ function _pinParticipant({ getState }, next, action) { return next(action); } -/** - * Sets the audio-only flag for the current conference. When audio-only is set, - * local video is muted and last N is set to 0 to avoid receiving remote video. - * - * @param {Store} store - The redux store in which the specified {@code action} - * is being dispatched. - * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the - * specified {@code action} to the specified {@code store}. - * @param {Action} action - The redux action {@code SET_AUDIO_ONLY} which is - * being dispatched in the specified {@code store}. - * @private - * @returns {Object} The value returned by {@code next(action)}. - */ -function _setAudioOnly({ dispatch, getState }, next, action) { - const { audioOnly: oldValue } = getState()['features/base/conference']; - const result = next(action); - const { audioOnly: newValue } = getState()['features/base/conference']; - - // Send analytics. We could've done it in the action creator setAudioOnly. - // I don't know why it has to happen as early as possible but the analytics - // were originally sent before the SET_AUDIO_ONLY action was even dispatched - // in the redux store so I'm now sending the analytics as early as possible. - if (oldValue !== newValue) { - sendAnalytics(createAudioOnlyChangedEvent(newValue)); - logger.log(`Audio-only ${newValue ? 'enabled' : 'disabled'}`); - } - - // Set lastN to 0 in case audio-only is desired; leave it as undefined, - // otherwise, and the default lastN value will be chosen automatically. - dispatch(setLastN(newValue ? 0 : undefined)); - - // Mute/unmute the local video. - dispatch( - setVideoMuted( - newValue, - VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, - action.ensureVideoTrack)); - - if (typeof APP !== 'undefined') { - // TODO This should be a temporary solution that lasts only until video - // tracks and all ui is moved into react/redux on the web. - APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, newValue); - } - - return result; -} - -/** - * Sets the last N (value) of the video channel in the conference. - * - * @param {Store} store - The redux store in which the specified {@code action} - * is being dispatched. - * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the - * specified {@code action} to the specified {@code store}. - * @param {Action} action - The redux action {@code SET_LASTN} which is being - * dispatched in the specified {@code store}. - * @private - * @returns {Object} The value returned by {@code next(action)}. - */ -function _setLastN({ getState }, next, action) { - const { conference } = getState()['features/base/conference']; - - if (conference) { - try { - conference.setLastN(action.lastN); - } catch (err) { - logger.error(`Failed to set lastN: ${err}`); - } - } - - return next(action); -} - /** * Helper function for updating the preferred receiver video constraint, based * on the user preference and the internal maximum. diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js index 2791f94c6..17ffccdab 100644 --- a/react/features/base/conference/reducer.js +++ b/react/features/base/conference/reducer.js @@ -15,7 +15,6 @@ import { CONFERENCE_WILL_LEAVE, LOCK_STATE_CHANGED, P2P_STATUS_CHANGED, - SET_AUDIO_ONLY, SET_DESKTOP_SHARING_ENABLED, SET_FOLLOW_ME, SET_MAX_RECEIVER_VIDEO_QUALITY, @@ -76,9 +75,6 @@ ReducerRegistry.register( case P2P_STATUS_CHANGED: return _p2pStatusChanged(state, action); - case SET_AUDIO_ONLY: - return _setAudioOnly(state, action); - case SET_DESKTOP_SHARING_ENABLED: return _setDesktopSharingEnabled(state, action); @@ -346,20 +342,6 @@ function _p2pStatusChanged(state, action) { return set(state, 'p2p', action.p2p); } -/** - * Reduces a specific Redux action SET_AUDIO_ONLY of the feature - * base/conference. - * - * @param {Object} state - The Redux state of the feature base/conference. - * @param {Action} action - The Redux action SET_AUDIO_ONLY to reduce. - * @private - * @returns {Object} The new state of the feature base/conference after the - * reduction of the specified action. - */ -function _setAudioOnly(state, action) { - return set(state, 'audioOnly', action.audioOnly); -} - /** * Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature * base/conference. diff --git a/react/features/base/lastn/actionTypes.js b/react/features/base/lastn/actionTypes.js new file mode 100644 index 000000000..a80addfe5 --- /dev/null +++ b/react/features/base/lastn/actionTypes.js @@ -0,0 +1,11 @@ +// @flow + +/** + * The type of (redux) action which sets the video channel's lastN (value). + * + * { + * type: SET_LASTN, + * lastN: number + * } + */ +export const SET_LASTN = 'SET_LASTN'; diff --git a/react/features/base/lastn/actions.js b/react/features/base/lastn/actions.js new file mode 100644 index 000000000..76dc491d6 --- /dev/null +++ b/react/features/base/lastn/actions.js @@ -0,0 +1,34 @@ +// @flow + +import { SET_LASTN } from './actionTypes'; + +import type { Dispatch } from 'redux'; + +/** + * Sets the video channel's last N (value) of the current conference. A value of + * undefined shall be used to reset it to the default value. + * + * @param {(number|undefined)} lastN - The last N value to be set. + * @returns {Function} + */ +export function setLastN(lastN: ?number) { + return (dispatch: Dispatch, getState: Function) => { + if (typeof lastN === 'undefined') { + const config = getState()['features/base/config']; + + /* eslint-disable no-param-reassign */ + + lastN = config.channelLastN; + if (typeof lastN === 'undefined') { + lastN = -1; + } + + /* eslint-enable no-param-reassign */ + } + + dispatch({ + type: SET_LASTN, + lastN + }); + }; +} diff --git a/react/features/base/lastn/index.js b/react/features/base/lastn/index.js new file mode 100644 index 000000000..c85feec07 --- /dev/null +++ b/react/features/base/lastn/index.js @@ -0,0 +1,6 @@ +// @flow + +export * from './actions'; +export * from './actionTypes'; + +import './middleware'; diff --git a/react/features/base/lastn/middleware.js b/react/features/base/lastn/middleware.js new file mode 100644 index 000000000..03e738dc2 --- /dev/null +++ b/react/features/base/lastn/middleware.js @@ -0,0 +1,160 @@ +// @flow + +import { getLogger } from 'jitsi-meet-logger'; + +import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes'; +import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes'; + +import { SET_AUDIO_ONLY } from '../audio-only'; +import { CONFERENCE_JOINED } from '../conference/actionTypes'; +import { MiddlewareRegistry } from '../redux'; + +import { setLastN } from './actions'; +import { SET_LASTN } from './actionTypes'; + +declare var APP: Object; + +const logger = getLogger('features/base/lastn'); + + +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case APP_STATE_CHANGED: + return _appStateChanged(store, next, action); + + case CONFERENCE_JOINED: + return _conferenceJoined(store, next, action); + + case SET_AUDIO_ONLY: + return _setAudioOnly(store, next, action); + + case SET_FILMSTRIP_ENABLED: + return _setFilmstripEnabled(store, next, action); + + case SET_LASTN: + return _setLastN(store, next, action); + } + + return next(action); +}); + +/** + * Adjusts the lasN value based on the app state. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code APP_STATE_CHANGED} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _appStateChanged({ dispatch, getState }, next, action) { + const { enabled: audioOnly } = getState()['features/base/audio-only']; + + if (!audioOnly) { + const { appState } = action; + const lastN = appState === 'active' ? undefined : 0; + + dispatch(setLastN(lastN)); + logger.log(`App state changed - updated lastN to ${String(lastN)}`); + } + + return next(action); +} + +/** + * Adjusts the lasN value upon joining a conference. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _conferenceJoined({ dispatch, getState }, next, action) { + const { conference } = action; + const { enabled: audioOnly } = getState()['features/base/audio-only']; + + audioOnly && conference.getLastN() !== 0 && dispatch(setLastN(0)); + + return next(action); +} + +/** + * Sets the audio-only flag for the current conference. When audio-only is set, + * local video is muted and last N is set to 0 to avoid receiving remote video. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code SET_AUDIO_ONLY} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _setAudioOnly({ dispatch }, next, action) { + const { audioOnly } = action; + + // Set lastN to 0 in case audio-only is desired; leave it as undefined, + // otherwise, and the default lastN value will be chosen automatically. + dispatch(setLastN(audioOnly ? 0 : undefined)); + + return next(action); +} + +/** + * Notifies the feature filmstrip that the action {@link SET_FILMSTRIP_ENABLED} + * is being dispatched within a specific redux store. + * + * @param {Store} store - The redux store in which the specified action is being + * dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action {@code SET_FILMSTRIP_ENABLED} which + * is being dispatched in the specified store. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _setFilmstripEnabled({ dispatch, getState }, next, action) { + // FIXME This action is not currently dispatched on web. + if (typeof APP === 'undefined') { + const { enabled } = action; + const { enabled: audioOnly } = getState()['features/base/audio-only']; + + audioOnly || dispatch(setLastN(enabled ? undefined : 1)); + } + + return next(action); +} + +/** + * Sets the last N (value) of the video channel in the conference. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code SET_LASTN} which is being + * dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _setLastN({ getState }, next, action) { + const { conference } = getState()['features/base/conference']; + + if (conference) { + try { + conference.setLastN(action.lastN); + } catch (err) { + logger.error(`Failed to set lastN: ${err}`); + } + } + + return next(action); +} diff --git a/react/features/base/media/middleware.js b/react/features/base/media/middleware.js index 6bbb784ea..4f14f5710 100644 --- a/react/features/base/media/middleware.js +++ b/react/features/base/media/middleware.js @@ -4,16 +4,20 @@ import { createStartAudioOnlyEvent, createStartMutedConfigurationEvent, createSyncTrackStateEvent, + createTrackMutedEvent, sendAnalytics } from '../../analytics'; -import { isRoomValid, SET_ROOM, setAudioOnly } from '../conference'; +import { APP_STATE_CHANGED } from '../../mobile/background'; + +import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only'; +import { isRoomValid, SET_ROOM } from '../conference'; import JitsiMeetJS from '../lib-jitsi-meet'; import { MiddlewareRegistry } from '../redux'; import { getPropertyValue } from '../settings'; import { setTrackMuted, TRACK_ADDED } from '../tracks'; import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions'; -import { CAMERA_FACING_MODE } from './constants'; +import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants'; import { _AUDIO_INITIAL_MEDIA_STATE, _VIDEO_INITIAL_MEDIA_STATE @@ -29,6 +33,12 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); */ MiddlewareRegistry.register(store => next => action => { switch (action.type) { + case APP_STATE_CHANGED: + return _appStateChanged(store, next, action); + + case SET_AUDIO_ONLY: + return _setAudioOnly(store, next, action); + case SET_ROOM: return _setRoom(store, next, action); @@ -45,6 +55,51 @@ MiddlewareRegistry.register(store => next => action => { return next(action); }); +/** + * Adjusts the video muted state based on the app state. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code APP_STATE_CHANGED} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _appStateChanged({ dispatch }, next, action) { + const { appState } = action; + const mute = appState !== 'active'; // Note that 'background' and 'inactive' are treated equal. + + sendAnalytics(createTrackMutedEvent('video', 'background mode', mute)); + + dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.BACKGROUND)); + + return next(action); +} + +/** + * Adjusts the video muted state based on the audio-only state. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code SET_AUDIO_ONLY} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _setAudioOnly({ dispatch }, next, action) { + const { audioOnly } = action; + + sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly)); + + dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY)); + + return next(action); +} + /** * Notifies the feature base/media that the action {@link SET_ROOM} is being * dispatched within a specific redux {@code store}. diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index e5dd605d0..066213979 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -314,7 +314,7 @@ export function shouldRenderParticipantVideo( return false; } - const audioOnly = state['features/base/conference'].audioOnly; + const audioOnly = state['features/base/audio-only'].enabled; const connectionStatus = participant.connectionStatus || JitsiParticipantConnectionStatus.ACTIVE; const videoTrack = getTrackByMediaTypeAndParticipant( diff --git a/react/features/base/settings/middleware.js b/react/features/base/settings/middleware.js index db67ef0a1..e178cac74 100644 --- a/react/features/base/settings/middleware.js +++ b/react/features/base/settings/middleware.js @@ -1,6 +1,6 @@ // @flow -import { setAudioOnly } from '../conference'; +import { setAudioOnly } from '../audio-only'; import { getLocalParticipant, participantUpdated } from '../participants'; import { MiddlewareRegistry } from '../redux'; diff --git a/react/features/filmstrip/index.js b/react/features/filmstrip/index.js index 0a8f4ad7b..0bacc21c5 100644 --- a/react/features/filmstrip/index.js +++ b/react/features/filmstrip/index.js @@ -4,5 +4,4 @@ export * from './components'; export * from './constants'; export * from './functions'; -import './middleware'; import './reducer'; diff --git a/react/features/filmstrip/middleware.js b/react/features/filmstrip/middleware.js deleted file mode 100644 index a04b718ea..000000000 --- a/react/features/filmstrip/middleware.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow - -import { setLastN } from '../base/conference'; -import { MiddlewareRegistry } from '../base/redux'; - -import { SET_FILMSTRIP_ENABLED } from './actionTypes'; - -declare var APP: Object; - -MiddlewareRegistry.register(store => next => action => { - switch (action.type) { - case SET_FILMSTRIP_ENABLED: - return _setFilmstripEnabled(store, next, action); - } - - return next(action); -}); - -/** - * Notifies the feature filmstrip that the action {@link SET_FILMSTRIP_ENABLED} - * is being dispatched within a specific redux store. - * - * @param {Store} store - The redux store in which the specified action is being - * dispatched. - * @param {Dispatch} next - The redux dispatch function to dispatch the - * specified action to the specified store. - * @param {Action} action - The redux action {@code SET_FILMSTRIP_ENABLED} which - * is being dispatched in the specified store. - * @private - * @returns {Object} The value returned by {@code next(action)}. - */ -function _setFilmstripEnabled({ dispatch, getState }, next, action) { - const result = next(action); - - // FIXME This action is not currently dispatched on web. - if (typeof APP === 'undefined') { - const state = getState(); - const { enabled } = state['features/filmstrip']; - const { audioOnly } = state['features/base/conference']; - - // FIXME Audio-only mode fiddles with lastN as well. That's why we don't - // touch lastN in audio-only mode. But it's not clear what the value of - // lastN should be upon exit from audio-only mode if the filmstrip is - // disabled already. Currently, audio-only mode will set undefined - // regardless of whether the filmstrip is disabled. But we don't have a - // practical use case in which audio-only mode is exited while the - // filmstrip is disabled. - audioOnly || dispatch(setLastN(enabled ? undefined : 1)); - } - - return result; -} diff --git a/react/features/mobile/audio-mode/middleware.js b/react/features/mobile/audio-mode/middleware.js index 010c13e18..e0c543aaf 100644 --- a/react/features/mobile/audio-mode/middleware.js +++ b/react/features/mobile/audio-mode/middleware.js @@ -2,12 +2,12 @@ import { NativeModules } from 'react-native'; +import { SET_AUDIO_ONLY } from '../../base/audio-only'; import { APP_WILL_MOUNT } from '../../base/app'; import { CONFERENCE_FAILED, CONFERENCE_LEFT, CONFERENCE_JOINED, - SET_AUDIO_ONLY, getCurrentConference } from '../../base/conference'; import { MiddlewareRegistry } from '../../base/redux'; @@ -53,8 +53,9 @@ MiddlewareRegistry.register(({ getState }) => next => action => { */ case CONFERENCE_JOINED: case SET_AUDIO_ONLY: { - const { audioOnly, conference } - = getState()['features/base/conference']; + const state = getState(); + const { conference } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; conference && (mode = audioOnly diff --git a/react/features/mobile/background/actions.js b/react/features/mobile/background/actions.js index 26cb2cd16..786b9bf8b 100644 --- a/react/features/mobile/background/actions.js +++ b/react/features/mobile/background/actions.js @@ -1,14 +1,5 @@ // @flow -import type { Dispatch } from 'redux'; - -import { - createTrackMutedEvent, - sendAnalytics -} from '../../analytics'; -import { setLastN } from '../../base/conference'; -import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media'; - import { _SET_APP_STATE_LISTENER, APP_STATE_CHANGED } from './actionTypes'; /** @@ -28,34 +19,6 @@ export function _setAppStateListener(listener: ?Function) { }; } -/** - * Signals that the app should mute video because it's now running in the - * background, or unmute it because it came back from the background. If video - * was already muted nothing will happen; otherwise, it will be muted. When - * coming back from the background the previous state will be restored. - * - * @param {boolean} muted - True if video should be muted; false, otherwise. - * @protected - * @returns {Function} - */ -export function _setBackgroundVideoMuted(muted: boolean) { - return (dispatch: Dispatch, getState: Function) => { - // Disable remote video when we mute by setting lastN to 0. Skip it if - // the conference is in audio-only mode, as it's already configured to - // have no video. Leave it as undefined when unmuting, the default value - // for last N will be chosen automatically. - const { audioOnly } = getState()['features/base/conference']; - - audioOnly || dispatch(setLastN(muted ? 0 : undefined)); - - sendAnalytics(createTrackMutedEvent( - 'video', - 'callkit.background.video')); - - dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND)); - }; -} - /** * Signals that the App state has changed (in terms of execution state). The * application can be in 3 states: 'active', 'inactive' and 'background'. diff --git a/react/features/mobile/background/middleware.js b/react/features/mobile/background/middleware.native.js similarity index 71% rename from react/features/mobile/background/middleware.js rename to react/features/mobile/background/middleware.native.js index 5074d8b8e..2566b78ed 100644 --- a/react/features/mobile/background/middleware.js +++ b/react/features/mobile/background/middleware.native.js @@ -8,13 +8,9 @@ import { MiddlewareRegistry } from '../../base/redux'; import { _setAppStateListener as _setAppStateListenerA, - _setBackgroundVideoMuted, appStateChanged } from './actions'; -import { - _SET_APP_STATE_LISTENER, - APP_STATE_CHANGED -} from './actionTypes'; +import { _SET_APP_STATE_LISTENER } from './actionTypes'; /** * Middleware that captures App lifetime actions and subscribes to application @@ -31,15 +27,10 @@ MiddlewareRegistry.register(store => next => action => { case _SET_APP_STATE_LISTENER: return _setAppStateListenerF(store, next, action); - case APP_STATE_CHANGED: - _appStateChanged(store.dispatch, action.appState); - break; - case APP_WILL_MOUNT: { const { dispatch } = store; - dispatch( - _setAppStateListenerA(_onAppStateChange.bind(undefined, dispatch))); + dispatch(_setAppStateListenerA(_onAppStateChange.bind(undefined, dispatch))); break; } @@ -51,37 +42,6 @@ MiddlewareRegistry.register(store => next => action => { return next(action); }); -/** - * Handles app state changes. Dispatches the necessary redux actions for the - * local video to be muted when the app goes to the background, and to be - * unmuted when the app comes back. - * - * @param {Dispatch} dispatch - The redux {@code dispatch} function. - * @param {string} appState - The current app state. - * @private - * @returns {void} - */ -function _appStateChanged(dispatch: Function, appState: string) { - let muted; - - switch (appState) { - case 'active': - muted = false; - break; - - case 'background': - muted = true; - break; - - case 'inactive': - default: - // XXX: We purposely don't handle the 'inactive' app state. - return; - } - - dispatch(_setBackgroundVideoMuted(muted)); -} - /** * Called by React Native's AppState API to notify that the application state * has changed. Dispatches the change within the (associated) redux store. diff --git a/react/features/mobile/background/middleware.web.js b/react/features/mobile/background/middleware.web.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/mobile/background/reducer.js b/react/features/mobile/background/reducer.js index 1b04112b4..e9eeea134 100644 --- a/react/features/mobile/background/reducer.js +++ b/react/features/mobile/background/reducer.js @@ -14,22 +14,20 @@ const DEFAULT_STATE = { appState: 'active' }; -ReducerRegistry.register( - 'features/background', - (state = DEFAULT_STATE, action) => { - switch (action.type) { - case _SET_APP_STATE_LISTENER: - return { - ...state, - appStateListener: action.listener - }; +ReducerRegistry.register('features/background', (state = DEFAULT_STATE, action) => { + switch (action.type) { + case _SET_APP_STATE_LISTENER: + return { + ...state, + appStateListener: action.listener + }; - case APP_STATE_CHANGED: - return { - ...state, - appState: action.appState - }; - } + case APP_STATE_CHANGED: + return { + ...state, + appState: action.appState + }; + } - return state; - }); + return state; +}); diff --git a/react/features/mobile/call-integration/middleware.js b/react/features/mobile/call-integration/middleware.js index 65c19cc8a..5aa8b804f 100644 --- a/react/features/mobile/call-integration/middleware.js +++ b/react/features/mobile/call-integration/middleware.js @@ -6,13 +6,13 @@ import uuid from 'uuid'; import { createTrackMutedEvent, sendAnalytics } from '../../analytics'; import { appNavigate } from '../../app'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; +import { SET_AUDIO_ONLY } from '../../base/audio-only'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT, CONFERENCE_WILL_JOIN, CONFERENCE_WILL_LEAVE, - SET_AUDIO_ONLY, getConferenceName, getCurrentConference } from '../../base/conference'; diff --git a/react/features/mobile/full-screen/middleware.js b/react/features/mobile/full-screen/middleware.js index c6dc626e5..7012fd36b 100644 --- a/react/features/mobile/full-screen/middleware.js +++ b/react/features/mobile/full-screen/middleware.js @@ -46,7 +46,7 @@ MiddlewareRegistry.register(store => next => action => { StateListenerRegistry.register( /* selector */ state => { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const conference = getCurrentConference(state); return conference ? !audioOnly : false; @@ -68,7 +68,7 @@ function _onImmersiveChange({ getState }) { const { appState } = state['features/background']; if (appState === 'active') { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const conference = getCurrentConference(state); const fullScreen = conference ? !audioOnly : false; diff --git a/react/features/mobile/proximity/middleware.js b/react/features/mobile/proximity/middleware.js index 39712ecbe..f4549da0b 100644 --- a/react/features/mobile/proximity/middleware.js +++ b/react/features/mobile/proximity/middleware.js @@ -11,7 +11,7 @@ import { StateListenerRegistry } from '../../base/redux'; */ StateListenerRegistry.register( /* selector */ state => { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const conference = getCurrentConference(state); return Boolean(conference && audioOnly); diff --git a/react/features/mobile/wake-lock/middleware.js b/react/features/mobile/wake-lock/middleware.js index 08b4b7546..2a56864e8 100644 --- a/react/features/mobile/wake-lock/middleware.js +++ b/react/features/mobile/wake-lock/middleware.js @@ -9,7 +9,7 @@ import { StateListenerRegistry } from '../../base/redux'; */ StateListenerRegistry.register( /* selector */ state => { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const conference = getCurrentConference(state); return Boolean(conference && !audioOnly); diff --git a/react/features/toolbox/components/VideoMuteButton.js b/react/features/toolbox/components/VideoMuteButton.js index 8bfaf5a25..5e06bde15 100644 --- a/react/features/toolbox/components/VideoMuteButton.js +++ b/react/features/toolbox/components/VideoMuteButton.js @@ -7,7 +7,7 @@ import { createToolbarEvent, sendAnalytics } from '../../analytics'; -import { setAudioOnly } from '../../base/conference'; +import { setAudioOnly } from '../../base/audio-only'; import { translate } from '../../base/i18n'; import { MEDIA_TYPE, @@ -162,7 +162,7 @@ class VideoMuteButton extends AbstractVideoMuteButton { * }} */ function _mapStateToProps(state): Object { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const tracks = state['features/base/tracks']; return { diff --git a/react/features/toolbox/components/native/AudioOnlyButton.js b/react/features/toolbox/components/native/AudioOnlyButton.js index 2f59debdd..fcc355ef6 100644 --- a/react/features/toolbox/components/native/AudioOnlyButton.js +++ b/react/features/toolbox/components/native/AudioOnlyButton.js @@ -1,6 +1,6 @@ // @flow -import { toggleAudioOnly } from '../../../base/conference'; +import { toggleAudioOnly } from '../../../base/audio-only'; import { translate } from '../../../base/i18n'; import { connect } from '../../../base/redux'; import { AbstractButton } from '../../../base/toolbox'; @@ -66,7 +66,7 @@ class AudioOnlyButton extends AbstractButton { * }} */ function _mapStateToProps(state): Object { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; return { _audioOnly: Boolean(audioOnly) diff --git a/react/features/toolbox/components/native/ToggleCameraButton.js b/react/features/toolbox/components/native/ToggleCameraButton.js index 50921f9c8..99ddbfbeb 100644 --- a/react/features/toolbox/components/native/ToggleCameraButton.js +++ b/react/features/toolbox/components/native/ToggleCameraButton.js @@ -71,7 +71,7 @@ class ToggleCameraButton extends AbstractButton { * }} */ function _mapStateToProps(state): Object { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const tracks = state['features/base/tracks']; return { diff --git a/react/features/video-quality/components/AbstractVideoQualityLabel.js b/react/features/video-quality/components/AbstractVideoQualityLabel.js index f3046b5a2..363d3af4c 100644 --- a/react/features/video-quality/components/AbstractVideoQualityLabel.js +++ b/react/features/video-quality/components/AbstractVideoQualityLabel.js @@ -33,7 +33,7 @@ export default class AbstractVideoQualityLabel extends Component

{ * }} */ export function _abstractMapStateToProps(state: Object) { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; return { _audioOnly: audioOnly diff --git a/react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js b/react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js index 411311176..3dbace747 100644 --- a/react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js +++ b/react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js @@ -96,7 +96,7 @@ class OverflowMenuVideoQualityItem extends Component { */ function _mapStateToProps(state) { return { - _audioOnly: state['features/base/conference'].audioOnly, + _audioOnly: state['features/base/audio-only'].enabled, _receiverVideoQuality: state['features/base/conference'].preferredReceiverVideoQuality }; diff --git a/react/features/video-quality/components/VideoQualityLabel.web.js b/react/features/video-quality/components/VideoQualityLabel.web.js index 9f62a46c8..8a451ab43 100644 --- a/react/features/video-quality/components/VideoQualityLabel.web.js +++ b/react/features/video-quality/components/VideoQualityLabel.web.js @@ -156,7 +156,7 @@ function _mapResolutionToTranslationsKeys(resolution) { * }} */ function _mapStateToProps(state) { - const { audioOnly } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; const { resolution, participantId } = state['features/large-video']; const videoTrackOnLargeVideo = getTrackByMediaTypeAndParticipant( state['features/base/tracks'], diff --git a/react/features/video-quality/components/VideoQualitySlider.web.js b/react/features/video-quality/components/VideoQualitySlider.web.js index 8f78de235..911d5d56a 100644 --- a/react/features/video-quality/components/VideoQualitySlider.web.js +++ b/react/features/video-quality/components/VideoQualitySlider.web.js @@ -4,15 +4,9 @@ import InlineMessage from '@atlaskit/inline-message'; import React, { Component } from 'react'; import type { Dispatch } from 'redux'; -import { - createToolbarEvent, - sendAnalytics -} from '../../analytics'; -import { - VIDEO_QUALITY_LEVELS, - setAudioOnly, - setPreferredReceiverVideoQuality -} from '../../base/conference'; +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { setAudioOnly } from '../../base/audio-only'; +import { VIDEO_QUALITY_LEVELS, setPreferredReceiverVideoQuality } from '../../base/conference'; import { translate } from '../../base/i18n'; import JitsiMeetJS from '../../base/lib-jitsi-meet'; import { connect } from '../../base/redux'; @@ -406,11 +400,8 @@ class VideoQualitySlider extends Component { * }} */ function _mapStateToProps(state) { - const { - audioOnly, - p2p, - preferredReceiverVideoQuality - } = state['features/base/conference']; + const { enabled: audioOnly } = state['features/base/audio-only']; + const { p2p, preferredReceiverVideoQuality } = state['features/base/conference']; return { _audioOnly: audioOnly,