ios: default in-call audio to the loudspeaker (AVAudioSession) at build time
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||||
@@ -38,6 +38,18 @@ set_str CFBundleVersion "$BUILD_NO"
|
|||||||
# Allow the webview to load our HTTPS origin (we do NOT enable arbitrary cleartext).
|
# Allow the webview to load our HTTPS origin (we do NOT enable arbitrary cleartext).
|
||||||
"$PB" -c "Delete :NSAppTransportSecurity" "$PLIST" 2>/dev/null || true
|
"$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:"
|
echo "Info.plist patched:"
|
||||||
"$PB" -c "Print :NSCameraUsageDescription" "$PLIST"
|
"$PB" -c "Print :NSCameraUsageDescription" "$PLIST"
|
||||||
"$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST"
|
"$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST"
|
||||||
|
|||||||
Reference in New Issue
Block a user