fix: eliminate voice ducking caused by rogue LiveKit audio elements

Add MutationObserver to neutralize LiveKit's re-attached <audio> elements,
mark our keep-alive elements with data-opencord, detach tracks on unsubscribe,
remove dangerous blanket .play(), and soften compressor to transparent limiter.
This commit is contained in:
Jannis Braun
2026-02-20 07:03:41 +01:00
parent 6f777f97ce
commit 5c38f076d3
22 changed files with 511 additions and 254 deletions
+59 -14
View File
@@ -299,7 +299,10 @@ export function useLiveKit() {
const connect = useCallback(async (channelId: string) => {
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected) return;
const gen = ++_connectGeneration;
// Ensure AudioContext is created and resumed before tracks arrive
await AudioManager.getInstance().resumeContext();
// 1. Reset state immediately to reflect "Loading/Switching" in UI
setRoom(null);
setParticipants([]);
@@ -354,7 +357,12 @@ export function useLiveKit() {
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.TrackUnsubscribed, (track) => {
if (track.kind === Track.Kind.Audio) {
(track as RemoteAudioTrack).detach();
}
guardedUpdate();
});
newRoom.on(RoomEvent.LocalTrackPublished, (publication: LocalTrackPublication) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
@@ -374,9 +382,11 @@ export function useLiveKit() {
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.ParticipantMetadataChanged, guardedUpdate);
newRoom.on(RoomEvent.TrackPublished, (publication: RemoteTrackPublication, participant: RemoteParticipant) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(participant.identity);
useVoiceStore.getState().watchStream(userId);
if (
publication.source === Track.Source.ScreenShare ||
publication.source === Track.Source.ScreenShareAudio
) {
(publication as RemoteTrackPublication).setSubscribed(false);
}
guardedUpdate();
});
@@ -425,7 +435,19 @@ export function useLiveKit() {
useVoiceStore.getState().setIsLiveKitConnected(true);
updateParticipants();
// Unsubscribe from any remote screen share tracks that auto-subscribed during connect
newRoom.remoteParticipants.forEach((rp) => {
rp.trackPublications.forEach((pub) => {
if (
(pub.source === Track.Source.ScreenShare || pub.source === Track.Source.ScreenShareAudio) &&
pub.isSubscribed
) {
(pub as RemoteTrackPublication).setSubscribed(false);
}
});
});
// Initial mute state check
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
@@ -441,7 +463,10 @@ export function useLiveKit() {
const connectDm = useCallback(async (dmChannelId: string) => {
const gen = ++_connectGeneration;
// Ensure AudioContext is created and resumed before tracks arrive
await AudioManager.getInstance().resumeContext();
// 1. Reset state immediately
setRoom(null);
setParticipants([]);
@@ -479,7 +504,12 @@ export function useLiveKit() {
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.TrackUnsubscribed, (track) => {
if (track.kind === Track.Kind.Audio) {
(track as RemoteAudioTrack).detach();
}
guardedUpdate();
});
newRoom.on(RoomEvent.LocalTrackPublished, (publication: LocalTrackPublication) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
@@ -498,9 +528,11 @@ export function useLiveKit() {
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.TrackPublished, (publication: RemoteTrackPublication, participant: RemoteParticipant) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(participant.identity);
useVoiceStore.getState().watchStream(userId);
if (
publication.source === Track.Source.ScreenShare ||
publication.source === Track.Source.ScreenShareAudio
) {
(publication as RemoteTrackPublication).setSubscribed(false);
}
guardedUpdate();
});
@@ -533,13 +565,26 @@ export function useLiveKit() {
await newRoom.connect(url, token);
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
const fullId = `dm-${dmChannelId}`;
_activeRoom = newRoom;
connectedChannelRef.current = fullId;
_activeRoom = newRoom;
connectedChannelRef.current = fullId;
setConnectedChannelId(fullId);
setRoom(newRoom);
setRoom(newRoom);
setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
updateParticipants();
// Unsubscribe from any remote screen share tracks that auto-subscribed during connect
newRoom.remoteParticipants.forEach((rp) => {
rp.trackPublications.forEach((pub) => {
if (
(pub.source === Track.Source.ScreenShare || pub.source === Track.Source.ScreenShareAudio) &&
pub.isSubscribed
) {
(pub as RemoteTrackPublication).setSubscribed(false);
}
});
});
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });