Skip to content

Commit b6bc06e

Browse files
YashJainSCclaude
andauthored
fix(audio): clear audioSwitch synchronously in AudioSwitchHandler.stop() (#967)
Audio output could get stuck on the earpiece (very low volume) after a disconnect() + connect() on a reused Room on Android 12+ (CommDeviceAudioSwitch). stop() nulled `audioSwitch` inside the posted teardown runnable while tearing down handler/thread synchronously. Because the field was not volatile and the write happened off the lock on the handler thread, a fast subsequent start() could read a stale (already-stopped) switch and skip re-creation via the `if (audioSwitch == null)` guard. With no new switch created, activate() never re-ran, so the routing cleared by the prior deactivate()'s clearCommunicationDevice() was never re-asserted and playout stayed on the earpiece until the next connect. Clear `audioSwitch` synchronously under the lock and mark it @volatile so start() reliably observes the teardown and re-creates the switch. The switch's stop() is still posted to its handler thread, since AbstractAudioSwitch is not threadsafe. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0c12e90 commit b6bc06e

2 files changed

Lines changed: 17 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Fix audio output getting stuck on the earpiece after reconnecting on a reused `Room` (Android 12+). `AudioSwitchHandler.stop()` now clears its `audioSwitch` reference synchronously (and the field is `@Volatile`) so a subsequent `start()` reliably observes the teardown and re-creates the switch, instead of racing the posted teardown runnable and reusing a stale, already-stopped switch.

livekit-android-sdk/src/main/java/io/livekit/android/audio/AudioSwitchHandler.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ constructor(private val context: Context) : AudioHandler {
199199
*/
200200
var forceHandleAudioRouting = false
201201

202+
// Volatile and nulled synchronously in stop() (rather than only inside the posted
203+
// teardown runnable) so that a subsequent start() reliably observes the teardown.
204+
@Volatile
202205
private var audioSwitch: AbstractAudioSwitch? = null
203206

204207
// AudioSwitch is not threadsafe, so all calls should be done through a single thread.
@@ -261,10 +264,17 @@ constructor(private val context: Context) : AudioHandler {
261264

262265
@Synchronized
263266
override fun stop() {
267+
// Null audioSwitch synchronously (under the lock) so a subsequent start() reliably
268+
// observes the teardown and re-creates the switch. Previously it was nulled inside the
269+
// posted runnable on the handler thread; with handler/thread torn down synchronously
270+
// below, a fast start() could read a stale, already-stopped switch and skip re-creation,
271+
// leaving audio routing broken (e.g. stuck on the earpiece) until the next connect.
272+
// The switch's stop() is still posted, since AbstractAudioSwitch is not threadsafe.
273+
val switchToStop = audioSwitch
274+
audioSwitch = null
264275
handler?.removeCallbacksAndMessages(null)
265276
handler?.postAtFrontOfQueue {
266-
audioSwitch?.stop()
267-
audioSwitch = null
277+
switchToStop?.stop()
268278
}
269279
thread?.quitSafely()
270280

0 commit comments

Comments
 (0)