fix(ios audio): pin .voiceChat before override(.none) so earpiece is reachable
Root cause (WebKit source MediaSessionManagerCocoa.mm + Apple DTS, confirmed by telemetry): WKWebView's WebRTC re-pins the session to mode .videoChat while capture is active, and .videoChat auto-implies .defaultToSpeaker. So override(.none) reverts to the mode default = LOUDSPEAKER, and .none alone can never reach the earpiece once WebKit flips the mode. Our first override won only because .voiceChat was still active. Fix: for earpiece, setMode(.voiceChat) (its default route IS the receiver) before override(.none) in both setSpeaker and the debounced route-change re-assert. Add an accessory guard so a connected BT/wired headset isn't yanked to the built-in receiver. Reconcile the launch patch: drop .defaultToSpeaker from inject-audio.js so it stops contradicting the plugin. native marker 1.0.4-mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,24 +9,28 @@ const p = process.argv[2];
|
||||
if (!p || !fs.existsSync(p)) { console.log(' (AppDelegate.swift not found — audio patch skipped)'); process.exit(0); }
|
||||
try {
|
||||
let s = fs.readFileSync(p, 'utf8');
|
||||
if (s.includes('.defaultToSpeaker')) { console.log(' audio launch-default already patched'); process.exit(0); }
|
||||
if (s.includes('bzcAudioLaunch')) { console.log(' audio launch baseline already patched'); process.exit(0); }
|
||||
const orig = s;
|
||||
if (!s.includes('import AVFoundation')) {
|
||||
if (s.includes('import Capacitor')) s = s.replace('import Capacitor', 'import Capacitor\nimport AVFoundation');
|
||||
else if (s.includes('import UIKit')) s = s.replace('import UIKit', 'import UIKit\nimport AVFoundation');
|
||||
}
|
||||
// NOTE: NO .defaultToSpeaker here. The earpiece<->speaker toggle plugin (mobile/plugins/audio-route) drives
|
||||
// the output port explicitly and needs .voiceChat's receiver default; .defaultToSpeaker would invert that and
|
||||
// make earpiece unreachable. This is only a launch-time baseline — WebKit re-pins the session per call anyway.
|
||||
const launch = [
|
||||
' // Default call audio to the loudspeaker (iOS uses the quiet earpiece otherwise).',
|
||||
' // bzcAudioLaunch: baseline voice-call audio session (earpiece-capable; the AudioRoute plugin and',
|
||||
' // the JS meet UI force the speaker per call). Keep in sync with mobile/plugins/audio-route load().',
|
||||
' do {',
|
||||
' let audioSession = AVAudioSession.sharedInstance()',
|
||||
' try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])',
|
||||
' try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP])',
|
||||
' try audioSession.setActive(true)',
|
||||
' } catch { }',
|
||||
'',
|
||||
].join('\n');
|
||||
const m = s.match(/func\s+application\([^)]*didFinishLaunchingWithOptions[^)]*\)\s*->\s*Bool\s*\{[^\n]*\n/);
|
||||
if (m) { const idx = m.index + m[0].length; s = s.slice(0, idx) + launch + s.slice(idx); }
|
||||
if (s !== orig && s.includes('.defaultToSpeaker')) { fs.writeFileSync(p, s); console.log(' AVAudioSession launch default-to-speaker injected'); }
|
||||
if (s !== orig && s.includes('bzcAudioLaunch')) { fs.writeFileSync(p, s); console.log(' AVAudioSession launch baseline injected'); }
|
||||
else { console.log(' (AppDelegate pattern not matched — audio patch skipped)'); }
|
||||
} catch (e) {
|
||||
console.log(' (audio patch error, skipped: ' + (e && e.message) + ')');
|
||||
|
||||
Reference in New Issue
Block a user