fix Live Call: route audio to speaker with proper communication device API

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.
This commit is contained in:
Bruno Deanoz 2026-06-24 13:49:04 +02:00
parent 0baab7db9e
commit f5d7d9a9ab
2 changed files with 31 additions and 6 deletions

View file

@ -62,6 +62,35 @@ class AudioPipeline(private val context: Context) {
@Volatile private var ducking = false @Volatile private var ducking = false
@Volatile private var capturing = 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 ────────────────────────────────────────────────────────── // ─── Capture ──────────────────────────────────────────────────────────
fun startCapture(scope: CoroutineScope): Boolean { fun startCapture(scope: CoroutineScope): Boolean {
@ -92,11 +121,6 @@ class AudioPipeline(private val context: Context) {
recorder = rec recorder = rec
previousAudioMode = audioManager.mode
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
@Suppress("DEPRECATION")
audioManager.isSpeakerphoneOn = true
if (AcousticEchoCanceler.isAvailable()) { if (AcousticEchoCanceler.isAvailable()) {
try { try {
aec = AcousticEchoCanceler.create(rec.audioSessionId) aec = AcousticEchoCanceler.create(rec.audioSessionId)
@ -147,7 +171,6 @@ class AudioPipeline(private val context: Context) {
aec = null aec = null
recorder?.release() recorder?.release()
recorder = null recorder = null
audioManager.mode = previousAudioMode
} }
// ─── Playback ───────────────────────────────────────────────────────── // ─── Playback ─────────────────────────────────────────────────────────
@ -220,6 +243,7 @@ class AudioPipeline(private val context: Context) {
fun release() { fun release() {
stopCapture() stopCapture()
stopPlayback() stopPlayback()
exitCallMode()
} }
// ─── Helpers ────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────

View file

@ -168,6 +168,7 @@ class LiveCallService : Service() {
connectTimeoutJob?.cancel() connectTimeoutJob?.cancel()
_status.value = CallStatus.LISTENING _status.value = CallStatus.LISTENING
pipeline.enterCallMode()
if (!pipeline.initPlayback()) { if (!pipeline.initPlayback()) {
Log.e(TAG, "Playback init failed") Log.e(TAG, "Playback init failed")
} }