diag(ios audio): setSpeaker returns actual output port + category/mode + native marker
Telemetry only showed 'override didn't throw', and the web __BUILD tag can't tell native binaries apart, so we couldn't see WHERE iOS actually routed the audio or which plugin build ran. setSpeaker now resolves with the real currentRoute output port (Receiver/Speaker/Bluetooth), the live AVAudioSession category/mode/options, and a native-build marker (1.0.2-diag) so the route log is unambiguous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,14 +7,15 @@ import AVFoundation
|
||||
//
|
||||
// WHY THIS SHAPE (earlier version left the earpiece silent):
|
||||
// * The category is configured WITHOUT .defaultToSpeaker, so overrideOutputAudioPort(.none) reliably means
|
||||
// the earpiece/receiver. WITH .defaultToSpeaker, .none falls back to the loudspeaker and the earpiece
|
||||
// never receives audio.
|
||||
// * setSpeaker flips ONLY overrideOutputAudioPort. It must NOT re-setCategory/setActive mid-call: doing so
|
||||
// tears down the audio unit WebKit's WebRTC engine is actively using and leaves the earpiece route silent
|
||||
// (forcing .speaker is a strong enough override to survive it, which is why only speaker "worked").
|
||||
// * WebKit reconfigures the shared AVAudioSession when call audio starts (and on headset plug/unplug), which
|
||||
// would clobber our choice — so we observe routeChangeNotification and re-assert the desired port. That
|
||||
// also removes the "call starts on speaker then drops to silence" flicker.
|
||||
// the earpiece/receiver. WITH .defaultToSpeaker, .none falls back to the loudspeaker.
|
||||
// * setSpeaker flips ONLY overrideOutputAudioPort — no re-setCategory/setActive mid-call, which would tear
|
||||
// down the audio unit WebKit's WebRTC engine is using and silence the earpiece.
|
||||
// * WebKit reconfigures the shared AVAudioSession when call audio starts / on headset plug-unplug, which
|
||||
// would clobber our choice — so we observe routeChangeNotification and re-assert the desired port.
|
||||
//
|
||||
// DIAGNOSTICS: setSpeaker resolves with the ACTUAL current output port + live category/mode/options and a
|
||||
// native-build marker, so the JS `route` telemetry shows exactly where iOS put the audio (the web __BUILD
|
||||
// tag can't distinguish native binaries). This is what turns "override didn't throw" into "audio is on X".
|
||||
@objc(AudioRoutePlugin)
|
||||
public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "AudioRoutePlugin"
|
||||
@@ -23,6 +24,9 @@ public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "setSpeaker", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
// Bump on every native change so the telemetry unambiguously identifies which binary is running.
|
||||
private static let nativeTag = "1.0.2-diag"
|
||||
|
||||
// Desired output — the source of truth, re-applied on every route change. Defaults to earpiece; the JS
|
||||
// side calls setSpeaker(true) at call start when it wants the loudspeaker (meetRoute defaults to 'speaker').
|
||||
private var wantSpeaker = false
|
||||
@@ -66,13 +70,27 @@ public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// Snapshot of what iOS actually selected — this is the data that ends the guessing.
|
||||
private func diag(_ applied: String) -> [String: Any] {
|
||||
let s = AVAudioSession.sharedInstance()
|
||||
let outs = s.currentRoute.outputs.map { $0.portType.rawValue }.joined(separator: "+")
|
||||
return [
|
||||
"route": applied,
|
||||
"native": AudioRoutePlugin.nativeTag,
|
||||
"outputs": outs.isEmpty ? "(none)" : outs, // "Receiver"=earpiece, "Speaker", "BluetoothA2DP"...
|
||||
"category": s.category.rawValue,
|
||||
"mode": s.mode.rawValue,
|
||||
"opts": Int(s.categoryOptions.rawValue)
|
||||
]
|
||||
}
|
||||
|
||||
@objc func setSpeaker(_ call: CAPPluginCall) {
|
||||
wantSpeaker = call.getBool("on") ?? true
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
// Flip ONLY the output port. No setCategory/setActive here — that would disrupt WebKit's audio.
|
||||
try session.overrideOutputAudioPort(wantSpeaker ? .speaker : .none)
|
||||
call.resolve(["route": wantSpeaker ? "speaker" : "earpiece"])
|
||||
call.resolve(diag(wantSpeaker ? "speaker" : "earpiece"))
|
||||
} catch {
|
||||
call.reject(error.localizedDescription)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user