diff --git a/.changeset/audioswitch-stale-switch-on-restart.md b/.changeset/audioswitch-stale-switch-on-restart.md new file mode 100644 index 000000000..37994cb5a --- /dev/null +++ b/.changeset/audioswitch-stale-switch-on-restart.md @@ -0,0 +1,5 @@ +--- +"client-sdk-android": patch +--- + +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. diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/audio/AudioSwitchHandler.kt b/livekit-android-sdk/src/main/java/io/livekit/android/audio/AudioSwitchHandler.kt index d80f8bf1a..1ab517a62 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/audio/AudioSwitchHandler.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/audio/AudioSwitchHandler.kt @@ -199,6 +199,9 @@ constructor(private val context: Context) : AudioHandler { */ var forceHandleAudioRouting = false + // Volatile and nulled synchronously in stop() (rather than only inside the posted + // teardown runnable) so that a subsequent start() reliably observes the teardown. + @Volatile private var audioSwitch: AbstractAudioSwitch? = null // AudioSwitch is not threadsafe, so all calls should be done through a single thread. @@ -261,10 +264,17 @@ constructor(private val context: Context) : AudioHandler { @Synchronized override fun stop() { + // Null audioSwitch synchronously (under the lock) so a subsequent start() reliably + // observes the teardown and re-creates the switch. Previously it was nulled inside the + // posted runnable on the handler thread; with handler/thread torn down synchronously + // below, a fast start() could read a stale, already-stopped switch and skip re-creation, + // leaving audio routing broken (e.g. stuck on the earpiece) until the next connect. + // The switch's stop() is still posted, since AbstractAudioSwitch is not threadsafe. + val switchToStop = audioSwitch + audioSwitch = null handler?.removeCallbacksAndMessages(null) handler?.postAtFrontOfQueue { - audioSwitch?.stop() - audioSwitch = null + switchToStop?.stop() } thread?.quitSafely()