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

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