Skip to content

Commit

Permalink
[PBE-4085] Difficulty setting up automatic audio output switching for…
Browse files Browse the repository at this point in the history
… calls on android (#1115)
  • Loading branch information
aleksandar-apostolov authored Jun 13, 2024
1 parent 5b5f112 commit 92aefe9
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 22 deletions.
14 changes: 10 additions & 4 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -409,10 +409,12 @@ public final class io/getstream/video/android/core/LocalStats {
}

public final class io/getstream/video/android/core/MediaManagerImpl {
public fun <init> (Landroid/content/Context;Lio/getstream/video/android/core/Call;Lkotlinx/coroutines/CoroutineScope;Lorg/webrtc/EglBase$Context;)V
public fun <init> (Landroid/content/Context;Lio/getstream/video/android/core/Call;Lkotlinx/coroutines/CoroutineScope;Lorg/webrtc/EglBase$Context;I)V
public synthetic fun <init> (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;
Expand Down Expand Up @@ -476,10 +478,12 @@ public final class io/getstream/video/android/core/MemberState {
}

public final class io/getstream/video/android/core/MicrophoneManager {
public fun <init> (Lio/getstream/video/android/core/MediaManagerImpl;Z)V
public fun <init> (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
Expand Down Expand Up @@ -817,7 +821,8 @@ public final class io/getstream/video/android/core/StreamVideoBuilder {
public fun <init> (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 <init> (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 <init> (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 <init> (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 <init> (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 <init> (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;
}
Expand Down Expand Up @@ -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 <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;I)V
public synthetic fun <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ public class Call(
this,
scope,
clientImpl.peerConnectionFactory.eglBase.eglBaseContext,
clientImpl.audioUsage,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -434,6 +436,8 @@ class MicrophoneManager(
setupCompleted = false
}

fun canHandleDeviceSwitch() = audioUsage != AudioAttributes.USAGE_MEDIA

// Internal logic
internal fun setup() {
if (setupCompleted) {
Expand All @@ -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
}

Expand All @@ -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." }
}
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -171,6 +173,7 @@ public class StreamVideoBuilder @JvmOverloads constructor(
testSfuAddress = localSfuAddress,
sounds = sounds,
permissionCheck = permissionCheck,
audioUsage = audioUsage,
)

if (user.type == UserType.Guest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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<Result<String>>? = null
Expand All @@ -165,7 +168,7 @@ internal class StreamVideoImpl internal constructor(
private lateinit var connectContinuation: Continuation<Result<ConnectedEvent>>

@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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MediaManagerImpl>(relaxed = true)
val actual = MicrophoneManager(mediaManager, false)
val actual = MicrophoneManager(mediaManager, false, audioUsage)
val context = mockk<Context>(relaxed = true)
val microphoneManager = spyk(actual)
every { mediaManager.context } returns context
Expand Down Expand Up @@ -63,7 +66,12 @@ class MicrophoneManagerTest {
fun `Don't crash when accessing audioHandler prior to setup`() {
// Given
val mediaManager = mockk<MediaManagerImpl>(relaxed = true)
val actual = MicrophoneManager(mediaManager, false)
val actual =
MicrophoneManager(
mediaManager,
false,
audioUsage = AudioAttributes.USAGE_VOICE_COMMUNICATION,
)
val context = mockk<Context>(relaxed = true)
val microphoneManager = spyk(actual)
every { mediaManager.context } returns context
Expand All @@ -83,7 +91,7 @@ class MicrophoneManagerTest {
fun `Ensure setup if ever the manager was cleaned`() {
// Given
val mediaManager = mockk<MediaManagerImpl>(relaxed = true)
val actual = MicrophoneManager(mediaManager, false)
val actual = MicrophoneManager(mediaManager, false, audioUsage)
val context = mockk<Context>(relaxed = true)
val microphoneManager = spyk(actual)
every { mediaManager.context } returns context
Expand Down Expand Up @@ -111,7 +119,7 @@ class MicrophoneManagerTest {
fun `Resume will call enable only if prior status was DeviceStatus#enabled`() {
// Given
val mediaManager = mockk<MediaManagerImpl>(relaxed = true)
val actual = MicrophoneManager(mediaManager, false)
val actual = MicrophoneManager(mediaManager, false, audioUsage)
val context = mockk<Context>(relaxed = true)
val microphoneManager = spyk(actual)
every { mediaManager.context } returns context
Expand Down

0 comments on commit 92aefe9

Please sign in to comment.