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.

ConnectionStatsTable.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. /* @flow */
  2. import React, { Component } from 'react';
  3. import { translate } from '../../base/i18n';
  4. /**
  5. * The type of the React {@code Component} props of
  6. * {@link ConnectionStatsTable}.
  7. */
  8. type Props = {
  9. /**
  10. * Statistics related to bandwidth.
  11. * {{
  12. * download: Number,
  13. * upload: Number
  14. * }}
  15. */
  16. bandwidth: Object,
  17. /**
  18. * Statistics related to bitrate.
  19. * {{
  20. * download: Number,
  21. * upload: Number
  22. * }}
  23. */
  24. bitrate: Object,
  25. /**
  26. * The number of bridges (aka media servers) currently used in the
  27. * conference.
  28. */
  29. bridgeCount: number,
  30. /**
  31. * Audio/video codecs in use for the connection.
  32. */
  33. codec: Object,
  34. /**
  35. * A message describing the connection quality.
  36. */
  37. connectionSummary: string,
  38. /**
  39. * The end-to-end round-trip-time.
  40. */
  41. e2eRtt: number,
  42. /**
  43. * Statistics related to frame rates for each ssrc.
  44. * {{
  45. * [ ssrc ]: Number
  46. * }}
  47. */
  48. framerate: Object,
  49. /**
  50. * Whether or not the statistics are for local video.
  51. */
  52. isLocalVideo: boolean,
  53. /**
  54. * The send-side max enabled resolution (aka the highest layer that is not
  55. * suspended on the send-side).
  56. */
  57. maxEnabledResolution: number,
  58. /**
  59. * Callback to invoke when the show additional stats link is clicked.
  60. */
  61. onShowMore: Function,
  62. /**
  63. * Statistics related to packet loss.
  64. * {{
  65. * download: Number,
  66. * upload: Number
  67. * }}
  68. */
  69. packetLoss: Object,
  70. /**
  71. * The region that we think the client is in.
  72. */
  73. region: string,
  74. /**
  75. * Statistics related to display resolutions for each ssrc.
  76. * {{
  77. * [ ssrc ]: {
  78. * height: Number,
  79. * width: Number
  80. * }
  81. * }}
  82. */
  83. resolution: Object,
  84. /**
  85. * The region of the media server that we are connected to.
  86. */
  87. serverRegion: string,
  88. /**
  89. * Whether or not additional stats about bandwidth and transport should be
  90. * displayed. Will not display even if true for remote participants.
  91. */
  92. shouldShowMore: boolean,
  93. /**
  94. * Invoked to obtain translated strings.
  95. */
  96. t: Function,
  97. /**
  98. * Statistics related to transports.
  99. */
  100. transport: Array<Object>
  101. };
  102. /**
  103. * React {@code Component} for displaying connection statistics.
  104. *
  105. * @extends Component
  106. */
  107. class ConnectionStatsTable extends Component<Props> {
  108. /**
  109. * Implements React's {@link Component#render()}.
  110. *
  111. * @inheritdoc
  112. * @returns {ReactElement}
  113. */
  114. render() {
  115. const { isLocalVideo } = this.props;
  116. return (
  117. <div className = 'connection-info'>
  118. { this._renderStatistics() }
  119. { isLocalVideo ? this._renderShowMoreLink() : null }
  120. { isLocalVideo && this.props.shouldShowMore
  121. ? this._renderAdditionalStats() : null }
  122. </div>
  123. );
  124. }
  125. /**
  126. * Creates a table as ReactElement that will display additional statistics
  127. * related to bandwidth and transport for the local user.
  128. *
  129. * @private
  130. * @returns {ReactElement}
  131. */
  132. _renderAdditionalStats() {
  133. return (
  134. <table className = 'connection-info__container'>
  135. <tbody>
  136. { this._renderBandwidth() }
  137. { this._renderTransport() }
  138. { this._renderRegion() }
  139. </tbody>
  140. </table>
  141. );
  142. }
  143. /**
  144. * Creates a table row as a ReactElement for displaying bandwidth related
  145. * statistics.
  146. *
  147. * @private
  148. * @returns {ReactElement}
  149. */
  150. _renderBandwidth() {
  151. const { download, upload } = this.props.bandwidth || {};
  152. return (
  153. <tr>
  154. <td>
  155. { this.props.t('connectionindicator.bandwidth') }
  156. </td>
  157. <td>
  158. <span className = 'connection-info__download'>
  159. &darr;
  160. </span>
  161. { download ? `${download} Kbps` : 'N/A' }
  162. <span className = 'connection-info__upload'>
  163. &uarr;
  164. </span>
  165. { upload ? `${upload} Kbps` : 'N/A' }
  166. </td>
  167. </tr>
  168. );
  169. }
  170. /**
  171. * Creates a a table row as a ReactElement for displaying bitrate related
  172. * statistics.
  173. *
  174. * @private
  175. * @returns {ReactElement}
  176. */
  177. _renderBitrate() {
  178. const { download, upload } = this.props.bitrate || {};
  179. return (
  180. <tr>
  181. <td>
  182. <span>
  183. { this.props.t('connectionindicator.bitrate') }
  184. </span>
  185. </td>
  186. <td>
  187. <span className = 'connection-info__download'>
  188. &darr;
  189. </span>
  190. { download ? `${download} Kbps` : 'N/A' }
  191. <span className = 'connection-info__upload'>
  192. &uarr;
  193. </span>
  194. { upload ? `${upload} Kbps` : 'N/A' }
  195. </td>
  196. </tr>
  197. );
  198. }
  199. /**
  200. * Creates a a table row as a ReactElement for displaying codec, if present.
  201. * This will typically be something like "Codecs (A/V): Opus, vp8".
  202. *
  203. * @private
  204. * @returns {ReactElement}
  205. */
  206. _renderCodecs() {
  207. const { codec, t } = this.props;
  208. if (!codec) {
  209. return;
  210. }
  211. let codecString;
  212. // Only report one codec, in case there are multiple for a user.
  213. Object.keys(codec || {})
  214. .forEach(ssrc => {
  215. const { audio, video } = codec[ssrc];
  216. codecString = `${audio}, ${video}`;
  217. });
  218. if (!codecString) {
  219. codecString = 'N/A';
  220. }
  221. return (
  222. <tr>
  223. <td>
  224. <span>{ t('connectionindicator.codecs') }</span>
  225. </td>
  226. <td>{ codecString }</td>
  227. </tr>
  228. );
  229. }
  230. /**
  231. * Creates a table row as a ReactElement for displaying a summary message
  232. * about the current connection status.
  233. *
  234. * @private
  235. * @returns {ReactElement}
  236. */
  237. _renderConnectionSummary() {
  238. return (
  239. <tr className = 'connection-info__status'>
  240. <td>
  241. <span>{ this.props.t('connectionindicator.status') }</span>
  242. </td>
  243. <td>{ this.props.connectionSummary }</td>
  244. </tr>
  245. );
  246. }
  247. /**
  248. * Creates a table row as a ReactElement for displaying end-to-end RTT and
  249. * the region.
  250. *
  251. * @returns {ReactElement}
  252. * @private
  253. */
  254. _renderE2eRtt() {
  255. const { e2eRtt, t } = this.props;
  256. const str = e2eRtt ? `${e2eRtt.toFixed(0)}ms` : 'N/A';
  257. return (
  258. <tr>
  259. <td>
  260. <span>{ t('connectionindicator.e2e_rtt') }</span>
  261. </td>
  262. <td>{ str }</td>
  263. </tr>
  264. );
  265. }
  266. /**
  267. * Creates a table row as a ReactElement for displaying the "connected to"
  268. * information.
  269. *
  270. * @returns {ReactElement}
  271. * @private
  272. */
  273. _renderRegion() {
  274. const { region, serverRegion, t } = this.props;
  275. let str = serverRegion;
  276. if (!serverRegion) {
  277. return;
  278. }
  279. if (region && serverRegion && region !== serverRegion) {
  280. str += ` from ${region}`;
  281. }
  282. return (
  283. <tr>
  284. <td>
  285. <span>{ t('connectionindicator.connectedTo') }</span>
  286. </td>
  287. <td>{ str }</td>
  288. </tr>
  289. );
  290. }
  291. /**
  292. * Creates a table row as a ReactElement for displaying the "bridge count"
  293. * information.
  294. *
  295. * @returns {*}
  296. * @private
  297. */
  298. _renderBridgeCount() {
  299. const { bridgeCount, t } = this.props;
  300. // 0 is valid, but undefined/null/NaN aren't.
  301. if (!bridgeCount && bridgeCount !== 0) {
  302. return;
  303. }
  304. return (
  305. <tr>
  306. <td>
  307. <span>{ t('connectionindicator.bridgeCount') }</span>
  308. </td>
  309. <td>{ bridgeCount }</td>
  310. </tr>
  311. );
  312. }
  313. /**
  314. * Creates a table row as a ReactElement for displaying frame rate related
  315. * statistics.
  316. *
  317. * @private
  318. * @returns {ReactElement}
  319. */
  320. _renderFrameRate() {
  321. const { framerate, t } = this.props;
  322. const frameRateString = Object.keys(framerate || {})
  323. .map(ssrc => framerate[ssrc])
  324. .join(', ') || 'N/A';
  325. return (
  326. <tr>
  327. <td>
  328. <span>{ t('connectionindicator.framerate') }</span>
  329. </td>
  330. <td>{ frameRateString }</td>
  331. </tr>
  332. );
  333. }
  334. /**
  335. * Creates a tables row as a ReactElement for displaying packet loss related
  336. * statistics.
  337. *
  338. * @private
  339. * @returns {ReactElement}
  340. */
  341. _renderPacketLoss() {
  342. const { packetLoss, t } = this.props;
  343. let packetLossTableData;
  344. if (packetLoss) {
  345. const { download, upload } = packetLoss;
  346. packetLossTableData = (
  347. <td>
  348. <span className = 'connection-info__download'>
  349. &darr;
  350. </span>
  351. { download === null ? 'N/A' : `${download}%` }
  352. <span className = 'connection-info__upload'>
  353. &uarr;
  354. </span>
  355. { upload === null ? 'N/A' : `${upload}%` }
  356. </td>
  357. );
  358. } else {
  359. packetLossTableData = <td>N/A</td>;
  360. }
  361. return (
  362. <tr>
  363. <td>
  364. <span>
  365. { t('connectionindicator.packetloss') }
  366. </span>
  367. </td>
  368. { packetLossTableData }
  369. </tr>
  370. );
  371. }
  372. /**
  373. * Creates a table row as a ReactElement for displaying resolution related
  374. * statistics.
  375. *
  376. * @private
  377. * @returns {ReactElement}
  378. */
  379. _renderResolution() {
  380. const { resolution, maxEnabledResolution, t } = this.props;
  381. let resolutionString = Object.keys(resolution || {})
  382. .map(ssrc => {
  383. const { width, height } = resolution[ssrc];
  384. return `${width}x${height}`;
  385. })
  386. .join(', ') || 'N/A';
  387. if (maxEnabledResolution && maxEnabledResolution < 720) {
  388. const maxEnabledResolutionTitle = t('connectionindicator.maxEnabledResolution');
  389. resolutionString += ` (${maxEnabledResolutionTitle} ${maxEnabledResolution}p)`;
  390. }
  391. return (
  392. <tr>
  393. <td>
  394. <span>{ t('connectionindicator.resolution') }</span>
  395. </td>
  396. <td>{ resolutionString }</td>
  397. </tr>
  398. );
  399. }
  400. /**
  401. * Creates a ReactElement for display a link to toggle showing additional
  402. * statistics.
  403. *
  404. * @private
  405. * @returns {ReactElement}
  406. */
  407. _renderShowMoreLink() {
  408. const translationKey
  409. = this.props.shouldShowMore
  410. ? 'connectionindicator.less'
  411. : 'connectionindicator.more';
  412. return (
  413. <a
  414. className = 'showmore link'
  415. onClick = { this.props.onShowMore } >
  416. { this.props.t(translationKey) }
  417. </a>
  418. );
  419. }
  420. /**
  421. * Creates a table as a ReactElement for displaying connection statistics.
  422. *
  423. * @private
  424. * @returns {ReactElement}
  425. */
  426. _renderStatistics() {
  427. const isRemoteVideo = !this.props.isLocalVideo;
  428. return (
  429. <table className = 'connection-info__container'>
  430. <tbody>
  431. { this._renderConnectionSummary() }
  432. { this._renderBitrate() }
  433. { this._renderPacketLoss() }
  434. { isRemoteVideo ? this._renderE2eRtt() : null }
  435. { isRemoteVideo ? this._renderRegion() : null }
  436. { this._renderResolution() }
  437. { this._renderFrameRate() }
  438. { this._renderCodecs() }
  439. { isRemoteVideo ? null : this._renderBridgeCount() }
  440. </tbody>
  441. </table>
  442. );
  443. }
  444. /**
  445. * Creates table rows as ReactElements for displaying transport related
  446. * statistics.
  447. *
  448. * @private
  449. * @returns {ReactElement[]}
  450. */
  451. _renderTransport() {
  452. const { t, transport } = this.props;
  453. if (!transport || transport.length === 0) {
  454. const NA = (
  455. <tr key = 'address'>
  456. <td>
  457. <span>{ t('connectionindicator.address') }</span>
  458. </td>
  459. <td>
  460. N/A
  461. </td>
  462. </tr>
  463. );
  464. return [ NA ];
  465. }
  466. const data = {
  467. localIP: [],
  468. localPort: [],
  469. remoteIP: [],
  470. remotePort: [],
  471. transportType: []
  472. };
  473. for (let i = 0; i < transport.length; i++) {
  474. const ip = getIP(transport[i].ip);
  475. const localIP = getIP(transport[i].localip);
  476. const localPort = getPort(transport[i].localip);
  477. const port = getPort(transport[i].ip);
  478. if (!data.remoteIP.includes(ip)) {
  479. data.remoteIP.push(ip);
  480. }
  481. if (!data.localIP.includes(localIP)) {
  482. data.localIP.push(localIP);
  483. }
  484. if (!data.localPort.includes(localPort)) {
  485. data.localPort.push(localPort);
  486. }
  487. if (!data.remotePort.includes(port)) {
  488. data.remotePort.push(port);
  489. }
  490. if (!data.transportType.includes(transport[i].type)) {
  491. data.transportType.push(transport[i].type);
  492. }
  493. }
  494. // All of the transports should be either P2P or JVB
  495. let isP2P = false, isTURN = false;
  496. if (transport.length) {
  497. isP2P = transport[0].p2p;
  498. isTURN = transport[0].localCandidateType === 'relay'
  499. || transport[0].remoteCandidateType === 'relay';
  500. }
  501. const additionalData = [];
  502. if (isP2P) {
  503. additionalData.push(
  504. <span> (p2p)</span>);
  505. }
  506. if (isTURN) {
  507. additionalData.push(<span> (turn)</span>);
  508. }
  509. // First show remote statistics, then local, and then transport type.
  510. const tableRowConfigurations = [
  511. {
  512. additionalData,
  513. data: data.remoteIP,
  514. key: 'remoteaddress',
  515. label: t('connectionindicator.remoteaddress',
  516. { count: data.remoteIP.length })
  517. },
  518. {
  519. data: data.remotePort,
  520. key: 'remoteport',
  521. label: t('connectionindicator.remoteport',
  522. { count: transport.length })
  523. },
  524. {
  525. data: data.localIP,
  526. key: 'localaddress',
  527. label: t('connectionindicator.localaddress',
  528. { count: data.localIP.length })
  529. },
  530. {
  531. data: data.localPort,
  532. key: 'localport',
  533. label: t('connectionindicator.localport',
  534. { count: transport.length })
  535. },
  536. {
  537. data: data.transportType,
  538. key: 'transport',
  539. label: t('connectionindicator.transport',
  540. { count: data.transportType.length })
  541. }
  542. ];
  543. return tableRowConfigurations.map(this._renderTransportTableRow);
  544. }
  545. /**
  546. * Creates a table row as a ReactElement for displaying a transport related
  547. * statistic.
  548. *
  549. * @param {Object} config - Describes the contents of the row.
  550. * @param {ReactElement} config.additionalData - Extra data to display next
  551. * to the passed in config.data.
  552. * @param {Array} config.data - The transport statistics to display.
  553. * @param {string} config.key - The ReactElement's key. Must be unique for
  554. * iterating over multiple child rows.
  555. * @param {string} config.label - The text to display describing the data.
  556. * @private
  557. * @returns {ReactElement}
  558. */
  559. _renderTransportTableRow(config: Object) {
  560. const { additionalData, data, key, label } = config;
  561. return (
  562. <tr key = { key }>
  563. <td>
  564. <span>
  565. { label }
  566. </span>
  567. </td>
  568. <td>
  569. { getStringFromArray(data) }
  570. { additionalData || null }
  571. </td>
  572. </tr>
  573. );
  574. }
  575. }
  576. /**
  577. * Utility for getting the IP from a transport statistics object's
  578. * representation of an IP.
  579. *
  580. * @param {string} value - The transport's IP to parse.
  581. * @private
  582. * @returns {string}
  583. */
  584. function getIP(value) {
  585. if (!value) {
  586. return '';
  587. }
  588. return value.substring(0, value.lastIndexOf(':'));
  589. }
  590. /**
  591. * Utility for getting the port from a transport statistics object's
  592. * representation of an IP.
  593. *
  594. * @param {string} value - The transport's IP to parse.
  595. * @private
  596. * @returns {string}
  597. */
  598. function getPort(value) {
  599. if (!value) {
  600. return '';
  601. }
  602. return value.substring(value.lastIndexOf(':') + 1, value.length);
  603. }
  604. /**
  605. * Utility for concatenating values in an array into a comma separated string.
  606. *
  607. * @param {Array} array - Transport statistics to concatenate.
  608. * @private
  609. * @returns {string}
  610. */
  611. function getStringFromArray(array) {
  612. let res = '';
  613. for (let i = 0; i < array.length; i++) {
  614. res += (i === 0 ? '' : ', ') + array[i];
  615. }
  616. return res;
  617. }
  618. export default translate(ConnectionStatsTable);