From 9ca8f78c69eb42d0072182dab773ef5502686c9a Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 23 Jan 2025 13:53:58 +0000 Subject: [PATCH] fix(ENGDESK-37644): Fix CallKit speaker button inconsistency - Updated AVAudioSession configuration with proper settings - Enhanced speaker/earpiece handling with proper locking - Added audio route change monitoring for CallKit integration - Improved error handling and logging - Added UI state notifications for audio route changes Fixes: ENGDESK-37644 --- TelnyxRTC/Telnyx/TxClient.swift | 93 +++++++++++++++++++++++++++++- TelnyxRTC/Telnyx/WebRTC/Peer.swift | 17 +++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/TelnyxRTC/Telnyx/TxClient.swift b/TelnyxRTC/Telnyx/TxClient.swift index 1f2639ec..c08c1c71 100644 --- a/TelnyxRTC/Telnyx/TxClient.swift +++ b/TelnyxRTC/Telnyx/TxClient.swift @@ -199,6 +199,51 @@ public class TxClient { self.serverConfiguration = TxServerConfiguration() self.configure() sessionId = UUID().uuidString.lowercased() + + // Start monitoring audio route changes + setupAudioRouteChangeMonitoring() + } + + private func setupAudioRouteChangeMonitoring() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAudioRouteChange), + name: AVAudioSession.routeChangeNotification, + object: nil) + } + + @objc private func handleAudioRouteChange(notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { + return + } + + let session = AVAudioSession.sharedInstance() + let currentRoute = session.currentRoute + + // Check if we have any output ports + guard let output = currentRoute.outputs.first else { + return + } + + Logger.log.i(message: "Audio route changed: \(output.portType), reason: \(reason)") + + switch reason { + case .categoryChange, .override, .routeConfigurationChange: + // Update speaker state based on current output + let isSpeaker = output.portType == .builtInSpeaker + speakerOn = isSpeaker + + // Notify UI of the change + NotificationCenter.default.post( + name: NSNotification.Name("AudioRouteChanged"), + object: nil, + userInfo: ["isSpeakerOn": isSpeaker] + ) + default: + break + } } // MARK: - Connection handling @@ -257,6 +302,12 @@ public class TxClient { call.hangup() } self.calls.removeAll() + + // Remove audio route change observer + NotificationCenter.default.removeObserver(self, + name: AVAudioSession.routeChangeNotification, + object: nil) + socket?.disconnect(reconnect: false) delegate?.onSocketDisconnected() } @@ -627,10 +678,29 @@ extension TxClient { public func setEarpiece() { do { let audioSession = AVAudioSession.sharedInstance() + let rtcAudioSession = RTCAudioSession.sharedInstance() + + rtcAudioSession.lockForConfiguration() + defer { + rtcAudioSession.unlockForConfiguration() + } + + // Configure audio session for earpiece output try audioSession.overrideOutputAudioPort(.none) + try rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord, + options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers]) + + // Update speaker state speakerOn = false + + // Post notification for UI update + NotificationCenter.default.post(name: NSNotification.Name("AudioRouteChanged"), + object: nil, + userInfo: ["isSpeakerOn": false]) + + Logger.log.i(message: "Earpiece mode enabled successfully") } catch let error { - Logger.log.e(message: "Error setting Earpiece \(error)") + Logger.log.e(message: "Error setting Earpiece: \(error)") } } @@ -638,10 +708,29 @@ extension TxClient { public func setSpeaker() { do { let audioSession = AVAudioSession.sharedInstance() + let rtcAudioSession = RTCAudioSession.sharedInstance() + + rtcAudioSession.lockForConfiguration() + defer { + rtcAudioSession.unlockForConfiguration() + } + + // Configure audio session for speaker output try audioSession.overrideOutputAudioPort(.speaker) + try rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord, + options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP, .mixWithOthers]) + + // Update speaker state speakerOn = true + + // Post notification for UI update + NotificationCenter.default.post(name: NSNotification.Name("AudioRouteChanged"), + object: nil, + userInfo: ["isSpeakerOn": true]) + + Logger.log.i(message: "Speaker mode enabled successfully") } catch let error { - Logger.log.e(message: "Error setting Speaker \(error)") + Logger.log.e(message: "Error setting Speaker: \(error)") } } } diff --git a/TelnyxRTC/Telnyx/WebRTC/Peer.swift b/TelnyxRTC/Telnyx/WebRTC/Peer.swift index 1bd2b9f2..1a735f5b 100644 --- a/TelnyxRTC/Telnyx/WebRTC/Peer.swift +++ b/TelnyxRTC/Telnyx/WebRTC/Peer.swift @@ -136,11 +136,24 @@ class Peer : NSObject, WebRTCEventHandler { Logger.log.i(message: "Peer:: Configuring AVAudioSession") self.rtcAudioSession.useManualAudio = true self.rtcAudioSession.isAudioEnabled = false + + // Configure audio session for VoIP calls try self.rtcAudioSession.setCategory(AVAudioSession.Category(rawValue: AVAudioSession.Category.playAndRecord.rawValue)) try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat) - Logger.log.i(message: "Peer:: Configuring AVAudioSession configured") + + // Enable mixing with other audio sessions + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord, + options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers]) + + // Set preferred audio I/O buffer duration + try self.rtcAudioSession.setPreferredIOBufferDuration(0.005) + + // Set preferred sample rate + try self.rtcAudioSession.setPreferredSampleRate(44100.0) + + Logger.log.i(message: "Peer:: AVAudioSession configured successfully") } catch let error { - Logger.log.e(message: "Peer:: Error changing AVAudioSession category: \(error.localizedDescription)") + Logger.log.e(message: "Peer:: Error configuring AVAudioSession: \(error.localizedDescription)") } self.rtcAudioSession.unlockForConfiguration() }