Skip to content

AURemoteIO not disposed after PeerConnection close, causing second P2P call to have no audio #255

@poker-hand

Description

@poker-hand

Environment

  • LiveKitWebRTC version: 125.6422.11 (M125)

  • iOS version: 18.x

  • Device: iPhone (reproducible on all iOS devices)

  • Usage pattern: Direct LKRTCPeerConnection usage (not via LiveKit Room API)

Bug Description

When using LKRTCPeerConnection directly for P2P calls on iOS, the first call works perfectly, but after hanging up and making a second call, audio playback is silent despite ICE connection succeeding and remote tracks being received.

The error in console:

AURemoteIO.cpp:1666  AUIOClient_StartIO failed (561145187)

Error code 561145187 (0x21706D63) = kAudioUnitErr_CannotDoInCurrentContext.

Root Cause Analysis

The Problem

WebRTC iOS's AudioDeviceIOS manages a AURemoteIO audio unit via VoiceProcessingAudioUnit. When a PeerConnection is closed, the audio unit is stopped and uninitialized, but NOT disposed:

// voice_processing_audio_unit.mm - Stop()
bool VoiceProcessingAudioUnit::Stop() {
  OSStatus result = AudioOutputUnitStop(vpio_unit_);  // Stop, but don't release
  state_ = kInitialized;
  return true;
}

// voice_processing_audio_unit.mm - Uninitialize()
bool VoiceProcessingAudioUnit::Uninitialize() {
OSStatus result = AudioUnitUninitialize(vpio_unit_); // Uninitialize, but don't release
state_ = kUninitialized;
return true;
}

The DisposeAudioUnit() method (called from destructor and error paths) should call AudioComponentInstanceDispose(vpio_unit_) to fully release the CoreAudio resource, but based on source code analysis, the AURemoteIO handle persists after PeerConnection close.

When a new PeerConnection is created for the second call, AudioDeviceIOS tries to start a new AURemoteIO, but detects the old instance is still running → kAudioUnitErr_CannotDoInCurrentContext.

Why This Didn't Happen with Older WebRTC Versions

This project previously used OWT SDK (Intel Open WebRTC Toolkit) with WebRTC ~M73 (2019), which did NOT have this bug. In M73, ShutdownPlayOrRecord() called AudioComponentInstanceDispose to fully release the AURemoteIO handle. Between M73 and M125, Google WebRTC changed the AudioDeviceIOS cleanup behavior.

Comparison

  OWT WebRTC (~M73) LiveKitWebRTC (M125)
ShutdownPlayOrRecord() Calls AudioComponentInstanceDispose Only AudioOutputUnitStop + AudioUnitUninitialize
Second P2P call Works No audio (AUIOClient_StartIO failed)

The only thing that works is exit(0) to kill the process, which is not acceptable for App Store.

Suggested Fix

Ensure DisposeAudioUnit() calls AudioComponentInstanceDispose to fully release the AURemoteIO handle:

void VoiceProcessingAudioUnit::DisposeAudioUnit() {
  if (vpio_unit_) {
    AudioOutputUnitStop(vpio_unit_);
    AudioUnitUninitialize(vpio_unit_);
    AudioComponentInstanceDispose(vpio_unit_);  // ← Add this line
    vpio_unit_ = nullptr;
  }
  state_ = kInitRequired;
}

Note: I was unable to locate the actual implementation of DisposeAudioUnit() in the livekit-prefixed-m125 branch — it's declared in voice_processing_audio_unit.h and called from voice_processing_audio_unit.mm, but the definition appears to be in a separate compilation unit (possibly moved by the "Audio Device Optimization" patch group). The exact patch location would need to be verified by cloning the full webrtc-sdk/webrtc repository.

Impact

This bug affects any iOS app that:

  1. Uses LKRTCPeerConnection directly (not via LiveKit Room API)

  2. Supports multiple sequential P2P calls within the same app session

Apps using LiveKit's Room API are not affected because LiveKit's AudioManager uses a different audio backend (AVAudioEngine + Voice Processing I/O) with proper lifecycle management via setEngineAvailability.

## Environment
  • LiveKitWebRTC version: 125.6422.11 (M125)
  • iOS version: 18.x
  • Device: iPhone (reproducible on all iOS devices)
  • Usage pattern: Direct LKRTCPeerConnection usage (not via LiveKit Room API)

Bug Description

When using LKRTCPeerConnection directly for P2P calls on iOS, the first call works perfectly, but after hanging up and making a second call, audio playback is silent despite ICE connection succeeding and remote tracks being received.

The error in console:

AURemoteIO.cpp:1666  AUIOClient_StartIO failed (561145187)

Error code 561145187 (0x21706D63) = kAudioUnitErr_CannotDoInCurrentContext.

Root Cause Analysis

The Problem

WebRTC iOS's AudioDeviceIOS manages a AURemoteIO audio unit via VoiceProcessingAudioUnit. When a PeerConnection is closed, the audio unit is stopped and uninitialized, but NOT disposed:

// voice_processing_audio_unit.mm - Stop()
bool VoiceProcessingAudioUnit::Stop() {
  OSStatus result = AudioOutputUnitStop(vpio_unit_);  // Stop, but don't release
  state_ = kInitialized;
  return true;
}

// voice_processing_audio_unit.mm - Uninitialize()
bool VoiceProcessingAudioUnit::Uninitialize() {
  OSStatus result = AudioUnitUninitialize(vpio_unit_);  // Uninitialize, but don't release
  state_ = kUninitialized;
  return true;
}

The DisposeAudioUnit() method (called from destructor and error paths) should call AudioComponentInstanceDispose(vpio_unit_) to fully release the CoreAudio resource, but based on source code analysis, the AURemoteIO handle persists after PeerConnection close.

When a new PeerConnection is created for the second call, AudioDeviceIOS tries to start a new AURemoteIO, but detects the old instance is still running → kAudioUnitErr_CannotDoInCurrentContext.

Why This Didn't Happen with Older WebRTC Versions

This project previously used OWT SDK (Intel Open WebRTC Toolkit) with WebRTC ~M73 (2019), which did NOT have this bug. In M73, ShutdownPlayOrRecord() called AudioComponentInstanceDispose to fully release the AURemoteIO handle. Between M73 and M125, Google WebRTC changed the AudioDeviceIOS cleanup behavior.

Comparison


OWT WebRTC (~M73) LiveKitWebRTC (M125)
ShutdownPlayOrRecord() Calls AudioComponentInstanceDispose Only AudioOutputUnitStop + AudioUnitUninitialize
Second P2P call Works No audio (AUIOClient_StartIO failed)

Log Comparison

First call (works):

addTrack audioTrack=EC51D715..., sender=OK, isEnabled=1
setRemoteSDP called, type=answer
didChangeIceConnectionState 2  ← ICE connected
[Audio works]

Second call (no audio):

addTrack audioTrack=1F5D4B1B..., sender=OK, isEnabled=1
setRemoteSDP called, type=answer
AUIOClient_StartIO failed (561145187)  ← Audio unit start failed
didChangeIceConnectionState 2  ← ICE connected
didAddReceiver track=..., kind=audio  ← Remote audio track received
[No audio]

Workarounds Attempted (All Failed)

# Approach Result
1 useManualAudio = YES/NO + isAudioEnabled toggle Interferes with signaling, remote SDP becomes recvonly
2 AVAudioSession.setActive(NO/YES) Kills first call's audio unit too
3 Audio track isEnabled = NO/YES toggle No effect (tracks are newly created each call)
4 Re-create RTCPeerConnectionFactory No effect (AudioDeviceModule still detects conflict)
5 Keep factory as singleton (don't release on disconnect) No effect (AURemoteIO still not disposed)
6 LKRTCAudioSession.setActive:NO after disconnect No effect (WebRTC C++ auto-deactivates after close)
7 Force release previousClient + delay before reconnect No effect
8 Double deactive (LKRTCAudioSession + AVAudioSession) + @autoreleasepool force release factory Worse - breaks WebRTC C++ state machine
9 audioDeviceModule.stopPlayout/stopRecording No effect (already stopped at disconnect time)

The only thing that works is exit(0) to kill the process, which is not acceptable for App Store.

Suggested Fix

Ensure DisposeAudioUnit() calls AudioComponentInstanceDispose to fully release the AURemoteIO handle:

void VoiceProcessingAudioUnit::DisposeAudioUnit() {
  if (vpio_unit_) {
    AudioOutputUnitStop(vpio_unit_);
    AudioUnitUninitialize(vpio_unit_);
    AudioComponentInstanceDispose(vpio_unit_);  // ← Add this line
    vpio_unit_ = nullptr;
  }
  state_ = kInitRequired;
}

Note: I was unable to locate the actual implementation of DisposeAudioUnit() in the livekit-prefixed-m125 branch — it's declared in voice_processing_audio_unit.h and called from voice_processing_audio_unit.mm, but the definition appears to be in a separate compilation unit (possibly moved by the "Audio Device Optimization" patch group). The exact patch location would need to be verified by cloning the full webrtc-sdk/webrtc repository.

Impact

This bug affects any iOS app that:

  1. Uses LKRTCPeerConnection directly (not via LiveKit Room API)
  2. Supports multiple sequential P2P calls within the same app session

Apps using LiveKit's Room API are not affected because LiveKit's AudioManager uses a different audio backend (AVAudioEngine + Voice Processing I/O) with proper lifecycle management via setEngineAvailability.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions