Skip to content

Commit

Permalink
[PBE-3853] Support noise cancellation (#1196)
Browse files Browse the repository at this point in the history
* new codegen

* Fix wrong tutorials

* add audio filter interface & UI

* fix-lint

* add noise_cancellatiom impl

* extract stream-video-webrtc-noise-cancellation

* impl NoiseCancellationFactory

* init m_model_path

* add utils

* log model passing

* log model passing

* store std:string

* add new util func

* store std:wstring

* cleanup

* create session

* process frames

* add missing functions

* support NC enable/disable

* resolve dependencies

* rename lib module

* improve logs

* remove logs

* clean up logs, extract destroyAll

* update webrtc lib

* [PBE-5300] migrate to m118.6

* fix spotless

* remove experimenting nc module

* use aar files

* use webrtc-android 1.2.0

* use webrtc-android 1.2.1

* add docs

* update docs

---------

Co-authored-by: Jaewoong Eum <[email protected]>
Co-authored-by: Tommaso Barbugli <[email protected]>
Co-authored-by: Thierry Schellenbach <[email protected]>
Co-authored-by: skydoves <[email protected]>
Co-authored-by: Aleksandar Apostolov <[email protected]>
  • Loading branch information
6 people authored Oct 9, 2024
1 parent 1f71ddb commit 8329e59
Show file tree
Hide file tree
Showing 26 changed files with 378 additions and 28 deletions.
3 changes: 3 additions & 0 deletions demo-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ dependencies {
implementation(project(":stream-video-android-filters-video"))
compileOnly(project(":stream-video-android-previewdata"))

// Noise Cancellation
implementation(libs.stream.video.android.noise.cancellation)

// Stream Chat SDK
implementation(libs.stream.chat.compose)
implementation(libs.stream.chat.offline)
Expand Down
1 change: 1 addition & 0 deletions demo-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Dogfooding"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import io.getstream.video.android.compose.ui.components.call.renderer.copy
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.RealtimeConnection
import io.getstream.video.android.core.call.state.ChooseLayout
import io.getstream.video.android.core.utils.isEnabled
import io.getstream.video.android.filters.video.BlurredBackgroundVideoFilter
import io.getstream.video.android.filters.video.VirtualBackgroundVideoFilter
import io.getstream.video.android.mock.StreamPreviewDataUtils
Expand Down Expand Up @@ -458,10 +459,17 @@ fun CallScreen(
}

if (isShowingSettingMenu) {
var isNoiseCancellationEnabled by remember {
mutableStateOf(call.isAudioProcessingEnabled())
}
val settings by call.state.settings.collectAsStateWithLifecycle()
val noiseCancellationFeatureEnabled = settings?.audio?.noiseCancellation?.isEnabled == true
SettingsMenu(
call = call,
selectedVideoFilter = selectedVideoFilter,
showDebugOptions = showDebugOptions,
noiseCancellationFeatureEnabled = noiseCancellationFeatureEnabled,
noiseCancellationEnabled = isNoiseCancellationEnabled,
onDismissed = { isShowingSettingMenu = false },
onSelectVideoFilter = { filterIndex ->
selectedVideoFilter = filterIndex
Expand All @@ -482,6 +490,9 @@ fun CallScreen(
isShowingSettingMenu = false
isShowingFeedbackDialog = true
},
onNoiseCancellation = {
isNoiseCancellationEnabled = call.toggleAudioProcessing()
},
) {
isShowingStats = true
isShowingSettingMenu = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.getstream.chat.android.client.ChatClient
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.DeviceStatus
import io.getstream.video.android.core.StreamVideo
import io.getstream.video.android.core.utils.isAutoOn
import io.getstream.video.android.datastore.delegate.StreamUserDataStore
import io.getstream.video.android.model.StreamCallId
import io.getstream.video.android.model.User
Expand Down Expand Up @@ -116,7 +117,7 @@ class CallLobbyViewModel @Inject constructor(
// based on it
val settings = call.state.settings.first { it != null }

val enabled = when (call.camera.status.first()) {
val isCameraEnabled = when (call.camera.status.first()) {
is DeviceStatus.NotSelected -> {
settings?.video?.cameraDefaultOn ?: false
}
Expand All @@ -131,7 +132,10 @@ class CallLobbyViewModel @Inject constructor(
}

// enable/disable camera capture (no preview would be visible otherwise)
call.camera.setEnabled(enabled)
call.camera.setEnabled(isCameraEnabled)

val isNoiseCancellationEnabled = settings?.audio?.noiseCancellation?.isAutoOn ?: false
call.setAudioProcessingEnabled(isNoiseCancellationEnabled)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.HeadsetMic
import androidx.compose.material.icons.filled.PortableWifiOff
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material.icons.filled.SettingsVoice
import androidx.compose.material.icons.filled.SpatialAudioOff
import androidx.compose.material.icons.filled.SpeakerPhone
import androidx.compose.material.icons.filled.SwitchLeft
import androidx.compose.material.icons.filled.VideoFile
Expand All @@ -46,6 +47,8 @@ import io.getstream.video.android.ui.menu.base.SubMenuItem
*/
fun defaultStreamMenu(
showDebugOptions: Boolean = false,
noiseCancellationFeatureEnabled: Boolean = false,
noiseCancellationEnabled: Boolean = false,
codecList: List<MediaCodecInfo>,
onCodecSelected: (MediaCodecInfo) -> Unit,
isScreenShareEnabled: Boolean,
Expand All @@ -57,6 +60,7 @@ fun defaultStreamMenu(
onKillSfuWsClick: () -> Unit,
onSwitchSfuClick: () -> Unit,
onShowFeedback: () -> Unit,
onNoiseCancellation: () -> Unit,
onDeviceSelected: (StreamAudioDevice) -> Unit,
availableDevices: List<StreamAudioDevice>,
loadRecordings: suspend () -> List<MenuItem>,
Expand Down Expand Up @@ -108,6 +112,16 @@ fun defaultStreamMenu(
action = onToggleScreenShare,
),
)
if (noiseCancellationFeatureEnabled) {
add(
ActionMenuItem(
title = "Noise cancellation",
icon = Icons.Default.SpatialAudioOff,
highlight = noiseCancellationEnabled,
action = onNoiseCancellation,
),
)
}
if (showDebugOptions) {
add(
SubMenuItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import io.getstream.video.android.compose.theme.VideoTheme
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.call.audio.AudioFilter
import io.getstream.video.android.core.call.audio.InputAudioFilter
import io.getstream.video.android.core.mapper.ReactionMapper
import io.getstream.video.android.tooling.extensions.toPx
import io.getstream.video.android.ui.call.ReactionsMenu
Expand All @@ -68,9 +68,12 @@ internal fun SettingsMenu(
call: Call,
selectedVideoFilter: Int,
showDebugOptions: Boolean,
noiseCancellationFeatureEnabled: Boolean,
noiseCancellationEnabled: Boolean,
onDismissed: () -> Unit,
onSelectVideoFilter: (Int) -> Unit,
onShowFeedback: () -> Unit,
onNoiseCancellation: () -> Unit,
onShowCallStats: () -> Unit,
) {
val context = LocalContext.current
Expand Down Expand Up @@ -104,7 +107,7 @@ internal fun SettingsMenu(

val onToggleAudioFilterClick: () -> Unit = {
if (call.audioFilter == null) {
call.audioFilter = object : AudioFilter {
call.audioFilter = object : InputAudioFilter {
override fun applyFilter(
audioFormat: Int,
channelCount: Int,
Expand Down Expand Up @@ -206,6 +209,8 @@ internal fun SettingsMenu(
},
items = defaultStreamMenu(
showDebugOptions = showDebugOptions,
noiseCancellationFeatureEnabled = noiseCancellationFeatureEnabled,
noiseCancellationEnabled = noiseCancellationEnabled,
codecList = codecInfos,
availableDevices = availableDevices,
onDeviceSelected = {
Expand All @@ -223,6 +228,7 @@ internal fun SettingsMenu(
onToggleAudioFilterClick = onToggleAudioFilterClick,
onSwitchSfuClick = onSwitchSfuClick,
onShowCallStats = onShowCallStats,
onNoiseCancellation = onNoiseCancellation,
isScreenShareEnabled = isScreenSharing,
loadRecordings = onLoadRecordings,
),
Expand Down Expand Up @@ -284,6 +290,7 @@ private fun SettingsMenuPreview() {
availableDevices = emptyList(),
onDeviceSelected = {},
onShowFeedback = {},
onNoiseCancellation = {},
loadRecordings = { emptyList() },
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ private fun DynamicMenuPreview() {
availableDevices = emptyList(),
onDeviceSelected = {},
onShowFeedback = {},
onNoiseCancellation = {},
loadRecordings = { emptyList() },
),
)
Expand All @@ -248,6 +249,7 @@ private fun DynamicMenuDebugOptionPreview() {
availableDevices = emptyList(),
onDeviceSelected = {},
onShowFeedback = {},
onNoiseCancellation = {},
loadRecordings = { emptyList() },
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.getstream.video.android.data.services.stream.StreamService
import io.getstream.video.android.datastore.delegate.StreamUserDataStore
import io.getstream.video.android.model.ApiKey
import io.getstream.video.android.model.User
import io.getstream.video.android.noise.cancellation.NoiseCancellation
import io.getstream.video.android.util.config.AppConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -207,6 +208,7 @@ object StreamVideoInitHelper {
authData.token
},
appName = "Stream Video Demo App",
audioProcessing = NoiseCancellation(context),
).build()
}
}
25 changes: 25 additions & 0 deletions demo-app/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2024 Stream.io Inc. All rights reserved.
Licensed under the Stream License;
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/GetStream/stream-video-android/blob/main/LICENSE
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.
-->
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
<debug-overrides>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>
</network-security-config>
6 changes: 3 additions & 3 deletions docusaurus/docs/Android/02-tutorials/03-livestream.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,10 @@ It also went into more details about HLS & RTMP-in.
There are several advanced features that can improve the livestreaming experience:

* ** [Co-hosts](../03-guides/02-joining-creating-calls.mdx) ** You can add members to your livestream with elevated permissions. So you can have co-hosts, moderators etc.
* ** [Custom events](../03-guides/09-reactions-and-custom-events.mdx) ** You can use custom events on the call to share any additional data. Think about showing the score for a game, or any other realtime use case.
* ** [Reactions & Chat](../03-guides/09-reactions-and-custom-events.mdx) ** Users can react to the livestream, and you can add chat. This makes for a more engaging experience.
* ** [Custom events](../03-guides/10-reactions-and-custom-events.mdx) ** You can use custom events on the call to share any additional data. Think about showing the score for a game, or any other realtime use case.
* ** [Reactions & Chat](../03-guides/10-reactions-and-custom-events.mdx) ** Users can react to the livestream, and you can add chat. This makes for a more engaging experience.
* ** [Notifications](../06-advanced/01-ringing.mdx) ** You can notify users via push notifications when the livestream starts
* ** [Recording](../06-advanced/06-recording.mdx) ** The call recording functionality allows you to record the call with various options and layouts
* ** [Recording](../06-advanced/09-recording.mdx) ** The call recording functionality allows you to record the call with various options and layouts

### Recap

Expand Down
139 changes: 139 additions & 0 deletions docusaurus/docs/Android/03-guides/05-noise-cancellation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: Noise Cancellation
description: How to implement noise cancellation in Stream Video Android SDK
---

Noise Cancellation capabilities of our [Android Video SDK](https://github.com/GetStream/stream-video-android) can be enabled by installing our [NoiseCancellation](https://central.sonatype.com/artifact/io.getstream/stream-video-android-noise-cancellation/overview) package. Under the hood, this package uses the technology developed by [krisp.ai](https://krisp.ai/).

## Installation

### Add the library to your project

To add the Stream Video Noise Cancellation library, open your app's `build.gradle.kts` file and add the following dependency:

```kotlin
dependencies {
implementation("io.getstream:stream-video-android-noise-cancellation:1.0.1")
}
```

Make sure to replace `1.0.1` with the latest version of the noise cancellation library.

## Integration

Our Android SDK provides a utility component that makes the integration smoother. You'll need to create a `NoiseCancellation` instance and pass it to the `StreamVideoBuilder` when initializing the SDK.
```kotlin
import io.getstream.video.android.core.StreamVideoBuilder
import io.getstream.video.android.noise.cancellation.NoiseCancellation

// ...

val noiseCancellation = NoiseCancellation(context)
val streamVideo = StreamVideoBuilder(
context = context,
apiKey = apiKey,
user = user,
token = token,
// ... other configuration options
audioProcessing = noiseCancellation
).build()

// ...
```

## Feature availability

The availability of noise cancellation is controlled by the call settings. You can check the availability and status of noise cancellation through the `Call` object:

```kotlin
val call: Call = // ... obtain your call object
val noiseCancellationMode = call.state.settings.value?.audio?.noiseCancellation?.mode
```

There are three possible modes for noise cancellation:

### Available

```kotlin
if (noiseCancellationMode == NoiseCancellationSettings.Mode.Available) {
// The feature is enabled on the dashboard and available for the call
// You can present noise cancellation toggle UI in your application
}
```

The feature has been enabled on the dashboard and it's available for the call. In this case, you are free to present any noise cancellation toggle UI in your application.

:::info
Even though the feature may be enabled for your call, you should note that NoiseCancellation is a very performance-heavy process. For that reason, it's recommended to only allow the feature on devices with sufficient processing power.

While there isn't a definitive way to determine if a device can handle noise cancellation efficiently, you can use the following method to check for advanced audio processing capabilities:

```kotlin
import android.content.pm.PackageManager

val context: Context = // ... obtain your context
val hasAdvancedAudioProcessing = context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)
```

This can serve as an indicator of whether the device might be capable of handling noise cancellation efficiently. Devices with this feature are more likely to have the necessary hardware to support performance-intensive audio processing tasks.

For the most accurate assessment of noise cancellation performance, you may want to consider implementing your own benchmarking or testing mechanism on different device models.
:::

For more info, you can refer to our UI docs about Noise Cancellation.

### Disabled

````kotlin
if (noiseCancellationMode == NoiseCancellationSettings.Mode.Disabled) {
// The feature is not enabled on the dashboard or not available for the call
// You should hide any noise cancellation toggle UI in your application
}
````

The feature hasn't been enabled on the dashboard or the feature isn't available for the call. In this case, you should hide any noise cancellation toggle UI in your application.

### AutoOn

````kotlin
if (noiseCancellationMode == NoiseCancellationSettings.Mode.AutoOn) {
// Noise cancellation is automatically enabled
}
````

Similar to `Available` with the difference that if possible, the StreamVideo SDK will enable the filter automatically, when the user joins the call.

:::note
The requirements for `AutoOn` to work properly are:

1. A `NoiseCancellation` instance provided when you initialize StreamVideo:
```kotlin
val noiseCancellation = NoiseCancellation(context)
val streamVideo = StreamVideoBuilder(
// ... other parameters
audioProcessing = noiseCancellation
).build()
```
2. Device has sufficient processing power (you can use the `FEATURE_AUDIO_PRO` check as an indicator)
:::


## Activate/Deactivate the filter

To toggle noise cancellation during a call, you can use the `toggleAudioProcessing()` method on the `StreamVideo` instance:

```kotlin
val streamVideo: StreamVideo = // ... obtain your StreamVideo instance

// Check if audio processing (noise cancellation) is enabled
val isAudioProcessingEnabled = streamVideo.isAudioProcessingEnabled()

// Toggle noise cancellation
val isEnabled = streamVideo.toggleAudioProcessing()

// Or using the setAudioProcessingEnabled method
streamVideo.setAudioProcessingEnabled(!isAudioProcessingEnabled)
```

Note that toggling noise cancellation affects all ongoing and future calls for the current `StreamVideo` instance.

Loading

0 comments on commit 8329e59

Please sign in to comment.