[RN] Add audio only mode for conferences
The behavior can be triggered with the toggleAudioOnly action, which is currently fired with a button. The following aspects of the conference will change when in audio only mode: - local video is muted - last N is set to 0 (effectively muting remote video) - full-screen mode is exited - audio mode is set to "audio chat" (default output is the earpiece) - the wake lock is disengaged One aspect not handled in this patch is disabling the video mute button while in audio only mode. The user should not be able to turn back video on in that case.
This commit is contained in:
parent
4ec4c45a90
commit
8fe3dce649
|
@ -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');
|
||||
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 } />
|
||||
<ToolbarButton
|
||||
iconName = 'star'
|
||||
iconStyle = { iconStyle }
|
||||
onClick = { this.props._onToggleAudioOnly }
|
||||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue