Committed by
GitHub
Communication workaround for Android 11+ (#363) (#364)
* Communication mode workaround * clean up implementation * tests and spotless * Inject dispatcher * Tie communication workaround to playout instead of record
正在显示
10 个修改的文件
包含
289 行增加
和
3 行删除
| @@ -104,6 +104,22 @@ class AudioOptions( | @@ -104,6 +104,22 @@ class AudioOptions( | ||
| 104 | * Not used if [audioDeviceModule] is provided. | 104 | * Not used if [audioDeviceModule] is provided. |
| 105 | */ | 105 | */ |
| 106 | val javaAudioDeviceModuleCustomizer: ((builder: JavaAudioDeviceModule.Builder) -> Unit)? = null, | 106 | val javaAudioDeviceModuleCustomizer: ((builder: JavaAudioDeviceModule.Builder) -> Unit)? = null, |
| 107 | + | ||
| 108 | + /** | ||
| 109 | + * On Android 11+, the audio mode will reset itself from [AudioManager.MODE_IN_COMMUNICATION] if | ||
| 110 | + * there is no audio playback or capture for 6 seconds (for example when joining a room with | ||
| 111 | + * no speakers and the local mic is muted.) This mode reset will cause unexpected | ||
| 112 | + * behavior when trying to change the volume, causing it to not properly change the volume. | ||
| 113 | + * | ||
| 114 | + * We use a workaround by playing a silent audio track to keep the communication mode from | ||
| 115 | + * resetting. | ||
| 116 | + * | ||
| 117 | + * Setting this flag to true will disable the workaround. | ||
| 118 | + * | ||
| 119 | + * This flag is a no-op when the audio mode is set to anything other than | ||
| 120 | + * [AudioManager.MODE_IN_COMMUNICATION]. | ||
| 121 | + */ | ||
| 122 | + val disableCommunicationModeWorkaround: Boolean = false, | ||
| 107 | ) | 123 | ) |
| 108 | 124 | ||
| 109 | /** | 125 | /** |
| 1 | +/* | ||
| 2 | + * Copyright 2024 LiveKit, Inc. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +package io.livekit.android.audio | ||
| 18 | + | ||
| 19 | +import android.annotation.SuppressLint | ||
| 20 | +import android.media.AudioAttributes | ||
| 21 | +import android.media.AudioFormat | ||
| 22 | +import android.media.AudioManager | ||
| 23 | +import android.media.AudioTrack | ||
| 24 | +import android.os.Build | ||
| 25 | +import androidx.annotation.RequiresApi | ||
| 26 | +import io.livekit.android.dagger.InjectionNames | ||
| 27 | +import io.livekit.android.util.CloseableCoroutineScope | ||
| 28 | +import kotlinx.coroutines.MainCoroutineDispatcher | ||
| 29 | +import kotlinx.coroutines.flow.MutableStateFlow | ||
| 30 | +import kotlinx.coroutines.flow.combine | ||
| 31 | +import kotlinx.coroutines.flow.distinctUntilChanged | ||
| 32 | +import kotlinx.coroutines.launch | ||
| 33 | +import java.nio.ByteBuffer | ||
| 34 | +import javax.inject.Inject | ||
| 35 | +import javax.inject.Named | ||
| 36 | +import javax.inject.Singleton | ||
| 37 | + | ||
| 38 | +/** | ||
| 39 | + * @see CommunicationWorkaroundImpl | ||
| 40 | + */ | ||
| 41 | +interface CommunicationWorkaround { | ||
| 42 | + | ||
| 43 | + fun start() | ||
| 44 | + fun stop() | ||
| 45 | + fun onStartPlayout() | ||
| 46 | + fun onStopPlayout() | ||
| 47 | + | ||
| 48 | + fun dispose() | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +class NoopCommunicationWorkaround | ||
| 52 | +@Inject | ||
| 53 | +constructor() : CommunicationWorkaround { | ||
| 54 | + override fun start() { | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + override fun stop() { | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + override fun onStartPlayout() { | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + override fun onStopPlayout() { | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + override fun dispose() { | ||
| 67 | + } | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +/** | ||
| 71 | + * Work around for communication mode resetting after 6 seconds if no audio playback or capture. | ||
| 72 | + * Issue only happens on 11+ (version code R). | ||
| 73 | + * https://issuetracker.google.com/issues/209493718 | ||
| 74 | + */ | ||
| 75 | +@Singleton | ||
| 76 | +@RequiresApi(Build.VERSION_CODES.R) | ||
| 77 | +class CommunicationWorkaroundImpl | ||
| 78 | +@Inject | ||
| 79 | +constructor( | ||
| 80 | + @Named(InjectionNames.DISPATCHER_MAIN) | ||
| 81 | + dispatcher: MainCoroutineDispatcher, | ||
| 82 | +) : CommunicationWorkaround { | ||
| 83 | + | ||
| 84 | + private val coroutineScope = CloseableCoroutineScope(dispatcher) | ||
| 85 | + private val started = MutableStateFlow(false) | ||
| 86 | + private val playoutStopped = MutableStateFlow(true) | ||
| 87 | + | ||
| 88 | + private var audioTrack: AudioTrack? = null | ||
| 89 | + | ||
| 90 | + init { | ||
| 91 | + coroutineScope.launch { | ||
| 92 | + started.combine(playoutStopped) { a, b -> a to b } | ||
| 93 | + .distinctUntilChanged() | ||
| 94 | + .collect { (started, playoutStopped) -> | ||
| 95 | + onStateChanged(started, playoutStopped) | ||
| 96 | + } | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + override fun start() { | ||
| 101 | + started.value = true | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + override fun stop() { | ||
| 105 | + started.value = false | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + override fun onStartPlayout() { | ||
| 109 | + playoutStopped.value = false | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + override fun onStopPlayout() { | ||
| 113 | + playoutStopped.value = true | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + @SuppressLint("NewApi") | ||
| 117 | + private fun onStateChanged(started: Boolean, playoutStopped: Boolean) { | ||
| 118 | + if (started && playoutStopped) { | ||
| 119 | + startAudioTrackIfNeeded() | ||
| 120 | + } else { | ||
| 121 | + stopAudioTrackIfNeeded() | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + @SuppressLint("Range") | ||
| 126 | + private fun startAudioTrackIfNeeded() { | ||
| 127 | + if (audioTrack != null) { | ||
| 128 | + return | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + val sampleRate = 16000 | ||
| 132 | + val audioFormat = AudioFormat.ENCODING_PCM_16BIT | ||
| 133 | + val bytesPerFrame = 1 * getBytesPerSample(audioFormat) | ||
| 134 | + val framesPerBuffer: Int = sampleRate / 100 // 10 ms | ||
| 135 | + val byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer) | ||
| 136 | + | ||
| 137 | + audioTrack = AudioTrack.Builder() | ||
| 138 | + .setAudioFormat( | ||
| 139 | + AudioFormat.Builder() | ||
| 140 | + .setEncoding(audioFormat) | ||
| 141 | + .setSampleRate(sampleRate) | ||
| 142 | + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) | ||
| 143 | + .build(), | ||
| 144 | + ) | ||
| 145 | + .setAudioAttributes( | ||
| 146 | + AudioAttributes.Builder() | ||
| 147 | + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) | ||
| 148 | + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) | ||
| 149 | + .build(), | ||
| 150 | + ) | ||
| 151 | + .setBufferSizeInBytes(byteBuffer.capacity()) | ||
| 152 | + .setTransferMode(AudioTrack.MODE_STATIC) | ||
| 153 | + .setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE) | ||
| 154 | + .build() | ||
| 155 | + | ||
| 156 | + audioTrack?.write(byteBuffer, byteBuffer.remaining(), AudioTrack.WRITE_BLOCKING) | ||
| 157 | + audioTrack?.setLoopPoints(0, framesPerBuffer - 1, -1) | ||
| 158 | + | ||
| 159 | + audioTrack?.play() | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + // Reference from Android code, AudioFormat.getBytesPerSample. BitPerSample / 8 | ||
| 163 | + // Default audio data format is PCM 16 bits per sample. | ||
| 164 | + // Guaranteed to be supported by all devices | ||
| 165 | + private fun getBytesPerSample(audioFormat: Int): Int { | ||
| 166 | + return when (audioFormat) { | ||
| 167 | + AudioFormat.ENCODING_PCM_8BIT -> 1 | ||
| 168 | + AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_IEC61937, AudioFormat.ENCODING_DEFAULT -> 2 | ||
| 169 | + AudioFormat.ENCODING_PCM_FLOAT -> 4 | ||
| 170 | + AudioFormat.ENCODING_INVALID -> throw IllegalArgumentException("Bad audio format $audioFormat") | ||
| 171 | + else -> throw IllegalArgumentException("Bad audio format $audioFormat") | ||
| 172 | + } | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + private fun stopAudioTrackIfNeeded() { | ||
| 176 | + audioTrack?.stop() | ||
| 177 | + audioTrack?.release() | ||
| 178 | + audioTrack = null | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + override fun dispose() { | ||
| 182 | + coroutineScope.close() | ||
| 183 | + stop() | ||
| 184 | + stopAudioTrackIfNeeded() | ||
| 185 | + } | ||
| 186 | +} |
| @@ -17,11 +17,17 @@ | @@ -17,11 +17,17 @@ | ||
| 17 | package io.livekit.android.dagger | 17 | package io.livekit.android.dagger |
| 18 | 18 | ||
| 19 | import android.media.AudioAttributes | 19 | import android.media.AudioAttributes |
| 20 | +import android.media.AudioManager | ||
| 21 | +import android.os.Build | ||
| 20 | import dagger.Module | 22 | import dagger.Module |
| 21 | import dagger.Provides | 23 | import dagger.Provides |
| 22 | import io.livekit.android.AudioType | 24 | import io.livekit.android.AudioType |
| 23 | import io.livekit.android.audio.AudioHandler | 25 | import io.livekit.android.audio.AudioHandler |
| 24 | import io.livekit.android.audio.AudioSwitchHandler | 26 | import io.livekit.android.audio.AudioSwitchHandler |
| 27 | +import io.livekit.android.audio.CommunicationWorkaround | ||
| 28 | +import io.livekit.android.audio.CommunicationWorkaroundImpl | ||
| 29 | +import io.livekit.android.audio.NoopCommunicationWorkaround | ||
| 30 | +import io.livekit.android.memory.CloseableManager | ||
| 25 | import javax.inject.Named | 31 | import javax.inject.Named |
| 26 | import javax.inject.Provider | 32 | import javax.inject.Provider |
| 27 | import javax.inject.Singleton | 33 | import javax.inject.Singleton |
| @@ -62,4 +68,27 @@ internal object AudioHandlerModule { | @@ -62,4 +68,27 @@ internal object AudioHandlerModule { | ||
| 62 | audioStreamType = audioOutputType.audioStreamType | 68 | audioStreamType = audioOutputType.audioStreamType |
| 63 | } | 69 | } |
| 64 | } | 70 | } |
| 71 | + | ||
| 72 | + @Provides | ||
| 73 | + @Singleton | ||
| 74 | + @JvmSuppressWildcards | ||
| 75 | + fun communicationWorkaround( | ||
| 76 | + @Named(InjectionNames.OVERRIDE_DISABLE_COMMUNICATION_WORKAROUND) | ||
| 77 | + disableCommunicationWorkaround: Boolean, | ||
| 78 | + audioType: AudioType, | ||
| 79 | + closeableManager: CloseableManager, | ||
| 80 | + commWorkaroundImplProvider: Provider<CommunicationWorkaroundImpl>, | ||
| 81 | + ): CommunicationWorkaround { | ||
| 82 | + return if ( | ||
| 83 | + !disableCommunicationWorkaround && | ||
| 84 | + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && | ||
| 85 | + audioType.audioMode == AudioManager.MODE_IN_COMMUNICATION | ||
| 86 | + ) { | ||
| 87 | + commWorkaroundImplProvider.get().apply { | ||
| 88 | + closeableManager.registerClosable { this.dispose() } | ||
| 89 | + } | ||
| 90 | + } else { | ||
| 91 | + NoopCommunicationWorkaround() | ||
| 92 | + } | ||
| 93 | + } | ||
| 65 | } | 94 | } |
| @@ -52,5 +52,6 @@ internal object InjectionNames { | @@ -52,5 +52,6 @@ internal object InjectionNames { | ||
| 52 | internal const val OVERRIDE_VIDEO_DECODER_FACTORY = "override_video_decoder_factory" | 52 | internal const val OVERRIDE_VIDEO_DECODER_FACTORY = "override_video_decoder_factory" |
| 53 | internal const val OVERRIDE_AUDIO_HANDLER = "override_audio_handler" | 53 | internal const val OVERRIDE_AUDIO_HANDLER = "override_audio_handler" |
| 54 | internal const val OVERRIDE_AUDIO_OUTPUT_TYPE = "override_audio_output_type" | 54 | internal const val OVERRIDE_AUDIO_OUTPUT_TYPE = "override_audio_output_type" |
| 55 | + internal const val OVERRIDE_DISABLE_COMMUNICATION_WORKAROUND = "override_disable_communication_workaround" | ||
| 55 | internal const val OVERRIDE_EGL_BASE = "override_egl_base" | 56 | internal const val OVERRIDE_EGL_BASE = "override_egl_base" |
| 56 | } | 57 | } |
| @@ -62,6 +62,10 @@ internal class OverridesModule(private val overrides: LiveKitOverrides) { | @@ -62,6 +62,10 @@ internal class OverridesModule(private val overrides: LiveKitOverrides) { | ||
| 62 | fun audioOutputType() = overrides.audioOptions?.audioOutputType | 62 | fun audioOutputType() = overrides.audioOptions?.audioOutputType |
| 63 | 63 | ||
| 64 | @Provides | 64 | @Provides |
| 65 | + @Named(InjectionNames.OVERRIDE_DISABLE_COMMUNICATION_WORKAROUND) | ||
| 66 | + fun disableCommunicationWorkAround() = overrides.audioOptions?.disableCommunicationModeWorkaround ?: false | ||
| 67 | + | ||
| 68 | + @Provides | ||
| 65 | @Named(InjectionNames.OVERRIDE_EGL_BASE) | 69 | @Named(InjectionNames.OVERRIDE_EGL_BASE) |
| 66 | @Nullable | 70 | @Nullable |
| 67 | fun eglBase() = overrides.eglBase | 71 | fun eglBase() = overrides.eglBase |
| @@ -25,6 +25,7 @@ import androidx.annotation.Nullable | @@ -25,6 +25,7 @@ import androidx.annotation.Nullable | ||
| 25 | import dagger.Module | 25 | import dagger.Module |
| 26 | import dagger.Provides | 26 | import dagger.Provides |
| 27 | import io.livekit.android.LiveKit | 27 | import io.livekit.android.LiveKit |
| 28 | +import io.livekit.android.audio.CommunicationWorkaround | ||
| 28 | import io.livekit.android.memory.CloseableManager | 29 | import io.livekit.android.memory.CloseableManager |
| 29 | import io.livekit.android.util.LKLog | 30 | import io.livekit.android.util.LKLog |
| 30 | import io.livekit.android.util.LoggingLevel | 31 | import io.livekit.android.util.LoggingLevel |
| @@ -91,6 +92,7 @@ internal object RTCModule { | @@ -91,6 +92,7 @@ internal object RTCModule { | ||
| 91 | audioOutputAttributes: AudioAttributes, | 92 | audioOutputAttributes: AudioAttributes, |
| 92 | appContext: Context, | 93 | appContext: Context, |
| 93 | closeableManager: CloseableManager, | 94 | closeableManager: CloseableManager, |
| 95 | + communicationWorkaround: CommunicationWorkaround, | ||
| 94 | ): AudioDeviceModule { | 96 | ): AudioDeviceModule { |
| 95 | if (audioDeviceModuleOverride != null) { | 97 | if (audioDeviceModuleOverride != null) { |
| 96 | return audioDeviceModuleOverride | 98 | return audioDeviceModuleOverride |
| @@ -130,6 +132,7 @@ internal object RTCModule { | @@ -130,6 +132,7 @@ internal object RTCModule { | ||
| 130 | LKLog.e { "onWebRtcAudioTrackError: $errorMessage" } | 132 | LKLog.e { "onWebRtcAudioTrackError: $errorMessage" } |
| 131 | } | 133 | } |
| 132 | } | 134 | } |
| 135 | + | ||
| 133 | val audioRecordStateCallback: JavaAudioDeviceModule.AudioRecordStateCallback = object : | 136 | val audioRecordStateCallback: JavaAudioDeviceModule.AudioRecordStateCallback = object : |
| 134 | JavaAudioDeviceModule.AudioRecordStateCallback { | 137 | JavaAudioDeviceModule.AudioRecordStateCallback { |
| 135 | override fun onWebRtcAudioRecordStart() { | 138 | override fun onWebRtcAudioRecordStart() { |
| @@ -146,10 +149,12 @@ internal object RTCModule { | @@ -146,10 +149,12 @@ internal object RTCModule { | ||
| 146 | JavaAudioDeviceModule.AudioTrackStateCallback { | 149 | JavaAudioDeviceModule.AudioTrackStateCallback { |
| 147 | override fun onWebRtcAudioTrackStart() { | 150 | override fun onWebRtcAudioTrackStart() { |
| 148 | LKLog.v { "Audio playout starts" } | 151 | LKLog.v { "Audio playout starts" } |
| 152 | + communicationWorkaround.onStartPlayout() | ||
| 149 | } | 153 | } |
| 150 | 154 | ||
| 151 | override fun onWebRtcAudioTrackStop() { | 155 | override fun onWebRtcAudioTrackStop() { |
| 152 | LKLog.v { "Audio playout stops" } | 156 | LKLog.v { "Audio playout stops" } |
| 157 | + communicationWorkaround.onStopPlayout() | ||
| 153 | } | 158 | } |
| 154 | } | 159 | } |
| 155 | 160 |
| @@ -31,6 +31,7 @@ import io.livekit.android.ConnectOptions | @@ -31,6 +31,7 @@ import io.livekit.android.ConnectOptions | ||
| 31 | import io.livekit.android.RoomOptions | 31 | import io.livekit.android.RoomOptions |
| 32 | import io.livekit.android.Version | 32 | import io.livekit.android.Version |
| 33 | import io.livekit.android.audio.AudioHandler | 33 | import io.livekit.android.audio.AudioHandler |
| 34 | +import io.livekit.android.audio.CommunicationWorkaround | ||
| 34 | import io.livekit.android.dagger.InjectionNames | 35 | import io.livekit.android.dagger.InjectionNames |
| 35 | import io.livekit.android.e2ee.E2EEManager | 36 | import io.livekit.android.e2ee.E2EEManager |
| 36 | import io.livekit.android.e2ee.E2EEOptions | 37 | import io.livekit.android.e2ee.E2EEOptions |
| @@ -69,6 +70,7 @@ constructor( | @@ -69,6 +70,7 @@ constructor( | ||
| 69 | val audioHandler: AudioHandler, | 70 | val audioHandler: AudioHandler, |
| 70 | private val closeableManager: CloseableManager, | 71 | private val closeableManager: CloseableManager, |
| 71 | private val e2EEManagerFactory: E2EEManager.Factory, | 72 | private val e2EEManagerFactory: E2EEManager.Factory, |
| 73 | + private val communicationWorkaround: CommunicationWorkaround, | ||
| 72 | ) : RTCEngine.Listener, ParticipantListener { | 74 | ) : RTCEngine.Listener, ParticipantListener { |
| 73 | 75 | ||
| 74 | private lateinit var coroutineScope: CoroutineScope | 76 | private lateinit var coroutineScope: CoroutineScope |
| @@ -133,8 +135,16 @@ constructor( | @@ -133,8 +135,16 @@ constructor( | ||
| 133 | var state: State by flowDelegate(State.DISCONNECTED) { new, old -> | 135 | var state: State by flowDelegate(State.DISCONNECTED) { new, old -> |
| 134 | if (new != old) { | 136 | if (new != old) { |
| 135 | when (new) { | 137 | when (new) { |
| 136 | - State.CONNECTING -> audioHandler.start() | ||
| 137 | - State.DISCONNECTED -> audioHandler.stop() | 138 | + State.CONNECTING -> { |
| 139 | + audioHandler.start() | ||
| 140 | + communicationWorkaround.start() | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + State.DISCONNECTED -> { | ||
| 144 | + audioHandler.stop() | ||
| 145 | + communicationWorkaround.stop() | ||
| 146 | + } | ||
| 147 | + | ||
| 138 | else -> {} | 148 | else -> {} |
| 139 | } | 149 | } |
| 140 | } | 150 | } |
| 1 | +/* | ||
| 2 | + * Copyright 2024 LiveKit, Inc. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +package io.livekit.android.util | ||
| 18 | + | ||
| 19 | +import android.os.Handler | ||
| 20 | +import android.os.Looper | ||
| 21 | + | ||
| 22 | +fun Handler.runOrPost(r: Runnable) { | ||
| 23 | + if (Looper.myLooper() == this.looper) { | ||
| 24 | + r.run() | ||
| 25 | + } else { | ||
| 26 | + post(r) | ||
| 27 | + } | ||
| 28 | +} |
| 1 | /* | 1 | /* |
| 2 | - * Copyright 2023 LiveKit, Inc. | 2 | + * Copyright 2023-2024 LiveKit, Inc. |
| 3 | * | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. | 5 | * you may not use this file except in compliance with the License. |
| @@ -19,10 +19,15 @@ package io.livekit.android.mock.dagger | @@ -19,10 +19,15 @@ package io.livekit.android.mock.dagger | ||
| 19 | import dagger.Binds | 19 | import dagger.Binds |
| 20 | import dagger.Module | 20 | import dagger.Module |
| 21 | import io.livekit.android.audio.AudioHandler | 21 | import io.livekit.android.audio.AudioHandler |
| 22 | +import io.livekit.android.audio.CommunicationWorkaround | ||
| 22 | import io.livekit.android.audio.NoAudioHandler | 23 | import io.livekit.android.audio.NoAudioHandler |
| 24 | +import io.livekit.android.audio.NoopCommunicationWorkaround | ||
| 23 | 25 | ||
| 24 | @Module | 26 | @Module |
| 25 | interface TestAudioHandlerModule { | 27 | interface TestAudioHandlerModule { |
| 26 | @Binds | 28 | @Binds |
| 27 | fun audioHandler(audioHandler: NoAudioHandler): AudioHandler | 29 | fun audioHandler(audioHandler: NoAudioHandler): AudioHandler |
| 30 | + | ||
| 31 | + @Binds | ||
| 32 | + fun communicationWorkaround(communicationWorkaround: NoopCommunicationWorkaround): CommunicationWorkaround | ||
| 28 | } | 33 | } |
| @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider | @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider | ||
| 23 | import androidx.test.platform.app.InstrumentationRegistry | 23 | import androidx.test.platform.app.InstrumentationRegistry |
| 24 | import io.livekit.android.assert.assertIsClassList | 24 | import io.livekit.android.assert.assertIsClassList |
| 25 | import io.livekit.android.audio.NoAudioHandler | 25 | import io.livekit.android.audio.NoAudioHandler |
| 26 | +import io.livekit.android.audio.NoopCommunicationWorkaround | ||
| 26 | import io.livekit.android.coroutines.TestCoroutineRule | 27 | import io.livekit.android.coroutines.TestCoroutineRule |
| 27 | import io.livekit.android.e2ee.E2EEManager | 28 | import io.livekit.android.e2ee.E2EEManager |
| 28 | import io.livekit.android.events.* | 29 | import io.livekit.android.events.* |
| @@ -99,6 +100,7 @@ class RoomTest { | @@ -99,6 +100,7 @@ class RoomTest { | ||
| 99 | audioHandler = NoAudioHandler(), | 100 | audioHandler = NoAudioHandler(), |
| 100 | closeableManager = CloseableManager(), | 101 | closeableManager = CloseableManager(), |
| 101 | e2EEManagerFactory = e2EEManagerFactory, | 102 | e2EEManagerFactory = e2EEManagerFactory, |
| 103 | + communicationWorkaround = NoopCommunicationWorkaround(), | ||
| 102 | ) | 104 | ) |
| 103 | } | 105 | } |
| 104 | 106 |
-
请 注册 或 登录 后发表评论