From d3d584760595f2387992acd294aaa05e09c90a61 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Wed, 19 Aug 2020 16:38:10 -0500 Subject: [PATCH] feat: configurable quality levels for video height Allows to adjust thresholds which control the video quality level in the thumbnail view. Changes the default behaviour to request the SD (360p) resolution only when the thumbnails are at least 360 pixels tall and the height of 720 is required for the high quality level. The thresholds can be configured with the 'videoQuality.minHeightForQualityLvl' config property. Check the description in the config.js for more details. --- config.js | 15 +++++ react/features/base/conference/functions.js | 35 +----------- react/features/video-quality/constants.js | 12 ++++ react/features/video-quality/functions.js | 62 +++++++++++++++++++++ react/features/video-quality/index.js | 2 + react/features/video-quality/middleware.js | 7 ++- react/features/video-quality/reducer.js | 41 ++++++++++++++ react/features/video-quality/selector.js | 11 ++++ 8 files changed, 148 insertions(+), 37 deletions(-) create mode 100644 react/features/video-quality/constants.js create mode 100644 react/features/video-quality/functions.js create mode 100644 react/features/video-quality/reducer.js create mode 100644 react/features/video-quality/selector.js 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; +}