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 12KB

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