From 3da6a60729194b5168aba3c86766f9630ed4a8f3 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:30:17 +0900 Subject: [PATCH 1/6] feat: configure the iOS audio session natively in the default path setupIOSAudioManagement's default path now pushes the audio-session policy to native via AudioDeviceModule.setAutomaticAudioSessionConfiguration() instead of registering willEnable/didDisable JS handlers, so the audio engine's worker thread no longer round-trips to JS in the stock path. That round trip could stall or deadlock engine operations when the JS thread was itself blocked in a synchronous bridge call. Custom configurations (onConfigureNativeAudio) keep the JS handler path, still bounded by the native wait timeout. Repeated setup calls and switches between the default and custom paths are safe: each setup supersedes the previous one by claiming a token, stale cleanup functions become no-ops, and each path arms its own mechanism before tearing down the other's so the engine hooks stay owned throughout a switch. --- src/audio/AudioManager.ts | 81 ++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/src/audio/AudioManager.ts b/src/audio/AudioManager.ts index cddf3bf7..9c2f99e1 100644 --- a/src/audio/AudioManager.ts +++ b/src/audio/AudioManager.ts @@ -1,7 +1,10 @@ import { Platform } from 'react-native'; import AudioSession, { type AppleAudioConfiguration } from './AudioSession'; import { log } from '../logger'; -import { audioDeviceModuleEvents } from '@livekit/react-native-webrtc'; +import { + audioDeviceModuleEvents, + AudioDeviceModule, +} from '@livekit/react-native-webrtc'; export type AudioEngineConfigurationState = { isPlayoutEnabled: boolean; @@ -10,13 +13,33 @@ export type AudioEngineConfigurationState = { }; const kAudioEngineErrorFailedToConfigureAudioSession = -4100; -let activeAudioManagementSetup: object | undefined; +let activeSetupToken: object | undefined; /** * @inline */ type CleanupFn = () => void; +// Wraps a path-specific teardown so each setup supersedes the previous one +// cleanly: the returned cleanup tears down only while its setup is still the +// active one, so it runs at most once and a stale cleanup from a superseded +// setup is a no-op. A new setup defuses the previous cleanup by replacing the +// token rather than by running its teardown, because each path's body already +// replaces the other path's mechanism in an order that never leaves the hooks +// unowned - running the old teardown first would reopen exactly that window. +function finalizeAudioManagement( + token: object, + teardown: CleanupFn +): CleanupFn { + return () => { + if (activeSetupToken !== token) { + return; + } + activeSetupToken = undefined; + teardown(); + }; +} + /** * Sets up automatic iOS audio session management based on audio engine state. * @@ -38,8 +61,48 @@ export function setupIOSAudioManagement( return () => {}; } + // Supersede any previous setup (safe to call repeatedly, and to switch between + // default and custom). Claiming the token defuses the previous cleanup without + // running it. The path bodies below then replace the other path's mechanism in + // an order that keeps the hooks owned throughout the switch. const setupToken = {}; - activeAudioManagementSetup = setupToken; + activeSetupToken = setupToken; + + // Default path: configure the AVAudioSession natively so the engine's + // worker thread never round-trips to JS in willEnable/didDisable - that round + // trip is what can deadlock. The native observer applies `recording` while + // recording, `playout` while playout-only, and deactivates on full stop. + if (!onConfigureNativeAudio) { + AudioDeviceModule.setAutomaticAudioSessionConfiguration({ + recording: getDefaultAppleAudioConfigurationForAudioState({ + isPlayoutEnabled: true, + isRecordingEnabled: true, + preferSpeakerOutput, + }), + playout: getDefaultAppleAudioConfigurationForAudioState({ + isPlayoutEnabled: true, + isRecordingEnabled: false, + preferSpeakerOutput, + }), + deactivateOnStop: true, + }); + + // Set native config first, then clear any handlers a prior custom setup left + // registered, so native (not a stale JS handler) owns the hooks. In the brief + // overlap a still-registered handler wins, so a racing callback is never dropped. + audioDeviceModuleEvents.setWillEnableEngineHandler(null); + audioDeviceModuleEvents.setDidDisableEngineHandler(null); + + return finalizeAudioManagement(setupToken, () => { + AudioDeviceModule.setAutomaticAudioSessionConfiguration(null); + }); + } + + // Custom path: derive + apply the session config in JS via the engine handlers + // (still bounded by the native 2s wait). The native default is cleared *after* + // the handlers are registered (below) so the JS handler, which takes precedence, + // owns the hooks throughout the switch. + let audioEngineState: AudioEngineConfigurationState = { isPlayoutEnabled: false, isRecordingEnabled: false, @@ -106,14 +169,14 @@ export function setupIOSAudioManagement( audioDeviceModuleEvents.setWillEnableEngineHandler(handleEngineStateUpdate); audioDeviceModuleEvents.setDidDisableEngineHandler(handleEngineStateUpdate); - return () => { - if (activeAudioManagementSetup !== setupToken) { - return; - } - activeAudioManagementSetup = undefined; + // Handlers are live now, so clear the native default - the JS handler takes + // precedence, so there is never a window where neither path is active. + AudioDeviceModule.setAutomaticAudioSessionConfiguration(null); + + return finalizeAudioManagement(setupToken, () => { audioDeviceModuleEvents.setWillEnableEngineHandler(null); audioDeviceModuleEvents.setDidDisableEngineHandler(null); - }; + }); } // Kept in sync with `getDefaultAppleAudioConfigurationForMode` in From 3313d9d0e6958c0b9a49cd6ec07b0ed2f3345841 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:56:21 +0900 Subject: [PATCH 2/6] docs: document the onConfigureNativeAudio contract and path switching Spell out that the custom callback runs inside the audio engine's lifecycle callbacks under a bounded native wait, so it must return quickly and must not call into the WebRTC engine or a peer connection. Doing so can block on the very engine operation the callback is holding up, stalling it until the wait times out. Also note that the default path involves no JavaScript per transition, that repeated setup calls supersede the previous one, and that switching paths is best done while disconnected. --- src/audio/AudioManager.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/audio/AudioManager.ts b/src/audio/AudioManager.ts index 9c2f99e1..4f4358f0 100644 --- a/src/audio/AudioManager.ts +++ b/src/audio/AudioManager.ts @@ -47,9 +47,27 @@ function finalizeAudioManagement( * invokes it for you by default unless `autoConfigureAudioSession: false` * is passed. * + * By default the audio session is configured and activated natively as the + * audio engine changes state, with no JavaScript involvement per transition. + * + * When `onConfigureNativeAudio` is provided, it runs inside the audio + * engine's lifecycle callbacks while native code waits for the result, with + * the wait bounded at a few seconds per callback. The callback must return + * quickly and should only derive the configuration to apply. It must not + * call APIs that enter the WebRTC engine or a peer connection (for example + * `addTransceiver`, `getUserMedia`, or device enumeration): those can block + * on the same engine operation the callback is holding up, and the operation + * would stall until the native wait times out. + * + * Calling this again replaces the previous setup, including switching + * between the default and custom paths. Prefer switching while disconnected. + * A switch during an active call only takes full effect from the next audio + * engine transition onward. + * * @param preferSpeakerOutput - Whether to prefer speaker output. Defaults to true. * @param onConfigureNativeAudio - Optional custom callback for determining audio configuration. - * @returns A cleanup function that removes the event handlers. + * @returns A cleanup function that removes the installed handlers or native + * configuration. A cleanup function from a superseded setup is a no-op. */ export function setupIOSAudioManagement( preferSpeakerOutput = true, From b7c8e8f50e4c275522d2eb1446fe5aa4792b66c2 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:15:54 +0900 Subject: [PATCH 3/6] chore(example): sync Podfile.lock with WebRTC-SDK 144.7559.10 --- example/ios/Podfile.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 243c9610..51af5601 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,10 +5,10 @@ PODS: - FBLazyVector (0.82.1) - fmt (11.0.2) - glog (0.3.5) - - hermes-engine (0.82.0): - - hermes-engine/Pre-built (= 0.82.0) - - hermes-engine/Pre-built (0.82.0) - - livekit-react-native (2.11.0-beta.1): + - hermes-engine (0.82.1): + - hermes-engine/Pre-built (= 0.82.1) + - hermes-engine/Pre-built (0.82.1) + - livekit-react-native (2.11.0): - boost - DoubleConversion - fast_float @@ -39,7 +39,7 @@ PODS: - Yoga - livekit-react-native-webrtc (144.1.0): - React-Core - - WebRTC-SDK (= 144.7559.04) + - WebRTC-SDK (= 144.7559.10) - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -2530,7 +2530,7 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) - - WebRTC-SDK (144.7559.04) + - WebRTC-SDK (144.7559.10) - Yoga (0.0.0) DEPENDENCIES: @@ -2789,10 +2789,10 @@ SPEC CHECKSUMS: FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 8642d8f14a548ab718ec112e9bebdfdd154138b5 - livekit-react-native: a051e6aa4c21e1b2429d136a3d544ae0ccfa08a6 - livekit-react-native-webrtc: 6384bffff182166749ea42ccc419cee6785b0603 - RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f + hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 + livekit-react-native: b8a86fb1c57270437f8a010437c0570585a13625 + livekit-react-native-webrtc: 9add5324b1c13f41577dffc01c2388d8e84d8348 + RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 RCTTypeSafety: c693294e3993056955c3010eb1ebc574f1fcded6 @@ -2862,7 +2862,7 @@ SPEC CHECKSUMS: RNCAsyncStorage: 01e4301688a611936e3644746ffe3325d3181952 RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - WebRTC-SDK: ac5965a3cc2c258973466e3b1739ab0308bab0d0 + WebRTC-SDK: 1d7fc2ae91da6edd63e25f3882ff24f86a8d9d8b Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb PODFILE CHECKSUM: a921a659e37cd7dfb12da519493e10c9f38400a1 From da3d1c5db3652cb9e734e828e9d7ce415a1ed067 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:07:05 +0900 Subject: [PATCH 4/6] chore: add changeset --- .changeset/native-default-audio-session.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/native-default-audio-session.md diff --git a/.changeset/native-default-audio-session.md b/.changeset/native-default-audio-session.md new file mode 100644 index 00000000..e63ba6a3 --- /dev/null +++ b/.changeset/native-default-audio-session.md @@ -0,0 +1,5 @@ +--- +'@livekit/react-native': minor +--- + +Configure the iOS audio session natively in the default path, removing the audio engine's JS round trip and its deadlock window From 80f04c52e912bc0c14d29ceb3bbdc6b56c74c42a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:47:38 +0900 Subject: [PATCH 5/6] docs: mid-call path switching is unsupported, not merely delayed The previous wording claimed a switch during an active call takes full effect from the next engine transition. That undersold the failure mode: switching away from a custom setup mid-call abandons the audio session activation its handlers took, and with the shared activation count nobody releases it, so the session can stay active after the call ends. State it as unsupported with the concrete consequence. --- src/audio/AudioManager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/audio/AudioManager.ts b/src/audio/AudioManager.ts index 4f4358f0..0f1ad372 100644 --- a/src/audio/AudioManager.ts +++ b/src/audio/AudioManager.ts @@ -60,9 +60,10 @@ function finalizeAudioManagement( * would stall until the native wait times out. * * Calling this again replaces the previous setup, including switching - * between the default and custom paths. Prefer switching while disconnected. - * A switch during an active call only takes full effect from the next audio - * engine transition onward. + * between the default and custom paths. Switch while disconnected: a switch + * during an active call is unsupported. In particular, switching away from a + * custom setup mid-call abandons the audio session activation that the custom + * handlers took, so the session can stay active after the call ends. * * @param preferSpeakerOutput - Whether to prefer speaker output. Defaults to true. * @param onConfigureNativeAudio - Optional custom callback for determining audio configuration. From a7fd6bc3d9242526645c3ff90a0b9eff5b024ec6 Mon Sep 17 00:00:00 2001 From: davidliu Date: Wed, 22 Jul 2026 22:51:17 +0900 Subject: [PATCH 6/6] Allow users to directly configure the native policy --- .changeset/native-default-audio-session.md | 2 +- README.md | 21 ++- src/audio/AudioManager.ts | 141 +++++++++++++++------ src/audio/AudioManagerLegacy.ts | 23 ++-- src/audio/AudioSession.ts | 8 +- 5 files changed, 137 insertions(+), 58 deletions(-) diff --git a/.changeset/native-default-audio-session.md b/.changeset/native-default-audio-session.md index e63ba6a3..11cff52f 100644 --- a/.changeset/native-default-audio-session.md +++ b/.changeset/native-default-audio-session.md @@ -2,4 +2,4 @@ '@livekit/react-native': minor --- -Configure the iOS audio session natively in the default path, removing the audio engine's JS round trip and its deadlock window +Configure the iOS audio session natively in the default path, removing the audio engine's JS round trip and its deadlock window. Deprecate the unsafe `onConfigureNativeAudio` callback form of `setupIOSAudioManagement`. Developers can instead pass in a static `IOSAudioSessionPolicy`. diff --git a/README.md b/README.md index 3bc8c7e2..38653824 100644 --- a/README.md +++ b/README.md @@ -353,19 +353,26 @@ If your app manages `AVAudioSession` directly, disable the automatic setup: registerGlobals({ autoConfigureAudioSession: false }); ``` -For custom automatic behavior, disable the default setup and call -`setupIOSAudioManagement` once during app startup: +For custom categories/modes, disable the default setup and pass a static +policy to `setupIOSAudioManagement` once during app startup: ```js import { registerGlobals, setupIOSAudioManagement } from '@livekit/react-native'; registerGlobals({ autoConfigureAudioSession: false }); -const cleanup = setupIOSAudioManagement(true, (state) => ({ - audioCategory: state.isRecordingEnabled ? 'playAndRecord' : 'playback', - audioCategoryOptions: ['mixWithOthers'], - audioMode: state.preferSpeakerOutput ? 'videoChat' : 'voiceChat', -})); +const cleanup = setupIOSAudioManagement(true, { + recording: { + audioCategory: 'playAndRecord', + audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], + audioMode: 'videoChat', + }, + playout: { + audioCategory: 'playback', + audioCategoryOptions: ['mixWithOthers'], + audioMode: 'spokenAudio', + }, +}); ``` The older `useIOSAudioManagement(room, ...)` hook is still exported for diff --git a/src/audio/AudioManager.ts b/src/audio/AudioManager.ts index 0f1ad372..05e0c4bc 100644 --- a/src/audio/AudioManager.ts +++ b/src/audio/AudioManager.ts @@ -12,6 +12,27 @@ export type AudioEngineConfigurationState = { preferSpeakerOutput: boolean; }; +/** + * Static recording/playout policy for {@link setupIOSAudioManagement}. + */ +export type IOSAudioSessionPolicy = { + recording: AppleAudioConfiguration; + playout: AppleAudioConfiguration; + /** + * Whether to deactivate the audio session when both playout and recording + * are disabled. Defaults to true. + */ + deactivateOnStop?: boolean; +}; + +/** + * @deprecated Unsafe. Prefer {@link IOSAudioSessionPolicy} or the default + * native path. See {@link setupIOSAudioManagement}. + */ +export type OnConfigureNativeAudio = ( + configurationState: AudioEngineConfigurationState +) => AppleAudioConfiguration; + const kAudioEngineErrorFailedToConfigureAudioSession = -4100; let activeSetupToken: object | undefined; @@ -50,31 +71,50 @@ function finalizeAudioManagement( * By default the audio session is configured and activated natively as the * audio engine changes state, with no JavaScript involvement per transition. * - * When `onConfigureNativeAudio` is provided, it runs inside the audio - * engine's lifecycle callbacks while native code waits for the result, with - * the wait bounded at a few seconds per callback. The callback must return - * quickly and should only derive the configuration to apply. It must not - * call APIs that enter the WebRTC engine or a peer connection (for example - * `addTransceiver`, `getUserMedia`, or device enumeration): those can block - * on the same engine operation the callback is holding up, and the operation - * would stall until the native wait times out. + * For custom categories/modes, pass a static {@link IOSAudioSessionPolicy}. + * That policy is pushed to native once and applied on the audio worker thread. * * Calling this again replaces the previous setup, including switching - * between the default and custom paths. Switch while disconnected: a switch + * between the default and custom paths. Switch while disconnected; a switch * during an active call is unsupported. In particular, switching away from a - * custom setup mid-call abandons the audio session activation that the custom - * handlers took, so the session can stay active after the call ends. + * custom callback setup mid-call abandons the audio session activation that + * the custom handlers took, so the session may stay active after the call ends. * * @param preferSpeakerOutput - Whether to prefer speaker output. Defaults to true. - * @param onConfigureNativeAudio - Optional custom callback for determining audio configuration. * @returns A cleanup function that removes the installed handlers or native * configuration. A cleanup function from a superseded setup is a no-op. */ +export function setupIOSAudioManagement( + preferSpeakerOutput?: boolean +): CleanupFn; +/** + * Sets up automatic iOS audio session management with a static native policy. + * + * @param preferSpeakerOutput - Unused when a full policy is provided; kept for + * call-site consistency with the default overload. + * @param policy - Static recording/playout configuration applied natively. + * @see {@link setupIOSAudioManagement} for the default native path and usage notes. + */ +export function setupIOSAudioManagement( + preferSpeakerOutput: boolean, + policy: IOSAudioSessionPolicy +): CleanupFn; +/** + * @deprecated The `onConfigureNativeAudio` callback is unsafe. It runs inside + * the audio engine's lifecycle callbacks while native code waits for the + * result (bounded at a few seconds). The callback must return quickly and + * must not call APIs that enter the WebRTC engine or a peer connection + * (for example `addTransceiver`, `getUserMedia`, or device enumeration): + * those can block on the same engine operation the callback is holding up. + * Prefer the default native path, or pass an {@link IOSAudioSessionPolicy}. + */ +export function setupIOSAudioManagement( + preferSpeakerOutput: boolean, + onConfigureNativeAudio: OnConfigureNativeAudio +): CleanupFn; export function setupIOSAudioManagement( preferSpeakerOutput = true, - onConfigureNativeAudio?: ( - configurationState: AudioEngineConfigurationState - ) => AppleAudioConfiguration + policyOrCallback?: IOSAudioSessionPolicy | OnConfigureNativeAudio ): CleanupFn { if (Platform.OS !== 'ios') { return () => {}; @@ -87,35 +127,64 @@ export function setupIOSAudioManagement( const setupToken = {}; activeSetupToken = setupToken; - // Default path: configure the AVAudioSession natively so the engine's - // worker thread never round-trips to JS in willEnable/didDisable - that round - // trip is what can deadlock. The native observer applies `recording` while - // recording, `playout` while playout-only, and deactivates on full stop. - if (!onConfigureNativeAudio) { - AudioDeviceModule.setAutomaticAudioSessionConfiguration({ - recording: getDefaultAppleAudioConfigurationForAudioState({ + if (typeof policyOrCallback === 'function') { + return setupCustomCallbackPath( + setupToken, + preferSpeakerOutput, + policyOrCallback + ); + } + + return setupNativePolicyPath(setupToken, preferSpeakerOutput, policyOrCallback); +} + +function setupNativePolicyPath( + setupToken: object, + preferSpeakerOutput: boolean, + policy?: IOSAudioSessionPolicy +): CleanupFn { + // Configure the AVAudioSession natively so the engine's worker thread never + // round-trips to JS in willEnable/didDisable - that round trip is what can + // deadlock. The native observer applies `recording` while recording, + // `playout` while playout-only, and deactivates on full stop when requested. + AudioDeviceModule.setAutomaticAudioSessionConfiguration({ + recording: + policy?.recording ?? + getDefaultAppleAudioConfigurationForAudioState({ isPlayoutEnabled: true, isRecordingEnabled: true, preferSpeakerOutput, }), - playout: getDefaultAppleAudioConfigurationForAudioState({ + playout: + policy?.playout ?? + getDefaultAppleAudioConfigurationForAudioState({ isPlayoutEnabled: true, isRecordingEnabled: false, preferSpeakerOutput, }), - deactivateOnStop: true, - }); + deactivateOnStop: policy?.deactivateOnStop ?? true, + }); - // Set native config first, then clear any handlers a prior custom setup left - // registered, so native (not a stale JS handler) owns the hooks. In the brief - // overlap a still-registered handler wins, so a racing callback is never dropped. - audioDeviceModuleEvents.setWillEnableEngineHandler(null); - audioDeviceModuleEvents.setDidDisableEngineHandler(null); + // Set native config first, then clear any handlers a prior custom setup left + // registered, so native (not a stale JS handler) owns the hooks. In the brief + // overlap a still-registered handler wins, so a racing callback is never dropped. + audioDeviceModuleEvents.setWillEnableEngineHandler(null); + audioDeviceModuleEvents.setDidDisableEngineHandler(null); - return finalizeAudioManagement(setupToken, () => { - AudioDeviceModule.setAutomaticAudioSessionConfiguration(null); - }); - } + return finalizeAudioManagement(setupToken, () => { + AudioDeviceModule.setAutomaticAudioSessionConfiguration(null); + }); +} + +function setupCustomCallbackPath( + setupToken: object, + preferSpeakerOutput: boolean, + onConfigureNativeAudio: OnConfigureNativeAudio +): CleanupFn { + log.warn( + 'setupIOSAudioManagement(onConfigureNativeAudio) is deprecated and unsafe. ' + + 'Prefer the default native path, or pass an IOSAudioSessionPolicy object.' + ); // Custom path: derive + apply the session config in JS via the engine handlers // (still bounded by the native 2s wait). The native default is cleared *after* @@ -140,9 +209,7 @@ export function setupIOSAudioManagement( log.info('AudioSession deactivating...'); await AudioSession.stopAudioSession(); } else if (newState.isRecordingEnabled || newState.isPlayoutEnabled) { - const config = onConfigureNativeAudio - ? onConfigureNativeAudio(newState) - : getDefaultAppleAudioConfigurationForAudioState(newState); + const config = onConfigureNativeAudio(newState); log.info('AudioSession configuring category:', config.audioCategory); await AudioSession.setAppleAudioConfiguration(config); if (!oldState.isPlayoutEnabled && !oldState.isRecordingEnabled) { diff --git a/src/audio/AudioManagerLegacy.ts b/src/audio/AudioManagerLegacy.ts index 2d815299..b1a69264 100644 --- a/src/audio/AudioManagerLegacy.ts +++ b/src/audio/AudioManagerLegacy.ts @@ -20,12 +20,16 @@ import { * The `room` parameter is ignored — audio session is now managed * via audio engine events, not room track counts. * + * The `onConfigureNativeAudio` callback is also deprecated and unsafe: it + * runs under a bounded native wait on the audio worker thread. Prefer the + * default native path, or pass an `IOSAudioSessionPolicy` to + * `setupIOSAudioManagement`. + * * Note: the `trackState` passed to `onConfigureNativeAudio` is now * derived from the audio engine's playout/recording state, not from * publication counts. Edge cases can differ. For example, a * published-but-muted local audio track that previously yielded - * `localOnly` may now appear as `remoteOnly` or `none`. Callers with - * nuanced per-state logic should migrate to `setupIOSAudioManagement`. + * `localOnly` may now appear as `remoteOnly` or `none`. */ export function useIOSAudioManagement( _room: Room, @@ -43,19 +47,20 @@ export function useIOSAudioManagement( callbackRef.current = onConfigureNativeAudio; useEffect(() => { + // Without a custom callback, use the safe native default path. + if (!callbackRef.current) { + return setupIOSAudioManagement(preferSpeakerOutput); + } + const wrapped = ( state: AudioEngineConfigurationState ): AppleAudioConfiguration => { - const cb = callbackRef.current; + const cb = callbackRef.current!; const trackState = engineStateToTrackState(state); - return cb - ? cb(trackState, state.preferSpeakerOutput) - : getDefaultAppleAudioConfigurationForMode( - trackState, - state.preferSpeakerOutput - ); + return cb(trackState, state.preferSpeakerOutput); }; + // setupIOSAudioManagement warns that the callback form is deprecated. return setupIOSAudioManagement(preferSpeakerOutput, wrapped); }, [preferSpeakerOutput]); } diff --git a/src/audio/AudioSession.ts b/src/audio/AudioSession.ts index 0169fd71..56916c6d 100644 --- a/src/audio/AudioSession.ts +++ b/src/audio/AudioSession.ts @@ -199,10 +199,10 @@ export type AppleAudioConfiguration = { /** * @deprecated Retained only for the legacy `useIOSAudioManagement` hook and - * `getDefaultAppleAudioConfigurationForMode`. New code should pass an - * `onConfigureNativeAudio` callback to `setupIOSAudioManagement`, which - * receives an `AudioEngineConfigurationState` (playout/recording/speaker - * booleans). That shape has no direct `AudioTrackState` equivalent. + * `getDefaultAppleAudioConfigurationForMode`. New code should use + * `setupIOSAudioManagement` (optionally with an `IOSAudioSessionPolicy`). + * That API is driven by playout/recording engine state, which has no direct + * `AudioTrackState` equivalent. */ export type AudioTrackState = | 'none'