Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
383e41d
Update to latest proto
PratimMallick Oct 16, 2025
8a48b85
Send stereo = true in Audio track's TrackInfo if hifi is enabled in d…
PratimMallick Oct 22, 2025
f5d513f
fix: microphone not getting enabled if we join with USAGE_MEDIA mode
PratimMallick Oct 22, 2025
576f342
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Oct 22, 2025
379ee0f
Provide a way to set the audio usage from app to call.speaker - it ca…
PratimMallick Oct 30, 2025
ed1a969
Provide a way on the debug option menu to switch between usage_media …
PratimMallick Oct 30, 2025
1ef32b9
Expose the audioDeviceModule from peerConnectionFactory
PratimMallick Oct 30, 2025
16e8e8a
Apply spotless
PratimMallick Oct 30, 2025
2ddc7b7
Upgraded to webrtc version that supports switching audio usage mid call
PratimMallick Oct 30, 2025
a8690a8
api dump
PratimMallick Oct 30, 2025
e6b9abd
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Oct 30, 2025
503c9d7
Dont send stereo = true in trackInfo while publishing. This will be d…
PratimMallick Oct 30, 2025
bbbbf2b
Added unit tests to test the audioUsage stateFlow of SpeakerManager
PratimMallick Oct 30, 2025
ace13ef
Merge branch 'develop' into hifi_audio
aleksandar-apostolov Oct 31, 2025
789ad8b
Merge branch 'develop' into hifi_audio
aleksandar-apostolov Nov 3, 2025
57bba73
refactor(demo-app): simplify onToggleAudioUsage by removing suspend m…
PratimMallick Nov 3, 2025
b755b0a
Merge remote-tracking branch 'origin/hifi_audio' into hifi_audio
PratimMallick Nov 3, 2025
f96d3ec
Fix: `setAudioUsage` of speakerManager does not work if called before…
PratimMallick Nov 4, 2025
accddc9
Added : Enable Hifi audio recording
PratimMallick Nov 4, 2025
b52e65b
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick Nov 4, 2025
de3d61f
Make setters for audio,video and screenshare tracks so that they dont…
PratimMallick Nov 5, 2025
6fd8c65
Fix: have a setter to peerConnectionFactory so that it can be accesse…
PratimMallick Nov 5, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import io.getstream.video.android.compose.ui.components.video.config.videoRender
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.call.state.CallAction
import io.getstream.video.android.core.call.state.ToggleCamera
import io.getstream.video.android.core.call.state.ToggleHifiAudio
import io.getstream.video.android.core.call.state.ToggleMicrophone
import io.getstream.video.android.core.events.ParticipantCount
import io.getstream.video.android.mock.StreamPreviewDataUtils
Expand Down Expand Up @@ -130,6 +131,9 @@ fun CallLobbyScreen(
onToggleMicrophone = {
callLobbyViewModel.enableMicrophone(it)
},
onToggleHifiAudio = {
callLobbyViewModel.setAudioBitrateProfile(it)
},
call = call,
) {
LobbyDescription(callLobbyViewModel = callLobbyViewModel)
Expand Down Expand Up @@ -228,6 +232,7 @@ private fun CallLobbyBodyResponsive(
isMicrophoneEnabled: Boolean,
onToggleCamera: (Boolean) -> Unit,
onToggleMicrophone: (Boolean) -> Unit,
onToggleHifiAudio: (Boolean) -> Unit,
description: @Composable () -> Unit,
) {
val configuration = LocalConfiguration.current
Expand All @@ -240,6 +245,7 @@ private fun CallLobbyBodyResponsive(
isMicrophoneEnabled,
onToggleCamera,
onToggleMicrophone,
onToggleHifiAudio,
description,
)
} else {
Expand All @@ -250,6 +256,7 @@ private fun CallLobbyBodyResponsive(
isMicrophoneEnabled,
onToggleCamera,
onToggleMicrophone,
onToggleHifiAudio,
description,
)
}
Expand All @@ -264,6 +271,7 @@ private fun CallLobbyBodyPortrait(
isMicrophoneEnabled: Boolean,
onToggleCamera: (Boolean) -> Unit,
onToggleMicrophone: (Boolean) -> Unit,
onToggleHifiAudio: (Boolean) -> Unit,
description: @Composable () -> Unit,
) {
Column(
Expand Down Expand Up @@ -321,6 +329,7 @@ private fun CallLobbyBodyPortrait(
when (action) {
is ToggleCamera -> onToggleCamera(action.isEnabled)
is ToggleMicrophone -> onToggleMicrophone(action.isEnabled)
is ToggleHifiAudio -> onToggleHifiAudio(action.isHifiAudioEnabled)
else -> Unit
}
},
Expand All @@ -345,6 +354,7 @@ private fun CallLobbyBodyLandscape(
isMicrophoneEnabled: Boolean,
onToggleCamera: (Boolean) -> Unit,
onToggleMicrophone: (Boolean) -> Unit,
onToggleHifiAudio: (Boolean) -> Unit,
description: @Composable () -> Unit,
) {
Box(modifier = Modifier.background(VideoTheme.colors.baseSheetPrimary)) {
Expand All @@ -360,6 +370,7 @@ private fun CallLobbyBodyLandscape(
when (action) {
is ToggleCamera -> onToggleCamera(action.isEnabled)
is ToggleMicrophone -> onToggleMicrophone(action.isEnabled)
is ToggleHifiAudio -> onToggleHifiAudio(action.isHifiAudioEnabled)
else -> Unit
}
}
Expand Down Expand Up @@ -576,6 +587,7 @@ private fun CallLobbyBodyPortraitPreview() {
call = previewCall,
onToggleMicrophone = {},
onToggleCamera = {},
onToggleHifiAudio = {},
) {
LobbyDescriptionContent(participantCounts = ParticipantCount(1, 1)) {}
}
Expand All @@ -598,6 +610,7 @@ private fun CallLobbyBodyLandscapePreview() {
call = previewCall,
onToggleMicrophone = {},
onToggleCamera = {},
onToggleHifiAudio = {},
) {
LobbyDescriptionContent(participantCounts = ParticipantCount(1, 1)) {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import stream.video.sfu.models.AudioBitrateProfile
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -170,6 +171,15 @@ class CallLobbyViewModel @Inject constructor(
call.microphone.setEnabled(enabled)
}

fun setAudioBitrateProfile(isHifiAudioEnabled: Boolean) {
val newProfile = if (isHifiAudioEnabled) {
AudioBitrateProfile.AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY
} else {
AudioBitrateProfile.AUDIO_BITRATE_PROFILE_VOICE_STANDARD_UNSPECIFIED
}
call.microphone.setAudioBitrateProfile(newProfile)
}

fun signOut() {
viewModelScope.launch {
googleSignInClient.signOut()
Expand Down
18 changes: 16 additions & 2 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -7702,6 +7702,7 @@ public final class io/getstream/video/android/core/MicrophoneManager {
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 getAudioBitrateProfile ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getAudioUsage ()I
public final fun getAudioUsageProvider ()Lkotlin/jvm/functions/Function0;
public final fun getDevices ()Lkotlinx/coroutines/flow/StateFlow;
Expand All @@ -7715,6 +7716,7 @@ public final class io/getstream/video/android/core/MicrophoneManager {
public final fun resume (Z)V
public static synthetic fun resume$default (Lio/getstream/video/android/core/MicrophoneManager;ZILjava/lang/Object;)V
public final fun select (Lio/getstream/video/android/core/audio/StreamAudioDevice;)V
public final fun setAudioBitrateProfile (Lstream/video/sfu/models/AudioBitrateProfile;)Z
public final fun setEnabled (ZZ)V
public static synthetic fun setEnabled$default (Lio/getstream/video/android/core/MicrophoneManager;ZZILjava/lang/Object;)V
}
Expand Down Expand Up @@ -8300,8 +8302,9 @@ public class io/getstream/video/android/core/call/connection/StreamPeerConnectio
}

public final class io/getstream/video/android/core/call/connection/StreamPeerConnectionFactory {
public fun <init> (Landroid/content/Context;ILkotlin/jvm/functions/Function0;Lorg/webrtc/ManagedAudioProcessingFactory;)V
public synthetic fun <init> (Landroid/content/Context;ILkotlin/jvm/functions/Function0;Lorg/webrtc/ManagedAudioProcessingFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Landroid/content/Context;ILkotlin/jvm/functions/Function0;Lorg/webrtc/ManagedAudioProcessingFactory;Lkotlin/jvm/functions/Function0;Lorg/webrtc/EglBase;)V
public synthetic fun <init> (Landroid/content/Context;ILkotlin/jvm/functions/Function0;Lorg/webrtc/ManagedAudioProcessingFactory;Lkotlin/jvm/functions/Function0;Lorg/webrtc/EglBase;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun dispose ()V
public final fun getAdm ()Lorg/webrtc/audio/JavaAudioDeviceModule;
public final fun getEglBase ()Lorg/webrtc/EglBase;
public final fun getSenderCapabilities (Lorg/webrtc/MediaStreamTrack$MediaType;)Lorg/webrtc/RtpCapabilities;
Expand Down Expand Up @@ -8452,6 +8455,17 @@ public final class io/getstream/video/android/core/call/state/ToggleCamera : io/
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/call/state/ToggleHifiAudio : io/getstream/video/android/core/call/state/CallAction {
public fun <init> (Z)V
public final fun component1 ()Z
public final fun copy (Z)Lio/getstream/video/android/core/call/state/ToggleHifiAudio;
public static synthetic fun copy$default (Lio/getstream/video/android/core/call/state/ToggleHifiAudio;ZILjava/lang/Object;)Lio/getstream/video/android/core/call/state/ToggleHifiAudio;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public final fun isHifiAudioEnabled ()Z
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/call/state/ToggleMicrophone : io/getstream/video/android/core/call/state/CallAction {
public fun <init> (Z)V
public final fun component1 ()Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.threeten.bp.OffsetDateTime
import org.webrtc.EglBase
import org.webrtc.PeerConnection
import org.webrtc.RendererCommon
import org.webrtc.VideoSink
Expand Down Expand Up @@ -232,13 +233,97 @@ public class Call(
internal var connectStartTime = 0L
internal var reconnectStartTime = 0L

internal var peerConnectionFactory: StreamPeerConnectionFactory =
StreamPeerConnectionFactory(
context = clientImpl.context,
audioProcessing = clientImpl.audioProcessing,
audioUsage = clientImpl.callServiceConfigRegistry.get(type).audioUsage,
audioUsageProvider = { clientImpl.callServiceConfigRegistry.get(type).audioUsage },
)
/**
* EGL base context shared between peerConnectionFactory and mediaManager
* to break circular dependency.
*/
internal val eglBase: EglBase by lazy {
EglBase.create()
}

// peerConnectionFactory is nullable and recreated when audioBitrateProfile changes (before joining)
private var _peerConnectionFactory: StreamPeerConnectionFactory? = null

internal var peerConnectionFactory: StreamPeerConnectionFactory
get() {
if (_peerConnectionFactory == null) {
_peerConnectionFactory = StreamPeerConnectionFactory(
context = clientImpl.context,
audioProcessing = clientImpl.audioProcessing,
audioUsage = clientImpl.callServiceConfigRegistry.get(type).audioUsage,
audioUsageProvider = { clientImpl.callServiceConfigRegistry.get(type).audioUsage },
audioBitrateProfileProvider = { mediaManager.microphone.audioBitrateProfile.value },
sharedEglBase = eglBase,
)
}
return _peerConnectionFactory!!
}
set(value) {
_peerConnectionFactory = value
}

/**
* Checks if the audioBitrateProfile has changed since the factory was created,
* and recreates the factory if needed. This should only be called before joining.
*
* If the factory hasn't been created yet, it will be created with the current profile
* when first accessed, so no recreation is needed.
*/
internal fun ensureFactoryMatchesAudioProfile() {
val factory = _peerConnectionFactory

// If factory hasn't been created yet, it will be created with current profile automatically
if (factory == null) {
return
}

// Check if current profile differs from the profile used to create the factory
val factoryProfile = factory.audioBitrateProfile
val currentProfile = mediaManager.microphone.audioBitrateProfile.value

if (factoryProfile != null && currentProfile != factoryProfile) {
logger.i {
"Audio bitrate profile changed from $factoryProfile to $currentProfile. " +
"Recreating factory before joining."
}
recreateFactoryAndAudioTracks()
}
}

/**
* Recreates peerConnectionFactory, audioSource, audioTrack, videoSource and videoTrack
* with the current audioBitrateProfile. This should only be called before the call is joined.
*/
internal fun recreateFactoryAndAudioTracks() {
val wasMicrophoneEnabled = microphone.status.value is DeviceStatus.Enabled
val wasCameraEnabled = camera.status.value is DeviceStatus.Enabled

// Dispose all tracks and sources first
mediaManager.disposeTracksAndSources()

// Recreate the factory (which will use the new audioBitrateProfile)
recreatePeerConnectionFactory()

// Re-enable tracks if they were enabled
if (wasMicrophoneEnabled) {
// audioTrack will be recreated on next access, then we enable it
microphone.enable(fromUser = false)
}
if (wasCameraEnabled) {
// videoTrack will be recreated on next access, then we enable it
camera.enable(fromUser = false)
}
}

/**
* Recreates peerConnectionFactory with the current audioBitrateProfile.
* This should only be called before the call is joined.
*/
internal fun recreatePeerConnectionFactory() {
_peerConnectionFactory?.dispose()
_peerConnectionFactory = null
// Next access to peerConnectionFactory will recreate it with current profile
}

internal val clientCapabilities = ConcurrentHashMap<String, ClientCapability>().apply {
put(
Expand All @@ -255,7 +340,7 @@ public class Call(
clientImpl.context,
this,
scope,
peerConnectionFactory.eglBase.eglBaseContext,
eglBase.eglBaseContext,
clientImpl.callServiceConfigRegistry.get(type).audioUsage,
) { clientImpl.callServiceConfigRegistry.get(type).audioUsage }
}
Expand Down Expand Up @@ -423,6 +508,10 @@ public class Call(
}
// if we are a guest user, make sure we wait for the token before running the join flow
clientImpl.guestUserJob?.await()

// Ensure factory is created with the current audioBitrateProfile before joining
ensureFactoryMatchesAudioProfile()

// the join flow should retry up to 3 times
// if the error is not permanent
// and fail immediately on permanent errors
Expand Down Expand Up @@ -940,9 +1029,9 @@ public class Call(
) {
logger.d { "[initRenderer] #sfu; #track; sessionId: $sessionId" }

// Note this comes from peerConnectionFactory.eglBase
// Note this comes from the shared eglBase
videoRenderer.init(
peerConnectionFactory.eglBase.eglBaseContext,
eglBase.eglBaseContext,
object : RendererCommon.RendererEvents {
override fun onFirstFrameRendered() {
val width = videoRenderer.measuredWidth
Expand Down
Loading
Loading