diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js index 133d6fe63..ce576ec4b 100644 --- a/react/features/base/conference/actionTypes.js +++ b/react/features/base/conference/actionTypes.js @@ -68,6 +68,31 @@ export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE'); */ export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED'); +/** + * The type of the Redux action which sets the audio-only flag for the current + * conference. + * + * { + * type: SET_AUDIO_ONLY, + * audioOnly: boolean + * } + */ +export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY'); + +/** + * The type of redux action which signals that video will be muted because the + * audio-only mode was enabled / disabled. + * + * { + * type: _SET_AUDIO_ONLY_VIDEO_MUTED, + * muted: boolean + * } + * + * @protected + */ +export const _SET_AUDIO_ONLY_VIDEO_MUTED + = Symbol('_SET_AUDIO_ONLY_VIDEO_MUTED'); + /** * The type of redux action which sets the video channel's lastN (value). * @@ -75,7 +100,6 @@ export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED'); * type: SET_LASTN, * lastN: number * } - * */ export const SET_LASTN = Symbol('SET_LASTN'); diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index c3e1bbba8..ad6556395 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -1,4 +1,5 @@ import { JitsiConferenceEvents } from '../lib-jitsi-meet'; +import { setVideoMuted } from '../media'; import { dominantSpeakerChanged, getLocalParticipant, @@ -17,6 +18,8 @@ import { CONFERENCE_WILL_JOIN, CONFERENCE_WILL_LEAVE, LOCK_STATE_CHANGED, + SET_AUDIO_ONLY, + _SET_AUDIO_ONLY_VIDEO_MUTED, SET_LASTN, SET_PASSWORD, SET_ROOM @@ -295,6 +298,63 @@ function _lockStateChanged(conference, locked) { }; } +/** + * Sets the audio-only flag for the current JitsiConference. + * + * @param {boolean} audioOnly - True if the conference should be audio only; + * false, otherwise. + * @private + * @returns {{ + * type: SET_AUDIO_ONLY, + * audioOnly: boolean + * }} + */ +function _setAudioOnly(audioOnly) { + return { + type: SET_AUDIO_ONLY, + audioOnly + }; +} + +/** + * Signals that the app should mute video because it's now in audio-only mode, + * or unmute it because it no longer is. If video was already muted, nothing + * will happen; otherwise, it will be muted. When audio-only mode is disabled, + * the previous state will be restored. + * + * @param {boolean} muted - True if video should be muted; false, otherwise. + * @protected + * @returns {Function} + */ +export function _setAudioOnlyVideoMuted(muted: boolean) { + return (dispatch, getState) => { + if (muted) { + const { video } = getState()['features/base/media']; + + if (video.muted) { + // Video is already muted, do nothing. + return; + } + } else { + const { audioOnlyVideoMuted } + = getState()['features/base/conference']; + + if (!audioOnlyVideoMuted) { + // We didn't mute video, do nothing. + return; + } + } + + // Remember that local video was muted due to the audio-only mode + // vs user's choice. + dispatch({ + type: _SET_AUDIO_ONLY_VIDEO_MUTED, + muted + }); + dispatch(setVideoMuted(muted)); + }; +} + /** * 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. @@ -403,3 +463,16 @@ export function setRoom(room) { room }; } + +/** + * 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)); + }; +} diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index d399abec2..170bb0a76 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -8,8 +8,12 @@ import { import { MiddlewareRegistry } from '../redux'; import { TRACK_ADDED, TRACK_REMOVED } from '../tracks'; -import { createConference } from './actions'; -import { SET_LASTN } from './actionTypes'; +import { + createConference, + _setAudioOnlyVideoMuted, + setLastN +} from './actions'; +import { SET_AUDIO_ONLY, SET_LASTN } from './actionTypes'; import { _addLocalTracksToConference, _handleParticipantError, @@ -30,6 +34,9 @@ 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); @@ -116,6 +123,35 @@ function _pinParticipant(store, 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 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 SET_AUDIO_ONLY which is being + * dispatched in the specified store. + * @private + * @returns {Object} The new state that is the result of the reduction of the + * specified action. + */ +function _setAudioOnly(store, next, action) { + const result = 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. + store.dispatch(setLastN(audioOnly ? 0 : undefined)); + + // Mute local video + store.dispatch(_setAudioOnlyVideoMuted(audioOnly)); + + return result; +} + /** * Sets the last N (value) of the video channel in the conference. * diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js index 358b52d1a..c594762fa 100644 --- a/react/features/base/conference/reducer.js +++ b/react/features/base/conference/reducer.js @@ -11,6 +11,8 @@ import { CONFERENCE_LEFT, CONFERENCE_WILL_LEAVE, LOCK_STATE_CHANGED, + SET_AUDIO_ONLY, + _SET_AUDIO_ONLY_VIDEO_MUTED, SET_PASSWORD, SET_ROOM } from './actionTypes'; @@ -37,6 +39,12 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => { case LOCK_STATE_CHANGED: return _lockStateChanged(state, action); + case SET_AUDIO_ONLY: + return _setAudioOnly(state, action); + + case _SET_AUDIO_ONLY_VIDEO_MUTED: + return _setAudioOnlyVideoMuted(state, action); + case SET_PASSWORD: return _setPassword(state, action); @@ -71,6 +79,8 @@ function _conferenceFailed(state, action) { return ( setStateProperties(state, { + audioOnly: undefined, + audioOnlyVideoMuted: undefined, conference: undefined, leaving: undefined, locked: undefined, @@ -144,6 +154,8 @@ function _conferenceLeft(state, action) { return ( setStateProperties(state, { + audioOnly: undefined, + audioOnlyVideoMuted: undefined, conference: undefined, leaving: undefined, locked: undefined, @@ -200,6 +212,35 @@ function _lockStateChanged(state, action) { return setStateProperty(state, 'locked', action.locked || undefined); } +/** + * 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 setStateProperty(state, 'audioOnly', action.audioOnly); +} + +/** + * Reduces a specific Redux action _SET_AUDIO_ONLY_VIDEO_MUTED 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_VIDEO_MUTED to + * reduce. + * @private + * @returns {Object} The new state of the feature base/conference after the + * reduction of the specified action. + */ +function _setAudioOnlyVideoMuted(state, action) { + return setStateProperty(state, 'audioOnlyVideoMuted', action.muted); +} + /** * Reduces a specific Redux action SET_PASSWORD of the feature base/conference. * diff --git a/react/features/mobile/audio-mode/middleware.js b/react/features/mobile/audio-mode/middleware.js index 78aae9253..25a262fd7 100644 --- a/react/features/mobile/audio-mode/middleware.js +++ b/react/features/mobile/audio-mode/middleware.js @@ -6,7 +6,8 @@ import { APP_WILL_MOUNT } from '../../app'; import { CONFERENCE_FAILED, CONFERENCE_LEFT, - CONFERENCE_WILL_JOIN + CONFERENCE_WILL_JOIN, + SET_AUDIO_ONLY } from '../../base/conference'; import { MiddlewareRegistry } from '../../base/redux'; @@ -21,8 +22,6 @@ import { MiddlewareRegistry } from '../../base/redux'; MiddlewareRegistry.register(store => next => action => { const AudioMode = NativeModules.AudioMode; - // The react-native module AudioMode is implemented on iOS at the time of - // this writing. if (AudioMode) { let mode; @@ -34,14 +33,18 @@ MiddlewareRegistry.register(store => next => action => { break; case CONFERENCE_WILL_JOIN: { - const conference = store.getState()['features/base/conference']; + const { audioOnly } = store.getState()['features/base/conference']; + mode = audioOnly ? AudioMode.AUDIO_CALL : AudioMode.VIDEO_CALL; + break; + } + + case SET_AUDIO_ONLY: mode - = conference.audioOnly + = action.audioOnly ? AudioMode.AUDIO_CALL : AudioMode.VIDEO_CALL; break; - } default: mode = null; diff --git a/react/features/mobile/background/actions.js b/react/features/mobile/background/actions.js index 788806106..f724dba27 100644 --- a/react/features/mobile/background/actions.js +++ b/react/features/mobile/background/actions.js @@ -42,10 +42,12 @@ export function _setBackgroundVideoMuted(muted: boolean) { // for last N will be chosen automatically. const { audioOnly } = getState()['features/base/conference']; - if (!audioOnly) { - dispatch(setLastN(muted ? 0 : undefined)); + if (audioOnly) { + return; } + dispatch(setLastN(muted ? 0 : undefined)); + if (muted) { const { video } = getState()['features/base/media']; diff --git a/react/features/mobile/full-screen/middleware.js b/react/features/mobile/full-screen/middleware.js index 377ed4c7b..6d73a99a5 100644 --- a/react/features/mobile/full-screen/middleware.js +++ b/react/features/mobile/full-screen/middleware.js @@ -7,7 +7,8 @@ import { APP_STATE_CHANGED } from '../background'; import { CONFERENCE_FAILED, CONFERENCE_LEFT, - CONFERENCE_WILL_JOIN + CONFERENCE_WILL_JOIN, + SET_AUDIO_ONLY } from '../../base/conference'; import { Platform } from '../../base/react'; import { MiddlewareRegistry } from '../../base/redux'; @@ -50,6 +51,10 @@ MiddlewareRegistry.register(store => next => action => { case CONFERENCE_LEFT: fullScreen = false; break; + + case SET_AUDIO_ONLY: + fullScreen = !action.audioOnly; + break; } if (fullScreen !== null) { diff --git a/react/features/mobile/wake-lock/middleware.js b/react/features/mobile/wake-lock/middleware.js index bc57e27e2..1071c1c51 100644 --- a/react/features/mobile/wake-lock/middleware.js +++ b/react/features/mobile/wake-lock/middleware.js @@ -3,7 +3,8 @@ import KeepAwake from 'react-native-keep-awake'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, - CONFERENCE_LEFT + CONFERENCE_LEFT, + SET_AUDIO_ONLY } from '../../base/conference'; import { MiddlewareRegistry } from '../../base/redux'; @@ -18,12 +19,9 @@ import { MiddlewareRegistry } from '../../base/redux'; MiddlewareRegistry.register(store => next => action => { switch (action.type) { case CONFERENCE_JOINED: { - const state = store.getState()['features/base/conference']; + const { audioOnly } = store.getState()['features/base/conference']; - // TODO(saghul): Implement audio-only mode. - if (!state.audioOnly) { - _setWakeLock(true); - } + _setWakeLock(!audioOnly); break; } @@ -31,6 +29,10 @@ MiddlewareRegistry.register(store => next => action => { case CONFERENCE_LEFT: _setWakeLock(false); break; + + case SET_AUDIO_ONLY: + _setWakeLock(!action.audioOnly); + break; } return next(action); diff --git a/react/features/toolbox/components/Toolbox.native.js b/react/features/toolbox/components/Toolbox.native.js index 69b2b6286..2d51af8ff 100644 --- a/react/features/toolbox/components/Toolbox.native.js +++ b/react/features/toolbox/components/Toolbox.native.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import { connect } from 'react-redux'; +import { toggleAudioOnly } from '../../base/conference'; import { MEDIA_TYPE, toggleCameraFacingMode } from '../../base/media'; import { Container } from '../../base/react'; import { ColorPalette } from '../../base/styles'; @@ -40,7 +41,7 @@ class Toolbox extends Component { _onHangup: React.PropTypes.func, /** - * Handler for room locking. + * Sets the lock i.e. password protection of the conference/room. */ _onRoomLock: React.PropTypes.func, @@ -50,7 +51,13 @@ class Toolbox extends Component { _onToggleAudio: React.PropTypes.func, /** - * Handler for toggling camera facing mode. + * Toggles the audio-only flag of the conference. + */ + _onToggleAudioOnly: React.PropTypes.func, + + /** + * Switches between the front/user-facing and back/environment-facing + * cameras. */ _onToggleCameraFacingMode: React.PropTypes.func, @@ -198,6 +205,12 @@ class Toolbox extends Component { onClick = { this.props._onRoomLock } style = { style } underlayColor = { underlayColor } /> + ); @@ -224,6 +237,7 @@ Object.assign(Toolbox.prototype, { * @param {Function} dispatch - Redux action dispatcher. * @returns {{ * _onRoomLock: Function, + * _onToggleAudioOnly: Function, * _onToggleCameraFacingMode: Function, * }} * @private @@ -233,11 +247,10 @@ function _mapDispatchToProps(dispatch) { ...abstractMapDispatchToProps(dispatch), /** - * Dispatches an action to set the lock i.e. password protection of the - * conference/room. + * Sets the lock i.e. password protection of the conference/room. * * @private - * @returns {Object} - Dispatched action. + * @returns {Object} Dispatched action. * @type {Function} */ _onRoomLock() { @@ -245,11 +258,22 @@ function _mapDispatchToProps(dispatch) { }, /** - * Switches between the front/user-facing and rear/environment-facing + * Toggles the audio-only flag of the conference. + * + * @private + * @returns {Object} Dispatched action. + * @type {Function} + */ + _onToggleAudioOnly() { + return dispatch(toggleAudioOnly()); + }, + + /** + * Switches between the front/user-facing and back/environment-facing * cameras. * * @private - * @returns {Object} - Dispatched action. + * @returns {Object} Dispatched action. * @type {Function} */ _onToggleCameraFacingMode() {