diff --git a/.changeset/adm-proxy-worker-thread.md b/.changeset/adm-proxy-worker-thread.md new file mode 100644 index 000000000..0c538bcce --- /dev/null +++ b/.changeset/adm-proxy-worker-thread.md @@ -0,0 +1,10 @@ +--- +livekit: patch +webrtc-sys: patch +--- + +Make AdmProxy worker-thread-affine: all platform ADM access now happens on the WebRTC worker thread, matching the ADM threading contract. + +- Fixes Android platform recording delivering no audio: the audio transport was never registered on the lazily created ADM. +- Fixes a shutdown race by keeping the runtime threads alive as long as Rust can reach the audio device controller. +- Adds a `platform_audio` example exercising the PlatformAudio API and the worker-thread marshaling. diff --git a/docs/ADM_PROXY_DESIGN.md b/docs/ADM_PROXY_DESIGN.md index 797192de1..b6f3d5fb9 100644 --- a/docs/ADM_PROXY_DESIGN.md +++ b/docs/ADM_PROXY_DESIGN.md @@ -100,9 +100,11 @@ These two methods must coexist without interference. │ │ ▼ ▼ ┌────────────────────────────────────────────┐ - │ Platform ADM (Lazy Init) │ - │ Created when: AcquirePlatformAdm() │ - │ Destroyed when: ref_count → 0 │ + │ Platform ADM │ + │ Created: eagerly at proxy construction │ + │ (lazily on first acquire on Android) │ + │ Active when: ref_count > 0 │ + │ Destroyed: with the proxy │ └──────────────────┬─────────────────────────┘ │ ┌──────────┴──────────┐ @@ -180,26 +182,33 @@ These two methods must coexist without interference. 3. **NativeAudioSource**: Existing API for manual audio frame pushing 4. **external_audio_source.patch**: WebRTC patch to prevent audio mixing conflicts -### Lazy Initialization + Reference Counting Pattern +### Reference Counting Pattern -The Platform ADM is **not** created at startup. Instead, it's created lazily when first needed: +The Platform ADM instance exists for the proxy's whole lifetime, but stays idle until acquired. The ref count and gates decide whether it actually touches the hardware: ```cpp // adm_proxy.h class AdmProxy : public webrtc::AudioDeviceModule { - // Platform ADM is created lazily, NOT at startup - webrtc::scoped_refptr platform_adm_; + // All state is owned by the worker thread, no locks needed - // Reference count for Platform ADM lifecycle - int platform_adm_ref_count_ = 0; + // Synthetic ADM pumps the pipeline when platform playout is inactive + webrtc::scoped_refptr synthetic_adm_ + RTC_GUARDED_BY(worker_thread_); + + // Platform ADM for real audio I/O + webrtc::scoped_refptr platform_adm_ + RTC_GUARDED_BY(worker_thread_); + + // Reference count for Platform ADM users + int platform_adm_ref_count_ RTC_GUARDED_BY(worker_thread_) = 0; // Gate controls whether microphone recording is active // Default: FALSE - NativeAudioSource works without interference - bool recording_enabled_ = false; + bool recording_enabled_ RTC_GUARDED_BY(worker_thread_) = false; // Gate controls whether playout goes through platform speakers // Default: FALSE - synthetic mode (FFI callbacks to application) - bool playout_enabled_ = false; + bool playout_enabled_ RTC_GUARDED_BY(worker_thread_) = false; }; ``` @@ -207,12 +216,32 @@ class AdmProxy : public webrtc::AudioDeviceModule { | Method | Effect | |--------|--------| -| `AcquirePlatformAdm()` | Increments ref_count. Creates Platform ADM on first call. | -| `ReleasePlatformAdm()` | Decrements ref_count. Terminates Platform ADM when count reaches 0. | +| `AcquirePlatformAdm()` | Increments ref_count. Switches to platform mode on 0 → 1 (and creates the ADM on Android). | +| `ReleasePlatformAdm()` | Decrements ref_count. Switches back to synthetic mode on 1 → 0. The Platform ADM instance stays alive until the proxy is destroyed. | + +**Creation timing:** -**Why Lazy Initialization?** +On most platforms the Platform ADM is created eagerly in the proxy constructor. iOS requires early AVAudioSession setup to avoid KVO race conditions, and keeping a single instance for the proxy's lifetime avoids the races that re-creation caused. Whether the platform hardware is actually used is controlled purely by the ref count and the gates below, so eager creation does not interfere with synthetic mode. Android is the exception, its ADM is created lazily on first acquire because JNI may not be initialized when the proxy is constructed. -On iOS, creating Platform ADM configures the AVAudioSession for VoIP mode. This interferes with Unity's AudioSource playback even when PlatformAudio isn't being used. By deferring Platform ADM creation until actually needed, synthetic mode works correctly. +### Threading Model + +The proxy is worker-thread-affine, matching WebRTC's own convention that the +ADM is created and driven on the worker thread: + +- Every public method marshals once at the boundary onto the worker thread + (`RunOnWorker`, a `BlockingCall` that runs inline when the caller is already + the worker). Calls originating from WebRTC internals (AudioState, the voice + engine) are already on the worker and never hop. +- All mutable state, including both sub ADMs, is owned by the worker thread. + There are no mutexes, and mode transitions are plain sequential worker code, + so transitions cannot interleave. +- Platform ADM implementations bind a sequence checker to their construction + thread. The proxy is constructed on the worker thread so the eagerly created + Platform ADM binds to it. Destruction may happen on any thread and hops to + the worker for teardown. +- No realtime audio path goes through the proxy. The registered + `AudioTransport` is invoked directly by the sub ADMs from their own audio + threads. ### Recording/Playout Gates @@ -240,27 +269,7 @@ When Platform ADM is active, additional gates control behavior: ### Synthetic Playout Mode -When Platform ADM is not active (or `playout_enabled_ = false`), the SDK uses synthetic playout to keep WebRTC's audio pipeline functioning: - -```cpp -// AdmProxy runs a periodic task that pulls audio from WebRTC -void AdmProxy::StartSyntheticPlayoutTask() { - // 10ms task interval (100 calls/second) - stub_audio_queue_->PostDelayedTask( - [this] { - if (playing_ && audio_transport_) { - // Pull audio from WebRTC to keep pipeline alive - audio_transport_->NeedMorePlayData( - kSamplesPer10Ms, kBytesPerSample, kChannels, - kSampleRate, stub_data_.data(), samples_out, - &elapsed_time_ms, &ntp_time_ms); - } - // Reschedule - StartSyntheticPlayoutTask(); - }, - TimeDelta::Millis(10)); -} -``` +When platform playout is not active (ref_count is 0 or `playout_enabled_ = false`), the proxy delegates playout to `SyntheticAudioDevice` (webrtc-sys/src/synthetic_audio_device.cpp), a minimal ADM that runs a 10ms repeating task on its own task queue and pulls audio from WebRTC via `NeedMorePlayData()` without touching platform hardware. The proxy registers the same `AudioTransport` with both sub ADMs and starts/stops whichever one the current mode selects. **Why Synthetic Playout is Needed:** @@ -272,7 +281,7 @@ void AdmProxy::StartSyntheticPlayoutTask() { | Mode | Recording | Playout | Platform ADM | Use Case | |------|-----------|---------|--------------|----------| -| Synthetic | NativeAudioSource | FFI callbacks | Not created | Unity audio, agents | +| Synthetic | NativeAudioSource | FFI callbacks | Idle (created, not started) | Unity audio, agents | | Platform | ADM microphone | ADM speakers | Active | VoIP with AEC | | Hybrid | Both supported | Both supported | Active | Mixed scenarios | @@ -328,11 +337,12 @@ This is the default mode used by Unity, agents, and applications that manage the ┌─────────────────────────────────────────────────────────────────┐ │ AdmProxy │ │ │ - │ platform_adm_ = NULL ◀─── Platform ADM NOT created │ + │ platform_adm_ = idle ◀─── created but not started │ + │ ref_count = 0 │ │ playout_enabled_ = false │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ - │ │ Synthetic Playout Task (10ms interval) │ │ + │ │ SyntheticAudioDevice (10ms internal task) │ │ │ │ │ │ │ │ NeedMorePlayData() ─────▶ Pulls audio from mixer │ │ │ │ (audio NOT sent to speakers) │ │ @@ -351,10 +361,10 @@ This is the default mode used by Unity, agents, and applications that manage the KEY POINTS: - • Platform ADM is NEVER created in synthetic mode + • Platform ADM is never STARTED in synthetic mode (no mic/speaker I/O) + (on Android it is not even created until first acquire) • No interference with Unity AudioSource or application audio routing - • iOS: AVAudioSession NOT configured for VoIP → Unity audio works - • Synthetic playout task keeps WebRTC pipeline alive + • SyntheticAudioDevice keeps the WebRTC pipeline alive ``` ### Flow 2: Platform Audio Mode (PlatformAudio with ADM) @@ -376,9 +386,8 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea │ 1. runtime.acquire_platform_adm() │ │ └─▶ AdmProxy::AcquirePlatformAdm() │ │ └─▶ ref_count++ (1) │ - │ └─▶ CreatePlatformAdm() [first time only] │ - │ └─▶ webrtc::AudioDeviceModule::Create() │ - │ └─▶ platform_adm_->Init() │ + │ └─▶ on 0 -> 1: switch playout/recording to platform mode │ + │ (Android only: create + Init the ADM first) │ │ │ │ 2. runtime.set_adm_recording_enabled(true) │ │ └─▶ AdmProxy::set_recording_enabled(true) │ @@ -483,21 +492,22 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea │ SYNTHETIC MODE │ │ (Default State) │ │ │ - │ • platform_adm_ = NULL │ + │ • platform_adm_ = idle │ │ • platform_adm_ref_count_ = 0 │ │ • recording_enabled_ = false │ │ • playout_enabled_ = false │ │ │ │ AdmProxy handles all ADM calls: │ │ - Recording ops → no-op (success) │ - │ - Playout ops → synthetic task │ + │ - Playout ops → SyntheticAudioDevice│ │ │ └──────────────────┬──────────────────┘ │ PlatformAudio::new() └─▶ acquire_platform_adm() └─▶ ref_count = 1 - └─▶ CreatePlatformAdm() + └─▶ switch to platform mode + (Android: create + Init ADM first) │ ▼ ┌─────────────────────────────────────┐ @@ -519,7 +529,8 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea drop(PlatformAudio) └─▶ release_platform_adm() └─▶ ref_count = 0 - └─▶ TerminatePlatformAdm() + └─▶ switch back to synthetic mode + (ADM stops but stays alive) │ ▼ ┌─────────────────────────────────────┐ @@ -527,7 +538,7 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea │ SYNTHETIC MODE │ │ (Back to Default) │ │ │ - │ • platform_adm_ = NULL │ + │ • platform_adm_ = idle │ │ • platform_adm_ref_count_ = 0 │ │ • recording_enabled_ = false │ │ • playout_enabled_ = false │ @@ -548,7 +559,7 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea ┌──────────────────────┐ ┌──────────────────────┐ │ PlatformAudio::new() │ │ drop(audio) │ │ ref_count: 0 → 1 │ │ ref_count: 1 → 0 │ - │ CREATE Platform ADM │ │ TERMINATE Platform │ + │ START Platform ADM │ │ STOP Platform ADM │ └──────────┬───────────┘ └──────────┬───────────┘ │ │ ▼ ▼ @@ -567,7 +578,7 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea │ audio1 = │ │ audio2 = │ │drop(audio1│ │drop(audio2│ │ new() │ │ new() │ │ │ │ │ │ ref: 0→1 │ │ ref: 1→2 │ │ ref: 2→1 │ │ ref: 1→0 │ - │ CREATE │ │ (reuse) │ │ (still │ │ TERMINATE │ + │ START │ │ (reuse) │ │ (still │ │ STOP │ │ ADM │ │ │ │ active) │ │ ADM │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ @@ -602,69 +613,50 @@ This mode is used for VoIP applications that need AEC and direct microphone/spea │ │ │ When both Unity clients call DisposeRequest: │ │ handle_1 dispose → ref_count = 1 │ - │ handle_2 dispose → ref_count = 0 → TERMINATE │ + │ handle_2 dispose → ref_count = 0 → back to synthetic mode │ └─────────────────────────────────────────────────────────────────┘ ``` -### Why Lazy Initialization Matters - -``` - iOS Audio Session Problem & Solution - - ═══════════════════ WITHOUT LAZY INIT (Problem) ═══════════════════ +### Why the ADM Stays Idle Instead of Lazily Created - App Startup - ──────────── - │ - ▼ - ┌─────────────────────────────────────────────────────────────────┐ - │ PeerConnectionFactory created │ - │ └─▶ AdmProxy created │ - │ └─▶ Platform ADM created immediately │ - │ └─▶ iOS: AVAudioSession configured for VoIP mode │ - │ └─▶ Audio session category = PlayAndRecord │ - │ └─▶ Other audio (Unity AudioSource) INTERRUPTED │ - └─────────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────────────────────────────┐ - │ Unity app tries to play audio via AudioSource │ - │ ❌ FAILS - AVAudioSession is in VoIP mode! │ - └─────────────────────────────────────────────────────────────────┘ +Creating the Platform ADM does not start audio I/O. The instance registers +its observers (iOS audio session KVO, macOS device listeners) at +construction, but the microphone and speakers are only touched once +platform mode is entered (ref_count > 0 with the gates enabled). So in +synthetic mode application audio (e.g. Unity AudioSource) is not +interfered with even though the ADM object exists. +The instance is also kept alive when the last `PlatformAudio` is dropped: +release only stops playout/recording and switches back to synthetic mode. +Repeatedly destroying and re-creating the ADM caused iOS KVO race +conditions, so a single instance persists for the proxy's lifetime. - ════════════════════ WITH LAZY INIT (Solution) ════════════════════ +Android is the one platform where creation is deferred to the first +acquire, because the ADM needs JNI (application context) which may not be +initialized when the PeerConnectionFactory is constructed. +``` App Startup ──────────── │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ PeerConnectionFactory created │ - │ └─▶ AdmProxy created │ - │ └─▶ platform_adm_ = NULL (NOT created!) │ - │ └─▶ iOS: AVAudioSession NOT configured │ - │ └─▶ Other audio works normally! │ - └─────────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────────────────────────────┐ - │ Unity app plays audio via AudioSource │ - │ ✅ WORKS - using synthetic mode, no ADM interference │ + │ └─▶ AdmProxy created (on the worker thread) │ + │ └─▶ Platform ADM created + Init, but NOT started │ + │ └─▶ no mic/speaker I/O, app audio works normally │ └─────────────────────────────────────────────────────────────────┘ │ ▼ (Later, if needed) ┌─────────────────────────────────────────────────────────────────┐ │ PlatformAudio::new() called for VoIP │ - │ └─▶ acquire_platform_adm() │ - │ └─▶ CreatePlatformAdm() - NOW creates Platform ADM │ - │ └─▶ iOS: AVAudioSession configured for VoIP │ + │ └─▶ acquire_platform_adm() + enable gates │ + │ └─▶ playout/recording switch to the Platform ADM │ │ │ │ drop(PlatformAudio) │ │ └─▶ release_platform_adm() │ - │ └─▶ TerminatePlatformAdm() - destroys Platform ADM │ - │ └─▶ iOS: AVAudioSession released │ - │ └─▶ Unity audio can work again! │ + │ └─▶ Platform ADM stops, synthetic mode resumes │ + │ (instance stays alive until the proxy is destroyed) │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -1303,13 +1295,14 @@ FFI responses include optional error messages: // webrtc-sys/include/livekit/adm_proxy.h class AdmProxy : public webrtc::AudioDeviceModule { public: + // Must be constructed on the worker thread explicit AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread); ~AdmProxy() override; // Platform ADM Lifecycle Management - bool AcquirePlatformAdm(); // Increment ref, create ADM on first call - void ReleasePlatformAdm(); // Decrement ref, terminate ADM when 0 + bool AcquirePlatformAdm(); // Increment ref, switch to platform mode on 0 -> 1 + void ReleasePlatformAdm(); // Decrement ref, back to synthetic mode on 1 -> 0 int platform_adm_ref_count() const; bool is_platform_adm_active() const; @@ -1322,68 +1315,73 @@ class AdmProxy : public webrtc::AudioDeviceModule { // All AudioDeviceModule methods with gated behavior private: - bool CreatePlatformAdm(); // Called by AcquirePlatformAdm - void TerminatePlatformAdm(); // Called by ReleasePlatformAdm + // Runs fn on the worker thread, inline when already on it + template + auto RunOnWorker(Fn&& fn) const; - void StartSyntheticPlayoutTask(); // Keep WebRTC alive in synthetic mode - void StopSyntheticPlayoutTask(); + // Forwards to the platform ADM on the worker, or returns default_value + template + R WithPlatformAdm(R default_value, Fn&& fn) const; - const webrtc::Environment env_; - webrtc::Thread* worker_thread_; + void SwitchPlayoutMode() RTC_RUN_ON(worker_thread_); + void SwitchRecordingAdm() RTC_RUN_ON(worker_thread_); - mutable webrtc::Mutex mutex_; + const webrtc::Environment env_; + webrtc::Thread* const worker_thread_; - // Platform ADM (created lazily, NOT at startup) - webrtc::scoped_refptr platform_adm_; - int platform_adm_ref_count_ = 0; + // All mutable state is owned by the worker thread, no mutex + webrtc::scoped_refptr synthetic_adm_ + RTC_GUARDED_BY(worker_thread_); + webrtc::scoped_refptr platform_adm_ + RTC_GUARDED_BY(worker_thread_); + int platform_adm_ref_count_ RTC_GUARDED_BY(worker_thread_) = 0; // Control flags - bool recording_enabled_ = false; // Default: NativeAudioSource mode - bool playout_enabled_ = false; // Default: synthetic mode (FFI callbacks) - - // Synthetic playout task - std::unique_ptr stub_audio_queue_; - webrtc::RepeatingTaskHandle stub_audio_task_; - std::vector stub_data_; + bool recording_enabled_ RTC_GUARDED_BY(worker_thread_) = false; + bool playout_enabled_ RTC_GUARDED_BY(worker_thread_) = false; }; ``` -### Lazy Initialization Implementation +### Lifecycle Implementation + +Both sub ADMs are created and initialized in the constructor, which runs on +the worker thread (Android creates the platform ADM lazily on first acquire +instead, since JNI may not be ready at construction time). Acquire and +release just move the ref count and switch modes: ```cpp // webrtc-sys/src/adm_proxy.cpp -AdmProxy::AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread) - : env_(env), worker_thread_(worker_thread), stub_data_(kSamplesPer10Ms * kChannels) { - // Platform ADM is NOT created here - lazy initialization - RTC_LOG(LS_INFO) << "AdmProxy: Lazy initialization mode (no Platform ADM yet)"; -} - bool AdmProxy::AcquirePlatformAdm() { - webrtc::MutexLock lock(&mutex_); - platform_adm_ref_count_++; - - if (platform_adm_ref_count_ == 1) { - // First acquisition - create Platform ADM - if (!CreatePlatformAdm()) { - platform_adm_ref_count_--; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (!platform_adm_) { return false; } - RTC_LOG(LS_INFO) << "AdmProxy: Platform ADM created and initialized"; - } - return platform_adm_ != nullptr; + int old_ref_count = platform_adm_ref_count_; + platform_adm_ref_count_++; + if (old_ref_count == 0) { + SwitchPlayoutMode(); + SwitchRecordingAdm(); + } + return true; + }); } void AdmProxy::ReleasePlatformAdm() { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_ref_count_ <= 0) return; - - platform_adm_ref_count_--; - if (platform_adm_ref_count_ == 0) { - // Last release - terminate Platform ADM - TerminatePlatformAdm(); - RTC_LOG(LS_INFO) << "AdmProxy: Platform ADM terminated, returning to synthetic mode"; - } + RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_ref_count_ <= 0) { + return; + } + platform_adm_ref_count_--; + // The Platform ADM is NOT terminated here, it stays alive until the + // destructor to avoid iOS KVO races from re-creating it + if (platform_adm_ref_count_ == 0) { + SwitchPlayoutMode(); + SwitchRecordingAdm(); + } + }); } ``` @@ -1391,23 +1389,25 @@ void AdmProxy::ReleasePlatformAdm() { ```cpp int32_t AdmProxy::InitRecording() { - webrtc::MutexLock lock(&mutex_); - if (!recording_enabled_ || !platform_adm_) { - recording_initialized_ = true; // Track state even in synthetic mode - return 0; // Success but no-op - } - return platform_adm_->InitRecording(); + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + auto* adm = RecordingAdm(); // null unless ref_count > 0 && recording_enabled_ + if (!adm) { + return 0; // Success but no-op, keeps WebRTC's init flow happy + } + return adm->InitRecording(); + }); } int32_t AdmProxy::StartPlayout() { - webrtc::MutexLock lock(&mutex_); - if (!playout_enabled_ || !platform_adm_) { - // Synthetic mode - start stub task to keep WebRTC pipeline alive + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); playing_ = true; - StartSyntheticPlayoutTask(); - return 0; - } - return platform_adm_->StartPlayout(); + if (IsPlatformPlayoutActive()) { + return platform_adm_->StartPlayout(); + } + return synthetic_adm_ ? synthetic_adm_->StartPlayout() : -1; + }); } ``` diff --git a/livekit/examples/platform_audio.rs b/livekit/examples/platform_audio.rs new file mode 100644 index 000000000..461ca4ac5 --- /dev/null +++ b/livekit/examples/platform_audio.rs @@ -0,0 +1,218 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// PlatformAudio exerciser. Demonstrates the PlatformAudio API and verifies +// the AdmProxy worker-thread marshaling, all without a LiveKit server: +// +// - acquire/release lifecycle and full runtime teardown/reacquire cycles +// - device enumeration, selection, and hot-swap while recording +// - recording start/stop on real hardware +// - audio processing (AEC/AGC/NS) configuration +// - heavy concurrent access from many threads, the access pattern that +// exercises the proxy's marshaling onto the WebRTC worker thread +// +// Run: cargo run -p livekit --example platform_audio +// Success criteria: prints ALL PHASES PASSED without hang, crash, or errors. +// Requires audio hardware and microphone permission, so it is an example +// rather than a CI test. + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +use livekit::{AudioProcessingOptions, PlatformAudio}; +use log::{Level, LevelFilter, Metadata, Record}; + +// Counts LkRuntime teardowns so the test can assert that dropping the last +// PlatformAudio really destroys the factory and the AdmProxy with it +static RUNTIME_DROPS: AtomicUsize = AtomicUsize::new(0); + +struct StdoutLogger; + +impl log::Log for StdoutLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Debug + } + fn log(&self, record: &Record) { + let msg = record.args().to_string(); + if msg.contains("LkRuntime::drop") { + RUNTIME_DROPS.fetch_add(1, Ordering::Relaxed); + } + if record.level() <= Level::Info { + println!("[{}] {}", record.level(), msg); + } + } + fn flush(&self) {} +} + +static LOGGER: StdoutLogger = StdoutLogger; + +fn main() { + log::set_logger(&LOGGER).unwrap(); + log::set_max_level(LevelFilter::Debug); + + println!("=== Phase 1: basic lifecycle ==="); + let audio = PlatformAudio::new().expect("PlatformAudio::new() failed"); + println!("ref_count after new: {}", audio.ref_count()); + + let rec_devices: Vec<_> = audio.recording_devices().collect(); + let play_devices: Vec<_> = audio.playout_devices().collect(); + println!("recording devices ({}):", rec_devices.len()); + for d in &rec_devices { + println!(" [{}] {} ({})", d.index, d.name, d.id.as_str()); + } + println!("playout devices ({}):", play_devices.len()); + for d in &play_devices { + println!(" [{}] {} ({})", d.index, d.name, d.id.as_str()); + } + assert!(!rec_devices.is_empty(), "no recording devices found"); + assert!(!play_devices.is_empty(), "no playout devices found"); + + println!( + "hardware aec/agc/ns available: {}/{}/{}", + audio.is_hardware_aec_available(), + audio.is_hardware_agc_available(), + audio.is_hardware_ns_available() + ); + println!( + "active aec/agc/ns type: {:?}/{:?}/{:?}", + audio.active_aec_type(), + audio.active_agc_type(), + audio.active_ns_type() + ); + + println!("=== Phase 2: device selection + recording ==="); + audio.set_recording_device(&rec_devices[0].id).expect("set_recording_device"); + audio.set_playout_device(&play_devices[0].id).expect("set_playout_device"); + // Exercises the by-guid lookup plus the stop/init/start dance (the + // start branch only runs when playout is live, which needs a room) + audio.switch_playout_device(&play_devices[0].id).expect("switch_playout_device"); + + match audio.start_recording() { + Ok(()) => { + println!("recording started (initialized: {})", audio.is_recording_initialized()); + thread::sleep(Duration::from_millis(300)); + // Hot-swap while recording if a second device exists + if rec_devices.len() > 1 { + println!("switching recording device while recording..."); + audio.switch_recording_device(&rec_devices[1].id).expect("switch_recording_device"); + thread::sleep(Duration::from_millis(300)); + audio.switch_recording_device(&rec_devices[0].id).expect("switch back"); + } + audio.stop_recording().expect("stop_recording"); + println!("recording stopped"); + } + Err(e) => { + // Mic permission may be denied for the terminal, still a valid + // control-plane exercise, but report it + println!("start_recording failed (mic permission?): {e}"); + } + } + + println!("=== Phase 3: audio processing reconfiguration ==="); + for prefer_hw in [true, false, true] { + audio + .configure_audio_processing(AudioProcessingOptions { + echo_cancellation: true, + noise_suppression: true, + auto_gain_control: true, + prefer_hardware_processing: prefer_hw, + }) + .expect("configure_audio_processing"); + } + audio.set_echo_cancellation(false, false).expect("set_echo_cancellation"); + audio.set_echo_cancellation(true, true).expect("set_echo_cancellation"); + + println!("=== Phase 4: concurrent hammering (16 threads x 50 iterations) ==="); + let errors = Arc::new(AtomicUsize::new(0)); + let start = Instant::now(); + let mut handles = Vec::new(); + for t in 0..16usize { + let audio = audio.clone(); + let errors = errors.clone(); + handles.push(thread::spawn(move || { + for i in 0..50usize { + match t % 4 { + 0 => { + // enumerate + let n = audio.recording_devices().count(); + if n == 0 { + errors.fetch_add(1, Ordering::Relaxed); + } + let _ = audio.playout_devices().count(); + } + 1 => { + // getters + let _ = audio.ref_count(); + let _ = audio.is_recording_initialized(); + let _ = audio.is_hardware_aec_available(); + let _ = audio.active_aec_type(); + } + 2 => { + // recording start/stop churn + let _ = audio.start_recording(); + let _ = audio.stop_recording(); + } + _ => { + // acquire/release churn: extra instances created and + // dropped concurrently, forcing mode switches + if let Ok(extra) = PlatformAudio::new() { + let _ = extra.ref_count(); + drop(extra); + } else { + errors.fetch_add(1, Ordering::Relaxed); + } + } + } + if i % 25 == 0 { + thread::sleep(Duration::from_millis(1)); + } + } + })); + } + for h in handles { + h.join().expect("worker thread panicked"); + } + println!( + "concurrent phase done in {:?}, errors: {}", + start.elapsed(), + errors.load(Ordering::Relaxed) + ); + assert_eq!(errors.load(Ordering::Relaxed), 0, "errors during concurrent phase"); + + println!("=== Phase 5: full runtime teardown / reacquire churn ==="); + // With no rooms alive, dropping the last PlatformAudio destroys the whole + // LkRuntime (factory + AdmProxy), so each iteration exercises AdmProxy + // construction on the worker and destruction initiated from this thread + drop(audio); + let drops_before = RUNTIME_DROPS.load(Ordering::Relaxed); + for i in 0..5 { + let a = PlatformAudio::new().expect("reacquire"); + assert_eq!(a.ref_count(), 1, "iteration {i}: expected sole owner"); + let _ = a.start_recording(); + if i % 2 == 0 { + // Drop while recording is active: release must stop recording + // via SwitchRecordingAdm before the runtime is torn down + } else { + let _ = a.stop_recording(); + } + drop(a); + } + let teardowns = RUNTIME_DROPS.load(Ordering::Relaxed) - drops_before; + println!("full runtime teardowns in phase 5: {teardowns}"); + assert!(teardowns >= 5, "expected >= 5 LkRuntime teardowns, got {teardowns}"); + + println!("ALL PHASES PASSED"); +} diff --git a/webrtc-sys/include/livekit/adm_proxy.h b/webrtc-sys/include/livekit/adm_proxy.h index d099f916e..31555103d 100644 --- a/webrtc-sys/include/livekit/adm_proxy.h +++ b/webrtc-sys/include/livekit/adm_proxy.h @@ -16,18 +16,16 @@ #pragma once -#include +#include #include "api/environment/environment.h" #include "api/scoped_refptr.h" +#include "api/sequence_checker.h" #include "livekit/synthetic_audio_device.h" #include "modules/audio_device/include/audio_device.h" #include "modules/audio_device/include/audio_device_defines.h" -#include "rtc_base/synchronization/mutex.h" - -namespace webrtc { -class Thread; -} // namespace webrtc +#include "rtc_base/thread.h" +#include "rtc_base/thread_annotations.h" namespace livekit_ffi { @@ -41,6 +39,25 @@ namespace livekit_ffi { /// capture and speaker playout. Used when PlatformAudio is active for VoIP /// with AEC. /// +/// ## Threading Model +/// +/// The proxy is worker-thread-affine. Every public method marshals once at the +/// boundary onto the WebRTC worker thread via `RunOnWorker` and runs inline +/// when the caller is already on the worker, which is the case for all calls +/// originating from WebRTC internals (AudioState, the voice engine). All +/// mutable state, including both sub ADMs, is owned by the worker thread, so +/// no locks are needed and mode transitions execute as plain sequential code. +/// +/// This also satisfies the platform ADM threading contract: platform +/// implementations bind a sequence checker to their construction thread and +/// expect every control call on it. The proxy must therefore be constructed +/// on the worker thread, so the eagerly created platform ADM binds to it. +/// Destruction may happen on any thread, teardown hops to the worker. +/// +/// Note for future extensions: platform ADM observer callbacks must not call +/// synchronously back into AudioDeviceController, as that would block the +/// observer thread on the worker while the worker may be waiting on it. +/// /// ## Mode Selection /// /// - **Playout**: Uses Platform ADM when `ref_count > 0 && playout_enabled`, @@ -65,6 +82,7 @@ namespace livekit_ffi { /// class AdmProxy : public webrtc::AudioDeviceModule { public: + /// Must be constructed on the worker thread. explicit AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread); ~AdmProxy() override; @@ -83,8 +101,8 @@ class AdmProxy : public webrtc::AudioDeviceModule { /// Releases a reference to the Platform ADM. /// - /// When the reference count reaches zero, the Platform ADM is terminated - /// and the proxy returns to synthetic mode. + /// When the reference count reaches zero, the proxy returns to synthetic + /// mode. The Platform ADM instance stays alive until destruction. void ReleasePlatformAdm(); /// Returns the current reference count for the Platform ADM. @@ -206,72 +224,89 @@ class AdmProxy : public webrtc::AudioDeviceModule { int32_t SetObserver(webrtc::AudioDeviceObserver* observer) override; private: + // Runs fn on the worker thread, inline when already on it. + template + auto RunOnWorker(Fn&& fn) const { + if (worker_thread_->IsCurrent()) { + return fn(); + } + return worker_thread_->BlockingCall(std::forward(fn)); + } + + // Forwards a call to the platform ADM on the worker thread. + // Returns default_value when no platform ADM is available. + template + R WithPlatformAdm(R default_value, Fn&& fn) const { + return RunOnWorker([&]() -> R { + RTC_DCHECK_RUN_ON(worker_thread_); + if (!platform_adm_) { + return default_value; + } + return fn(*platform_adm_); + }); + } + // Returns true if platform mode is active for playout // (ref_count > 0 && playout_enabled) - bool is_platform_playout_active() const; + bool IsPlatformPlayoutActive() const RTC_RUN_ON(worker_thread_); - // Returns the ADM to use for recording operations - // - Platform ADM when recording is enabled (ref_count > 0 && recording_enabled) - // - nullptr otherwise (recording not available in synthetic mode) - webrtc::AudioDeviceModule* recording_adm() const; + // Returns the ADM to use for recording operations. + // Platform ADM when recording is enabled (ref_count > 0 && recording_enabled), + // nullptr otherwise (recording not available in synthetic mode). + webrtc::AudioDeviceModule* RecordingAdm() const RTC_RUN_ON(worker_thread_); - // Switches playout mode based on current state. - // Called when ref_count or playout_enabled changes. + // Switches playout between synthetic and platform mode based on current + // state. Called when ref_count or playout_enabled changes. // If playout is active, stops the old mode and starts the new one. - // Must be called with mutex_ held. - void SwitchPlayoutModeIfNeeded(); + void SwitchPlayoutMode() RTC_RUN_ON(worker_thread_); - // Switches recording to the correct ADM based on current mode. + // Switches recording to the correct ADM based on current state. // Called when ref_count or recording_enabled changes. // If recording is active, stops the old ADM and starts the new one. - // Must be called with mutex_ held. - void SwitchRecordingAdmIfNeeded(); + void SwitchRecordingAdm() RTC_RUN_ON(worker_thread_); #if defined(__ANDROID__) - // Lazily creates the Platform ADM on Android. - // Must be called with mutex_ held. + // Lazily creates and initializes the Platform ADM on Android. // Returns true if ADM is available after the call. - bool EnsurePlatformAdmCreated(); + bool EnsurePlatformAdmCreated() RTC_RUN_ON(worker_thread_); #endif const webrtc::Environment env_; - webrtc::Thread* worker_thread_; + webrtc::Thread* const worker_thread_; - // Mutex for thread-safe access to mutable state - mutable webrtc::Mutex mutex_; + // All mutable state below is owned by the worker thread. // Synthetic ADM for synthetic mode - pumps the WebRTC audio pipeline without // platform audio via SyntheticAudioDevice's internal task. - webrtc::scoped_refptr synthetic_adm_; + webrtc::scoped_refptr synthetic_adm_ + RTC_GUARDED_BY(worker_thread_); // Platform ADM for real audio I/O (microphone capture, speaker playout with AEC) - webrtc::scoped_refptr platform_adm_; + webrtc::scoped_refptr platform_adm_ + RTC_GUARDED_BY(worker_thread_); // Reference count for Platform ADM users (PlatformAudio instances) - int platform_adm_ref_count_ = 0; + int platform_adm_ref_count_ RTC_GUARDED_BY(worker_thread_) = 0; // Audio transport callback (registered by WebRTC) - webrtc::AudioTransport* audio_transport_ = nullptr; + webrtc::AudioTransport* audio_transport_ RTC_GUARDED_BY(worker_thread_) = + nullptr; // State tracking - bool playout_initialized_ = false; - bool recording_initialized_ = false; - bool playing_ = false; - bool recording_ = false; + bool playing_ RTC_GUARDED_BY(worker_thread_) = false; + bool recording_ RTC_GUARDED_BY(worker_thread_) = false; // Control flags // When false (default), recording operations are no-ops (NativeAudioSource mode) - bool recording_enabled_ = false; + bool recording_enabled_ RTC_GUARDED_BY(worker_thread_) = false; // When false (default), playout uses synthetic mode (internal task pumps audio) - bool playout_enabled_ = false; - - // Selected device information (for re-initialization after ADM restart) - // We store both index and GUID. GUID is preferred for restoration as it's - // stable across device hot-plug events. - uint16_t selected_playout_device_ = 0; - uint16_t selected_recording_device_ = 0; - std::string selected_playout_guid_; - std::string selected_recording_guid_; + bool playout_enabled_ RTC_GUARDED_BY(worker_thread_) = false; + + // Selected device indices, stored so a selection made before the Platform + // ADM exists (Android lazy creation) can be re-applied once it is created. + // Index 0 (the default device) is treated as never explicitly selected. + uint16_t selected_playout_device_ RTC_GUARDED_BY(worker_thread_) = 0; + uint16_t selected_recording_device_ RTC_GUARDED_BY(worker_thread_) = 0; }; } // namespace livekit_ffi diff --git a/webrtc-sys/include/livekit/audio_device_controller.h b/webrtc-sys/include/livekit/audio_device_controller.h index e1e12cd1e..db68e9151 100644 --- a/webrtc-sys/include/livekit/audio_device_controller.h +++ b/webrtc-sys/include/livekit/audio_device_controller.h @@ -24,9 +24,12 @@ namespace livekit_ffi { +class RtcRuntime; + class AudioDeviceController { public: - explicit AudioDeviceController(webrtc::scoped_refptr adm_proxy); + AudioDeviceController(std::shared_ptr rtc_runtime, + webrtc::scoped_refptr adm_proxy); // Device enumeration int16_t playout_devices() const; @@ -77,6 +80,9 @@ class AudioDeviceController { bool is_platform_adm_active() const; private: + // The AdmProxy marshals its calls onto the runtime's worker thread, keep + // the runtime (and its threads) alive as long as Rust can reach the proxy + std::shared_ptr rtc_runtime_; webrtc::scoped_refptr adm_proxy_; }; diff --git a/webrtc-sys/src/adm_proxy.cpp b/webrtc-sys/src/adm_proxy.cpp index 209f39d53..16b8ff5cf 100644 --- a/webrtc-sys/src/adm_proxy.cpp +++ b/webrtc-sys/src/adm_proxy.cpp @@ -19,8 +19,8 @@ #include "api/audio/audio_device.h" #include "api/audio/create_audio_device_module.h" #include "api/make_ref_counted.h" +#include "rtc_base/checks.h" #include "rtc_base/logging.h" -#include "rtc_base/thread.h" #if defined(__ANDROID__) #include @@ -33,6 +33,11 @@ namespace livekit_ffi { AdmProxy::AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread) : env_(env), worker_thread_(worker_thread) { + RTC_DCHECK(worker_thread_); + // The proxy must be constructed on the worker thread so the platform ADM + // created below binds its sequence checker to it. + RTC_DCHECK_RUN_ON(worker_thread_); + // Create the synthetic ADM for synthetic mode. SyntheticAudioDevice pumps // the WebRTC audio pipeline without platform audio, allowing FFI callbacks // to receive decoded remote audio. @@ -71,28 +76,32 @@ AdmProxy::AdmProxy(const webrtc::Environment& env, webrtc::Thread* worker_thread AdmProxy::~AdmProxy() { RTC_LOG(LS_VERBOSE) << "AdmProxy::~AdmProxy()"; - if (synthetic_adm_) { - synthetic_adm_->Terminate(); - synthetic_adm_ = nullptr; - } - - if (platform_adm_) { - platform_adm_->Terminate(); - platform_adm_ = nullptr; - } + // The last reference may be dropped on any thread. Tear down on the worker + // so the sub ADMs are terminated and destroyed on their owning thread. + RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (synthetic_adm_) { + synthetic_adm_->Terminate(); + synthetic_adm_ = nullptr; + } + if (platform_adm_) { + platform_adm_->Terminate(); + platform_adm_ = nullptr; + } + }); } // ============================================================================= -// Helper Methods +// Helper Methods (worker thread only) // ============================================================================= -bool AdmProxy::is_platform_playout_active() const { +bool AdmProxy::IsPlatformPlayoutActive() const { // Platform playout is active when: ref_count > 0 AND playout explicitly enabled. // Otherwise, synthetic mode handles playout via the internal pumping task. return platform_adm_ && platform_adm_ref_count_ > 0 && playout_enabled_; } -webrtc::AudioDeviceModule* AdmProxy::recording_adm() const { +webrtc::AudioDeviceModule* AdmProxy::RecordingAdm() const { // Recording only available through platform ADM when enabled. // Synthetic mode doesn't support recording (no microphone). if (platform_adm_ && platform_adm_ref_count_ > 0 && recording_enabled_) { @@ -106,8 +115,6 @@ webrtc::AudioDeviceModule* AdmProxy::recording_adm() const { // ============================================================================= #if defined(__ANDROID__) -// Lazily creates the Platform ADM on Android. Must be called with mutex held. -// Returns true if ADM is available (either already existed or successfully created). bool AdmProxy::EnsurePlatformAdmCreated() { if (platform_adm_) { return true; // Already created @@ -131,69 +138,90 @@ bool AdmProxy::EnsurePlatformAdmCreated() { return false; } + // WebRTC registered its audio transport before the ADM existed, + // pass it along so recorded audio actually reaches the pipeline + if (audio_transport_) { + platform_adm_->RegisterAudioCallback(audio_transport_); + } + + // Re-apply any device selection made before the ADM existed + if (selected_playout_device_ != 0) { + platform_adm_->SetPlayoutDevice(selected_playout_device_); + } + if (selected_recording_device_ != 0) { + platform_adm_->SetRecordingDevice(selected_recording_device_); + } + return true; } #endif bool AdmProxy::AcquirePlatformAdm() { - webrtc::MutexLock lock(&mutex_); + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); #if defined(__ANDROID__) - // On Android, lazily create the Platform ADM on first acquire. - // This ensures JNI is fully initialized before we try to create the ADM. - if (!EnsurePlatformAdmCreated()) { - RTC_LOG(LS_ERROR) << "AdmProxy::AcquirePlatformAdm() - Failed to create Platform ADM"; - return false; - } + // On Android, lazily create the Platform ADM on first acquire. + // This ensures JNI is fully initialized before we try to create the ADM. + if (!EnsurePlatformAdmCreated()) { + RTC_LOG(LS_ERROR) << "AdmProxy::AcquirePlatformAdm() - Failed to create Platform ADM"; + return false; + } #else - if (!platform_adm_) { - RTC_LOG(LS_ERROR) << "AdmProxy::AcquirePlatformAdm() - Platform ADM not available"; - return false; - } + if (!platform_adm_) { + RTC_LOG(LS_ERROR) << "AdmProxy::AcquirePlatformAdm() - Platform ADM not available"; + return false; + } #endif - int old_ref_count = platform_adm_ref_count_; - platform_adm_ref_count_++; + int old_ref_count = platform_adm_ref_count_; + platform_adm_ref_count_++; - // If this is the first acquisition and playout/recording is enabled, - // we may need to switch from synthetic mode to platform ADM - if (old_ref_count == 0) { - SwitchPlayoutModeIfNeeded(); - SwitchRecordingAdmIfNeeded(); - } + // If this is the first acquisition and playout/recording is enabled, + // we may need to switch from synthetic mode to platform ADM + if (old_ref_count == 0) { + SwitchPlayoutMode(); + SwitchRecordingAdm(); + } - return true; + return true; + }); } void AdmProxy::ReleasePlatformAdm() { - webrtc::MutexLock lock(&mutex_); + RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); - if (platform_adm_ref_count_ <= 0) { - RTC_LOG(LS_WARNING) << "AdmProxy::ReleasePlatformAdm() called with ref_count=" - << platform_adm_ref_count_; - return; - } + if (platform_adm_ref_count_ <= 0) { + RTC_LOG(LS_WARNING) << "AdmProxy::ReleasePlatformAdm() called with ref_count=" + << platform_adm_ref_count_; + return; + } - platform_adm_ref_count_--; + platform_adm_ref_count_--; - // If ref_count reaches 0, switch back from platform ADM to synthetic mode - // 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) { - SwitchPlayoutModeIfNeeded(); - SwitchRecordingAdmIfNeeded(); - } + // If ref_count reaches 0, switch back from platform ADM to synthetic mode + // 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) { + SwitchPlayoutMode(); + SwitchRecordingAdm(); + } + }); } int AdmProxy::platform_adm_ref_count() const { - webrtc::MutexLock lock(&mutex_); - return platform_adm_ref_count_; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + return platform_adm_ref_count_; + }); } bool AdmProxy::is_platform_adm_active() const { - webrtc::MutexLock lock(&mutex_); - // Platform ADM is considered active when there are users and playout/recording is enabled - return platform_adm_ != nullptr && platform_adm_ref_count_ > 0; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + return platform_adm_ != nullptr && platform_adm_ref_count_ > 0; + }); } // ============================================================================= @@ -201,43 +229,49 @@ bool AdmProxy::is_platform_adm_active() const { // ============================================================================= void AdmProxy::set_recording_enabled(bool enabled) { - webrtc::MutexLock lock(&mutex_); - if (recording_enabled_ == enabled) { - return; - } - recording_enabled_ = enabled; - SwitchRecordingAdmIfNeeded(); + RunOnWorker([this, enabled] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (recording_enabled_ == enabled) { + return; + } + recording_enabled_ = enabled; + SwitchRecordingAdm(); + }); } bool AdmProxy::recording_enabled() const { - webrtc::MutexLock lock(&mutex_); - return recording_enabled_; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + return recording_enabled_; + }); } void AdmProxy::set_playout_enabled(bool enabled) { - webrtc::MutexLock lock(&mutex_); - if (playout_enabled_ == enabled) { - return; - } - playout_enabled_ = enabled; - SwitchPlayoutModeIfNeeded(); + RunOnWorker([this, enabled] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (playout_enabled_ == enabled) { + return; + } + playout_enabled_ = enabled; + SwitchPlayoutMode(); + }); } bool AdmProxy::playout_enabled() const { - webrtc::MutexLock lock(&mutex_); - return playout_enabled_; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + return playout_enabled_; + }); } // ============================================================================= -// Mode Switching Helpers (called with mutex held) +// Mode Switching Helpers (worker thread only) // ============================================================================= -void AdmProxy::SwitchPlayoutModeIfNeeded() { +void AdmProxy::SwitchPlayoutMode() { if (!playing_) return; - bool use_platform = is_platform_playout_active(); - - if (use_platform) { + if (IsPlatformPlayoutActive()) { // Switch to platform mode - stop synthetic, start platform ADM if (synthetic_adm_) { synthetic_adm_->StopPlayout(); @@ -257,14 +291,14 @@ void AdmProxy::SwitchPlayoutModeIfNeeded() { } } -void AdmProxy::SwitchRecordingAdmIfNeeded() { +void AdmProxy::SwitchRecordingAdm() { if (!recording_) return; // Stop platform ADM recording (only one that supports recording) if (platform_adm_) platform_adm_->StopRecording(); // Start if new ADM supports recording - auto* adm = recording_adm(); + auto* adm = RecordingAdm(); if (adm) { adm->InitRecording(); adm->StartRecording(); @@ -278,599 +312,548 @@ void AdmProxy::SwitchRecordingAdmIfNeeded() { // ============================================================================= int32_t AdmProxy::ActiveAudioLayer(AudioLayer* audioLayer) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->ActiveAudioLayer(audioLayer); - } - *audioLayer = AudioLayer::kDummyAudio; - return 0; + return RunOnWorker([this, audioLayer] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->ActiveAudioLayer(audioLayer); + } + *audioLayer = AudioLayer::kDummyAudio; + return 0; + }); } int32_t AdmProxy::RegisterAudioCallback(webrtc::AudioTransport* transport) { - webrtc::MutexLock lock(&mutex_); - audio_transport_ = transport; + return RunOnWorker([this, transport] { + RTC_DCHECK_RUN_ON(worker_thread_); + audio_transport_ = transport; - // Register with both ADMs so they're ready when we switch modes - if (synthetic_adm_) { - synthetic_adm_->RegisterAudioCallback(transport); - } - if (platform_adm_) { - platform_adm_->RegisterAudioCallback(transport); - } - return 0; + // Register with both ADMs so they're ready when we switch modes + if (synthetic_adm_) { + synthetic_adm_->RegisterAudioCallback(transport); + } + if (platform_adm_) { + platform_adm_->RegisterAudioCallback(transport); + } + return 0; + }); } int32_t AdmProxy::Init() { - // Init is a no-op - Platform ADM is created lazily via AcquirePlatformAdm() + // Init is a no-op - the sub ADMs are initialized at creation time return 0; } int32_t AdmProxy::Terminate() { - webrtc::MutexLock lock(&mutex_); - - int32_t result = 0; - if (synthetic_adm_) { - result = synthetic_adm_->Terminate(); - } - if (platform_adm_) { - int32_t platform_result = platform_adm_->Terminate(); - if (result == 0) result = platform_result; - } - return result; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + int32_t result = 0; + if (synthetic_adm_) { + result = synthetic_adm_->Terminate(); + } + if (platform_adm_) { + int32_t platform_result = platform_adm_->Terminate(); + if (result == 0) result = platform_result; + } + return result; + }); } bool AdmProxy::Initialized() const { - webrtc::MutexLock lock(&mutex_); - // We're initialized if at least one ADM is initialized - bool synthetic_init = synthetic_adm_ && synthetic_adm_->Initialized(); - bool platform_init = platform_adm_ && platform_adm_->Initialized(); - return synthetic_init || platform_init; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + // We're initialized if at least one ADM is initialized + bool synthetic_init = synthetic_adm_ && synthetic_adm_->Initialized(); + bool platform_init = platform_adm_ && platform_adm_->Initialized(); + return synthetic_init || platform_init; + }); } int16_t AdmProxy::PlayoutDevices() { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->PlayoutDevices(); - } - // In synthetic mode, return 0 devices (no platform audio) - return 0; + // In synthetic mode, there are no platform devices + return WithPlatformAdm(0, [](webrtc::AudioDeviceModule& adm) { + return adm.PlayoutDevices(); + }); } int16_t AdmProxy::RecordingDevices() { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->RecordingDevices(); - } - // In synthetic mode, return 0 devices (no platform audio) - return 0; + // In synthetic mode, there are no platform devices + return WithPlatformAdm(0, [](webrtc::AudioDeviceModule& adm) { + return adm.RecordingDevices(); + }); } int32_t AdmProxy::PlayoutDeviceName(uint16_t index, char name[webrtc::kAdmMaxDeviceNameSize], char guid[webrtc::kAdmMaxGuidSize]) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->PlayoutDeviceName(index, name, guid); - } - return -1; + return WithPlatformAdm(-1, [&](webrtc::AudioDeviceModule& adm) { + return adm.PlayoutDeviceName(index, name, guid); + }); } int32_t AdmProxy::RecordingDeviceName(uint16_t index, char name[webrtc::kAdmMaxDeviceNameSize], char guid[webrtc::kAdmMaxGuidSize]) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->RecordingDeviceName(index, name, guid); - } - return -1; + return WithPlatformAdm(-1, [&](webrtc::AudioDeviceModule& adm) { + return adm.RecordingDeviceName(index, name, guid); + }); } int32_t AdmProxy::SetPlayoutDevice(uint16_t index) { - webrtc::MutexLock lock(&mutex_); - selected_playout_device_ = index; - - // Also store the GUID for this device for robust restoration - if (platform_adm_) { - char name[webrtc::kAdmMaxDeviceNameSize] = {0}; - char guid[webrtc::kAdmMaxGuidSize] = {0}; - if (platform_adm_->PlayoutDeviceName(index, name, guid) == 0) { - selected_playout_guid_ = guid; + return RunOnWorker([this, index] { + RTC_DCHECK_RUN_ON(worker_thread_); + selected_playout_device_ = index; + if (platform_adm_) { + return platform_adm_->SetPlayoutDevice(index); } - return platform_adm_->SetPlayoutDevice(index); - } - return 0; + return 0; + }); } int32_t AdmProxy::SetPlayoutDevice(WindowsDeviceType device) { - webrtc::MutexLock lock(&mutex_); - // Note: When using WindowsDeviceType, we can't easily get the GUID - // The GUID will be populated on next CreatePlatformAdm if needed - selected_playout_guid_.clear(); - if (platform_adm_) { - return platform_adm_->SetPlayoutDevice(device); - } - return 0; + return RunOnWorker([this, device] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->SetPlayoutDevice(device); + } + return 0; + }); } int32_t AdmProxy::SetRecordingDevice(uint16_t index) { - webrtc::MutexLock lock(&mutex_); - selected_recording_device_ = index; - - // Also store the GUID for this device for robust restoration - if (platform_adm_) { - char name[webrtc::kAdmMaxDeviceNameSize] = {0}; - char guid[webrtc::kAdmMaxGuidSize] = {0}; - if (platform_adm_->RecordingDeviceName(index, name, guid) == 0) { - selected_recording_guid_ = guid; + return RunOnWorker([this, index] { + RTC_DCHECK_RUN_ON(worker_thread_); + selected_recording_device_ = index; + if (platform_adm_) { + return platform_adm_->SetRecordingDevice(index); } - return platform_adm_->SetRecordingDevice(index); - } - return 0; + return 0; + }); } int32_t AdmProxy::SetRecordingDevice(WindowsDeviceType device) { - webrtc::MutexLock lock(&mutex_); - // Note: When using WindowsDeviceType, we can't easily get the GUID - // The GUID will be populated on next CreatePlatformAdm if needed - selected_recording_guid_.clear(); - if (platform_adm_) { - return platform_adm_->SetRecordingDevice(device); - } - return 0; + return RunOnWorker([this, device] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->SetRecordingDevice(device); + } + return 0; + }); } int32_t AdmProxy::PlayoutIsAvailable(bool* available) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->PlayoutIsAvailable(available); - } - *available = true; // Synthetic playout is always available - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->PlayoutIsAvailable(available); + } + *available = true; // Synthetic playout is always available + return 0; + }); } int32_t AdmProxy::InitPlayout() { - webrtc::MutexLock lock(&mutex_); - - if (is_platform_playout_active()) { - if (platform_adm_) { - int32_t result = platform_adm_->InitPlayout(); - if (result == 0) { - playout_initialized_ = true; - } - return result; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (IsPlatformPlayoutActive()) { + return platform_adm_->InitPlayout(); } - return -1; - } - - // Synthetic mode - if (synthetic_adm_) { - int32_t result = synthetic_adm_->InitPlayout(); - if (result == 0) { - playout_initialized_ = true; + // Synthetic mode + if (synthetic_adm_) { + return synthetic_adm_->InitPlayout(); } - return result; - } - return -1; + return -1; + }); } bool AdmProxy::PlayoutIsInitialized() const { - webrtc::MutexLock lock(&mutex_); - if (is_platform_playout_active()) { - return platform_adm_ && platform_adm_->PlayoutIsInitialized(); - } - // Synthetic mode - return synthetic_adm_ && synthetic_adm_->PlayoutIsInitialized(); + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (IsPlatformPlayoutActive()) { + return platform_adm_->PlayoutIsInitialized(); + } + // Synthetic mode + return synthetic_adm_ && synthetic_adm_->PlayoutIsInitialized(); + }); } int32_t AdmProxy::RecordingIsAvailable(bool* available) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->RecordingIsAvailable(available); - } - *available = false; // Recording not available in synthetic mode - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->RecordingIsAvailable(available); + } + *available = false; // Recording not available in synthetic mode + return 0; + }); } int32_t AdmProxy::InitRecording() { - webrtc::MutexLock lock(&mutex_); - - auto* adm = recording_adm(); - if (!adm) { - // Recording not available (no platform ADM or recording disabled) - // Return success to avoid breaking WebRTC's initialization flow - return 0; - } - - int32_t result = adm->InitRecording(); - if (result == 0) { - recording_initialized_ = true; - } - return result; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + auto* adm = RecordingAdm(); + if (!adm) { + // Recording not available (no platform ADM or recording disabled) + // Return success to avoid breaking WebRTC's initialization flow + return 0; + } + return adm->InitRecording(); + }); } bool AdmProxy::RecordingIsInitialized() const { - webrtc::MutexLock lock(&mutex_); - auto* adm = recording_adm(); - if (adm) { - return adm->RecordingIsInitialized(); - } - return false; // Recording not available + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + auto* adm = RecordingAdm(); + if (adm) { + return adm->RecordingIsInitialized(); + } + return false; // Recording not available + }); } int32_t AdmProxy::StartPlayout() { - webrtc::MutexLock lock(&mutex_); - playing_ = true; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + playing_ = true; - if (is_platform_playout_active()) { - if (platform_adm_) { + if (IsPlatformPlayoutActive()) { return platform_adm_->StartPlayout(); } + // Synthetic mode + if (synthetic_adm_) { + return synthetic_adm_->StartPlayout(); + } return -1; - } - - // Synthetic mode - if (synthetic_adm_) { - return synthetic_adm_->StartPlayout(); - } - return -1; + }); } int32_t AdmProxy::StopPlayout() { - webrtc::MutexLock lock(&mutex_); - playing_ = false; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + playing_ = false; - // Stop both ADMs - if (synthetic_adm_) { - synthetic_adm_->StopPlayout(); - } - if (platform_adm_) { - platform_adm_->StopPlayout(); - } - return 0; + // Stop both ADMs + if (synthetic_adm_) { + synthetic_adm_->StopPlayout(); + } + if (platform_adm_) { + platform_adm_->StopPlayout(); + } + return 0; + }); } bool AdmProxy::Playing() const { - webrtc::MutexLock lock(&mutex_); - if (is_platform_playout_active()) { - return platform_adm_ && platform_adm_->Playing(); - } - return synthetic_adm_ && synthetic_adm_->Playing(); + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (IsPlatformPlayoutActive()) { + return platform_adm_->Playing(); + } + return synthetic_adm_ && synthetic_adm_->Playing(); + }); } int32_t AdmProxy::StartRecording() { - webrtc::MutexLock lock(&mutex_); - - auto* adm = recording_adm(); - if (!adm) { - // Recording not available - return success to avoid breaking WebRTC - return 0; - } - - recording_ = true; - return adm->StartRecording(); + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + auto* adm = RecordingAdm(); + if (!adm) { + // Recording not available - return success to avoid breaking WebRTC + return 0; + } + recording_ = true; + return adm->StartRecording(); + }); } int32_t AdmProxy::StopRecording() { - webrtc::MutexLock lock(&mutex_); - recording_ = false; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + recording_ = false; - auto* adm = recording_adm(); - if (adm) { - return adm->StopRecording(); - } - return 0; + auto* adm = RecordingAdm(); + if (adm) { + return adm->StopRecording(); + } + return 0; + }); } bool AdmProxy::Recording() const { - webrtc::MutexLock lock(&mutex_); - auto* adm = recording_adm(); - if (adm) { - return adm->Recording(); - } - return false; + return RunOnWorker([this] { + RTC_DCHECK_RUN_ON(worker_thread_); + auto* adm = RecordingAdm(); + if (adm) { + return adm->Recording(); + } + return false; + }); } int32_t AdmProxy::InitSpeaker() { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->InitSpeaker(); - } - return 0; + return WithPlatformAdm(0, [](webrtc::AudioDeviceModule& adm) { + return adm.InitSpeaker(); + }); } bool AdmProxy::SpeakerIsInitialized() const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SpeakerIsInitialized(); - } - return true; + return WithPlatformAdm(true, [](webrtc::AudioDeviceModule& adm) { + return adm.SpeakerIsInitialized(); + }); } int32_t AdmProxy::InitMicrophone() { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->InitMicrophone(); - } - return 0; + return WithPlatformAdm(0, [](webrtc::AudioDeviceModule& adm) { + return adm.InitMicrophone(); + }); } bool AdmProxy::MicrophoneIsInitialized() const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MicrophoneIsInitialized(); - } - return false; + return WithPlatformAdm(false, [](webrtc::AudioDeviceModule& adm) { + return adm.MicrophoneIsInitialized(); + }); } int32_t AdmProxy::SpeakerVolumeIsAvailable(bool* available) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SpeakerVolumeIsAvailable(available); - } - *available = false; - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->SpeakerVolumeIsAvailable(available); + } + *available = false; + return 0; + }); } int32_t AdmProxy::SetSpeakerVolume(uint32_t volume) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetSpeakerVolume(volume); - } - return -1; + return WithPlatformAdm(-1, [volume](webrtc::AudioDeviceModule& adm) { + return adm.SetSpeakerVolume(volume); + }); } int32_t AdmProxy::SpeakerVolume(uint32_t* volume) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SpeakerVolume(volume); - } - return -1; + return WithPlatformAdm(-1, [volume](webrtc::AudioDeviceModule& adm) { + return adm.SpeakerVolume(volume); + }); } int32_t AdmProxy::MaxSpeakerVolume(uint32_t* maxVolume) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MaxSpeakerVolume(maxVolume); - } - return -1; + return WithPlatformAdm(-1, [maxVolume](webrtc::AudioDeviceModule& adm) { + return adm.MaxSpeakerVolume(maxVolume); + }); } int32_t AdmProxy::MinSpeakerVolume(uint32_t* minVolume) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MinSpeakerVolume(minVolume); - } - return -1; + return WithPlatformAdm(-1, [minVolume](webrtc::AudioDeviceModule& adm) { + return adm.MinSpeakerVolume(minVolume); + }); } int32_t AdmProxy::MicrophoneVolumeIsAvailable(bool* available) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MicrophoneVolumeIsAvailable(available); - } - *available = false; - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->MicrophoneVolumeIsAvailable(available); + } + *available = false; + return 0; + }); } int32_t AdmProxy::SetMicrophoneVolume(uint32_t volume) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetMicrophoneVolume(volume); - } - return -1; + return WithPlatformAdm(-1, [volume](webrtc::AudioDeviceModule& adm) { + return adm.SetMicrophoneVolume(volume); + }); } int32_t AdmProxy::MicrophoneVolume(uint32_t* volume) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MicrophoneVolume(volume); - } - return -1; + return WithPlatformAdm(-1, [volume](webrtc::AudioDeviceModule& adm) { + return adm.MicrophoneVolume(volume); + }); } int32_t AdmProxy::MaxMicrophoneVolume(uint32_t* maxVolume) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MaxMicrophoneVolume(maxVolume); - } - return -1; + return WithPlatformAdm(-1, [maxVolume](webrtc::AudioDeviceModule& adm) { + return adm.MaxMicrophoneVolume(maxVolume); + }); } int32_t AdmProxy::MinMicrophoneVolume(uint32_t* minVolume) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MinMicrophoneVolume(minVolume); - } - return -1; + return WithPlatformAdm(-1, [minVolume](webrtc::AudioDeviceModule& adm) { + return adm.MinMicrophoneVolume(minVolume); + }); } int32_t AdmProxy::SpeakerMuteIsAvailable(bool* available) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SpeakerMuteIsAvailable(available); - } - *available = false; - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->SpeakerMuteIsAvailable(available); + } + *available = false; + return 0; + }); } int32_t AdmProxy::SetSpeakerMute(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetSpeakerMute(enable); - } - return -1; + return WithPlatformAdm(-1, [enable](webrtc::AudioDeviceModule& adm) { + return adm.SetSpeakerMute(enable); + }); } int32_t AdmProxy::SpeakerMute(bool* enabled) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SpeakerMute(enabled); - } - return -1; + return WithPlatformAdm(-1, [enabled](webrtc::AudioDeviceModule& adm) { + return adm.SpeakerMute(enabled); + }); } int32_t AdmProxy::MicrophoneMuteIsAvailable(bool* available) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MicrophoneMuteIsAvailable(available); - } - *available = false; - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->MicrophoneMuteIsAvailable(available); + } + *available = false; + return 0; + }); } int32_t AdmProxy::SetMicrophoneMute(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetMicrophoneMute(enable); - } - return -1; + return WithPlatformAdm(-1, [enable](webrtc::AudioDeviceModule& adm) { + return adm.SetMicrophoneMute(enable); + }); } int32_t AdmProxy::MicrophoneMute(bool* enabled) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->MicrophoneMute(enabled); - } - return -1; + return WithPlatformAdm(-1, [enabled](webrtc::AudioDeviceModule& adm) { + return adm.MicrophoneMute(enabled); + }); } int32_t AdmProxy::StereoPlayoutIsAvailable(bool* available) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->StereoPlayoutIsAvailable(available); - } - *available = true; - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->StereoPlayoutIsAvailable(available); + } + *available = true; + return 0; + }); } int32_t AdmProxy::SetStereoPlayout(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetStereoPlayout(enable); - } - return 0; + return WithPlatformAdm(0, [enable](webrtc::AudioDeviceModule& adm) { + return adm.SetStereoPlayout(enable); + }); } int32_t AdmProxy::StereoPlayout(bool* enabled) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->StereoPlayout(enabled); - } - *enabled = true; - return 0; + return RunOnWorker([this, enabled] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->StereoPlayout(enabled); + } + *enabled = true; + return 0; + }); } int32_t AdmProxy::StereoRecordingIsAvailable(bool* available) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->StereoRecordingIsAvailable(available); - } - *available = false; - return 0; + return RunOnWorker([this, available] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->StereoRecordingIsAvailable(available); + } + *available = false; + return 0; + }); } int32_t AdmProxy::SetStereoRecording(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetStereoRecording(enable); - } - return 0; + return WithPlatformAdm(0, [enable](webrtc::AudioDeviceModule& adm) { + return adm.SetStereoRecording(enable); + }); } int32_t AdmProxy::StereoRecording(bool* enabled) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->StereoRecording(enabled); - } - *enabled = false; - return 0; + return RunOnWorker([this, enabled] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (platform_adm_) { + return platform_adm_->StereoRecording(enabled); + } + *enabled = false; + return 0; + }); } int32_t AdmProxy::PlayoutDelay(uint16_t* delayMS) const { - webrtc::MutexLock lock(&mutex_); - if (is_platform_playout_active()) { - if (platform_adm_) { + return RunOnWorker([this, delayMS] { + RTC_DCHECK_RUN_ON(worker_thread_); + if (IsPlatformPlayoutActive()) { return platform_adm_->PlayoutDelay(delayMS); } - } else if (synthetic_adm_) { - return synthetic_adm_->PlayoutDelay(delayMS); - } - *delayMS = 0; - return 0; + if (synthetic_adm_) { + return synthetic_adm_->PlayoutDelay(delayMS); + } + *delayMS = 0; + return 0; + }); } bool AdmProxy::BuiltInAECIsAvailable() const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->BuiltInAECIsAvailable(); - } - return false; + return WithPlatformAdm(false, [](webrtc::AudioDeviceModule& adm) { + return adm.BuiltInAECIsAvailable(); + }); } bool AdmProxy::BuiltInAGCIsAvailable() const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->BuiltInAGCIsAvailable(); - } - return false; + return WithPlatformAdm(false, [](webrtc::AudioDeviceModule& adm) { + return adm.BuiltInAGCIsAvailable(); + }); } bool AdmProxy::BuiltInNSIsAvailable() const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->BuiltInNSIsAvailable(); - } - return false; + return WithPlatformAdm(false, [](webrtc::AudioDeviceModule& adm) { + return adm.BuiltInNSIsAvailable(); + }); } int32_t AdmProxy::EnableBuiltInAEC(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->EnableBuiltInAEC(enable); - } - return -1; + return WithPlatformAdm(-1, [enable](webrtc::AudioDeviceModule& adm) { + return adm.EnableBuiltInAEC(enable); + }); } int32_t AdmProxy::EnableBuiltInAGC(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->EnableBuiltInAGC(enable); - } - return -1; + return WithPlatformAdm(-1, [enable](webrtc::AudioDeviceModule& adm) { + return adm.EnableBuiltInAGC(enable); + }); } int32_t AdmProxy::EnableBuiltInNS(bool enable) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->EnableBuiltInNS(enable); - } - return -1; + return WithPlatformAdm(-1, [enable](webrtc::AudioDeviceModule& adm) { + return adm.EnableBuiltInNS(enable); + }); } #if defined(WEBRTC_IOS) int AdmProxy::GetPlayoutAudioParameters(webrtc::AudioParameters* params) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->GetPlayoutAudioParameters(params); - } - return -1; + return WithPlatformAdm(-1, [params](webrtc::AudioDeviceModule& adm) { + return adm.GetPlayoutAudioParameters(params); + }); } int AdmProxy::GetRecordAudioParameters(webrtc::AudioParameters* params) const { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->GetRecordAudioParameters(params); - } - return -1; + return WithPlatformAdm(-1, [params](webrtc::AudioDeviceModule& adm) { + return adm.GetRecordAudioParameters(params); + }); } #endif int32_t AdmProxy::SetObserver(webrtc::AudioDeviceObserver* observer) { - webrtc::MutexLock lock(&mutex_); - if (platform_adm_) { - return platform_adm_->SetObserver(observer); - } - return 0; + return WithPlatformAdm(0, [observer](webrtc::AudioDeviceModule& adm) { + return adm.SetObserver(observer); + }); } } // namespace livekit_ffi diff --git a/webrtc-sys/src/audio_device_controller.cpp b/webrtc-sys/src/audio_device_controller.cpp index a4f1b8686..500b6efe9 100644 --- a/webrtc-sys/src/audio_device_controller.cpp +++ b/webrtc-sys/src/audio_device_controller.cpp @@ -22,8 +22,9 @@ namespace livekit_ffi { AudioDeviceController::AudioDeviceController( + std::shared_ptr rtc_runtime, webrtc::scoped_refptr adm_proxy) - : adm_proxy_(std::move(adm_proxy)) {} + : rtc_runtime_(std::move(rtc_runtime)), adm_proxy_(std::move(adm_proxy)) {} int16_t AudioDeviceController::playout_devices() const { return adm_proxy_->PlayoutDevices(); diff --git a/webrtc-sys/src/peer_connection_factory.cpp b/webrtc-sys/src/peer_connection_factory.cpp index cfb2d0c06..a10587fab 100644 --- a/webrtc-sys/src/peer_connection_factory.cpp +++ b/webrtc-sys/src/peer_connection_factory.cpp @@ -65,7 +65,8 @@ PeerConnectionFactory::PeerConnectionFactory( return webrtc::make_ref_counted( env_, rtc_runtime_->worker_thread()); }); - audio_device_ = std::make_shared(adm_proxy_); + audio_device_ = + std::make_shared(rtc_runtime_, adm_proxy_); dependencies.adm = adm_proxy_;