jiti-meet/react/features/video-menu/components/native/ConnectionStatusComponent.tsx

489 lines
15 KiB
TypeScript

/* eslint-disable lines-around-comment */
import React, { PureComponent } from 'react';
import { Text, View } from 'react-native';
import { withTheme } from 'react-native-paper';
import { IReduxState } from '../../../app/types';
// @ts-ignore
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
// @ts-ignore
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
// @ts-ignore
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { translate } from '../../../base/i18n/functions';
import { IconArrowDownLarge, IconArrowUpLarge } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getParticipantDisplayName } from '../../../base/participants/functions';
// @ts-ignore
import BaseIndicator from '../../../base/react/components/native/BaseIndicator';
import { connect } from '../../../base/redux/functions';
import {
getTrackByMediaTypeAndParticipant
} from '../../../base/tracks/functions.native';
import {
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../../connection-indicator/functions';
import statsEmitter from '../../../connection-indicator/statsEmitter';
// @ts-ignore
import styles from './styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 25;
const CONNECTION_QUALITY = [
// Full (3 bars)
{
msg: 'connectionindicator.quality.good',
percent: 30 // INDICATOR_DISPLAY_THRESHOLD
},
// 2 bars.
{
msg: 'connectionindicator.quality.nonoptimal',
percent: 10
},
// 1 bar.
{
msg: 'connectionindicator.quality.poor',
percent: 0
}
];
type IProps = {
/**
* Whether this participant's connection is inactive.
*/
_isConnectionStatusInactive: boolean;
/**
* Whether this participant's connection is interrupted.
*/
_isConnectionStatusInterrupted: boolean;
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean;
/**
* Display name of the participant retrieved from Redux.
*/
_participantDisplayName: string;
/**
* The Redux dispatch function.
*/
dispatch: Function;
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string;
/**
* The function to be used to translate i18n labels.
*/
t: Function;
/**
* Theme used for styles.
*/
theme: any;
};
/**
* The type of the React {@code Component} state of {@link ConnectionStatusComponent}.
*/
type IState = {
codecString: string;
connectionString: string;
downloadString: string;
packetLostDownloadString: string;
packetLostUploadString: string;
resolutionString: string;
serverRegionString: string;
uploadString: string;
};
/**
* Class to implement a popup menu that show the connection statistics.
*/
class ConnectionStatusComponent extends PureComponent<IProps, IState> {
/**
* Constructor of the component.
*
* @param {P} props - The read-only properties with which the new
* instance is to be initialized.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onStatsUpdated = this._onStatsUpdated.bind(this);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
this.state = {
resolutionString: 'N/A',
downloadString: 'N/A',
uploadString: 'N/A',
packetLostDownloadString: 'N/A',
packetLostUploadString: 'N/A',
serverRegionString: 'N/A',
codecString: 'N/A',
connectionString: 'N/A'
};
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactNode}
*/
render() {
const { t, theme } = this.props;
const { palette } = theme;
return (
<BottomSheet
onCancel = { this._onCancel }
renderHeader = { this._renderMenuHeader }>
<View style = { styles.statsWrapper }>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ t('connectionindicator.status') }
</Text>
<Text style = { styles.statsInfoText }>
{ t(this.state.connectionString) }
</Text>
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ t('connectionindicator.bitrate') }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText }>
{ this.state.downloadString }
</Text>
<BaseIndicator
icon = { IconArrowUpLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText }>
{ `${this.state.uploadString} Kbps` }
</Text>
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ t('connectionindicator.packetloss') }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText }>
{ this.state.packetLostDownloadString }
</Text>
<BaseIndicator
icon = { IconArrowUpLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText }>
{ this.state.packetLostUploadString }
</Text>
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ t('connectionindicator.resolution') }
</Text>
<Text style = { styles.statsInfoText }>
{ this.state.resolutionString }
</Text>
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ t('connectionindicator.codecs') }
</Text>
<Text style = { styles.statsInfoText }>
{ this.state.codecString }
</Text>
</View>
</View>
</BottomSheet>
);
}
/**
* Starts listening for stat updates.
*
* @inheritdoc
* returns {void}
*/
componentDidMount() {
statsEmitter.subscribeToClientStats(this.props.participantID, this._onStatsUpdated);
}
/**
* Updates which user's stats are being listened to.
*
* @inheritdoc
* returns {void}
*/
componentDidUpdate(prevProps: IProps) {
if (prevProps.participantID !== this.props.participantID) {
statsEmitter.unsubscribeToClientStats(
prevProps.participantID, this._onStatsUpdated);
statsEmitter.subscribeToClientStats(
this.props.participantID, this._onStatsUpdated);
}
}
/**
* Callback invoked when new connection stats associated with the passed in
* user ID are available. Will update the component's display of current
* statistics.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {void}
*/
_onStatsUpdated(stats = {}) {
const newState = this._buildState(stats);
this.setState(newState);
}
/**
* Extracts statistics and builds the state object.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {State}
*/
_buildState(stats: any) {
const { download: downloadBitrate, upload: uploadBitrate } = this._extractBitrate(stats) ?? {};
const { download: downloadPacketLost, upload: uploadPacketLost } = this._extractPacketLost(stats) ?? {};
return {
resolutionString: this._extractResolutionString(stats) ?? this.state.resolutionString,
downloadString: downloadBitrate ?? this.state.downloadString,
uploadString: uploadBitrate ?? this.state.uploadString,
packetLostDownloadString: downloadPacketLost === undefined
? this.state.packetLostDownloadString : `${downloadPacketLost}%`,
packetLostUploadString: uploadPacketLost === undefined
? this.state.packetLostUploadString : `${uploadPacketLost}%`,
serverRegionString: this._extractServer(stats) ?? this.state.serverRegionString,
codecString: this._extractCodecs(stats) ?? this.state.codecString,
connectionString: this._extractConnection(stats)
};
}
/**
* Extracts the resolution and framerate.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractResolutionString(stats: any) {
const { framerate, resolution } = stats;
const resolutionString = Object.keys(resolution || {})
.map(ssrc => {
const { width, height } = resolution[ssrc];
return `${width}x${height}`;
})
.join(', ') || null;
const frameRateString = Object.keys(framerate || {})
.map(ssrc => framerate[ssrc])
.join(', ') || null;
return resolutionString && frameRateString ? `${resolutionString}@${frameRateString}fps` : undefined;
}
/**
* Extracts the download and upload bitrates.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {{ download, upload }}
*/
_extractBitrate(stats: any) {
return stats.bitrate;
}
/**
* Extracts the download and upload packet lost.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {{ download, upload }}
*/
_extractPacketLost(stats: any) {
return stats.packetLoss;
}
/**
* Extracts the server name.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractServer(stats: any) {
return stats.serverRegion;
}
/**
* Extracts the audio and video codecs names.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractCodecs(stats: any) {
const { codec } = stats;
let codecString;
if (codec) {
const audioCodecs = Object.values(codec)
.map((c: any) => c.audio)
.filter(Boolean);
const videoCodecs = Object.values(codec)
.map((c: any) => c.video)
.filter(Boolean);
if (audioCodecs.length || videoCodecs.length) {
// Use a Set to eliminate duplicates.
codecString = Array.from(new Set([ ...audioCodecs, ...videoCodecs ])).join(', ');
}
}
return codecString;
}
/**
* Extracts the connection percentage and sets connection quality.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractConnection(stats: any) {
const { connectionQuality } = stats;
const {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
} = this.props;
if (_isConnectionStatusInactive) {
return 'connectionindicator.quality.inactive';
} else if (_isConnectionStatusInterrupted) {
return 'connectionindicator.quality.lost';
} else if (typeof connectionQuality === 'undefined') {
return 'connectionindicator.quality.good';
}
const qualityConfig = this._getQualityConfig(connectionQuality);
return qualityConfig.msg;
}
/**
* Get the quality configuration from CONNECTION_QUALITY which has a percentage
* that matches or exceeds the passed in percentage. The implementation
* assumes CONNECTION_QUALITY is already sorted by highest to lowest
* percentage.
*
* @param {number} percent - The connection percentage, out of 100, to find
* the closest matching configuration for.
* @private
* @returns {Object}
*/
_getQualityConfig(percent: number): any {
return CONNECTION_QUALITY.find(x => percent >= x.percent) || {};
}
/**
* Callback to hide the {@code ConnectionStatusComponent}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
statsEmitter.unsubscribeToClientStats(this.props.participantID, this._onStatsUpdated);
this.props.dispatch(hideSheet());
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { participantID } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] }>
<Avatar
participantId = { participantID }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }>
{ this.props._participantDisplayName }
</Text>
</View>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const { participantID } = ownProps;
const tracks = state['features/base/tracks'];
const _videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _isConnectionStatusInactive = isTrackStreamingStatusInactive(_videoTrack);
const _isConnectionStatusInterrupted = isTrackStreamingStatusInterrupted(_videoTrack);
return {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_participantDisplayName: getParticipantDisplayName(state, participantID)
};
}
// @ts-ignore
export default translate(connect(_mapStateToProps)(withTheme(ConnectionStatusComponent)));