diff --git a/package-lock.json b/package-lock.json
index e20cef2..006d1ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 565f4e8..b343d10 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/Shell/ConnectionTestResults.tsx b/src/components/Shell/ConnectionTestResults.tsx
new file mode 100644
index 0000000..2df5a5d
--- /dev/null
+++ b/src/components/Shell/ConnectionTestResults.tsx
@@ -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 (
+
+ ({ color: theme.palette.success.main })}
+ >
+
+ {' '}
+ Full network connection
+
+ )
+ } else if (hasHost) {
+ return (
+
+ ({ color: theme.palette.warning.main })}
+ >
+
+ {' '}
+ Partial network connection
+
+ )
+ } else {
+ return (
+
+ ({ color: theme.palette.error.main })}
+ >
+
+ {' '}
+ No network connection
+
+ )
+ }
+}
diff --git a/src/components/Shell/PeerList.tsx b/src/components/Shell/PeerList.tsx
index 4b2b951..16494d9 100644
--- a/src/components/Shell/PeerList.tsx
+++ b/src/components/Shell/PeerList.tsx
@@ -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
audioState: AudioState
peerAudios: Record
+ connectionTestResults: IConnectionTestResults
}
export const PeerList = ({
@@ -36,6 +39,7 @@ export const PeerList = ({
peerConnectionTypes,
audioState,
peerAudios,
+ connectionTestResults,
}: PeerListProps) => {
return (
+
+
+
+
{audioState === AudioState.PLAYING && (
diff --git a/src/components/Shell/Shell.tsx b/src/components/Shell/Shell.tsx
index 459d75d..369c909 100644
--- a/src/components/Shell/Shell.tsx
+++ b/src/components/Shell/Shell.tsx
@@ -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}
/>
{
+ 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 },
+ }
+}
diff --git a/src/contexts/ShellContext.ts b/src/contexts/ShellContext.ts
index 5c2393d..cc789fb 100644
--- a/src/contexts/ShellContext.ts
+++ b/src/contexts/ShellContext.ts
@@ -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>>
customUsername: string
setCustomUsername: Dispatch>
+ connectionTestResults: ConnectionTestResults
}
export const ShellContext = createContext({
@@ -60,4 +62,5 @@ export const ShellContext = createContext({
setPeerAudios: () => {},
customUsername: '',
setCustomUsername: () => {},
+ connectionTestResults: { hasHost: false, hasRelay: false },
})
diff --git a/src/polyfills.ts b/src/polyfills.ts
index 80b374e..fdfa270 100644
--- a/src/polyfills.ts
+++ b/src/polyfills.ts
@@ -1,3 +1,4 @@
+import 'webrtc-adapter'
import { Buffer } from 'buffer'
// @ts-ignore
diff --git a/src/services/ConnectionTest/ConnectionTest.ts b/src/services/ConnectionTest/ConnectionTest.ts
new file mode 100644
index 0000000..107b843
--- /dev/null
+++ b/src/services/ConnectionTest/ConnectionTest.ts
@@ -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
+
+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()