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.
|
||||
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: {
|
||||
//
|
||||
|
|
|
@ -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",
|
||||
"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": {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 { 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) {
|
||||
|
|
|
@ -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