From 134cb8999d432e215a35fdff4d70978cea3ad71d Mon Sep 17 00:00:00 2001 From: sravan Date: Sun, 19 Jul 2026 16:18:52 +0530 Subject: [PATCH] ios: default in-call audio to the loudspeaker (AVAudioSession) at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls were routing to the quiet earpiece. ios-patch.sh now runs a Node helper (Node 20 is already in the build env) that injects an AVAudioSession .playAndRecord/.voiceChat category with .defaultToSpeaker + Bluetooth into the generated AppDelegate. Tolerant: exits 0 and no-ops if the template differs, so it can never fail the Codemagic build. Verified locally against the Cap 7 AppDelegate template — injects correctly and is idempotent. First pass; if WebRTC re-grabs the session mid-call on device, a follow-up plugin will re-assert .overrideOutputAudioPort(.speaker). Co-Authored-By: Claude Opus 4.8 --- mobile/scripts/inject-audio.js | 36 ++++++++++++++++++++++++++++++++++ mobile/scripts/ios-patch.sh | 12 ++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 mobile/scripts/inject-audio.js diff --git a/mobile/scripts/inject-audio.js b/mobile/scripts/inject-audio.js new file mode 100644 index 0000000..6be7ac9 --- /dev/null +++ b/mobile/scripts/inject-audio.js @@ -0,0 +1,36 @@ +// Inject an AVAudioSession "default to the loudspeaker" setup into the freshly-generated Capacitor iOS +// AppDelegate, so calls don't route to the quiet earpiece. Run on Codemagic (macOS) from ios-patch.sh: +// node mobile/scripts/inject-audio.js mobile/ios/App/App/AppDelegate.swift +// TOLERANT by design: if the file is missing or the template doesn't match, it prints a note and exits 0 +// so it can NEVER fail the build. First-pass fix — if iOS WebRTC re-grabs the session mid-call on device, +// we follow up with a plugin that re-asserts .overrideOutputAudioPort(.speaker). +const fs = require('fs'); +const p = process.argv[2]; +if (!p || !fs.existsSync(p)) { console.log(' (AppDelegate.swift not found — AVAudioSession patch skipped)'); process.exit(0); } +try { + let s = fs.readFileSync(p, 'utf8'); + if (s.includes('AVAudioSession')) { console.log(' AVAudioSession already patched'); process.exit(0); } + const orig = s; + 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'); + const inject = [ + ' // Default call audio to the loudspeaker (iOS uses the quiet earpiece otherwise).', + ' do {', + ' let audioSession = AVAudioSession.sharedInstance()', + ' try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .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) + inject + s.slice(idx); } + if (s !== orig && s.includes('AVAudioSession')) { + fs.writeFileSync(p, s); + console.log(' AVAudioSession default-to-speaker injected into AppDelegate'); + } else { + console.log(' (AppDelegate pattern not matched — AVAudioSession patch skipped)'); + } +} catch (e) { + console.log(' (AVAudioSession patch error, skipped: ' + (e && e.message) + ')'); +} +process.exit(0); diff --git a/mobile/scripts/ios-patch.sh b/mobile/scripts/ios-patch.sh index f67fb80..2cef270 100644 --- a/mobile/scripts/ios-patch.sh +++ b/mobile/scripts/ios-patch.sh @@ -38,6 +38,18 @@ set_str CFBundleVersion "$BUILD_NO" # Allow the webview to load our HTTPS origin (we do NOT enable arbitrary cleartext). "$PB" -c "Delete :NSAppTransportSecurity" "$PLIST" 2>/dev/null || true +# ── Default in-call audio to the LOUDSPEAKER (AVAudioSession) ────────────────────────────────────── +# Without this, iOS routes WebRTC call audio to the quiet EARPIECE. A Node helper (Node 20 is already set +# up for this build) injects an AVAudioSession category into the generated AppDelegate so calls default to +# the speaker (headphones/Bluetooth still win when connected). The helper is tolerant and exits 0 even if +# the template differs, so it NEVER fails the build. First-pass fix — if WebRTC re-grabs the session +# mid-call on device, we follow up with a plugin that re-asserts .overrideOutputAudioPort(.speaker). +AD="mobile/ios/App/App/AppDelegate.swift" +if [ -f "$AD" ]; then + echo "Patching AppDelegate audio session" + node "$(dirname "$0")/inject-audio.js" "$AD" || echo " (AVAudioSession patch skipped — non-fatal)" +fi + echo "Info.plist patched:" "$PB" -c "Print :NSCameraUsageDescription" "$PLIST" "$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST"