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 {
|
else {
|
||||||
// --- STANDARD MODE (0% - 100%) ---
|
// --- STANDARD MODE (0% - 100%) ---
|
||||||
// Clean up boost pipeline if it exists
|
// Clean up boost pipeline if it exists
|
||||||
|
if (boostGainRef.current) {
|
||||||
|
boostGainRef.current.disconnect();
|
||||||
|
boostGainRef.current = null;
|
||||||
|
}
|
||||||
if (boostSourceRef.current) {
|
if (boostSourceRef.current) {
|
||||||
boostSourceRef.current.disconnect();
|
boostSourceRef.current.disconnect();
|
||||||
boostSourceRef.current = null;
|
boostSourceRef.current = null;
|
||||||
boostGainRef.current = null;
|
|
||||||
}
|
}
|
||||||
audioEl.muted = false;
|
audioEl.muted = false;
|
||||||
audioEl.volume = Math.min(volume, 1.0);
|
audioEl.volume = Math.min(volume, 1.0);
|
||||||
@@ -84,10 +87,13 @@ export function useAudioTrackPlayer(opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
|
if (boostGainRef.current) {
|
||||||
|
boostGainRef.current.disconnect();
|
||||||
|
boostGainRef.current = null;
|
||||||
|
}
|
||||||
if (boostSourceRef.current) {
|
if (boostSourceRef.current) {
|
||||||
boostSourceRef.current.disconnect();
|
boostSourceRef.current.disconnect();
|
||||||
boostSourceRef.current = null;
|
boostSourceRef.current = null;
|
||||||
boostGainRef.current = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [volume, muted, track]);
|
}, [volume, muted, track]);
|
||||||
|
|||||||
@@ -87,10 +87,13 @@ export function useAudioTrackPlayer(
|
|||||||
} else {
|
} else {
|
||||||
// --- STANDARD MODE (0% - 100%) ---
|
// --- STANDARD MODE (0% - 100%) ---
|
||||||
// Clean up boost pipeline if it exists
|
// Clean up boost pipeline if it exists
|
||||||
|
if (boostGainRef.current) {
|
||||||
|
boostGainRef.current.disconnect();
|
||||||
|
boostGainRef.current = null;
|
||||||
|
}
|
||||||
if (boostSourceRef.current) {
|
if (boostSourceRef.current) {
|
||||||
boostSourceRef.current.disconnect();
|
boostSourceRef.current.disconnect();
|
||||||
boostSourceRef.current = null;
|
boostSourceRef.current = null;
|
||||||
boostGainRef.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
audioEl.muted = false;
|
audioEl.muted = false;
|
||||||
@@ -103,10 +106,13 @@ export function useAudioTrackPlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (boostGainRef.current) {
|
||||||
|
boostGainRef.current.disconnect();
|
||||||
|
boostGainRef.current = null;
|
||||||
|
}
|
||||||
if (boostSourceRef.current) {
|
if (boostSourceRef.current) {
|
||||||
boostSourceRef.current.disconnect();
|
boostSourceRef.current.disconnect();
|
||||||
boostSourceRef.current = null;
|
boostSourceRef.current = null;
|
||||||
boostGainRef.current = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [volume, muted, track]);
|
}, [volume, muted, track]);
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ export function useLiveKit() {
|
|||||||
if (!track)
|
if (!track)
|
||||||
return;
|
return;
|
||||||
// Strict check: Track must be subscribed AND not muted to be considered "active"
|
// 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;
|
return;
|
||||||
const mt = track.mediaStreamTrack;
|
const mt = track.mediaStreamTrack;
|
||||||
if (!mt || mt.readyState !== 'live')
|
if (!mt || mt.readyState !== 'live')
|
||||||
@@ -278,10 +280,30 @@ export function useLiveKit() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
|
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.TrackUnsubscribed, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
newRoom.on(RoomEvent.LocalTrackPublished, (publication) => {
|
||||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
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.TrackMuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||||
@@ -391,10 +413,27 @@ export function useLiveKit() {
|
|||||||
updateParticipants(); };
|
updateParticipants(); };
|
||||||
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
|
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ParticipantDisconnected, 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.TrackUnsubscribed, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
newRoom.on(RoomEvent.LocalTrackPublished, (publication) => {
|
||||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
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.TrackMuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Participant,
|
Participant,
|
||||||
RemoteParticipant,
|
RemoteParticipant,
|
||||||
RemoteTrackPublication,
|
RemoteTrackPublication,
|
||||||
|
RemoteAudioTrack,
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
VideoPresets,
|
VideoPresets,
|
||||||
VideoPreset,
|
VideoPreset,
|
||||||
@@ -177,7 +178,8 @@ export function useLiveKit() {
|
|||||||
const track = pub.track;
|
const track = pub.track;
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
// Strict check: Track must be subscribed AND not muted to be considered "active"
|
// 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;
|
const mt = track.mediaStreamTrack;
|
||||||
if (!mt || mt.readyState !== 'live') return;
|
if (!mt || mt.readyState !== 'live') return;
|
||||||
@@ -343,10 +345,30 @@ export function useLiveKit() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
|
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.TrackUnsubscribed, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
newRoom.on(RoomEvent.LocalTrackPublished, (publication: LocalTrackPublication) => {
|
||||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
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.TrackMuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||||
@@ -451,10 +473,27 @@ export function useLiveKit() {
|
|||||||
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
|
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
|
||||||
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
|
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ParticipantDisconnected, 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.TrackUnsubscribed, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
|
newRoom.on(RoomEvent.LocalTrackPublished, (publication: LocalTrackPublication) => {
|
||||||
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
|
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.TrackMuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||||
|
|||||||
Reference in New Issue
Block a user