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.

ConnectionIndicator.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. // @flow
  2. import React from 'react';
  3. import type { Dispatch } from 'redux';
  4. import { translate } from '../../../base/i18n';
  5. import { Icon, IconConnectionActive, IconConnectionInactive } from '../../../base/icons';
  6. import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
  7. import { MEDIA_TYPE } from '../../../base/media';
  8. import { getLocalParticipant, getParticipantById } from '../../../base/participants';
  9. import { Popover } from '../../../base/popover';
  10. import { connect } from '../../../base/redux';
  11. import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
  12. import { ConnectionStatsTable } from '../../../connection-stats';
  13. import { saveLogs } from '../../actions';
  14. import AbstractConnectionIndicator, {
  15. INDICATOR_DISPLAY_THRESHOLD,
  16. type Props as AbstractProps,
  17. type State as AbstractState
  18. } from '../AbstractConnectionIndicator';
  19. declare var interfaceConfig: Object;
  20. /**
  21. * An array of display configurations for the connection indicator and its bars.
  22. * The ordering is done specifically for faster iteration to find a matching
  23. * configuration to the current connection strength percentage.
  24. *
  25. * @type {Object[]}
  26. */
  27. const QUALITY_TO_WIDTH: Array<Object> = [
  28. // Full (3 bars)
  29. {
  30. colorClass: 'status-high',
  31. percent: INDICATOR_DISPLAY_THRESHOLD,
  32. tip: 'connectionindicator.quality.good',
  33. width: '100%'
  34. },
  35. // 2 bars
  36. {
  37. colorClass: 'status-med',
  38. percent: 10,
  39. tip: 'connectionindicator.quality.nonoptimal',
  40. width: '66%'
  41. },
  42. // 1 bar
  43. {
  44. colorClass: 'status-low',
  45. percent: 0,
  46. tip: 'connectionindicator.quality.poor',
  47. width: '33%'
  48. }
  49. // Note: we never show 0 bars as long as there is a connection.
  50. ];
  51. /**
  52. * The type of the React {@code Component} props of {@link ConnectionIndicator}.
  53. */
  54. type Props = AbstractProps & {
  55. /**
  56. * The current condition of the user's connection, matching one of the
  57. * enumerated values in the library.
  58. */
  59. _connectionStatus: string,
  60. /**
  61. * Whether or not the component should ignore setting a visibility class for
  62. * hiding the component when the connection quality is not strong.
  63. */
  64. alwaysVisible: boolean,
  65. /**
  66. * The audio SSRC of this client.
  67. */
  68. audioSsrc: number,
  69. /**
  70. * The Redux dispatch function.
  71. */
  72. dispatch: Dispatch<any>,
  73. /**
  74. * Whether or not clicking the indicator should display a popover for more
  75. * details.
  76. */
  77. enableStatsDisplay: boolean,
  78. /**
  79. * The font-size for the icon.
  80. */
  81. iconSize: number,
  82. /**
  83. * Whether or not the displays stats are for local video.
  84. */
  85. isLocalVideo: boolean,
  86. /**
  87. * Relative to the icon from where the popover for more connection details
  88. * should display.
  89. */
  90. statsPopoverPosition: string,
  91. /**
  92. * Invoked to obtain translated strings.
  93. */
  94. t: Function,
  95. /**
  96. * The video SSRC of this client.
  97. */
  98. videoSsrc: number,
  99. /**
  100. * Invoked to save the conference logs.
  101. */
  102. _onSaveLogs: Function
  103. };
  104. /**
  105. * The type of the React {@code Component} state of {@link ConnectionIndicator}.
  106. */
  107. type State = AbstractState & {
  108. /**
  109. * Whether or not the popover content should display additional statistics.
  110. */
  111. showMoreStats: boolean
  112. };
  113. /**
  114. * Implements a React {@link Component} which displays the current connection
  115. * quality percentage and has a popover to show more detailed connection stats.
  116. *
  117. * @extends {Component}
  118. */
  119. class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
  120. /**
  121. * Initializes a new {@code ConnectionIndicator} instance.
  122. *
  123. * @param {Object} props - The read-only properties with which the new
  124. * instance is to be initialized.
  125. */
  126. constructor(props: Props) {
  127. super(props);
  128. this.state = {
  129. autoHideTimeout: undefined,
  130. showIndicator: false,
  131. showMoreStats: false,
  132. stats: {}
  133. };
  134. // Bind event handlers so they are only bound once for every instance.
  135. this._onToggleShowMore = this._onToggleShowMore.bind(this);
  136. }
  137. /**
  138. * Implements React's {@link Component#render()}.
  139. *
  140. * @inheritdoc
  141. * @returns {ReactElement}
  142. */
  143. render() {
  144. const visibilityClass = this._getVisibilityClass();
  145. const rootClassNames = `indicator-container ${visibilityClass}`;
  146. const colorClass = this._getConnectionColorClass();
  147. const indicatorContainerClassNames
  148. = `connection-indicator indicator ${colorClass}`;
  149. return (
  150. <Popover
  151. className = { rootClassNames }
  152. content = { this._renderStatisticsTable() }
  153. disablePopover = { !this.props.enableStatsDisplay }
  154. position = { this.props.statsPopoverPosition }>
  155. <div className = 'popover-trigger'>
  156. <div
  157. className = { indicatorContainerClassNames }
  158. style = {{ fontSize: this.props.iconSize }}>
  159. <div className = 'connection indicatoricon'>
  160. { this._renderIcon() }
  161. </div>
  162. </div>
  163. </div>
  164. </Popover>
  165. );
  166. }
  167. /**
  168. * Returns a CSS class that interprets the current connection status as a
  169. * color.
  170. *
  171. * @private
  172. * @returns {string}
  173. */
  174. _getConnectionColorClass() {
  175. const { _connectionStatus } = this.props;
  176. const { percent } = this.state.stats;
  177. const { INACTIVE, INTERRUPTED } = JitsiParticipantConnectionStatus;
  178. if (_connectionStatus === INACTIVE) {
  179. return 'status-other';
  180. } else if (_connectionStatus === INTERRUPTED) {
  181. return 'status-lost';
  182. } else if (typeof percent === 'undefined') {
  183. return 'status-high';
  184. }
  185. return this._getDisplayConfiguration(percent).colorClass;
  186. }
  187. /**
  188. * Returns a string that describes the current connection status.
  189. *
  190. * @private
  191. * @returns {string}
  192. */
  193. _getConnectionStatusTip() {
  194. let tipKey;
  195. switch (this.props._connectionStatus) {
  196. case JitsiParticipantConnectionStatus.INTERRUPTED:
  197. tipKey = 'connectionindicator.quality.lost';
  198. break;
  199. case JitsiParticipantConnectionStatus.INACTIVE:
  200. tipKey = 'connectionindicator.quality.inactive';
  201. break;
  202. default: {
  203. const { percent } = this.state.stats;
  204. if (typeof percent === 'undefined') {
  205. // If percentage is undefined then there are no stats available
  206. // yet, likely because only a local connection has been
  207. // established so far. Assume a strong connection to start.
  208. tipKey = 'connectionindicator.quality.good';
  209. } else {
  210. const config = this._getDisplayConfiguration(percent);
  211. tipKey = config.tip;
  212. }
  213. }
  214. }
  215. return this.props.t(tipKey);
  216. }
  217. /**
  218. * Get the icon configuration from QUALITY_TO_WIDTH which has a percentage
  219. * that matches or exceeds the passed in percentage. The implementation
  220. * assumes QUALITY_TO_WIDTH is already sorted by highest to lowest
  221. * percentage.
  222. *
  223. * @param {number} percent - The connection percentage, out of 100, to find
  224. * the closest matching configuration for.
  225. * @private
  226. * @returns {Object}
  227. */
  228. _getDisplayConfiguration(percent: number): Object {
  229. return QUALITY_TO_WIDTH.find(x => percent >= x.percent) || {};
  230. }
  231. /**
  232. * Returns additional class names to add to the root of the component. The
  233. * class names are intended to be used for hiding or showing the indicator.
  234. *
  235. * @private
  236. * @returns {string}
  237. */
  238. _getVisibilityClass() {
  239. const { _connectionStatus } = this.props;
  240. return this.state.showIndicator
  241. || this.props.alwaysVisible
  242. || _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED
  243. || _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
  244. ? 'show-connection-indicator' : 'hide-connection-indicator';
  245. }
  246. _onToggleShowMore: () => void;
  247. /**
  248. * Callback to invoke when the show more link in the popover content is
  249. * clicked. Sets the state which will determine if the popover should show
  250. * additional statistics about the connection.
  251. *
  252. * @returns {void}
  253. */
  254. _onToggleShowMore() {
  255. this.setState({ showMoreStats: !this.state.showMoreStats });
  256. }
  257. /**
  258. * Creates a ReactElement for displaying an icon that represents the current
  259. * connection quality.
  260. *
  261. * @returns {ReactElement}
  262. */
  263. _renderIcon() {
  264. if (this.props._connectionStatus
  265. === JitsiParticipantConnectionStatus.INACTIVE) {
  266. return (
  267. <span className = 'connection_ninja'>
  268. <Icon
  269. className = 'icon-ninja'
  270. size = '1.5em'
  271. src = { IconConnectionInactive } />
  272. </span>
  273. );
  274. }
  275. let iconWidth;
  276. let emptyIconWrapperClassName = 'connection_empty';
  277. if (this.props._connectionStatus
  278. === JitsiParticipantConnectionStatus.INTERRUPTED) {
  279. // emptyIconWrapperClassName is used by the torture tests to
  280. // identify lost connection status handling.
  281. emptyIconWrapperClassName = 'connection_lost';
  282. iconWidth = '0%';
  283. } else if (typeof this.state.stats.percent === 'undefined') {
  284. iconWidth = '100%';
  285. } else {
  286. const { percent } = this.state.stats;
  287. iconWidth = this._getDisplayConfiguration(percent).width;
  288. }
  289. return [
  290. <span
  291. className = { emptyIconWrapperClassName }
  292. key = 'icon-empty'>
  293. <Icon
  294. className = 'icon-gsm-bars'
  295. size = '1em'
  296. src = { IconConnectionActive } />
  297. </span>,
  298. <span
  299. className = 'connection_full'
  300. key = 'icon-full'
  301. style = {{ width: iconWidth }}>
  302. <Icon
  303. className = 'icon-gsm-bars'
  304. size = '1em'
  305. src = { IconConnectionActive } />
  306. </span>
  307. ];
  308. }
  309. /**
  310. * Creates a {@code ConnectionStatisticsTable} instance.
  311. *
  312. * @returns {ReactElement}
  313. */
  314. _renderStatisticsTable() {
  315. const {
  316. bandwidth,
  317. bitrate,
  318. bridgeCount,
  319. codec,
  320. e2eRtt,
  321. framerate,
  322. maxEnabledResolution,
  323. packetLoss,
  324. region,
  325. resolution,
  326. serverRegion,
  327. transport
  328. } = this.state.stats;
  329. return (
  330. <ConnectionStatsTable
  331. audioSsrc = { this.props.audioSsrc }
  332. bandwidth = { bandwidth }
  333. bitrate = { bitrate }
  334. bridgeCount = { bridgeCount }
  335. codec = { codec }
  336. connectionSummary = { this._getConnectionStatusTip() }
  337. e2eRtt = { e2eRtt }
  338. framerate = { framerate }
  339. isLocalVideo = { this.props.isLocalVideo }
  340. maxEnabledResolution = { maxEnabledResolution }
  341. onSaveLogs = { this.props._onSaveLogs }
  342. onShowMore = { this._onToggleShowMore }
  343. packetLoss = { packetLoss }
  344. participantId = { this.props.participantId }
  345. region = { region }
  346. resolution = { resolution }
  347. serverRegion = { serverRegion }
  348. shouldShowMore = { this.state.showMoreStats }
  349. transport = { transport }
  350. videoSsrc = { this.props.videoSsrc } />
  351. );
  352. }
  353. }
  354. /**
  355. * Maps redux actions to the props of the component.
  356. *
  357. * @param {Function} dispatch - The redux action {@code dispatch} function.
  358. * @returns {{
  359. * _onSaveLogs: Function,
  360. * }}
  361. * @private
  362. */
  363. export function _mapDispatchToProps(dispatch: Dispatch<any>) {
  364. return {
  365. /**
  366. * Saves the conference logs.
  367. *
  368. * @returns {Function}
  369. */
  370. _onSaveLogs() {
  371. dispatch(saveLogs());
  372. }
  373. };
  374. }
  375. /**
  376. * Maps part of the Redux state to the props of this component.
  377. *
  378. * @param {Object} state - The Redux state.
  379. * @param {Props} ownProps - The own props of the component.
  380. * @returns {Props}
  381. */
  382. export function _mapStateToProps(state: Object, ownProps: Props) {
  383. const { participantId } = ownProps;
  384. const conference = state['features/base/conference'].conference;
  385. const participant
  386. = typeof participantId === 'undefined' ? getLocalParticipant(state) : getParticipantById(state, participantId);
  387. const props = {
  388. _connectionStatus: participant?.connectionStatus
  389. };
  390. if (conference) {
  391. const firstVideoTrack = getTrackByMediaTypeAndParticipant(
  392. state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
  393. const firstAudioTrack = getTrackByMediaTypeAndParticipant(
  394. state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);
  395. return {
  396. ...props,
  397. audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
  398. videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
  399. };
  400. }
  401. return {
  402. ...props
  403. };
  404. }
  405. export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ConnectionIndicator));