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",
"secure-file-transfer": "^0.0.7",
"streamsaver": "^2.0.6",
"trystero": "^0.12.1",
"trystero": "github:jeremyckahn/trystero#feature/get-tracker-connections",
"typeface-public-sans": "^1.1.13",
"typeface-roboto": "^1.1.13",
"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 { Box } from '@mui/system'
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
@ -7,8 +10,21 @@ interface ConnectionTestResultsProps {
connectionTestResults: IConnectionTestResults
}
export const ConnectionTestResults = ({
connectionTestResults: { hasHost, hasRelay },
connectionTestResults: { hasHost, hasRelay, hasTracker },
}: 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) {
return (
<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 { Route, Routes } from 'react-router-dom'
import MuiDrawer from '@mui/material/Drawer'
import List from '@mui/material/List'
import ListItemIcon from '@mui/material/ListItemIcon'
@ -8,11 +9,14 @@ import IconButton from '@mui/material/IconButton'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import VolumeUp from '@mui/icons-material/VolumeUp'
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 { Username } from 'components/Username/Username'
import { AudioState, Peer } from 'models/chat'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
import { routes } from 'config/routes'
import { PeerListItem } from './PeerListItem'
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
@ -22,6 +26,7 @@ export const peerListWidth = 300
export interface PeerListProps extends PropsWithChildren {
userId: string
roomId: string | undefined
isPeerListOpen: boolean
onPeerListClose: () => void
peerList: Peer[]
@ -33,6 +38,7 @@ export interface PeerListProps extends PropsWithChildren {
export const PeerList = ({
userId,
roomId,
isPeerListOpen,
onPeerListClose,
peerList,
@ -64,9 +70,24 @@ export const PeerList = ({
<ChevronRightIcon />
</IconButton>
<ListItem>
<ConnectionTestResults
connectionTestResults={connectionTestResults}
/>
<Routes>
{/*
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>
</PeerListHeader>
<Divider />
@ -90,6 +111,24 @@ export const PeerList = ({
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>
</MuiDrawer>
)

View File

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

View File

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

View File

@ -62,5 +62,5 @@ export const ShellContext = createContext<ShellContextProps>({
setPeerAudios: () => {},
customUsername: '',
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 { parseCandidate } from 'sdp'
@ -12,6 +13,7 @@ export type ConnectionTestEvent = CustomEvent<ConnectionTest>
const checkExperationTime = 10 * 1000
export class ConnectionTest extends EventTarget {
hasTracker = false
hasHost = false
hasRelay = false
hasPeerReflexive = false
@ -19,7 +21,7 @@ export class ConnectionTest extends EventTarget {
rtcPeerConnection?: RTCPeerConnection
async runRtcPeerConnectionTest() {
async initRtcPeerConnectionTest() {
if (typeof RTCPeerConnection === 'undefined') return
const { iceServers } = rtcConfig
@ -91,9 +93,25 @@ export class ConnectionTest extends EventTarget {
} catch (e) {}
}
destroy() {
destroyRtcPeerConnectionTest() {
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()