forked from Shiloh/remnantchat
feat: Connection status (#119)
* feat: implement ConnectionTest * feat: display connection results * feat: keep network status up to date
This commit is contained in:
parent
4cf75b15b0
commit
3977a82224
34
package-lock.json
generated
34
package-lock.json
generated
@ -41,6 +41,7 @@
|
||||
"readable-web-to-node-stream": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sass": "^1.54.3",
|
||||
"sdp": "^3.2.0",
|
||||
"secure-file-transfer": "^0.0.7",
|
||||
"streamsaver": "^2.0.6",
|
||||
"trystero": "^0.12.0",
|
||||
@ -48,7 +49,8 @@
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.7.4",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"webrtc-adapter": "^8.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-syntax-highlighter": "^15.5.5",
|
||||
@ -23323,6 +23325,11 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
||||
},
|
||||
"node_modules/secp256k1": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
|
||||
@ -26169,6 +26176,18 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.2.tgz",
|
||||
"integrity": "sha512-jQWwqiAEAFZamWliJo0Q+dIC6ZMJ8BgCFvW/oXWVFby1Nw14dOUfPwZ3lVe4nafDXdTyCUT7xfLt5xXiioXUCQ==",
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/websocket-driver": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||
@ -43868,6 +43887,11 @@
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
},
|
||||
"sdp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
||||
},
|
||||
"secp256k1": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
|
||||
@ -45955,6 +45979,14 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="
|
||||
},
|
||||
"webrtc-adapter": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.2.tgz",
|
||||
"integrity": "sha512-jQWwqiAEAFZamWliJo0Q+dIC6ZMJ8BgCFvW/oXWVFby1Nw14dOUfPwZ3lVe4nafDXdTyCUT7xfLt5xXiioXUCQ==",
|
||||
"requires": {
|
||||
"sdp": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"websocket-driver": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"readable-web-to-node-stream": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sass": "^1.54.3",
|
||||
"sdp": "^3.2.0",
|
||||
"secure-file-transfer": "^0.0.7",
|
||||
"streamsaver": "^2.0.6",
|
||||
"trystero": "^0.12.0",
|
||||
@ -44,7 +45,8 @@
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.7.4",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"webrtc-adapter": "^8.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
49
src/components/Shell/ConnectionTestResults.tsx
Normal file
49
src/components/Shell/ConnectionTestResults.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Typography } from '@mui/material'
|
||||
import Circle from '@mui/icons-material/FiberManualRecord'
|
||||
|
||||
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
|
||||
|
||||
interface ConnectionTestResultsProps {
|
||||
connectionTestResults: IConnectionTestResults
|
||||
}
|
||||
export const ConnectionTestResults = ({
|
||||
connectionTestResults: { hasHost, hasRelay },
|
||||
}: ConnectionTestResultsProps) => {
|
||||
if (hasHost && hasRelay) {
|
||||
return (
|
||||
<Typography variant="subtitle2">
|
||||
<Typography
|
||||
component="span"
|
||||
sx={theme => ({ color: theme.palette.success.main })}
|
||||
>
|
||||
<Circle sx={{ fontSize: 'small' }} />
|
||||
</Typography>{' '}
|
||||
Full network connection
|
||||
</Typography>
|
||||
)
|
||||
} else if (hasHost) {
|
||||
return (
|
||||
<Typography variant="subtitle2">
|
||||
<Typography
|
||||
component="span"
|
||||
sx={theme => ({ color: theme.palette.warning.main })}
|
||||
>
|
||||
<Circle sx={{ fontSize: 'small' }} />
|
||||
</Typography>{' '}
|
||||
Partial network connection
|
||||
</Typography>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Typography variant="subtitle2">
|
||||
<Typography
|
||||
component="span"
|
||||
sx={theme => ({ color: theme.palette.error.main })}
|
||||
>
|
||||
<Circle sx={{ fontSize: 'small' }} />
|
||||
</Typography>{' '}
|
||||
No network connection
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ import { AudioState, Peer } from 'models/chat'
|
||||
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||
|
||||
import { PeerListItem } from './PeerListItem'
|
||||
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
|
||||
import { ConnectionTestResults } from './ConnectionTestResults'
|
||||
|
||||
export const peerListWidth = 300
|
||||
|
||||
@ -26,6 +28,7 @@ export interface PeerListProps extends PropsWithChildren {
|
||||
peerConnectionTypes: Record<string, PeerConnectionType>
|
||||
audioState: AudioState
|
||||
peerAudios: Record<string, HTMLAudioElement>
|
||||
connectionTestResults: IConnectionTestResults
|
||||
}
|
||||
|
||||
export const PeerList = ({
|
||||
@ -36,6 +39,7 @@ export const PeerList = ({
|
||||
peerConnectionTypes,
|
||||
audioState,
|
||||
peerAudios,
|
||||
connectionTestResults,
|
||||
}: PeerListProps) => {
|
||||
return (
|
||||
<MuiDrawer
|
||||
@ -59,9 +63,15 @@ export const PeerList = ({
|
||||
<IconButton onClick={onPeerListClose} aria-label="Close peer list">
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
<ListItem>
|
||||
<ConnectionTestResults
|
||||
connectionTestResults={connectionTestResults}
|
||||
/>
|
||||
</ListItem>
|
||||
</PeerListHeader>
|
||||
<Divider />
|
||||
<List>
|
||||
<Divider />
|
||||
<ListItem divider={true}>
|
||||
{audioState === AudioState.PLAYING && (
|
||||
<ListItemIcon>
|
||||
|
@ -29,6 +29,7 @@ import { RouteContent } from './RouteContent'
|
||||
import { PeerList } from './PeerList'
|
||||
import { QRCodeDialog } from './QRCodeDialog'
|
||||
import { RoomShareDialog } from './RoomShareDialog'
|
||||
import { useConnectionTest } from './useConnectionTest'
|
||||
|
||||
export interface ShellProps extends PropsWithChildren {
|
||||
userPeerId: string
|
||||
@ -90,6 +91,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
setIsAlertShowing(true)
|
||||
}, [])
|
||||
|
||||
const { connectionTestResults } = useConnectionTest()
|
||||
|
||||
const shellContextValue = useMemo(
|
||||
() => ({
|
||||
tabHasFocus,
|
||||
@ -118,6 +121,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
setPeerAudios,
|
||||
customUsername,
|
||||
setCustomUsername,
|
||||
connectionTestResults,
|
||||
}),
|
||||
[
|
||||
isPeerListOpen,
|
||||
@ -143,6 +147,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
setPeerAudios,
|
||||
customUsername,
|
||||
setCustomUsername,
|
||||
connectionTestResults,
|
||||
]
|
||||
)
|
||||
|
||||
@ -326,6 +331,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
peerConnectionTypes={peerConnectionTypes}
|
||||
audioState={audioState}
|
||||
peerAudios={peerAudios}
|
||||
connectionTestResults={connectionTestResults}
|
||||
/>
|
||||
<QRCodeDialog
|
||||
isOpen={isQRCodeDialogOpen}
|
||||
|
74
src/components/Shell/useConnectionTest.ts
Normal file
74
src/components/Shell/useConnectionTest.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { sleep } from 'utils'
|
||||
import {
|
||||
ConnectionTest,
|
||||
ConnectionTestEvent,
|
||||
ConnectionTestEvents,
|
||||
} from 'services/ConnectionTest/ConnectionTest'
|
||||
|
||||
export interface ConnectionTestResults {
|
||||
hasHost: boolean
|
||||
hasRelay: boolean
|
||||
}
|
||||
|
||||
export const useConnectionTest = () => {
|
||||
const [hasHost, setHasHost] = useState(false)
|
||||
const [hasRelay, setHasRelay] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkConnection = async () => {
|
||||
const connectionTest = new ConnectionTest()
|
||||
|
||||
const handleHasHostChanged = ((event: ConnectionTestEvent) => {
|
||||
if (event.detail.hasHost) {
|
||||
setHasHost(true)
|
||||
|
||||
connectionTest.removeEventListener(
|
||||
ConnectionTestEvents.HAS_HOST_CHANGED,
|
||||
handleHasHostChanged
|
||||
)
|
||||
}
|
||||
}) as EventListener
|
||||
|
||||
connectionTest.addEventListener(
|
||||
ConnectionTestEvents.HAS_HOST_CHANGED,
|
||||
handleHasHostChanged
|
||||
)
|
||||
|
||||
const handleHasRelayChanged = ((event: ConnectionTestEvent) => {
|
||||
if (event.detail.hasRelay) {
|
||||
setHasRelay(true)
|
||||
|
||||
connectionTest.removeEventListener(
|
||||
ConnectionTestEvents.HAS_RELAY_CHANGED,
|
||||
handleHasRelayChanged
|
||||
)
|
||||
}
|
||||
}) as EventListener
|
||||
|
||||
connectionTest.addEventListener(
|
||||
ConnectionTestEvents.HAS_RELAY_CHANGED,
|
||||
handleHasRelayChanged
|
||||
)
|
||||
|
||||
try {
|
||||
await connectionTest.runRtcPeerConnectionTest()
|
||||
} catch (e) {
|
||||
setHasHost(false)
|
||||
setHasRelay(false)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
while (true) {
|
||||
await checkConnection()
|
||||
await sleep(20 * 1000)
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
connectionTestResults: { hasHost, hasRelay },
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import { createContext, Dispatch, SetStateAction } from 'react'
|
||||
import { AlertOptions } from 'models/shell'
|
||||
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
||||
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||
import { ConnectionTestResults } from 'components/Shell/useConnectionTest'
|
||||
|
||||
interface ShellContextProps {
|
||||
tabHasFocus: boolean
|
||||
@ -32,6 +33,7 @@ interface ShellContextProps {
|
||||
setPeerAudios: Dispatch<SetStateAction<Record<string, HTMLAudioElement>>>
|
||||
customUsername: string
|
||||
setCustomUsername: Dispatch<SetStateAction<string>>
|
||||
connectionTestResults: ConnectionTestResults
|
||||
}
|
||||
|
||||
export const ShellContext = createContext<ShellContextProps>({
|
||||
@ -60,4 +62,5 @@ export const ShellContext = createContext<ShellContextProps>({
|
||||
setPeerAudios: () => {},
|
||||
customUsername: '',
|
||||
setCustomUsername: () => {},
|
||||
connectionTestResults: { hasHost: false, hasRelay: false },
|
||||
})
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'webrtc-adapter'
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
// @ts-ignore
|
||||
|
77
src/services/ConnectionTest/ConnectionTest.ts
Normal file
77
src/services/ConnectionTest/ConnectionTest.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { rtcConfig } from 'config/rtcConfig'
|
||||
import { parseCandidate } from 'sdp'
|
||||
|
||||
export enum ConnectionTestEvents {
|
||||
CONNECTION_TEST_RESULTS_UPDATED = 'CONNECTION_TEST_RESULTS_UPDATED',
|
||||
HAS_HOST_CHANGED = 'HAS_HOST_CHANGED',
|
||||
HAS_RELAY_CHANGED = 'HAS_RELAY_CHANGED',
|
||||
}
|
||||
|
||||
export type ConnectionTestEvent = CustomEvent<ConnectionTest>
|
||||
|
||||
export class ConnectionTest extends EventTarget {
|
||||
hasHost = false
|
||||
hasRelay = false
|
||||
hasPeerReflexive = false
|
||||
hasServerReflexive = false
|
||||
|
||||
async runRtcPeerConnectionTest() {
|
||||
if (typeof RTCPeerConnection === 'undefined') return
|
||||
|
||||
const { iceServers } = rtcConfig
|
||||
|
||||
const rtcPeerConnection = new RTCPeerConnection({
|
||||
iceServers,
|
||||
})
|
||||
|
||||
rtcPeerConnection.addEventListener('icecandidate', event => {
|
||||
if (event.candidate?.candidate.length) {
|
||||
const parsedCandidate = parseCandidate(event.candidate.candidate)
|
||||
let eventType: ConnectionTestEvents | undefined
|
||||
|
||||
switch (parsedCandidate.type) {
|
||||
case 'host':
|
||||
this.hasHost = true
|
||||
eventType = ConnectionTestEvents.HAS_HOST_CHANGED
|
||||
break
|
||||
|
||||
case 'relay':
|
||||
this.hasRelay = true
|
||||
eventType = ConnectionTestEvents.HAS_RELAY_CHANGED
|
||||
break
|
||||
|
||||
case 'prflx':
|
||||
this.hasPeerReflexive = true
|
||||
break
|
||||
|
||||
case 'srflx':
|
||||
this.hasServerReflexive = true
|
||||
break
|
||||
}
|
||||
|
||||
if (typeof eventType !== 'undefined') {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(eventType, {
|
||||
detail: this,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new Event(ConnectionTestEvents.CONNECTION_TEST_RESULTS_UPDATED)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Kick off the connection test
|
||||
try {
|
||||
const rtcSessionDescription = await rtcPeerConnection.createOffer({
|
||||
offerToReceiveAudio: true,
|
||||
})
|
||||
|
||||
rtcPeerConnection.setLocalDescription(rtcSessionDescription)
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
export const connectionTest = new ConnectionTest()
|
Loading…
x
Reference in New Issue
Block a user