Summary
After upgrading io.livekit:livekit-android from 2.24.0 → 2.25.x, livestream audio drops to the
earpiece (receiver) at very low volume the second time we connect on a reused Room instance
(i.e. room.disconnect() followed by room.connect() on the same object). This happens only on
Android 12+ (API 31+) and only with the new CommDeviceAudioSwitch path introduced in
#910.
The same single-Room-instance reuse pattern worked correctly on 2.24.0 and earlier — audio
stayed on the loudspeaker across switches. So this is a regression, not a new usage requirement.
We have a working app-side workaround (forcing the communication device via AudioManager), but it
contends with AudioSwitchHandler and feels like papering over an SDK bug. Filing for a proper fix /
guidance on whether reusing a Room across disconnect()/connect() is supported.
Environment
|
|
| SDK affected |
io.livekit:livekit-android 2.25.0 → 2.25.3 (latest tested) |
| Last known good |
2.24.0 (same app code, single reused Room, no issue) |
| OS |
Android 15 (API 35); reproduced on Android 12+ generally |
| Device (one repro) |
Motorola moto g45 5G |
| Audio config |
AudioOptions(audioOutputType = AudioType.CallAudioType()) |
| Usage |
Single long-lived Room, reused across stream switches via disconnect() + connect() |
| No external audio device connected (bare phone, built-in speaker expected) |
|
Symptom (platform audio HAL logs)
Captured from audio_hw_primary (snd_device = the route the HAL enabled):
1st connect (correct — ends on speaker):
audio_hw_primary: enable_snd_device: snd_device(1: handset)
audio_hw_primary: enable_snd_device: snd_device(3: speaker) <- ends on speaker, audio loud
2nd connect on the SAME Room after disconnect()+connect() (broken — stuck on earpiece):
audio_hw_primary: enable_snd_device: snd_device(1: handset) <- only this; never re-enables speaker
Audio now plays through the earpiece at very low volume.
For contrast, our Tencent/TRTC path always ends on snd_device(3: speaker) and is never affected.
Expected vs actual
- Expected: after
disconnect() + connect() on a reused Room, output routing is re-established
the same way as on the first connect (loudspeaker when no headset is present).
- Actual: routing is left on the earpiece; the speaker is never re-enabled, and
room.audioHandler (an AudioSwitchHandler) reports an empty availableAudioDevices at this point.
Root-cause analysis (what changed in the SDK)
The change
PR #910 — "CommDeviceAudioSwitch upgrade"
(shipped in v2.25.0) rewrote
AudioSwitchHandler.kt
to use CommDeviceAudioSwitch (backed by AudioManager.setCommunicationDevice() /
clearCommunicationDevice()) on Android S and above, instead of the legacy AudioSwitch.
Relevant behavior in AudioSwitchHandler:
availableAudioDevices is a pass-through that returns empty when the switch is stopped:
val availableAudioDevices: List<AudioDevice>
get() = audioSwitch?.availableAudioDevices ?: listOf()
start() only constructs the underlying switch once (if (audioSwitch == null)), and stop()
nulls it out:
// stop()
handler?.postAtFrontOfQueue {
audioSwitch?.stop()
audioSwitch = null
}
Why this breaks a reused Room
On Android 12+, our switch flow is:
room.disconnect() → the SDK stops the audio handler → AudioSwitchHandler.stop() →
audioSwitch.stop() → CommDeviceAudioSwitch.onDeactivate() → clearCommunicationDevice(),
which reverts the OS communication device to its default (the earpiece) — and audioSwitch is set
to null, so availableAudioDevices is now empty. (The matching
setCommunicationDevice() in onActivate()
is what selected the speaker on the first connect.)
room.connect() on the same Room → the speaker route is never re-asserted. Either the
handler is not cleanly re-start()ed for the reused room, or the re-start does not re-select an
output device (the device list is empty / not re-enumerated in time). The OS therefore stays on the
earpiece set in step 1.
On the legacy AudioSwitch (≤ 2.24.0), routing did not go through
setCommunicationDevice/clearCommunicationDevice, so the loudspeaker route persisted across the
reused-Room disconnect/connect and audio stayed loud. That is the regression.
Minimal reproduction
Usage pattern (one long-lived Room, reused across streams):
// Created once:
val room = LiveKit.create(
appContext = context,
overrides = LiveKitOverrides(
audioOptions = AudioOptions(audioOutputType = AudioType.CallAudioType())
)
)
// First stream — connects fine, audio on loudspeaker:
room.connect(url1, token1)
// Switch to another stream on the SAME room instance:
room.disconnect()
room.connect(url2, token2) // <-- Android 12+: audio now on earpiece, very low volume
Steps:
- On an Android 12+ device with no headset/BT connected.
connect() to a room → confirm audio is on the loudspeaker.
disconnect() then connect() (same Room) to a second room.
- Observe audio is now on the earpiece.
(room.audioHandler as AudioSwitchHandler).availableAudioDevices
is empty at this point.
Diagnostics
- After step 3,
availableAudioDevices on the room's AudioSwitchHandler returns an empty list.
- HAL shows the route flips to
snd_device(1: handset) and never returns to snd_device(3: speaker)
(see Symptom above).
- Reverting only the SDK version back to 2.24.0 (no app changes) makes the problem disappear.
Questions for the LiveKit team
- Is reusing a
Room across disconnect() → connect() a supported pattern? If a Room is
intended to be connect-once, please document it; we'll switch to a fresh Room per stream. If reuse
is supported, this looks like a regression in the CommDeviceAudioSwitch lifecycle.
- On a reused
Room, should AudioSwitchHandler.start() be re-invoked on the subsequent connect()
so availableAudioDevices is re-populated and the output device re-selected? Today it appears not to
re-route, leaving the OS on the earpiece set by clearCommunicationDevice() during the prior
disconnect().
- Should
clearCommunicationDevice() on stop() be paired with a guaranteed re-selection on the next
start()/connect, rather than leaving the OS on its default (earpiece)?
- Is an empty
availableAudioDevices expected while the room is connected? It's the signal that the
underlying switch was stopped and not restarted for the reused room.
References
Additional context
Add any other context about the problem here.
Summary
After upgrading
io.livekit:livekit-androidfrom 2.24.0 → 2.25.x, livestream audio drops to theearpiece (receiver) at very low volume the second time we connect on a reused
Roominstance(i.e.
room.disconnect()followed byroom.connect()on the same object). This happens only onAndroid 12+ (API 31+) and only with the new
CommDeviceAudioSwitchpath introduced in#910.
The same single-
Room-instance reuse pattern worked correctly on 2.24.0 and earlier — audiostayed on the loudspeaker across switches. So this is a regression, not a new usage requirement.
We have a working app-side workaround (forcing the communication device via
AudioManager), but itcontends with
AudioSwitchHandlerand feels like papering over an SDK bug. Filing for a proper fix /guidance on whether reusing a
Roomacrossdisconnect()/connect()is supported.Environment
io.livekit:livekit-android2.25.0 → 2.25.3 (latest tested)Room, no issue)AudioOptions(audioOutputType = AudioType.CallAudioType())Room, reused across stream switches viadisconnect()+connect()Symptom (platform audio HAL logs)
Captured from
audio_hw_primary(snd_device= the route the HAL enabled):1st connect (correct — ends on speaker):
2nd connect on the SAME Room after disconnect()+connect() (broken — stuck on earpiece):
Audio now plays through the earpiece at very low volume.
For contrast, our Tencent/TRTC path always ends on
snd_device(3: speaker)and is never affected.Expected vs actual
disconnect()+connect()on a reusedRoom, output routing is re-establishedthe same way as on the first connect (loudspeaker when no headset is present).
room.audioHandler(anAudioSwitchHandler) reports an emptyavailableAudioDevicesat this point.Root-cause analysis (what changed in the SDK)
The change
PR #910 — "CommDeviceAudioSwitch upgrade"
(shipped in v2.25.0) rewrote
AudioSwitchHandler.ktto use
CommDeviceAudioSwitch(backed byAudioManager.setCommunicationDevice()/clearCommunicationDevice()) on Android S and above, instead of the legacyAudioSwitch.Relevant behavior in
AudioSwitchHandler:availableAudioDevicesis a pass-through that returns empty when the switch is stopped:start()only constructs the underlying switch once (if (audioSwitch == null)), andstop()nulls it out:
Why this breaks a reused
RoomOn Android 12+, our switch flow is:
room.disconnect()→ the SDK stops the audio handler →AudioSwitchHandler.stop()→audioSwitch.stop()→CommDeviceAudioSwitch.onDeactivate()→clearCommunicationDevice(),which reverts the OS communication device to its default (the earpiece) — and
audioSwitchis setto
null, soavailableAudioDevicesis now empty. (The matchingsetCommunicationDevice()inonActivate()is what selected the speaker on the first connect.)
room.connect()on the sameRoom→ the speaker route is never re-asserted. Either thehandler is not cleanly re-
start()ed for the reused room, or the re-start does not re-select anoutput device (the device list is empty / not re-enumerated in time). The OS therefore stays on the
earpiece set in step 1.
On the legacy
AudioSwitch(≤ 2.24.0), routing did not go throughsetCommunicationDevice/clearCommunicationDevice, so the loudspeaker route persisted across thereused-
Roomdisconnect/connect and audio stayed loud. That is the regression.Minimal reproduction
Usage pattern (one long-lived
Room, reused across streams):Steps:
connect()to a room → confirm audio is on the loudspeaker.disconnect()thenconnect()(sameRoom) to a second room.(room.audioHandler as AudioSwitchHandler).availableAudioDevicesis empty at this point.
Diagnostics
availableAudioDeviceson the room'sAudioSwitchHandlerreturns an empty list.snd_device(1: handset)and never returns tosnd_device(3: speaker)(see Symptom above).
Questions for the LiveKit team
Roomacrossdisconnect()→connect()a supported pattern? If aRoomisintended to be connect-once, please document it; we'll switch to a fresh
Roomper stream. If reuseis supported, this looks like a regression in the
CommDeviceAudioSwitchlifecycle.Room, shouldAudioSwitchHandler.start()be re-invoked on the subsequentconnect()so
availableAudioDevicesis re-populated and the output device re-selected? Today it appears not tore-route, leaving the OS on the earpiece set by
clearCommunicationDevice()during the priordisconnect().clearCommunicationDevice()onstop()be paired with a guaranteed re-selection on the nextstart()/connect, rather than leaving the OS on its default (earpiece)?availableAudioDevicesexpected while the room is connected? It's the signal that theunderlying switch was stopped and not restarted for the reused room.
References
AudioSwitchHandler.kt@ v2.25.3com.github.davidliu:audioswitch@039a35aefab7747c557242fa216c9ea11743b604(pinned in the SDK'sgradle/libs.versions.toml) —CommDeviceAudioSwitch.kt(onActivate()→setCommunicationDevice,onDeactivate()→clearCommunicationDevice)Additional context
Add any other context about the problem here.