Files
BizGaze_Remote/mobile/scripts/inject-audio.js
T
Sravan 90d49d29d0 speaker: native AudioRoute plugin for real earpiece<->speaker toggle on iOS (batch144)
- ios-patch.sh now injects an AudioRoutePlugin (CAPBridgedPlugin, Capacitor 7 auto-registers it)
  into AppDelegate.swift with setSpeaker({on}) -> AVAudioSession.overrideOutputAudioPort. Tolerant/
  build-safe: if it doesn't register, the web call just no-ops (can't crash or fail the build).
- web: nativeAudioRoute()/bzApplyRoute() drive the plugin; toggleSpeakerphone + the on-join/on-tap
  unlock now actually switch the route on iOS (setSinkId can't). canRouteAudio() shows the toggle
  when the native plugin is present. Dormant until the next Codemagic build ships the plugin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:31:25 +05:30

75 lines
3.9 KiB
JavaScript

// 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.
const fs = require('fs');
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('AudioRoutePlugin')) { console.log(' audio already patched'); process.exit(0); }
const orig = s;
// ---- 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 = [
' // 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 && !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)'); }
} catch (e) {
console.log(' (audio patch error, skipped: ' + (e && e.message) + ')');
}
process.exit(0);