fix(ios,audio): don't call setPreferredInput after speaker/receiver override (silent route reversion)
User report: 'speakerphone (built-in mic input)' logs printed
successfully but audio output didn't actually switch to speaker.
No exception thrown by either AVAudioSession call.
Root cause:
Previous _selectSpeaker called:
1. await overrideOutputAudioPort(.speaker)
2. await setPreferredInput(builtInMic)
Apple-documented behavior in .voiceChat mode: when
setPreferredInput is called, iOS recalculates the entire route
based on the natural input/output pairing for the selected port.
Built-in mic's natural output pairing is the receiver/earpiece
(matches the 'I'm talking on a phone' UX of .voiceChat). So step 2
caused iOS to SILENTLY REVERT the speaker override from step 1
and route output back through the earpiece.
Net: await chain returned without exception (both calls 'succeeded'
in API terms), debugPrint logged the success message, but the user
heard the call audio coming out of the earpiece, not the speaker.
Same issue affected _selectReceiver (after the speaker bug fix
was reverted): redundant setPreferredInput(builtInMic) call risked
the same recalc race.
Fix:
* _selectSpeaker: only call overrideOutputAudioPort(.speaker).
No setPreferredInput. The override alone is sufficient \u2014 the
input stays on whatever the system was already using (built-in
mic by default, or BT/wired if connected).
* _selectReceiver: only call overrideOutputAudioPort(.none).
No setPreferredInput. Removing the speaker override naturally
returns to .voiceChat's default route (receiver).
* _selectInput (BT / wired / USB): unchanged. These inputs PAIR
their own output device, so .none + setPreferredInput is the
correct combo (user hears audio through the same device they
speak into).
flutter build ios --release --no-codesign: 21.2 s, Runner.app
30.4 MB.
This commit is contained in:
@@ -768,29 +768,24 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
|
||||
Future<void> _selectSpeaker() async {
|
||||
try {
|
||||
// Apple docs: overrideOutputAudioPort requires the audio
|
||||
// session's category to be .playAndRecord AND the session
|
||||
// active. We set both in AppDelegate.swift on launch +
|
||||
// foreground (commit 0466000 / 4ee2b38 / be160f5). If this
|
||||
// setter throws, log the underlying error so the user can
|
||||
// see what's wrong instead of a silent no-op picker.
|
||||
// Apple-documented quirk: in .voiceChat mode, calling
|
||||
// setPreferredInput(builtInMic) AFTER overrideOutputAudioPort(.speaker)
|
||||
// causes iOS to recalculate the route. Built-in mic naturally
|
||||
// pairs with the receiver (not the speaker), so the system
|
||||
// SILENTLY REVERTS the speaker override and routes audio
|
||||
// back through the earpiece. Net: the await chain returns
|
||||
// successfully ('no exception'), but the user hears no
|
||||
// change.
|
||||
//
|
||||
// Also: setPreferredInput(builtInMic) AFTER the override so
|
||||
// the input route matches the output we just forced. Without
|
||||
// pairing input + output, the session can land in an
|
||||
// inconsistent state where speakerphone is on but the mic
|
||||
// is still routed to a previously-selected BT input \u2014 the
|
||||
// user hears themselves through the speaker but the server
|
||||
// hears nothing.
|
||||
// Fix: do NOT call setPreferredInput when forcing speaker.
|
||||
// The speaker override is sufficient on its own \u2014 input
|
||||
// remains on whatever the system was already using (built-in
|
||||
// mic by default, or BT/wired if connected and selected
|
||||
// elsewhere). For consistency, only switch input when the
|
||||
// user explicitly picks a non-speaker input row.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.speaker);
|
||||
final builtIn = _availableInputs
|
||||
.where((p) => p.portType == AVAudioSessionPort.builtInMic)
|
||||
.toList();
|
||||
if (builtIn.isNotEmpty) {
|
||||
await AVAudioSession().setPreferredInput(builtIn.first);
|
||||
}
|
||||
debugPrint('chanora: audio output -> speakerphone (built-in mic input)');
|
||||
debugPrint('chanora: audio output -> speakerphone (override applied)');
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectSpeaker FAILED: $e\n$st');
|
||||
}
|
||||
@@ -800,17 +795,14 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
|
||||
Future<void> _selectReceiver() async {
|
||||
try {
|
||||
// Same quirk in reverse: removing the speaker override
|
||||
// (.none) is enough to restore the .voiceChat default route,
|
||||
// which is the built-in receiver. Calling setPreferredInput
|
||||
// explicitly here is redundant and risks the same recalc
|
||||
// race that broke _selectSpeaker before.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
|
||||
// Also clear any preferred input so it falls back to built-in.
|
||||
// We do that by selecting the built-in mic if available.
|
||||
final builtIn = _availableInputs
|
||||
.where((p) => p.portType == AVAudioSessionPort.builtInMic)
|
||||
.toList();
|
||||
if (builtIn.isNotEmpty) {
|
||||
await AVAudioSession().setPreferredInput(builtIn.first);
|
||||
}
|
||||
debugPrint('chanora: audio output -> receiver/earpiece');
|
||||
debugPrint('chanora: audio output -> receiver (override cleared)');
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectReceiver FAILED: $e\n$st');
|
||||
}
|
||||
@@ -821,7 +813,10 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
Future<void> _selectInput(AVAudioSessionPortDescription port) async {
|
||||
try {
|
||||
// Drop any speakerphone override so the route follows the
|
||||
// selected input (BT / wired headset move output to themselves).
|
||||
// selected input. BT / wired headset / USB inputs pair their
|
||||
// OWN output (the user hears audio through the same device
|
||||
// they speak into), so .none + setPreferredInput is the
|
||||
// correct combo here.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
|
||||
await AVAudioSession().setPreferredInput(port);
|
||||
|
||||
Reference in New Issue
Block a user