audio-only,lastn: move audio-only and last N handling to standalone features

This refactors all handling of audio-only and last N to 2 features in preparation
for "low bandwidth mode".

The main motivation to do this is that lastN is a "global" setting so it helps
to have all processing for it in a single place.
This commit is contained in:
Saúl Ibarra Corretgé 2019-07-31 14:47:52 +02:00 committed by Saúl Ibarra Corretgé
parent 6ddd17c769
commit 467c9d36cf
35 changed files with 422 additions and 394 deletions

View File

@ -1345,8 +1345,7 @@ export default {
* @returns {boolean} * @returns {boolean}
*/ */
isAudioOnly() { isAudioOnly() {
return Boolean( return Boolean(APP.store.getState()['features/base/audio-only'].enabled);
APP.store.getState()['features/base/conference'].audioOnly);
}, },
videoSwitchInProgress: false, videoSwitchInProgress: false,

View File

@ -7,8 +7,8 @@ import { toURLString } from '../../base/util';
import '../../follow-me'; import '../../follow-me';
import { OverlayContainer } from '../../overlay'; import { OverlayContainer } from '../../overlay';
// Enable rejoin analytics import '../../base/lastn'; // Register lastN middleware
import '../../rejoin'; import '../../rejoin'; // Enable rejoin analytics
import { appNavigate } from '../actions'; import { appNavigate } from '../actions';
import { getDefaultURL } from '../functions'; import { getDefaultURL } from '../functions';

View File

@ -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';

View File

@ -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<any>, 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<any>, getState: Function) => {
const { enabled } = getState()['features/base/audio-only'];
return dispatch(setAudioOnly(!enabled, true));
};
}

View File

@ -0,0 +1,6 @@
// @flow
export * from './actions';
export * from './actionTypes';
import './reducer';

View File

@ -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;
}
});

View File

@ -118,17 +118,6 @@ export const LOCK_STATE_CHANGED = 'LOCK_STATE_CHANGED';
*/ */
export const P2P_STATUS_CHANGED = 'P2P_STATUS_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 type of (redux) action which sets the desktop sharing enabled flag for
* the current conference. * the current conference.
@ -152,16 +141,6 @@ export const SET_DESKTOP_SHARING_ENABLED
*/ */
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME'; 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 * 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 * received from remote participants, even if the user prefers a larger video

View File

@ -35,10 +35,8 @@ import {
KICKED_OUT, KICKED_OUT,
LOCK_STATE_CHANGED, LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED, P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED, SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME, SET_FOLLOW_ME,
SET_LASTN,
SET_MAX_RECEIVER_VIDEO_QUALITY, SET_MAX_RECEIVER_VIDEO_QUALITY,
SET_PASSWORD, SET_PASSWORD,
SET_PASSWORD_FAILED, 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. * 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<any>, 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. * 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<any>, getState: Function) => {
const { audioOnly } = getState()['features/base/conference'];
return dispatch(setAudioOnly(!audioOnly, true));
};
}
/** /**
* Changing conference subject. * Changing conference subject.
* *

View File

@ -4,7 +4,6 @@ import { reloadNow } from '../../app';
import { import {
ACTION_PINNED, ACTION_PINNED,
ACTION_UNPINNED, ACTION_UNPINNED,
createAudioOnlyChangedEvent,
createConnectionEvent, createConnectionEvent,
createOfferAnswerFailedEvent, createOfferAnswerFailedEvent,
createPinnedEvent, createPinnedEvent,
@ -12,7 +11,6 @@ import {
} from '../../analytics'; } from '../../analytics';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection'; import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection';
import { JitsiConferenceErrors } from '../lib-jitsi-meet'; import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
import { import {
getParticipantById, getParticipantById,
getPinnedParticipant, getPinnedParticipant,
@ -20,14 +18,12 @@ import {
PIN_PARTICIPANT PIN_PARTICIPANT
} from '../participants'; } from '../participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux'; import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks'; import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import { import {
conferenceFailed, conferenceFailed,
conferenceWillLeave, conferenceWillLeave,
createConference, createConference,
setLastN,
setSubject setSubject
} from './actions'; } from './actions';
import { import {
@ -36,16 +32,14 @@ import {
CONFERENCE_SUBJECT_CHANGED, CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_WILL_LEAVE, CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED, DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
SET_PENDING_SUBJECT_CHANGE, SET_PENDING_SUBJECT_CHANGE,
SET_ROOM SET_ROOM
} from './actionTypes'; } from './actionTypes';
import { import {
_addLocalTracksToConference, _addLocalTracksToConference,
_removeLocalTracksFromConference,
forEachConference, forEachConference,
getCurrentConference, getCurrentConference
_removeLocalTracksFromConference
} from './functions'; } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename); const logger = require('jitsi-meet-logger').getLogger(__filename);
@ -93,12 +87,6 @@ MiddlewareRegistry.register(store => next => action => {
case PIN_PARTICIPANT: case PIN_PARTICIPANT:
return _pinParticipant(store, next, action); 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: case SET_ROOM:
return _setRoom(store, next, action); return _setRoom(store, next, action);
@ -197,21 +185,10 @@ function _conferenceFailed(store, next, action) {
*/ */
function _conferenceJoined({ dispatch, getState }, next, action) { function _conferenceJoined({ dispatch, getState }, next, action) {
const result = next(action); const result = next(action);
const { conference } = action;
const { pendingSubjectChange } = getState()['features/base/conference'];
const { pendingSubjectChange && dispatch(setSubject(pendingSubjectChange));
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));
// FIXME: Very dirty solution. This will work on web only. // FIXME: Very dirty solution. This will work on web only.
// When the user closes the window or quits the browser, lib-jitsi-meet // 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); 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 * Helper function for updating the preferred receiver video constraint, based
* on the user preference and the internal maximum. * on the user preference and the internal maximum.

View File

@ -15,7 +15,6 @@ import {
CONFERENCE_WILL_LEAVE, CONFERENCE_WILL_LEAVE,
LOCK_STATE_CHANGED, LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED, P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED, SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME, SET_FOLLOW_ME,
SET_MAX_RECEIVER_VIDEO_QUALITY, SET_MAX_RECEIVER_VIDEO_QUALITY,
@ -76,9 +75,6 @@ ReducerRegistry.register(
case P2P_STATUS_CHANGED: case P2P_STATUS_CHANGED:
return _p2pStatusChanged(state, action); return _p2pStatusChanged(state, action);
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
case SET_DESKTOP_SHARING_ENABLED: case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action); return _setDesktopSharingEnabled(state, action);
@ -346,20 +342,6 @@ function _p2pStatusChanged(state, action) {
return set(state, 'p2p', action.p2p); 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 * Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
* base/conference. * base/conference.

View File

@ -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';

View File

@ -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<any>, 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
});
};
}

View File

@ -0,0 +1,6 @@
// @flow
export * from './actions';
export * from './actionTypes';
import './middleware';

View File

@ -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);
}

View File

@ -4,16 +4,20 @@ import {
createStartAudioOnlyEvent, createStartAudioOnlyEvent,
createStartMutedConfigurationEvent, createStartMutedConfigurationEvent,
createSyncTrackStateEvent, createSyncTrackStateEvent,
createTrackMutedEvent,
sendAnalytics sendAnalytics
} from '../../analytics'; } 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 JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings'; import { getPropertyValue } from '../settings';
import { setTrackMuted, TRACK_ADDED } from '../tracks'; import { setTrackMuted, TRACK_ADDED } from '../tracks';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions'; import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
import { CAMERA_FACING_MODE } from './constants'; import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
import { import {
_AUDIO_INITIAL_MEDIA_STATE, _AUDIO_INITIAL_MEDIA_STATE,
_VIDEO_INITIAL_MEDIA_STATE _VIDEO_INITIAL_MEDIA_STATE
@ -29,6 +33,12 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
*/ */
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case APP_STATE_CHANGED:
return _appStateChanged(store, next, action);
case SET_AUDIO_ONLY:
return _setAudioOnly(store, next, action);
case SET_ROOM: case SET_ROOM:
return _setRoom(store, next, action); return _setRoom(store, next, action);
@ -45,6 +55,51 @@ MiddlewareRegistry.register(store => next => action => {
return 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 * Notifies the feature base/media that the action {@link SET_ROOM} is being
* dispatched within a specific redux {@code store}. * dispatched within a specific redux {@code store}.

View File

@ -314,7 +314,7 @@ export function shouldRenderParticipantVideo(
return false; return false;
} }
const audioOnly = state['features/base/conference'].audioOnly; const audioOnly = state['features/base/audio-only'].enabled;
const connectionStatus = participant.connectionStatus const connectionStatus = participant.connectionStatus
|| JitsiParticipantConnectionStatus.ACTIVE; || JitsiParticipantConnectionStatus.ACTIVE;
const videoTrack = getTrackByMediaTypeAndParticipant( const videoTrack = getTrackByMediaTypeAndParticipant(

View File

@ -1,6 +1,6 @@
// @flow // @flow
import { setAudioOnly } from '../conference'; import { setAudioOnly } from '../audio-only';
import { getLocalParticipant, participantUpdated } from '../participants'; import { getLocalParticipant, participantUpdated } from '../participants';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';

View File

@ -4,5 +4,4 @@ export * from './components';
export * from './constants'; export * from './constants';
export * from './functions'; export * from './functions';
import './middleware';
import './reducer'; import './reducer';

View File

@ -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;
}

View File

@ -2,12 +2,12 @@
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import { SET_AUDIO_ONLY } from '../../base/audio-only';
import { APP_WILL_MOUNT } from '../../base/app'; import { APP_WILL_MOUNT } from '../../base/app';
import { import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
CONFERENCE_JOINED, CONFERENCE_JOINED,
SET_AUDIO_ONLY,
getCurrentConference getCurrentConference
} from '../../base/conference'; } from '../../base/conference';
import { MiddlewareRegistry } from '../../base/redux'; import { MiddlewareRegistry } from '../../base/redux';
@ -53,8 +53,9 @@ MiddlewareRegistry.register(({ getState }) => next => action => {
*/ */
case CONFERENCE_JOINED: case CONFERENCE_JOINED:
case SET_AUDIO_ONLY: { case SET_AUDIO_ONLY: {
const { audioOnly, conference } const state = getState();
= getState()['features/base/conference']; const { conference } = state['features/base/conference'];
const { enabled: audioOnly } = state['features/base/audio-only'];
conference conference
&& (mode = audioOnly && (mode = audioOnly

View File

@ -1,14 +1,5 @@
// @flow // @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'; 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<any>, 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 * Signals that the App state has changed (in terms of execution state). The
* application can be in 3 states: 'active', 'inactive' and 'background'. * application can be in 3 states: 'active', 'inactive' and 'background'.

View File

@ -8,13 +8,9 @@ import { MiddlewareRegistry } from '../../base/redux';
import { import {
_setAppStateListener as _setAppStateListenerA, _setAppStateListener as _setAppStateListenerA,
_setBackgroundVideoMuted,
appStateChanged appStateChanged
} from './actions'; } from './actions';
import { import { _SET_APP_STATE_LISTENER } from './actionTypes';
_SET_APP_STATE_LISTENER,
APP_STATE_CHANGED
} from './actionTypes';
/** /**
* Middleware that captures App lifetime actions and subscribes to application * Middleware that captures App lifetime actions and subscribes to application
@ -31,15 +27,10 @@ MiddlewareRegistry.register(store => next => action => {
case _SET_APP_STATE_LISTENER: case _SET_APP_STATE_LISTENER:
return _setAppStateListenerF(store, next, action); return _setAppStateListenerF(store, next, action);
case APP_STATE_CHANGED:
_appStateChanged(store.dispatch, action.appState);
break;
case APP_WILL_MOUNT: { case APP_WILL_MOUNT: {
const { dispatch } = store; const { dispatch } = store;
dispatch( dispatch(_setAppStateListenerA(_onAppStateChange.bind(undefined, dispatch)));
_setAppStateListenerA(_onAppStateChange.bind(undefined, dispatch)));
break; break;
} }
@ -51,37 +42,6 @@ MiddlewareRegistry.register(store => next => action => {
return 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 * Called by React Native's AppState API to notify that the application state
* has changed. Dispatches the change within the (associated) redux store. * has changed. Dispatches the change within the (associated) redux store.

View File

@ -14,9 +14,7 @@ const DEFAULT_STATE = {
appState: 'active' appState: 'active'
}; };
ReducerRegistry.register( ReducerRegistry.register('features/background', (state = DEFAULT_STATE, action) => {
'features/background',
(state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case _SET_APP_STATE_LISTENER: case _SET_APP_STATE_LISTENER:
return { return {
@ -32,4 +30,4 @@ ReducerRegistry.register(
} }
return state; return state;
}); });

View File

@ -6,13 +6,13 @@ import uuid from 'uuid';
import { createTrackMutedEvent, sendAnalytics } from '../../analytics'; import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
import { appNavigate } from '../../app'; import { appNavigate } from '../../app';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { SET_AUDIO_ONLY } from '../../base/audio-only';
import { import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN, CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE, CONFERENCE_WILL_LEAVE,
SET_AUDIO_ONLY,
getConferenceName, getConferenceName,
getCurrentConference getCurrentConference
} from '../../base/conference'; } from '../../base/conference';

View File

@ -46,7 +46,7 @@ MiddlewareRegistry.register(store => next => action => {
StateListenerRegistry.register( StateListenerRegistry.register(
/* selector */ state => { /* selector */ state => {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state); const conference = getCurrentConference(state);
return conference ? !audioOnly : false; return conference ? !audioOnly : false;
@ -68,7 +68,7 @@ function _onImmersiveChange({ getState }) {
const { appState } = state['features/background']; const { appState } = state['features/background'];
if (appState === 'active') { if (appState === 'active') {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state); const conference = getCurrentConference(state);
const fullScreen = conference ? !audioOnly : false; const fullScreen = conference ? !audioOnly : false;

View File

@ -11,7 +11,7 @@ import { StateListenerRegistry } from '../../base/redux';
*/ */
StateListenerRegistry.register( StateListenerRegistry.register(
/* selector */ state => { /* selector */ state => {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state); const conference = getCurrentConference(state);
return Boolean(conference && audioOnly); return Boolean(conference && audioOnly);

View File

@ -9,7 +9,7 @@ import { StateListenerRegistry } from '../../base/redux';
*/ */
StateListenerRegistry.register( StateListenerRegistry.register(
/* selector */ state => { /* selector */ state => {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state); const conference = getCurrentConference(state);
return Boolean(conference && !audioOnly); return Boolean(conference && !audioOnly);

View File

@ -7,7 +7,7 @@ import {
createToolbarEvent, createToolbarEvent,
sendAnalytics sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { setAudioOnly } from '../../base/conference'; import { setAudioOnly } from '../../base/audio-only';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { import {
MEDIA_TYPE, MEDIA_TYPE,
@ -162,7 +162,7 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
* }} * }}
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks']; const tracks = state['features/base/tracks'];
return { return {

View File

@ -1,6 +1,6 @@
// @flow // @flow
import { toggleAudioOnly } from '../../../base/conference'; import { toggleAudioOnly } from '../../../base/audio-only';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { AbstractButton } from '../../../base/toolbox'; import { AbstractButton } from '../../../base/toolbox';
@ -66,7 +66,7 @@ class AudioOnlyButton extends AbstractButton<Props, *> {
* }} * }}
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
return { return {
_audioOnly: Boolean(audioOnly) _audioOnly: Boolean(audioOnly)

View File

@ -71,7 +71,7 @@ class ToggleCameraButton extends AbstractButton<Props, *> {
* }} * }}
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks']; const tracks = state['features/base/tracks'];
return { return {

View File

@ -33,7 +33,7 @@ export default class AbstractVideoQualityLabel<P: Props> extends Component<P> {
* }} * }}
*/ */
export function _abstractMapStateToProps(state: Object) { export function _abstractMapStateToProps(state: Object) {
const { audioOnly } = state['features/base/conference']; const { enabled: audioOnly } = state['features/base/audio-only'];
return { return {
_audioOnly: audioOnly _audioOnly: audioOnly

View File

@ -96,7 +96,7 @@ class OverflowMenuVideoQualityItem extends Component<Props> {
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
return { return {
_audioOnly: state['features/base/conference'].audioOnly, _audioOnly: state['features/base/audio-only'].enabled,
_receiverVideoQuality: _receiverVideoQuality:
state['features/base/conference'].preferredReceiverVideoQuality state['features/base/conference'].preferredReceiverVideoQuality
}; };

View File

@ -156,7 +156,7 @@ function _mapResolutionToTranslationsKeys(resolution) {
* }} * }}
*/ */
function _mapStateToProps(state) { 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 { resolution, participantId } = state['features/large-video'];
const videoTrackOnLargeVideo = getTrackByMediaTypeAndParticipant( const videoTrackOnLargeVideo = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], state['features/base/tracks'],

View File

@ -4,15 +4,9 @@ import InlineMessage from '@atlaskit/inline-message';
import React, { Component } from 'react'; import React, { Component } from 'react';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { import { createToolbarEvent, sendAnalytics } from '../../analytics';
createToolbarEvent, import { setAudioOnly } from '../../base/audio-only';
sendAnalytics import { VIDEO_QUALITY_LEVELS, setPreferredReceiverVideoQuality } from '../../base/conference';
} from '../../analytics';
import {
VIDEO_QUALITY_LEVELS,
setAudioOnly,
setPreferredReceiverVideoQuality
} from '../../base/conference';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import JitsiMeetJS from '../../base/lib-jitsi-meet'; import JitsiMeetJS from '../../base/lib-jitsi-meet';
import { connect } from '../../base/redux'; import { connect } from '../../base/redux';
@ -406,11 +400,8 @@ class VideoQualitySlider extends Component<Props> {
* }} * }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
const { const { enabled: audioOnly } = state['features/base/audio-only'];
audioOnly, const { p2p, preferredReceiverVideoQuality } = state['features/base/conference'];
p2p,
preferredReceiverVideoQuality
} = state['features/base/conference'];
return { return {
_audioOnly: audioOnly, _audioOnly: audioOnly,