diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index b80732107c..980354ff43 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -409,10 +409,12 @@ public final class io/getstream/video/android/core/LocalStats { } public final class io/getstream/video/android/core/MediaManagerImpl { - public fun (Landroid/content/Context;Lio/getstream/video/android/core/Call;Lkotlinx/coroutines/CoroutineScope;Lorg/webrtc/EglBase$Context;)V + public fun (Landroid/content/Context;Lio/getstream/video/android/core/Call;Lkotlinx/coroutines/CoroutineScope;Lorg/webrtc/EglBase$Context;I)V + public synthetic fun (Landroid/content/Context;Lio/getstream/video/android/core/Call;Lkotlinx/coroutines/CoroutineScope;Lorg/webrtc/EglBase$Context;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun cleanup ()V public final fun getAudioSource ()Lorg/webrtc/AudioSource; public final fun getAudioTrack ()Lorg/webrtc/AudioTrack; + public final fun getAudioUsage ()I public final fun getCall ()Lio/getstream/video/android/core/Call; public final fun getContext ()Landroid/content/Context; public final fun getEglBaseContext ()Lorg/webrtc/EglBase$Context; @@ -476,10 +478,12 @@ public final class io/getstream/video/android/core/MemberState { } public final class io/getstream/video/android/core/MicrophoneManager { - public fun (Lio/getstream/video/android/core/MediaManagerImpl;Z)V + public fun (Lio/getstream/video/android/core/MediaManagerImpl;ZI)V + public final fun canHandleDeviceSwitch ()Z public final fun cleanup ()V public final fun disable (Z)V public static synthetic fun disable$default (Lio/getstream/video/android/core/MicrophoneManager;ZILjava/lang/Object;)V + public final fun getAudioUsage ()I public final fun getDevices ()Lkotlinx/coroutines/flow/StateFlow; public final fun getMediaManager ()Lio/getstream/video/android/core/MediaManagerImpl; public final fun getPreferSpeakerphone ()Z @@ -817,7 +821,8 @@ public final class io/getstream/video/android/core/StreamVideoBuilder { public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;)V public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;Z)V public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;)V - public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;I)V + public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun build ()Lio/getstream/video/android/core/StreamVideo; public final fun getScope ()Lkotlinx/coroutines/CoroutineScope; } @@ -1024,7 +1029,8 @@ public final class io/getstream/video/android/core/call/connection/StreamPeerCon } public final class io/getstream/video/android/core/call/connection/StreamPeerConnectionFactory { - public fun (Landroid/content/Context;)V + public fun (Landroid/content/Context;I)V + public synthetic fun (Landroid/content/Context;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getEglBase ()Lorg/webrtc/EglBase; public final fun makeAudioSource (Lorg/webrtc/MediaConstraints;)Lorg/webrtc/AudioSource; public static synthetic fun makeAudioSource$default (Lio/getstream/video/android/core/call/connection/StreamPeerConnectionFactory;Lorg/webrtc/MediaConstraints;ILjava/lang/Object;)Lorg/webrtc/AudioSource; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 96ff91ab5f..d83f433d37 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -214,6 +214,7 @@ public class Call( this, scope, clientImpl.peerConnectionFactory.eglBase.eglBaseContext, + clientImpl.audioUsage, ) } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt index 066aa90d6c..1bcc227d30 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt @@ -30,6 +30,7 @@ import android.os.IBinder import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import io.getstream.log.taggedLogger +import io.getstream.video.android.core.audio.AudioHandler import io.getstream.video.android.core.audio.AudioSwitchHandler import io.getstream.video.android.core.audio.StreamAudioDevice import io.getstream.video.android.core.audio.StreamAudioDevice.Companion.fromAudio @@ -328,11 +329,12 @@ class ScreenShareManager( class MicrophoneManager( val mediaManager: MediaManagerImpl, val preferSpeakerphone: Boolean, + val audioUsage: Int, ) { // Internal data private val logger by taggedLogger("Media:MicrophoneManager") - private lateinit var audioHandler: AudioSwitchHandler + private lateinit var audioHandler: AudioHandler private var setupCompleted: Boolean = false internal var audioManager: AudioManager? = null internal var priorStatus: DeviceStatus? = null @@ -434,6 +436,8 @@ class MicrophoneManager( setupCompleted = false } + fun canHandleDeviceSwitch() = audioUsage != AudioAttributes.USAGE_MEDIA + // Internal logic internal fun setup() { if (setupCompleted) { @@ -445,14 +449,18 @@ class MicrophoneManager( audioManager?.allowedCapturePolicy = AudioAttributes.ALLOW_CAPTURE_BY_ALL } - audioHandler = - AudioSwitchHandler(mediaManager.context, preferSpeakerphone) { devices, selected -> - logger.i { "audio devices. selected $selected, available devices are $devices" } - _devices.value = devices.map { it.fromAudio() } - _selectedDevice.value = selected?.fromAudio() - } + if (canHandleDeviceSwitch()) { + audioHandler = + AudioSwitchHandler(mediaManager.context, preferSpeakerphone) { devices, selected -> + logger.i { "audio devices. selected $selected, available devices are $devices" } + _devices.value = devices.map { it.fromAudio() } + _selectedDevice.value = selected?.fromAudio() + } - audioHandler.start() + audioHandler.start() + } else { + logger.d { "[MediaManager#setup] usage is MEDIA, cannot handle device switch" } + } setupCompleted = true } @@ -463,7 +471,7 @@ class MicrophoneManager( private fun ifAudioHandlerInitialized(then: (audioHandler: AudioSwitchHandler) -> Unit) { if (this::audioHandler.isInitialized) { - then(this.audioHandler) + then(this.audioHandler as AudioSwitchHandler) } else { logger.e { "Audio handler not initialized. Ensure calling setup(), before using the handler." } } @@ -802,6 +810,7 @@ class MediaManagerImpl( val call: Call, val scope: CoroutineScope, val eglBaseContext: EglBase.Context, + val audioUsage: Int = defaultAudioUsage, ) { private val filterVideoProcessor = FilterVideoProcessor({ call.videoFilter }, { camera.surfaceTextureHelper }) @@ -838,7 +847,7 @@ class MediaManagerImpl( ) internal val camera = CameraManager(this, eglBaseContext) - internal val microphone = MicrophoneManager(this, preferSpeakerphone = true) + internal val microphone = MicrophoneManager(this, preferSpeakerphone = true, audioUsage) internal val speaker = SpeakerManager(this, microphone) internal val screenShare = ScreenShareManager(this, eglBaseContext) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt index 7a1b870fa8..051c81dde6 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt @@ -71,6 +71,7 @@ import kotlinx.coroutines.launch * @property sounds Overwrite the default SDK sounds. See [Sounds]. * @property crashOnMissingPermission if [permissionCheck] returns false there will be an exception. * @property permissionCheck used to check for system permission based on call capabilities. See [StreamPermissionCheck]. + * @property audioUsage used to signal to the system how to treat the audio tracks (voip or media). */ public class StreamVideoBuilder @JvmOverloads constructor( context: Context, @@ -90,6 +91,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( private val sounds: Sounds = Sounds(), private val crashOnMissingPermission: Boolean = true, private val permissionCheck: StreamPermissionCheck = DefaultStreamPermissionCheck(), + private val audioUsage: Int = defaultAudioUsage, ) { private val context: Context = context.applicationContext @@ -171,6 +173,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( testSfuAddress = localSfuAddress, sounds = sounds, permissionCheck = permissionCheck, + audioUsage = audioUsage, ) if (user.type == UserType.Guest) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt index 9df6b2de9c..f1f1176ded 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt @@ -17,6 +17,7 @@ package io.getstream.video.android.core import android.content.Context +import android.media.AudioAttributes import androidx.lifecycle.Lifecycle import io.getstream.android.push.PushDevice import io.getstream.log.taggedLogger @@ -125,6 +126,7 @@ import kotlin.coroutines.Continuation import kotlin.coroutines.resumeWithException internal const val WAIT_FOR_CONNECTION_ID_TIMEOUT = 5000L +internal const val defaultAudioUsage = AudioAttributes.USAGE_VOICE_COMMUNICATION /** * @param lifecycle The lifecycle used to observe changes in the process @@ -145,6 +147,7 @@ internal class StreamVideoImpl internal constructor( internal val sounds: Sounds, internal val crashOnMissingPermission: Boolean = true, internal val permissionCheck: StreamPermissionCheck = DefaultStreamPermissionCheck(), + internal val audioUsage: Int = defaultAudioUsage, ) : StreamVideo, NotificationHandler by streamNotificationManager { private var locationJob: Deferred>? = null @@ -165,7 +168,7 @@ internal class StreamVideoImpl internal constructor( private lateinit var connectContinuation: Continuation> @InternalStreamVideoApi - public var peerConnectionFactory = StreamPeerConnectionFactory(context) + public var peerConnectionFactory = StreamPeerConnectionFactory(context, audioUsage) public override val userId = user.id private val logger by taggedLogger("Call:StreamVideo") diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/connection/StreamPeerConnectionFactory.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/connection/StreamPeerConnectionFactory.kt index efaa6c789a..27413a0b60 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/connection/StreamPeerConnectionFactory.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/connection/StreamPeerConnectionFactory.kt @@ -17,9 +17,11 @@ package io.getstream.video.android.core.call.connection import android.content.Context +import android.media.AudioAttributes import android.os.Build import io.getstream.log.taggedLogger import io.getstream.video.android.core.call.video.FilterVideoProcessor +import io.getstream.video.android.core.defaultAudioUsage import io.getstream.video.android.core.model.IceCandidate import io.getstream.video.android.core.model.StreamPeerType import kotlinx.coroutines.CoroutineScope @@ -44,8 +46,13 @@ import java.nio.ByteBuffer * Builds a factory that provides [PeerConnection]s when requested. * * @param context Used to build the underlying native components for the factory. + * @param audioUsage signal to the system how the audio tracks are used. + * Set this to [AudioAttributes.USAGE_MEDIA] if you want the audio track to behave like media, useful for livestreaming scenarios. */ -public class StreamPeerConnectionFactory(private val context: Context) { +public class StreamPeerConnectionFactory( + private val context: Context, + private val audioUsage: Int = defaultAudioUsage, +) { private val webRtcLogger by taggedLogger("Call:WebRTC") private val audioLogger by taggedLogger("Call:AudioTrackCallback") @@ -151,7 +158,15 @@ public class StreamPeerConnectionFactory(private val context: Context) { .builder(context) .setUseHardwareAcousticEchoCanceler( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q, - ) + ).apply { + if (audioUsage != defaultAudioUsage) { + setAudioAttributes( + AudioAttributes.Builder().setUsage(audioUsage) + .build(), + ) + audioLogger.d { "[setAudioAttributes] usage: $audioUsage" } + } + } .setUseHardwareNoiseSuppressor(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) .setAudioRecordErrorCallback(object : JavaAudioDeviceModule.AudioRecordErrorCallback { @@ -297,7 +312,10 @@ public class StreamPeerConnectionFactory(private val context: Context) { * @return [VideoSource] that can be used to build tracks. */ - internal fun makeVideoSource(isScreencast: Boolean, filterVideoProcessor: FilterVideoProcessor): VideoSource = + internal fun makeVideoSource( + isScreencast: Boolean, + filterVideoProcessor: FilterVideoProcessor, + ): VideoSource = factory.createVideoSource(isScreencast).apply { setVideoProcessor(filterVideoProcessor) } diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt index cf75712efd..f10bb6141b 100644 --- a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt @@ -17,6 +17,7 @@ package io.getstream.video.android.core import android.content.Context +import android.media.AudioAttributes import android.media.AudioManager import io.mockk.every import io.mockk.mockk @@ -28,11 +29,13 @@ import org.junit.Test class MicrophoneManagerTest { + private val audioUsage = AudioAttributes.USAGE_VOICE_COMMUNICATION + @Test fun `Ensure setup is called prior to any action onto the microphone manager`() = runTest { // Given val mediaManager = mockk(relaxed = true) - val actual = MicrophoneManager(mediaManager, false) + val actual = MicrophoneManager(mediaManager, false, audioUsage) val context = mockk(relaxed = true) val microphoneManager = spyk(actual) every { mediaManager.context } returns context @@ -63,7 +66,12 @@ class MicrophoneManagerTest { fun `Don't crash when accessing audioHandler prior to setup`() { // Given val mediaManager = mockk(relaxed = true) - val actual = MicrophoneManager(mediaManager, false) + val actual = + MicrophoneManager( + mediaManager, + false, + audioUsage = AudioAttributes.USAGE_VOICE_COMMUNICATION, + ) val context = mockk(relaxed = true) val microphoneManager = spyk(actual) every { mediaManager.context } returns context @@ -83,7 +91,7 @@ class MicrophoneManagerTest { fun `Ensure setup if ever the manager was cleaned`() { // Given val mediaManager = mockk(relaxed = true) - val actual = MicrophoneManager(mediaManager, false) + val actual = MicrophoneManager(mediaManager, false, audioUsage) val context = mockk(relaxed = true) val microphoneManager = spyk(actual) every { mediaManager.context } returns context @@ -111,7 +119,7 @@ class MicrophoneManagerTest { fun `Resume will call enable only if prior status was DeviceStatus#enabled`() { // Given val mediaManager = mockk(relaxed = true) - val actual = MicrophoneManager(mediaManager, false) + val actual = MicrophoneManager(mediaManager, false, audioUsage) val context = mockk(relaxed = true) val microphoneManager = spyk(actual) every { mediaManager.context } returns context