Skip to content

PlatformAudio stops working after the last PeerConnection is released: AdmProxy::Terminate tears down the platform ADM #1468

Description

@WorkTimer

Summary

Once the last PeerConnection in the process is destroyed, PlatformAudio stops working: the next PlatformAudio::new() logs -1 recording devices, -1 playout devices, and recording_devices() never returns.

Cause

The media engine is reference-counted per PeerConnection (ConnectionContext::MediaEngineReference): WebRtcVoiceEngine::Init() runs when the first PeerConnection is created, and WebRtcVoiceEngine::Terminate() when the last one is destroyed, which calls adm()->Terminate().

AdmProxy::Init() is a no-op (the synthetic and platform ADMs are initialized in the constructor), but AdmProxy::Terminate() forwards to both sub-ADMs. After one Terminate/Init cycle the platform ADM stays uninitialized: RecordingDevices() / PlayoutDevices() return -1, and PlatformAudio::recording_device_count() casts that i16 to usize, so recording_devices() walks 2^64 - 1 indices at 100% CPU. The synthetic ADM loses its playout queue the same way.

Where the Terminate comes from (macOS, lldb, worker thread):

PeerConnection::~PeerConnection
  -> ConnectionContext::MediaEngineReference::~MediaEngineReference
  -> WebRtcVoiceEngine::Terminate
  -> AdmProxy::Terminate
  -> AudioDeviceModuleImpl::Terminate
  -> AudioDeviceMac::Terminate

When it is reachable

Whenever closed PeerConnections are actually released and no other PeerConnection is alive, for example:

  1. PlatformAudio::new() and publish a track created from audio.rtc_source()
  2. room.close() the only room and drop it
  3. Use PlatformAudio again: recording_devices(), or connect a new room and publish the microphone -> hangs

We hit it in a fork that carries #1405, which lets the room's graph be released after close. With #1436 / #1435 it also follows server-ended rooms. It stays hidden while any PeerConnection is kept alive (a leaked one, or a second room connected in the same process).

What we did

  • AdmProxy::Terminate() no longer terminates the sub-ADMs; they live as long as the proxy and are still terminated in ~AdmProxy. The voice engine already stops playout and recording and unregisters its callback before it calls Terminate(), so there is nothing left to stop there. Re-initializing them in Init() would also work, but would have to restore device selection and the playout/recording mode state.
  • PlatformAudio treats a negative device count as zero instead of casting it.
  • Regression test: connect, publish a PlatformAudio track, close the last room and wait until it is dropped; the recording-device count must be unchanged and the next room must publish the microphone again (before the fix: 5 devices, then 0).

Environment: macOS 26 on Apple Silicon, libwebrtc webrtc-51ef663 (m144), livekit-server 1.13.6 and LiveKit Cloud.

Activity

  1. RaphaelFakhri commented on Sep 29, 2026

    @RaphaelFakhri

    The usize cast in PlatformAudio::recording_device_count() and playout_device_count() can be fixed independently of the AdmProxy::Terminate() change. A negative i16 from the ADM wraps to usize::MAX, so recording_devices() never returns. A small PR maps a negative count to zero and adds a unit test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions