Skip to content

Audio Session Defaults - #1019

Merged
pblazej merged 3 commits into
mainfrom
blaze/audio-session-defaults
Aug 31, 2026
Merged

Audio Session Defaults#1019
pblazej merged 3 commits into
mainfrom
blaze/audio-session-defaults

Conversation

@pblazej

@pblazej pblazej commented May 27, 2026

Copy link
Copy Markdown
Contributor

Reworks the iOS audio session defaults. No public API changes; observable behavior changes are listed below with rationale.

  • .playAndRecord is sticky for the life of the engine session. Category selection is now a pure selectConfiguration(state:) driven by the engine state plus a hasRecorded bit: .playAndRecord once recording engages, kept until the WebRTC engine stops, .playback otherwise. Muting the microphone no longer flips the category back to .playback — a mid-session category change tears down Apple's Voice Processing I/O and leaves echo cancellation dead for the rest of the call. The bit is keyed to the engine's own session requirement, so external requirement holders (e.g. a prepared SoundPlayer sound, which lives for the lifetime of the prepared sound) don't pin .playAndRecord after the call ends. Covered by new hermetic unit tests (AudioSessionCategorySelectionTests).
  • .mixWithOthers dropped from the .playAndRecord presets (kept on the listener .playback preset). On the in-call path it lets another app hold the audio device alongside the call — a known echo source (see e.g. Agora's echo troubleshooting) — and the WebRTC ADM needs a retry loop to work around a -66637 engine-init race when it is set (related: LiveKit participant audio becomes inaudible during incoming calls or alarms #1011, Audio Recording Fails to Resume System Audio After Incoming Call Interruption in Background #886). It has a turbulent history here — added in Adjust AVAudioSession CategoryOptions (include .mixWithOthers) #228, removed in Remove .mixWithOthers when category .playAndRecord #260, re-entered during the AVAudioEngine ADM rewrite (AVAudioEngine version AudioDeviceModule #536); this is a clean revert. This matches the Flutter SDK's communication preset (allowBluetooth/A2DP/allowAirPlay, no mixWithOthers). Note the user-visible consequence: publishing the mic now interrupts other apps' audio for the rest of the call; muting doesn't bring it back.
  • Docs: Docs/audio.md gains an "Audio session category selection" section covering the rules above, what counts as "recording", and the mic-permission implications.

Compared to earlier revisions of this PR:

  • The permission-driven selection (canPublishMicrophone.playAndRecord) is gone. canPublish defaults to true in standard tokens, so it made .playback unreachable — every subscriber would have activated a .playAndRecord session (mic prompt; TCC kill for apps without NSMicrophoneUsageDescription). The viewer-mic-prompt bug it targeted is native — the ADM's teardown path touched AVAudioEngine.inputNode, which alone instantiates the input unit — and is fixed by Do not instantiate AVAudioEngine.inputNode on playout-only engine teardown webrtc-sdk/webrtc#286, shipped in 150.7871.01 (Bump WebRTC to 150.7871.01 #1103).
  • The .ambient category reset before deactivation (volume-rocker register fix) was dropped: no other iOS SDK we checked (LiveKit Flutter/React Native, Stream Video, Agora, Twilio, upstream RTCAudioSession) resets or restores the category on teardown, and .ambient would break apps that manage their own category (no background audio, silent-switch muting). A restore-previous-configuration approach may follow separately.
  • .allowAirPlay is kept. QA1803 only documents implied options for .voiceChat/.videoChat (allowBluetooth, defaultToSpeaker); it doesn't back removing AirPlay, and Flutter keeps it too.

Comment thread Sources/LiveKit/Audio/AudioSessionEngineObserver.swift Outdated
@pblazej

pblazej commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

📱 TF build 2.15.0.b20260527

@pblazej

pblazej commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I think I'll postpone it till minor release with better docs/changelog.

@pblazej
pblazej requested a review from MaxHeimbrock August 27, 2026 12:54
@pblazej

pblazej commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

cc @MaxHeimbrock maybe good to match platform audio here? (or vice versa)

devin-ai-integration[bot]

This comment was marked as resolved.

@MaxHeimbrock

MaxHeimbrock commented Aug 27, 2026

Copy link
Copy Markdown

cc @MaxHeimbrock maybe good to match platform audio here? (or vice versa)

Thanks for the cc. I am currently improving platform audio with Unity where Unity itself also tries to configure the audio session. This might have implications that are different from the ones in Swift. Once the Unity platform audio integration is more robust I will try to see if we can align it more with Swift.

Btw how do you test this, do you do a lot of on device testing?

pblazej and others added 2 commits August 31, 2026 12:45
Muting the microphone drops the recording requirement, which previously
flipped the category back to .playback. A mid-session category change
tears down Apple's Voice Processing I/O, so echo cancellation stayed off
until the session ended.

A sticky `hasRecorded` bit, cleared when the WebRTC engine stops, holds
.playAndRecord across mute toggles. It is keyed to the engine's own
session requirement, so an externally held requirement (a prepared
SoundPlayer sound) does not pin the category after the call. Category
selection moves into a pure `selectConfiguration(state:)` so it can be
tested directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Letting another app share the audio device during a call is a known cause
of echo, and the WebRTC ADM carries a retry loop to work around a -66637
(kAudioUnitErr_Initialized) engine-init race that only appears when the
option is set (related symptom: #1011).

The option entered the .playAndRecord defaults during the AVAudioEngine
ADM rewrite (#536), having been added in #228 and removed again in #260.
It stays on the .playback preset, where mixing is correct for a listener.
This matches what client-sdk-flutter already configures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pblazej
pblazej force-pushed the blaze/audio-session-defaults branch from d231636 to 23a1e6b Compare August 31, 2026 10:51
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pblazej
pblazej force-pushed the blaze/audio-session-defaults branch from 23a1e6b to 6ec10a3 Compare August 31, 2026 11:06
@pblazej

pblazej commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

I narrowed the scope of this PR a bit, to avoid speculative changes, focusing on:

  • playAndRecord stickiness
  • mixWithOthers bugfix 🐞

The impact of resetting after call is hard to estimate atm + it can be implemented on the consumer side with moderate effort.

@pblazej
pblazej merged commit ae75761 into main Aug 31, 2026
32 checks passed
@pblazej
pblazej deleted the blaze/audio-session-defaults branch August 31, 2026 11:39
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.

3 participants