選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ConnectionStatsTable.tsx 20KB

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