feat(config): add last N limit mapping (#7422)
Adds 'lastNLimits' config value which allows to define last N value per number of participants. See config.js for more details.
This commit is contained in:
parent
168dbd6276
commit
cc9cb6a874
|
@ -0,0 +1,29 @@
|
||||||
|
// babel is used for jest
|
||||||
|
// FIXME make jest work with webpack if possible?
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
plugins: [
|
||||||
|
|
||||||
|
// Stage 2
|
||||||
|
'@babel/plugin-proposal-export-default-from',
|
||||||
|
'@babel/plugin-proposal-export-namespace-from',
|
||||||
|
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
|
||||||
|
// Stage 3
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
[ '@babel/plugin-proposal-class-properties', { loose: false } ],
|
||||||
|
'@babel/plugin-proposal-json-strings',
|
||||||
|
|
||||||
|
// lib-jitsi-meet
|
||||||
|
'@babel/plugin-transform-flow-strip-types'
|
||||||
|
],
|
||||||
|
presets: [
|
||||||
|
'@babel/env',
|
||||||
|
'@babel/preset-flow',
|
||||||
|
'@babel/react'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
16
config.js
16
config.js
|
@ -212,6 +212,22 @@ var config = {
|
||||||
// Default value for the channel "last N" attribute. -1 for unlimited.
|
// Default value for the channel "last N" attribute. -1 for unlimited.
|
||||||
channelLastN: -1,
|
channelLastN: -1,
|
||||||
|
|
||||||
|
// Provides a way to use different "last N" values based on the number of participants in the conference.
|
||||||
|
// The keys in an Object represent number of participants and the values are "last N" to be used when number of
|
||||||
|
// participants gets to or above the number.
|
||||||
|
//
|
||||||
|
// For the given example mapping, "last N" will be set to 20 as long as there are at least 5, but less than
|
||||||
|
// 29 participants in the call and it will be lowered to 15 when the 30th participant joins. The 'channelLastN'
|
||||||
|
// will be used as default until the first threshold is reached.
|
||||||
|
//
|
||||||
|
// lastNLimits: {
|
||||||
|
// 5: 20,
|
||||||
|
// 30: 15,
|
||||||
|
// 50: 10,
|
||||||
|
// 70: 5,
|
||||||
|
// 90: 2
|
||||||
|
// },
|
||||||
|
|
||||||
// // Options for the recording limit notification.
|
// // Options for the recording limit notification.
|
||||||
// recordingLimit: {
|
// recordingLimit: {
|
||||||
//
|
//
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
moduleFileExtensions: [
|
||||||
|
'js'
|
||||||
|
],
|
||||||
|
testMatch: [
|
||||||
|
'<rootDir>/react/**/?(*.)+(test)?(.web).js?(x)'
|
||||||
|
],
|
||||||
|
verbose: true
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -125,6 +125,7 @@
|
||||||
"expose-loader": "0.7.5",
|
"expose-loader": "0.7.5",
|
||||||
"flow-bin": "0.104.0",
|
"flow-bin": "0.104.0",
|
||||||
"imports-loader": "0.7.1",
|
"imports-loader": "0.7.1",
|
||||||
|
"jest": "26.1.0",
|
||||||
"jetifier": "1.6.4",
|
"jetifier": "1.6.4",
|
||||||
"metro-react-native-babel-preset": "0.56.0",
|
"metro-react-native-babel-preset": "0.56.0",
|
||||||
"node-sass": "4.14.1",
|
"node-sass": "4.14.1",
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . && flow",
|
"lint": "eslint . && flow",
|
||||||
"postinstall": "jetify",
|
"postinstall": "jetify",
|
||||||
|
"test": "jest",
|
||||||
"validate": "npm ls"
|
"validate": "npm ls"
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import '../base/dialog/reducer';
|
||||||
import '../base/flags/reducer';
|
import '../base/flags/reducer';
|
||||||
import '../base/jwt/reducer';
|
import '../base/jwt/reducer';
|
||||||
import '../base/known-domains/reducer';
|
import '../base/known-domains/reducer';
|
||||||
|
import '../base/lastn/reducer';
|
||||||
import '../base/lib-jitsi-meet/reducer';
|
import '../base/lib-jitsi-meet/reducer';
|
||||||
import '../base/logging/reducer';
|
import '../base/logging/reducer';
|
||||||
import '../base/media/reducer';
|
import '../base/media/reducer';
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Checks if the given Object is a correct last N limit mapping, coverts both keys and values to numbers and sorts
|
||||||
|
* the keys in ascending order.
|
||||||
|
*
|
||||||
|
* @param {Object} lastNLimits - The Object to be verified.
|
||||||
|
* @returns {undefined|Map<number, number>}
|
||||||
|
*/
|
||||||
|
export function validateLastNLimits(lastNLimits) {
|
||||||
|
// Checks if only numbers are used
|
||||||
|
if (typeof lastNLimits !== 'object'
|
||||||
|
|| !Object.keys(lastNLimits).length
|
||||||
|
|| Object.keys(lastNLimits)
|
||||||
|
.find(limit => limit === null || isNaN(Number(limit))
|
||||||
|
|| lastNLimits[limit] === null || isNaN(Number(lastNLimits[limit])))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts to numbers and sorts the keys
|
||||||
|
const sortedMapping = new Map();
|
||||||
|
const orderedLimits = Object.keys(lastNLimits)
|
||||||
|
.map(n => Number(n))
|
||||||
|
.sort((n1, n2) => n1 - n2);
|
||||||
|
|
||||||
|
for (const limit of orderedLimits) {
|
||||||
|
sortedMapping.set(limit, Number(lastNLimits[limit]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns "last N" value which corresponds to a level defined in the {@code lastNLimits} mapping. See
|
||||||
|
* {@code config.js} for more detailed explanation on how the mapping is defined.
|
||||||
|
*
|
||||||
|
* @param {number} participantsCount - The current number of participants in the conference.
|
||||||
|
* @param {Map<number, number>} [lastNLimits] - The mapping of number of participants to "last N" values. NOTE that
|
||||||
|
* this function expects a Map that has been preprocessed by {@link validateLastNLimits}, because the keys must be
|
||||||
|
* sorted in ascending order and both keys and values should be numbers.
|
||||||
|
* @returns {number|undefined} - A "last N" number if there was a corresponding "last N" value matched with the number
|
||||||
|
* of participants or {@code undefined} otherwise.
|
||||||
|
*/
|
||||||
|
export function limitLastN(participantsCount, lastNLimits) {
|
||||||
|
let selectedLimit;
|
||||||
|
|
||||||
|
for (const participantsN of lastNLimits.keys()) {
|
||||||
|
if (participantsCount >= participantsN) {
|
||||||
|
selectedLimit = participantsN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedLimit ? lastNLimits.get(selectedLimit) : undefined;
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { limitLastN, validateLastNLimits } from './functions';
|
||||||
|
|
||||||
|
describe('limitsLastN', () => {
|
||||||
|
describe('when a correct limit mapping is given', () => {
|
||||||
|
const limits = new Map();
|
||||||
|
|
||||||
|
limits.set(5, -1);
|
||||||
|
limits.set(10, 8);
|
||||||
|
limits.set(20, 5);
|
||||||
|
|
||||||
|
it('returns undefined when less participants that the first limit', () => {
|
||||||
|
expect(limitLastN(2, limits)).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('picks the first limit correctly', () => {
|
||||||
|
expect(limitLastN(5, limits)).toBe(-1);
|
||||||
|
expect(limitLastN(9, limits)).toBe(-1);
|
||||||
|
});
|
||||||
|
it('picks the middle limit correctly', () => {
|
||||||
|
expect(limitLastN(10, limits)).toBe(8);
|
||||||
|
expect(limitLastN(13, limits)).toBe(8);
|
||||||
|
expect(limitLastN(19, limits)).toBe(8);
|
||||||
|
});
|
||||||
|
it('picks the top limit correctly', () => {
|
||||||
|
expect(limitLastN(20, limits)).toBe(5);
|
||||||
|
expect(limitLastN(23, limits)).toBe(5);
|
||||||
|
expect(limitLastN(100, limits)).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateLastNLimits', () => {
|
||||||
|
describe('validates the input by returning undefined', () => {
|
||||||
|
it('if lastNLimits param is not an Object', () => {
|
||||||
|
expect(validateLastNLimits(5)).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('if any key is not a number', () => {
|
||||||
|
const limits = {
|
||||||
|
'abc': 8,
|
||||||
|
5: -1,
|
||||||
|
20: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('if any value is not a number', () => {
|
||||||
|
const limits = {
|
||||||
|
8: 'something',
|
||||||
|
5: -1,
|
||||||
|
20: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('if any value is null', () => {
|
||||||
|
const limits = {
|
||||||
|
1: 1,
|
||||||
|
5: null,
|
||||||
|
20: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('if any value is undefined', () => {
|
||||||
|
const limits = {
|
||||||
|
1: 1,
|
||||||
|
5: undefined,
|
||||||
|
20: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('if the map is empty', () => {
|
||||||
|
expect(validateLastNLimits({})).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('sorts by the keys', () => {
|
||||||
|
const mappingKeys = validateLastNLimits({
|
||||||
|
10: 5,
|
||||||
|
3: 3,
|
||||||
|
5: 4
|
||||||
|
}).keys();
|
||||||
|
|
||||||
|
expect(mappingKeys.next().value).toBe(3);
|
||||||
|
expect(mappingKeys.next().value).toBe(5);
|
||||||
|
expect(mappingKeys.next().value).toBe(10);
|
||||||
|
expect(mappingKeys.next().done).toBe(true);
|
||||||
|
});
|
||||||
|
it('converts keys and values to numbers', () => {
|
||||||
|
const mapping = validateLastNLimits({
|
||||||
|
3: 3,
|
||||||
|
5: 4,
|
||||||
|
10: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const key of mapping.keys()) {
|
||||||
|
expect(typeof key).toBe('number');
|
||||||
|
expect(typeof mapping.get(key)).toBe('number');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,9 +7,18 @@ import { SCREEN_SHARE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-la
|
||||||
import { shouldDisplayTileView } from '../../video-layout/functions';
|
import { shouldDisplayTileView } from '../../video-layout/functions';
|
||||||
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
|
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
|
||||||
import { CONFERENCE_JOINED } from '../conference/actionTypes';
|
import { CONFERENCE_JOINED } from '../conference/actionTypes';
|
||||||
import { getParticipantById } from '../participants/functions';
|
import {
|
||||||
|
PARTICIPANT_JOINED,
|
||||||
|
PARTICIPANT_KICKED,
|
||||||
|
PARTICIPANT_LEFT
|
||||||
|
} from '../participants/actionTypes';
|
||||||
|
import {
|
||||||
|
getParticipantById,
|
||||||
|
getParticipantCount
|
||||||
|
} from '../participants/functions';
|
||||||
import { MiddlewareRegistry } from '../redux';
|
import { MiddlewareRegistry } from '../redux';
|
||||||
|
|
||||||
|
import { limitLastN } from './functions';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
@ -21,6 +30,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case APP_STATE_CHANGED:
|
case APP_STATE_CHANGED:
|
||||||
case CONFERENCE_JOINED:
|
case CONFERENCE_JOINED:
|
||||||
|
case PARTICIPANT_JOINED:
|
||||||
|
case PARTICIPANT_KICKED:
|
||||||
|
case PARTICIPANT_LEFT:
|
||||||
case SCREEN_SHARE_PARTICIPANTS_UPDATED:
|
case SCREEN_SHARE_PARTICIPANTS_UPDATED:
|
||||||
case SELECT_LARGE_VIDEO_PARTICIPANT:
|
case SELECT_LARGE_VIDEO_PARTICIPANT:
|
||||||
case SET_AUDIO_ONLY:
|
case SET_AUDIO_ONLY:
|
||||||
|
@ -47,6 +59,8 @@ function _updateLastN({ getState }) {
|
||||||
const { appState } = state['features/background'] || {};
|
const { appState } = state['features/background'] || {};
|
||||||
const { enabled: filmStripEnabled } = state['features/filmstrip'];
|
const { enabled: filmStripEnabled } = state['features/filmstrip'];
|
||||||
const config = state['features/base/config'];
|
const config = state['features/base/config'];
|
||||||
|
const { lastNLimits } = state['features/base/lastn'];
|
||||||
|
const participantCount = getParticipantCount(state);
|
||||||
|
|
||||||
if (!conference) {
|
if (!conference) {
|
||||||
logger.debug('There is no active conference, not updating last N');
|
logger.debug('There is no active conference, not updating last N');
|
||||||
|
@ -57,6 +71,13 @@ function _updateLastN({ getState }) {
|
||||||
const defaultLastN = typeof config.channelLastN === 'undefined' ? -1 : config.channelLastN;
|
const defaultLastN = typeof config.channelLastN === 'undefined' ? -1 : config.channelLastN;
|
||||||
let lastN = defaultLastN;
|
let lastN = defaultLastN;
|
||||||
|
|
||||||
|
// Apply last N limit based on the # of participants
|
||||||
|
const limitedLastN = limitLastN(participantCount, lastNLimits);
|
||||||
|
|
||||||
|
if (limitedLastN !== undefined) {
|
||||||
|
lastN = limitedLastN;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof appState !== 'undefined' && appState !== 'active') {
|
if (typeof appState !== 'undefined' && appState !== 'active') {
|
||||||
lastN = 0;
|
lastN = 0;
|
||||||
} else if (audioOnly) {
|
} else if (audioOnly) {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {
|
||||||
|
SET_CONFIG
|
||||||
|
} from '../config';
|
||||||
|
import { ReducerRegistry, set } from '../redux';
|
||||||
|
|
||||||
|
import { validateLastNLimits } from './functions';
|
||||||
|
|
||||||
|
ReducerRegistry.register('features/base/lastn', (state = { }, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_CONFIG:
|
||||||
|
return _setConfig(state, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a specific Redux action SET_CONFIG.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of feature base/lastn.
|
||||||
|
* @param {Action} action - The Redux action SET_CONFIG to reduce.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} The new state after the reduction of the specified action.
|
||||||
|
*/
|
||||||
|
function _setConfig(state, { config }) {
|
||||||
|
return set(state, 'lastNLimits', validateLastNLimits(config.lastNLimits));
|
||||||
|
}
|
Loading…
Reference in New Issue