From f5d7d9a9ab5e70ea85f25b5b3af33a9680ce4d22 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Wed, 24 Jun 2026 13:49:04 +0200 Subject: [PATCH] fix Live Call: route audio to speaker with proper communication device API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MODE_IN_COMMUNICATION routes to earpiece by default — no sound on speaker. The deprecated isSpeakerphoneOn doesn't work on Android 12+. Fix: enterCallMode()/exitCallMode() lifecycle — sets MODE_IN_COMMUNICATION + setCommunicationDevice(SPEAKER) on API 31+ (S25 Ultra = Android 15). Called BEFORE AudioTrack creation so the routing is active when playback starts. exitCallMode() restores previous audio mode on call end. --- .../java/dev/kaizen/app/live/AudioPipeline.kt | 36 +++++++++++++++---- .../dev/kaizen/app/live/LiveCallService.kt | 1 + 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/live/AudioPipeline.kt b/app/src/main/java/dev/kaizen/app/live/AudioPipeline.kt index a43cf48..3db3c13 100644 --- a/app/src/main/java/dev/kaizen/app/live/AudioPipeline.kt +++ b/app/src/main/java/dev/kaizen/app/live/AudioPipeline.kt @@ -62,6 +62,35 @@ class AudioPipeline(private val context: Context) { @Volatile private var ducking = false @Volatile private var capturing = false + // ─── Audio Mode ─────────────────────────────────────────────────────── + + fun enterCallMode() { + previousAudioMode = audioManager.mode + audioManager.mode = AudioManager.MODE_IN_COMMUNICATION + + if (android.os.Build.VERSION.SDK_INT >= 31) { + val speaker = audioManager.availableCommunicationDevices + .firstOrNull { it.type == android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } + if (speaker != null) { + audioManager.setCommunicationDevice(speaker) + Log.i(TAG, "Communication device → speaker") + } + } else { + @Suppress("DEPRECATION") + audioManager.isSpeakerphoneOn = true + } + } + + fun exitCallMode() { + if (android.os.Build.VERSION.SDK_INT >= 31) { + audioManager.clearCommunicationDevice() + } else { + @Suppress("DEPRECATION") + audioManager.isSpeakerphoneOn = false + } + audioManager.mode = previousAudioMode + } + // ─── Capture ────────────────────────────────────────────────────────── fun startCapture(scope: CoroutineScope): Boolean { @@ -92,11 +121,6 @@ class AudioPipeline(private val context: Context) { recorder = rec - previousAudioMode = audioManager.mode - audioManager.mode = AudioManager.MODE_IN_COMMUNICATION - @Suppress("DEPRECATION") - audioManager.isSpeakerphoneOn = true - if (AcousticEchoCanceler.isAvailable()) { try { aec = AcousticEchoCanceler.create(rec.audioSessionId) @@ -147,7 +171,6 @@ class AudioPipeline(private val context: Context) { aec = null recorder?.release() recorder = null - audioManager.mode = previousAudioMode } // ─── Playback ───────────────────────────────────────────────────────── @@ -220,6 +243,7 @@ class AudioPipeline(private val context: Context) { fun release() { stopCapture() stopPlayback() + exitCallMode() } // ─── Helpers ────────────────────────────────────────────────────────── diff --git a/app/src/main/java/dev/kaizen/app/live/LiveCallService.kt b/app/src/main/java/dev/kaizen/app/live/LiveCallService.kt index 0af13da..c2651b9 100644 --- a/app/src/main/java/dev/kaizen/app/live/LiveCallService.kt +++ b/app/src/main/java/dev/kaizen/app/live/LiveCallService.kt @@ -168,6 +168,7 @@ class LiveCallService : Service() { connectTimeoutJob?.cancel() _status.value = CallStatus.LISTENING + pipeline.enterCallMode() if (!pipeline.initPlayback()) { Log.e(TAG, "Playback init failed") }