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

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