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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  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. role = 'button'
  506. tabIndex = { 0 }>
  507. { this.props.t('connectionindicator.savelogs') }
  508. </a>
  509. <span> | </span>
  510. </span>
  511. );
  512. }
  513. /**
  514. * Creates a ReactElement for display a link to toggle showing additional
  515. * statistics.
  516. *
  517. * @private
  518. * @returns {ReactElement}
  519. */
  520. _renderShowMoreLink() {
  521. const translationKey
  522. = this.props.shouldShowMore
  523. ? 'connectionindicator.less'
  524. : 'connectionindicator.more';
  525. return (
  526. <a
  527. className = 'showmore link'
  528. onClick = { this.props.onShowMore }
  529. role = 'button'
  530. tabIndex = { 0 }>
  531. { this.props.t(translationKey) }
  532. </a>
  533. );
  534. }
  535. /**
  536. * Creates a table as a ReactElement for displaying connection statistics.
  537. *
  538. * @private
  539. * @returns {ReactElement}
  540. */
  541. _renderStatistics() {
  542. const isRemoteVideo = !this.props.isLocalVideo;
  543. return (
  544. <table className = 'connection-info__container'>
  545. <tbody>
  546. { this._renderConnectionSummary() }
  547. { this._renderBitrate() }
  548. { this._renderPacketLoss() }
  549. { isRemoteVideo ? this._renderE2eRtt() : null }
  550. { isRemoteVideo ? this._renderRegion() : null }
  551. { this._renderResolution() }
  552. { this._renderFrameRate() }
  553. { this._renderCodecs() }
  554. { isRemoteVideo ? null : this._renderBridgeCount() }
  555. </tbody>
  556. </table>
  557. );
  558. }
  559. /**
  560. * Creates table rows as ReactElements for displaying transport related
  561. * statistics.
  562. *
  563. * @private
  564. * @returns {ReactElement[]}
  565. */
  566. _renderTransport() {
  567. const { t, transport } = this.props;
  568. if (!transport || transport.length === 0) {
  569. const NA = (
  570. <tr key = 'address'>
  571. <td>
  572. <span>{ t('connectionindicator.address') }</span>
  573. </td>
  574. <td>
  575. N/A
  576. </td>
  577. </tr>
  578. );
  579. return [ NA ];
  580. }
  581. const data = {
  582. localIP: [],
  583. localPort: [],
  584. remoteIP: [],
  585. remotePort: [],
  586. transportType: []
  587. };
  588. for (let i = 0; i < transport.length; i++) {
  589. const ip = getIP(transport[i].ip);
  590. const localIP = getIP(transport[i].localip);
  591. const localPort = getPort(transport[i].localip);
  592. const port = getPort(transport[i].ip);
  593. if (!data.remoteIP.includes(ip)) {
  594. data.remoteIP.push(ip);
  595. }
  596. if (!data.localIP.includes(localIP)) {
  597. data.localIP.push(localIP);
  598. }
  599. if (!data.localPort.includes(localPort)) {
  600. data.localPort.push(localPort);
  601. }
  602. if (!data.remotePort.includes(port)) {
  603. data.remotePort.push(port);
  604. }
  605. if (!data.transportType.includes(transport[i].type)) {
  606. data.transportType.push(transport[i].type);
  607. }
  608. }
  609. // All of the transports should be either P2P or JVB
  610. let isP2P = false, isTURN = false;
  611. if (transport.length) {
  612. isP2P = transport[0].p2p;
  613. isTURN = transport[0].localCandidateType === 'relay'
  614. || transport[0].remoteCandidateType === 'relay';
  615. }
  616. const additionalData = [];
  617. if (isP2P) {
  618. additionalData.push(
  619. <span> (p2p)</span>);
  620. }
  621. if (isTURN) {
  622. additionalData.push(<span> (turn)</span>);
  623. }
  624. // First show remote statistics, then local, and then transport type.
  625. const tableRowConfigurations = [
  626. {
  627. additionalData,
  628. data: data.remoteIP,
  629. key: 'remoteaddress',
  630. label: t('connectionindicator.remoteaddress',
  631. { count: data.remoteIP.length })
  632. },
  633. {
  634. data: data.remotePort,
  635. key: 'remoteport',
  636. label: t('connectionindicator.remoteport',
  637. { count: transport.length })
  638. },
  639. {
  640. data: data.localIP,
  641. key: 'localaddress',
  642. label: t('connectionindicator.localaddress',
  643. { count: data.localIP.length })
  644. },
  645. {
  646. data: data.localPort,
  647. key: 'localport',
  648. label: t('connectionindicator.localport',
  649. { count: transport.length })
  650. },
  651. {
  652. data: data.transportType,
  653. key: 'transport',
  654. label: t('connectionindicator.transport',
  655. { count: data.transportType.length })
  656. }
  657. ];
  658. return tableRowConfigurations.map(this._renderTransportTableRow);
  659. }
  660. /**
  661. * Creates a table row as a ReactElement for displaying a transport related
  662. * statistic.
  663. *
  664. * @param {Object} config - Describes the contents of the row.
  665. * @param {ReactElement} config.additionalData - Extra data to display next
  666. * to the passed in config.data.
  667. * @param {Array} config.data - The transport statistics to display.
  668. * @param {string} config.key - The ReactElement's key. Must be unique for
  669. * iterating over multiple child rows.
  670. * @param {string} config.label - The text to display describing the data.
  671. * @private
  672. * @returns {ReactElement}
  673. */
  674. _renderTransportTableRow(config: Object) {
  675. const { additionalData, data, key, label } = config;
  676. return (
  677. <tr key = { key }>
  678. <td>
  679. <span>
  680. { label }
  681. </span>
  682. </td>
  683. <td>
  684. { getStringFromArray(data) }
  685. { additionalData || null }
  686. </td>
  687. </tr>
  688. );
  689. }
  690. }
  691. /**
  692. * Utility for getting the IP from a transport statistics object's
  693. * representation of an IP.
  694. *
  695. * @param {string} value - The transport's IP to parse.
  696. * @private
  697. * @returns {string}
  698. */
  699. function getIP(value) {
  700. if (!value) {
  701. return '';
  702. }
  703. return value.substring(0, value.lastIndexOf(':'));
  704. }
  705. /**
  706. * Utility for getting the port from a transport statistics object's
  707. * representation of an IP.
  708. *
  709. * @param {string} value - The transport's IP to parse.
  710. * @private
  711. * @returns {string}
  712. */
  713. function getPort(value) {
  714. if (!value) {
  715. return '';
  716. }
  717. return value.substring(value.lastIndexOf(':') + 1, value.length);
  718. }
  719. /**
  720. * Utility for concatenating values in an array into a comma separated string.
  721. *
  722. * @param {Array} array - Transport statistics to concatenate.
  723. * @private
  724. * @returns {string}
  725. */
  726. function getStringFromArray(array) {
  727. let res = '';
  728. for (let i = 0; i < array.length; i++) {
  729. res += (i === 0 ? '' : ', ') + array[i];
  730. }
  731. return res;
  732. }
  733. export default translate(ConnectionStatsTable);