Speaking indicator stuck off for one participant after an SFU full reconnect (until they toggle their mic) #40
Closed
opened 2026-09-19 00:37:16 -04:00 by jared
·
1 comment
No Branch/Tag Specified
lotus
chore/dev-tool-security
feat/expected-devices
lotus-upstream-v0.28.0
feat/call-summary-details
feat/soundboard-mic-mix
feat/mobile-self-tile
feat/screenshare-audio-state
feat/decorations-origin
docs/lotus-call-error
lotus-stall-retry
docs/lotus-protocol
lotus-soundboard-not-permitted
lotus-mic-sampler-churn
lotus-call-error
fix/audio-output-list
lotus-notify-none
lotus-screenshare-prompt
lotus-call-origin
v0.28.0-lotus.4
v0.28.0-lotus.3
v0.28.0-lotus.2
v0.28.0-lotus.1
v0.25.0-lotus.29
v0.25.0-lotus.28
v0.25.0-lotus.27
v0.25.0-lotus.26
v0.25.0-lotus.25
v0.25.0-lotus.24
v0.25.0-lotus.23
v0.25.0-lotus.22
v0.25.0-lotus.21
v0.25.0-lotus.20
v0.25.0-lotus.19
v0.25.0-lotus.18
v0.25.0-lotus.17
v0.25.0-lotus.16
v0.25.0-lotus.15
v0.25.0-lotus.14
v0.25.0-lotus.13
v0.25.0-lotus.12
v0.25.0-lotus.11
v0.25.0-lotus.10
v0.25.0-lotus.9
v0.25.0-lotus.8
v0.25.0-lotus.7
v0.25.0-lotus.6
v0.25.0-lotus.5
v0.25.0-lotus.4
v0.25.0-lotus.3
v0.25.0-lotus.2
v0.25.0-lotus.1
v0.20.1
Labels
Clear labels
a11y
area: audio-pipeline
area: build-publish
area: deafen
area: decorations
area: denoise
area: livekit
area: mobile-ui
area: soundboard-inject
area: upstream-sync
area: widget-actions
bug
dependencies
docs
enhancement
performance
priority: critical
priority: high
priority: low
priority: medium
research
security
tech-debt
ux
Accessibility
Fork area: audio-pipeline
Fork area: build-publish
Fork area: deafen
Fork area: decorations
Fork area: denoise
Fork area: livekit
Fork area: mobile-ui
Fork area: soundboard-inject
Fork area: upstream-sync
Fork area: widget-actions
Something is not working
Package versions and advisories
Docs wrong or missing
New feature
Audio pipeline cost, re-render storms, leaks
Data loss, security hole, or crash on a main path
Broken feature or serious usability problem
Minor issue or polish
Wrong behaviour in an edge case or notable degradation
Origin checks, capability leaks, media/key handling
Code health, fragile patterns
Usability or visual inconsistency
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: LotusGuild/element-call#40
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while sweeping the fork features end-to-end (cinny #174).
Repro (local calls stack, two headless clients, both with a continuous fake-mic tone):
io.lotus.call_stateshowsspeaking=truefor both.killthelivekit-serverprocess, restart it 2 s later.speakingstaysfalseon both sides — including in their own client — for as long as we watched (12 s+). Which one is stuck varies from run to run (alice in two runs, bob in one). Denoise tier does not matter (seen with Browser-native and with the ML tier).Reproduced 3/3 runs. Script:
repro-sfu-restart-speaking.mjs(attached; needs the cinnyscripts/dev-homeserver.sh callsstack; restart LiveKit from a shell when the script dropslk-restart.flag).Where to look:
speaking$inUserMediaViewModel.tsisobserveParticipantEvents(p, ParticipantEvent.IsSpeakingChanged)on whateverparticipant$currently holds. After a full reconnect livekit-client recreates remote participants with new SIDs and re-issues the local SID; ifparticipant$keeps the pre-reconnect object for one member, or the firstSpeakersChangedafter the join response is missed,isSpeakingnever flips again until something else (a mute round-trip) forces a fresh event. Candidate fix: onRoomEvent.Reconnectedre-derive fromparticipant.isSpeaking/audioLevel(or re-emit the participant), rather than relying on the next change event.Impact: speaking rings / "X is speaking…" status bar wrong for one person after any SFU restart (= every LiveKit deploy). Audio itself is unaffected, so medium priority.
Root-caused — and it is a test artifact, not a product bug.
livekit-client only ever learns about speaking via server deltas (
SpeakersChanged→Room.handleSpeakersChanged, "updates are sent only when there's a change"). There is no speaker snapshot in the join response. So a participant whose audio is already active on the SFU at the moment a connection (re)joins produces no delta for that connection until their level next changes — and my fake mics are a continuous 10 s tone that never pauses, so the transition never comes. Which participant gets stuck is just whoever the new SFU flagged active before the other side's join finished, hence the run-to-run variation. Muting and unmuting is simply the first level change.Real speech has pauses every few seconds, so after a reconnect (or when joining a call where someone is mid-sentence) the indicator catches up on the speaker's next pause. Nothing in the fork's
call_statepipeline is wrong: it faithfully mirrorsparticipant.isSpeaking.Closing. If we ever want belt-and-braces, the place would be upstream livekit-client (seed
isSpeakingfromaudioLevelonReconnected), not the fork.