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

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