Skip to content

Commit 80b00a1

Browse files
authored
Move audio handling to background thread to avoid UI freezes. (#715)
1 parent 6c23991 commit 80b00a1

File tree

3 files changed

+38
-12
lines changed

3 files changed

+38
-12
lines changed

.changeset/fresh-schools-chew.md

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+
Move audio handling to background thread to avoid UI freezes.

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import android.media.AudioAttributes
2121
import android.media.AudioManager
2222
import android.os.Build
2323
import android.os.Handler
24+
import android.os.HandlerThread
2425
import android.os.Looper
2526
import com.twilio.audioswitch.*
27+
import io.livekit.android.util.LKLog
2628
import javax.inject.Inject
2729
import javax.inject.Singleton
2830

@@ -138,13 +140,26 @@ constructor(private val context: Context) : AudioHandler {
138140

139141
private var audioSwitch: AbstractAudioSwitch? = null
140142

141-
// AudioSwitch is not threadsafe, so all calls should be done on the main thread.
142-
private val handler = Handler(Looper.getMainLooper())
143+
// AudioSwitch is not threadsafe, so all calls should be done through a single thread.
144+
private var handler: Handler? = null
145+
private var thread: HandlerThread? = null
143146

147+
@Synchronized
144148
override fun start() {
149+
if (handler != null || thread != null) {
150+
LKLog.i { "AudioSwitchHandler called start multiple times?" }
151+
}
152+
153+
if (thread == null) {
154+
thread = HandlerThread("AudioSwitchHandlerThread").also { it.start() }
155+
}
156+
if (handler == null) {
157+
handler = Handler(thread!!.looper)
158+
}
159+
145160
if (audioSwitch == null) {
146-
handler.removeCallbacksAndMessages(null)
147-
handler.postAtFrontOfQueue {
161+
handler?.removeCallbacksAndMessages(null)
162+
handler?.postAtFrontOfQueue {
148163
val switch =
149164
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
150165
AudioSwitch(
@@ -176,12 +191,17 @@ constructor(private val context: Context) : AudioHandler {
176191
}
177192
}
178193

194+
@Synchronized
179195
override fun stop() {
180-
handler.removeCallbacksAndMessages(null)
181-
handler.postAtFrontOfQueue {
196+
handler?.removeCallbacksAndMessages(null)
197+
handler?.postAtFrontOfQueue {
182198
audioSwitch?.stop()
183199
audioSwitch = null
184200
}
201+
thread?.quitSafely()
202+
203+
handler = null
204+
thread = null
185205
}
186206

187207
/**
@@ -199,11 +219,12 @@ constructor(private val context: Context) : AudioHandler {
199219
/**
200220
* Select a specific audio device.
201221
*/
222+
@Synchronized
202223
fun selectDevice(audioDevice: AudioDevice?) {
203-
if (Looper.myLooper() == Looper.getMainLooper()) {
224+
if (Looper.myLooper() == handler?.looper) {
204225
audioSwitch?.selectDevice(audioDevice)
205226
} else {
206-
handler.post {
227+
handler?.post {
207228
audioSwitch?.selectDevice(audioDevice)
208229
}
209230
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 LiveKit, Inc.
2+
* Copyright 2024-2025 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import androidx.annotation.RequiresApi
2626
import io.livekit.android.dagger.InjectionNames
2727
import io.livekit.android.util.CloseableCoroutineScope
2828
import io.livekit.android.util.LKLog
29-
import kotlinx.coroutines.MainCoroutineDispatcher
29+
import kotlinx.coroutines.CoroutineDispatcher
3030
import kotlinx.coroutines.flow.MutableStateFlow
3131
import kotlinx.coroutines.flow.collectLatest
3232
import kotlinx.coroutines.flow.combine
@@ -83,8 +83,8 @@ constructor() : CommunicationWorkaround {
8383
internal class CommunicationWorkaroundImpl
8484
@Inject
8585
constructor(
86-
@Named(InjectionNames.DISPATCHER_MAIN)
87-
dispatcher: MainCoroutineDispatcher,
86+
@Named(InjectionNames.DISPATCHER_IO)
87+
dispatcher: CoroutineDispatcher,
8888
) : CommunicationWorkaround {
8989

9090
private val coroutineScope = CloseableCoroutineScope(dispatcher)

0 commit comments

Comments
 (0)