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

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