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:
Paweł Domas 2020-08-03 12:39:17 -05:00 committed by GitHub
parent 168dbd6276
commit cc9cb6a874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 7538 additions and 3 deletions

29
babel.config.js Normal file
View File

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

View File

@ -212,6 +212,22 @@ var config = {
// Default value for the channel "last N" attribute. -1 for unlimited.
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.
// recordingLimit: {
//

9
jest.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
moduleFileExtensions: [
'js'
],
testMatch: [
'<rootDir>/react/**/?(*.)+(test)?(.web).js?(x)'
],
verbose: true
};

7282
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -125,6 +125,7 @@
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"jest": "26.1.0",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.14.1",
@ -144,6 +145,7 @@
"scripts": {
"lint": "eslint . && flow",
"postinstall": "jetify",
"test": "jest",
"validate": "npm ls"
},
"browser": {

View File

@ -11,6 +11,7 @@ import '../base/dialog/reducer';
import '../base/flags/reducer';
import '../base/jwt/reducer';
import '../base/known-domains/reducer';
import '../base/lastn/reducer';
import '../base/lib-jitsi-meet/reducer';
import '../base/logging/reducer';
import '../base/media/reducer';

View File

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

View File

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

View File

@ -7,9 +7,18 @@ import { SCREEN_SHARE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-la
import { shouldDisplayTileView } from '../../video-layout/functions';
import { SET_AUDIO_ONLY } from '../audio-only/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 { limitLastN } from './functions';
import logger from './logger';
declare var APP: Object;
@ -21,6 +30,9 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_STATE_CHANGED:
case CONFERENCE_JOINED:
case PARTICIPANT_JOINED:
case PARTICIPANT_KICKED:
case PARTICIPANT_LEFT:
case SCREEN_SHARE_PARTICIPANTS_UPDATED:
case SELECT_LARGE_VIDEO_PARTICIPANT:
case SET_AUDIO_ONLY:
@ -47,6 +59,8 @@ function _updateLastN({ getState }) {
const { appState } = state['features/background'] || {};
const { enabled: filmStripEnabled } = state['features/filmstrip'];
const config = state['features/base/config'];
const { lastNLimits } = state['features/base/lastn'];
const participantCount = getParticipantCount(state);
if (!conference) {
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;
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') {
lastN = 0;
} else if (audioOnly) {

View File

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