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 fd19e8cdeea..faaa2114cbb 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 @@ -35,7 +35,6 @@ import android.media.AudioRecord.READ_BLOCKING import android.media.projection.MediaProjection import android.os.Build import android.os.IBinder -import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService @@ -698,7 +697,6 @@ class MicrophoneManager( * * Requires Android M (API 23) or higher. */ - @RequiresApi(Build.VERSION_CODES.M) fun setupUsbDeviceDetection() { if (audioDeviceCallback != null) { logger.d { "[setupUsbDeviceDetection] Already set up" } @@ -742,7 +740,6 @@ class MicrophoneManager( /** * Updates the list of available USB input devices. */ - @RequiresApi(Build.VERSION_CODES.M) private fun updateUsbDeviceList() { val am = audioManager ?: return @@ -773,7 +770,6 @@ class MicrophoneManager( * @param device The USB device to use, or null to restore default routing. * @return true if the device was selected successfully, false otherwise. */ - @RequiresApi(Build.VERSION_CODES.M) fun selectUsbDevice(device: UsbAudioInputDevice?): Boolean { logger.i { "[selectUsbDevice] Selecting USB device: ${device?.name}" } @@ -793,10 +789,15 @@ class MicrophoneManager( /** * Clears the USB device selection, restoring default audio routing. + * + * Resets the state directly rather than going through [selectUsbDevice] with null, + * because the WebRTC ADM's setPreferredInputDevice does not accept null safely. + * When the USB device is physically removed, the system automatically falls back + * to default audio routing, so an explicit ADM call is unnecessary. */ - @RequiresApi(Build.VERSION_CODES.M) fun clearUsbDeviceSelection() { - selectUsbDevice(null) + logger.i { "[clearUsbDeviceSelection] Clearing USB device selection" } + _selectedUsbDevice.value = null } /** @@ -804,7 +805,6 @@ class MicrophoneManager( * * @return StateFlow of available USB input devices. */ - @RequiresApi(Build.VERSION_CODES.M) fun listUsbDevices(): StateFlow> { setupUsbDeviceDetection() return usbInputDevices @@ -813,7 +813,6 @@ class MicrophoneManager( /** * Cleans up USB device detection callback. */ - @RequiresApi(Build.VERSION_CODES.M) private fun cleanupUsbDeviceDetection() { audioDeviceCallback?.let { callback -> audioManager?.unregisterAudioDeviceCallback(callback) @@ -881,9 +880,7 @@ class MicrophoneManager( fun cleanup() { ifAudioHandlerInitialized { it.stop() } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - cleanupUsbDeviceDetection() - } + cleanupUsbDeviceDetection() setupCompleted.set(false) } 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 2f32bbed181..3f360392cd7 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 @@ -175,6 +175,12 @@ public class StreamPeerConnectionFactory( private var adm: JavaAudioDeviceModule? = null + @Volatile + private var pendingPreferredInputDevice: AudioDeviceInfo? = null + + @Volatile + private var hasPendingPreferredDevice = false + private fun createFactory(): PeerConnectionFactory { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context) @@ -208,6 +214,14 @@ public class StreamPeerConnectionFactory( adm = initAudioDeviceModule() + if (hasPendingPreferredDevice) { + adm?.setPreferredInputDevice(pendingPreferredInputDevice) + audioLogger.i { + "[createFactory] Applied pending preferred input device: ${pendingPreferredInputDevice?.productName}" + } + hasPendingPreferredDevice = false + } + // Capture the audio bitrate profile when creating the factory val currentAudioBitrateProfile = audioBitrateProfileProvider?.invoke() val isMusicHighQuality = currentAudioBitrateProfile == @@ -345,17 +359,23 @@ public class StreamPeerConnectionFactory( * This allows routing audio input to a specific device, such as a USB microphone * that may not be detected by AudioSwitch (e.g., Rode Wireless Go II). * - * Must be called on API 23+ (Android M). On older versions, this is a no-op. - * * @param deviceInfo The AudioDeviceInfo to use for recording, or null to restore default routing. * @return true if the preference was set successfully, false otherwise. */ - @RequiresApi(Build.VERSION_CODES.M) fun setPreferredAudioInputDevice(deviceInfo: AudioDeviceInfo?): Boolean { return try { - adm?.setPreferredInputDevice(deviceInfo) - audioLogger.i { - "[setPreferredAudioInputDevice] Set preferred input device: ${deviceInfo?.productName}" + val currentAdm = adm + if (currentAdm != null) { + currentAdm.setPreferredInputDevice(deviceInfo) + audioLogger.i { + "[setPreferredAudioInputDevice] Set preferred input device: ${deviceInfo?.productName}" + } + } else { + pendingPreferredInputDevice = deviceInfo + hasPendingPreferredDevice = true + audioLogger.w { + "[setPreferredAudioInputDevice] ADM not yet initialized, queuing preference: ${deviceInfo?.productName}" + } } true } catch (e: Exception) {