User Volume
diff --git a/packages/web/src/hooks/useLiveKit.ts b/packages/web/src/hooks/useLiveKit.ts
index 8a87421c..5bdd778a 100644
--- a/packages/web/src/hooks/useLiveKit.ts
+++ b/packages/web/src/hooks/useLiveKit.ts
@@ -111,6 +111,17 @@ export function setStreamSubscription(room: Room | null, targetIdentity: string,
});
}
+export function setCameraSubscription(room: Room | null, targetIdentity: string, subscribed: boolean) {
+ if (!room) return;
+ const rp = room.remoteParticipants.get(targetIdentity);
+ if (!rp) return;
+ rp.trackPublications.forEach((pub) => {
+ if (pub.source === Track.Source.Camera) {
+ (pub as RemoteTrackPublication).setSubscribed(subscribed);
+ }
+ });
+}
+
function parseIdentity(identity: string): { userId: string; username: string } {
const parts = identity.split(':');
return { userId: parts[0] ?? identity, username: parts[1] ?? identity };
@@ -167,9 +178,12 @@ export function useLiveKit() {
let lkVideoTrack: Track | null = null;
let lkScreenTrack: Track | null = null;
let hasScreenSharePublication = false;
+ let hasCameraPublication = false;
p.trackPublications.forEach((pub) => {
// Detect screen share publication even if unsubscribed
if (pub.source === Track.Source.ScreenShare) hasScreenSharePublication = true;
+ // Detect camera publication even if unsubscribed (for unwatched cameras)
+ if (pub.source === Track.Source.Camera) hasCameraPublication = true;
const track = pub.track;
if (!track) return;
@@ -212,7 +226,7 @@ export function useLiveKit() {
homeUserId,
isMuted: isPartMuted,
isDeafened: isPartDeafened,
- isCameraOn: !!videoTrack,
+ isCameraOn: hasCameraPublication && p.isCameraEnabled, // True even when unsubscribed
isScreenSharing: hasScreenSharePublication, // True even when unsubscribed
isLocal,
audioTrack,
diff --git a/packages/web/src/stores/voiceStore.ts b/packages/web/src/stores/voiceStore.ts
index ac6c2a05..89b59994 100644
--- a/packages/web/src/stores/voiceStore.ts
+++ b/packages/web/src/stores/voiceStore.ts
@@ -39,12 +39,15 @@ interface VoiceState {
streamVolumes: Map; // userId → 0-200 (100 default)
streamMutes: Map; // userId → muted?
watchingStreams: Set; // userIds we're watching
+ unwatchedCameras: Set; // userIds whose cameras we've opted out of
streamAttenuationEnabled: boolean; // global toggle, default true
streamAttenuationStrength: number; // 0-100, default 50
setStreamVolume: (userId: string, volume: number) => void;
setStreamMute: (userId: string, muted: boolean) => void;
watchStream: (userId: string) => void;
unwatchStream: (userId: string) => void;
+ unwatchCamera: (userId: string) => void;
+ rewatchCamera: (userId: string) => void;
clearStreamVolume: (userId: string) => void;
clearStreamMute: (userId: string) => void;
setStreamAttenuationEnabled: (enabled: boolean) => void;
@@ -135,6 +138,7 @@ export const useVoiceStore = create()(
streamVolumes: new Map(),
streamMutes: new Map(),
watchingStreams: new Set(),
+ unwatchedCameras: new Set(),
streamAttenuationEnabled: false,
streamAttenuationStrength: 50,
@@ -166,6 +170,20 @@ export const useVoiceStore = create()(
return { watchingStreams: newSet };
});
},
+ unwatchCamera: (userId) => {
+ set((state) => {
+ const newSet = new Set(state.unwatchedCameras);
+ newSet.add(userId);
+ return { unwatchedCameras: newSet };
+ });
+ },
+ rewatchCamera: (userId) => {
+ set((state) => {
+ const newSet = new Set(state.unwatchedCameras);
+ newSet.delete(userId);
+ return { unwatchedCameras: newSet };
+ });
+ },
clearStreamVolume: (userId) => {
set((state) => {
const newMap = new Map(state.streamVolumes);
@@ -366,6 +384,7 @@ export const useVoiceStore = create()(
streamVolumes: new Map(),
streamMutes: new Map(),
watchingStreams: new Set(),
+ unwatchedCameras: new Set(),
voiceUsers,
};
});
@@ -391,6 +410,7 @@ export const useVoiceStore = create()(
streamVolumes: new Map(),
streamMutes: new Map(),
watchingStreams: new Set(),
+ unwatchedCameras: new Set(),
});
},
@@ -420,6 +440,7 @@ export const useVoiceStore = create()(
streamVolumes: new Map(),
streamMutes: new Map(),
watchingStreams: new Set(),
+ unwatchedCameras: new Set(),
serverMutedUserIds: new Set(),
serverDeafenedUserIds: new Set(),
}),
@@ -494,6 +515,7 @@ export const useVoiceStore = create()(
merged.streamVolumes = currentState.streamVolumes;
merged.streamMutes = currentState.streamMutes;
merged.watchingStreams = currentState.watchingStreams;
+ merged.unwatchedCameras = currentState.unwatchedCameras;
return merged;
},
}