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

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