您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

VideoQualityDialog.web.js 11KB

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