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

VideoQualitySlider.web.js 12KB

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