added network performance tab

This commit is contained in:
J Taylor 2021-09-28 12:31:05 +01:00
parent fff932f979
commit a65e97ee9a
22 changed files with 165 additions and 152 deletions

View File

@ -1,5 +1,6 @@
import React from 'react';
import { PeopleAside } from '@custom/shared/components/Aside/PeopleAside';
import { NetworkAside } from '@custom/shared/components/Aside';
import { PeopleAside } from '@custom/shared/components/Aside';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
export const Asides = () => {
@ -8,6 +9,7 @@ export const Asides = () => {
return (
<>
<PeopleAside />
<NetworkAside />
{asides.map((AsideComponent) => (
<AsideComponent key={AsideComponent.name} />
))}

View File

@ -1,4 +0,0 @@
{
"presets": ["next/babel"],
"plugins": ["inline-react-svg"]
}

View File

@ -1,13 +0,0 @@
import React from 'react';
import App from '@custom/basic-call/components/App';
import { RecordingProvider } from '../../contexts/RecordingProvider';
// Extend our basic call app component with the recording context
export const AppWithRecording = () => (
<RecordingProvider>
<App />
</RecordingProvider>
);
export default AppWithRecording;

View File

@ -1 +0,0 @@
export { AppWithRecording as default } from './App';

View File

@ -1,47 +0,0 @@
import React, { useEffect } from 'react';
import { TrayButton } from '@custom/shared/components/Tray';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { ReactComponent as IconRecord } from '@custom/shared/icons/record-md.svg';
import {
RECORDING_ERROR,
RECORDING_RECORDING,
RECORDING_SAVED,
RECORDING_UPLOADING,
useRecording,
} from '../../contexts/RecordingProvider';
import { RECORDING_MODAL } from '../RecordingModal';
export const Tray = () => {
const { openModal } = useUIState();
const { recordingState } = useRecording();
useEffect(() => {
console.log(`⏺️ Recording state: ${recordingState}`);
if (recordingState === RECORDING_ERROR) {
// show error modal here
}
}, [recordingState]);
const isRecording = [
RECORDING_RECORDING,
RECORDING_UPLOADING,
RECORDING_SAVED,
].includes(recordingState);
return (
<>
<TrayButton
label={isRecording ? 'Recording' : 'Record'}
orange={isRecording}
onClick={() => openModal(RECORDING_MODAL)}
>
<IconRecord />
</TrayButton>
</>
);
};
export default Tray;

View File

@ -1 +0,0 @@
export { Tray as default } from './Tray';

View File

@ -1,8 +0,0 @@
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
DAILY_DOMAIN=
# Obtained from https://dashboard.daily.co/developers
DAILY_API_KEY=
# Daily REST API endpoint
DAILY_REST_DOMAIN=https://api.daily.co/v1

View File

@ -1,13 +0,0 @@
const withPlugins = require('next-compose-plugins');
const withTM = require('next-transpile-modules')([
'@custom/shared',
'@custom/basic-call',
]);
const packageJson = require('./package.json');
module.exports = withPlugins([withTM], {
env: {
PROJECT_TITLE: packageJson.description,
},
});

View File

@ -1,25 +0,0 @@
{
"name": "@custom/live-fitness",
"description": "Live Fitness",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@custom/shared": "*",
"@custom/basic-call": "*",
"next": "^11.0.0",
"pluralize": "^8.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"babel-plugin-module-resolver": "^4.1.0",
"next-compose-plugins": "^2.2.1",
"next-transpile-modules": "^8.0.0"
}
}

View File

@ -1,12 +0,0 @@
import React from 'react';
import App from '@custom/basic-call/pages/_app';
import AppWithRecording from '../components/App';
import { RecordingModal } from '../components/RecordingModal';
import Tray from '../components/Tray';
App.modals = [RecordingModal];
App.customAppComponent = <AppWithRecording />;
App.customTrayComponent = <Tray />;
export default App;

View File

@ -1 +0,0 @@
../../basic-call/pages/api

View File

@ -1,16 +0,0 @@
import Index from '@custom/basic-call/pages';
import getDemoProps from '@custom/shared/lib/demoProps';
export async function getStaticProps() {
const defaultProps = getDemoProps();
return {
props: {
...defaultProps,
forceFetchToken: true,
forceOwner: true,
},
};
}
export default Index;

View File

@ -1 +0,0 @@
../basic-call/public

View File

@ -0,0 +1,107 @@
import React, { useCallback, useMemo, useEffect, useState } from 'react';
import { Aside } from '@custom/shared/components/Aside';
import { useCallState } from '../../contexts/CallProvider';
import { useUIState } from '../../contexts/UIStateProvider';
import Capsule from '../Capsule';
export const NETWORK_ASIDE = 'network';
const NETWORK_QUALITY_LABELS = {
low: 'Low',
'very-low': 'Very Low',
good: 'Good',
};
export const NetworkAside = () => {
const { callObject } = useCallState();
const { showAside, setShowAside } = useUIState();
const [networkStats, setNetworkStats] = useState();
const updateStats = useCallback(async () => {
setNetworkStats(await callObject.getNetworkStats());
}, [callObject]);
useEffect(() => {
if (!callObject) {
return;
}
updateStats();
const i = setInterval(updateStats, 2000);
return () => clearInterval(i);
}, [callObject, updateStats]);
const downloadKbs = useMemo(
() =>
Math.round(
(networkStats?.stats?.latest?.videoRecvBitsPerSecond ?? 0) / 1000
),
[networkStats]
);
const uploadKbs = useMemo(
() =>
Math.round(
(networkStats?.stats?.latest?.videoSendBitsPerSecond ?? 0) / 1000
),
[networkStats]
);
if (!showAside || showAside !== NETWORK_ASIDE) {
return null;
}
return (
<Aside onClose={() => setShowAside(false)}>
<div className="network-aside">
{networkStats ? (
<>
<div className="panel">
<h4>Packet Loss:</h4>
Your network quality is:
<Capsule variant="success">
{NETWORK_QUALITY_LABELS[networkStats.threshold]}
</Capsule>
</div>
<div className="panel">
<h4>Download rate:</h4>
{downloadKbs} kbps
</div>
<div className="panel">
<h4>Upload rate:</h4>
{uploadKbs} kbps
</div>
<div className="note">
Download and upload rates reflect bandwidth used by this call.
Updated every 2 seconds.
</div>
</>
) : (
<>Fetching network stats...</>
)}
<style jsx>{`
.panel {
background-color: var(--gray-wash);
border-radius: var(--radius-sm);
padding: var(--spacing-sm);
margin: var(--spacing-xxs);
}
.panel h4 {
margin: 0 0 var(--spacing-xxs) 0;
}
.note {
margin: var(--spacing-xxs);
color: var(--text-mid);
font-size: 0.875rem;
}
`}</style>
</div>
</Aside>
);
};
export default NetworkAside;

View File

@ -1,2 +1,3 @@
export { Aside } from './Aside';
export { PeopleAside } from './PeopleAside';
export { NetworkAside } from './NetworkAside';

View File

@ -0,0 +1,42 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
export const Capsule = ({ children, variant }) => (
<span className={classNames('capsule', variant)}>
{children}
<style jsx>{`
display: inline-flex;
padding: 4px 6px;
margin: 0 6px;
align-items: center;
line-height: 1;
justify-content: center;
border-radius: 5px;
font-size: 0.75rem;
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: 1px;
.capsule.success {
background-color: var(--green-default);
color: #ffffff;
}
.capsule.warning {
background-color: var(--secondary-default);
color: #ffffff;
}
.capsule.error {
background-color: var(--red-default);
color: #ffffff;
}
`}</style>
</span>
);
Capsule.propTypes = {
children: PropTypes.node,
variant: PropTypes.oneOf(['success', 'warning', 'error', 'info']),
};
export default Capsule;

View File

@ -0,0 +1 @@
export { Capsule as default } from './Capsule';

View File

@ -15,7 +15,7 @@ export const MessageCard = ({
hideBack = false,
onBack,
}) => (
<Card className={error && 'error'}>
<Card className={error ? 'error' : ''}>
{header && <CardHeader>{header}</CardHeader>}
{children && <CardBody>{children}</CardBody>}
{!hideBack && (
@ -23,7 +23,7 @@ export const MessageCard = ({
{onBack ? (
<Button onClick={() => onBack()}>Go back</Button>
) : (
<Button href="/">Go back</Button>
<Button onClick={() => window.location.reload()}>Go back</Button>
)}
</CardFooter>
)}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { NETWORK_ASIDE } from '@custom/shared/components/Aside/NetworkAside';
import { PEOPLE_ASIDE } from '@custom/shared/components/Aside/PeopleAside';
import { DEVICE_MODAL } from '@custom/shared/components/DeviceSelectModal';
import { useCallState } from '@custom/shared/contexts/CallProvider';
@ -9,6 +10,7 @@ import { ReactComponent as IconCameraOn } from '@custom/shared/icons/camera-on-m
import { ReactComponent as IconLeave } from '@custom/shared/icons/leave-md.svg';
import { ReactComponent as IconMicOff } from '@custom/shared/icons/mic-off-md.svg';
import { ReactComponent as IconMicOn } from '@custom/shared/icons/mic-on-md.svg';
import { ReactComponent as IconNetwork } from '@custom/shared/icons/network-md.svg';
import { ReactComponent as IconPeople } from '@custom/shared/icons/people-md.svg';
import { ReactComponent as IconSettings } from '@custom/shared/icons/settings-md.svg';
import { Tray, TrayButton } from './Tray';
@ -47,7 +49,9 @@ export const BasicTray = () => {
<TrayButton label="Settings" onClick={() => openModal(DEVICE_MODAL)}>
<IconSettings />
</TrayButton>
<TrayButton label="Network" onClick={() => toggleAside(NETWORK_ASIDE)}>
<IconNetwork />
</TrayButton>
<TrayButton label="People" onClick={() => toggleAside(PEOPLE_ASIDE)}>
<IconPeople />
</TrayButton>

View File

@ -81,7 +81,8 @@ export const useCallUI = ({
callEnded()
) : (
<MessageCard>
You have left the call. We hope you had fun!
You have left the call (either manually or because the room
expired). We hope you had fun!
</MessageCard>
);
default:
@ -89,11 +90,7 @@ export const useCallUI = ({
}
return (
<MessageCard
error
header="An unknown error occured"
onBack={() => window.location.reload()}
>
<MessageCard error header="An unknown error occured">
A fatal error occured in the call loop. Please check you have entered a
valid <code>DAILY_DOMAIN</code> and <code>DAILY_API_KEY</code>{' '}
environmental variables.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" stroke="currentColor"><polyline points="5 14 8 17 11 14" stroke="currentColor"></polyline><line x1="8" y1="17" x2="8" y2="1.751" stroke="currentColor"></line><polyline points="13 10 16 7 19 10" stroke="currentColor"></polyline><line x1="16" y1="7" x2="16" y2="22.246" stroke="currentColor"></line><circle cx="12" cy="12" r="11"></circle></g></svg>

After

Width:  |  Height:  |  Size: 509 B