fix(lotus): screenshare-audio mute survives a re-share — mute via the renderer, not setVolume

"Mute Screenshare Audio" (io.lotus.set_deafen screenshareAudioMuted) used
RemoteParticipant.setVolume(0, ScreenShareAudio). EC's own createVolumeControls
writes volume 1 through the same setter the moment a new screenshare media item
resolves, so when the sharer stopped and re-shared (or a late joiner shared)
the audio came back at full volume while the host button still said
"Unmute Screenshare Audio". Reproduced on the local calls stack with two
headless clients: after a re-share the screen_share_audio element read vol=1.

Now the flag is a global behavior (muteScreenshareAudio$) that
LivekitRoomAudioRenderer turns into the `muted` prop of every
Track.Source.ScreenShareAudio element — the exact mechanism deafen already uses
(pub.setEnabled(false): the server stops sending). Verified via the
RemoteTrackPublication behind each <audio>: the re-published track (new sid)
mounts with enabled=false while muted and re-enables on unmute; deafen +
undeafen leaves it muted; teardown resets the flag so the next call starts
clean. Unit tests updated; renderer test asserts only ScreenShareAudio
elements get muted by the new prop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-18 23:54:25 -04:00
co-authored by Claude Opus 5
parent bc0e5ed432
commit ea579cb998
6 changed files with 149 additions and 118 deletions
+5 -1
View File
@@ -73,6 +73,7 @@ import { matrixRTCMode as matrixRTCModeSetting } from "../settings/settings";
import { ReactionsReader } from "../reactions/ReactionsReader";
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { muteScreenshareAudio$ } from "../lotus/lotusScreenshareAudio";
import { useMediaDevices } from "../MediaDevicesContext.ts";
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
import {
@@ -261,6 +262,8 @@ export const InCallView: FC<InCallViewProps> = ({
const { showControls, header: headerStyle } = useUrlParams();
const muteAllAudio = useBehavior(muteAllAudio$);
// [lotus] host-driven "Mute Screenshare Audio" (io.lotus.set_deafen).
const muteScreenshareAudio = useBehavior(muteScreenshareAudio$);
const toggleAudio = useBehavior(muteStates.audio.toggle$);
const toggleVideo = useBehavior(muteStates.video.toggle$);
const setAudioEnabled = useBehavior(muteStates.audio.setEnabled$);
@@ -309,7 +312,7 @@ export const InCallView: FC<InCallViewProps> = ({
// [lotus] Handle the host's io.lotus.set_deafen action to silence remote
// audio (and optionally screenshare audio) at the LiveKit source. No-op
// unless the host sends the action.
useEffect(() => startLotusDeafen(vm), [vm]);
useEffect(() => startLotusDeafen(), []);
const fatalCallError = useBehavior(vm.fatalError$);
// Stop the rendering and throw for the error boundary
@@ -657,6 +660,7 @@ export const InCallView: FC<InCallViewProps> = ({
livekitRoom={livekitRoom}
validIdentities={participants}
muted={muteAllAudio}
screenshareAudioMuted={muteScreenshareAudio}
/>
))}
{renderContent()}