Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/platform-audio-stability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
livekit: minor
libwebrtc: minor
livekit-ffi: patch
webrtc-sys: patch
---

Fix platform ADM teardown races on macOS and release FFI handles on dispose.

- Clear remaining FFI handles during `FfiServer::dispose` so native resources are released across repeated initialize/shutdown cycles.
- Stop and detach platform/synthetic audio I/O before peer connection factory teardown, preventing `CaptureWorkerThread` from delivering into destroyed transports.
- Close rooms before dropping FFI track handles and stop platform capture when releasing the platform ADM reference.
- Add `LkRuntime::shutdown_audio_io()` and `PeerConnectionFactoryExt::shutdown_audio_io()` for explicit audio I/O shutdown during runtime teardown.
8 changes: 8 additions & 0 deletions libwebrtc/src/native/peer_connection_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,14 @@ impl PeerConnectionFactory {
pub fn is_platform_adm_active(&self) -> bool {
self.sys_handle.audio_device().is_platform_adm_active()
}

/// Stops platform/synthetic audio I/O and detaches WebRTC callbacks.
///
/// Call before tearing down peer connections so capture worker threads
/// cannot deliver frames into transports that are being destroyed.
pub fn shutdown_audio_io(&self) {
self.sys_handle.shutdown_audio_io();
}
}

#[cfg(test)]
Expand Down
7 changes: 7 additions & 0 deletions libwebrtc/src/peer_connection_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ pub mod native {
fn release_platform_adm(&self);
fn platform_adm_ref_count(&self) -> i32;
fn is_platform_adm_active(&self) -> bool;

/// Stops platform/synthetic audio I/O before runtime teardown.
fn shutdown_audio_io(&self);
}

impl PeerConnectionFactoryExt for PeerConnectionFactory {
Expand Down Expand Up @@ -298,5 +301,9 @@ pub mod native {
fn is_platform_adm_active(&self) -> bool {
self.handle.is_platform_adm_active()
}

fn shutdown_audio_io(&self) {
self.handle.shutdown_audio_io();
}
}
}
3 changes: 2 additions & 1 deletion livekit-ffi/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,9 @@ impl FfiServer {

self.logger.set_capture_logs(false);

// Drop all handles
*self.config.lock() = None; // Invalidate the config
self.ffi_handles.clear();
self.handle_dropped_txs.clear();
}

pub fn send_event(&self, message: proto::ffi_event::Message) -> FfiResult<()> {
Expand Down
6 changes: 4 additions & 2 deletions livekit-ffi/src/server/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,10 @@ impl FfiRoom {

/// Close the room and stop the tasks
pub async fn close(&self, server: &'static FfiServer, reason: DisconnectReason) {
// Close the room first so local tracks are unpublished and WebRTC
// stops platform capture before FFI track handles are dropped.
let _ = self.inner.room.close_with_reason(reason.into()).await;

// drop associated track handles
for (_, &handle) in self.inner.track_handle_lookup.lock().iter() {
if server.drop_handle(handle) {
Expand All @@ -369,8 +373,6 @@ impl FfiRoom {
}
}

let _ = self.inner.room.close_with_reason(reason.into()).await;

let handle = self.handle.lock().await.take();
if let Some(handle) = handle {
let _ = handle.close_tx.send(());
Expand Down
5 changes: 3 additions & 2 deletions livekit/src/platform_audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,9 @@ struct PlatformAdmHandle {
impl Drop for PlatformAdmHandle {
fn drop(&mut self) {
log::debug!("PlatformAdmHandle dropped - releasing Platform ADM");
// Release Platform ADM reference
// When ref_count reaches 0, the Platform ADM is terminated
// Stop platform capture before releasing the ADM reference so the
// capture worker cannot deliver frames during teardown.
let _ = self.runtime.stop_recording();
self.runtime.release_platform_adm();
log::info!(
"PlatformAdmHandle: released Platform ADM (ref_count now: {})",
Expand Down
8 changes: 8 additions & 0 deletions livekit/src/rtc_engine/lk_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,18 @@ impl LkRuntime {
pub(crate) fn is_platform_adm_active(&self) -> bool {
self.pc_factory.is_platform_adm_active()
}

/// Stops platform/synthetic audio I/O before runtime teardown.
#[cfg(not(target_arch = "wasm32"))]
pub fn shutdown_audio_io(&self) {
self.pc_factory.shutdown_audio_io();
}
}

impl Drop for LkRuntime {
fn drop(&mut self) {
log::debug!("LkRuntime::drop()");
#[cfg(not(target_arch = "wasm32"))]
self.shutdown_audio_io();
}
}
11 changes: 11 additions & 0 deletions webrtc-sys/include/livekit/adm_proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ class AdmProxy : public webrtc::AudioDeviceModule {
/// Returns true if Platform ADM is currently active (ref_count > 0).
bool is_platform_adm_active() const;

/// Stops platform and synthetic audio I/O and detaches the WebRTC
/// AudioTransport callback.
///
/// Call before tearing down the peer connection factory so capture worker
/// threads cannot deliver frames into transports that are being destroyed.
void StopAudioIO();

// ===========================================================================
// Recording/Playout Control
// ===========================================================================
Expand Down Expand Up @@ -227,6 +234,10 @@ class AdmProxy : public webrtc::AudioDeviceModule {
// Must be called with mutex_ held.
void SwitchRecordingAdmIfNeeded();

// Stops platform capture/playout and detaches its callback without touching
// synthetic mode. Must be called with mutex_ held.
void StopPlatformAudioIO();

#if defined(__ANDROID__)
// Lazily creates the Platform ADM on Android.
// Must be called with mutex_ held.
Expand Down
4 changes: 4 additions & 0 deletions webrtc-sys/include/livekit/peer_connection_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class PeerConnectionFactory {

RtpCapabilities rtp_receiver_capabilities(MediaType type) const;

/// Stops platform/synthetic audio I/O and detaches callbacks on the worker
/// thread before peer connection factory teardown.
void shutdown_audio_io() const;

std::shared_ptr<RtcRuntime> rtc_runtime() const { return rtc_runtime_; }
std::shared_ptr<AudioDeviceController> audio_device() const;

Expand Down
60 changes: 55 additions & 5 deletions webrtc-sys/src/adm_proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ AdmProxy::AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread
AdmProxy::~AdmProxy() {
RTC_LOG(LS_VERBOSE) << "AdmProxy::~AdmProxy()";

StopAudioIO();

webrtc::MutexLock lock(&mutex_);
if (synthetic_adm_) {
synthetic_adm_->Terminate();
synthetic_adm_ = nullptr;
Expand Down Expand Up @@ -180,6 +183,7 @@ void AdmProxy::ReleasePlatformAdm() {
// Note: We do NOT terminate the Platform ADM - it stays alive until destructor.
// This avoids iOS KVO race conditions from re-creating the ADM.
if (platform_adm_ref_count_ == 0) {
StopPlatformAudioIO();
SwitchPlayoutModeIfNeeded();
SwitchRecordingAdmIfNeeded();
}
Expand Down Expand Up @@ -273,6 +277,42 @@ void AdmProxy::SwitchRecordingAdmIfNeeded() {
}
}

void AdmProxy::StopPlatformAudioIO() {
recording_ = false;

if (platform_adm_) {
platform_adm_->RegisterAudioCallback(nullptr);
platform_adm_->StopRecording();
platform_adm_->StopPlayout();
// platform_adm_ is kept alive for re-acquire and iOS compatibility; see
// ReleasePlatformAdm().
}
}

void AdmProxy::StopAudioIO() {
webrtc::MutexLock lock(&mutex_);

recording_ = false;
playing_ = false;
recording_initialized_ = false;
playout_initialized_ = false;

if (platform_adm_) {
platform_adm_->RegisterAudioCallback(nullptr);
platform_adm_->StopRecording();
platform_adm_->StopPlayout();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, curiously, why couldn't we set platform_adm_ = nullptr after calling StopXXX() function ?

}

if (synthetic_adm_) {
synthetic_adm_->RegisterAudioCallback(nullptr);
synthetic_adm_->StopRecording();
synthetic_adm_->StopPlayout();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

// synthetic_adm_ is kept alive until ~AdmProxy() / Terminate().
}

audio_transport_ = nullptr;
}

// =============================================================================
// AudioDeviceModule Interface Implementation
// =============================================================================
Expand Down Expand Up @@ -306,8 +346,9 @@ int32_t AdmProxy::Init() {
}

int32_t AdmProxy::Terminate() {
webrtc::MutexLock lock(&mutex_);
StopAudioIO();

webrtc::MutexLock lock(&mutex_);
int32_t result = 0;
if (synthetic_adm_) {
result = synthetic_adm_->Terminate();
Expand Down Expand Up @@ -554,11 +595,20 @@ int32_t AdmProxy::StopRecording() {
webrtc::MutexLock lock(&mutex_);
recording_ = false;

auto* adm = recording_adm();
if (adm) {
return adm->StopRecording();
int32_t result = 0;
if (platform_adm_) {
const int32_t platform_result = platform_adm_->StopRecording();
if (result == 0) {
result = platform_result;
}
}
return 0;
if (synthetic_adm_) {
const int32_t synthetic_result = synthetic_adm_->StopRecording();
if (result == 0) {
result = synthetic_result;
}
}
return result;
}

bool AdmProxy::Recording() const {
Expand Down
10 changes: 10 additions & 0 deletions webrtc-sys/src/peer_connection_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ PeerConnectionFactory::PeerConnectionFactory(
PeerConnectionFactory::~PeerConnectionFactory() {
RTC_LOG(LS_VERBOSE) << "PeerConnectionFactory::~PeerConnectionFactory()";

shutdown_audio_io();

peer_factory_ = nullptr;
audio_device_ = nullptr;
rtc_runtime_->worker_thread()->BlockingCall(
Expand Down Expand Up @@ -163,6 +165,14 @@ std::shared_ptr<AudioDeviceController> PeerConnectionFactory::audio_device() con
return audio_device_;
}

void PeerConnectionFactory::shutdown_audio_io() const {
rtc_runtime_->worker_thread()->BlockingCall([this] {
if (adm_proxy_) {
adm_proxy_->StopAudioIO();
}
});
}

std::shared_ptr<PeerConnectionFactory> create_peer_connection_factory() {
return std::make_shared<PeerConnectionFactory>(RtcRuntime::create());
}
Expand Down
2 changes: 2 additions & 0 deletions webrtc-sys/src/peer_connection_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub mod ffi {

fn create_peer_connection_factory() -> SharedPtr<PeerConnectionFactory>;

fn shutdown_audio_io(self: &PeerConnectionFactory);

fn create_peer_connection(
self: &PeerConnectionFactory,
config: RtcConfiguration,
Expand Down
Loading