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):
|
// WHY THIS SHAPE (earlier version left the earpiece silent):
|
||||||
// * The category is configured WITHOUT .defaultToSpeaker, so overrideOutputAudioPort(.none) reliably means
|
// * 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
|
// the earpiece/receiver. WITH .defaultToSpeaker, .none falls back to the loudspeaker.
|
||||||
// never receives audio.
|
// * setSpeaker flips ONLY overrideOutputAudioPort — no re-setCategory/setActive mid-call, which would tear
|
||||||
// * setSpeaker flips ONLY overrideOutputAudioPort. It must NOT re-setCategory/setActive mid-call: doing so
|
// down the audio unit WebKit's WebRTC engine is using and silence the earpiece.
|
||||||
// tears down the audio unit WebKit's WebRTC engine is actively using and leaves the earpiece route silent
|
// * WebKit reconfigures the shared AVAudioSession when call audio starts / on headset plug-unplug, which
|
||||||
// (forcing .speaker is a strong enough override to survive it, which is why only speaker "worked").
|
// would clobber our choice — so we observe routeChangeNotification and re-assert the desired port.
|
||||||
// * 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
|
// DIAGNOSTICS: setSpeaker resolves with the ACTUAL current output port + live category/mode/options and a
|
||||||
// also removes the "call starts on speaker then drops to silence" flicker.
|
// 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)
|
@objc(AudioRoutePlugin)
|
||||||
public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||||
public let identifier = "AudioRoutePlugin"
|
public let identifier = "AudioRoutePlugin"
|
||||||
@@ -23,6 +24,9 @@ public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
CAPPluginMethod(name: "setSpeaker", returnType: CAPPluginReturnPromise)
|
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
|
// 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').
|
// side calls setSpeaker(true) at call start when it wants the loudspeaker (meetRoute defaults to 'speaker').
|
||||||
private var wantSpeaker = false
|
private var wantSpeaker = false
|
||||||
@@ -66,13 +70,27 @@ public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
} catch { }
|
} 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) {
|
@objc func setSpeaker(_ call: CAPPluginCall) {
|
||||||
wantSpeaker = call.getBool("on") ?? true
|
wantSpeaker = call.getBool("on") ?? true
|
||||||
let session = AVAudioSession.sharedInstance()
|
let session = AVAudioSession.sharedInstance()
|
||||||
do {
|
do {
|
||||||
// Flip ONLY the output port. No setCategory/setActive here — that would disrupt WebKit's audio.
|
// Flip ONLY the output port. No setCategory/setActive here — that would disrupt WebKit's audio.
|
||||||
try session.overrideOutputAudioPort(wantSpeaker ? .speaker : .none)
|
try session.overrideOutputAudioPort(wantSpeaker ? .speaker : .none)
|
||||||
call.resolve(["route": wantSpeaker ? "speaker" : "earpiece"])
|
call.resolve(diag(wantSpeaker ? "speaker" : "earpiece"))
|
||||||
} catch {
|
} catch {
|
||||||
call.reject(error.localizedDescription)
|
call.reject(error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audio-route",
|
"name": "audio-route",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "iOS earpiece/speaker audio route toggle for Biz Connect",
|
"description": "iOS earpiece/speaker audio route toggle for Biz Connect",
|
||||||
"main": "dist/plugin.cjs.js",
|
"main": "dist/plugin.cjs.js",
|
||||||
"module": "dist/esm/index.js",
|
"module": "dist/esm/index.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user