fix: local stream video visibility and audio double-playback
Show local user's own screen share in their stream tile by bypassing the isSubscribed check for local participants and auto-watching/unwatching local streams on publish/unpublish. Eliminate audio double-playback by detaching LiveKit's auto-attached audio elements so GlobalAudioRenderer is the sole playback path, making attenuation and volume controls effective. Fix GainNode leak in useAudioTrackPlayer cleanup paths.
This commit is contained in:
@@ -71,10 +71,13 @@ export function useAudioTrackPlayer(opts) {
|
||||
else {
|
||||
// --- STANDARD MODE (0% - 100%) ---
|
||||
// Clean up boost pipeline if it exists
|
||||
if (boostGainRef.current) {
|
||||
boostGainRef.current.disconnect();
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
if (boostSourceRef.current) {
|
||||
boostSourceRef.current.disconnect();
|
||||
boostSourceRef.current = null;
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
audioEl.muted = false;
|
||||
audioEl.volume = Math.min(volume, 1.0);
|
||||
@@ -84,10 +87,13 @@ export function useAudioTrackPlayer(opts) {
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (boostGainRef.current) {
|
||||
boostGainRef.current.disconnect();
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
if (boostSourceRef.current) {
|
||||
boostSourceRef.current.disconnect();
|
||||
boostSourceRef.current = null;
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [volume, muted, track]);
|
||||
|
||||
@@ -87,10 +87,13 @@ export function useAudioTrackPlayer(
|
||||
} else {
|
||||
// --- STANDARD MODE (0% - 100%) ---
|
||||
// Clean up boost pipeline if it exists
|
||||
if (boostGainRef.current) {
|
||||
boostGainRef.current.disconnect();
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
if (boostSourceRef.current) {
|
||||
boostSourceRef.current.disconnect();
|
||||
boostSourceRef.current = null;
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
|
||||
audioEl.muted = false;
|
||||
@@ -103,10 +106,13 @@ export function useAudioTrackPlayer(
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (boostGainRef.current) {
|
||||
boostGainRef.current.disconnect();
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
if (boostSourceRef.current) {
|
||||
boostSourceRef.current.disconnect();
|
||||
boostSourceRef.current = null;
|
||||
boostGainRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [volume, muted, track]);
|
||||
|
||||
@@ -124,7 +124,9 @@ export function useLiveKit() {
|
||||
if (!track)
|
||||
return;
|
||||
// Strict check: Track must be subscribed AND not muted to be considered "active"
|
||||
if (pub.isMuted || !pub.isSubscribed)
|
||||
if (pub.isMuted)
|
||||
return;
|
||||
if (!isLocal && !pub.isSubscribed)
|
||||
return;
|
||||
const mt = track.mediaStreamTrack;
|
||||
if (!mt || mt.readyState !== 'live')
|
||||
@@ -278,10 +280,30 @@ export function useLiveKit() {
|
||||
}
|
||||
});
|
||||
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
||||
// LiveKit auto-attaches a hidden <audio> element for subscribed audio tracks.
|
||||
// GlobalAudioRenderer is the sole audio playback path with volume/attenuation/boost.
|
||||
// Detach LiveKit's internal element to prevent double-playback.
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
track.detach();
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, (publication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().watchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().unwatchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||
@@ -391,10 +413,27 @@ export function useLiveKit() {
|
||||
updateParticipants(); };
|
||||
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
|
||||
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
track.detach();
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, (publication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().watchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().unwatchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Participant,
|
||||
RemoteParticipant,
|
||||
RemoteTrackPublication,
|
||||
RemoteAudioTrack,
|
||||
ConnectionState,
|
||||
VideoPresets,
|
||||
VideoPreset,
|
||||
@@ -177,7 +178,8 @@ export function useLiveKit() {
|
||||
const track = pub.track;
|
||||
if (!track) return;
|
||||
// Strict check: Track must be subscribed AND not muted to be considered "active"
|
||||
if (pub.isMuted || !pub.isSubscribed) return;
|
||||
if (pub.isMuted) return;
|
||||
if (!isLocal && !pub.isSubscribed) return;
|
||||
|
||||
const mt = track.mediaStreamTrack;
|
||||
if (!mt || mt.readyState !== 'live') return;
|
||||
@@ -343,10 +345,30 @@ export function useLiveKit() {
|
||||
}
|
||||
});
|
||||
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
||||
// LiveKit auto-attaches a hidden <audio> element for subscribed audio tracks.
|
||||
// GlobalAudioRenderer is the sole audio playback path with volume/attenuation/boost.
|
||||
// Detach LiveKit's internal element to prevent double-playback.
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
(track as RemoteAudioTrack).detach();
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, (publication: LocalTrackPublication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().watchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication: LocalTrackPublication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().unwatchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||
@@ -451,10 +473,27 @@ export function useLiveKit() {
|
||||
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
|
||||
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
|
||||
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
(track as RemoteAudioTrack).detach();
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
||||
newRoom.on(RoomEvent.LocalTrackPublished, (publication: LocalTrackPublication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().watchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication: LocalTrackPublication) => {
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
const { userId } = parseIdentity(newRoom.localParticipant.identity);
|
||||
useVoiceStore.getState().unwatchStream(userId);
|
||||
}
|
||||
guardedUpdate();
|
||||
});
|
||||
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||
|
||||
Reference in New Issue
Block a user