Skip to content

Add iOS session states, speaker preference, and route events (PAR-021) - #372

Draft
MaxHeimbrock wants to merge 4 commits into
max/ios-foreground-audio-recoveryfrom
max/par-021-unity-ios-session-features
Draft

Add iOS session states, speaker preference, and route events (PAR-021)#372
MaxHeimbrock wants to merge 4 commits into
max/ios-foreground-audio-recoveryfrom
max/par-021-unity-ios-session-features

Conversation

@MaxHeimbrock

Copy link
Copy Markdown
Contributor

Background

PR #346 made iOS playout stable by putting the app in charge of the audio session, but it left the session in a single static config (PlayAndRecord + VideoChat) for the whole PlatformAudio lifetime — so Unity music plays at call volume even when the user is just listening — and PAR-019's routing API (#370) had no iOS backend: IsSpeakerOutputPreferred round-tripped without effect and DevicesChanged never fired. This PR is Option A's iOS half: speaker-vs-earpiece preference, session state transitions, and route-change observation, kept behind the C#↔.mm seam so phase B can move the semantics native (PAR-008/PAR-017) and shrink the plugin mechanically.

Stacked on #369 (which is on #346), and the branch has max/par-019-routing-api-surface (#370) merged in for the IRouteController seam — the diff shows PAR-019's commit until #370 merges. Merge order: #346#369#370 → this.

Changes

  • Session-state machine in LiveKitAudioSession.mm, driven from C# (PlatformAudio knows recording state): ctor → playout-only, StartRecording/StopRecording → recording/playout-only, SetSessionAudioEnabled(false) → idle. State table (documented in the .mm header):
    • idle / playout-only: PlayAndRecord + mode Default + MixWithOthers (+ DefaultToSpeaker iff speaker preferred) — music-friendly between/before calls
    • recording: PlayAndRecord + mode VideoChat (speaker) / VoiceChat (earpiece), no DefaultToSpeaker — the preference is expressed by the mode alone, never overrideOutputAudioPort, so headsets always win
  • Open question from the card, resolved with evidence: the fork's ADM supports playout-only via an input-disabled VPIO unit (InitPlayout → InitPlayOrRecord(false) → Initialize(sr, enable_input=false)), but nothing establishes VPIO under category Playback — hence playout-only keeps PlayAndRecord with mode Default + MixWithOthers. Music volume was measured on device, not assumed.
  • Every apply is mirrored into WebRTC's RTCAudioSessionConfiguration snapshot via the reflected setWebRTCConfiguration: (still NSClassFromString only, no link-time dependency), so ADM-driven reconfigurations re-apply our config instead of the +initialize-frozen snapshot. The Recover iOS platform audio after backgrounding (Unity 6 focus race) #369 foreground recovery now re-asserts the current state's config instead of hardcoded VideoChat, and logs expected vs. actual.
  • New IosRouteController implements the PAR-019 seam: playout list = the session's current output route with real Kind/IsSelected (port-type mapping lives in the .mm), DevicesChanged raised from AVAudioSessionRouteChangeNotification (marshalled to the Unity main thread by PlatformAudio), OutputPreference reduces to the Speaker/Earpiece relative order, SelectOutput throws the documented NotSupportedException pointing at AVRoutePickerView.
  • Audio-unit rebuild after mid-call mode changes: the ADM skips the VPIO rebuild on same-rate route changes (HandleValidRouteChange → HandleSampleRateChange no-ops), which left the loudspeaker attenuated by receiver-calibrated echo control after an earpiece → speaker toggle (device-observed). LiveKit_ApplySessionConfig now cycles isAudioEnabled after a mode change while call audio is wanted — the same rebuild the foreground recovery uses — at the cost of a brief audio gap per toggle.
  • iOS 26 SDK: AllowBluetoothAllowBluetoothHFP behind the same version guard the WebRTC fork uses; no option is load-bearing for the in-call speaker preference.
  • Public XML docs updated truthfully for iOS (device list = current route, event semantics, typed SelectOutput throw). No public API surface changes (PAR-019's surface is frozen).

Testing

  • csc on the real iOS-player Bee rsp and the editor rsp, both clean; clang -fsyntax-only against the iphoneos SDK under ARC and MRC
  • Device-tested (2026-08-14, full timeline on both Unity 2022.3.62f1 and Unity 6 builds): listen-only join keeps music on the loudspeaker in the music-friendly config; unmute switches to VideoChat with working AEC; mid-call speaker↔earpiece toggling applies immediately; DevicesChanged fires on BT connect/disconnect and mode switches, on the Unity main thread; interruption (Siri/call) and background/foreground recovery work with who-won logs; music survives hang-up; session restores on dispose. The quiet-loudspeaker-after-toggle issue found in the first Unity 6 round is fixed by the rebuild commit and was re-verified on device.

🤖 Generated with Claude Code

MaxHeimbrock and others added 4 commits August 14, 2026 11:43
Define the phase-A routing API on PlatformAudio, purely in C# and shaped
identically to the planned FFI-backed implementation so app code written
against it survives the plumbing swap:

- AudioOutputKind enum (values mirror the planned FFI proto enum 1:1)
- AudioDevice.Kind / AudioDevice.IsSelected
- OutputPreference ranked policy (default BT > wired > speaker > earpiece)
- IsSpeakerOutputPreferred as documented sugar over the list order
- SelectOutput / ClearOutputOverride sticky override
- DevicesChanged event (playout, recording) on the Unity main thread
- internal IRouteController seam with desktop (FFI enumeration/GUID
  selection) and unsupported-mobile implementations; Android/iOS
  backends plug into the seam in follow-up work

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… into max/par-021-unity-ios-session-features
The audio session config is now derived from a small state machine
(idle / playout-only / recording) driven from C#, where PlatformAudio
knows the recording state, plus a speaker-vs-earpiece preference
expressed through the session mode (VideoChat/VoiceChat) — never
overrideOutputAudioPort, so external devices always win. Non-recording
states drop to mode Default + MixWithOthers to keep Unity audio
unprocessed between calls; PlayAndRecord stays because the fork's ADM
supports playout-only via an input-disabled VPIO unit but nothing
guarantees VPIO under the Playback category. Every apply is mirrored
into WebRTC's RTCAudioSessionConfiguration snapshot (reflected, no
link-time dependency) so ADM-driven restarts re-apply the same config.

Route changes are observed via AVAudioSessionRouteChangeNotification
and forwarded to the new IosRouteController, which implements the
PAR-019 seam: the playout list is the session's current output route
with real Kind/IsSelected, DevicesChanged is raised on route changes
(marshalled to the Unity main thread by PlatformAudio), OutputPreference
reduces to the Speaker/Earpiece relative order, and SelectOutput throws
the documented NotSupportedException pointing at AVRoutePickerView.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ADM skips the VPIO rebuild on route changes that keep the hardware
sample rate (HandleValidRouteChange -> HandleSampleRateChange no-ops),
so a live VoiceChat -> VideoChat switch left the unit calibrated for
the receiver — device-observed as an attenuated loudspeaker after an
earpiece -> speaker toggle. Cycle isAudioEnabled after a mode change
(when call audio is wanted) to force a clean rebuild against the new
route, the same mechanism the foreground recovery already uses. The
recovery and configure paths pass NO: the former cycles itself, the
latter runs before the unit exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant