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

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