feat(stats) add stats for mobile

This commit is contained in:
tmoldovan8x8 2020-12-22 11:12:52 +02:00 committed by GitHub
parent 8d813a499c
commit 5ecb5717c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 549 additions and 17 deletions

View File

@ -293,7 +293,7 @@ PODS:
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webrtc (1.87.1):
- react-native-webrtc (1.87.2):
- React-Core
- react-native-webview (11.0.2):
- React-Core
@ -562,7 +562,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webrtc: 40eca4cac200fda34fb843da07e3402211bbbd10
react-native-webrtc: e6fca8432542dd1c77afa6c59629f0176ed78ee6
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

View File

@ -83,6 +83,7 @@
"bandwidth": "Estimated bandwidth:",
"bitrate": "Bitrate:",
"bridgeCount": "Server count: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Connected to:",
"framerate": "Frame rate:",
"less": "Show less",

View File

@ -844,6 +844,7 @@
"standardDefinition": "Standard definition"
},
"videothumbnail": {
"connectionInfo": "Connection Info",
"domute": "Mute",
"domuteOthers": "Mute everyone else",
"flip": "Flip",

10
package-lock.json generated
View File

@ -10776,8 +10776,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#8bb653f1d6d450f4d17f56eb6bd4c353138f6829",
"from": "github:jitsi/lib-jitsi-meet#8bb653f1d6d450f4d17f56eb6bd4c353138f6829",
"version": "github:jitsi/lib-jitsi-meet#310983c5b0398d213a2ca054c2eb0e9797fa1ae0",
"from": "github:jitsi/lib-jitsi-meet#310983c5b0398d213a2ca054c2eb0e9797fa1ae0",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",
@ -14284,9 +14284,9 @@
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
},
"react-native-webrtc": {
"version": "1.87.1",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.87.1.tgz",
"integrity": "sha512-XIztid40ohLUoOIDqpavskyAPzopWIjNOoC/y3AtTymt+o+W/rIHZ9Qw8JZCaIjWh2AIrcO2wtb/f1aMWSz2Zw==",
"version": "1.87.2",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.87.2.tgz",
"integrity": "sha512-bUMoMvfK17nT8S2w16bpL1uMMyDvDwOmhVjGrP6FDrCS7lAx/w2jVMUtZlNVS6zCJXN92wTkYJ6P3z+nr2hhNg==",
"requires": {
"base64-js": "^1.1.2",
"cross-os": "^1.3.0",

View File

@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#8bb653f1d6d450f4d17f56eb6bd4c353138f6829",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#310983c5b0398d213a2ca054c2eb0e9797fa1ae0",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@ -84,7 +84,7 @@
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.87.1",
"react-native-webrtc": "1.87.2",
"react-native-webview": "11.0.2",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 24l-8-9h6v-15h4v15h6z"/>
</svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 0l8 9h-6v15h-4v-15h-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 129 B

View File

@ -4,8 +4,10 @@ export { default as IconAdd } from './add.svg';
export { default as IconAddPeople } from './link.svg';
export { default as IconArrowBack } from './arrow_back.svg';
export { default as IconArrowDown } from './arrow_down.svg';
export { default as IconArrowDownLarge } from './arrow_down_large.svg';
export { default as IconArrowDownSmall } from './arrow-down-small.svg';
export { default as IconArrowUp } from './arrow_up.svg';
export { default as IconArrowUpLarge } from './arrow_up_large.svg';
export { default as IconArrowLeft } from './arrow-left.svg';
export { default as IconAudioOnly } from './visibility.svg';
export { default as IconAudioOnlyOff } from './visibility-off.svg';

View File

@ -7,6 +7,7 @@ import { Icon } from '../../../icons';
import { type StyleType } from '../../../styles';
import styles from './indicatorstyles';
import { BASE_INDICATOR } from './styles';
type Props = {
@ -40,7 +41,9 @@ export default class BaseIndicator extends Component<Props> {
const { highlight, icon, iconStyle } = this.props;
return (
<View style = { highlight ? styles.highlightedIndicator : null }>
<View
style = { [ BASE_INDICATOR,
highlight ? styles.highlightedIndicator : null ] }>
<Icon
src = { icon }
style = { [

View File

@ -208,6 +208,11 @@ export const TINTED_VIEW_DEFAULT = {
opacity: 0.8
};
export const BASE_INDICATOR = {
alignItems: 'center',
justifyContent: 'center'
};
/**
* The styles of the generic React {@code Component}s implemented by the feature
* base/react.

View File

@ -21,6 +21,7 @@ import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayNameLabel } from '../../../display-name';
import { RemoteVideoMenu } from '../../../remote-video-menu';
import ConnectionStatusComponent from '../../../remote-video-menu/components/native/ConnectionStatusComponent';
import { toggleToolboxVisible } from '../../../toolbox/actions.native';
import AudioMutedIndicator from './AudioMutedIndicator';
@ -54,7 +55,7 @@ type Props = {
/**
* Handles long press on the thumbnail.
*/
_onShowRemoteVideoMenu: ?Function,
_onThumbnailLongPress: ?Function,
/**
* Whether to show the dominant speaker indicator or not.
@ -120,7 +121,7 @@ function Thumbnail(props: Props) {
_audioMuted: audioMuted,
_largeVideo: largeVideo,
_onClick,
_onShowRemoteVideoMenu,
_onThumbnailLongPress,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
_styles,
@ -140,7 +141,7 @@ function Thumbnail(props: Props) {
return (
<Container
onClick = { _onClick }
onLongPress = { participant.local ? undefined : _onShowRemoteVideoMenu }
onLongPress = { _onThumbnailLongPress }
style = { [
styles.thumbnail,
participant.pinned && !tileView
@ -230,12 +231,18 @@ function _mapDispatchToProps(dispatch: Function, ownProps): Object {
*
* @returns {void}
*/
_onShowRemoteVideoMenu() {
_onThumbnailLongPress() {
const { participant } = ownProps;
dispatch(openDialog(RemoteVideoMenu, {
participant
}));
if (participant.local) {
dispatch(openDialog(ConnectionStatusComponent, {
participantID: participant.id
}));
} else {
dispatch(openDialog(RemoteVideoMenu, {
participant
}));
}
}
};
}

View File

@ -0,0 +1,51 @@
// @flow
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconInfo } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import ConnectionStatusComponent from './ConnectionStatusComponent';
export type Props = AbstractButtonProps & {
/**
* The redux {@code 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
};
/**
* A remote video menu button which shows the connection statistics.
*/
class ConnectionStatusButton extends AbstractButton<Props, *> {
icon = IconInfo;
label = 'videothumbnail.connectionInfo';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
dispatch(openDialog(ConnectionStatusComponent, {
participantID
}));
}
}
export default translate(connect()(ConnectionStatusButton));

View File

@ -0,0 +1,431 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { BottomSheet, isDialogOpen, hideDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconArrowDownLarge, IconArrowUpLarge } from '../../../base/icons';
import { getParticipantDisplayName } from '../../../base/participants';
import { BaseIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import { StyleType, ColorPalette } from '../../../base/styles';
import statsEmitter from '../../../connection-indicator/statsEmitter';
import styles from './styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 25;
const CONNECTION_QUALITY = [
'Low',
'Medium',
'Good'
];
export type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string,
/**
* The color-schemed stylesheet of the BottomSheet.
*/
_bottomSheetStyles: StyleType,
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean,
/**
* Display name of the participant retreived from Redux.
*/
_participantDisplayName: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
}
/**
* The type of the React {@code Component} state of {@link ConnectionStatusComponent}.
*/
type State = {
resolutionString: string,
downloadString: string,
uploadString: string,
packetLostDownloadString: string,
packetLostUploadString: string,
serverRegionString: string,
codecString: string,
connectionString: string
};
// eslint-disable-next-line prefer-const
let ConnectionStatusComponent_;
/**
* Class to implement a popup menu that show the connection statistics.
*/
class ConnectionStatusComponent extends Component<Props, State> {
/**
* Constructor of the component.
*
* @param {P} props - The read-only properties with which the new
* instance is to be initialized.
*
* @inheritdoc
*/
constructor(props: Props) {
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 {React$Node}
*/
render(): React$Node {
const { t } = this.props;
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 }>
{ this.state.connectionString }
</Text>
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ `${t('connectionindicator.bitrate')}` }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
iconStyle = {{
color: ColorPalette.darkGrey
}} />
<Text style = { styles.statsInfoText }>
{ this.state.downloadString }
</Text>
<BaseIndicator
icon = { IconArrowUpLarge }
iconStyle = {{
color: ColorPalette.darkGrey
}} />
<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: ColorPalette.darkGrey
}} />
<Text style = { styles.statsInfoText }>
{ this.state.packetLostDownloadString }
</Text>
<BaseIndicator
icon = { IconArrowUpLarge }
iconStyle = {{
color: ColorPalette.darkGrey
}} />
<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: Props) {
if (prevProps.participantID !== this.props.participantID) {
statsEmitter.unsubscribeToClientStats(
prevProps.participantID, this._onStatsUpdated);
statsEmitter.subscribeToClientStats(
this.props.participantID, this._onStatsUpdated);
}
}
_onStatsUpdated: Object => void;
/**
* 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) {
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) ?? this.state.connectionString
};
}
/**
* Extracts the resolution and framerate.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractResolutionString(stats) {
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) {
return stats.bitrate;
}
/**
* Extracts the download and upload packet lost.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {{ download, upload }}
*/
_extractPacketLost(stats) {
return stats.packetLoss;
}
/**
* Extracts the server name.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractServer(stats) {
return stats.serverRegion;
}
/**
* Extracts the audio and video codecs names.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractCodecs(stats) {
const { codec } = stats;
let codecString;
// Only report one codec, in case there are multiple for a user.
Object.keys(codec || {})
.forEach(ssrc => {
const { audio, video } = codec[ssrc];
codecString = `${audio}, ${video}`;
});
return codecString;
}
/**
* Extracts the connection percentage and sets connection quality.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractConnection(stats) {
const { connectionQuality } = stats;
if (connectionQuality) {
const signalLevel = Math.floor(connectionQuality / 33.4);
return CONNECTION_QUALITY[signalLevel];
}
}
_onCancel: () => boolean;
/**
* Callback to hide the {@code ConnectionStatusComponent}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
statsEmitter.unsubscribeToClientStats(
this.props.participantID, this._onStatsUpdated);
if (this.props._isOpen) {
this.props.dispatch(hideDialog(ConnectionStatusComponent_));
return true;
}
return false;
}
_renderMenuHeader: () => React$Element<any>;
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { _bottomSheetStyles, 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, ownProps) {
const { participantID } = ownProps;
return {
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
_isOpen: isDialogOpen(state, ConnectionStatusComponent_),
_participantDisplayName: getParticipantDisplayName(state, participantID)
};
}
ConnectionStatusComponent_ = translate(connect(_mapStateToProps)(ConnectionStatusComponent));
export default ConnectionStatusComponent_;

View File

@ -13,6 +13,7 @@ import { StyleType } from '../../../base/styles';
import { PrivateMessageButton } from '../../../chat';
import { hideRemoteVideoMenu } from '../../actions';
import ConnectionStatusButton from './ConnectionStatusButton';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
@ -106,6 +107,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
<PinButton { ...buttonProps } />
<PrivateMessageButton { ...buttonProps } />
<MuteEveryoneElseButton { ...buttonProps } />
<ConnectionStatusButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@ -25,5 +25,28 @@ export default createStyleSheet({
fontSize: MD_FONT_SIZE,
marginLeft: MD_ITEM_MARGIN_PADDING,
opacity: 0.90
},
statsTitleText: {
fontSize: 16,
fontWeight: 'bold',
marginRight: 3
},
statsInfoText: {
fontSize: 16,
marginRight: 2,
marginLeft: 2
},
statsInfoCell: {
alignItems: 'center',
flexDirection: 'row',
height: 30,
justifyContent: 'flex-start'
},
statsWrapper: {
marginVertical: 10
}
});