Environment
@livekit/react-native: 2.12.0
@livekit/react-native-webrtc: 144.1.2
livekit-client: 2.20.1
- React Native 0.85 (Fabric), Expo SDK 55, Hermes
- iOS 18, 26, devices from iPhone 12 through iPhone 17 generation (not device-specific)
- Android: never reproduces (0 occurrences over 30 days vs ~120 on iOS)
- Audio session driven natively via the official
setAppleAudioConfiguration / audio session management in 144.x. Viewer (playout-only) session: category .playback, mode .moviePlayback, empty options. Viewers never publish; microphone permission is never requested.
Summary
For a subscriber-only participant on iOS, the AVAudioEngine used by the AudioEngine-based audio device module stops within the first ~5–25 seconds after subscribing to a remote audio track (occasionally later, mid-session). Playback goes silent while everything else looks healthy. The ADM's internal state desynchronizes from the engine: it still reports playout as active, so nothing inside the ADM ever restarts the engine, and the participant stays silent until the receive stream is torn down and rebuilt.
We instrumented production heavily (Sentry), so we can share the exact state fingerprint and hard field numbers, plus a recovery experiment that narrows the defect down.
State fingerprint at the moment of failure
Captured via AudioDeviceModule / RTCAudioSession state and WebRTC inbound-rtp stats while the user reports silence:
AudioDeviceModule.isEngineRunning() == false // engine actually stopped
AudioDeviceModule.isPlaying() == true // ADM believes playout is active
room.state == connected
canPlaybackAudio == true
rtcSessionActive == true // AVAudioSession category .playback, active
inbound-rtp packetsReceived -> increasing normally
inbound-rtp totalAudioEnergy -> flat (nothing decoded/played)
Packets keep arriving and queue in the jitter buffer; nothing is rendered. Because isPlaying() still returns true, the ADM has no reason to re-init, so the stall is permanent from the user's perspective.
There is a second, rarer variant where the desync is inverted:
isEngineRunning() == true
isPlaying() == true
rtcSessionActive == false // AVAudioSession was never activated
totalAudioEnergy -> flat
Frequency
- ~120 occurrences / ~50 distinct users over 2 weeks, 100% iOS.
- Most stalls begin within the first 25 seconds after the audio track subscription.
- In a single 40-minute live room we observed ~10 distinct viewers hit it, several of them repeatedly (stall → recovery → stall again minutes later).
We cannot reproduce it on demand; it is statistical, but frequent enough that any live room with a few dozen iOS viewers hits it multiple times.
Recovery experiment (this is what points at the ADM)
We shipped a client-side watchdog that polls isEngineRunning() every 5s for playout-only participants and attempts recovery when it reads false while connected:
- Availability cycle —
setEngineAvailability({input:false, output:false}) then {input:false, output:true}: restores audio sub-second in the majority of stalls (engine restarts, isEngineRunning() flips to true, decoded energy resumes). This strongly suggests the engine is simply stopped and a forced reconfiguration is sufficient — i.e. the ADM could self-heal.
- A raw
AudioDeviceModule.startPlayout() is rejected with -1 in this state (presumably because libwebrtc requires its own InitPlayout first), so that is not a viable recovery path from the JS side.
- A minority of devices are toggle-refractory: the availability cycle brings the engine back up but it stops again immediately (3 attempts, all
isEngineRunning() == false afterwards). For those, unsubscribing and resubscribing the remote audio publications — which rebuilds the receive stream and drives a full InitPlayout from inside libwebrtc — recovers audio (~15s).
What we ruled out
- Network / SFU: packets flow normally the whole time; video keeps playing.
- Mute state, volume,
AudioSession category conflicts: session is active with .playback at failure time (variant 1).
- Device class / iOS version specific: spread across models and 26.x versions.
- Android: never occurs, same JS code path.
Disclosure
We maintain local patches on @livekit/react-native-webrtc (audio-session policy application, an interruption watchdog, PiP changes). None of them modify AudioEngineDevice start/stop or the ADM state machine, but we want to be upfront that our binary is not stock. The failure class predates several of those patches; we have not yet run an A/B against fully-default audio session management.
Ask
- Should the AudioEngine ADM detect
engine stopped && playout active and re-init itself? From our field data, an internal restart equivalent to the availability cycle would silently fix the majority of stalls.
- Is there a known race in engine (re)build vs. AVAudioSession activation around track subscription that matches this fingerprint (
isPlaying() == true, engine dead)? The inverted variant (engine running, session never activated) looks like the same race from the other side.
- Happy to provide full sanitized telemetry payloads, timing sequences per user, or to test candidate fixes in our production demo channel — we can validate a fix against real traffic within days.
Sample telemetry (sanitized)
Stall detected (flow sample ~20s after join):
{
"roomState": "connected",
"canPlaybackAudio": true,
"enginePlaying": true,
"engineRecording": false,
"engineRunning": false,
"appleAudio": {
"category": "AVAudioSessionCategoryPlayback",
"mode": "AVAudioSessionModeMoviePlayback",
"otherAudioPlaying": false,
"outputs": "Speaker",
"rtcSessionActive": true,
"rtcSessionInterrupted": false
},
"packetsDelta": 250,
"packetsTotal": 934,
"energyTotal": 0,
"consecutiveSilentSamples": 3
}
Successful recovery via availability cycle:
{
"attempt": 1,
"recovered": true,
"enginePlaying": true,
"engineRecording": false,
"engineRunning": true
}
Toggle-refractory device (3 attempts, later recovered by resubscribe):
{ "attempt": 3, "recovered": false, "enginePlaying": true, "engineRunning": false }
Environment
@livekit/react-native: 2.12.0@livekit/react-native-webrtc: 144.1.2livekit-client: 2.20.1setAppleAudioConfiguration/ audio session management in 144.x. Viewer (playout-only) session: category.playback, mode.moviePlayback, empty options. Viewers never publish; microphone permission is never requested.Summary
For a subscriber-only participant on iOS, the AVAudioEngine used by the AudioEngine-based audio device module stops within the first ~5–25 seconds after subscribing to a remote audio track (occasionally later, mid-session). Playback goes silent while everything else looks healthy. The ADM's internal state desynchronizes from the engine: it still reports playout as active, so nothing inside the ADM ever restarts the engine, and the participant stays silent until the receive stream is torn down and rebuilt.
We instrumented production heavily (Sentry), so we can share the exact state fingerprint and hard field numbers, plus a recovery experiment that narrows the defect down.
State fingerprint at the moment of failure
Captured via
AudioDeviceModule/RTCAudioSessionstate and WebRTC inbound-rtp stats while the user reports silence:Packets keep arriving and queue in the jitter buffer; nothing is rendered. Because
isPlaying()still returns true, the ADM has no reason to re-init, so the stall is permanent from the user's perspective.There is a second, rarer variant where the desync is inverted:
Frequency
We cannot reproduce it on demand; it is statistical, but frequent enough that any live room with a few dozen iOS viewers hits it multiple times.
Recovery experiment (this is what points at the ADM)
We shipped a client-side watchdog that polls
isEngineRunning()every 5s for playout-only participants and attempts recovery when it readsfalsewhile connected:setEngineAvailability({input:false, output:false})then{input:false, output:true}: restores audio sub-second in the majority of stalls (engine restarts,isEngineRunning()flips to true, decoded energy resumes). This strongly suggests the engine is simply stopped and a forced reconfiguration is sufficient — i.e. the ADM could self-heal.AudioDeviceModule.startPlayout()is rejected with-1in this state (presumably because libwebrtc requires its ownInitPlayoutfirst), so that is not a viable recovery path from the JS side.isEngineRunning() == falseafterwards). For those, unsubscribing and resubscribing the remote audio publications — which rebuilds the receive stream and drives a fullInitPlayoutfrom inside libwebrtc — recovers audio (~15s).What we ruled out
AudioSessioncategory conflicts: session is active with.playbackat failure time (variant 1).Disclosure
We maintain local patches on
@livekit/react-native-webrtc(audio-session policy application, an interruption watchdog, PiP changes). None of them modify AudioEngineDevice start/stop or the ADM state machine, but we want to be upfront that our binary is not stock. The failure class predates several of those patches; we have not yet run an A/B against fully-default audio session management.Ask
engine stopped && playout activeand re-init itself? From our field data, an internal restart equivalent to the availability cycle would silently fix the majority of stalls.isPlaying() == true, engine dead)? The inverted variant (engine running, session never activated) looks like the same race from the other side.Sample telemetry (sanitized)
Stall detected (flow sample ~20s after join):
{ "roomState": "connected", "canPlaybackAudio": true, "enginePlaying": true, "engineRecording": false, "engineRunning": false, "appleAudio": { "category": "AVAudioSessionCategoryPlayback", "mode": "AVAudioSessionModeMoviePlayback", "otherAudioPlaying": false, "outputs": "Speaker", "rtcSessionActive": true, "rtcSessionInterrupted": false }, "packetsDelta": 250, "packetsTotal": 934, "energyTotal": 0, "consecutiveSilentSamples": 3 }Successful recovery via availability cycle:
{ "attempt": 1, "recovered": true, "enginePlaying": true, "engineRecording": false, "engineRunning": true }Toggle-refractory device (3 attempts, later recovered by resubscribe):
{ "attempt": 3, "recovered": false, "enginePlaying": true, "engineRunning": false }