Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

ConnectionStatsTable.tsx 21KB

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