diff --git a/web/src/components/VideoGrid.tsx b/web/src/components/VideoGrid.tsx
index 59501ba..ac038ee 100644
--- a/web/src/components/VideoGrid.tsx
+++ b/web/src/components/VideoGrid.tsx
@@ -62,44 +62,45 @@ function VideoTile({ participant, isLocal, room }: { participant: Participant; i
export function VideoGrid() {
const room = useVoiceStore((s) => s._room);
- const isVideoOn = useVoiceStore((s) => s.isVideoOn);
+ const voiceParticipants = useVoiceStore((s) => s.participants);
- if (!room || !isVideoOn) return null;
+ if (!room) return null;
- const participants: Participant[] = [
- room.localParticipant,
- ...Array.from(room.remoteParticipants.values()),
- ];
+ const hasCamParticipants = voiceParticipants.filter((p) => p.hasVideo);
- const hasCam = participants.filter((p) => {
- const pub = p.getTrackPublication(Track.Source.Camera);
- return pub && !pub.isMuted;
- });
+ if (hasCamParticipants.length === 0) return null;
- if (hasCam.length === 0) return null;
+ const getLiveKitParticipant = (identity: string) => {
+ if (identity === room.localParticipant.identity) return room.localParticipant;
+ return room.remoteParticipants.get(identity);
+ };
return (
- ── video [{hasCam.length}] ──
+ ── video [{hasCamParticipants.length}] ──
- {participants.map((p) => (
-
- ))}
+ {hasCamParticipants.map((vp) => {
+ const p = getLiveKitParticipant(vp.identity);
+ if (!p) return null;
+ return (
+
+ );
+ })}
);
diff --git a/web/src/stores/voice.ts b/web/src/stores/voice.ts
index 3453022..114b90a 100644
--- a/web/src/stores/voice.ts
+++ b/web/src/stores/voice.ts
@@ -18,6 +18,7 @@ export interface VoiceParticipant {
username: string;
isMuted: boolean;
isSpeaking: boolean;
+ hasVideo: boolean;
}
export interface WhisperNotification {
@@ -50,8 +51,8 @@ interface VoiceState {
leaveVoice: () => Promise;
toggleMute: () => void;
toggleDeafen: () => void;
- toggleVideo: () => void;
- toggleScreenShare: () => void;
+ toggleVideo: () => Promise;
+ toggleScreenShare: () => Promise;
toggleNoiseSuppression: () => void;
_addWhisper: (w: WhisperNotification) => void;
whisperTo: (targetUserId: string, targetUsername: string, message?: string) => void;
@@ -60,11 +61,13 @@ interface VoiceState {
function participantToVoice(p: Participant): VoiceParticipant {
const micPub = p.getTrackPublication(Track.Source.Microphone);
+ const camPub = p.getTrackPublication(Track.Source.Camera);
return {
identity: p.identity,
username: p.name || p.identity,
isMuted: micPub?.isMuted ?? true,
isSpeaking: p.isSpeaking,
+ hasVideo: !!(camPub && !camPub.isMuted),
};
}
@@ -154,6 +157,11 @@ export const useVoiceStore = create((set, get) => ({
syncParticipants();
});
+ room.on(RoomEvent.TrackPublished, () => syncParticipants());
+ room.on(RoomEvent.TrackUnpublished, () => syncParticipants());
+ room.on(RoomEvent.TrackSubscribed, () => syncParticipants());
+ room.on(RoomEvent.TrackUnsubscribed, () => syncParticipants());
+
room.on(
RoomEvent.ActiveSpeakersChanged,
(speakers: Participant[]) => {