chore: show call participant mute and camera-off state (#279)

Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
This commit was merged in pull request #279.
This commit is contained in:
2026-05-21 20:58:53 +00:00
committed by hodlbod
parent ffd06ab561
commit 0d61278c56
7 changed files with 148 additions and 34 deletions
+15 -2
View File
@@ -29,8 +29,6 @@ export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
export const currentVoiceRoom = writable<Room | undefined>(undefined)
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
@@ -43,6 +41,21 @@ export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
export const speakingParticipants = writable<VoiceParticipant[]>([])
export const participantMediaState = writable(
new Map<string, {muted: boolean; cameraOn: boolean}>(),
)
export const mediaStateByIdentity = derived(
[participantMediaState, currentVoiceSession, voiceMicMuted],
([$media, $session, $micMuted]) =>
(identity: string) => {
if ($session?.room.localParticipant.identity === identity) {
return {muted: $micMuted, cameraOn: $session.cameraOn}
}
return $media.get(identity) ?? {muted: true, cameraOn: false}
},
)
export const isParticipantSpeaking = derived(
speakingParticipants,
$participants => (p: VoiceParticipant) =>
+32 -23
View File
@@ -6,14 +6,16 @@ import {
DisconnectReason,
LocalParticipant,
LocalTrackPublication,
Participant,
Room as LiveKitRoom,
RoomEvent,
Track,
TrackPublication,
supportsAudioOutputSelection,
type AudioCaptureOptions,
} from "livekit-client"
import {derived, get} from "svelte/store"
import {map, not, removeUndefined, uniqBy} from "@welshman/lib"
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@welshman/app"
@@ -25,8 +27,7 @@ import {
voiceMicMuted,
participantFromLiveKitIdentity,
participantKey,
participantPubkeyMap,
pubkeyFromLiveKitIdentity,
participantMediaState,
speakingParticipants,
VoiceState,
type VoiceParticipant,
@@ -76,20 +77,23 @@ export const switchVoiceActiveDevice = async (
}
}
const addParticipant = (identity: string) => {
participantPubkeyMap.update(m => {
const deleteParticipant = (identity: string) => {
participantMediaState.update(m => new Map(reject(nthEq(0, identity), [...m])))
}
const syncParticipantMedia = (participant: Participant) => {
const state = {muted: !participant.isMicrophoneEnabled, cameraOn: participant.isCameraEnabled}
participantMediaState.update(m => {
const prev = m.get(participant.identity)
if (prev?.muted === state.muted && prev?.cameraOn === state.cameraOn) return m
const next = new Map(m)
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
next.set(participant.identity, state)
return next
})
}
const deleteParticipant = (identity: string) => {
participantPubkeyMap.update(m => {
const next = new Map(m)
next.delete(identity)
return next
})
const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
syncParticipantMedia(participant)
}
const fetchLivekitToken = async (
@@ -125,15 +129,15 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
derived(
[
participantPubkeyMap,
participantMediaState,
currentVoiceRoom,
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
],
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
([$participantMediaState, $currentVoiceRoom, $publishedParticipantList]) => {
const inCall = $participantMediaState.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
if (inCall) {
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
const participants = [...$participantMediaState.keys()].map(participantFromLiveKitIdentity)
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
} else {
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
@@ -186,7 +190,7 @@ const onRoomDisconnected = (reason?: DisconnectReason) => {
pushToast({theme: "error", message})
}
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
participantMediaState.set(new Map())
}
const onTrackSubscribed = (track: Track) => {
@@ -216,8 +220,8 @@ const playJoinSound = () => {
audio.play().catch(() => {})
}
const onParticipantConnected = (participant: {identity: string}) => {
addParticipant(participant.identity)
const onParticipantConnected = (participant: Participant) => {
syncParticipantMedia(participant)
playJoinSound()
}
@@ -275,6 +279,11 @@ export const joinVoiceRoom = async (
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
liveKitRoom.on(RoomEvent.TrackMuted, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackUnmuted, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackPublished, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackUnpublished, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.LocalTrackPublished, onParticipantMediaChanged)
try {
await Promise.race([
@@ -289,10 +298,10 @@ export const joinVoiceRoom = async (
throw e
}
participantPubkeyMap.set(new Map())
addParticipant(liveKitRoom.localParticipant.identity)
participantMediaState.set(new Map())
syncParticipantMedia(liveKitRoom.localParticipant)
for (const p of liveKitRoom.remoteParticipants.values()) {
addParticipant(p.identity)
syncParticipantMedia(p)
}
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
@@ -346,7 +355,7 @@ export const leaveVoiceRoom = async () => {
resetVideoCallLayout()
session.room.disconnect()
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
participantMediaState.set(new Map())
}
export const rejoinVoiceRoom = async (): Promise<void> => {