This commit is contained in:
168
react/features/video-quality/components/Slider.web.tsx
Normal file
168
react/features/video-quality/components/Slider.web.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The 'aria-label' text.
|
||||
*/
|
||||
ariaLabel: string;
|
||||
|
||||
/**
|
||||
* The maximum value for slider value.
|
||||
*/
|
||||
max: number;
|
||||
|
||||
/**
|
||||
* The minimum value for slider value.
|
||||
*/
|
||||
min: number;
|
||||
|
||||
/**
|
||||
* Callback invoked on change.
|
||||
*/
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
|
||||
/**
|
||||
* The granularity that the value must adhere to.
|
||||
*/
|
||||
step: number;
|
||||
|
||||
/**
|
||||
* The current value where the knob is positioned.
|
||||
*/
|
||||
value: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
// keep the same height for all elements:
|
||||
// input, input track & fake track(div)
|
||||
const height = 6;
|
||||
|
||||
const inputTrack = {
|
||||
background: 'transparent',
|
||||
height
|
||||
};
|
||||
const inputThumb = {
|
||||
background: theme.palette.text01,
|
||||
border: 0,
|
||||
borderRadius: '50%',
|
||||
height: 24,
|
||||
width: 24
|
||||
};
|
||||
|
||||
const focused = {
|
||||
outline: `1px solid ${theme.palette.ui06}`
|
||||
};
|
||||
|
||||
return {
|
||||
sliderContainer: {
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
textAlign: 'center'
|
||||
|
||||
},
|
||||
knobContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginLeft: 2,
|
||||
marginRight: 2,
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
},
|
||||
knob: {
|
||||
background: theme.palette.text01,
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
height,
|
||||
width: 6
|
||||
},
|
||||
track: {
|
||||
background: theme.palette.text03,
|
||||
borderRadius: Number(theme.shape.borderRadius) / 2,
|
||||
height
|
||||
},
|
||||
slider: {
|
||||
// Use an additional class here to override global CSS specificity
|
||||
'&.custom-slider': {
|
||||
'-webkit-appearance': 'none',
|
||||
background: 'transparent',
|
||||
height,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
|
||||
'&.focus-visible': {
|
||||
// override global styles in order to use our own color
|
||||
outline: 'none !important',
|
||||
|
||||
'&::-webkit-slider-runnable-track': focused,
|
||||
'&::ms-track': focused,
|
||||
'&::-moz-range-track': focused
|
||||
},
|
||||
|
||||
'&::-webkit-slider-runnable-track': {
|
||||
'-webkit-appearance': 'none',
|
||||
...inputTrack
|
||||
},
|
||||
'&::-webkit-slider-thumb': {
|
||||
'-webkit-appearance': 'none',
|
||||
position: 'relative',
|
||||
top: -6,
|
||||
...inputThumb
|
||||
},
|
||||
|
||||
'&::ms-track': {
|
||||
...inputTrack
|
||||
},
|
||||
'&::-ms-thumb': {
|
||||
...inputThumb
|
||||
},
|
||||
|
||||
'&::-moz-range-track': {
|
||||
...inputTrack
|
||||
},
|
||||
'&::-moz-range-thumb': {
|
||||
...inputThumb
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom slider.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Slider({ ariaLabel, max, min, onChange, step, value }: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
const knobs = [ ...Array(Math.floor((max - min) / step) + 1) ];
|
||||
|
||||
return (
|
||||
<div className = { classes.sliderContainer }>
|
||||
<ul
|
||||
aria-hidden = { true }
|
||||
className = { cx('empty-list', classes.knobContainer) }>
|
||||
{knobs.map((_, i) => (
|
||||
<li
|
||||
className = { classes.knob }
|
||||
key = { `knob-${i}` } />))}
|
||||
</ul>
|
||||
<div className = { classes.track } />
|
||||
<input
|
||||
aria-label = { ariaLabel }
|
||||
className = { cx(classes.slider, 'custom-slider') }
|
||||
max = { max }
|
||||
min = { min }
|
||||
onChange = { onChange }
|
||||
step = { step }
|
||||
type = 'range'
|
||||
value = { value } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Slider;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconPerformance } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
|
||||
import VideoQualityDialog from './VideoQualityDialog.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link VideoQualityButton}.
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether or not audio only mode is currently enabled.
|
||||
*/
|
||||
_audioOnly: boolean;
|
||||
|
||||
/**
|
||||
* The currently configured maximum quality resolution to be received from
|
||||
* and sent to remote participants.
|
||||
*/
|
||||
_videoQuality: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* React {@code Component} responsible for displaying a button in the overflow
|
||||
* menu of the toolbar, including an icon showing the currently selected
|
||||
* max receive quality.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoQualityButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.callQuality';
|
||||
override label = 'videoStatus.performanceSettings';
|
||||
override tooltip = 'videoStatus.performanceSettings';
|
||||
override icon = IconPerformance;
|
||||
|
||||
/**
|
||||
* Handles clicking the button, and opens the video quality dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('video.quality'));
|
||||
|
||||
dispatch(openDialog(VideoQualityDialog));
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(translate(VideoQualityButton));
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
|
||||
import VideoQualitySlider from './VideoQualitySlider.web';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the component
|
||||
* {@code VideoQualitySlider} in a dialog.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
export default class VideoQualityDialog extends Component {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = 'videoStatus.performanceSettings'>
|
||||
<VideoQualitySlider />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import ExpandedLabel, { IProps as AbstractProps } from '../../base/label/components/native/ExpandedLabel';
|
||||
|
||||
import { AUD_LABEL_COLOR } from './styles';
|
||||
|
||||
type Props = AbstractProps & WithTranslation;
|
||||
|
||||
/**
|
||||
* A react {@code Component} that implements an expanded label as tooltip-like
|
||||
* component to explain the meaning of the {@code VideoQualityLabel}.
|
||||
*/
|
||||
class VideoQualityExpandedLabel extends ExpandedLabel<Props> {
|
||||
/**
|
||||
* Returns the color this expanded label should be rendered with.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getColor() {
|
||||
return AUD_LABEL_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label specific text of this {@code ExpandedLabel}.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLabel() {
|
||||
return this.props.t('videoStatus.audioOnlyExpanded');
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(VideoQualityExpandedLabel);
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Label from '../../base/label/components/native/Label';
|
||||
import { StyleType, combineStyles } from '../../base/styles/functions.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether or not the conference is in audio only mode.
|
||||
*/
|
||||
_audioOnly: boolean;
|
||||
|
||||
/**
|
||||
* Style of the component passed as props.
|
||||
*/
|
||||
style?: StyleType;
|
||||
}
|
||||
|
||||
/**
|
||||
* React {@code Component} responsible for displaying a label that indicates
|
||||
* the displayed video state of the current conference.
|
||||
*
|
||||
* NOTE: Due to the lack of actual video quality information on mobile side,
|
||||
* this component currently only displays audio only indicator, but the naming
|
||||
* is kept consistent with web and in the future we may introduce the required
|
||||
* api and extend this component with actual quality indication.
|
||||
*/
|
||||
class VideoQualityLabel extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Implements React {@link Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _audioOnly, style, t } = this.props;
|
||||
|
||||
if (!_audioOnly) {
|
||||
// We don't have info about the quality so no need for the indicator
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label // @ts-ignore
|
||||
style = { combineStyles(styles.indicatorAudioOnly, style) }
|
||||
text = { t('videoStatus.audioOnly') } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code AbstractVideoQualityLabel}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioOnly: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
|
||||
return {
|
||||
_audioOnly: audioOnly
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(VideoQualityLabel));
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IconPerformance } from '../../base/icons/svg';
|
||||
import Label from '../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../base/label/constants';
|
||||
import Tooltip from '../../base/tooltip/components/Tooltip';
|
||||
import { shouldDisplayVideoQualityLabel } from '../selector';
|
||||
|
||||
import VideoQualityDialog from './VideoQualityDialog.web';
|
||||
|
||||
/**
|
||||
* React {@code Component} responsible for displaying a label that indicates
|
||||
* the displayed video state of the current conference. {@code AudioOnlyLabel}
|
||||
* Will display when the conference is in audio only mode. {@code HDVideoLabel}
|
||||
* Will display if not in audio only mode and a high-definition large video is
|
||||
* being displayed.
|
||||
*
|
||||
* @returns {JSX}
|
||||
*/
|
||||
const VideoQualityLabel = () => {
|
||||
const _audioOnly = useSelector((state: IReduxState) => state['features/base/audio-only'].enabled);
|
||||
const _visible = useSelector(shouldDisplayVideoQualityLabel);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!_visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let className, icon, labelContent, tooltipKey;
|
||||
|
||||
if (_audioOnly) {
|
||||
className = 'audio-only';
|
||||
labelContent = t('videoStatus.audioOnly');
|
||||
tooltipKey = 'videoStatus.labelTooltipAudioOnly';
|
||||
} else {
|
||||
className = 'current-video-quality';
|
||||
icon = IconPerformance;
|
||||
tooltipKey = 'videoStatus.performanceSettings';
|
||||
}
|
||||
|
||||
const onClick = () => dispatch(openDialog(VideoQualityDialog));
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { t(tooltipKey) }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
accessibilityText = { t(tooltipKey) }
|
||||
className = { className }
|
||||
color = { COLORS.white }
|
||||
icon = { icon }
|
||||
iconColor = '#fff'
|
||||
id = 'videoResolutionLabel'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { onClick }
|
||||
text = { labelContent } />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoQualityLabel;
|
||||
@@ -0,0 +1,385 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { setAudioOnly } from '../../base/audio-only/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { setLastN } from '../../base/lastn/actions';
|
||||
import { getLastNForQualityLevel } from '../../base/lastn/functions';
|
||||
import { setPreferredVideoQuality } from '../actions';
|
||||
import { DEFAULT_LAST_N, VIDEO_QUALITY_LEVELS } from '../constants';
|
||||
import logger from '../logger';
|
||||
|
||||
import Slider from './Slider.web';
|
||||
|
||||
const {
|
||||
ULTRA,
|
||||
HIGH,
|
||||
STANDARD,
|
||||
LOW
|
||||
} = VIDEO_QUALITY_LEVELS;
|
||||
|
||||
/**
|
||||
* Creates an analytics event for a press of one of the buttons in the video
|
||||
* quality dialog.
|
||||
*
|
||||
* @param {string} quality - The quality which was selected.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
const createEvent = function(quality: string) {
|
||||
return createToolbarEvent(
|
||||
'video.quality',
|
||||
{
|
||||
quality
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoQualitySlider}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether or not the conference is in audio only mode.
|
||||
*/
|
||||
_audioOnly: Boolean;
|
||||
|
||||
/**
|
||||
* The channelLastN value configured for the conference.
|
||||
*/
|
||||
_channelLastN?: number;
|
||||
|
||||
/**
|
||||
* Whether or not the conference is in peer to peer mode.
|
||||
*/
|
||||
_p2p?: Object;
|
||||
|
||||
/**
|
||||
* The currently configured maximum quality resolution to be sent and
|
||||
* received from the remote participants.
|
||||
*/
|
||||
_sendrecvVideoQuality: number;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Invoked to request toggling of audio only mode.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
dialog: {
|
||||
color: theme.palette.text01
|
||||
},
|
||||
dialogDetails: {
|
||||
...theme.typography.bodyShortRegularLarge,
|
||||
marginBottom: 16
|
||||
},
|
||||
dialogContents: {
|
||||
background: theme.palette.ui01,
|
||||
padding: '16px 16px 48px 16px'
|
||||
},
|
||||
sliderDescription: {
|
||||
...theme.typography.heading6,
|
||||
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 40
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a slider for selecting a
|
||||
* new receive video quality.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoQualitySlider extends Component<IProps> {
|
||||
_sliderOptions: Array<{
|
||||
audioOnly?: boolean;
|
||||
onSelect: Function;
|
||||
textKey: string;
|
||||
videoQuality?: number;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoQualitySlider} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._enableAudioOnly = this._enableAudioOnly.bind(this);
|
||||
this._enableHighDefinition = this._enableHighDefinition.bind(this);
|
||||
this._enableLowDefinition = this._enableLowDefinition.bind(this);
|
||||
this._enableStandardDefinition
|
||||
= this._enableStandardDefinition.bind(this);
|
||||
this._enableUltraHighDefinition = this._enableUltraHighDefinition.bind(this);
|
||||
this._onSliderChange = this._onSliderChange.bind(this);
|
||||
|
||||
/**
|
||||
* An array of configuration options for displaying a choice in the
|
||||
* input. The onSelect callback will be invoked when the option is
|
||||
* selected and videoQuality helps determine which choice matches with
|
||||
* the currently active quality level.
|
||||
*
|
||||
* @private
|
||||
* @type {Object[]}
|
||||
*/
|
||||
this._sliderOptions = [
|
||||
{
|
||||
audioOnly: true,
|
||||
onSelect: this._enableAudioOnly,
|
||||
textKey: 'audioOnly.audioOnly'
|
||||
},
|
||||
{
|
||||
onSelect: this._enableLowDefinition,
|
||||
textKey: 'videoStatus.lowDefinition',
|
||||
videoQuality: LOW
|
||||
},
|
||||
{
|
||||
onSelect: this._enableStandardDefinition,
|
||||
textKey: 'videoStatus.standardDefinition',
|
||||
videoQuality: STANDARD
|
||||
},
|
||||
{
|
||||
onSelect: this._enableUltraHighDefinition,
|
||||
textKey: 'videoStatus.highDefinition',
|
||||
videoQuality: ULTRA
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
const activeSliderOption = this._mapCurrentQualityToSliderValue();
|
||||
|
||||
return (
|
||||
<div className = { clsx('video-quality-dialog', classes.dialog) }>
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { classes.dialogDetails }>
|
||||
{t('videoStatus.adjustFor')}
|
||||
</div>
|
||||
<div className = { classes.dialogContents }>
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { classes.sliderDescription }>
|
||||
<span>{t('videoStatus.bestPerformance')}</span>
|
||||
<span>{t('videoStatus.highestQuality')}</span>
|
||||
</div>
|
||||
<Slider
|
||||
ariaLabel = { t('videoStatus.callQuality') }
|
||||
max = { this._sliderOptions.length - 1 }
|
||||
min = { 0 }
|
||||
onChange = { this._onSliderChange }
|
||||
step = { 1 }
|
||||
value = { activeSliderOption } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to enable audio only mode.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_enableAudioOnly() {
|
||||
sendAnalytics(createEvent('audio.only'));
|
||||
logger.log('Video quality: audio only enabled');
|
||||
this.props.dispatch(setAudioOnly(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the action of the high definition video being selected.
|
||||
* Dispatches an action to receive high quality video from remote
|
||||
* participants.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_enableHighDefinition() {
|
||||
sendAnalytics(createEvent('high'));
|
||||
logger.log('Video quality: high enabled');
|
||||
this._setPreferredVideoQuality(HIGH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to receive low quality video from remote
|
||||
* participants.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_enableLowDefinition() {
|
||||
sendAnalytics(createEvent('low'));
|
||||
logger.log('Video quality: low enabled');
|
||||
this._setPreferredVideoQuality(LOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to receive standard quality video from remote
|
||||
* participants.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_enableStandardDefinition() {
|
||||
sendAnalytics(createEvent('standard'));
|
||||
logger.log('Video quality: standard enabled');
|
||||
this._setPreferredVideoQuality(STANDARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to receive ultra HD quality video from remote
|
||||
* participants.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_enableUltraHighDefinition() {
|
||||
sendAnalytics(createEvent('ultra high'));
|
||||
logger.log('Video quality: ultra high enabled');
|
||||
this._setPreferredVideoQuality(ULTRA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the current video quality state with corresponding index of the
|
||||
* component's slider options.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_mapCurrentQualityToSliderValue() {
|
||||
const { _audioOnly, _sendrecvVideoQuality } = this.props;
|
||||
const { _sliderOptions } = this;
|
||||
|
||||
if (_audioOnly) {
|
||||
const audioOnlyOption = _sliderOptions.find(
|
||||
({ audioOnly }) => audioOnly);
|
||||
|
||||
// @ts-ignore
|
||||
return _sliderOptions.indexOf(audioOnlyOption);
|
||||
}
|
||||
|
||||
for (let i = 0; i < _sliderOptions.length; i++) {
|
||||
if (Number(_sliderOptions[i].videoQuality) >= _sendrecvVideoQuality) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a callback when the selected video quality changes.
|
||||
*
|
||||
* @param {Object} event - The slider's change event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSliderChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { _audioOnly, _sendrecvVideoQuality } = this.props;
|
||||
const {
|
||||
// @ts-ignore
|
||||
audioOnly,
|
||||
|
||||
// @ts-ignore
|
||||
onSelect,
|
||||
|
||||
// @ts-ignore
|
||||
videoQuality
|
||||
} = this._sliderOptions[event.target.value as keyof typeof this._sliderOptions];
|
||||
|
||||
// Take no action if the newly chosen option does not change audio only
|
||||
// or video quality state.
|
||||
if ((_audioOnly && audioOnly)
|
||||
|| (!_audioOnly && videoQuality === _sendrecvVideoQuality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for changing the preferred maximum video quality to receive and
|
||||
* disable audio only.
|
||||
*
|
||||
* @param {number} qualityLevel - The new maximum video quality. Should be
|
||||
* a value enumerated in {@code VIDEO_QUALITY_LEVELS}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setPreferredVideoQuality(qualityLevel: number) {
|
||||
this.props.dispatch(setPreferredVideoQuality(qualityLevel));
|
||||
if (this.props._audioOnly) {
|
||||
this.props.dispatch(setAudioOnly(false));
|
||||
}
|
||||
|
||||
// Determine the lastN value based on the quality setting.
|
||||
let { _channelLastN = DEFAULT_LAST_N } = this.props;
|
||||
|
||||
_channelLastN = _channelLastN === -1 ? DEFAULT_LAST_N : _channelLastN;
|
||||
const lastN = getLastNForQualityLevel(qualityLevel, _channelLastN);
|
||||
|
||||
// Set the lastN for the conference.
|
||||
this.props.dispatch(setLastN(lastN));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code VideoQualitySlider} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
const { p2p } = state['features/base/conference'];
|
||||
const { preferredVideoQuality } = state['features/video-quality'];
|
||||
const { channelLastN } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_audioOnly: audioOnly,
|
||||
_channelLastN: channelLastN,
|
||||
_p2p: p2p,
|
||||
_sendrecvVideoQuality: preferredVideoQuality
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(withStyles(VideoQualitySlider, styles)));
|
||||
20
react/features/video-quality/components/styles.ts
Normal file
20
react/features/video-quality/components/styles.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ColorPalette } from '../../base/styles/components/styles/ColorPalette';
|
||||
import { createStyleSheet } from '../../base/styles/functions.any';
|
||||
import BaseTheme from '../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const AUD_LABEL_COLOR = ColorPalette.green;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of the feature video-quality.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Style for the audio-only indicator.
|
||||
*/
|
||||
indicatorAudioOnly: {
|
||||
backgroundColor: AUD_LABEL_COLOR,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
height: 32
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user