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:
parent
2c29674a48
commit
291ed0c2b9
2853
package-lock.json
generated
2853
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
1
src/__mocks__/trystero/torrent.ts
Normal file
1
src/__mocks__/trystero/torrent.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const getTrackers = () => ({})
|
@ -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.">
|
||||||
|
@ -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>
|
||||||
|
<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={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>
|
||||||
)
|
)
|
||||||
|
@ -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}
|
||||||
|
@ -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 },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 },
|
||||||
})
|
})
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user