Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions demo-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,6 @@ dependencies {
// Http
implementation(libs.okhttp)

implementation(libs.audioswitch)

// Logging
implementation(libs.okhttp.logging)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,45 @@ internal fun SettingsMenu(
is StreamAudioDevice.Speakerphone -> Icons.Default.SpeakerPhone
is StreamAudioDevice.WiredHeadset -> Icons.Default.HeadsetMic
}
// Compare devices by type and audioDeviceInfo ID (if available) since audio can be null when using custom audio switch
val selected = selectedMicroPhoneDevice
val isSelected = when {
selected == null -> false
else -> {
// First try to compare by audioDeviceInfo ID if both have it
val itInfoId = when (it) {
is StreamAudioDevice.BluetoothHeadset -> it.audioDeviceInfo?.id
is StreamAudioDevice.WiredHeadset -> it.audioDeviceInfo?.id
is StreamAudioDevice.Earpiece -> it.audioDeviceInfo?.id
is StreamAudioDevice.Speakerphone -> it.audioDeviceInfo?.id
}
val selectedInfoId = when (selected) {
is StreamAudioDevice.BluetoothHeadset -> selected.audioDeviceInfo?.id
is StreamAudioDevice.WiredHeadset -> selected.audioDeviceInfo?.id
is StreamAudioDevice.Earpiece -> selected.audioDeviceInfo?.id
is StreamAudioDevice.Speakerphone -> selected.audioDeviceInfo?.id
}

if (itInfoId != null && selectedInfoId != null) {
// Both have audioDeviceInfo, compare by ID
itInfoId == selectedInfoId
} else {
// Fall back to type comparison
when {
it is StreamAudioDevice.BluetoothHeadset && selected is StreamAudioDevice.BluetoothHeadset -> true
it is StreamAudioDevice.WiredHeadset && selected is StreamAudioDevice.WiredHeadset -> true
it is StreamAudioDevice.Earpiece && selected is StreamAudioDevice.Earpiece -> true
it is StreamAudioDevice.Speakerphone && selected is StreamAudioDevice.Speakerphone -> true
else -> false
}
}
}
}
AudioDeviceUiState(
it,
it.name,
icon,
it.audio.name == selectedMicroPhoneDevice?.audio?.name,
it.audioDeviceInfo?.id == selectedMicroPhoneDevice?.audioDeviceInfo?.id,
)
Comment on lines +219 to 258
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unused isSelected variable creates selection bug when audioDeviceInfo is null.

The isSelected variable (lines 221-252) implements proper fallback logic for when audioDeviceInfo is null, but it's never used. Line 257 uses a direct ID comparison that returns false when either device lacks an audioDeviceInfo, breaking selection highlighting for the Twilio AudioSwitch path where audioDeviceInfo can be null.

🔎 Proposed fix - use the computed isSelected
         AudioDeviceUiState(
             it,
             it.name,
             icon,
-            it.audioDeviceInfo?.id == selectedMicroPhoneDevice?.audioDeviceInfo?.id,
+            isSelected,
         )

}

Expand Down
3 changes: 0 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ coil = "2.6.0"
landscapist = "2.4.2"
accompanist = "0.34.0"
telephoto = "0.3.0"
audioswitch = "1.2.0"
libyuv = "0.36.0"

wire = "4.7.0"
Expand Down Expand Up @@ -139,8 +138,6 @@ accompanist-permission = { group = "com.google.accompanist", name = "accompanist
stream-video-android-noise-cancellation = { module = "io.getstream:stream-video-android-noise-cancellation", version.ref = "streamNoiseCancellation" }
telephoto = { group = "me.saket.telephoto", name = "zoomable", version.ref = "telephoto" }

audioswitch = { group = "com.twilio", name = "audioswitch", version.ref = "audioswitch"}

libyuv = { group = "io.github.crow-misia.libyuv", name = "libyuv-android", version.ref = "libyuv"}

wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version.ref = "wire" }
Expand Down
85 changes: 34 additions & 51 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -8503,85 +8503,79 @@ public abstract interface class io/getstream/video/android/core/api/SignalServer
}

public abstract interface class io/getstream/video/android/core/audio/AudioHandler {
public abstract fun selectDevice (Lio/getstream/video/android/core/audio/StreamAudioDevice;)V
public abstract fun start ()V
public abstract fun stop ()V
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check git history for AudioHandler
echo "=== Checking git history for AudioHandler.kt ==="
git log --oneline --all -- "**/AudioHandler.kt" 2>/dev/null | head -20 || echo "No git history found"

echo ""
echo "=== Searching for AudioHandler implementations ==="
rg "implements AudioHandler|: AudioHandler" -t kotlin -t java 2>/dev/null || echo "No implementations found"

echo ""
echo "=== Checking for AudioHandler interface definition ==="
fd "AudioHandler.kt" -x cat {} \;

Repository: GetStream/stream-video-android

Length of output: 6657


Correct API signature for selectDevice method—parameter should be nullable.

The selectDevice method in the Kotlin source (selectDevice(audioDevice: StreamAudioDevice?)) accepts a nullable parameter, but the API file shows it as non-nullable (Lio/getstream/video/android/core/audio/StreamAudioDevice;). Update the API file to match the actual implementation signature with the nullable marker.

🤖 Prompt for AI Agents
In stream-video-android-core/api/stream-video-android-core.api around line 8508,
the abstract method signature shows the parameter as non-nullable; update the
method parameter type to match the Kotlin implementation's nullable type by
changing the parameter descriptor to the nullable form (add the nullable marker
to the StreamAudioDevice type) so the API line reflects the actual signature
selectDevice(StreamAudioDevice?).

}

public final class io/getstream/video/android/core/audio/AudioSwitchHandler : io/getstream/video/android/core/audio/AudioHandler {
public static final field Companion Lio/getstream/video/android/core/audio/AudioSwitchHandler$Companion;
public fun <init> (Landroid/content/Context;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V
public final fun selectDevice (Lcom/twilio/audioswitch/AudioDevice;)V
public fun start ()V
public fun stop ()V
}

public final class io/getstream/video/android/core/audio/AudioSwitchHandler$Companion {
}

public abstract class io/getstream/video/android/core/audio/StreamAudioDevice {
public static final field Companion Lio/getstream/video/android/core/audio/StreamAudioDevice$Companion;
public static final fun fromAudio (Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/audio/StreamAudioDevice;
public abstract fun getAudio ()Lcom/twilio/audioswitch/AudioDevice;
public static final fun fromAudioDeviceInfo (Landroid/media/AudioDeviceInfo;)Lio/getstream/video/android/core/audio/StreamAudioDevice;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see several breaking changes. If we have decided to move forward with this approach, I suggest we draft a migration document to help developers transition to the newer APIs.

public abstract fun getAudioDeviceInfo ()Landroid/media/AudioDeviceInfo;
public abstract fun getName ()Ljava/lang/String;
public static final fun toAudioDevice (Lio/getstream/video/android/core/audio/StreamAudioDevice;)Lcom/twilio/audioswitch/AudioDevice;
public static final fun toAudioDeviceInfo (Lio/getstream/video/android/core/audio/StreamAudioDevice;Landroid/media/AudioManager;)Landroid/media/AudioDeviceInfo;
}

public final class io/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset : io/getstream/video/android/core/audio/StreamAudioDevice {
public fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)V
public synthetic fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> ()V
public fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)V
public synthetic fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lcom/twilio/audioswitch/AudioDevice;
public final fun copy (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset;Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset;
public final fun component2 ()Landroid/media/AudioDeviceInfo;
public final fun copy (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)Lio/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset;Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$BluetoothHeadset;
public fun equals (Ljava/lang/Object;)Z
public fun getAudio ()Lcom/twilio/audioswitch/AudioDevice;
public fun getAudioDeviceInfo ()Landroid/media/AudioDeviceInfo;
public fun getName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/audio/StreamAudioDevice$Companion {
public final fun fromAudio (Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/audio/StreamAudioDevice;
public final fun toAudioDevice (Lio/getstream/video/android/core/audio/StreamAudioDevice;)Lcom/twilio/audioswitch/AudioDevice;
public final fun fromAudioDeviceInfo (Landroid/media/AudioDeviceInfo;)Lio/getstream/video/android/core/audio/StreamAudioDevice;
public final fun toAudioDeviceInfo (Lio/getstream/video/android/core/audio/StreamAudioDevice;Landroid/media/AudioManager;)Landroid/media/AudioDeviceInfo;
}

public final class io/getstream/video/android/core/audio/StreamAudioDevice$Earpiece : io/getstream/video/android/core/audio/StreamAudioDevice {
public fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)V
public synthetic fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> ()V
public fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)V
public synthetic fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lcom/twilio/audioswitch/AudioDevice;
public final fun copy (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Earpiece;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$Earpiece;Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Earpiece;
public final fun component2 ()Landroid/media/AudioDeviceInfo;
public final fun copy (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Earpiece;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$Earpiece;Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Earpiece;
public fun equals (Ljava/lang/Object;)Z
public fun getAudio ()Lcom/twilio/audioswitch/AudioDevice;
public fun getAudioDeviceInfo ()Landroid/media/AudioDeviceInfo;
public fun getName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone : io/getstream/video/android/core/audio/StreamAudioDevice {
public fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)V
public synthetic fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> ()V
public fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)V
public synthetic fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lcom/twilio/audioswitch/AudioDevice;
public final fun copy (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone;Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone;
public final fun component2 ()Landroid/media/AudioDeviceInfo;
public final fun copy (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone;Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$Speakerphone;
public fun equals (Ljava/lang/Object;)Z
public fun getAudio ()Lcom/twilio/audioswitch/AudioDevice;
public fun getAudioDeviceInfo ()Landroid/media/AudioDeviceInfo;
public fun getName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset : io/getstream/video/android/core/audio/StreamAudioDevice {
public fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)V
public synthetic fun <init> (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> ()V
public fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)V
public synthetic fun <init> (Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lcom/twilio/audioswitch/AudioDevice;
public final fun copy (Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset;Ljava/lang/String;Lcom/twilio/audioswitch/AudioDevice;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset;
public final fun component2 ()Landroid/media/AudioDeviceInfo;
public final fun copy (Ljava/lang/String;Landroid/media/AudioDeviceInfo;)Lio/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset;
public static synthetic fun copy$default (Lio/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset;Ljava/lang/String;Landroid/media/AudioDeviceInfo;ILjava/lang/Object;)Lio/getstream/video/android/core/audio/StreamAudioDevice$WiredHeadset;
public fun equals (Ljava/lang/Object;)Z
public fun getAudio ()Lcom/twilio/audioswitch/AudioDevice;
public fun getAudioDeviceInfo ()Landroid/media/AudioDeviceInfo;
public fun getName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -8812,17 +8806,6 @@ public final class io/getstream/video/android/core/call/state/Reaction : io/gets
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/call/state/SelectAudioDevice : io/getstream/video/android/core/call/state/CallAction {
public fun <init> (Lcom/twilio/audioswitch/AudioDevice;)V
public final fun component1 ()Lcom/twilio/audioswitch/AudioDevice;
public final fun copy (Lcom/twilio/audioswitch/AudioDevice;)Lio/getstream/video/android/core/call/state/SelectAudioDevice;
public static synthetic fun copy$default (Lio/getstream/video/android/core/call/state/SelectAudioDevice;Lcom/twilio/audioswitch/AudioDevice;ILjava/lang/Object;)Lio/getstream/video/android/core/call/state/SelectAudioDevice;
public fun equals (Ljava/lang/Object;)Z
public final fun getAudioDevice ()Lcom/twilio/audioswitch/AudioDevice;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/call/state/Settings : io/getstream/video/android/core/call/state/CallAction {
public fun <init> (Z)V
public final fun component1 ()Z
Expand Down
2 changes: 0 additions & 2 deletions stream-video-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ dependencies {
// webrtc
api(libs.stream.webrtc)

implementation(libs.audioswitch)

// video filter dependencies
implementation(libs.libyuv)

Expand Down
4 changes: 3 additions & 1 deletion stream-video-android-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />-->
Comment on lines +29 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing BLUETOOTH_CONNECT permission for API 31+.

The comment states that BLUETOOTH_CONNECT is required for API 31+, but the permission is commented out. On Android 12+ (API 31+), Bluetooth audio device enumeration and connection require the BLUETOOTH_CONNECT runtime permission. Without declaring it, the app cannot request this permission at runtime, effectively breaking Bluetooth audio functionality on modern Android versions.

🔎 Proposed fix
     <!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
     <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
-<!--    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />-->
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />-->
<!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
🤖 Prompt for AI Agents
In stream-video-android-core/src/main/AndroidManifest.xml around lines 29 to 31
the BLUETOOTH_CONNECT permission is commented out which prevents
declaring/requesting the runtime permission required on Android 12+ (API 31+)
for Bluetooth enumeration/connection; uncomment or add a uses-permission entry
for android.permission.BLUETOOTH_CONNECT (no maxSdkVersion) so the app can
request this runtime permission on API 31+ and retain the existing BLUETOOTH
entry with android:maxSdkVersion="30" for older APIs.

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,13 @@ public class Call(
testInstanceProvider.mediaManagerCreator!!.invoke()
} else {
MediaManagerImpl(
clientImpl.context,
this,
scope,
eglBase.eglBaseContext,
clientImpl.callServiceConfigRegistry.get(type).audioUsage,
) { clientImpl.callServiceConfigRegistry.get(type).audioUsage }
context = clientImpl.context,
call = this,
scope = scope,
eglBaseContext = eglBase.eglBaseContext,
audioUsage = clientImpl.callServiceConfigRegistry.get(type).audioUsage,
audioUsageProvider = { clientImpl.callServiceConfigRegistry.get(type).audioUsage },
)
}
}

Expand Down Expand Up @@ -1351,7 +1352,9 @@ public class Call(
private fun monitorHeadset() {
microphone.devices.onEach { availableDevices ->
logger.d {
"[monitorHeadset] new available devices, prev selected: ${microphone.nonHeadsetFallbackDevice}"
"[monitorHeadset] new available devices, prev selected: ${
microphone.nonHeadsetFallbackDevice
}"
}

val bluetoothHeadset =
Expand Down
Loading
Loading