Resolve a default audio session from engine state when no policy was pushed - #1182
Open
hiroshihorie wants to merge 2 commits into
Open
Resolve a default audio session from engine state when no policy was pushed#1182hiroshihorie wants to merge 2 commits into
hiroshihorie wants to merge 2 commits into
Conversation
…pushed On iOS the audio engine refuses to enable recording unless the audio session category permits input, and livekit_client owns that session. The native engine observer only applied a configuration that Dart had pushed, and the only push site in automatic mode was Room.connect. Any recording that started earlier (pre-connect audio, a pre-join microphone preview, an engine start driven from native before the Flutter side exists) ran against the app-default soloAmbient category and failed with kAudioEngineErrorAudioSessionInvalidCategory (-9001), reported as AudioProcessingException(applyFailed). The observer now resolves a built-in playAndRecord preset from engine state when nothing has been pushed and automatic management is on, matching the Swift SDK's AudioSessionEngineObserver, which derives the session from engine state alone. The Dart-pushed policy becomes an override rather than a prerequisite. Dart passes preferSpeakerOutput so the preset picks the same mode the Dart policy would. Audio device module results -9000, -9001 and -4100 now get their own error codes and surface as TrackCreateException or the new AudioSessionException instead of an audio processing failure.
…ping helpers AudioManager prefers speaker output by default, so the built-in preset now defaults to videoChat as well. Otherwise the connect-time push would switch the live session from voiceChat to videoChat. Also simplifies effectiveConfigurationLocked and the Dart error mapping, and shortens the changeset entries.
hiroshihorie
marked this pull request as ready for review
August 27, 2026 16:47
hiroshihorie
requested review from
cloudwebrtc and
xianshijing-lk
as code owners
August 27, 2026 16:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On iOS the WebRTC audio engine refuses to enable recording unless the audio session category permits input. Its pre-enable check returns
kAudioEngineErrorAudioSessionInvalidCategory(-9001), which the SDK reported asAudioProcessingException(applyFailed): Audio engine returned error code: -9001.livekit_clientowns the iOS audio session since #1108 (LiveKitPlugin.swiftdisables flutter_webrtc's session management at registration). The native engine observer (LKAudioEngineObserver.willEnableEngine) is the right hook and runs before the engine's check, but it only applied a configuration that Dart had pushed. In automatic mode the only push site wasRoom.connect, so anything that started recording earlier met an empty cache, the observer returned "proceed" with the session stillsoloAmbient, and the engine rolled back:Room.withPreConnectAudio, whichSessionOptions.preConnectAudioenables by default, so the Flutter agent starter failed on every "Start call"setEngineAvailabilityon a CallKit killed-state wakeBecause the preconnect throw happens before
connect, the cache was never seeded and the failure repeated on every attempt.How the Swift SDK handles this
AudioSessionEngineObserver.engineWillEnablederives the session configuration from the requested engine state alone (playAndRecordpresets while recording,playbackfor playout only), synchronously inside the engine's enable call. Nothing is configured "before connect". The engine asks, the observer configures, the engine starts. The Flutter plugin already has the same observer in the same place, it just had no built-in policy.Fix
LKAudioEngineObserver.effectiveConfigurationLockednow resolves a built-inplayAndRecordpreset (allowBluetooth | allowBluetoothA2DP | allowAirPlay,voiceChat, orvideoChatwhen the speaker is preferred) whenever nothing has been pushed and automatic management is on. The existing playout-onlyplaybackbranch applies to it as well. Manual mode still leaves the session alone. The Dart-pushed policy becomes an override rather than a prerequisite, and for the defaultAudioSessionOptions.communicationit pushes the same values, so the connect-time push does not change the live session.Dart passes a new
preferSpeakerOutputflag onconfigureNativeAudioso the preset picks the same mode the Dart policy would. The preset andResolvedAudioSessionPolicy.appleConfigurationare marked keep-in-sync. Making native the single source of truth for the automatic-mode policy is a follow-up, since it changes the channel contract.Error mapping
Audio device module results now get their own error codes on the
startLocalRecordingandsetEngineAvailabilitychannels, mirroring client-sdk-swift'scheckAdmResult:InsufficientDevicePermissiondeviceAccessDeniedTrackCreateExceptionAudioSessionInvalidCategoryaudioSessionInvalidCategoryAudioSessionException(new)FailedToConfigureAudioSessionaudioSessionConfigureFailedAudioSessionException(new)applyFailed,setEngineAvailability)AudioSessionExceptionis a new public class, hence the second changeset.Relation to #1179
@MaxHeimbrock's #1179 diagnosed this first and fixes it by pushing the policy from
LocalAudioTrack.startCapture(prepareRecording()). This PR takes the other layer: it makes the native observer self-sufficient like Swift's, so engine enables that never pass through Dart are covered too, and no Dart-side flag has to track native cache state. With this merged,prepareRecording()becomes redundant.Testing
Start callfailed with -9001 on every attempt. Patched,startCapture()succeeds two seconds before the connect-time policy push, the preconnect buffer is sent to the agent, and the call connects.flutter analyze,flutter test,dart format --set-exit-if-changed,import_sorter --exit-if-changedclean.lkPlatform()has no seam), same limitation as Configure the audio session before recording starts, not only on connect #1179 noted.Fixes #1165
Refs #1042