CloudWebRTC
Committed by GitHub

feat: add external audio processing api. (#319)

* feat: add exteranl audio processing api.

* update.

* fix spotlessKotlinCheck.

* update.

* Comments.

* fix typo.

* upgrade libwebrtc to 114.5735.08.

* code format.

* rename and add more method.

* update.

* add siwtch dialog for audio processor.

* format.

* format.

* add AudioProcessorOptions.

* revert changes.

* Code cleanup

* spotless

* fix test compile

* spotless

* Renaming of functions and removing url/token checking from isEnabled

---------

Co-authored-by: davidliu <davidliu@deviange.net>
[versions]
webrtc = "114.5735.07"
webrtc = "114.5735.08"
androidJainSipRi = "1.3.0-91"
androidx-core = "1.10.1"
... ...
... ... @@ -20,6 +20,7 @@ import android.media.AudioAttributes
import android.media.AudioManager
import io.livekit.android.audio.AudioFocusHandler
import io.livekit.android.audio.AudioHandler
import io.livekit.android.audio.AudioProcessorOptions
import io.livekit.android.audio.AudioSwitchHandler
import io.livekit.android.audio.NoAudioHandler
import io.livekit.android.room.Room
... ... @@ -126,6 +127,11 @@ class AudioOptions(
* [AudioManager.MODE_IN_COMMUNICATION].
*/
val disableCommunicationModeWorkaround: Boolean = false,
/**
* Options for processing the mic and incoming audio.
*/
val audioProcessorOptions: AudioProcessorOptions? = null,
)
/**
... ...
/*
* Copyright 2023 LiveKit, Inc.
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.audio
/**
* Interface for controlling external audio processing.
*/
interface AudioProcessingController {
/**
* Set the audio processing to be used for capture post.
*/
fun setCapturePostProcessing(processing: AudioProcessorInterface?)
/**
* Set whether to bypass mode the capture post processing.
*/
fun setBypassForCapturePostProcessing(bypass: Boolean)
/**
* Set the audio processing to be used for render pre.
*/
fun setRenderPreProcessing(processing: AudioProcessorInterface?)
/**
* Set whether to bypass mode the render pre processing.
*/
fun setBypassForRenderPreProcessing(bypass: Boolean)
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.audio
import java.nio.ByteBuffer
/**
* Interface for external audio processing.
*/
interface AudioProcessorInterface {
/**
* Check if the audio processing is enabled.
*/
fun isEnabled(): Boolean
/**
* Get the name of the audio processing.
*/
fun getName(): String
/**
* Initialize the audio processing.
*/
fun initializeAudioProcessing(sampleRateHz: Int, numChannels: Int)
/**
* Called when the sample rate has changed.
*/
fun resetAudioProcessing(newRate: Int)
/**
* Process the audio frame (10ms).
*/
fun processAudio(numBands: Int, numFrames: Int, buffer: ByteBuffer)
}
... ...
/*
* Copyright 2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.audio
data class AudioProcessorOptions(
/**
* Audio processor for captured audio.
*/
val capturePostProcessor: AudioProcessorInterface? = null,
/**
* When true, bypass the processing for captured audio.
*
* Noop if [capturePostProcessor] is null.
*/
val capturePostBypass: Boolean = false,
/**
* Audio processor for rendered audio.
*/
val renderPreProcessor: AudioProcessorInterface? = null,
/**
* When true, bypass the processing for rendered audio.
*
* Noop if [renderPreProcessor] is null.
*/
val renderPreBypass: Boolean = false,
)
... ...
... ... @@ -47,6 +47,7 @@ internal object InjectionNames {
// Overrides
internal const val OVERRIDE_OKHTTP = "override_okhttp"
internal const val OVERRIDE_AUDIO_DEVICE_MODULE = "override_audio_device_module"
internal const val OVERRIDE_AUDIO_PROCESSOR_OPTIONS = "override_audio_processor_options"
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"
... ...
... ... @@ -38,6 +38,11 @@ internal class OverridesModule(private val overrides: LiveKitOverrides) {
fun audioDeviceModule() = overrides.audioOptions?.audioDeviceModule
@Provides
@Named(InjectionNames.OVERRIDE_AUDIO_PROCESSOR_OPTIONS)
@Nullable
fun audioProcessorOptions() = overrides.audioOptions?.audioProcessorOptions
@Provides
@Named(InjectionNames.OVERRIDE_JAVA_AUDIO_DEVICE_MODULE_CUSTOMIZER)
@Nullable
fun javaAudioDeviceModuleCustomizer() = overrides.audioOptions?.javaAudioDeviceModuleCustomizer
... ...
... ... @@ -25,10 +25,13 @@ import androidx.annotation.Nullable
import dagger.Module
import dagger.Provides
import io.livekit.android.LiveKit
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioProcessorOptions
import io.livekit.android.audio.CommunicationWorkaround
import io.livekit.android.memory.CloseableManager
import io.livekit.android.util.LKLog
import io.livekit.android.util.LoggingLevel
import io.livekit.android.webrtc.CustomAudioProcessingFactory
import io.livekit.android.webrtc.CustomVideoDecoderFactory
import io.livekit.android.webrtc.CustomVideoEncoderFactory
import livekit.org.webrtc.*
... ... @@ -45,6 +48,22 @@ internal object RTCModule {
/**
* Certain classes require libwebrtc to be initialized prior to use.
*
* If your provision depends on libwebrtc initialization, just add it
* as a dependency in your method signature.
*
* Example:
*
* ```
* @Provides
* fun someFactory(
* @Suppress("UNUSED_PARAMETER")
* @Named(InjectionNames.LIB_WEBRTC_INITIALIZATION)
* webrtcInitialization: LibWebrtcInitialization
* ): SomeFactory {
* ...
* }
* ```
*/
@Provides
@Singleton
... ... @@ -216,6 +235,28 @@ internal object RTCModule {
}
@Provides
@Singleton
fun customAudioProcessingFactory(
@Suppress("UNUSED_PARAMETER")
@Named(InjectionNames.LIB_WEBRTC_INITIALIZATION)
webrtcInitialization: LibWebrtcInitialization,
@Named(InjectionNames.OVERRIDE_AUDIO_PROCESSOR_OPTIONS)
audioProcessorOptions: AudioProcessorOptions?,
): CustomAudioProcessingFactory {
return CustomAudioProcessingFactory(audioProcessorOptions ?: AudioProcessorOptions())
}
@Provides
fun audioProcessingController(customAudioProcessingFactory: CustomAudioProcessingFactory): AudioProcessingController {
return customAudioProcessingFactory
}
@Provides
fun audioProcessingFactory(customAudioProcessingFactory: CustomAudioProcessingFactory): AudioProcessingFactory {
return customAudioProcessingFactory.getAudioProcessingFactory()
}
@Provides
fun videoDecoderFactory(
@Suppress("UNUSED_PARAMETER")
@Named(InjectionNames.LIB_WEBRTC_INITIALIZATION)
... ... @@ -246,9 +287,11 @@ internal object RTCModule {
@Named(InjectionNames.OVERRIDE_PEER_CONNECTION_FACTORY_OPTIONS)
peerConnectionFactoryOptions: PeerConnectionFactory.Options?,
memoryManager: CloseableManager,
audioProcessingFactory: AudioProcessingFactory?,
): PeerConnectionFactory {
return PeerConnectionFactory.builder()
.setAudioDeviceModule(audioDeviceModule)
.setAudioProcessingFactory(audioProcessingFactory)
.setVideoEncoderFactory(videoEncoderFactory)
.setVideoDecoderFactory(videoDecoderFactory)
.apply {
... ...
... ... @@ -31,6 +31,8 @@ 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.audio.AudioProcessingController
import io.livekit.android.audio.AudioProcessorOptions
import io.livekit.android.audio.CommunicationWorkaround
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.e2ee.E2EEManager
... ... @@ -63,7 +65,7 @@ constructor(
@Assisted private val context: Context,
private val engine: RTCEngine,
private val eglBase: EglBase,
private val localParticipantFactory: LocalParticipant.Factory,
localParticipantFactory: LocalParticipant.Factory,
private val defaultsManager: DefaultsManager,
@Named(InjectionNames.DISPATCHER_DEFAULT)
private val defaultDispatcher: CoroutineDispatcher,
... ... @@ -73,6 +75,7 @@ constructor(
private val closeableManager: CloseableManager,
private val e2EEManagerFactory: E2EEManager.Factory,
private val communicationWorkaround: CommunicationWorkaround,
val audioProcessingController: AudioProcessingController,
) : RTCEngine.Listener, ParticipantListener {
private lateinit var coroutineScope: CoroutineScope
... ... @@ -181,6 +184,11 @@ constructor(
var adaptiveStream: Boolean = false
/**
* audio processing is enabled
*/
var audioProcessorIsEnabled: Boolean = false
/**
* Dynamically pauses video layers that are not being consumed by any subscribers,
* significantly reducing publishing CPU and bandwidth usage.
*
... ... @@ -203,6 +211,11 @@ constructor(
var e2eeOptions: E2EEOptions? = null
/**
* @see external audio processing options
*/
var audioProcessorOptions: AudioProcessorOptions? = null
/**
* Default options to use when creating an audio track.
*/
var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.webrtc
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioProcessorInterface
import io.livekit.android.audio.AudioProcessorOptions
import livekit.org.webrtc.AudioProcessingFactory
import livekit.org.webrtc.ExternalAudioProcessingFactory
import java.nio.ByteBuffer
class CustomAudioProcessingFactory(private val audioProcessorOptions: AudioProcessorOptions) : AudioProcessingController {
private val externalAudioProcessor = ExternalAudioProcessingFactory()
init {
if (audioProcessorOptions.capturePostProcessor != null) {
setCapturePostProcessing(audioProcessorOptions.capturePostProcessor)
setBypassForCapturePostProcessing(audioProcessorOptions.capturePostBypass)
} else {
setCapturePostProcessing(null)
setBypassForCapturePostProcessing(false)
}
if (audioProcessorOptions.renderPreProcessor != null) {
setRenderPreProcessing(audioProcessorOptions.renderPreProcessor)
setBypassForRenderPreProcessing(audioProcessorOptions.renderPreBypass)
} else {
setRenderPreProcessing(null)
setBypassForRenderPreProcessing(false)
}
}
fun getAudioProcessingFactory(): AudioProcessingFactory {
return externalAudioProcessor
}
override fun setCapturePostProcessing(processing: AudioProcessorInterface?) {
externalAudioProcessor.setCapturePostProcessing(
processing.toAudioProcessing(),
)
}
override fun setBypassForCapturePostProcessing(bypass: Boolean) {
externalAudioProcessor.setBypassFlagForCapturePost(bypass)
}
override fun setRenderPreProcessing(processing: AudioProcessorInterface?) {
externalAudioProcessor.setRenderPreProcessing(
processing.toAudioProcessing(),
)
}
override fun setBypassForRenderPreProcessing(bypass: Boolean) {
externalAudioProcessor.setBypassFlagForRenderPre(bypass)
}
private class AudioProcessingBridge(
var audioProcessing: AudioProcessorInterface? = null,
) : ExternalAudioProcessingFactory.AudioProcessing {
override fun initialize(sampleRateHz: Int, numChannels: Int) {
audioProcessing?.initializeAudioProcessing(sampleRateHz, numChannels)
}
override fun reset(newRate: Int) {
audioProcessing?.resetAudioProcessing(newRate)
}
override fun process(numBands: Int, numFrames: Int, buffer: ByteBuffer?) {
audioProcessing?.processAudio(numBands, numFrames, buffer!!)
}
}
private fun AudioProcessorInterface?.toAudioProcessing(): ExternalAudioProcessingFactory.AudioProcessing {
return AudioProcessingBridge(this)
}
}
... ...
/*
* Copyright 2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.mock
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.audio.AudioProcessorInterface
class MockAudioProcessingController : AudioProcessingController {
override fun setCapturePostProcessing(processing: AudioProcessorInterface?) {
}
override fun setBypassForCapturePostProcessing(bypass: Boolean) {
}
override fun setRenderPreProcessing(processing: AudioProcessorInterface?) {
}
override fun setBypassForRenderPreProcessing(bypass: Boolean) {
}
}
... ...
... ... @@ -20,8 +20,10 @@ import android.content.Context
import android.javax.sdp.SdpFactory
import dagger.Module
import dagger.Provides
import io.livekit.android.audio.AudioProcessingController
import io.livekit.android.dagger.CapabilitiesGetter
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.mock.MockAudioProcessingController
import io.livekit.android.mock.MockEglBase
import livekit.org.webrtc.*
import javax.inject.Named
... ... @@ -40,6 +42,11 @@ object TestRTCModule {
fun eglContext(eglBase: EglBase): EglBase.Context = eglBase.eglBaseContext
@Provides
fun audioProcessingController(): AudioProcessingController {
return MockAudioProcessingController()
}
@Provides
@Singleton
fun peerConnectionFactory(
appContext: Context,
... ...
... ... @@ -101,6 +101,7 @@ class RoomTest {
closeableManager = CloseableManager(),
e2EEManagerFactory = e2EEManagerFactory,
communicationWorkaround = NoopCommunicationWorkaround(),
audioProcessingController = MockAudioProcessingController(),
)
}
... ...
... ... @@ -25,8 +25,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.github.ajalt.timberkt.Timber
import io.livekit.android.AudioOptions
import io.livekit.android.LiveKit
import io.livekit.android.LiveKitOverrides
import io.livekit.android.RoomOptions
import io.livekit.android.audio.AudioProcessorOptions
import io.livekit.android.audio.AudioSwitchHandler
import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.events.RoomEvent
... ... @@ -56,6 +59,7 @@ class CallViewModel(
application: Application,
val e2ee: Boolean = false,
val e2eeKey: String? = "",
val audioProcessorOptions: AudioProcessorOptions? = null,
) : AndroidViewModel(application) {
private fun getE2EEOptions(): E2EEOptions? {
... ... @@ -67,9 +71,20 @@ class CallViewModel(
return e2eeOptions
}
private fun getRoomOptions(): RoomOptions {
return RoomOptions(
adaptiveStream = true,
dynacast = true,
e2eeOptions = getE2EEOptions(),
)
}
val room = LiveKit.create(
appContext = application,
options = RoomOptions(adaptiveStream = true, dynacast = true),
options = getRoomOptions(),
overrides = LiveKitOverrides(
audioOptions = AudioOptions(audioProcessorOptions = audioProcessorOptions),
),
)
val audioHandler = room.audioHandler as AudioSwitchHandler
... ... @@ -103,6 +118,12 @@ class CallViewModel(
private val mutableScreencastEnabled = MutableLiveData(false)
val screenshareEnabled = mutableScreencastEnabled.hide()
private val mutableEnhancedNsEnabled = MutableLiveData(false)
val enhancedNsEnabled = mutableEnhancedNsEnabled.hide()
private val mutableEnableAudioProcessor = MutableLiveData(true)
val enableAudioProcessor = mutableEnableAudioProcessor.hide()
// Emits a string whenever a data message is received.
private val mutableDataReceived = MutableSharedFlow<String>()
val dataReceived = mutableDataReceived
... ... @@ -176,14 +197,36 @@ class CallViewModel(
}
}
fun toggleEnhancedNs(enabled: Boolean? = null) {
if (enabled != null) {
mutableEnableAudioProcessor.postValue(enabled)
room.audioProcessingController.setBypassForCapturePostProcessing(!enabled)
return
}
if (room.audioProcessorIsEnabled) {
if (enableAudioProcessor.value == true) {
room.audioProcessingController.setBypassForCapturePostProcessing(true)
mutableEnableAudioProcessor.postValue(false)
} else {
room.audioProcessingController.setBypassForCapturePostProcessing(false)
mutableEnableAudioProcessor.postValue(true)
}
}
}
private suspend fun connectToRoom() {
try {
room.e2eeOptions = getE2EEOptions()
room.audioProcessorOptions = audioProcessorOptions
room.connect(
url = url,
token = token,
)
mutableEnhancedNsEnabled.postValue(room.audioProcessorIsEnabled)
mutableEnableAudioProcessor.postValue(true)
// Create and publish audio/video tracks
val localParticipant = room.localParticipant
localParticipant.setMicrophoneEnabled(true)
... ...
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 4 14 L 4 18" />
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 8 12 L 8 20" />
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 12 7 L 12 25" />
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 16 14 L 16 18" />
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 20 10 L 20 22" />
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 24 7 L 24 25" />
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeMiterLimit="10"
android:strokeLineCap="round"
android:pathData="M 28 14 L 28 18" />
</vector>
... ...
/*
* Copyright 2023 LiveKit, Inc.
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -28,22 +28,55 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.ajalt.timberkt.Timber
import com.xwray.groupie.GroupieAdapter
import io.livekit.android.audio.AudioProcessorInterface
import io.livekit.android.audio.AudioProcessorOptions
import io.livekit.android.sample.common.R
import io.livekit.android.sample.databinding.CallActivityBinding
import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog
import io.livekit.android.sample.dialog.showDebugMenuDialog
import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
import kotlinx.coroutines.flow.collectLatest
import kotlinx.parcelize.Parcelize
import java.nio.ByteBuffer
class CallActivity : AppCompatActivity() {
val viewModel: CallViewModel by viewModelByFactory {
private val viewModel: CallViewModel by viewModelByFactory {
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
?: throw NullPointerException("args is null!")
CallViewModel(args.url, args.token, application, args.e2ee, args.e2eeKey)
val audioProcessor = object : AudioProcessorInterface {
override fun isEnabled(): Boolean {
Timber.d { "${getName()} isEnabled" }
return true
}
override fun getName(): String {
return "fake_noise_cancellation"
}
override fun initializeAudioProcessing(sampleRateHz: Int, numChannels: Int) {
Timber.d { "${getName()} initialize" }
}
override fun resetAudioProcessing(newRate: Int) {
Timber.d { "${getName()} reset" }
}
override fun processAudio(numBands: Int, numFrames: Int, buffer: ByteBuffer) {
Timber.d { "${getName()} process" }
}
}
val audioProcessorOptions = AudioProcessorOptions(
capturePostProcessor = audioProcessor,
)
CallViewModel(args.url, args.token, application, args.e2ee, args.e2eeKey, audioProcessorOptions)
}
lateinit var binding: CallActivityBinding
private lateinit var binding: CallActivityBinding
private val screenCaptureIntentLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
... ... @@ -146,6 +179,18 @@ class CallActivity : AppCompatActivity() {
.show()
}
viewModel.enhancedNsEnabled.observe(this) { enabled ->
binding.enhancedNs.visibility = if (enabled) {
android.view.View.VISIBLE
} else {
android.view.View.GONE
}
}
binding.enhancedNs.setOnClickListener {
showAudioProcessorSwitchDialog(viewModel)
}
binding.exit.setOnClickListener { finish() }
// Controls row 2
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.sample.dialog
import android.R
import android.app.Activity
import android.app.AlertDialog
import android.widget.ArrayAdapter
import io.livekit.android.sample.CallViewModel
fun Activity.showAudioProcessorSwitchDialog(callViewModel: CallViewModel) {
var name = callViewModel.audioProcessorOptions?.capturePostProcessor?.getName()
var enabled = if (callViewModel.enableAudioProcessor.value == true) "On" else "Off"
val builder = with(AlertDialog.Builder(this)) {
setTitle("AudioProcessor for mic: \n[$name] is $enabled")
val arrayAdapter = ArrayAdapter<String>(this@showAudioProcessorSwitchDialog, R.layout.select_dialog_item)
arrayAdapter.add("On")
arrayAdapter.add("Off")
setAdapter(arrayAdapter) { dialog, index ->
when (index) {
0 -> callViewModel.toggleEnhancedNs(true)
1 -> callViewModel.toggleEnhancedNs(false)
}
dialog.dismiss()
}
}
builder.show()
}
... ...
... ... @@ -81,6 +81,17 @@
app:tint="@android:color/white" />
<ImageView
android:id="@+id/enhanced_ns"
android:visibility="gone"
android:layout_width="@dimen/control_size"
android:layout_height="@dimen/control_size"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:padding="@dimen/control_padding"
android:src="@drawable/voice_wave_24"
app:tint="@android:color/white" />
<ImageView
android:id="@+id/exit"
android:layout_width="@dimen/control_size"
android:layout_height="@dimen/control_size"
... ...