/* eslint-disable react/no-multi-comp */
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../base/environment/utils';
import Icon from '../../base/icons/components/Icon';
import { IconGear } from '../../base/icons/svg';
import ContextMenu from '../../base/ui/components/web/ContextMenu';
type DownloadUpload = {
download: number;
upload: number;
};
/**
* The type of the React {@code Component} props of
* {@link ConnectionStatsTable}.
*/
interface IProps {
/**
* The audio SSRC of this client.
*/
audioSsrc: number;
/**
* Statistics related to bandwidth.
* {{
* download: Number,
* upload: Number
* }}.
*/
bandwidth: DownloadUpload;
/**
* Statistics related to bitrate.
* {{
* download: Number,
* upload: Number
* }}.
*/
bitrate: DownloadUpload;
/**
* The number of bridges (aka media servers) currently used in the
* conference.
*/
bridgeCount: number;
/**
* Audio/video codecs in use for the connection.
*/
codec: {
[key: string]: {
audio: string | undefined;
video: string | undefined;
};
};
/**
* A message describing the connection quality.
*/
connectionSummary: string;
/**
* Whether or not should display the "Show More" link.
*/
disableShowMoreStats: boolean;
/**
* Whether or not the participant was verified.
*/
e2eeVerified?: boolean;
/**
* Whether to enable assumed bandwidth.
*/
enableAssumedBandwidth?: boolean;
/**
* Whether or not should display the "Save Logs" link.
*/
enableSaveLogs: boolean;
/**
* Statistics related to frame rates for each ssrc.
* {{
* [ ssrc ]: Number
* }}.
*/
framerate: {
[ssrc: string]: number;
};
/**
* Whether or not the statistics are for local video.
*/
isLocalVideo: boolean;
/**
* Whether we are in narrow layout mode or not.
*/
isNarrowLayout: boolean;
/**
* Whether or not the statistics are for screen share.
*/
isVirtualScreenshareParticipant: boolean;
/**
* The send-side max enabled resolution (aka the highest layer that is not
* suspended on the send-side).
*/
maxEnabledResolution: number;
/**
* Callback to invoke when the user clicks on the open bandwidth settings dialog icon.
*/
onOpenBandwidthDialog: () => void;
/**
* Callback to invoke when the user clicks on the download logs link.
*/
onSaveLogs: () => void;
/**
* Callback to invoke when the show additional stats link is clicked.
*/
onShowMore: (e?: React.MouseEvent) => void;
/**
* Statistics related to packet loss.
* {{
* download: Number,
* upload: Number
* }}.
*/
packetLoss: DownloadUpload;
/**
* The endpoint id of this client.
*/
participantId: string;
/**
* The region that we think the client is in.
*/
region: string;
/**
* Statistics related to display resolutions for each ssrc.
* {{
* [ ssrc ]: {
* height: Number,
* width: Number
* }
* }}.
*/
resolution: {
[ssrc: string]: {
height: number;
width: number;
};
};
/**
* The region of the media server that we are connected to.
*/
serverRegion: string;
/**
* Whether or not additional stats about bandwidth and transport should be
* displayed. Will not display even if true for remote participants.
*/
shouldShowMore: boolean;
/**
* Statistics related to transports.
*/
transport: Array<{
ip: string;
localCandidateType: string;
localip: string;
p2p: boolean;
remoteCandidateType: string;
transportType: string;
type: string;
}>;
/**
* The video SSRC of this client.
*/
videoSsrc: number;
}
/**
* Click handler.
*
* @param {SyntheticEvent} event - The click event.
* @returns {void}
*/
function onClick(event: React.MouseEvent) {
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
// needs to be stopped.
event.stopPropagation();
}
const useStyles = makeStyles()(theme => {
return {
actions: {
margin: '10px auto',
textAlign: 'center'
},
assumedBandwidth: {
cursor: 'pointer',
margin: '0 5px'
},
bandwidth: {
alignItems: 'center',
display: 'flex'
},
connectionStatsTable: {
'&, & > table': {
fontSize: '12px',
fontWeight: 400,
'& td': {
padding: '2px 0'
}
},
'& > table': {
whiteSpace: 'nowrap'
},
'& td:nth-child(n-1)': {
paddingLeft: '5px'
},
'& $upload, & $download': {
marginRight: '2px'
}
},
contextMenu: {
position: 'relative',
margin: 0,
right: 'auto',
padding: `${theme.spacing(2)} ${theme.spacing(1)}`
},
download: {},
mobile: {
margin: theme.spacing(3)
},
status: {
fontWeight: 'bold'
},
upload: {},
link: {
cursor: 'pointer',
color: theme.palette.link01,
transition: 'color .2s ease',
border: 0,
background: 0,
padding: 0,
display: 'inline',
fontWeight: 'bold',
'&:hover': {
color: theme.palette.link01Hover,
textDecoration: 'underline'
},
'&:active': {
color: theme.palette.link01Active
}
}
};
});
const ConnectionStatsTable = ({
audioSsrc,
bandwidth,
bitrate,
bridgeCount,
codec,
connectionSummary,
disableShowMoreStats,
e2eeVerified,
enableAssumedBandwidth,
enableSaveLogs,
framerate,
isVirtualScreenshareParticipant,
isLocalVideo,
isNarrowLayout,
maxEnabledResolution,
onOpenBandwidthDialog,
onSaveLogs,
onShowMore,
packetLoss,
participantId,
region,
resolution,
serverRegion,
shouldShowMore,
transport,
videoSsrc
}: IProps) => {
const { classes, cx } = useStyles();
const { t } = useTranslation();
const _renderResolution = () => {
let resolutionString = 'N/A';
if (resolution && videoSsrc) {
const { width, height } = resolution[videoSsrc] ?? {};
if (width && height) {
resolutionString = `${width}x${height}`;
if (maxEnabledResolution && maxEnabledResolution < 720 && !isVirtualScreenshareParticipant) {
const maxEnabledResolutionTitle = t('connectionindicator.maxEnabledResolution');
resolutionString += ` (${maxEnabledResolutionTitle} ${maxEnabledResolution}p)`;
}
}
}
return (
{t('connectionindicator.resolution')}
{resolutionString}
);
};
const _renderFrameRate = () => {
let frameRateString = 'N/A';
if (framerate) {
frameRateString = String(framerate[videoSsrc] ?? 'N/A');
}
return (
{t('connectionindicator.framerate')}
{frameRateString}
);
};
const _renderScreenShareStatus = () => {
const className = cx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() });
return (
{_renderResolution()}
{_renderFrameRate()}
);
};
const _renderBandwidth = () => {
const { download, upload } = bandwidth || {};
return (
{t('connectionindicator.bandwidth')}
↓
{download ? `${download} Kbps` : 'N/A'}
↑
{upload ? `${upload} Kbps` : 'N/A'}
{enableAssumedBandwidth && (
)}
);
};
const _renderTransportTableRow = (config: any) => {
const { additionalData, data, key, label } = config;
return (
{label}
{getStringFromArray(data)}
{additionalData || null}
);
};
const _renderTransport = () => {
if (!transport || transport.length === 0) {
const NA = (
{t('connectionindicator.address')}
N/A
);
return [ NA ];
}
const data: {
localIP: string[];
localPort: string[];
remoteIP: string[];
remotePort: string[];
transportType: string[];
} = {
localIP: [],
localPort: [],
remoteIP: [],
remotePort: [],
transportType: []
};
for (let i = 0; i < transport.length; i++) {
const ip = getIP(transport[i].ip);
const localIP = getIP(transport[i].localip);
const localPort = getPort(transport[i].localip);
const port = getPort(transport[i].ip);
if (!data.remoteIP.includes(ip)) {
data.remoteIP.push(ip);
}
if (!data.localIP.includes(localIP)) {
data.localIP.push(localIP);
}
if (!data.localPort.includes(localPort)) {
data.localPort.push(localPort);
}
if (!data.remotePort.includes(port)) {
data.remotePort.push(port);
}
if (!data.transportType.includes(transport[i].type)) {
data.transportType.push(transport[i].type);
}
}
// All of the transports should be either P2P or JVB
let isP2P = false, isTURN = false;
if (transport.length) {
isP2P = transport[0].p2p;
isTURN = transport[0].localCandidateType === 'relay'
|| transport[0].remoteCandidateType === 'relay';
}
const additionalData = [];
if (isP2P) {
additionalData.push(
(p2p) );
}
if (isTURN) {
additionalData.push( (turn) );
}
// First show remote statistics, then local, and then transport type.
const tableRowConfigurations = [
{
additionalData,
data: data.remoteIP,
key: 'remoteaddress',
label: t('connectionindicator.remoteaddress',
{ count: data.remoteIP.length })
},
{
data: data.remotePort,
key: 'remoteport',
label: t('connectionindicator.remoteport',
{ count: transport.length })
},
{
data: data.localIP,
key: 'localaddress',
label: t('connectionindicator.localaddress',
{ count: data.localIP.length })
},
{
data: data.localPort,
key: 'localport',
label: t('connectionindicator.localport',
{ count: transport.length })
},
{
data: data.transportType,
key: 'transport',
label: t('connectionindicator.transport',
{ count: data.transportType.length })
}
];
return tableRowConfigurations.map(_renderTransportTableRow);
};
const _renderRegion = () => {
let str = serverRegion;
if (!serverRegion) {
return;
}
if (region && serverRegion && region !== serverRegion) {
str += ` from ${region}`;
}
return (
{t('connectionindicator.connectedTo')}
{str}
);
};
const _renderBridgeCount = () => {
// 0 is valid, but undefined/null/NaN aren't.
if (!bridgeCount && bridgeCount !== 0) {
return;
}
return (
{t('connectionindicator.bridgeCount')}
{bridgeCount}
);
};
const _renderAudioSsrc = () => (
{t('connectionindicator.audio_ssrc')}
{audioSsrc || 'N/A'}
);
const _renderVideoSsrc = () => (
{t('connectionindicator.video_ssrc')}
{videoSsrc || 'N/A'}
);
const _renderParticipantId = () => (
{t('connectionindicator.participant_id')}
{participantId || 'N/A'}
);
const _renderE2EEVerified = () => {
if (e2eeVerified === undefined) {
return;
}
return (
{t('connectionindicator.e2eeVerified')}
{t(`connectionindicator.${e2eeVerified ? 'yes' : 'no'}`)}
);
};
const _renderAdditionalStats = () => (
{isLocalVideo ? _renderBandwidth() : null}
{isLocalVideo ? _renderTransport() : null}
{_renderRegion()}
{isLocalVideo ? _renderBridgeCount() : null}
{_renderAudioSsrc()}
{_renderVideoSsrc()}
{_renderParticipantId()}
{_renderE2EEVerified()}
);
const _renderBitrate = () => {
const { download, upload } = bitrate || {};
return (
{t('connectionindicator.bitrate')}
↓
{download ? `${download} Kbps` : 'N/A'}
↑
{upload ? `${upload} Kbps` : 'N/A'}
);
};
const _renderCodecs = () => {
let codecString = 'N/A';
if (codec) {
const audioCodec = codec[audioSsrc]?.audio;
const videoCodec = codec[videoSsrc]?.video;
if (audioCodec || videoCodec) {
codecString = [ audioCodec, videoCodec ].filter(Boolean).join(', ');
}
}
return (
{t('connectionindicator.codecs')}
{codecString}
);
};
const _renderConnectionSummary = () => (
{t('connectionindicator.status')}
{connectionSummary}
);
const _renderPacketLoss = () => {
let packetLossTableData;
if (packetLoss) {
const { download, upload } = packetLoss;
packetLossTableData = (
↓
{download === null ? 'N/A' : `${download}%`}
↑
{upload === null ? 'N/A' : `${upload}%`}
);
} else {
packetLossTableData = N/A ;
}
return (
{t('connectionindicator.packetloss')}
{packetLossTableData}
);
};
const _renderSaveLogs = () => (
{t('connectionindicator.savelogs')}
|
);
const _renderShowMoreLink = () => {
const translationKey
= shouldShowMore
? 'connectionindicator.less'
: 'connectionindicator.more';
return (
{t(translationKey)}
);
};
const _renderStatistics = () => (
{_renderConnectionSummary()}
{_renderBitrate()}
{_renderPacketLoss()}
{_renderResolution()}
{_renderFrameRate()}
{_renderCodecs()}
);
if (isVirtualScreenshareParticipant) {
return _renderScreenShareStatus();
}
return (
{_renderStatistics()}
{isLocalVideo && enableSaveLogs ? _renderSaveLogs() : null}
{!disableShowMoreStats && _renderShowMoreLink()}
{shouldShowMore ? _renderAdditionalStats() : null}
);
};
/**
* Utility for getting the IP from a transport statistics object's
* representation of an IP.
*
* @param {string} value - The transport's IP to parse.
* @private
* @returns {string}
*/
function getIP(value: string) {
if (!value) {
return '';
}
return value.substring(0, value.lastIndexOf(':'));
}
/**
* Utility for getting the port from a transport statistics object's
* representation of an IP.
*
* @param {string} value - The transport's IP to parse.
* @private
* @returns {string}
*/
function getPort(value: string) {
if (!value) {
return '';
}
return value.substring(value.lastIndexOf(':') + 1, value.length);
}
/**
* Utility for concatenating values in an array into a comma separated string.
*
* @param {Array} array - Transport statistics to concatenate.
* @private
* @returns {string}
*/
function getStringFromArray(array: string[]) {
let res = '';
for (let i = 0; i < array.length; i++) {
res += (i === 0 ? '' : ', ') + array[i];
}
return res;
}
export default ConnectionStatsTable;