feat: [closes #15] Show connection type (#117)

* feat: [#15] implement getPeerConnectionTypes
* feat: [#15] display connection type icon
* refactor: extract PeerListItem to its own file
* feat: [#15] show connection details via tooltip
* fix: style stable peer name
This commit is contained in:
Jeremy Kahn 2023-03-25 14:40:07 -05:00 committed by GitHub
parent 87ffd1df56
commit 4cf75b15b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 13 deletions

View File

@ -20,7 +20,10 @@ export const PeerNameDisplay = ({
return ( return (
<Typography component="span" {...rest}> <Typography component="span" {...rest}>
{friendlyName} {friendlyName}
<Typography variant="caption"> ({getPeerName(userId)})</Typography> <Typography variant="caption" {...rest}>
{' '}
({getPeerName(userId)})
</Typography>
</Typography> </Typography>
) )
} else { } else {

View File

@ -54,6 +54,7 @@ export function useRoom(
const { const {
peerList, peerList,
setPeerList, setPeerList,
setPeerConnectionTypes,
tabHasFocus, tabHasFocus,
showAlert, showAlert,
setRoomId, setRoomId,
@ -368,6 +369,12 @@ export function useRoom(
sendPeerMetadata({ customUsername, userId }) sendPeerMetadata({ customUsername, userId })
}, [customUsername, userId, sendPeerMetadata]) }, [customUsername, userId, sendPeerMetadata])
useEffect(() => {
;(async () => {
setPeerConnectionTypes(await peerRoom.getPeerConnectionTypes())
})()
}, [peerList, peerRoom, setPeerConnectionTypes])
return { return {
isPrivate, isPrivate,
handleInlineMediaUpload, handleInlineMediaUpload,

View File

@ -10,12 +10,11 @@ import VolumeUp from '@mui/icons-material/VolumeUp'
import ListItem from '@mui/material/ListItem' import ListItem from '@mui/material/ListItem'
import { PeerListHeader } from 'components/Shell/PeerListHeader' import { PeerListHeader } from 'components/Shell/PeerListHeader'
import { AudioVolume } from 'components/AudioVolume'
import { PeerNameDisplay } from 'components/PeerNameDisplay'
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 { PeerDownloadFileButton } from './PeerDownloadFileButton' import { PeerListItem } from './PeerListItem'
export const peerListWidth = 300 export const peerListWidth = 300
@ -24,6 +23,7 @@ export interface PeerListProps extends PropsWithChildren {
isPeerListOpen: boolean isPeerListOpen: boolean
onPeerListClose: () => void onPeerListClose: () => void
peerList: Peer[] peerList: Peer[]
peerConnectionTypes: Record<string, PeerConnectionType>
audioState: AudioState audioState: AudioState
peerAudios: Record<string, HTMLAudioElement> peerAudios: Record<string, HTMLAudioElement>
} }
@ -33,6 +33,7 @@ export const PeerList = ({
isPeerListOpen, isPeerListOpen,
onPeerListClose, onPeerListClose,
peerList, peerList,
peerConnectionTypes,
audioState, audioState,
peerAudios, peerAudios,
}: PeerListProps) => { }: PeerListProps) => {
@ -72,15 +73,12 @@ export const PeerList = ({
</ListItemText> </ListItemText>
</ListItem> </ListItem>
{peerList.map((peer: Peer) => ( {peerList.map((peer: Peer) => (
<ListItem key={peer.peerId} divider={true}> <PeerListItem
<PeerDownloadFileButton peer={peer} /> key={peer.peerId}
<ListItemText> peer={peer}
<PeerNameDisplay>{peer.userId}</PeerNameDisplay> peerConnectionTypes={peerConnectionTypes}
{peer.peerId in peerAudios && ( peerAudios={peerAudios}
<AudioVolume audioEl={peerAudios[peer.peerId]} /> />
)}
</ListItemText>
</ListItem>
))} ))}
</List> </List>
</MuiDrawer> </MuiDrawer>

View File

@ -0,0 +1,76 @@
import { Box } from '@mui/system'
import ListItemText from '@mui/material/ListItemText'
import SyncAltIcon from '@mui/icons-material/SyncAlt'
import NetworkPingIcon from '@mui/icons-material/NetworkPing'
import ListItem from '@mui/material/ListItem'
import { Tooltip } from '@mui/material'
import { AudioVolume } from 'components/AudioVolume'
import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { Peer } from 'models/chat'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
import { PeerDownloadFileButton } from './PeerDownloadFileButton'
interface PeerListItemProps {
peer: Peer
peerConnectionTypes: Record<string, PeerConnectionType>
peerAudios: Record<string, HTMLAudioElement>
}
export const PeerListItem = ({
peer,
peerConnectionTypes,
peerAudios,
}: PeerListItemProps): JSX.Element => {
const hasPeerConnection = peer.peerId in peerConnectionTypes
const isPeerConnectionDirect =
peerConnectionTypes[peer.peerId] === PeerConnectionType.DIRECT
return (
<ListItem key={peer.peerId} divider={true}>
<PeerDownloadFileButton peer={peer} />
<ListItemText>
{hasPeerConnection ? (
<Tooltip
title={
isPeerConnectionDirect ? (
<>
You are connected directly to{' '}
<PeerNameDisplay
sx={{ fontSize: 'inherit', fontWeight: 'inherit' }}
>
{peer.userId}
</PeerNameDisplay>
</>
) : (
<>
You are connected to{' '}
<PeerNameDisplay
sx={{ fontSize: 'inherit', fontWeight: 'inherit' }}
>
{peer.userId}
</PeerNameDisplay>{' '}
via a relay server. Your connection is still private and
encrypted, but performance may be degraded.
</>
)
}
>
<Box component="span" sx={{ pr: 1, cursor: 'pointer' }}>
{isPeerConnectionDirect ? (
<SyncAltIcon color="success" />
) : (
<NetworkPingIcon color="warning" />
)}
</Box>
</Tooltip>
) : null}
<PeerNameDisplay>{peer.userId}</PeerNameDisplay>
{peer.peerId in peerAudios && (
<AudioVolume audioEl={peerAudios[peer.peerId]} />
)}
</ListItemText>
</ListItem>
)
}

View File

@ -19,6 +19,8 @@ import { AlertOptions } from 'models/shell'
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat' import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
import { ErrorBoundary } from 'components/ErrorBoundary' import { ErrorBoundary } from 'components/ErrorBoundary'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
import { Drawer } from './Drawer' import { Drawer } from './Drawer'
import { UpgradeDialog } from './UpgradeDialog' import { UpgradeDialog } from './UpgradeDialog'
import { ShellAppBar } from './ShellAppBar' import { ShellAppBar } from './ShellAppBar'
@ -65,6 +67,9 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const [password, setPassword] = useState<string | undefined>(undefined) const [password, setPassword] = useState<string | undefined>(undefined)
const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen) const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen)
const [peerList, setPeerList] = useState<Peer[]>([]) // except self const [peerList, setPeerList] = useState<Peer[]>([]) // except self
const [peerConnectionTypes, setPeerConnectionTypes] = useState<
Record<string, PeerConnectionType>
>({})
const [tabHasFocus, setTabHasFocus] = useState(true) const [tabHasFocus, setTabHasFocus] = useState(true)
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED) const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED) const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
@ -101,6 +106,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setIsPeerListOpen, setIsPeerListOpen,
peerList, peerList,
setPeerList, setPeerList,
peerConnectionTypes,
setPeerConnectionTypes,
audioState, audioState,
setAudioState, setAudioState,
videoState, videoState,
@ -120,6 +127,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
password, password,
setPassword, setPassword,
peerList, peerList,
peerConnectionTypes,
tabHasFocus, tabHasFocus,
showRoomControls, showRoomControls,
setShowRoomControls, setShowRoomControls,
@ -315,6 +323,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
isPeerListOpen={isPeerListOpen} isPeerListOpen={isPeerListOpen}
onPeerListClose={handlePeerListClick} onPeerListClose={handlePeerListClick}
peerList={peerList} peerList={peerList}
peerConnectionTypes={peerConnectionTypes}
audioState={audioState} audioState={audioState}
peerAudios={peerAudios} peerAudios={peerAudios}
/> />

View File

@ -2,6 +2,7 @@ import { createContext, Dispatch, SetStateAction } from 'react'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat' import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
interface ShellContextProps { interface ShellContextProps {
tabHasFocus: boolean tabHasFocus: boolean
@ -17,6 +18,10 @@ interface ShellContextProps {
setIsPeerListOpen: Dispatch<SetStateAction<boolean>> setIsPeerListOpen: Dispatch<SetStateAction<boolean>>
peerList: Peer[] peerList: Peer[]
setPeerList: Dispatch<SetStateAction<Peer[]>> setPeerList: Dispatch<SetStateAction<Peer[]>>
peerConnectionTypes: Record<string, PeerConnectionType>
setPeerConnectionTypes: Dispatch<
SetStateAction<Record<string, PeerConnectionType>>
>
audioState: AudioState audioState: AudioState
setAudioState: Dispatch<SetStateAction<AudioState>> setAudioState: Dispatch<SetStateAction<AudioState>>
videoState: VideoState videoState: VideoState
@ -43,6 +48,8 @@ export const ShellContext = createContext<ShellContextProps>({
setIsPeerListOpen: () => {}, setIsPeerListOpen: () => {},
peerList: [], peerList: [],
setPeerList: () => {}, setPeerList: () => {},
peerConnectionTypes: {},
setPeerConnectionTypes: () => {},
audioState: AudioState.STOPPED, audioState: AudioState.STOPPED,
setAudioState: () => {}, setAudioState: () => {},
videoState: VideoState.STOPPED, videoState: VideoState.STOPPED,

View File

@ -17,6 +17,11 @@ export enum PeerStreamType {
SCREEN = 'SCREEN', SCREEN = 'SCREEN',
} }
export enum PeerConnectionType {
DIRECT = 'DIRECT',
RELAY = 'RELAY',
}
const streamQueueAddDelay = 1000 const streamQueueAddDelay = 1000
export class PeerRoom { export class PeerRoom {
@ -112,9 +117,44 @@ export class PeerRoom {
getPeers = () => { getPeers = () => {
const peers = this.room.getPeers() const peers = this.room.getPeers()
return Object.keys(peers) return Object.keys(peers)
} }
getPeerConnectionTypes = async () => {
const peers = this.room.getPeers()
const peerConnections: Record<string, PeerConnectionType> = {}
await Promise.all(
Object.entries(peers).map(async ([peerId, rtcPeerConnection]) => {
const stats = await rtcPeerConnection.getStats()
let selectedLocalCandidate
// https://stackoverflow.com/a/61571171/470685
for (const { type, state, localCandidateId } of stats.values())
if (
type === 'candidate-pair' &&
state === 'succeeded' &&
localCandidateId
) {
selectedLocalCandidate = localCandidateId
break
}
const isRelay =
!!selectedLocalCandidate &&
stats.get(selectedLocalCandidate)?.candidateType === 'relay'
peerConnections[peerId] = isRelay
? PeerConnectionType.RELAY
: PeerConnectionType.DIRECT
})
)
return peerConnections
}
makeAction = <T>(namespace: string) => { makeAction = <T>(namespace: string) => {
return this.room.makeAction<T>(namespace) return this.room.makeAction<T>(namespace)
} }