2026-07-20 00:31:25 +05:30
|
|
|
// Patch the freshly-generated Capacitor iOS AppDelegate for call audio. Run on Codemagic (macOS) from
|
|
|
|
|
// ios-patch.sh: node mobile/scripts/inject-audio.js mobile/ios/App/App/AppDelegate.swift
|
|
|
|
|
// It does two things, both TOLERANT (exits 0 and no-ops if the template doesn't match, so it can NEVER
|
|
|
|
|
// fail the build):
|
|
|
|
|
// 1. At launch, set the AVAudioSession so calls DEFAULT to the loudspeaker (iOS uses the quiet earpiece).
|
|
|
|
|
// 2. Append an AudioRoute Capacitor plugin (Capacitor 7 auto-registers any CAPBridgedPlugin in the app
|
|
|
|
|
// target) so the web can switch earpiece<->speaker at runtime via
|
|
|
|
|
// window.Capacitor.Plugins.AudioRoute.setSpeaker({ on: true|false }).
|
|
|
|
|
// If the plugin fails to register for any reason, the web call simply no-ops — it cannot crash the app.
|
2026-07-19 16:18:52 +05:30
|
|
|
const fs = require('fs');
|
|
|
|
|
const p = process.argv[2];
|
2026-07-20 00:31:25 +05:30
|
|
|
if (!p || !fs.existsSync(p)) { console.log(' (AppDelegate.swift not found — audio patch skipped)'); process.exit(0); }
|
2026-07-19 16:18:52 +05:30
|
|
|
try {
|
|
|
|
|
let s = fs.readFileSync(p, 'utf8');
|
2026-07-20 00:31:25 +05:30
|
|
|
if (s.includes('AudioRoutePlugin')) { console.log(' audio already patched'); process.exit(0); }
|
2026-07-19 16:18:52 +05:30
|
|
|
const orig = s;
|
2026-07-20 00:31:25 +05:30
|
|
|
|
|
|
|
|
// ---- imports ----
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
if (!s.includes('import Capacitor')) {
|
|
|
|
|
if (s.includes('import UIKit')) s = s.replace('import UIKit', 'import UIKit\nimport Capacitor');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- 1) launch: default call audio to the loudspeaker ----
|
|
|
|
|
const launch = [
|
2026-07-19 16:18:52 +05:30
|
|
|
' // 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/);
|
2026-07-20 00:31:25 +05:30
|
|
|
if (m && !s.includes('.defaultToSpeaker')) { const idx = m.index + m[0].length; s = s.slice(0, idx) + launch + s.slice(idx); }
|
|
|
|
|
|
|
|
|
|
// ---- 2) append the AudioRoute plugin (earpiece/speaker toggle) ----
|
|
|
|
|
const plugin = [
|
|
|
|
|
'',
|
|
|
|
|
'// Earpiece <-> speaker toggle for calls. Capacitor 7 auto-registers any CAPBridgedPlugin in the app',
|
|
|
|
|
'// target, so the web calls window.Capacitor.Plugins.AudioRoute.setSpeaker({ on: true|false }).',
|
|
|
|
|
'@objc(AudioRoutePlugin)',
|
|
|
|
|
'public class AudioRoutePlugin: CAPPlugin, CAPBridgedPlugin {',
|
|
|
|
|
' public let identifier = "AudioRoutePlugin"',
|
|
|
|
|
' public let jsName = "AudioRoute"',
|
|
|
|
|
' public let pluginMethods: [CAPPluginMethod] = [',
|
|
|
|
|
' CAPPluginMethod(name: "setSpeaker", returnType: CAPPluginReturnPromise)',
|
|
|
|
|
' ]',
|
|
|
|
|
' @objc func setSpeaker(_ call: CAPPluginCall) {',
|
|
|
|
|
' let on = call.getBool("on") ?? true',
|
|
|
|
|
' let session = AVAudioSession.sharedInstance()',
|
|
|
|
|
' do {',
|
|
|
|
|
' try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP])',
|
|
|
|
|
' try session.setActive(true)',
|
|
|
|
|
' try session.overrideOutputAudioPort(on ? .speaker : .none)',
|
|
|
|
|
' call.resolve(["route": on ? "speaker" : "earpiece"])',
|
|
|
|
|
' } catch {',
|
|
|
|
|
' call.reject(error.localizedDescription)',
|
|
|
|
|
' }',
|
|
|
|
|
' }',
|
|
|
|
|
'}',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n');
|
|
|
|
|
s = s + plugin;
|
|
|
|
|
|
|
|
|
|
if (s !== orig) { fs.writeFileSync(p, s); console.log(' AVAudioSession default-to-speaker + AudioRoute plugin injected'); }
|
|
|
|
|
else { console.log(' (AppDelegate pattern not matched — audio patch skipped)'); }
|
2026-07-19 16:18:52 +05:30
|
|
|
} catch (e) {
|
2026-07-20 00:31:25 +05:30
|
|
|
console.log(' (audio patch error, skipped: ' + (e && e.message) + ')');
|
2026-07-19 16:18:52 +05:30
|
|
|
}
|
|
|
|
|
process.exit(0);
|