Merge pull request #349 from havfo/feat-network-indicator

create component "NetworkIndicator"

add to VideoView for Me,
receive data from server / getTransportStats,
estimate of connection/value [wip]
show recv/send bitrate in advanced mode

* Change probe counting from sec to state inc

* Rebuild, fix null error with null

* Store probe from zero

* Add highestBitrate

* Fix eslint errors

* remove graphical indicator, refactor only to statistics

* uninstall "wifi-network-indicator" package

* Remove "netInfo" class

* Change "advanced mode" box info "bitrate" from MB/s to Mb/s

* fix my own fault

* Fix crash when screen sharing is running

* Update transport stats only when advancedMode is on

* Fix/Mute eslint Multiline support info

* Fix lint

* Set VideoView/netInfo prop as not required

* Fix lint

* Set RoomClient/_getTransportStats() as public

Co-authored-by: Stefan Otto <stefan.otto@uninett.no>
auto_join_3.3
Håvar Aambø Fosstveit 2020-05-20 18:02:47 +02:00 committed by GitHub
commit 21a56f3b40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 213 additions and 48 deletions

View File

@ -14,6 +14,7 @@ import * as lobbyPeerActions from './actions/lobbyPeerActions';
import * as consumerActions from './actions/consumerActions'; import * as consumerActions from './actions/consumerActions';
import * as producerActions from './actions/producerActions'; import * as producerActions from './actions/producerActions';
import * as notificationActions from './actions/notificationActions'; import * as notificationActions from './actions/notificationActions';
import * as transportActions from './actions/transportActions';
let createTorrent; let createTorrent;
@ -616,6 +617,36 @@ export default class RoomClient
}); });
} }
async getTransportStats()
{
try
{
if (this._recvTransport)
{
logger.debug('getTransportStats() - recv [transportId: "%s"]', this._recvTransport.id);
const recv = await this.sendRequest('getTransportStats', { transportId: this._recvTransport.id });
store.dispatch(
transportActions.addTransportStats(recv, 'recv'));
}
if (this._sendTransport)
{
logger.debug('getTransportStats() - send [transportId: "%s"]', this._sendTransport.id);
const send = await this.sendRequest('getTransportStats', { transportId: this._sendTransport.id });
store.dispatch(
transportActions.addTransportStats(send, 'send'));
}
}
catch (error)
{
logger.error('getTransportStats() | failed: %o', error);
}
}
async sendRequest(method, data) async sendRequest(method, data)
{ {
logger.debug('sendRequest() [method:"%s", data:"%o"]', method, data); logger.debug('sendRequest() [method:"%s", data:"%o"]', method, data);
@ -1043,7 +1074,7 @@ export default class RoomClient
{ {
if (volume < this._hark.lastVolume) if (volume < this._hark.lastVolume)
{ {
volume = this._hark.lastVolume - Math.pow((volume - this._hark.lastVolume)/(100 + this._hark.lastVolume),4)*2; volume = this._hark.lastVolume - Math.pow((volume - this._hark.lastVolume)/(100 + this._hark.lastVolume), 4)*2;
} }
this._hark.lastVolume = volume; this._hark.lastVolume = volume;
store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume));
@ -2092,6 +2123,7 @@ export default class RoomClient
{ {
switch (notification.method) switch (notification.method)
{ {
case 'enteredLobby': case 'enteredLobby':
{ {
store.dispatch(roomActions.setInLobby(true)); store.dispatch(roomActions.setInLobby(true));

View File

@ -0,0 +1,5 @@
export const addTransportStats = (transport, type) =>
({
type : 'ADD_TRANSPORT_STATS',
payload : { transport, type }
});

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
meProducersSelector, meProducersSelector,
@ -176,6 +176,7 @@ const Me = (props) =>
screenProducer, screenProducer,
extraVideoProducers, extraVideoProducers,
canShareScreen, canShareScreen,
transports,
noiseVolume, noiseVolume,
classes classes
} = props; } = props;
@ -333,6 +334,20 @@ const Me = (props) =>
); );
} }
useEffect(() =>
{
let poll;
const interval = 1000;
if (advancedMode)
{
poll = setInterval(() => roomClient.getTransportStats(), interval);
}
return () => clearInterval(poll);
}, [ roomClient, advancedMode ]);
return ( return (
<React.Fragment> <React.Fragment>
<div <div
@ -673,6 +688,7 @@ const Me = (props) =>
videoVisible={videoVisible} videoVisible={videoVisible}
audioCodec={micProducer && micProducer.codec} audioCodec={micProducer && micProducer.codec}
videoCodec={webcamProducer && webcamProducer.codec} videoCodec={webcamProducer && webcamProducer.codec}
netInfo={transports && transports}
audioScore={audioScore} audioScore={audioScore}
videoScore={videoScore} videoScore={videoScore}
onChangeDisplayName={(displayName) => onChangeDisplayName={(displayName) =>
@ -888,7 +904,8 @@ Me.propTypes =
canShareScreen : PropTypes.bool.isRequired, canShareScreen : PropTypes.bool.isRequired,
noiseVolume : PropTypes.number, noiseVolume : PropTypes.number,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired,
transports : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () => const makeMapStateToProps = () =>
@ -916,6 +933,7 @@ const makeMapStateToProps = () =>
settings : state.settings, settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId, activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen : hasPermission(state), canShareScreen : hasPermission(state),
transports : state.transports,
noiseVolume : volume noiseVolume : volume
}; };
}; };
@ -937,7 +955,8 @@ export default withRoomContext(connect(
Math.round(next.peerVolumes[next.me.id]) && Math.round(next.peerVolumes[next.me.id]) &&
prev.peers === next.peers && prev.peers === next.peers &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings prev.settings === next.settings &&
prev.transports === next.transports
); );
} }
} }

View File

@ -25,6 +25,7 @@ const styles = (theme) =>
flexDirection : 'column', flexDirection : 'column',
overflow : 'hidden' overflow : 'hidden'
}, },
video : video :
{ {
flex : '100 100 auto', flex : '100 100 auto',
@ -75,17 +76,51 @@ const styles = (theme) =>
{ {
padding : theme.spacing(0.5), padding : theme.spacing(0.5),
borderRadius : 2, borderRadius : 2,
'& p' : userSelect : 'none',
{ margin : 0,
userSelect : 'none', color : 'rgba(255, 255, 255, 0.7)',
margin : 0, fontSize : '0.8em',
color : 'rgba(255, 255, 255, 0.7)',
fontSize : '0.8em'
},
'&.left' : '&.left' :
{ {
backgroundColor : 'rgba(0, 0, 0, 0.25)' backgroundColor : 'rgba(0, 0, 0, 0.25)',
}, display : 'grid',
gap : '1px 5px',
// eslint-disable-next-line
gridTemplateAreas : '\
"AcodL Acod Acod Acod Acod" \
"VcodL Vcod Vcod Vcod Vcod" \
"ResL Res Res Res Res" \
"RecvL RecvBps RecvBps RecvSum RecvSum" \
"SendL SendBps SendBps SendSum SendSum" \
"IPlocL IPloc IPloc IPloc IPloc" \
"IPsrvL IPsrv IPsrv IPsrv IPsrv" \
"STLcurrL STLcurr STLcurr STLcurr STLcurr" \
"STLprefL STLpref STLpref STLpref STLpref"',
'& .AcodL' : { gridArea: 'AcodL' },
'& .Acod' : { gridArea: 'Acod' },
'& .VcodL' : { gridArea: 'VcodL' },
'& .Vcod' : { gridArea: 'Vcod' },
'& .ResL' : { gridArea: 'ResL' },
'& .Res' : { gridArea: 'Res' },
'& .RecvL' : { gridArea: 'RecvL' },
'& .RecvBps' : { gridArea: 'RecvBps', justifySelf: 'flex-end' },
'& .RecvSum' : { gridArea: 'RecvSum', justifySelf: 'flex-end' },
'& .SendL' : { gridArea: 'SendL' },
'& .SendBps' : { gridArea: 'SendBps', justifySelf: 'flex-end' },
'& .SendSum' : { gridArea: 'SendSum', justifySelf: 'flex-end' },
'& .IPlocL' : { gridArea: 'IPlocL' },
'& .IPloc' : { gridArea: 'IPloc' },
'& .IPsrvL' : { gridArea: 'IPsrvL' },
'& .IPsrv' : { gridArea: 'IPsrv' },
'& .STLcurrL' : { gridArea: 'STLcurrL' },
'& .STLcurr' : { gridArea: 'STLcurr' },
'& .STLprefL' : { gridArea: 'STLprefL' },
'& .STLpref' : { gridArea: 'STLpref' }
},
'&.right' : '&.right' :
{ {
marginLeft : 'auto', marginLeft : 'auto',
@ -170,7 +205,8 @@ class VideoView extends React.PureComponent
videoCodec, videoCodec,
onChangeDisplayName, onChangeDisplayName,
children, children,
classes classes,
netInfo
} = this.props; } = this.props;
const { const {
@ -244,25 +280,74 @@ class VideoView extends React.PureComponent
<div className={classes.info}> <div className={classes.info}>
<div className={classes.media}> <div className={classes.media}>
<div className={classnames(classes.box, 'left', { hidden: !advancedMode })}> <div className={classnames(classes.box, 'left', { hidden: !advancedMode })}>
{ audioCodec && <p>{audioCodec}</p> } { audioCodec &&
<React.Fragment>
{ videoCodec && <span className={'AcodL'}>Acod: </span>
<p> <span className={'Acod'}>
{videoCodec} {audioCodec}
</p> </span>
</React.Fragment>
} }
{ videoMultiLayer && { videoCodec &&
<p> <React.Fragment>
{`current spatial-temporal layers: ${consumerCurrentSpatialLayer} ${consumerCurrentTemporalLayer}`} <span className={'VcodL'}>Vcod: </span>
<br /> <span className={'Vcod'}>
{`preferred spatial-temporal layers: ${consumerPreferredSpatialLayer} ${consumerPreferredTemporalLayer}`} {videoCodec}
</p> </span>
</React.Fragment>
} }
{ (videoVisible && videoWidth !== null) && { (videoVisible && videoWidth !== null) &&
<p>{videoWidth}x{videoHeight}</p> <React.Fragment>
<span className={'ResL'}>Res: </span>
<span className={'Res'}>
{videoWidth}x{videoHeight}
</span>
</React.Fragment>
} }
{ isMe && !isScreen &&
(netInfo.recv && netInfo.send && netInfo.send.iceSelectedTuple) &&
<React.Fragment>
<span className={'RecvL'}>Recv: </span>
<span className={'RecvBps'}>
{(netInfo.recv.sendBitrate/1024/1024).toFixed(2)}Mb/s
</span>
<span className={'RecvSum'}>
{(netInfo.recv.bytesSent/1024/1024).toFixed(2)}MB
</span>
<span className={'SendL'}>Send: </span>
<span className={'SendBps'}>
{(netInfo.send.recvBitrate/1024/1024).toFixed(2)}Mb/s
</span>
<span className={'SendSum'}>
{(netInfo.send.bytesReceived/1024/1024).toFixed(2)}MB
</span>
<span className={'IPlocL'}>IPloc: </span>
<span className={'IPloc'}>
{netInfo.send.iceSelectedTuple.remoteIp}
</span>
<span className={'IPsrvL'}>IPsrv: </span>
<span className={'IPsrv'}>
{netInfo.send.iceSelectedTuple.localIp}
</span>
</React.Fragment>
}
{ videoMultiLayer &&
<React.Fragment>
<span className={'STLcurrL'}>STLcurr: </span>
<span className={'STLcurr'}>{consumerCurrentSpatialLayer} {consumerCurrentTemporalLayer}</span>
<span className={'STLprefL'}>STLpref: </span>
<span className={'STLpref'}>{consumerPreferredSpatialLayer} {consumerPreferredTemporalLayer}</span>
</React.Fragment>
}
</div> </div>
{ showQuality && { showQuality &&
<div className={classnames(classes.box, 'right')}> <div className={classnames(classes.box, 'right')}>
@ -277,20 +362,24 @@ class VideoView extends React.PureComponent
<div className={classes.peer}> <div className={classes.peer}>
<div className={classes.box}> <div className={classes.box}>
{ isMe ? { isMe ?
<EditableInput <React.Fragment>
value={displayName} <EditableInput
propName='newDisplayName' value={displayName}
className={classes.displayNameEdit} propName='newDisplayName'
classLoading='loading' className={classes.displayNameEdit}
classInvalid='invalid' classLoading='loading'
shouldBlockWhileLoading classInvalid='invalid'
editProps={{ shouldBlockWhileLoading
maxLength : 30, editProps={{
autoCorrect : 'off', maxLength : 30,
spellCheck : false autoCorrect : 'off',
}} spellCheck : false
onChange={({ newDisplayName }) => onChangeDisplayName(newDisplayName)} }}
/> onChange={
({ newDisplayName }) =>
onChangeDisplayName(newDisplayName)}
/>
</React.Fragment>
: :
<span className={classes.displayNameStatic}> <span className={classes.displayNameStatic}>
{displayName} {displayName}
@ -466,7 +555,8 @@ VideoView.propTypes =
videoCodec : PropTypes.string, videoCodec : PropTypes.string,
onChangeDisplayName : PropTypes.func, onChangeDisplayName : PropTypes.func,
children : PropTypes.object, children : PropTypes.object,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired,
netInfo : PropTypes.object
}; };
export default withStyles(styles)(VideoView); export default withStyles(styles)(VideoView);

View File

@ -1,5 +1,3 @@
class AudioAnalyzer extends EventEmitter class AudioAnalyzer extends EventEmitter
{ {

View File

@ -11,14 +11,16 @@ import chat from './chat';
import toolarea from './toolarea'; import toolarea from './toolarea';
import files from './files'; import files from './files';
import settings from './settings'; import settings from './settings';
import transports from './transports';
export default combineReducers({ export default combineReducers({
room, room,
me, me,
producers, producers,
consumers,
transports,
peers, peers,
lobbyPeers, lobbyPeers,
consumers,
peerVolumes, peerVolumes,
notifications, notifications,
chat, chat,

View File

@ -0,0 +1,19 @@
const initialState = {};
const transports = (state = initialState, action) =>
{
switch (action.type)
{
case 'ADD_TRANSPORT_STATS':
{
const { transport, type } = action.payload;
return { ...state, [type]: transport[0] };
}
default:
return state;
}
};
export default transports;