feat(connection-test): Display tracker connection status (#128)

* feat(ConnectionTest): track tracker state
* feat(ConnectionTest): show tracker searching state
* chore(deps): use github:jeremyckahn/trystero#feature/get-tracker-connections
* feat(connection-test): hide network indicator in non-room routes
* feat(connection-test): show peer searching status
* feat(connection-test): hide peer searching UI when not in a room
This commit is contained in:
Jeremy Kahn 2023-07-13 09:50:54 -05:00 committed by GitHub
parent 2c29674a48
commit 291ed0c2b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1263 additions and 1717 deletions

2861
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,7 +40,7 @@
"sdp": "^3.2.0", "sdp": "^3.2.0",
"secure-file-transfer": "^0.0.7", "secure-file-transfer": "^0.0.7",
"streamsaver": "^2.0.6", "streamsaver": "^2.0.6",
"trystero": "^0.12.1", "trystero": "github:jeremyckahn/trystero#feature/get-tracker-connections",
"typeface-public-sans": "^1.1.13", "typeface-public-sans": "^1.1.13",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^4.7.4", "typescript": "^4.7.4",

View File

@ -0,0 +1 @@
export const getTrackers = () => ({})

View File

@ -1,5 +1,8 @@
import { Tooltip, Typography } from '@mui/material' import CircularProgress from '@mui/material/CircularProgress'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import Circle from '@mui/icons-material/FiberManualRecord' import Circle from '@mui/icons-material/FiberManualRecord'
import { Box } from '@mui/system'
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest' import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
@ -7,8 +10,21 @@ interface ConnectionTestResultsProps {
connectionTestResults: IConnectionTestResults connectionTestResults: IConnectionTestResults
} }
export const ConnectionTestResults = ({ export const ConnectionTestResults = ({
connectionTestResults: { hasHost, hasRelay }, connectionTestResults: { hasHost, hasRelay, hasTracker },
}: ConnectionTestResultsProps) => { }: ConnectionTestResultsProps) => {
if (!hasTracker) {
return (
<Typography variant="subtitle2">
<Box
sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}
>
<CircularProgress size={16} sx={{ mr: 1.5 }} />
<span>Searching for servers...</span>
</Box>
</Typography>
)
}
if (hasHost && hasRelay) { if (hasHost && hasRelay) {
return ( return (
<Tooltip title="Connections can be established with all peers that also have a full network connection."> <Tooltip title="Connections can be established with all peers that also have a full network connection.">

View File

@ -1,4 +1,5 @@
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
import { Route, Routes } from 'react-router-dom'
import MuiDrawer from '@mui/material/Drawer' import MuiDrawer from '@mui/material/Drawer'
import List from '@mui/material/List' import List from '@mui/material/List'
import ListItemIcon from '@mui/material/ListItemIcon' import ListItemIcon from '@mui/material/ListItemIcon'
@ -8,11 +9,14 @@ import IconButton from '@mui/material/IconButton'
import ChevronRightIcon from '@mui/icons-material/ChevronRight' import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import VolumeUp from '@mui/icons-material/VolumeUp' import VolumeUp from '@mui/icons-material/VolumeUp'
import ListItem from '@mui/material/ListItem' import ListItem from '@mui/material/ListItem'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import { PeerListHeader } from 'components/Shell/PeerListHeader' import { PeerListHeader } from 'components/Shell/PeerListHeader'
import { Username } from 'components/Username/Username' import { Username } from 'components/Username/Username'
import { AudioState, Peer } from 'models/chat' import { AudioState, Peer } from 'models/chat'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom' import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
import { routes } from 'config/routes'
import { PeerListItem } from './PeerListItem' import { PeerListItem } from './PeerListItem'
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest' import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
@ -22,6 +26,7 @@ export const peerListWidth = 300
export interface PeerListProps extends PropsWithChildren { export interface PeerListProps extends PropsWithChildren {
userId: string userId: string
roomId: string | undefined
isPeerListOpen: boolean isPeerListOpen: boolean
onPeerListClose: () => void onPeerListClose: () => void
peerList: Peer[] peerList: Peer[]
@ -33,6 +38,7 @@ export interface PeerListProps extends PropsWithChildren {
export const PeerList = ({ export const PeerList = ({
userId, userId,
roomId,
isPeerListOpen, isPeerListOpen,
onPeerListClose, onPeerListClose,
peerList, peerList,
@ -64,9 +70,24 @@ export const PeerList = ({
<ChevronRightIcon /> <ChevronRightIcon />
</IconButton> </IconButton>
<ListItem> <ListItem>
<ConnectionTestResults <Routes>
connectionTestResults={connectionTestResults} {/*
/> This stub route is needed to silence spurious warnings in the tests.
*/}
<Route path={routes.ROOT} element={<></>}></Route>
{[routes.PUBLIC_ROOM, routes.PRIVATE_ROOM].map(route => (
<Route
key={route}
path={route}
element={
<ConnectionTestResults
connectionTestResults={connectionTestResults}
/>
}
/>
))}
</Routes>
</ListItem> </ListItem>
</PeerListHeader> </PeerListHeader>
<Divider /> <Divider />
@ -90,6 +111,24 @@ export const PeerList = ({
peerAudios={peerAudios} peerAudios={peerAudios}
/> />
))} ))}
{peerList.length === 0 &&
typeof roomId === 'string' &&
connectionTestResults.hasTracker &&
connectionTestResults.hasHost ? (
<>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
m: 2,
}}
>
<CircularProgress size={16} sx={{ mr: 1.5 }} />
<span>Searching for peers...</span>
</Box>
</>
) : null}
</List> </List>
</MuiDrawer> </MuiDrawer>
) )

View File

@ -325,6 +325,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
</RouteContent> </RouteContent>
<PeerList <PeerList
userId={userPeerId} userId={userPeerId}
roomId={roomId}
isPeerListOpen={isPeerListOpen} isPeerListOpen={isPeerListOpen}
onPeerListClose={handlePeerListClick} onPeerListClose={handlePeerListClick}
peerList={peerList} peerList={peerList}

View File

@ -9,16 +9,19 @@ import {
export interface ConnectionTestResults { export interface ConnectionTestResults {
hasHost: boolean hasHost: boolean
hasRelay: boolean hasRelay: boolean
hasTracker: boolean
} }
const pollInterval = 20 * 1000 const rtcPollInterval = 20 * 1000
const trackerPollInterval = 5 * 1000
export const useConnectionTest = () => { export const useConnectionTest = () => {
const [hasHost, setHasHost] = useState(false) const [hasHost, setHasHost] = useState(false)
const [hasRelay, setHasRelay] = useState(false) const [hasRelay, setHasRelay] = useState(false)
const [hasTracker, setHasTracker] = useState(false)
useEffect(() => { useEffect(() => {
const checkConnection = async () => { const checkRtcConnection = async () => {
const connectionTest = new ConnectionTest() const connectionTest = new ConnectionTest()
const handleHasHostChanged = ((event: ConnectionTestEvent) => { const handleHasHostChanged = ((event: ConnectionTestEvent) => {
@ -58,10 +61,10 @@ export const useConnectionTest = () => {
ConnectionTestEvents.HAS_RELAY_CHANGED, ConnectionTestEvents.HAS_RELAY_CHANGED,
handleHasRelayChanged handleHasRelayChanged
) )
}, pollInterval) }, rtcPollInterval)
try { try {
await connectionTest.runRtcPeerConnectionTest() await connectionTest.initRtcPeerConnectionTest()
} catch (e) { } catch (e) {
setHasHost(false) setHasHost(false)
setHasRelay(false) setHasRelay(false)
@ -73,14 +76,21 @@ export const useConnectionTest = () => {
;(async () => { ;(async () => {
while (true) { while (true) {
const connectionTest = await checkConnection() const connectionTest = await checkRtcConnection()
await sleep(pollInterval) await sleep(rtcPollInterval)
connectionTest.destroy() connectionTest.destroyRtcPeerConnectionTest()
}
})()
;(async () => {
while (true) {
const connectionTest = new ConnectionTest()
setHasTracker(connectionTest.testTrackerConnection())
await sleep(trackerPollInterval)
} }
})() })()
}, []) }, [])
return { return {
connectionTestResults: { hasHost, hasRelay }, connectionTestResults: { hasHost, hasRelay, hasTracker },
} }
} }

View File

@ -62,5 +62,5 @@ export const ShellContext = createContext<ShellContextProps>({
setPeerAudios: () => {}, setPeerAudios: () => {},
customUsername: '', customUsername: '',
setCustomUsername: () => {}, setCustomUsername: () => {},
connectionTestResults: { hasHost: false, hasRelay: false }, connectionTestResults: { hasHost: false, hasRelay: false, hasTracker: false },
}) })

View File

@ -1,3 +1,4 @@
import { getTrackers } from 'trystero/torrent'
import { rtcConfig } from 'config/rtcConfig' import { rtcConfig } from 'config/rtcConfig'
import { parseCandidate } from 'sdp' import { parseCandidate } from 'sdp'
@ -12,6 +13,7 @@ export type ConnectionTestEvent = CustomEvent<ConnectionTest>
const checkExperationTime = 10 * 1000 const checkExperationTime = 10 * 1000
export class ConnectionTest extends EventTarget { export class ConnectionTest extends EventTarget {
hasTracker = false
hasHost = false hasHost = false
hasRelay = false hasRelay = false
hasPeerReflexive = false hasPeerReflexive = false
@ -19,7 +21,7 @@ export class ConnectionTest extends EventTarget {
rtcPeerConnection?: RTCPeerConnection rtcPeerConnection?: RTCPeerConnection
async runRtcPeerConnectionTest() { async initRtcPeerConnectionTest() {
if (typeof RTCPeerConnection === 'undefined') return if (typeof RTCPeerConnection === 'undefined') return
const { iceServers } = rtcConfig const { iceServers } = rtcConfig
@ -91,9 +93,25 @@ export class ConnectionTest extends EventTarget {
} catch (e) {} } catch (e) {}
} }
destroy() { destroyRtcPeerConnectionTest() {
this.rtcPeerConnection?.close() this.rtcPeerConnection?.close()
} }
testTrackerConnection() {
const trackers = getTrackers()
const readyStates = Object.values(trackers).map(
({ readyState }) => readyState
)
const areAnyTrackersConnected = readyStates.some(
readyState => readyState === WebSocket.OPEN
)
this.hasTracker = areAnyTrackersConnected
return this.hasTracker
}
} }
export const connectionTest = new ConnectionTest() export const connectionTest = new ConnectionTest()