Native calls: route audio to the speaker by default (was earpiece)
The .voiceChat AVAudioSession mode defaults to the earpiece, so call audio came out of the earpiece and only settled after mute/unmute toggling. Switch to .videoChat + .defaultToSpeaker + allow Bluetooth so audio goes to the loudspeaker by default while wired/BT headsets still win. Add preferSpeaker() (override to speaker when on the built-in receiver) in didActivate AND after mic toggles (the route can flip back to earpiece on unmute). Report the chosen route in telemetry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,16 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
Task { await r?.disconnect() }
|
||||
}
|
||||
|
||||
// Route call audio to the LOUDSPEAKER when we'd otherwise be on the quiet earpiece. Guarded so a connected
|
||||
// wired/Bluetooth headset (any non-receiver route) still wins. Re-asserted on mute toggles because enabling
|
||||
// the mic can flip the route back to the earpiece.
|
||||
private func preferSpeaker() {
|
||||
let s = AVAudioSession.sharedInstance()
|
||||
if s.currentRoute.outputs.contains(where: { $0.portType == .builtInReceiver }) {
|
||||
try? s.overrideOutputAudioPort(.speaker)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JS-callable methods
|
||||
|
||||
@objc func getToken(_ call: CAPPluginCall) { call.resolve(["token": voipToken]) }
|
||||
@@ -285,7 +295,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
|
||||
public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
let r = room
|
||||
Task { try? await r?.localParticipant.setMicrophone(enabled: !action.isMuted) }
|
||||
Task { [weak self] in
|
||||
try? await r?.localParticipant.setMicrophone(enabled: !action.isMuted)
|
||||
self?.preferSpeaker() // toggling the mic can flip the route back to the earpiece — re-assert speaker
|
||||
}
|
||||
notifyListeners("setMuted", data: ["callUUID": action.callUUID.uuidString, "muted": action.isMuted])
|
||||
action.fulfill()
|
||||
}
|
||||
@@ -295,9 +308,14 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
// dead mic / no-audio and late speaker routing. Don't call setActive(true): CallKit already activated it.
|
||||
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
do {
|
||||
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers])
|
||||
// Route to the LOUDSPEAKER by default (earpiece was the .voiceChat default). .videoChat +
|
||||
// .defaultToSpeaker prefers the speaker while still letting wired/Bluetooth headsets win.
|
||||
try audioSession.setCategory(.playAndRecord, mode: .videoChat,
|
||||
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
|
||||
try AudioManager.shared.setEngineAvailability(.default)
|
||||
notifyListeners("audioActivated", data: ["ok": true])
|
||||
preferSpeaker() // belt-and-braces: if we still landed on the earpiece, force the speaker
|
||||
let route = audioSession.currentRoute.outputs.first?.portType.rawValue ?? "none"
|
||||
notifyListeners("audioActivated", data: ["ok": true, "route": route])
|
||||
} catch {
|
||||
notifyListeners("audioActivated", data: ["ok": false, "error": String(describing: error)])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user