Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/ffi-handle-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
livekit-ffi: patch
---

Clear remaining FFI handles during dispose so platform audio resources are released across repeated initialize/shutdown cycles.
26 changes: 24 additions & 2 deletions livekit-ffi/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,21 @@ impl FfiServer {

self.logger.set_capture_logs(false);

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

// Drop remaining FFI handles so native resources they own are released on dispose.
log::debug!("{} FFI handles remaining at dispose before clearing", self.ffi_handles.len());
let platform_audio_handles = self
.ffi_handles
.iter()
.filter(|handle| handle.value().is::<platform_audio::FfiPlatformAudio>())
.count();
log::debug!(

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.

unless we add an explicit API to shutdown the PlatformAudio, otherwise it is expected that some platform_audio_handles will remain valid here.

"{} PlatformAudio FFI handles remaining at dispose before clearing",
platform_audio_handles
);
self.ffi_handles.clear();
Comment thread
alan-george-lk marked this conversation as resolved.
self.handle_dropped_txs.clear();
}

pub fn send_event(&self, message: proto::ffi_event::Message) -> FfiResult<()> {
Expand Down Expand Up @@ -205,6 +218,9 @@ impl FfiServer {
where
T: FfiHandle,
{
if std::any::type_name::<T>().contains("platform_audio::FfiPlatformAudio") {
log::debug!("storing PlatformAudio FFI handle {id}");
}
self.ffi_handles.insert(id, Box::new(handle));
}

Expand Down Expand Up @@ -254,7 +270,13 @@ impl FfiServer {
}

pub fn drop_handle(&self, id: FfiHandleId) -> bool {
let existed = self.ffi_handles.remove(&id).is_some();
let removed = self.ffi_handles.remove(&id);
let existed = removed.is_some();
if let Some((_, handle)) = &removed {
if handle.is::<platform_audio::FfiPlatformAudio>() {
log::debug!("dropping PlatformAudio FFI handle {id}");
}
}
self.handle_dropped_txs.remove(&id);
if !existed {
log::warn!("Attempted to drop unknown FFI handle: {id}");
Expand Down
9 changes: 8 additions & 1 deletion livekit-ffi/src/server/platform_audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ use crate::{proto, FfiResult};

/// FFI wrapper for PlatformAudio handle.
pub struct FfiPlatformAudio {
pub handle_id: u64,
pub audio: PlatformAudio,
}

impl FfiHandle for FfiPlatformAudio {}

impl Drop for FfiPlatformAudio {
fn drop(&mut self) {
log::debug!("[PLATFORM_AUDIO_FFI] dropped PlatformAudio handle_id={}", self.handle_id);
}
}

pub fn on_new_platform_audio(
server: &'static FfiServer,
_req: proto::NewPlatformAudioRequest,
Expand All @@ -48,7 +55,7 @@ pub fn on_new_platform_audio(
playout_device_count: playout_count,
};

server.store_handle(handle_id, FfiPlatformAudio { audio });
server.store_handle(handle_id, FfiPlatformAudio { handle_id, audio });

Ok(proto::NewPlatformAudioResponse {
message: Some(proto::new_platform_audio_response::Message::PlatformAudio(
Expand Down
13 changes: 13 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,19 @@ PeerConnectionFactory::PeerConnectionFactory(
PeerConnectionFactory::~PeerConnectionFactory() {
RTC_LOG(LS_VERBOSE) << "PeerConnectionFactory::~PeerConnectionFactory()";

// Terminate the ADM *before* destroying peer_factory_. peer_factory_ owns the
// AudioState/AudioTransportImpl that the platform ADM's capture worker thread
// (e.g. AudioDeviceMac::CaptureWorkerThread) delivers into via
// RecordedDataIsAvailable -> SendProcessedData. Destroying peer_factory_ first
// frees that AudioTransportImpl while the capture worker is still running,
// so the worker dereferences freed memory (BUS / use-after-free observed on
// macOS). AdmProxy::Terminate() stops recording/playout, which joins the
// ADM's worker threads and closes that window. Runs on the worker thread to
// match WebRTC's ADM threading expectations.
if (adm_proxy_) {
rtc_runtime_->worker_thread()->BlockingCall([this] { adm_proxy_->Terminate(); });
}

peer_factory_ = nullptr;
audio_device_ = nullptr;
rtc_runtime_->worker_thread()->BlockingCall(
Expand Down
Loading