Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ import io.getstream.video.android.ui.menu.transcriptions.TranscriptionUiStateMan
import io.getstream.video.android.util.filters.SampleAudioFilter
import java.nio.ByteBuffer

/**
* Displays a settings popup for a Call with device, audio, video filter, transcription, and debug controls.
*
* Provides UI and actions to select audio/video devices and codecs, toggle audio usage and filters,
* control noise cancellation and incoming video visibility, manage recordings/transcriptions, and
* perform debug operations (ICE/SFU actions). The popup dismisses via onDismissed.
*
* @param call The active Call instance whose state and actions the menu manipulates.
* @param selectedVideoFilter Index of the currently selected video filter.
* @param showDebugOptions Whether debug-related menu items should be shown.
* @param noiseCancellationFeatureEnabled Whether the noise-cancellation feature is available.
* @param noiseCancellationEnabled Whether noise cancellation is currently enabled.
* @param onDismissed Callback invoked when the menu should be dismissed.
* @param onSelectVideoFilter Callback invoked with the selected filter index.
* @param onShowFeedback Callback invoked to open feedback UI.
* @param onNoiseCancellation Callback invoked to toggle or configure noise cancellation.
* @param selectedIncomingVideoResolution Currently selected preferred incoming video resolution, or null.
* @param onSelectIncomingVideoResolution Callback invoked with a new preferred incoming video resolution or null.
* @param isIncomingVideoEnabled Whether incoming video is enabled.
* @param onToggleIncomingVideoVisibility Callback invoked to show/hide incoming video.
* @param onShowCallStats Callback invoked to display call statistics.
* @param onSelectScaleType Callback invoked to change the remote video scaling type.
* @param closedCaptionUiState UI state for closed captions.
* @param onClosedCaptionsToggle Callback invoked to toggle closed captions.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
internal fun SettingsMenu(
Expand Down Expand Up @@ -429,4 +454,4 @@ private fun SettingsMenuPreview() {
),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,11 @@ object StreamVideoInitHelper {
).enqueue()
}

/** Sets up and returns the [StreamVideo] required to connect to the API. */
/**
* Builds a configured StreamVideo instance for the provided API key and user.
*
* @return A ready-to-use StreamVideo configured with the given user, token, logging level, call service registry, notification and telecom settings.
*/
@OptIn(ExperimentalStreamVideoApi::class)
private fun initializeStreamVideo(
context: Context,
Expand Down Expand Up @@ -338,4 +342,4 @@ object StreamVideoInitHelper {
useInBuiltAudioSwitch = true,
).build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,13 @@ public class Call(
}
}

/**
* Observes available microphone devices and ensures an appropriate audio input is selected.
*
* When a Bluetooth headset becomes available it is selected; otherwise a wired headset is selected
* if present. If no headset is available, the previously saved non-headset fallback device is
* restored when available.
*/
private fun monitorHeadset() {
microphone.devices.onEach { availableDevices ->
logger.d {
Expand Down Expand Up @@ -1780,4 +1787,4 @@ public data class CreateCallOptions(
}
return memberRequestList
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,11 @@ class MicrophoneManager(
}

/**
* Select a specific device
* Selects the given audio output device and updates related internal state.
*
* Requests the initialized audio handler to switch to `device`, sets the manager's selected device state, updates the speaker manager status when the speakerphone is (de)selected, and records a non-headset fallback device for future use.
*
* @param device The desired `StreamAudioDevice` to select, or `null` to clear selection.
*/
fun select(device: StreamAudioDevice?) {
logger.i { "selecting device $device" }
Expand Down Expand Up @@ -719,7 +723,17 @@ class MicrophoneManager(

fun canHandleDeviceSwitch() = audioUsageProvider.invoke() != AudioAttributes.USAGE_MEDIA

// Internal logic
/**
* Initializes audio routing and device handling for the microphone manager.
*
* Sets up the Android AudioManager and, if device switching is supported and not already initialized,
* creates and starts an AudioHandler configured with a device priority that respects `preferSpeaker`.
* Once device information becomes available (or immediately if already set up), the optional
* `onAudioDevicesUpdate` callback is invoked and internal device/selection state is updated.
*
* @param preferSpeaker When true, prioritizes the speakerphone over the earpiece when building the preferred device list.
* @param onAudioDevicesUpdate Optional callback invoked once the available audio devices and current selection have been populated (or immediately if setup was already completed).
*/
internal fun setup(preferSpeaker: Boolean = false, onAudioDevicesUpdate: (() -> Unit)? = null) {
synchronized(this) {
var capturedOnAudioDevicesUpdate = onAudioDevicesUpdate
Expand Down Expand Up @@ -790,11 +804,25 @@ class MicrophoneManager(
}
}

/**
* Ensures audio handler setup (initializing it if necessary) and runs the provided action when audio devices update.
*
* @param preferSpeaker If true, prefer speakerphone in the initial device priority when setting up.
* @param actual Action to invoke when the audio devices are updated.
*/
internal fun enforceSetup(preferSpeaker: Boolean = false, actual: () -> Unit) = setup(
preferSpeaker,
onAudioDevicesUpdate = actual,
)

/**
* Invokes the provided block with the initialized `AudioHandler` if available.
*
* If the audio handler has been initialized, the `then` lambda is called with it;
* otherwise the function logs an error indicating setup() must be called first.
*
* @param then Lambda to execute with the initialized `AudioHandler`.
*/
private fun ifAudioHandlerInitialized(then: (audioHandler: AudioHandler) -> Unit) {
if (this::audioHandler.isInitialized) {
then(this.audioHandler)
Expand Down Expand Up @@ -1393,4 +1421,4 @@ class MediaManagerImpl(
}
}

fun MediaStreamTrack.trySetEnabled(enabled: Boolean) = safeCall { setEnabled(enabled) }
fun MediaStreamTrack.trySetEnabled(enabled: Boolean) = safeCall { setEnabled(enabled) }
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,17 @@ public class StreamVideoBuilder @JvmOverloads constructor(
}

/**
* Builds the [StreamVideo] client.
* Constructs and initializes a configured StreamVideo client.
*
* @return The [StreamVideo] client.
* Performs validation, sets up logging and time library, initializes notification and connection modules,
* attempts background coordinator connection and location loading, installs the client as the singleton,
* and returns the created client.
*
* @throws RuntimeException If an instance of the client already exists and [ensureSingleInstance] is set to true.
* @throws IllegalArgumentException If [apiKey] is blank.
* @throws IllegalArgumentException If [user] type is [UserType.Authenticated] and the [user] id is blank.
* @throws IllegalArgumentException If [user] type is [UserType.Authenticated] and both [token] and [tokenProvider] are empty.
* @throws ConnectException If the WebSocket connection fails.
* @return The initialized StreamVideo client.
* @throws RuntimeException If an existing StreamVideo instance is present and `ensureSingleInstance` is true.
* @throws IllegalArgumentException If `apiKey` is blank.
* @throws IllegalArgumentException If `token` is blank.
* @throws IllegalArgumentException If `user` is of type `UserType.Authenticated` and the user id is blank.
*/
public fun build(): StreamVideo {
val lifecycle = ProcessLifecycleOwner.get().lifecycle
Expand Down Expand Up @@ -365,4 +367,4 @@ internal val tokenRepository = TokenRepository("")
sealed class GEO {
/** Run calls over our global edge network, this is the default and right for most applications */
object GlobalEdgeNetwork : GEO()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ package io.getstream.video.android.core.audio
*/
internal interface AudioDeviceManager {
/**
* Enumerates available audio devices.
*/
* Lists the currently available audio devices.
*
* @return A list of available StreamAudioDevice instances representing detected audio input/output devices.
*/
fun enumerateDevices(): List<StreamAudioDevice>

/**
* Selects an audio device for routing.
* @param device The device to select
* @return true if selection was successful, false otherwise
*/
* Sets the active audio routing device.
*
* @param device The device to make active for audio routing.
* @return `true` if the device was successfully selected, `false` otherwise.
*/
fun selectDevice(device: StreamAudioDevice): Boolean

/**
Expand All @@ -40,17 +43,21 @@ internal interface AudioDeviceManager {
fun clearDevice()

/**
* Gets the currently selected device.
*/
* Returns the currently selected audio device.
*
* @return The selected StreamAudioDevice, or `null` if no device is selected.
*/
fun getSelectedDevice(): StreamAudioDevice?

/**
* Starts the device manager (registers listeners, etc.)
*/
* Initializes the device manager and prepares it for use.
*/
fun start()

/**
* Stops the device manager (unregisters listeners, etc.)
*/
* Releases resources and tears down the device manager.
*
* Stops device monitoring, unregisters listeners or observers, and performs any necessary cleanup so the manager is no longer active. The manager must be started again before it can be used after this call.
*/
fun stop()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,22 @@ public interface AudioHandler {
public fun start()

/**
* Called when a room is disconnected.
*/
* Stops audio device management and releases related resources.
*
* Cancels any pending initialization, stops any active audio switch on the main thread,
* and clears internal references so audio handling is fully stopped when a room is disconnected.
*/
public fun stop()

public fun selectDevice(audioDevice: StreamAudioDevice?)
/**
* Selects the given StreamAudioDevice for audio output.
*
* Converts the provided StreamAudioDevice to its underlying Twilio AudioDevice and applies it.
* Passing `null` clears the current selection.
*
* @param audioDevice The StreamAudioDevice to select, or `null` to clear selection.
*/
public fun selectDevice(audioDevice: StreamAudioDevice?)
}

/**
Expand Down Expand Up @@ -77,6 +88,12 @@ public class AudioSwitchHandler(
}
}

/**
* Stops and releases the audio switcher, cancelling any pending initialization.
*
* Cancels any pending main-thread runnables related to audio-switch initialization and schedules
* a main-thread task to stop the active AudioSwitch (if any) and clear its reference.
*/
override fun stop() {
logger.d { "[stop] no args" }
mainThreadHandler.removeCallbacksAndMessages(null)
Expand All @@ -86,20 +103,32 @@ public class AudioSwitchHandler(
}
}

/**
* Selects the given StreamAudioDevice for audio output by converting it to Twilio's AudioDevice and delegating to the existing selection logic.
*
* @param audioDevice The StreamAudioDevice to select; pass `null` to clear the current selection.
*/
override fun selectDevice(audioDevice: StreamAudioDevice?) {
val twilioDevice = convertStreamDeviceToTwilioDevice(audioDevice)
selectDevice(twilioDevice)
}

/**
* Selects the given Twilio audio device and applies the change.
*
* @param audioDevice The Twilio `AudioDevice` to select; pass `null` to clear the current selection.
*/
public fun selectDevice(audioDevice: AudioDevice?) {
logger.i { "[selectDevice] audioDevice: $audioDevice" }
audioSwitch?.selectDevice(audioDevice)
audioSwitch?.activate()
}

/**
* Converts a StreamAudioDevice to Twilio's AudioDevice.
* Returns null if the input is null.
* Extracts the underlying Twilio `AudioDevice` from a `StreamAudioDevice`.
*
* @param streamDevice The `StreamAudioDevice` to convert, or `null`.
* @return The contained `AudioDevice`, or `null` if `streamDevice` is `null`.
*/
private fun convertStreamDeviceToTwilioDevice(streamDevice: StreamAudioDevice?): AudioDevice? {
return streamDevice?.audio
Expand Down Expand Up @@ -127,4 +156,4 @@ public class AudioSwitchHandler(
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ import com.twilio.audioswitch.AudioDeviceChangeListener as TwilioAudioDeviceChan
*/
internal object AudioHandlerFactory {
/**
* Creates an AudioHandler instance based on the useInBuiltAudioSwitch flag.
* Create an AudioHandler configured either with the in-built StreamAudioSwitch or the Twilio AudioSwitch.
*
* @param context Android context
* @param preferredStreamAudioDeviceList List of preferred audio device types in priority order
* @param twilioAudioDeviceChangeListener Callback for device changes
* @param useInBuiltAudioSwitch If true, uses custom StreamAudioSwitch; if false, uses Twilio AudioSwitch
* @return An AudioHandler instance
* When `useInBuiltAudioSwitch` is true, returns a StreamAudioSwitchHandler using `preferredStreamAudioDeviceList`
* and `streamAudioDeviceChangeListener`. When false, returns an AudioSwitchHandler with `preferredStreamAudioDeviceList`
* mapped to Twilio `AudioDevice` types (unmapped types fall back to speakerphone) and `twilioAudioDeviceChangeListener`.
*
* @param context Android context used to initialize the audio handler.
* @param preferredStreamAudioDeviceList Preferred StreamAudioDevice classes in priority order.
* @param twilioAudioDeviceChangeListener Callback invoked on Twilio audio device changes (used when not using in-built switch).
* @param streamAudioDeviceChangeListener Callback invoked on Stream audio device changes (used when using in-built switch).
* @param useInBuiltAudioSwitch If true, use the in-built StreamAudioSwitch; otherwise use the Twilio AudioSwitch.
* @return An AudioHandler instance configured according to `useInBuiltAudioSwitch` and the provided preferences/listeners.
*/
fun create(
context: Context,
Expand Down Expand Up @@ -69,7 +74,10 @@ internal object AudioHandlerFactory {
}

/**
* Converts a Twilio AudioDevice to StreamAudioDevice.
* Map a Twilio `AudioDevice` to the corresponding `StreamAudioDevice`.
*
* @param twilioDevice The Twilio `AudioDevice` to convert.
* @return A `StreamAudioDevice` instance representing the same physical device as the provided Twilio `AudioDevice`.
*/
private fun convertTwilioDeviceToStreamDevice(twilioDevice: AudioDevice): StreamAudioDevice {
return when (twilioDevice) {
Expand All @@ -87,4 +95,4 @@ internal object AudioHandlerFactory {
)
}
}
}
}
Loading
Loading