Skip to content

Audio routes to earpiece after disconnect() + connect() on a reused Room (Android 12+) — regression since v2.25.0 #966

@YashJainSC

Description

@YashJainSC

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:

  1. 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.)
  2. 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:

  1. On an Android 12+ device with no headset/BT connected.
  2. connect() to a room → confirm audio is on the loudspeaker.
  3. disconnect() then connect() (same Room) to a second room.
  4. 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

  1. 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.
  2. 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().
  3. 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)?
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions