fix: video blink when toggling mic mute in calls (#277)

This commit is contained in:
2026-05-20 16:44:38 +00:00
parent eb8dd330b6
commit c1c00eca7e
4 changed files with 18 additions and 14 deletions
+3 -1
View File
@@ -6,11 +6,13 @@ export type VoiceSession = {
url: string url: string
h: string h: string
room: LiveKitRoom room: LiveKitRoom
muted: boolean
cameraOn: boolean cameraOn: boolean
screenShareOn: boolean screenShareOn: boolean
} }
/** Mic mute state is separate so toggling it does not re-render video tiles. */
export const voiceMicMuted = writable(true)
export type Pubkey = string export type Pubkey = string
export type VoiceParticipant = {pubkey?: Pubkey; identity: string} export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
+8 -6
View File
@@ -13,7 +13,7 @@ import {
type AudioCaptureOptions, type AudioCaptureOptions,
} from "livekit-client" } from "livekit-client"
import {derived, get} from "svelte/store" import {derived, get} from "svelte/store"
import {map, removeUndefined, uniqBy} from "@welshman/lib" import {map, not, removeUndefined, uniqBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util" import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@welshman/app" import {signer} from "@welshman/app"
@@ -22,6 +22,7 @@ import {AbortError, whenAborted, whenTimeout} from "$lib/util"
import { import {
currentVoiceRoom, currentVoiceRoom,
currentVoiceSession, currentVoiceSession,
voiceMicMuted,
participantFromLiveKitIdentity, participantFromLiveKitIdentity,
participantKey, participantKey,
participantPubkeyMap, participantPubkeyMap,
@@ -173,6 +174,7 @@ const setUpMicrophone = async (
const onRoomDisconnected = (reason?: DisconnectReason) => { const onRoomDisconnected = (reason?: DisconnectReason) => {
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) { if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
@@ -295,11 +297,11 @@ export const joinVoiceRoom = async (
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant) const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
voiceMicMuted.set(muted)
currentVoiceSession.set({ currentVoiceSession.set({
url, url,
h, h,
room: liveKitRoom, room: liveKitRoom,
muted,
cameraOn: false, cameraOn: false,
screenShareOn: false, screenShareOn: false,
}) })
@@ -339,6 +341,7 @@ export const leaveVoiceRoom = async () => {
voiceState.set(VoiceState.Disconnected) voiceState.set(VoiceState.Disconnected)
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
session.room.disconnect() session.room.disconnect()
@@ -356,18 +359,17 @@ export const toggleMute = async () => {
const session = get(currentVoiceSession) const session = get(currentVoiceSession)
if (!session) return if (!session) return
const muted = !session.muted voiceMicMuted.update(not)
if (muted) { if (get(voiceMicMuted)) {
// Disable and re-enable microphone to trigger permission prompt // Disable and re-enable microphone to trigger permission prompt
session.room.localParticipant.setMicrophoneEnabled(false) session.room.localParticipant.setMicrophoneEnabled(false)
currentVoiceSession.set({...session, muted})
return return
} }
try { try {
await session.room.localParticipant.setMicrophoneEnabled(true) await session.room.localParticipant.setMicrophoneEnabled(true)
currentVoiceSession.set({...session, muted})
} catch (e) { } catch (e) {
voiceMicMuted.set(true)
pushToast({theme: "error", message: "Could not access microphone"}) pushToast({theme: "error", message: "Could not access microphone"})
} }
} }
+2 -2
View File
@@ -216,8 +216,8 @@
data-tip={pinned ? "Exit spotlight" : "Spotlight"} data-tip={pinned ? "Exit spotlight" : "Spotlight"}
aria-pressed={pinned} aria-pressed={pinned}
class={cx( class={cx(
"absolute right-1 top-1 z-20 btn btn-xs btn-square btn-ghost", "absolute right-1 top-1 z-20 btn btn-xs btn-square",
pinned ? "btn-active bg-primary/25 text-primary" : "bg-base-100/70", pinned ? "btn-primary" : "btn-ghost bg-base-100",
)} )}
onclick={spotlightHandlerFor(tileKey(tile))}> onclick={spotlightHandlerFor(tileKey(tile))}>
<Icon icon={Pin} size={3} /> <Icon icon={Pin} size={3} />
+5 -5
View File
@@ -41,6 +41,7 @@
VoiceState, VoiceState,
currentVoiceSession, currentVoiceSession,
currentVoiceRoom, currentVoiceRoom,
voiceMicMuted,
voiceState, voiceState,
isLocalSpeaking, isLocalSpeaking,
} from "@app/call/stores" } from "@app/call/stores"
@@ -183,18 +184,17 @@
</Button> </Button>
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession} {:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
<Button <Button
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"} data-tip={$voiceMicMuted ? "Unmute" : "Mute"}
class={cx( class={cx(
mediaToggleClass, mediaToggleClass,
"overflow-visible", "overflow-visible",
!$currentVoiceSession.muted && $isLocalSpeaking && "text-primary", !$voiceMicMuted && $isLocalSpeaking && "text-primary",
$currentVoiceSession.muted && $voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
)} )}
onclick={toggleMute}> onclick={toggleMute}>
<span class="relative inline-flex items-center justify-center overflow-visible"> <span class="relative inline-flex items-center justify-center overflow-visible">
<Icon icon={Microphone} size={4} /> <Icon icon={Microphone} size={4} />
{#if $currentVoiceSession.muted} {#if $voiceMicMuted}
<span <span
class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible" class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible"
aria-hidden="true"> aria-hidden="true">