Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PBE-3853] Support noise cancellation #1196

Merged
merged 89 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
0517fc9
Merge pull request #549 from GetStream/develop
skydoves Jun 22, 2023
fe80252
new codegen
tbarbugli Jun 22, 2023
2937415
Merge branch 'main' of github.com:GetStream/stream-video-android
tbarbugli Jun 22, 2023
63bd04d
Merge branch 'develop'
tschellenbach Jun 22, 2023
d769d23
Merge pull request #554 from GetStream/develop
skydoves Jun 23, 2023
041a0d7
Merge pull request #557 from GetStream/develop
skydoves Jun 26, 2023
f5011b6
Merge pull request #563 from GetStream/develop
skydoves Jun 28, 2023
8f83f73
Merge pull request #573 from GetStream/develop
skydoves Jun 28, 2023
d616bc1
Merge pull request #577 from GetStream/develop
skydoves Jun 28, 2023
255b5e1
Merge pull request #579 from GetStream/develop
skydoves Jun 28, 2023
e132d5c
Merge pull request #583 from GetStream/develop
skydoves Jun 30, 2023
5b679e7
Merge branch 'develop'
skydoves Jul 3, 2023
f857e09
Merge branch 'main' of https://github.com/GetStream/stream-video-android
skydoves Jul 3, 2023
145e038
Merge branch 'develop'
skydoves Jul 4, 2023
8ef0ebd
Merge branch 'develop'
skydoves Jul 4, 2023
41d67dd
Merge branch 'develop'
skydoves Jul 4, 2023
2fe4cdc
Merge branch 'develop'
skydoves Jul 21, 2023
fc708bf
Merge branch 'develop' of github.com:GetStream/stream-video-android
tschellenbach Jul 31, 2023
da0219b
Merge branch 'develop'
skydoves Aug 1, 2023
28da1a9
Merge branch 'develop'
tschellenbach Aug 4, 2023
e23a614
Merge branch 'develop'
skydoves Aug 4, 2023
aa119ed
Merge branch 'develop'
skydoves Aug 7, 2023
8d9e791
Merge branch 'develop'
skydoves Aug 8, 2023
9064627
Merge branch 'develop'
skydoves Aug 14, 2023
073275b
Merge branch 'develop'
skydoves Aug 14, 2023
c90fcfd
Merge branch 'develop'
skydoves Aug 22, 2023
06aaf50
Merge branch 'develop'
skydoves Aug 24, 2023
5428fcc
Merge branch 'develop'
skydoves Aug 25, 2023
ebcdea7
Merge branch 'develop'
skydoves Sep 5, 2023
8c67f3e
Merge branch 'develop'
skydoves Sep 5, 2023
6a22bad
Merge branch 'develop'
skydoves Sep 21, 2023
91dd83f
Merge branch 'develop'
skydoves Sep 21, 2023
f779467
Merge branch 'develop'
skydoves Sep 25, 2023
14d3085
Merge branch 'develop'
skydoves Oct 24, 2023
281a6ef
Merge branch 'develop'
skydoves Oct 31, 2023
898fdf0
Merge branch 'develop'
skydoves Oct 31, 2023
b1daecb
Merge branch 'develop'
skydoves Oct 31, 2023
877f0f4
Fix wrong tutorials
skydoves Nov 22, 2023
4468d6d
Merge branch 'develop' of https://github.com/getstream/stream-video-a…
skydoves Nov 27, 2023
835f917
Merge branch 'develop' of https://github.com/getstream/stream-video-a…
skydoves Nov 27, 2023
84afafc
Merge branch 'develop' of https://github.com/getstream/stream-video-a…
skydoves Nov 28, 2023
ae2ac24
Merge branch 'develop'
skydoves Nov 28, 2023
8d0ce23
Merge branch 'develop'
skydoves Dec 12, 2023
c72d96b
Merge branch 'develop'
skydoves Dec 12, 2023
39106b8
Merge branch 'develop'
skydoves Jan 18, 2024
5c952f7
Merge branch 'develop'
skydoves Jan 31, 2024
711234d
Merge branch 'develop'
skydoves Feb 26, 2024
66b931b
Merge branch 'develop'
aleksandar-apostolov Mar 15, 2024
9b1dcc5
Merge branch 'develop'
aleksandar-apostolov Mar 19, 2024
0cdb6e5
Merge branch 'develop'
aleksandar-apostolov Mar 19, 2024
591ee86
Merge branch 'develop'
aleksandar-apostolov Apr 1, 2024
55fc028
Merge branch 'develop'
aleksandar-apostolov Apr 12, 2024
78317b1
add audio filter interface & UI
kanat Apr 15, 2024
7e75161
Merge branch 'develop' into feature/audio-filter
kanat Aug 12, 2024
ff5cdb0
fix-lint
kanat Aug 12, 2024
696470e
add noise_cancellatiom impl
kanat Aug 22, 2024
f2bfb6f
Merge remote-tracking branch 'origin/PBE-6044-device-switch-improveme…
kanat Sep 23, 2024
07811ee
extract stream-video-webrtc-noise-cancellation
kanat Sep 25, 2024
f9beabc
impl NoiseCancellationFactory
kanat Sep 25, 2024
c361719
init m_model_path
kanat Sep 25, 2024
7dfa46a
add utils
kanat Sep 25, 2024
8df6d06
log model passing
kanat Sep 25, 2024
accb972
log model passing
kanat Sep 25, 2024
1a8114e
store std:string
kanat Sep 25, 2024
2526022
add new util func
kanat Sep 25, 2024
1443826
store std:wstring
kanat Sep 25, 2024
0c655dd
cleanup
kanat Sep 25, 2024
aeeeb44
create session
kanat Sep 25, 2024
09aca0c
process frames
kanat Sep 25, 2024
c10a9b5
add missing functions
kanat Sep 27, 2024
6baaa01
support NC enable/disable
kanat Sep 27, 2024
b7b526a
resolve dependencies
kanat Sep 27, 2024
7bfb25e
rename lib module
kanat Sep 27, 2024
1dc6b5d
improve logs
kanat Sep 27, 2024
c51ff3a
remove logs
kanat Sep 27, 2024
ca530ce
clean up logs, extract destroyAll
kanat Sep 28, 2024
af7421c
update webrtc lib
kanat Sep 28, 2024
71844ff
[PBE-5300] migrate to m118.6
kanat Oct 2, 2024
f9c331a
fix spotless
kanat Oct 2, 2024
f56eb73
Merge branch 'develop' into feature/audio-filter
kanat Oct 2, 2024
cea7c0f
remove experimenting nc module
kanat Oct 2, 2024
141ea5f
Merge branch 'feature/audio-filter' of github.com:GetStream/stream-vi…
kanat Oct 2, 2024
b0377d3
use aar files
kanat Oct 2, 2024
dbb3687
Merge branch 'develop' into feature/audio-filter
kanat Oct 7, 2024
f653f5f
use webrtc-android 1.2.0
kanat Oct 7, 2024
8a947bf
use webrtc-android 1.2.1
kanat Oct 8, 2024
da9433a
add docs
kanat Oct 8, 2024
f6ea652
update docs
kanat Oct 8, 2024
24dcc4c
Merge branch 'develop' into feature/audio-filter
aleksandar-apostolov Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
kanat marked this conversation as resolved.
Show resolved Hide resolved
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
Loading