You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VideoQualitySlider.web.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. // @flow
  2. import React, { Component } from 'react';
  3. import type { Dispatch } from 'redux';
  4. import { createToolbarEvent, sendAnalytics } from '../../analytics';
  5. import { setAudioOnly } from '../../base/audio-only';
  6. import { translate } from '../../base/i18n';
  7. import { setLastN, getLastNForQualityLevel } from '../../base/lastn';
  8. import { connect } from '../../base/redux';
  9. import { setPreferredVideoQuality } from '../actions';
  10. import { DEFAULT_LAST_N, VIDEO_QUALITY_LEVELS } from '../constants';
  11. import logger from '../logger';
  12. const {
  13. ULTRA,
  14. HIGH,
  15. STANDARD,
  16. LOW
  17. } = VIDEO_QUALITY_LEVELS;
  18. /**
  19. * Creates an analytics event for a press of one of the buttons in the video
  20. * quality dialog.
  21. *
  22. * @param {string} quality - The quality which was selected.
  23. * @returns {Object} The event in a format suitable for sending via
  24. * sendAnalytics.
  25. */
  26. const createEvent = function(quality) {
  27. return createToolbarEvent(
  28. 'video.quality',
  29. {
  30. quality
  31. });
  32. };
  33. /**
  34. * The type of the React {@code Component} props of {@link VideoQualitySlider}.
  35. */
  36. type Props = {
  37. /**
  38. * Whether or not the conference is in audio only mode.
  39. */
  40. _audioOnly: Boolean,
  41. /**
  42. * The channelLastN value configured for the conference.
  43. */
  44. _channelLastN: Number,
  45. /**
  46. * Whether or not the conference is in peer to peer mode.
  47. */
  48. _p2p: Boolean,
  49. /**
  50. * The currently configured maximum quality resolution to be sent and
  51. * received from the remote participants.
  52. */
  53. _sendrecvVideoQuality: Number,
  54. /**
  55. * Invoked to request toggling of audio only mode.
  56. */
  57. dispatch: Dispatch<any>,
  58. /**
  59. * Invoked to obtain translated strings.
  60. */
  61. t: Function
  62. };
  63. /**
  64. * Implements a React {@link Component} which displays a slider for selecting a
  65. * new receive video quality.
  66. *
  67. * @extends Component
  68. */
  69. class VideoQualitySlider extends Component<Props> {
  70. _sliderOptions: Array<Object>;
  71. /**
  72. * Initializes a new {@code VideoQualitySlider} instance.
  73. *
  74. * @param {Object} props - The read-only React Component props with which
  75. * the new instance is to be initialized.
  76. */
  77. constructor(props) {
  78. super(props);
  79. // Bind event handlers so they are only bound once for every instance.
  80. this._enableAudioOnly = this._enableAudioOnly.bind(this);
  81. this._enableHighDefinition = this._enableHighDefinition.bind(this);
  82. this._enableLowDefinition = this._enableLowDefinition.bind(this);
  83. this._enableStandardDefinition
  84. = this._enableStandardDefinition.bind(this);
  85. this._enableUltraHighDefinition = this._enableUltraHighDefinition.bind(this);
  86. this._onSliderChange = this._onSliderChange.bind(this);
  87. /**
  88. * An array of configuration options for displaying a choice in the
  89. * input. The onSelect callback will be invoked when the option is
  90. * selected and videoQuality helps determine which choice matches with
  91. * the currently active quality level.
  92. *
  93. * @private
  94. * @type {Object[]}
  95. */
  96. this._sliderOptions = [
  97. {
  98. audioOnly: true,
  99. onSelect: this._enableAudioOnly,
  100. textKey: 'audioOnly.audioOnly'
  101. },
  102. {
  103. onSelect: this._enableLowDefinition,
  104. textKey: 'videoStatus.lowDefinition',
  105. videoQuality: LOW
  106. },
  107. {
  108. onSelect: this._enableStandardDefinition,
  109. textKey: 'videoStatus.standardDefinition',
  110. videoQuality: STANDARD
  111. },
  112. {
  113. onSelect: this._enableUltraHighDefinition,
  114. textKey: 'videoStatus.highDefinition',
  115. videoQuality: ULTRA
  116. }
  117. ];
  118. }
  119. /**
  120. * Implements React's {@link Component#render()}.
  121. *
  122. * @inheritdoc
  123. * @returns {ReactElement}
  124. */
  125. render() {
  126. const { t } = this.props;
  127. const activeSliderOption = this._mapCurrentQualityToSliderValue();
  128. return (
  129. <div className = { 'video-quality-dialog' }>
  130. <h3 className = 'video-quality-dialog-title'>
  131. { t('videoStatus.callQuality') }
  132. </h3>
  133. <div className = 'video-quality-dialog-contents'>
  134. <div className = 'video-quality-dialog-slider-container'>
  135. { /* FIXME: onChange and onMouseUp are both used for
  136. * compatibility with IE11. This workaround can be
  137. * removed after upgrading to React 16.
  138. */ }
  139. <input
  140. className = 'video-quality-dialog-slider'
  141. max = { this._sliderOptions.length - 1 }
  142. min = '0'
  143. onChange = { this._onSliderChange }
  144. onMouseUp = { this._onSliderChange }
  145. step = '1'
  146. type = 'range'
  147. value
  148. = { activeSliderOption } />
  149. </div>
  150. <div className = 'video-quality-dialog-labels'>
  151. { this._createLabels(activeSliderOption) }
  152. </div>
  153. </div>
  154. </div>
  155. );
  156. }
  157. /**
  158. * Creates React Elements to display mock tick marks with associated labels.
  159. *
  160. * @param {number} activeLabelIndex - Which of the sliderOptions should
  161. * display as currently active.
  162. * @private
  163. * @returns {ReactElement[]}
  164. */
  165. _createLabels(activeLabelIndex) {
  166. const labelsCount = this._sliderOptions.length;
  167. const maxWidthOfLabel = `${100 / labelsCount}%`;
  168. return this._sliderOptions.map((sliderOption, index) => {
  169. const style = {
  170. maxWidth: maxWidthOfLabel,
  171. left: `${(index * 100) / (labelsCount - 1)}%`
  172. };
  173. const isActiveClass = activeLabelIndex === index ? 'active' : '';
  174. const className
  175. = `video-quality-dialog-label-container ${isActiveClass}`;
  176. return (
  177. <div
  178. className = { className }
  179. key = { index }
  180. style = { style }>
  181. <div className = 'video-quality-dialog-label'>
  182. { this.props.t(sliderOption.textKey) }
  183. </div>
  184. </div>
  185. );
  186. });
  187. }
  188. _enableAudioOnly: () => void;
  189. /**
  190. * Dispatches an action to enable audio only mode.
  191. *
  192. * @private
  193. * @returns {void}
  194. */
  195. _enableAudioOnly() {
  196. sendAnalytics(createEvent('audio.only'));
  197. logger.log('Video quality: audio only enabled');
  198. this.props.dispatch(setAudioOnly(true));
  199. }
  200. _enableHighDefinition: () => void;
  201. /**
  202. * Handles the action of the high definition video being selected.
  203. * Dispatches an action to receive high quality video from remote
  204. * participants.
  205. *
  206. * @private
  207. * @returns {void}
  208. */
  209. _enableHighDefinition() {
  210. sendAnalytics(createEvent('high'));
  211. logger.log('Video quality: high enabled');
  212. this._setPreferredVideoQuality(HIGH);
  213. }
  214. _enableLowDefinition: () => void;
  215. /**
  216. * Dispatches an action to receive low quality video from remote
  217. * participants.
  218. *
  219. * @private
  220. * @returns {void}
  221. */
  222. _enableLowDefinition() {
  223. sendAnalytics(createEvent('low'));
  224. logger.log('Video quality: low enabled');
  225. this._setPreferredVideoQuality(LOW);
  226. }
  227. _enableStandardDefinition: () => void;
  228. /**
  229. * Dispatches an action to receive standard quality video from remote
  230. * participants.
  231. *
  232. * @private
  233. * @returns {void}
  234. */
  235. _enableStandardDefinition() {
  236. sendAnalytics(createEvent('standard'));
  237. logger.log('Video quality: standard enabled');
  238. this._setPreferredVideoQuality(STANDARD);
  239. }
  240. _enableUltraHighDefinition: () => void;
  241. /**
  242. * Dispatches an action to receive ultra HD quality video from remote
  243. * participants.
  244. *
  245. * @private
  246. * @returns {void}
  247. */
  248. _enableUltraHighDefinition() {
  249. sendAnalytics(createEvent('ultra high'));
  250. logger.log('Video quality: ultra high enabled');
  251. this._setPreferredVideoQuality(ULTRA);
  252. }
  253. /**
  254. * Matches the current video quality state with corresponding index of the
  255. * component's slider options.
  256. *
  257. * @private
  258. * @returns {void}
  259. */
  260. _mapCurrentQualityToSliderValue() {
  261. const { _audioOnly, _sendrecvVideoQuality } = this.props;
  262. const { _sliderOptions } = this;
  263. if (_audioOnly) {
  264. const audioOnlyOption = _sliderOptions.find(
  265. ({ audioOnly }) => audioOnly);
  266. return _sliderOptions.indexOf(audioOnlyOption);
  267. }
  268. for (let i = 0; i < _sliderOptions.length; i++) {
  269. if (_sliderOptions[i].videoQuality >= _sendrecvVideoQuality) {
  270. return i;
  271. }
  272. }
  273. return -1;
  274. }
  275. _onSliderChange: () => void;
  276. /**
  277. * Invokes a callback when the selected video quality changes.
  278. *
  279. * @param {Object} event - The slider's change event.
  280. * @private
  281. * @returns {void}
  282. */
  283. _onSliderChange(event) {
  284. const { _audioOnly, _sendrecvVideoQuality } = this.props;
  285. const {
  286. audioOnly,
  287. onSelect,
  288. videoQuality
  289. } = this._sliderOptions[event.target.value];
  290. // Take no action if the newly chosen option does not change audio only
  291. // or video quality state.
  292. if ((_audioOnly && audioOnly)
  293. || (!_audioOnly && videoQuality === _sendrecvVideoQuality)) {
  294. return;
  295. }
  296. onSelect();
  297. }
  298. /**
  299. * Helper for changing the preferred maximum video quality to receive and
  300. * disable audio only.
  301. *
  302. * @param {number} qualityLevel - The new maximum video quality. Should be
  303. * a value enumerated in {@code VIDEO_QUALITY_LEVELS}.
  304. * @private
  305. * @returns {void}
  306. */
  307. _setPreferredVideoQuality(qualityLevel) {
  308. this.props.dispatch(setPreferredVideoQuality(qualityLevel));
  309. if (this.props._audioOnly) {
  310. this.props.dispatch(setAudioOnly(false));
  311. }
  312. // Determine the lastN value based on the quality setting.
  313. let { _channelLastN = DEFAULT_LAST_N } = this.props;
  314. _channelLastN = _channelLastN === -1 ? DEFAULT_LAST_N : _channelLastN;
  315. const lastN = getLastNForQualityLevel(qualityLevel, _channelLastN);
  316. // Set the lastN for the conference.
  317. this.props.dispatch(setLastN(lastN));
  318. }
  319. }
  320. /**
  321. * Maps (parts of) the Redux state to the associated props for the
  322. * {@code VideoQualitySlider} component.
  323. *
  324. * @param {Object} state - The Redux state.
  325. * @private
  326. * @returns {Props}
  327. */
  328. function _mapStateToProps(state) {
  329. const { enabled: audioOnly } = state['features/base/audio-only'];
  330. const { p2p } = state['features/base/conference'];
  331. const { preferredVideoQuality } = state['features/video-quality'];
  332. const { channelLastN } = state['features/base/config'];
  333. return {
  334. _audioOnly: audioOnly,
  335. _channelLastN: channelLastN,
  336. _p2p: p2p,
  337. _sendrecvVideoQuality: preferredVideoQuality
  338. };
  339. }
  340. export default translate(connect(_mapStateToProps)(VideoQualitySlider));