Platform docs
Auth, users, webhooks & more

Hi-Fi Audio

This cookbook demonstrates how to use ToggleHifiAudioAction in your Compose UI to allow users to toggle between standard and high-fidelity audio modes.

Overview

ToggleHifiAudioAction is a pre-built Compose component that provides a toggle button for switching between standard voice audio (AUDIO_BITRATE_PROFILE_VOICE_STANDARD_UNSPECIFIED) and high-fidelity music audio (AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY).

Basic Usage

The simplest way to use ToggleHifiAudioAction is to provide the current audio bitrate profile state and handle the action:

@Composable
fun MyLobbyScreen(call: Call) {
    val audioBitrateProfile by call.microphone.audioBitrateProfile.collectAsStateWithLifecycle()
    val isMusicHighQuality = audioBitrateProfile == AudioBitrateProfile.AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY

    ToggleHifiAudioAction(
        isMusicHighQuality = isMusicHighQuality,
        onCallAction = { action: ToggleHifiAudio ->
            // Handle the toggle action
            handleHifiAudioToggle(call, action.isHifiAudioEnabled)
        }
    )
}

Using in Lobby Screen with buildDefaultLobbyControlActions

The easiest way to add the HiFi audio toggle to your lobby screen is using the buildDefaultLobbyControlActions function:

@Composable
fun LobbyControls(
    call: Call,
    showHifiAudioToggle: Boolean = false,
) {
    val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle()
    val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle()

    val actions = buildDefaultLobbyControlActions(
        call = call,
        onCallAction = { action ->
            handleCallAction(call, action)
        },
        isCameraEnabled = isCameraEnabled,
        isMicrophoneEnabled = isMicrophoneEnabled,
        showHifiAudioToggle = showHifiAudioToggle,
    )

    // Render the actions
    Row {
        actions.forEach { action ->
            action()
        }
    }
}

Using in an in-call control bar

The same ToggleHifiAudioAction works after join. buildDefaultCallControlActions does not add it, so append it to that list. DefaultOnCallActionHandler already handles ToggleHifiAudio.

@Composable
fun InCallControls(
    call: Call,
    showHifiAudioToggle: Boolean,
) {
    val audioBitrateProfile by call.microphone.audioBitrateProfile.collectAsStateWithLifecycle()
    val isMusicHighQuality =
        audioBitrateProfile == AudioBitrateProfile.AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY

    val actions = buildDefaultCallControlActions(
        call = call,
        onCallAction = { action ->
            DefaultOnCallActionHandler.onCallAction(call, action)
        },
    )

    Row {
        actions.forEach { action ->
            action()
        }
        if (showHifiAudioToggle) {
            ToggleHifiAudioAction(
                isMusicHighQuality = isMusicHighQuality,
                onCallAction = { action ->
                    DefaultOnCallActionHandler.onCallAction(call, action)
                },
            )
        }
    }
}

Checking if HiFi audio is enabled

Before showing the toggle, check if HiFi audio is enabled in the call settings:

@Composable
fun LobbyScreen(call: Call) {
    val hifiAudioEnabled by call.state.settings
        .map { it?.audio?.hifiAudioEnabled ?: false }
        .collectAsStateWithLifecycle(initialValue = false)

    // Settings is loaded by the SDK asynchronously
    val settingsLoaded by call.state.settings
        .map { it != null }
        .collectAsStateWithLifecycle(initialValue = false)

    val showHifiAudioToggle = settingsLoaded && hifiAudioEnabled

    // Use showHifiAudioToggle in your UI
    LobbyControls(
        call = call,
        showHifiAudioToggle = showHifiAudioToggle,
    )
}

Handling the Toggle Action

When the user taps the toggle, you need to handle the ToggleHifiAudio action. Here's how to implement the handler:

Using DefaultOnCallActionHandler

The SDK provides a default handler that you can use in the lobby or after join:

val actions = buildDefaultLobbyControlActions(
    call = call,
    onCallAction = { action ->
        DefaultOnCallActionHandler.onCallAction(call, action)
    },
    showHifiAudioToggle = showHifiAudioToggle,
)

Custom Handler Implementation

For more control, implement your own handler:

fun handleCallAction(call: Call, action: CallAction) {
    when (action) {
        is ToggleHifiAudio -> {
            val newProfile = if (action.isHifiAudioEnabled) {
                AudioBitrateProfile.AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY
            } else {
                AudioBitrateProfile.AUDIO_BITRATE_PROFILE_VOICE_STANDARD_UNSPECIFIED
            }

            // Launch in a coroutine scope since setAudioBitrateProfile is suspend
            CoroutineScope(SupervisorJob() + Dispatchers.Main).launch {
                val result = call.microphone.setAudioBitrateProfile(newProfile)
                if (result.isFailure) {
                    // Dashboard does not allow HiFi, or call settings could not be fetched
                    Log.e("LobbyScreen", "Failed to set audio bitrate profile: ${result.exceptionOrNull()?.message}")
                }
            }
        }
        // Handle other actions...
        else -> Unit
    }
}

For better separation of concerns, handle the action in your ViewModel:

class CallLobbyViewModel(private val call: Call) : ViewModel() {

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

            val result = call.microphone.setAudioBitrateProfile(newProfile)
            if (result.isFailure) {
                Log.e(
                    "CallLobbyViewModel",
                    "Failed to set audio bitrate profile: ${result.exceptionOrNull()?.message}"
                )
            }
        }
    }
}

// In your Composable
@Composable
fun LobbyScreen(viewModel: CallLobbyViewModel) {
    val actions = buildDefaultLobbyControlActions(
        call = viewModel.call,
        onCallAction = { action ->
            when (action) {
                is ToggleHifiAudio -> {
                    viewModel.setAudioBitrateProfile(action.isHifiAudioEnabled)
                }
                // Handle other actions...
                else -> Unit
            }
        },
        showHifiAudioToggle = showHifiAudioToggle,
    )
}

Complete Example

Here's a complete example showing how to integrate ToggleHifiAudioAction in a lobby screen:

@Composable
fun CallLobbyScreen(
    call: Call,
    onBack: () -> Unit,
) {
    // Check if HiFi audio is enabled
    val hifiAudioEnabled by call.state.settings
        .map { it?.audio?.hifiAudioEnabled ?: false }
        .collectAsStateWithLifecycle(initialValue = false)

    val settingsLoaded by call.state.settings
        .map { it != null }
        .collectAsStateWithLifecycle(initialValue = false)

    val showHifiAudioToggle = settingsLoaded && hifiAudioEnabled

    // Get device states
    val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle()
    val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle()

    // Build control actions
    val actions = buildDefaultLobbyControlActions(
        call = call,
        onCallAction = { action ->
            handleCallAction(call, action)
        },
        isCameraEnabled = isCameraEnabled,
        isMicrophoneEnabled = isMicrophoneEnabled,
        showHifiAudioToggle = showHifiAudioToggle,
    )

    // Render UI
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        // Your lobby content here...

        // Control actions row
        Row(
            modifier = Modifier.padding(16.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            actions.forEach { action ->
                action()
            }
        }
    }
}

private fun handleCallAction(call: Call, action: CallAction) {
    when (action) {
        is ToggleHifiAudio -> {
            val newProfile = if (action.isHifiAudioEnabled) {
                AudioBitrateProfile.AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY
            } else {
                AudioBitrateProfile.AUDIO_BITRATE_PROFILE_VOICE_STANDARD_UNSPECIFIED
            }

            CoroutineScope(SupervisorJob() + Dispatchers.Main).launch {
                call.microphone.setAudioBitrateProfile(newProfile)
            }
        }
        // Handle other actions (ToggleMicrophone, ToggleCamera, etc.)
        else -> Unit
    }
}

Customization

You can customize the appearance of ToggleHifiAudioAction using its styling parameters:

ToggleHifiAudioAction(
    modifier = Modifier,
    isMusicHighQuality = isMusicHighQuality,
    enabled = true,
    shape = RoundedCornerShape(8.dp),
    enabledColor = Color.Blue,
    disabledColor = Color.Gray,
    enabledIconTint = Color.White,
    disabledIconTint = Color.Black,
    onStyle = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(),
    offStyle = VideoTheme.styles.buttonStyles.tertiaryIconButtonStyle(),
    onCallAction = { action ->
        handleHifiAudioToggle(call, action.isHifiAudioEnabled)
    }
)

Important notes

  1. Before or during a call: setAudioBitrateProfile works in the lobby and after join. Bind the button to call.microphone.audioBitrateProfile. That flow only moves on success, so a refused mid-call stage snaps the toggle back.

  2. Dashboard settings: HiFi audio must be enabled for the call type. If it is not, setAudioBitrateProfile returns a failure.

  3. Result<Unit>: Success means the profile is in force. Failure means it is not. The exception message names any live stage that refused. See High fidelity audio for what the SDK changes under the hood.

  4. Error handling: The Result fails when HiFi is not allowed on the call type, when call settings could not be fetched, or when a live stage refused. It does not fail merely because the call is already joined.

Add Chat to my app: getstream.io/SKILL.md

The fastest way to build with Stream. Start a new project or improve an existing one. Full CLI and documentation integration out of the box.


Ask your agent:

/stream Build me a Social App with Feeds and Moderation.
/stream Any livestream calls running?
/stream Video Android v1: <Your Question>