Skip to content

Commit

Permalink
Add connection indicators
Browse files Browse the repository at this point in the history
  • Loading branch information
third774 committed Aug 16, 2024
1 parent b202213 commit 1389636
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 10 deletions.
25 changes: 25 additions & 0 deletions app/components/ConnectionIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { cn } from '~/utils/style'
import { Icon } from './Icon/Icon'

export type ConnectionQuality = 'healthy' | 'tolerable' | 'unhealthy' | 'bad'

export function getConnectionQuality(packetLoss: number): ConnectionQuality {
if (packetLoss > 0.05) return 'bad'
if (packetLoss > 0.03) return 'unhealthy'
if (packetLoss > 0.01) return 'tolerable'
return 'healthy'
}

export function ConnectionIndicator(props: { quality: ConnectionQuality }) {
return (
<Icon
className={cn(
props.quality === 'healthy' && 'text-green-400',
props.quality === 'tolerable' && 'text-green-400',
props.quality === 'unhealthy' && 'text-yellow-400',
props.quality === 'bad' && 'text-red-400'
)}
type={props.quality === 'bad' ? 'SignalSlashIcon' : 'SignalIcon'}
/>
)
}
2 changes: 2 additions & 0 deletions app/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PhoneXMarkIcon,
PlusIcon,
ServerStackIcon,
SignalIcon,
SignalSlashIcon,
UserGroupIcon,
VideoCameraIcon,
Expand Down Expand Up @@ -52,6 +53,7 @@ const iconMap = {
EllipsisVerticalIcon,
ClipboardDocumentCheckIcon,
ClipboardDocumentIcon,
SignalIcon,
SignalSlashIcon,
ExclamationCircleIcon,
ServerStackIcon,
Expand Down
44 changes: 34 additions & 10 deletions app/components/Participant.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import { forwardRef, useEffect } from 'react'
import { forwardRef, useEffect, useMemo } from 'react'
import { Flipped } from 'react-flip-toolkit'
import { of } from 'rxjs'
import { useSubscribedState } from '~/hooks/rxjsHooks'
import { useDeadPulledTrackMonitor } from '~/hooks/useDeadPulledTrackMonitor'
import { useRoomContext } from '~/hooks/useRoomContext'
import { useUserMetadata } from '~/hooks/useUserMetadata'
import type { User } from '~/types/Messages'
import isNonNullable from '~/utils/isNonNullable'
import populateTraceLink from '~/utils/populateTraceLink'
import { getPacketLoss$ } from '~/utils/rxjs/getPacketLoss$'
import { cn } from '~/utils/style'
import { AudioGlow } from './AudioGlow'
import { AudioIndicator } from './AudioIndicator'
import { Button } from './Button'
import {
ConnectionIndicator,
getConnectionQuality,
} from './ConnectionIndicator'
import { HoverFade } from './HoverFade'
import { Icon } from './Icon/Icon'
import { MuteUserButton } from './MuteUserButton'
Expand Down Expand Up @@ -44,7 +52,7 @@ export const Participant = forwardRef<
ref
) => {
const { data } = useUserMetadata(user.name)
const { traceLink } = useRoomContext()
const { traceLink, peer } = useRoomContext()

useDeadPulledTrackMonitor(
user.tracks.video,
Expand All @@ -70,6 +78,17 @@ export const Participant = forwardRef<
}
}, [flipId, isScreenShare, setPinnedId])

const packetLoss$ = useMemo(
() =>
getPacketLoss$(
peer.peerConnection$,
of([audioTrack, videoTrack].filter(isNonNullable))
),
[audioTrack, peer.peerConnection$, videoTrack]
)

const packetLoss = useSubscribedState(packetLoss$, 0)

return (
<div
className="grow shrink text-base basis-[calc(var(--flex-container-width)_-_var(--gap)_*_3)]"
Expand Down Expand Up @@ -170,14 +189,19 @@ export const Participant = forwardRef<
</div>
)}
{data?.displayName && user.transceiverSessionId && (
<OptionalLink
className="absolute m-2 leading-none text-shadow left-1 bottom-1"
href={populateTraceLink(user.transceiverSessionId, traceLink)}
target="_blank"
rel="noopener noreferrer"
>
{data.displayName}
</OptionalLink>
<div className="flex items-center gap-2 absolute m-2 text-shadow left-1 bottom-1">
<ConnectionIndicator
quality={getConnectionQuality(packetLoss)}
/>
<OptionalLink
className="leading-none"
href={populateTraceLink(user.transceiverSessionId, traceLink)}
target="_blank"
rel="noopener noreferrer"
>
{data.displayName}
</OptionalLink>
</div>
)}
<div className="absolute top-0 right-0 flex gap-4 p-4">
{user.raisedHand && (
Expand Down
95 changes: 95 additions & 0 deletions app/utils/rxjs/getPacketLoss$.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Observable } from 'rxjs'
import { combineLatest, interval, map, pairwise, switchMap } from 'rxjs'

export interface PacketLossStats {
inboundPacketLossPercentage: number
outboundPacketLossPercentage: number
}

function statsReports$(
peerConnection$: Observable<RTCPeerConnection>,
statReportInterval = 3000
) {
return combineLatest([peerConnection$, interval(statReportInterval)]).pipe(
switchMap(([peerConnection]) => peerConnection.getStats()),
pairwise()
)
}

export function getPacketLoss$(
peerConnection$: Observable<RTCPeerConnection>,
tracks$: Observable<MediaStreamTrack[]>
) {
return combineLatest([
tracks$,
peerConnection$,
statsReports$(peerConnection$),
]).pipe(
map(([tracks, peerConnection, [previousStatsReport, newStatsReport]]) => {
const trackToMidMap = peerConnection
.getTransceivers()
.reduce((map, t) => {
const track = t.sender.track ?? t.receiver.track
if (track !== null && t.mid !== null) {
map.set(track, t.mid)
}
return map
}, new Map<MediaStreamTrack, string>())
const relevantMids = new Set<string>()
for (const track of tracks) {
const mid = trackToMidMap.get(track)
if (mid) {
relevantMids.add(mid)
}
}
let inboundPacketsReceived = 0
let inboundPacketsLost = 0
let outboundPacketsSent = 0
let outboundPacketsLost = 0

newStatsReport.forEach((report) => {
if (!relevantMids.has(report.mid)) return
const previous = previousStatsReport.get(report.id)
if (!previous) return

if (report.type === 'inbound-rtp') {
inboundPacketsLost += report.packetsLost - previous.packetsLost
inboundPacketsReceived +=
report.packetsReceived - previous.packetsReceived
} else if (report.type === 'outbound-rtp') {
const packetsSent = report.packetsSent - previous.packetsSent
// Find the corresponding remote-inbound-rtp report
const remoteInboundReport = Array.from(newStatsReport.values()).find(
(r) => r.type === 'remote-inbound-rtp' && r.ssrc === report.ssrc
)
const previousRemoteInboundReport = Array.from(
previousStatsReport.values()
).find(
(r) => r.type === 'remote-inbound-rtp' && r.ssrc === previous.ssrc
)
if (
remoteInboundReport &&
previousRemoteInboundReport &&
packetsSent > 0
) {
outboundPacketsSent += report.packetsSent - previous.packetsSent
outboundPacketsLost +=
remoteInboundReport.packetsLost -
previousRemoteInboundReport.packetsLost
}
}
})

let packetsLost = inboundPacketsLost + outboundPacketsLost
let packetsSent =
inboundPacketsReceived + outboundPacketsSent + packetsLost
let packetLossPercentage = 0

if (packetsSent > 0) {
packetLossPercentage = Math.max(0, packetsLost / packetsSent)
}

return packetLossPercentage
})
)
}

0 comments on commit 1389636

Please sign in to comment.