Environment
- Device: iPhone 17 Pro Max (physical device)
- iOS: 26.6
- React Native: 0.86.0, bare, New Architecture
- Expo: not present
@livekit/react-native: 2.12.0
@livekit/react-native-webrtc: 144.1.2
livekit-client: 2.20.1
- iOS Pod
WebRTC-SDK: 144.7559.10
- Client: receive/playout-only, no local microphone track
- Launch: process kill followed by cold start
Expected behavior
After StartPlayout(), AVAudioEngine and the output AudioUnit should remain running and continuously render the received remote RTP.
Actual behavior
On an affected cold start:
startAndReturnError = true
NSError = nil
AVAudioEngine.running = true
output AudioUnit IsRunning = true
+19.796 ms:
PreRender + PostRender
960 frames
no PostRenderError
+9.956 ms after the last render callback:
output AudioUnit IsRunning = false
AVAudioEngine.running = false
after the drop:
LastRenderError = 0
remote RTP bytes keep progressing
ADM outputRunning = true
ADM playing = true
no automatic restart
totalAudioEnergy = 0
totalSamplesDuration = 0.02 s
The minimal reproducer observed this exact signature in 2 of 3 playout-only cold starts. The third playout-only run was healthy and rendered continuously, so the issue is intermittent.
Controlled input comparison
The reproducer has two otherwise identical branches before the same room.connect():
- A: playout-only, no ADM input call — 2/3 signature-drop, 1/3 stable.
- B:
AudioDeviceModule.startLocalRecording() before connect, without publishing a local track — 2/2 stable.
Both B runs confirmed inputEnabled=true, inputRunning=true, recording=true, hundreds of output render callbacks, and progressing RTP energy/duration.
In this deliberately bounded sample, the comparison is consistent with an input-dependent first-cycle difference. It is not proof that input engagement causes stability, nor proof of the exact mechanism: B also causes the automatic LiveKit audio-session policy to use playAndRecord/videoChat instead of A's playback/spokenAudio, and it builds the input mixer/sink path (and potentially VPIO). The report does not propose enabling input as a product workaround.
Important negative evidence
Across the closed failing transition:
- no WebRTC lifecycle stop/disable/release event was observed;
- no stop call was observed through the instrumented application/WebRTC paths;
- no observed
AVAudioSession.setActive(false) through those paths;
- no observed WebRTC
AudioOutputUnitStop or AudioUnitUninitialize through those paths;
- no interruption, route change, engine configuration-change notification, or media-services reset was recorded;
autoShutdownEnabled=false in the equivalent product trace;
LastRenderError=0, with no PostRenderError.
These are bounded non-hits from the instrumented paths, not a claim that no lower system path exists.
The key state-machine inconsistency is that the hardware-facing output transitions to stopped while AudioEngineDevice::EngineState.output_running remains true. Playing() therefore keeps reporting true and the engine-start condition is never re-entered.
M144 source correlation
In the M144-family audio_engine_device.mm, StartPlayout() commits state.output_running = true, and Playing() reports that logical field. The output-only graph and the input+output graph are configured through different node paths; input engagement can additionally enable the voice-processing path. This source shape is consistent with the observed stale state, but the binary was not rebuilt from that branch and this report does not claim a source-level root cause from correlation alone.
The narrow working hypothesis is therefore: an output-only first-cycle transition stops the physical I/O without entering a lifecycle path that clears or restarts output_running. The bounded A/B result keeps this as the leading family of causes; the exact internal transition remains unknown.
Reproduction steps
- Clone the minimal repository and use Node 24+.
- Copy
runtimeConfig.example.ts to ignored runtimeConfig.ts; configure a short-lived listener URL/token.
- Run
npm ci, then cd ios && pod install.
- Start
qa/publisher.mjs with short-lived publisher credentials for the same Room. It publishes a deterministic 440 Hz mono track and prints no credential.
- Build and install the native
Debug target on a physical iPhone. A native rebuild is required.
- Run the separate microphone permission preflight UI test.
- Kill/cold-launch and run A (
playout-only) several times. The included runner terminates and relaunches the process.
- Run B (
input-preengaged) with the same app, Room, publisher and device.
- Export the
closed-trace: attachment from each .xcresult.
The realtime callback performs only preallocated atomic writes. AudioUnit properties, ADM state and receiver stats are read passively after the observed window.
#1051 reports remote silence while the engine/output remains running until microphone input is enabled. Here the output AudioUnit starts, renders a real quantum, then IsRunning and AVAudioEngine.running both become false while ADM state remains true. The receive-only/duplex relationship may be in the same family, but the observed state transition is different.
client-sdk-swift#1069 is close at the product level: a receive-only listener remains silent until local microphone transmission begins, and its snapshots report engineRunning=false. Its reported lifecycle is different, however: no engine-start callback and no remote PCM render are observed before input starts. In this React Native reproducer, startAndReturnError succeeds, the output AudioUnit becomes running, and one 960-frame PreRender/PostRender quantum is observed before both the AudioUnit and AVAudioEngine stop while ADM output state remains true. react-native-webrtc#1069 also uses the Swift SDK and an application-managed .playback/.default session; this reproducer uses React Native with LiveKit's automatic .playback/.spokenAudio path.
Why this is not react-native-webrtc #89
#89 concerns the old JS/worker-queue deadlock window. This reproducer uses the 2.12.0 native default audio-session path, has no JS engine handlers, the JS UI remains responsive, startAndReturnError succeeds, and the AudioUnit actually renders before stopping.
Attachments
Could you confirm whether AudioEngineDevice has a known output-only first-cycle issue on iOS 26.6, or advise which internal I/O lifecycle transition should update/restart output_running when the output AudioUnit stops without an engine configuration notification?
Environment
@livekit/react-native: 2.12.0@livekit/react-native-webrtc: 144.1.2livekit-client: 2.20.1WebRTC-SDK: 144.7559.10Expected behavior
After
StartPlayout(),AVAudioEngineand the output AudioUnit should remain running and continuously render the received remote RTP.Actual behavior
On an affected cold start:
The minimal reproducer observed this exact signature in 2 of 3 playout-only cold starts. The third playout-only run was healthy and rendered continuously, so the issue is intermittent.
Controlled input comparison
The reproducer has two otherwise identical branches before the same
room.connect():AudioDeviceModule.startLocalRecording()before connect, without publishing a local track — 2/2 stable.Both B runs confirmed
inputEnabled=true,inputRunning=true,recording=true, hundreds of output render callbacks, and progressing RTP energy/duration.In this deliberately bounded sample, the comparison is consistent with an input-dependent first-cycle difference. It is not proof that input engagement causes stability, nor proof of the exact mechanism: B also causes the automatic LiveKit audio-session policy to use
playAndRecord/videoChatinstead of A'splayback/spokenAudio, and it builds the input mixer/sink path (and potentially VPIO). The report does not propose enabling input as a product workaround.Important negative evidence
Across the closed failing transition:
AVAudioSession.setActive(false)through those paths;AudioOutputUnitStoporAudioUnitUninitializethrough those paths;autoShutdownEnabled=falsein the equivalent product trace;LastRenderError=0, with noPostRenderError.These are bounded non-hits from the instrumented paths, not a claim that no lower system path exists.
The key state-machine inconsistency is that the hardware-facing output transitions to stopped while
AudioEngineDevice::EngineState.output_runningremains true.Playing()therefore keeps reporting true and the engine-start condition is never re-entered.M144 source correlation
In the M144-family
audio_engine_device.mm,StartPlayout()commitsstate.output_running = true, andPlaying()reports that logical field. The output-only graph and the input+output graph are configured through different node paths; input engagement can additionally enable the voice-processing path. This source shape is consistent with the observed stale state, but the binary was not rebuilt from that branch and this report does not claim a source-level root cause from correlation alone.The narrow working hypothesis is therefore: an output-only first-cycle transition stops the physical I/O without entering a lifecycle path that clears or restarts
output_running. The bounded A/B result keeps this as the leading family of causes; the exact internal transition remains unknown.Reproduction steps
runtimeConfig.example.tsto ignoredruntimeConfig.ts; configure a short-lived listener URL/token.npm ci, thencd ios && pod install.qa/publisher.mjswith short-lived publisher credentials for the same Room. It publishes a deterministic 440 Hz mono track and prints no credential.Debugtarget on a physical iPhone. A native rebuild is required.playout-only) several times. The included runner terminates and relaunches the process.input-preengaged) with the same app, Room, publisher and device.closed-trace:attachment from each.xcresult.The realtime callback performs only preallocated atomic writes. AudioUnit properties, ADM state and receiver stats are read passively after the observed window.
Why this is not Swift SDK react-native-webrtc#1051
#1051 reports remote silence while the engine/output remains running until microphone input is enabled. Here the output AudioUnit starts, renders a real quantum, then
IsRunningandAVAudioEngine.runningboth become false while ADM state remains true. The receive-only/duplex relationship may be in the same family, but the observed state transition is different.Relation to Swift SDK react-native-webrtc#1069
client-sdk-swift#1069is close at the product level: a receive-only listener remains silent until local microphone transmission begins, and its snapshots reportengineRunning=false. Its reported lifecycle is different, however: no engine-start callback and no remote PCM render are observed before input starts. In this React Native reproducer,startAndReturnErrorsucceeds, the output AudioUnit becomes running, and one 960-frame PreRender/PostRender quantum is observed before both the AudioUnit andAVAudioEnginestop while ADM output state remains true. react-native-webrtc#1069 also uses the Swift SDK and an application-managed.playback/.defaultsession; this reproducer uses React Native with LiveKit's automatic.playback/.spokenAudiopath.Why this is not react-native-webrtc #89
#89 concerns the old JS/worker-queue deadlock window. This reproducer uses the 2.12.0 native default audio-session path, has no JS engine handlers, the JS UI remains responsive,
startAndReturnErrorsucceeds, and the AudioUnit actually renders before stopping.Attachments
artifacts/raw/.xcresultbundles separately; they contain no Room URL/token in the trace payload.Could you confirm whether
AudioEngineDevicehas a known output-only first-cycle issue on iOS 26.6, or advise which internal I/O lifecycle transition should update/restartoutput_runningwhen the output AudioUnit stops without an engine configuration notification?