davidliu
Committed by GitHub

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

@@ -113,6 +113,7 @@ dependencies { @@ -113,6 +113,7 @@ dependencies {
113 implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0' 113 implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0'
114 api 'com.github.webrtc-sdk:android:97.4692.04' 114 api 'com.github.webrtc-sdk:android:97.4692.04'
115 api "com.squareup.okhttp3:okhttp:4.9.1" 115 api "com.squareup.okhttp3:okhttp:4.9.1"
  116 + implementation "com.twilio:audioswitch:1.1.5"
116 implementation "androidx.annotation:annotation:1.3.0" 117 implementation "androidx.annotation:annotation:1.3.0"
117 implementation "androidx.core:core:${versions.androidx_core}" 118 implementation "androidx.core:core:${versions.androidx_core}"
118 implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}" 119 implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}"
1 package io.livekit.android 1 package io.livekit.android
2 2
  3 +import android.app.Application
3 import android.content.Context 4 import android.content.Context
4 import io.livekit.android.dagger.DaggerLiveKitComponent 5 import io.livekit.android.dagger.DaggerLiveKitComponent
5 import io.livekit.android.dagger.create 6 import io.livekit.android.dagger.create
@@ -47,6 +48,11 @@ class LiveKit { @@ -47,6 +48,11 @@ class LiveKit {
47 overrides: LiveKitOverrides = LiveKitOverrides(), 48 overrides: LiveKitOverrides = LiveKitOverrides(),
48 ): Room { 49 ): Room {
49 val ctx = appContext.applicationContext 50 val ctx = appContext.applicationContext
  51 +
  52 + if (ctx !is Application) {
  53 + LKLog.w { "Application context was not found, this may cause memory leaks." }
  54 + }
  55 +
50 val component = DaggerLiveKitComponent 56 val component = DaggerLiveKitComponent
51 .factory() 57 .factory()
52 .create(ctx, overrides) 58 .create(ctx, overrides)
1 package io.livekit.android 1 package io.livekit.android
2 2
  3 +import io.livekit.android.audio.AudioHandler
  4 +import io.livekit.android.audio.NoAudioHandler
3 import okhttp3.OkHttpClient 5 import okhttp3.OkHttpClient
4 import org.webrtc.VideoDecoderFactory 6 import org.webrtc.VideoDecoderFactory
5 import org.webrtc.VideoEncoderFactory 7 import org.webrtc.VideoEncoderFactory
@@ -36,4 +38,10 @@ data class LiveKitOverrides( @@ -36,4 +38,10 @@ data class LiveKitOverrides(
36 * Override the [VideoDecoderFactory] used by the library. 38 * Override the [VideoDecoderFactory] used by the library.
37 */ 39 */
38 val videoDecoderFactory: VideoDecoderFactory? = null, 40 val videoDecoderFactory: VideoDecoderFactory? = null,
  41 +
  42 + /**
  43 + * Override the default [AudioHandler]. Use [NoAudioHandler] to turn off automatic audio handling.
  44 + */
  45 +
  46 + val audioHandler: AudioHandler? = null
39 ) 47 )
  1 +package io.livekit.android.audio
  2 +
  3 +/**
  4 + * Interface for handling android audio routing.
  5 + */
  6 +interface AudioHandler {
  7 + /**
  8 + * Called when a room is started.
  9 + */
  10 + fun start()
  11 +
  12 + /**
  13 + * Called when a room is disconnected.
  14 + */
  15 + fun stop()
  16 +}
  1 +package io.livekit.android.audio
  2 +
  3 +import android.content.Context
  4 +import com.twilio.audioswitch.AudioSwitch
  5 +import javax.inject.Inject
  6 +import javax.inject.Singleton
  7 +
  8 +@Singleton
  9 +class AudioSwitchHandler
  10 +@Inject
  11 +constructor(context: Context) : AudioHandler {
  12 + private val audioSwitch = AudioSwitch(context)
  13 + override fun start() {
  14 + audioSwitch.start { _, _ -> }
  15 + audioSwitch.activate()
  16 + }
  17 +
  18 + override fun stop() {
  19 + audioSwitch.stop()
  20 + }
  21 +}
  1 +package io.livekit.android.audio
  2 +
  3 +import javax.inject.Inject
  4 +
  5 +/**
  6 + * A dummy implementation that does no audio handling.
  7 + */
  8 +class NoAudioHandler
  9 +@Inject
  10 +constructor() : AudioHandler {
  11 + override fun start() {
  12 + }
  13 +
  14 + override fun stop() {
  15 + }
  16 +}
  1 +package io.livekit.android.dagger
  2 +
  3 +import androidx.annotation.Nullable
  4 +import dagger.Module
  5 +import dagger.Provides
  6 +import io.livekit.android.audio.AudioHandler
  7 +import io.livekit.android.audio.AudioSwitchHandler
  8 +import javax.inject.Named
  9 +import javax.inject.Provider
  10 +
  11 +@Module
  12 +object AudioHandlerModule {
  13 + @Provides
  14 + fun audioHandler(
  15 + audioSwitchHandler: Provider<AudioSwitchHandler>,
  16 + @Named(InjectionNames.OVERRIDE_AUDIO_HANDLER)
  17 + @Nullable
  18 + audioHandlerOverride: AudioHandler?
  19 + ): AudioHandler {
  20 + return audioHandlerOverride ?: audioSwitchHandler.get()
  21 + }
  22 +}
@@ -30,4 +30,5 @@ object InjectionNames { @@ -30,4 +30,5 @@ object InjectionNames {
30 internal const val OVERRIDE_JAVA_AUDIO_DEVICE_MODULE_CUSTOMIZER = "override_java_audio_device_module_customizer" 30 internal const val OVERRIDE_JAVA_AUDIO_DEVICE_MODULE_CUSTOMIZER = "override_java_audio_device_module_customizer"
31 internal const val OVERRIDE_VIDEO_ENCODER_FACTORY = "override_video_encoder_factory" 31 internal const val OVERRIDE_VIDEO_ENCODER_FACTORY = "override_video_encoder_factory"
32 internal const val OVERRIDE_VIDEO_DECODER_FACTORY = "override_video_decoder_factory" 32 internal const val OVERRIDE_VIDEO_DECODER_FACTORY = "override_video_decoder_factory"
  33 + internal const val OVERRIDE_AUDIO_HANDLER = "override_audio_handler"
33 } 34 }
@@ -17,6 +17,7 @@ import javax.inject.Singleton @@ -17,6 +17,7 @@ import javax.inject.Singleton
17 WebModule::class, 17 WebModule::class,
18 JsonFormatModule::class, 18 JsonFormatModule::class,
19 OverridesModule::class, 19 OverridesModule::class,
  20 + AudioHandlerModule::class,
20 ] 21 ]
21 ) 22 )
22 internal interface LiveKitComponent { 23 internal interface LiveKitComponent {
@@ -34,4 +34,9 @@ class OverridesModule(private val overrides: LiveKitOverrides) { @@ -34,4 +34,9 @@ class OverridesModule(private val overrides: LiveKitOverrides) {
34 @Nullable 34 @Nullable
35 fun videoDecoderFactory() = overrides.videoDecoderFactory 35 fun videoDecoderFactory() = overrides.videoDecoderFactory
36 36
  37 + @Provides
  38 + @Named(InjectionNames.OVERRIDE_AUDIO_HANDLER)
  39 + @Nullable
  40 + fun audioHandler() = overrides.audioHandler
  41 +
37 } 42 }
@@ -13,6 +13,7 @@ import dagger.assisted.AssistedInject @@ -13,6 +13,7 @@ import dagger.assisted.AssistedInject
13 import io.livekit.android.ConnectOptions 13 import io.livekit.android.ConnectOptions
14 import io.livekit.android.RoomOptions 14 import io.livekit.android.RoomOptions
15 import io.livekit.android.Version 15 import io.livekit.android.Version
  16 +import io.livekit.android.audio.AudioHandler
16 import io.livekit.android.dagger.InjectionNames 17 import io.livekit.android.dagger.InjectionNames
17 import io.livekit.android.events.BroadcastEventBus 18 import io.livekit.android.events.BroadcastEventBus
18 import io.livekit.android.events.ParticipantEvent 19 import io.livekit.android.events.ParticipantEvent
@@ -43,6 +44,7 @@ constructor( @@ -43,6 +44,7 @@ constructor(
43 private val defaultDispatcher: CoroutineDispatcher, 44 private val defaultDispatcher: CoroutineDispatcher,
44 @Named(InjectionNames.DISPATCHER_IO) 45 @Named(InjectionNames.DISPATCHER_IO)
45 private val ioDispatcher: CoroutineDispatcher, 46 private val ioDispatcher: CoroutineDispatcher,
  47 + private val audioHandler: AudioHandler,
46 ) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { 48 ) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() {
47 49
48 private lateinit var coroutineScope: CoroutineScope 50 private lateinit var coroutineScope: CoroutineScope
@@ -77,7 +79,15 @@ constructor( @@ -77,7 +79,15 @@ constructor(
77 79
78 @FlowObservable 80 @FlowObservable
79 @get:FlowObservable 81 @get:FlowObservable
80 - var state: State by flowDelegate(State.DISCONNECTED) 82 + var state: State by flowDelegate(State.DISCONNECTED) { new, old ->
  83 + if (new != old) {
  84 + when (new) {
  85 + State.CONNECTING -> audioHandler.start()
  86 + State.DISCONNECTED -> audioHandler.stop()
  87 + else -> {}
  88 + }
  89 + }
  90 + }
81 private set 91 private set
82 92
83 @FlowObservable 93 @FlowObservable
  1 +package io.livekit.android.mock.dagger
  2 +
  3 +import dagger.Binds
  4 +import dagger.Module
  5 +import io.livekit.android.audio.AudioHandler
  6 +import io.livekit.android.audio.NoAudioHandler
  7 +
  8 +@Module
  9 +interface TestAudioHandlerModule {
  10 + @Binds
  11 + fun audioHandler(audioHandler: NoAudioHandler): AudioHandler
  12 +}
@@ -15,6 +15,7 @@ import javax.inject.Singleton @@ -15,6 +15,7 @@ import javax.inject.Singleton
15 TestCoroutinesModule::class, 15 TestCoroutinesModule::class,
16 TestRTCModule::class, 16 TestRTCModule::class,
17 TestWebModule::class, 17 TestWebModule::class,
  18 + TestAudioHandlerModule::class,
18 JsonFormatModule::class, 19 JsonFormatModule::class,
19 ] 20 ]
20 ) 21 )
@@ -3,6 +3,7 @@ package io.livekit.android.room @@ -3,6 +3,7 @@ package io.livekit.android.room
3 import android.content.Context 3 import android.content.Context
4 import android.net.Network 4 import android.net.Network
5 import androidx.test.core.app.ApplicationProvider 5 import androidx.test.core.app.ApplicationProvider
  6 +import io.livekit.android.audio.NoAudioHandler
6 import io.livekit.android.coroutines.TestCoroutineRule 7 import io.livekit.android.coroutines.TestCoroutineRule
7 import io.livekit.android.events.EventCollector 8 import io.livekit.android.events.EventCollector
8 import io.livekit.android.events.RoomEvent 9 import io.livekit.android.events.RoomEvent
@@ -40,7 +41,7 @@ class RoomTest { @@ -40,7 +41,7 @@ class RoomTest {
40 41
41 var eglBase: EglBase = MockEglBase() 42 var eglBase: EglBase = MockEglBase()
42 43
43 - val localParticantFactory = object : LocalParticipant.Factory { 44 + val localParticipantFactory = object : LocalParticipant.Factory {
44 override fun create(info: LivekitModels.ParticipantInfo, dynacast: Boolean): LocalParticipant { 45 override fun create(info: LivekitModels.ParticipantInfo, dynacast: Boolean): LocalParticipant {
45 return Mockito.mock(LocalParticipant::class.java) 46 return Mockito.mock(LocalParticipant::class.java)
46 } 47 }
@@ -52,13 +53,14 @@ class RoomTest { @@ -52,13 +53,14 @@ class RoomTest {
52 fun setup() { 53 fun setup() {
53 context = ApplicationProvider.getApplicationContext() 54 context = ApplicationProvider.getApplicationContext()
54 room = Room( 55 room = Room(
55 - context,  
56 - rtcEngine,  
57 - eglBase,  
58 - localParticantFactory,  
59 - DefaultsManager(),  
60 - coroutineRule.dispatcher,  
61 - coroutineRule.dispatcher, 56 + context = context,
  57 + engine = rtcEngine,
  58 + eglBase = eglBase,
  59 + localParticipantFactory = localParticipantFactory,
  60 + defaultsManager = DefaultsManager(),
  61 + defaultDispatcher = coroutineRule.dispatcher,
  62 + ioDispatcher = coroutineRule.dispatcher,
  63 + audioHandler = NoAudioHandler(),
62 ) 64 )
63 } 65 }
64 66
@@ -16,7 +16,6 @@ import io.livekit.android.room.participant.LocalParticipant @@ -16,7 +16,6 @@ import io.livekit.android.room.participant.LocalParticipant
16 import io.livekit.android.room.participant.Participant 16 import io.livekit.android.room.participant.Participant
17 import io.livekit.android.room.participant.RemoteParticipant 17 import io.livekit.android.room.participant.RemoteParticipant
18 import io.livekit.android.room.track.* 18 import io.livekit.android.room.track.*
19 -import io.livekit.android.sample.audio.AppRTCAudioManager  
20 import io.livekit.android.util.flow 19 import io.livekit.android.util.flow
21 import kotlinx.coroutines.ExperimentalCoroutinesApi 20 import kotlinx.coroutines.ExperimentalCoroutinesApi
22 import kotlinx.coroutines.flow.* 21 import kotlinx.coroutines.flow.*
@@ -72,12 +71,7 @@ class CallViewModel( @@ -72,12 +71,7 @@ class CallViewModel(
72 private val mutablePermissionAllowed = MutableStateFlow(true) 71 private val mutablePermissionAllowed = MutableStateFlow(true)
73 val permissionAllowed = mutablePermissionAllowed.hide() 72 val permissionAllowed = mutablePermissionAllowed.hide()
74 73
75 - private val audioManager = AppRTCAudioManager(application)  
76 -  
77 init { 74 init {
78 -  
79 - audioManager.start(null)  
80 -  
81 viewModelScope.launch { 75 viewModelScope.launch {
82 launch { 76 launch {
83 error.collect { Timber.e(it) } 77 error.collect { Timber.e(it) }
@@ -200,7 +194,6 @@ class CallViewModel( @@ -200,7 +194,6 @@ class CallViewModel(
200 override fun onCleared() { 194 override fun onCleared() {
201 super.onCleared() 195 super.onCleared()
202 room.disconnect() 196 room.disconnect()
203 - audioManager.stop()  
204 } 197 }
205 198
206 fun setMicEnabled(enabled: Boolean) { 199 fun setMicEnabled(enabled: Boolean) {