-
Notifications
You must be signed in to change notification settings - Fork 240
feat(audio): engine availability control and external call system session mode #1127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
71c4ad9
48fb184
05ec2fa
3cbf322
5171793
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| minor type="added" "AudioManager engine availability control and externalCallSystem session mode for CallKit coordination" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Copyright 2026 LiveKit, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| /// Whether the WebRTC audio engine is allowed to run, per direction. | ||
| /// | ||
| /// This is the highest-priority gate over anything that may start the engine | ||
| /// (enabling the microphone, starting playback of remote audio). Requests | ||
| /// made while a direction is unavailable are not lost: the engine starts as | ||
| /// soon as availability allows. | ||
| /// | ||
| /// Used to coordinate with platform call systems that own audio activation | ||
| /// timing, such as CallKit on iOS: keep the engine unavailable until | ||
| /// `provider(didActivate:)` and make it available inside the | ||
| /// activate/deactivate window. See `AudioManager.setEngineAvailability`. | ||
| class AudioEngineAvailability { | ||
| /// Whether the engine may run its input (microphone / recording) side. | ||
| final bool isInputAvailable; | ||
|
|
||
| /// Whether the engine may run its output (playout / remote audio) side. | ||
| final bool isOutputAvailable; | ||
|
|
||
| const AudioEngineAvailability({ | ||
| required this.isInputAvailable, | ||
| required this.isOutputAvailable, | ||
| }); | ||
|
|
||
| /// Both input and output are available (the default). | ||
| static const defaultAvailability = AudioEngineAvailability(isInputAvailable: true, isOutputAvailable: true); | ||
|
|
||
| /// Neither input nor output is available. The engine will not run. | ||
| static const none = AudioEngineAvailability(isInputAvailable: false, isOutputAvailable: false); | ||
|
|
||
| @override | ||
| bool operator ==(Object other) => | ||
| other is AudioEngineAvailability && | ||
| other.isInputAvailable == isInputAvailable && | ||
| other.isOutputAvailable == isOutputAvailable; | ||
|
|
||
| @override | ||
| int get hashCode => Object.hash(isInputAvailable, isOutputAvailable); | ||
|
|
||
| @override | ||
| String toString() => | ||
| 'AudioEngineAvailability(isInputAvailable: $isInputAvailable, isOutputAvailable: $isOutputAvailable)'; | ||
| } |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Audio session configuration not re-applied when switching from manual to external-call-system mode The mode transition check ( Impact: An app that switches from manual to external-call-system mode mid-session will keep the old audio session configuration until the next engine lifecycle event, causing incorrect audio routing or category. Mechanism: the re-apply condition doesn't account for externalCallSystem being an automatic-configuration modeBefore this PR there were only two modes ( The fix should check whether the new mode is any automatic-configuration mode, e.g.: or equivalently check both (Refers to line 240) Was this helpful? React with 👍 or 👎 to provide feedback.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I think that's expected, mid-session switching is not really supported. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,10 +20,13 @@ import '../logger.dart'; | |
| import '../support/native.dart'; | ||
| import '../support/platform.dart'; | ||
| import 'android_audio_session_adapter.dart'; | ||
| import 'audio_engine_availability.dart'; | ||
| import 'audio_processing_state.dart'; | ||
| import 'audio_session.dart'; | ||
| import 'audio_session_policy.dart'; | ||
|
|
||
| export 'audio_engine_availability.dart'; | ||
|
|
||
| /// Snapshot of the WebRTC audio engine's playout/recording state. | ||
| /// | ||
| /// Surfaced by [AudioManager] from real audio-engine lifecycle events on the | ||
|
|
@@ -96,7 +99,11 @@ class AudioManager { | |
| /// A broadcast stream of audio engine state changes (native engine lifecycle). | ||
| Stream<AudioEngineState> get audioEngineStateStream => _audioEngineStateController.stream; | ||
|
|
||
| bool get _isAutomaticConfigurationEnabled => _managementMode == AudioSessionManagementMode.automatic; | ||
| // externalCallSystem keeps engine-driven configuration (like automatic) but | ||
| // the native side never activates/deactivates the session. | ||
| bool get _isAutomaticConfigurationEnabled => _managementMode != AudioSessionManagementMode.manual; | ||
|
|
||
| bool get _isSessionActivationEnabled => _managementMode != AudioSessionManagementMode.externalCallSystem; | ||
|
|
||
| @visibleForTesting | ||
| void resetForTest() { | ||
|
|
@@ -141,11 +148,16 @@ class AudioManager { | |
| /// `LiveKitClient.initialize(initialAudioSessionOptions: ...)` uses this so the | ||
| /// WebRTC initialization-time Android audio attributes and LiveKit's automatic | ||
| /// runtime session policy start from the same intent. Unlike | ||
| /// [setAudioSessionOptions], this keeps automatic management enabled and does | ||
| /// [setAudioSessionOptions], this keeps the current management mode and does | ||
| /// not apply native session changes immediately. | ||
| /// | ||
| /// Applies under [AudioSessionManagementMode.automatic] and | ||
| /// [AudioSessionManagementMode.externalCallSystem], both of which drive | ||
| /// configuration from engine lifecycle. Skipped under | ||
| /// [AudioSessionManagementMode.manual], where the app owns the options. | ||
| @internal | ||
| void setInitialAudioSessionOptions(AudioSessionOptions options) { | ||
| if (_managementMode != AudioSessionManagementMode.automatic) { | ||
| if (!_isAutomaticConfigurationEnabled) { | ||
| return; | ||
| } | ||
| _options = options; | ||
|
|
@@ -160,12 +172,57 @@ class AudioManager { | |
| /// | ||
| /// The speaker preference and force flag are owned by setSpeakerOutputPreferred | ||
| /// and are preserved across this call. | ||
| /// | ||
| /// Under [AudioSessionManagementMode.externalCallSystem] the mode is | ||
| /// preserved: the configuration is applied without activating the session. | ||
| Future<void> setAudioSessionOptions(AudioSessionOptions options) async { | ||
| await _enterManualMode(); | ||
| _options = options; | ||
| await _applyCurrentAudioSessionPolicy(); | ||
| } | ||
|
|
||
| /// Sets whether the WebRTC audio engine is allowed to run. | ||
| /// | ||
| /// This is the highest-priority gate over anything that may start the | ||
| /// engine (enabling the microphone, remote audio playback). Requests made | ||
| /// while unavailable are honored as soon as availability allows, so it is | ||
| /// safe to connect a room and publish the microphone while the engine is | ||
| /// still gated off. | ||
| /// | ||
| /// This is the core of CallKit coordination on iOS: set | ||
| /// [AudioEngineAvailability.none] before connecting, then flip to | ||
| /// [AudioEngineAvailability.defaultAvailability] in CallKit's | ||
| /// `provider(didActivate:)` and back to `none` in `didDeactivate`. Combine | ||
| /// with [AudioSessionManagementMode.externalCallSystem] so LiveKit never | ||
| /// activates the session itself. | ||
| /// | ||
| /// iOS/macOS only. A no-op elsewhere, so it is always safe to call from | ||
| /// cross-platform code. | ||
| /// | ||
| /// Throws if the native side rejects the change, so callers never assume | ||
| /// the engine is gated when it is not. | ||
| Future<void> setEngineAvailability(AudioEngineAvailability availability) async { | ||
| if (!lkPlatformIsApple()) return; | ||
| await Native.setEngineAvailability( | ||
| isInputAvailable: availability.isInputAvailable, | ||
| isOutputAvailable: availability.isOutputAvailable, | ||
| ); | ||
| } | ||
|
|
||
| /// The WebRTC audio engine's current availability. | ||
| /// | ||
| /// Returns [AudioEngineAvailability.defaultAvailability] on platforms | ||
| /// without engine gating (everything except iOS/macOS). | ||
| Future<AudioEngineAvailability> getEngineAvailability() async { | ||
| if (!lkPlatformIsApple()) return AudioEngineAvailability.defaultAvailability; | ||
| final response = await Native.getEngineAvailability(); | ||
| if (response == null) return AudioEngineAvailability.defaultAvailability; | ||
| return AudioEngineAvailability( | ||
| isInputAvailable: response['isInputAvailable'] as bool? ?? true, | ||
| isOutputAvailable: response['isOutputAvailable'] as bool? ?? true, | ||
| ); | ||
| } | ||
|
|
||
| /// Selects whether LiveKit manages the platform audio session automatically. | ||
| /// | ||
| /// In [AudioSessionManagementMode.manual], LiveKit does not update the audio | ||
|
|
@@ -186,8 +243,12 @@ class AudioManager { | |
| } | ||
|
|
||
| /// Switches to manual mode if not already, syncing the native side once. | ||
| /// | ||
| /// [AudioSessionManagementMode.externalCallSystem] is preserved: an explicit | ||
| /// configuration under an external call system applies without LiveKit | ||
| /// taking over the session lifecycle. | ||
| Future<void> _enterManualMode() async { | ||
| if (_managementMode == AudioSessionManagementMode.manual) return; | ||
| if (_managementMode != AudioSessionManagementMode.automatic) return; | ||
| _managementMode = AudioSessionManagementMode.manual; | ||
| await _syncAppleAudioSessionManagementMode(); | ||
| } | ||
|
|
@@ -199,6 +260,10 @@ class AudioManager { | |
| /// session on its own. Re-apply a configuration with [setAudioSessionOptions], | ||
| /// or hand control back with [setAudioSessionManagementMode]. | ||
| Future<void> deactivateAudioSession() async { | ||
| if (_managementMode == AudioSessionManagementMode.externalCallSystem) { | ||
| logger.warning('deactivateAudioSession skipped: the external call system owns session activation'); | ||
| return; | ||
| } | ||
|
Comment on lines
+263
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 deactivateAudioSession blocks on Android under externalCallSystem despite 'behaves like automatic' doc The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| await _enterManualMode(); | ||
| if (lkPlatformIs(PlatformType.iOS)) { | ||
| await Native.deactivateAppleAudioSession(); | ||
|
|
@@ -265,7 +330,10 @@ class AudioManager { | |
|
|
||
| Future<void> _syncAppleAudioSessionManagementMode() async { | ||
| if (lkPlatformIs(PlatformType.iOS)) { | ||
| await Native.setAppleAudioSessionAutomaticManagementEnabled(_isAutomaticConfigurationEnabled); | ||
| await Native.setAppleAudioSessionAutomaticManagementEnabled( | ||
| _isAutomaticConfigurationEnabled, | ||
| sessionActivationEnabled: _isSessionActivationEnabled, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.