davidliu
Committed by GitHub

switch to using AudioSwitch library, and have SDK handle audio routing by default (#104)

... ... @@ -113,6 +113,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0'
api 'com.github.webrtc-sdk:android:97.4692.04'
api "com.squareup.okhttp3:okhttp:4.9.1"
implementation "com.twilio:audioswitch:1.1.5"
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:${versions.androidx_core}"
implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}"
... ...
package io.livekit.android
import android.app.Application
import android.content.Context
import io.livekit.android.dagger.DaggerLiveKitComponent
import io.livekit.android.dagger.create
... ... @@ -47,6 +48,11 @@ class LiveKit {
overrides: LiveKitOverrides = LiveKitOverrides(),
): Room {
val ctx = appContext.applicationContext
if (ctx !is Application) {
LKLog.w { "Application context was not found, this may cause memory leaks." }
}
val component = DaggerLiveKitComponent
.factory()
.create(ctx, overrides)
... ...
package io.livekit.android
import io.livekit.android.audio.AudioHandler
import io.livekit.android.audio.NoAudioHandler
import okhttp3.OkHttpClient
import org.webrtc.VideoDecoderFactory
import org.webrtc.VideoEncoderFactory
... ... @@ -36,4 +38,10 @@ data class LiveKitOverrides(
* Override the [VideoDecoderFactory] used by the library.
*/
val videoDecoderFactory: VideoDecoderFactory? = null,
/**
* Override the default [AudioHandler]. Use [NoAudioHandler] to turn off automatic audio handling.
*/
val audioHandler: AudioHandler? = null
)
\ No newline at end of file
... ...
package io.livekit.android.audio
/**
* Interface for handling android audio routing.
*/
interface AudioHandler {
/**
* Called when a room is started.
*/
fun start()
/**
* Called when a room is disconnected.
*/
fun stop()
}
\ No newline at end of file
... ...
package io.livekit.android.audio
import android.content.Context
import com.twilio.audioswitch.AudioSwitch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AudioSwitchHandler
@Inject
constructor(context: Context) : AudioHandler {
private val audioSwitch = AudioSwitch(context)
override fun start() {
audioSwitch.start { _, _ -> }
audioSwitch.activate()
}
override fun stop() {
audioSwitch.stop()
}
}
\ No newline at end of file
... ...
package io.livekit.android.audio
import javax.inject.Inject
/**
* A dummy implementation that does no audio handling.
*/
class NoAudioHandler
@Inject
constructor() : AudioHandler {
override fun start() {
}
override fun stop() {
}
}
\ No newline at end of file
... ...
package io.livekit.android.dagger
import androidx.annotation.Nullable
import dagger.Module
import dagger.Provides
import io.livekit.android.audio.AudioHandler
import io.livekit.android.audio.AudioSwitchHandler
import javax.inject.Named
import javax.inject.Provider
@Module
object AudioHandlerModule {
@Provides
fun audioHandler(
audioSwitchHandler: Provider<AudioSwitchHandler>,
@Named(InjectionNames.OVERRIDE_AUDIO_HANDLER)
@Nullable
audioHandlerOverride: AudioHandler?
): AudioHandler {
return audioHandlerOverride ?: audioSwitchHandler.get()
}
}
\ No newline at end of file
... ...
... ... @@ -30,4 +30,5 @@ object InjectionNames {
internal const val OVERRIDE_JAVA_AUDIO_DEVICE_MODULE_CUSTOMIZER = "override_java_audio_device_module_customizer"
internal const val OVERRIDE_VIDEO_ENCODER_FACTORY = "override_video_encoder_factory"
internal const val OVERRIDE_VIDEO_DECODER_FACTORY = "override_video_decoder_factory"
internal const val OVERRIDE_AUDIO_HANDLER = "override_audio_handler"
}
\ No newline at end of file
... ...
... ... @@ -17,6 +17,7 @@ import javax.inject.Singleton
WebModule::class,
JsonFormatModule::class,
OverridesModule::class,
AudioHandlerModule::class,
]
)
internal interface LiveKitComponent {
... ...
... ... @@ -34,4 +34,9 @@ class OverridesModule(private val overrides: LiveKitOverrides) {
@Nullable
fun videoDecoderFactory() = overrides.videoDecoderFactory
@Provides
@Named(InjectionNames.OVERRIDE_AUDIO_HANDLER)
@Nullable
fun audioHandler() = overrides.audioHandler
}
... ...
... ... @@ -13,6 +13,7 @@ import dagger.assisted.AssistedInject
import io.livekit.android.ConnectOptions
import io.livekit.android.RoomOptions
import io.livekit.android.Version
import io.livekit.android.audio.AudioHandler
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.events.BroadcastEventBus
import io.livekit.android.events.ParticipantEvent
... ... @@ -43,6 +44,7 @@ constructor(
private val defaultDispatcher: CoroutineDispatcher,
@Named(InjectionNames.DISPATCHER_IO)
private val ioDispatcher: CoroutineDispatcher,
private val audioHandler: AudioHandler,
) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() {
private lateinit var coroutineScope: CoroutineScope
... ... @@ -77,7 +79,15 @@ constructor(
@FlowObservable
@get:FlowObservable
var state: State by flowDelegate(State.DISCONNECTED)
var state: State by flowDelegate(State.DISCONNECTED) { new, old ->
if (new != old) {
when (new) {
State.CONNECTING -> audioHandler.start()
State.DISCONNECTED -> audioHandler.stop()
else -> {}
}
}
}
private set
@FlowObservable
... ...
package io.livekit.android.mock.dagger
import dagger.Binds
import dagger.Module
import io.livekit.android.audio.AudioHandler
import io.livekit.android.audio.NoAudioHandler
@Module
interface TestAudioHandlerModule {
@Binds
fun audioHandler(audioHandler: NoAudioHandler): AudioHandler
}
\ No newline at end of file
... ...
... ... @@ -15,6 +15,7 @@ import javax.inject.Singleton
TestCoroutinesModule::class,
TestRTCModule::class,
TestWebModule::class,
TestAudioHandlerModule::class,
JsonFormatModule::class,
]
)
... ...
... ... @@ -3,6 +3,7 @@ package io.livekit.android.room
import android.content.Context
import android.net.Network
import androidx.test.core.app.ApplicationProvider
import io.livekit.android.audio.NoAudioHandler
import io.livekit.android.coroutines.TestCoroutineRule
import io.livekit.android.events.EventCollector
import io.livekit.android.events.RoomEvent
... ... @@ -40,7 +41,7 @@ class RoomTest {
var eglBase: EglBase = MockEglBase()
val localParticantFactory = object : LocalParticipant.Factory {
val localParticipantFactory = object : LocalParticipant.Factory {
override fun create(info: LivekitModels.ParticipantInfo, dynacast: Boolean): LocalParticipant {
return Mockito.mock(LocalParticipant::class.java)
}
... ... @@ -52,13 +53,14 @@ class RoomTest {
fun setup() {
context = ApplicationProvider.getApplicationContext()
room = Room(
context,
rtcEngine,
eglBase,
localParticantFactory,
DefaultsManager(),
coroutineRule.dispatcher,
coroutineRule.dispatcher,
context = context,
engine = rtcEngine,
eglBase = eglBase,
localParticipantFactory = localParticipantFactory,
defaultsManager = DefaultsManager(),
defaultDispatcher = coroutineRule.dispatcher,
ioDispatcher = coroutineRule.dispatcher,
audioHandler = NoAudioHandler(),
)
}
... ...
... ... @@ -16,7 +16,6 @@ import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.*
import io.livekit.android.sample.audio.AppRTCAudioManager
import io.livekit.android.util.flow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
... ... @@ -72,12 +71,7 @@ class CallViewModel(
private val mutablePermissionAllowed = MutableStateFlow(true)
val permissionAllowed = mutablePermissionAllowed.hide()
private val audioManager = AppRTCAudioManager(application)
init {
audioManager.start(null)
viewModelScope.launch {
launch {
error.collect { Timber.e(it) }
... ... @@ -200,7 +194,6 @@ class CallViewModel(
override fun onCleared() {
super.onCleared()
room.disconnect()
audioManager.stop()
}
fun setMicEnabled(enabled: Boolean) {
... ...