diff --git a/docs/fresh-audio-stream.md b/docs/fresh-audio-stream.md index 53743ce..2005449 100644 --- a/docs/fresh-audio-stream.md +++ b/docs/fresh-audio-stream.md @@ -64,6 +64,22 @@ renderer wake stalls. A 30-minute, two-participant application run measured p95 scheduled age at most 50 ms and maximum 56.911 ms, with bounded queues and no underruns; these are development measurements on one Windows machine. +## Device-pulled playout + +Applications with platform playout disabled can call `setExternalAudioPlayout(true)` +and drive `pullAudioPlayout()` from one device-clocked thread every 10 ms. One pull +serves all active Rooms: their shared `LkRuntime` owns one PeerConnectionFactory +and synthetic ADM. Read each active `FreshAudioStream` immediately after the pull. +No timer advances remote audio in this mode; disable it when the driver exits. + +The receive path is synchronous: `AudioTransportImpl::NeedMorePlayData` calls +`AudioMixerImpl::Mix`, which pulls every receive source, then `ChannelReceive` +decodes NetEq and calls its raw sink. `RemoteAudioSource::OnData` forwards to +`NativeAudioSink`, which fills the fresh queue inline on the caller thread. +A source not yet playing, or a NetEq error, can still produce no frame. +The Rust `external_playout_delivers_remote_frames_before_return` loopback test +checks immediate reads, sink thread identity, and internal timer resumption. + ## Publisher replacement regression The remote-audio routing fixture uncovered a publisher regression with mixed diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index 365be3f..7949fec 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -72,4 +72,16 @@ LIVEKIT_API void shutdown(); /// sound actually heard. Process-wide; clamped to 0..1000 ms. LIVEKIT_API void setAudioPlayoutDelay(std::chrono::milliseconds delay); +/// @brief Select application-clocked remote audio instead of the internal timer. +/// Process-wide, for applications that disable SDK platform playout. A single +/// caller thread must pull every 10 ms while enabled, or remote audio stalls. +/// @param enabled True to drive playout externally; false to resume the timer. +LIVEKIT_API void setExternalAudioPlayout(bool enabled); + +/// @brief Synchronously pull one 10 ms remote audio frame for all active Rooms. +/// Track sinks complete on this caller thread before return. Read active +/// FreshAudioStreams immediately afterwards. Use one caller thread only. +/// @return True if pulled; false if disabled, uninitialized, or not playing. +LIVEKIT_API bool pullAudioPlayout(); + } // namespace livekit diff --git a/patches/client-sdk-rust/0001-native-v2-runtime.patch b/patches/client-sdk-rust/0001-native-v2-runtime.patch index 0145d80..ac33c14 100644 --- a/patches/client-sdk-rust/0001-native-v2-runtime.patch +++ b/patches/client-sdk-rust/0001-native-v2-runtime.patch @@ -1,3 +1,19 @@ +diff --git a/.changeset/device-pulled-playout.md b/.changeset/device-pulled-playout.md +new file mode 100644 +index 00000000..47df01f5 +--- /dev/null ++++ b/.changeset/device-pulled-playout.md +@@ -0,0 +1,10 @@ ++--- ++webrtc-sys: patch ++libwebrtc: patch ++livekit: patch ++livekit-ffi: patch ++--- ++ ++Add process-wide external synthetic audio playout: a single application thread ++pulls one 10 ms frame synchronously for all Rooms, instead of the internal timer. ++Disabling external playout resumes the timer. diff --git a/.changeset/fresh-audio-ingress.md b/.changeset/fresh-audio-ingress.md new file mode 100644 index 00000000..8c07b23f @@ -340,10 +356,10 @@ index 6b0d8893..70167e97 100644 // Buffered path. diff --git a/libwebrtc/src/native/fresh_audio_stream.rs b/libwebrtc/src/native/fresh_audio_stream.rs new file mode 100644 -index 00000000..5d90f21d +index 00000000..8dddc7ef --- /dev/null +++ b/libwebrtc/src/native/fresh_audio_stream.rs -@@ -0,0 +1,372 @@ +@@ -0,0 +1,509 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); @@ -633,6 +649,143 @@ index 00000000..5d90f21d +mod tests { + use super::*; + ++ #[tokio::test] ++ async fn external_playout_delivers_remote_frames_before_return() { ++ use crate::{ ++ audio_frame::AudioFrame, audio_source::native::NativeAudioSource, ++ peer_connection_factory::native::PeerConnectionFactoryExt, prelude::*, ++ }; ++ use std::{borrow::Cow, sync::Mutex, thread}; ++ use tokio::sync::mpsc; ++ use webrtc_sys::playout_delay::{lk_pull_audio_playout, lk_set_external_audio_playout}; ++ ++ struct ExternalPlayout; ++ impl Drop for ExternalPlayout { ++ fn drop(&mut self) { ++ // SAFETY: The native mode setter synchronizes ADM lifetime. ++ unsafe { lk_set_external_audio_playout(false) }; ++ } ++ } ++ struct ThreadProbe { ++ owner: thread::ThreadId, ++ calls: AtomicU64, ++ wrong_thread: AtomicBool, ++ } ++ impl sys_at::AudioSink for ThreadProbe { ++ fn on_data(&self, _: &[i16], _: i32, _: usize, _: usize) { ++ if thread::current().id() != self.owner { ++ self.wrong_thread.store(true, Ordering::Relaxed); ++ } ++ self.calls.fetch_add(1, Ordering::Relaxed); ++ } ++ } ++ ++ // Run this process-global mode test in isolation from factory tests. ++ // SAFETY: All pulls in this test are on this one owner thread. ++ unsafe { lk_set_external_audio_playout(true) }; ++ let _external = ExternalPlayout; ++ let factory = PeerConnectionFactory::default(); ++ let publisher = factory.create_peer_connection(RtcConfiguration::default()).unwrap(); ++ let receiver = factory.create_peer_connection(RtcConfiguration::default()).unwrap(); ++ let source = NativeAudioSource::new(Default::default(), 48_000, 2, 0); ++ let local = factory.create_audio_track("external-playout", source.clone()); ++ publisher.add_track(MediaStreamTrack::Audio(local), &["test"]).unwrap(); ++ let (publisher_tx, mut publisher_rx) = mpsc::unbounded_channel(); ++ let (receiver_tx, mut receiver_rx) = mpsc::unbounded_channel(); ++ publisher.on_ice_candidate(Some(Box::new(move |candidate| { ++ let _ = publisher_tx.send(candidate); ++ }))); ++ receiver.on_ice_candidate(Some(Box::new(move |candidate| { ++ let _ = receiver_tx.send(candidate); ++ }))); ++ let remote = Arc::new(Mutex::new(None)); ++ let received = remote.clone(); ++ receiver.on_track(Some(Box::new(move |event| { ++ if let Some(MediaStreamTrack::Audio(track)) = event.receiver.track() { ++ *received.lock().unwrap() = Some(track); ++ } ++ }))); ++ tokio::time::timeout(Duration::from_secs(10), async { ++ let offer = publisher.create_offer(OfferOptions::default()).await.unwrap(); ++ publisher.set_local_description(offer.clone()).await.unwrap(); ++ receiver.set_remote_description(offer).await.unwrap(); ++ let answer = receiver.create_answer(AnswerOptions::default()).await.unwrap(); ++ receiver.set_local_description(answer.clone()).await.unwrap(); ++ publisher.set_remote_description(answer).await.unwrap(); ++ }) ++ .await ++ .expect("loopback negotiation deadline"); ++ let track = remote.lock().unwrap().take().expect("remote audio track"); ++ let stream = FreshAudioStream::new(track.clone()); ++ let probe = Arc::new(ThreadProbe { ++ owner: thread::current().id(), ++ calls: AtomicU64::new(0), ++ wrong_thread: AtomicBool::new(false), ++ }); ++ let sink = sys_at::ffi::new_native_audio_sink( ++ Box::new(sys_at::AudioSinkWrapper::new(probe.clone())), ++ 48_000, ++ 2, ++ ); ++ // SAFETY: The retained remote track is typed audio and outlives the sink. ++ let audio = unsafe { sys_at::ffi::media_to_audio(track.handle.sys_handle()) }; ++ audio.add_sink(&sink); ++ let samples = [2_000; FRESH_AUDIO_SAMPLES]; ++ let input = AudioFrame { ++ data: Cow::Borrowed(&samples), ++ sample_rate: 48_000, ++ num_channels: 2, ++ samples_per_channel: 480, ++ }; ++ let mut verified = 0; ++ let mut heard = false; ++ let deadline = Instant::now() + Duration::from_secs(10); ++ while Instant::now() < deadline && verified < 100 { ++ while let Ok(candidate) = publisher_rx.try_recv() { ++ receiver.add_ice_candidate(candidate).await.unwrap(); ++ } ++ while let Ok(candidate) = receiver_rx.try_recv() { ++ publisher.add_ice_candidate(candidate).await.unwrap(); ++ } ++ source.capture_frame_direct(&input).unwrap(); ++ assert_eq!(stream.stats().queued, 0, "timer produced a frame in external mode"); ++ let before = probe.calls.load(Ordering::Relaxed); ++ // SAFETY: External mode is enabled and this is its sole caller thread. ++ let pulled = unsafe { lk_pull_audio_playout() }; ++ if probe.calls.load(Ordering::Relaxed) > before { ++ assert!(pulled); ++ assert_eq!(probe.calls.load(Ordering::Relaxed), before + 1); ++ let frame = ++ stream.try_read(Duration::from_secs(1)).expect("synchronous remote PCM"); ++ heard |= frame.samples.iter().any(|sample| *sample > 100 || *sample < -100); ++ assert!(stream.try_read(Duration::from_secs(1)).is_none()); ++ verified += 1; ++ } ++ // Pace the simulated device, not a wait for sink delivery. ++ tokio::time::sleep(Duration::from_millis(10)).await; ++ } ++ assert_eq!(verified, 100, "remote receive source never became active"); ++ assert!(heard, "loopback delivered only silence"); ++ assert!(!probe.wrong_thread.load(Ordering::Relaxed)); ++ assert_eq!(stream.stats().dropped, 0); ++ // SAFETY: The mode setter waits for the last synchronous pull. ++ unsafe { lk_set_external_audio_playout(false) }; ++ // SAFETY: A disabled driver cannot call through the transport. ++ assert!(!unsafe { lk_pull_audio_playout() }); ++ let before = probe.calls.load(Ordering::Relaxed); ++ tokio::time::timeout(Duration::from_secs(1), async { ++ while probe.calls.load(Ordering::Relaxed) == before { ++ tokio::time::sleep(Duration::from_millis(1)).await; ++ } ++ }) ++ .await ++ .expect("internal timer did not resume"); ++ audio.remove_sink(&sink); ++ drop(stream); ++ publisher.close(); ++ receiver.close(); ++ } ++ + #[test] + fn overrun_stale_and_retirement() { + let queue = FreshAudioQueue::new(); @@ -831,10 +984,10 @@ index 7a4234e3..880e2825 100644 pub trait VideoFrameBufferExt: VideoBuffer { diff --git a/livekit-ffi/include/livekit_ffi.h b/livekit-ffi/include/livekit_ffi.h -index 15f64dfa..bf31c631 100644 +index 15f64dfa..6da08d39 100644 --- a/livekit-ffi/include/livekit_ffi.h +++ b/livekit-ffi/include/livekit_ffi.h -@@ -37,6 +37,15 @@ FfiHandleId livekit_ffi_request(const uint8_t *data, size_t len, +@@ -37,6 +37,23 @@ FfiHandleId livekit_ffi_request(const uint8_t *data, size_t len, bool livekit_ffi_drop_handle(FfiHandleId handle_id); @@ -846,6 +999,14 @@ index 15f64dfa..bf31c631 100644 + * subtracts it from the audio playout position. Clamped to 1000 ms. + */ +void livekit_ffi_set_audio_playout_delay(uint32_t delay_ms); ++ ++/// Enables process-wide application-clocked playout. A single caller must pull ++/// every 10 ms while enabled, or remote audio stalls. Disable to resume the timer. ++void livekit_ffi_set_external_audio_playout(bool enabled); ++ ++/// Synchronously pulls one 10 ms frame for all active Rooms; false if not playing ++/// or external playout is disabled. Call from the single playout owner thread. ++bool livekit_ffi_pull_audio_playout(); + } // extern "C" @@ -995,10 +1156,10 @@ index a4b610e0..0e177673 100644 message VideoSourceInfo { diff --git a/livekit-ffi/src/cabi.rs b/livekit-ffi/src/cabi.rs -index d9e83f13..0ea4fa91 100644 +index d9e83f13..4204694a 100644 --- a/livekit-ffi/src/cabi.rs +++ b/livekit-ffi/src/cabi.rs -@@ -108,6 +108,27 @@ pub extern "C" fn livekit_ffi_drop_handle(handle_id: FfiHandleId) -> bool { +@@ -108,6 +108,43 @@ pub extern "C" fn livekit_ffi_drop_handle(handle_id: FfiHandleId) -> bool { FFI_SERVER.drop_handle(handle_id) } @@ -1022,6 +1183,22 @@ index d9e83f13..0ea4fa91 100644 +pub extern "C" fn livekit_ffi_set_audio_playout_delay(delay_ms: u32) { + unsafe { webrtc_sys::playout_delay::lk_set_playout_delay_ms(delay_ms) } +} ++ ++/// Enables process-wide application-clocked playout. A single caller must pull ++/// every 10 ms while enabled, or remote audio stalls. Disable to resume the timer. ++#[no_mangle] ++pub extern "C" fn livekit_ffi_set_external_audio_playout(enabled: bool) { ++ // SAFETY: The native entry point synchronizes mode and ADM lifetime internally. ++ unsafe { webrtc_sys::playout_delay::lk_set_external_audio_playout(enabled) } ++} ++ ++/// Synchronously pulls one 10 ms frame for all active Rooms; false if not playing ++/// or external playout is disabled. Call from the single playout owner thread. ++#[no_mangle] ++pub extern "C" fn livekit_ffi_pull_audio_playout() -> bool { ++ // SAFETY: The native entry point guards the ADM through the synchronous pull. ++ unsafe { webrtc_sys::playout_delay::lk_pull_audio_playout() } ++} + #[no_mangle] pub extern "C" fn livekit_ffi_dispose() { @@ -1728,6 +1905,27 @@ index f01c754c..d83a61c8 100644 req.packet_trailer_features = options.frame_metadata_features.to_proto().into_iter().map(|f| f as i32).collect(); +diff --git a/livekit/src/rtc_engine/lk_runtime.rs b/livekit/src/rtc_engine/lk_runtime.rs +index e1c77f68..ad9b86b5 100644 +--- a/livekit/src/rtc_engine/lk_runtime.rs ++++ b/livekit/src/rtc_engine/lk_runtime.rs +@@ -322,7 +322,15 @@ impl LkRuntime { + + #[cfg(test)] + mod tests { +- use super::{LkRuntimeState, WebRtcRuntimeInitializedError}; ++ use super::{LkRuntime, LkRuntimeState, WebRtcRuntimeInitializedError}; ++ ++ #[test] ++ fn active_rooms_share_one_runtime_and_factory() { ++ let first = LkRuntime::instance(); ++ let second = LkRuntime::instance(); ++ assert!(std::sync::Arc::ptr_eq(&first, &second)); ++ assert!(std::ptr::eq(first.pc_factory(), second.pc_factory())); ++ } + + #[test] + fn zero_playout_delay_can_be_enabled_early_and_repeated() { diff --git a/livekit/src/rtc_engine/peer_transport.rs b/livekit/src/rtc_engine/peer_transport.rs index 7eccd2ef..0e41446d 100644 --- a/livekit/src/rtc_engine/peer_transport.rs @@ -2854,10 +3052,10 @@ index abf4f5ab..d49ab199 100644 std::shared_ptr create_peer_connection_factory(); diff --git a/webrtc-sys/include/livekit/synthetic_audio_device.h b/webrtc-sys/include/livekit/synthetic_audio_device.h -index 0d0bb8db..3b31e137 100644 +index 0d0bb8db..dd76d2a3 100644 --- a/webrtc-sys/include/livekit/synthetic_audio_device.h +++ b/webrtc-sys/include/livekit/synthetic_audio_device.h -@@ -24,6 +24,12 @@ +@@ -24,6 +24,16 @@ #include "rtc_base/synchronization/mutex.h" #include "rtc_base/task_utils/repeating_task.h" @@ -2866,10 +3064,40 @@ index 0d0bb8db..3b31e137 100644 +// PlayoutDelay, so audio/video synchronization and the playout estimates +// account for it. Clamped to 1000 ms; process-wide. +extern "C" void lk_set_playout_delay_ms(uint32_t delay_ms); ++// Process-wide single caller: pull one 10 ms frame while enabled, otherwise ++// remote audio stalls. Disabling restores the internal timer. ++extern "C" void lk_set_external_audio_playout(bool enabled); ++extern "C" bool lk_pull_audio_playout(); + namespace livekit_ffi { class SyntheticAudioDevice : public webrtc::AudioDeviceModule { +@@ -37,6 +47,8 @@ class SyntheticAudioDevice : public webrtc::AudioDeviceModule { + int32_t Init() override; + int32_t Terminate() override; + bool Initialized() const override; ++ void SetExternalPlayout(bool enabled); ++ bool PullPlayout(); + + int16_t PlayoutDevices() override; + int16_t RecordingDevices() override; +@@ -117,13 +129,15 @@ class SyntheticAudioDevice : public webrtc::AudioDeviceModule { + int32_t SetObserver(webrtc::AudioDeviceObserver* sink) override; + + private: ++ bool PullPlayoutLocked(); + mutable webrtc::Mutex mutex_; + std::vector data_; + std::unique_ptr audio_queue_; + webrtc::RepeatingTaskHandle audio_task_; +- webrtc::AudioTransport* audio_transport_; ++ webrtc::AudioTransport* audio_transport_{nullptr}; + const webrtc::Environment& env_; + bool playing_{false}; + bool initialized_{false}; ++ bool external_playout_{false}; + }; + } // namespace livekit_ffi diff --git a/webrtc-sys/include/livekit/video_frame_buffer.h b/webrtc-sys/include/livekit/video_frame_buffer.h index cb419fd3..4f09d876 100644 --- a/webrtc-sys/include/livekit/video_frame_buffer.h @@ -3203,10 +3431,10 @@ index 00000000..79dcb9c0 + +} // namespace livekit_ffi diff --git a/webrtc-sys/src/lib.rs b/webrtc-sys/src/lib.rs -index d33322f1..29af5e31 100644 +index d33322f1..8f93e47b 100644 --- a/webrtc-sys/src/lib.rs +++ b/webrtc-sys/src/lib.rs -@@ -43,6 +43,23 @@ pub mod video_track; +@@ -43,6 +43,27 @@ pub mod video_track; pub mod webrtc; pub mod yuv_helper; @@ -3224,6 +3452,10 @@ index d33322f1..29af5e31 100644 +pub mod playout_delay { + extern "C" { + pub fn lk_set_playout_delay_ms(delay_ms: u32); ++ /// Enables application-clocked playout; one caller must pull every 10 ms. ++ pub fn lk_set_external_audio_playout(enabled: bool); ++ /// Pulls one 10 ms frame synchronously; false when disabled or not playing. ++ pub fn lk_pull_audio_playout() -> bool; + } +} + @@ -3681,10 +3913,98 @@ index e97bed2d..24ae4db9 100644 uint32_t RtpSender::ssrc() const { diff --git a/webrtc-sys/src/synthetic_audio_device.cpp b/webrtc-sys/src/synthetic_audio_device.cpp -index 8074b9d9..ec76aef1 100644 +index 8074b9d9..58849cd9 100644 --- a/webrtc-sys/src/synthetic_audio_device.cpp +++ b/webrtc-sys/src/synthetic_audio_device.cpp -@@ -288,7 +288,12 @@ int32_t SyntheticAudioDevice::StereoRecording(bool* enabled) const { +@@ -23,6 +23,14 @@ const int kSamplesPer10Ms = kSampleRate / 100; + + namespace livekit_ffi { + ++namespace { ++// Lock order is registration then device. Hold registration through a pull so ++// Terminate cannot retire the instance while its transport is being called. ++webrtc::Mutex g_playout_mutex; ++SyntheticAudioDevice* g_playout_device = nullptr; ++bool g_external_playout = false; ++} // namespace ++ + SyntheticAudioDevice::SyntheticAudioDevice(const webrtc::Environment& env) + : env_(env), + data_(kSamplesPer10Ms * kChannels) {} +@@ -43,6 +51,7 @@ int32_t SyntheticAudioDevice::RegisterAudioCallback(webrtc::AudioTransport* tran + } + + int32_t SyntheticAudioDevice::Init() { ++ webrtc::MutexLock registration(&g_playout_mutex); + webrtc::MutexLock lock(&mutex_); + if (initialized_) + return 0; +@@ -55,38 +64,52 @@ int32_t SyntheticAudioDevice::Init() { + webrtc::RepeatingTaskHandle::Start(audio_queue_.get(), [this]() { + webrtc::MutexLock lock(&mutex_); + +- if (playing_) { +- int64_t elapsed_time_ms = -1; +- int64_t ntp_time_ms = -1; +- size_t n_samples_out = 0; +- void* data = data_.data(); +- +- // Request the AudioData, otherwise WebRTC will ignore the packets. +- // 10ms of audio data. +- audio_transport_->NeedMorePlayData( +- kSamplesPer10Ms, kBytesPerSample, kChannels, kSampleRate, data, +- n_samples_out, &elapsed_time_ms, &ntp_time_ms); +- } ++ if (!external_playout_) PullPlayoutLocked(); + + return webrtc::TimeDelta::Millis(10); + }); + + initialized_ = true; ++ external_playout_ = g_external_playout; ++ g_playout_device = this; + return 0; + } + + int32_t SyntheticAudioDevice::Terminate() { + { ++ webrtc::MutexLock registration(&g_playout_mutex); + webrtc::MutexLock lock(&mutex_); + if (!initialized_) + return 0; + + initialized_ = false; ++ if (g_playout_device == this) g_playout_device = nullptr; + } + audio_queue_ = nullptr; + return 0; + } + ++void SyntheticAudioDevice::SetExternalPlayout(bool enabled) { ++ webrtc::MutexLock lock(&mutex_); ++ external_playout_ = enabled; ++} ++ ++bool SyntheticAudioDevice::PullPlayout() { ++ webrtc::MutexLock lock(&mutex_); ++ return external_playout_ && PullPlayoutLocked(); ++} ++ ++bool SyntheticAudioDevice::PullPlayoutLocked() { ++ if (!initialized_ || !playing_ || !audio_transport_) return false; ++ int64_t elapsed_time_ms = -1; ++ int64_t ntp_time_ms = -1; ++ size_t n_samples_out = 0; ++ audio_transport_->NeedMorePlayData( ++ kSamplesPer10Ms, kBytesPerSample, kChannels, kSampleRate, data_.data(), ++ n_samples_out, &elapsed_time_ms, &ntp_time_ms); ++ return true; ++} ++ + bool SyntheticAudioDevice::Initialized() const { + webrtc::MutexLock lock(&mutex_); + return initialized_; +@@ -288,7 +311,12 @@ int32_t SyntheticAudioDevice::StereoRecording(bool* enabled) const { return 0; } @@ -3697,7 +4017,7 @@ index 8074b9d9..ec76aef1 100644 return 0; } -@@ -333,3 +338,8 @@ int32_t SyntheticAudioDevice::SetObserver(webrtc::AudioDeviceObserver* observer) +@@ -333,3 +361,21 @@ int32_t SyntheticAudioDevice::SetObserver(webrtc::AudioDeviceObserver* observer) } } // namespace livekit_ffi @@ -3706,6 +4026,19 @@ index 8074b9d9..ec76aef1 100644 + livekit_ffi::g_playout_delay_ms.store(static_cast(delay_ms > 1000 ? 1000 : delay_ms), + std::memory_order_relaxed); +} ++ ++extern "C" void lk_set_external_audio_playout(bool enabled) { ++ webrtc::MutexLock lock(&livekit_ffi::g_playout_mutex); ++ livekit_ffi::g_external_playout = enabled; ++ if (livekit_ffi::g_playout_device) ++ livekit_ffi::g_playout_device->SetExternalPlayout(enabled); ++} ++ ++extern "C" bool lk_pull_audio_playout() { ++ webrtc::MutexLock lock(&livekit_ffi::g_playout_mutex); ++ return livekit_ffi::g_playout_device && ++ livekit_ffi::g_playout_device->PullPlayout(); ++} diff --git a/webrtc-sys/src/video_decoder_factory.cpp b/webrtc-sys/src/video_decoder_factory.cpp index d22ae70d..676b8e5a 100644 --- a/webrtc-sys/src/video_decoder_factory.cpp diff --git a/src/livekit.cpp b/src/livekit.cpp index 6ed6c47..2011f86 100644 --- a/src/livekit.cpp +++ b/src/livekit.cpp @@ -32,15 +32,17 @@ bool initialize(const LogLevel& level) { return ffi_client.initialize(false); } -void setExternalVideoDecoder(const LkExternalVideoDecoder* decoder) { - livekit_ffi_set_external_video_decoder(decoder); -} +void setExternalVideoDecoder(const LkExternalVideoDecoder* decoder) { livekit_ffi_set_external_video_decoder(decoder); } void setAudioPlayoutDelay(std::chrono::milliseconds delay) { - livekit_ffi_set_audio_playout_delay(static_cast(std::clamp( - delay.count(), 0, 1000))); + livekit_ffi_set_audio_playout_delay( + static_cast(std::clamp(delay.count(), 0, 1000))); } +void setExternalAudioPlayout(bool enabled) { livekit_ffi_set_external_audio_playout(enabled); } + +bool pullAudioPlayout() { return livekit_ffi_pull_audio_playout(); } + bool isInitialized() { return FfiClient::instance().isInitialized(); } void shutdown() {