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)])
|
||||
}
|
||||
|
||||
@@ -3815,7 +3815,7 @@ async function setupNativeCall(){
|
||||
// Enforce the muted-by-default rule against the plugin's actual mic once the room connects (belt-and-braces
|
||||
// for builds whose plugin still connects the mic live). meetMic is the source of truth for the mic button.
|
||||
NC.addListener('callConnected', ()=>{ pdbg('nc-connected'); if(meetNative){ try{ NC.setMuted({ muted:!meetMic }); }catch(_){} } });
|
||||
NC.addListener('audioActivated', (e)=>{ pdbg('nc-audio', { ok:!!(e&&e.ok), err:(e&&e.error)||'' }); }); // CallKit audio-session activation (debug intermittent audio)
|
||||
NC.addListener('audioActivated', (e)=>{ pdbg('nc-audio', { ok:!!(e&&e.ok), err:(e&&e.error)||'', route:(e&&e.route)||'' }); }); // CallKit audio-session activation + output route (debug audio)
|
||||
NC.addListener('callError', (e)=>{ pdbg('nc-error', { err:(e&&e.error)||'' }); });
|
||||
// Muted from the CallKit system UI → reflect it in the meeting window's mic button.
|
||||
NC.addListener('setMuted', (e)=>{ if(!e||meetState!=='call'||!meetNative) return; meetMic=!e.muted; try{ updateMicBtn(); setTileMute('__local', !meetMic); meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); }catch(_){} });
|
||||
|
||||
Reference in New Issue
Block a user