diff --git a/config.js b/config.js index eb9533751..97f6c296b 100644 --- a/config.js +++ b/config.js @@ -244,6 +244,21 @@ var config = { // low: 200000, // standard: 500000, // high: 1500000 + // }, + // + // // The options can be used to override default thresholds of video thumbnail heights corresponding to + // // the video quality levels used in the application. At the time of this writing the allowed levels are: + // // 'low' - for the low quality level (180p at the time of this writing) + // // 'standard' - for the medium quality level (360p) + // // 'high' - for the high quality level (720p) + // // The keys should be positive numbers which represent the minimal thumbnail height for the quality level. + // // + // // With the default config value below the application will use 'low' quality until the thumbnails are + // // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to + // // the high quality. + // minHeightForQualityLvl: { + // 360: 'standard, + // 720: 'high' // } // }, diff --git a/react/features/base/conference/functions.js b/react/features/base/conference/functions.js index bd9cd5f5f..2c39dd8bd 100644 --- a/react/features/base/conference/functions.js +++ b/react/features/base/conference/functions.js @@ -17,8 +17,7 @@ import { AVATAR_ID_COMMAND, AVATAR_URL_COMMAND, EMAIL_COMMAND, - JITSI_CONFERENCE_URL_KEY, - VIDEO_QUALITY_LEVELS + JITSI_CONFERENCE_URL_KEY } from './constants'; import logger from './logger'; @@ -214,38 +213,6 @@ export function getCurrentConference(stateful: Function | Object) { return joining || passwordRequired || membersOnly; } -/** - * Finds the nearest match for the passed in {@link availableHeight} to am - * enumerated value in {@code VIDEO_QUALITY_LEVELS}. - * - * @param {number} availableHeight - The height to which a matching video - * quality level should be found. - * @returns {number} The closest matching value from - * {@code VIDEO_QUALITY_LEVELS}. - */ -export function getNearestReceiverVideoQualityLevel(availableHeight: number) { - const qualityLevels = [ - VIDEO_QUALITY_LEVELS.HIGH, - VIDEO_QUALITY_LEVELS.STANDARD, - VIDEO_QUALITY_LEVELS.LOW - ]; - - let selectedLevel = qualityLevels[0]; - - for (let i = 1; i < qualityLevels.length; i++) { - const previousValue = qualityLevels[i - 1]; - const currentValue = qualityLevels[i]; - const diffWithCurrent = Math.abs(availableHeight - currentValue); - const diffWithPrevious = Math.abs(availableHeight - previousValue); - - if (diffWithCurrent < diffWithPrevious) { - selectedLevel = currentValue; - } - } - - return selectedLevel; -} - /** * Returns the stored room name. * diff --git a/react/features/video-quality/constants.js b/react/features/video-quality/constants.js new file mode 100644 index 000000000..3306fd147 --- /dev/null +++ b/react/features/video-quality/constants.js @@ -0,0 +1,12 @@ +import { VIDEO_QUALITY_LEVELS } from '../base/conference'; + +/** + * Maps quality level names used in the config.videoQuality.minHeightForQualityLvl to the quality level constants used + * by the application. + * @type {Object} + */ +export const CFG_LVL_TO_APP_QUALITY_LVL = { + 'low': VIDEO_QUALITY_LEVELS.LOW, + 'standard': VIDEO_QUALITY_LEVELS.STANDARD, + 'high': VIDEO_QUALITY_LEVELS.HIGH +}; diff --git a/react/features/video-quality/functions.js b/react/features/video-quality/functions.js new file mode 100644 index 000000000..a17e7fae5 --- /dev/null +++ b/react/features/video-quality/functions.js @@ -0,0 +1,62 @@ +// @flow + +import { VIDEO_QUALITY_LEVELS } from '../base/conference'; + +import { CFG_LVL_TO_APP_QUALITY_LVL } from './constants'; + + +/** + * Selects {@code VIDEO_QUALITY_LEVELS} for the given {@link availableHeight} and threshold to quality mapping. + * + * @param {number} availableHeight - The height to which a matching video quality level should be found. + * @param {Map} heightToLevel - The threshold to quality level mapping. The keys are sorted in the + * ascending order. + * @returns {number} The matching value from {@code VIDEO_QUALITY_LEVELS}. + */ +export function getReceiverVideoQualityLevel(availableHeight: number, heightToLevel: Map): number { + let selectedLevel = VIDEO_QUALITY_LEVELS.LOW; + + for (const [ levelThreshold, level ] of heightToLevel.entries()) { + if (availableHeight >= levelThreshold) { + selectedLevel = level; + } + } + + return selectedLevel; +} + +/** + * Converts {@code Object} passed in the config which represents height thresholds to vide quality level mapping to + * a {@code Map}. + * + * @param {Object} minHeightForQualityLvl - The 'config.videoQuality.minHeightForQualityLvl' Object from + * the configuration. See config.js for more details. + * @returns {Map|undefined} - A mapping of minimal thumbnail height required for given quality level or + * {@code undefined} if the map contains invalid values. + */ +export function validateMinHeightForQualityLvl(minHeightForQualityLvl: Object): ?Map { + if (typeof minHeightForQualityLvl !== 'object' + || Object.keys(minHeightForQualityLvl).map(lvl => Number(lvl)) + .find(lvl => lvl === null || isNaN(lvl) || lvl < 0)) { + return undefined; + } + + const levelsSorted + = Object.keys(minHeightForQualityLvl) + .map(k => Number(k)) + .sort((a, b) => a - b); + const map = new Map(); + + for (const level of levelsSorted) { + const configQuality = minHeightForQualityLvl[level]; + const appQuality = CFG_LVL_TO_APP_QUALITY_LVL[configQuality]; + + if (!appQuality) { + return undefined; + } + + map.set(level, appQuality); + } + + return map; +} diff --git a/react/features/video-quality/index.js b/react/features/video-quality/index.js index a6be21126..6d28f521e 100644 --- a/react/features/video-quality/index.js +++ b/react/features/video-quality/index.js @@ -1,2 +1,4 @@ export * from './components'; export * from './actions'; + +import './reducer'; diff --git a/react/features/video-quality/middleware.js b/react/features/video-quality/middleware.js index 81f64594f..50f67a8c1 100644 --- a/react/features/video-quality/middleware.js +++ b/react/features/video-quality/middleware.js @@ -3,7 +3,6 @@ import { CONFERENCE_JOINED, VIDEO_QUALITY_LEVELS, - getNearestReceiverVideoQualityLevel, setMaxReceiverVideoQuality, setPreferredVideoQuality } from '../base/conference'; @@ -11,7 +10,9 @@ import { getParticipantCount } from '../base/participants'; import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { shouldDisplayTileView } from '../video-layout'; +import { getReceiverVideoQualityLevel } from './functions'; import logger from './logger'; +import { getMinHeightForQualityLvlMap } from './selector'; /** * Implements the middleware of the feature video-quality. @@ -66,7 +67,7 @@ StateListenerRegistry.register( if (reducedUI) { newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW; } else if (displayTileView && !Number.isNaN(thumbnailHeight)) { - newMaxRecvVideoQuality = getNearestReceiverVideoQualityLevel(thumbnailHeight); + newMaxRecvVideoQuality = getReceiverVideoQualityLevel(thumbnailHeight, getMinHeightForQualityLvlMap(state)); // Override HD level calculated for the thumbnail height when # of participants threshold is exceeded if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) { @@ -74,7 +75,7 @@ StateListenerRegistry.register( = participantCount > maxFullResolutionParticipants && newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD; - logger.info(`The nearest receiver video quality level for thumbnail height: ${thumbnailHeight}, ` + logger.info(`Video quality level for thumbnail height: ${thumbnailHeight}, ` + `is: ${newMaxRecvVideoQuality}, ` + `override: ${String(override)}, ` + `max full res N: ${maxFullResolutionParticipants}`); diff --git a/react/features/video-quality/reducer.js b/react/features/video-quality/reducer.js new file mode 100644 index 000000000..d1c17b4a0 --- /dev/null +++ b/react/features/video-quality/reducer.js @@ -0,0 +1,41 @@ +import { VIDEO_QUALITY_LEVELS } from '../base/conference'; +import { SET_CONFIG } from '../base/config'; +import { ReducerRegistry, set } from '../base/redux'; + +import { validateMinHeightForQualityLvl } from './functions'; +import logger from './logger'; + +const DEFAULT_STATE = { + minHeightForQualityLvl: new Map() +}; + +DEFAULT_STATE.minHeightForQualityLvl.set(360, VIDEO_QUALITY_LEVELS.STANDARD); +DEFAULT_STATE.minHeightForQualityLvl.set(720, VIDEO_QUALITY_LEVELS.HIGH); + +ReducerRegistry.register('features/base/videoquality', (state = DEFAULT_STATE, action) => { + switch (action.type) { + case SET_CONFIG: + return _setConfig(state, action); + } + + return state; +}); + +/** + * Extracts the height to quality level mapping from the new 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 }) { + const configuredMap = config?.videoQuality?.minHeightForQualityLvl; + const convertedMap = validateMinHeightForQualityLvl(configuredMap); + + if (configuredMap && !convertedMap) { + logger.error('Invalid config value videoQuality.minHeightForQualityLvl'); + } + + return convertedMap ? set(state, 'minHeightForQualityLvl', convertedMap) : state; +} diff --git a/react/features/video-quality/selector.js b/react/features/video-quality/selector.js new file mode 100644 index 000000000..45565cf16 --- /dev/null +++ b/react/features/video-quality/selector.js @@ -0,0 +1,11 @@ +// @flow + +/** + * Selects the thumbnail height to the quality level mapping from the config. + * + * @param {Object} state - The redux state. + * @returns {Map} + */ +export function getMinHeightForQualityLvlMap(state: Object): Map { + return state['features/base/videoquality'].minHeightForQualityLvl; +}