From 17b44805f545da6c7777631466d2d937d0f68a93 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:26:49 +0200 Subject: [PATCH] Restore cached iOS audio session on PlatformAudio dispose PlatformAudio switches the shared AVAudioSession to PlayAndRecord + VoiceChat on iOS but never restored it on dispose: the restore hook was dead code, and it only set Ambient without reactivating. After a call ended, the session stayed on the VoIP profile and deactivated, degrading normal Unity audio playback (quiet, receiver-biased routing). Snapshot the session's category/mode/options the first time LiveKit configures it (the state Unity set from its iOS Player Settings), and re-apply that snapshot + reactivate when the last PlatformAudio is disposed. Instance lifetime is tracked with an Interlocked counter so restore fires only on the final dispose, matching the native ADM ref-count teardown. Co-Authored-By: Claude Opus 4.8 (1M context) --- Runtime/Plugins/iOS/LiveKitAudioSession.mm | 60 +++++++++++++++++++--- Runtime/Scripts/Audio/PlatformAudio.cs | 19 +++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Runtime/Plugins/iOS/LiveKitAudioSession.mm b/Runtime/Plugins/iOS/LiveKitAudioSession.mm index f341fc56..835b0953 100644 --- a/Runtime/Plugins/iOS/LiveKitAudioSession.mm +++ b/Runtime/Plugins/iOS/LiveKitAudioSession.mm @@ -16,6 +16,32 @@ #import +// Snapshot of the AVAudioSession configuration as it was the first time LiveKit +// touched the session (i.e. whatever Unity set up from its iOS Player Settings). +// Captured lazily in LiveKit_ConfigureAudioSessionForVoIP and re-applied by +// LiveKit_RestoreDefaultAudioSession when the last PlatformAudio is disposed. +static BOOL s_hasCachedState = NO; +static NSString* s_cachedCategory = nil; +static NSString* s_cachedMode = nil; +static AVAudioSessionCategoryOptions s_cachedCategoryOptions = 0; + +/// Captures the current audio session category/mode/options exactly once, before +/// LiveKit reconfigures the session for VoIP. Subsequent calls are no-ops so the +/// snapshot always reflects the pristine, pre-LiveKit (Unity-configured) state. +static void LiveKit_CacheSessionStateIfNeeded() { + if (s_hasCachedState) { + return; + } + AVAudioSession* session = [AVAudioSession sharedInstance]; + // copy so the strings persist for the app lifetime regardless of ARC/MRC. + s_cachedCategory = [session.category copy]; + s_cachedMode = [session.mode copy]; + s_cachedCategoryOptions = session.categoryOptions; + s_hasCachedState = YES; + NSLog(@"LiveKit: cached audio session state: category=%@, mode=%@, options=%lu", + s_cachedCategory, s_cachedMode, (unsigned long)s_cachedCategoryOptions); +} + extern "C" { /// Configures the iOS audio session for VoIP/WebRTC use. @@ -28,6 +54,9 @@ /// Call this before creating PlatformAudio to ensure WebRTC can /// properly initialize the microphone and speaker. void LiveKit_ConfigureAudioSessionForVoIP() { + // Snapshot the pristine (Unity Player Settings) session before we change it. + LiveKit_CacheSessionStateIfNeeded(); + AVAudioSession* session = [AVAudioSession sharedInstance]; NSError* error = nil; @@ -54,16 +83,35 @@ void LiveKit_ConfigureAudioSessionForVoIP() { NSLog(@"LiveKit: Audio session configured for VoIP (PlayAndRecord + VoiceChat mode)"); } -/// Restores the audio session to the default ambient category. -/// Call this when PlatformAudio is disposed if you want to restore -/// the original audio behavior. +/// Restores the audio session to the state cached before LiveKit first configured +/// it for VoIP (see LiveKit_CacheSessionStateIfNeeded), and reactivates it so Unity +/// audio plays normally again. Call this when the last PlatformAudio is disposed. +/// Falls back to the ambient category if nothing was ever cached. void LiveKit_RestoreDefaultAudioSession() { AVAudioSession* session = [AVAudioSession sharedInstance]; NSError* error = nil; - [session setCategory:AVAudioSessionCategoryAmbient error:&error]; - if (error) { - NSLog(@"LiveKit: Failed to restore default audio session: %@", error.localizedDescription); + if (s_hasCachedState) { + if (![session setCategory:s_cachedCategory + mode:s_cachedMode + options:s_cachedCategoryOptions + error:&error] || error) { + NSLog(@"LiveKit: Failed to restore cached audio session (category=%@, mode=%@): %@", + s_cachedCategory, s_cachedMode, error.localizedDescription); + } + } else { + // Configure was never called, so we have nothing to restore to; fall back + // to the ambient category. + [session setCategory:AVAudioSessionCategoryAmbient error:&error]; + if (error) { + NSLog(@"LiveKit: Failed to restore default audio session: %@", error.localizedDescription); + } + } + + // Hand an active session back to Unity so its audio output resumes. + error = nil; + if (![session setActive:YES error:&error] || error) { + NSLog(@"LiveKit: Failed to reactivate audio session: %@", error.localizedDescription); } } diff --git a/Runtime/Scripts/Audio/PlatformAudio.cs b/Runtime/Scripts/Audio/PlatformAudio.cs index 7c113e20..517cc3b5 100644 --- a/Runtime/Scripts/Audio/PlatformAudio.cs +++ b/Runtime/Scripts/Audio/PlatformAudio.cs @@ -74,6 +74,11 @@ public sealed class PlatformAudio : IDisposable internal readonly FfiHandle Handle; private readonly PlatformAudioInfo _info; private bool _disposed = false; +#if UNITY_IOS && !UNITY_EDITOR + // Tracks live PlatformAudio instances so the iOS audio session is restored + // only when the last one is disposed (aligned with the native ADM ref-count). + private static int _instanceCount; +#endif /// /// Number of available recording (microphone) devices. @@ -119,6 +124,12 @@ public PlatformAudio() _info = platformAudio.Info; Utils.Debug($"PlatformAudio created: {RecordingDeviceCount} recording devices, {PlayoutDeviceCount} playout devices"); + +#if UNITY_IOS && !UNITY_EDITOR + // Count this instance only after successful construction so a failed + // ctor never leaves the counter stuck above zero. + System.Threading.Interlocked.Increment(ref _instanceCount); +#endif } /// @@ -365,6 +376,14 @@ public void Dispose() if (_disposed) return; Handle.Dispose(); _disposed = true; + +#if UNITY_IOS && !UNITY_EDITOR + // Restore the audio session Unity had before VoIP, but only once the + // last PlatformAudio is gone (the native ADM is likewise ref-counted). + if (System.Threading.Interlocked.Decrement(ref _instanceCount) == 0) + IOSAudioSessionHelper.LiveKit_RestoreDefaultAudioSession(); +#endif + Utils.Debug("PlatformAudio disposed"); } }