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.
This commit is contained in:
parent
89ad76142d
commit
d3d5847605
15
config.js
15
config.js
|
@ -244,6 +244,21 @@ var config = {
|
||||||
// low: 200000,
|
// low: 200000,
|
||||||
// standard: 500000,
|
// standard: 500000,
|
||||||
// high: 1500000
|
// 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'
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,7 @@ import {
|
||||||
AVATAR_ID_COMMAND,
|
AVATAR_ID_COMMAND,
|
||||||
AVATAR_URL_COMMAND,
|
AVATAR_URL_COMMAND,
|
||||||
EMAIL_COMMAND,
|
EMAIL_COMMAND,
|
||||||
JITSI_CONFERENCE_URL_KEY,
|
JITSI_CONFERENCE_URL_KEY
|
||||||
VIDEO_QUALITY_LEVELS
|
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
|
|
||||||
|
@ -214,38 +213,6 @@ export function getCurrentConference(stateful: Function | Object) {
|
||||||
return joining || passwordRequired || membersOnly;
|
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.
|
* Returns the stored room name.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
|
@ -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<number, number>} 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, number>): 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<number, number>|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<number, number> {
|
||||||
|
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;
|
||||||
|
}
|
|
@ -1,2 +1,4 @@
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
|
|
||||||
|
import './reducer';
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import {
|
import {
|
||||||
CONFERENCE_JOINED,
|
CONFERENCE_JOINED,
|
||||||
VIDEO_QUALITY_LEVELS,
|
VIDEO_QUALITY_LEVELS,
|
||||||
getNearestReceiverVideoQualityLevel,
|
|
||||||
setMaxReceiverVideoQuality,
|
setMaxReceiverVideoQuality,
|
||||||
setPreferredVideoQuality
|
setPreferredVideoQuality
|
||||||
} from '../base/conference';
|
} from '../base/conference';
|
||||||
|
@ -11,7 +10,9 @@ import { getParticipantCount } from '../base/participants';
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import { shouldDisplayTileView } from '../video-layout';
|
import { shouldDisplayTileView } from '../video-layout';
|
||||||
|
|
||||||
|
import { getReceiverVideoQualityLevel } from './functions';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
|
import { getMinHeightForQualityLvlMap } from './selector';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the middleware of the feature video-quality.
|
* Implements the middleware of the feature video-quality.
|
||||||
|
@ -66,7 +67,7 @@ StateListenerRegistry.register(
|
||||||
if (reducedUI) {
|
if (reducedUI) {
|
||||||
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
|
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
|
||||||
} else if (displayTileView && !Number.isNaN(thumbnailHeight)) {
|
} 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
|
// Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
|
||||||
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) {
|
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) {
|
||||||
|
@ -74,7 +75,7 @@ StateListenerRegistry.register(
|
||||||
= participantCount > maxFullResolutionParticipants
|
= participantCount > maxFullResolutionParticipants
|
||||||
&& newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD;
|
&& 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}, `
|
+ `is: ${newMaxRecvVideoQuality}, `
|
||||||
+ `override: ${String(override)}, `
|
+ `override: ${String(override)}, `
|
||||||
+ `max full res N: ${maxFullResolutionParticipants}`);
|
+ `max full res N: ${maxFullResolutionParticipants}`);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<number,number>}
|
||||||
|
*/
|
||||||
|
export function getMinHeightForQualityLvlMap(state: Object): Map<number, number> {
|
||||||
|
return state['features/base/videoquality'].minHeightForQualityLvl;
|
||||||
|
}
|
Loading…
Reference in New Issue