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

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