reactor_webrtc/lib.rs
1//! `reactor-webrtc` — a safe, idiomatic Rust API over an owned build of Google's
2//! WebRTC engine (see the `reactor-webrtc-sys` crate and `../../webrtc-build`).
3//!
4//! ## Shape
5//!
6//! [`PeerConnectionFactory`] → [`PeerConnection`] (with a closure-based
7//! [`PeerConnectionObserver`]) → SDP offer/answer, ICE, [`Track`]s (audio +
8//! video send/receive) and [`DataChannel`]s. RAII throughout: dropping a handle
9//! releases the native object.
10//!
11//! ICE credentials are libwebrtc's to generate, and it exposes no setter. When an
12//! application needs to *choose* its ufrag — so that a fronting layer can route on
13//! it — [`SessionDescription::with_ice_credentials`] substitutes them in the
14//! description before it is set locally, which is where libwebrtc reads the
15//! transport's ICE parameters from.
16//!
17//! Per-frame metadata ([`metadata`]) rides in a trailer appended to the encoded
18//! payload, which only works if the peer strips it again. That is negotiated for
19//! you: every [`PeerConnection::create_offer`] advertises the capability as a
20//! session-level `a=x-reactor-frame-metadata`,
21//! [`PeerConnection::create_answer`] mirrors an offer that asked for
22//! it, and [`PeerConnection::set_remote_description`] arms a
23//! [`FrameMetadataGate`] from what the remote declared, and installs the metadata
24//! steps on the video transceivers. Callers pass `user_data` when it is meaningful
25//! and never have to ask what the far end supports — a trailer reaches the wire only
26//! when the peer said it strips them. Set
27//! [`RtcConfiguration::frame_metadata`] to `false` to keep the whole thing out of a
28//! connection.
29//!
30//! A [`FrameTransform`] of your own composes with that rather than displacing it:
31//! the crate owns libwebrtc's single transformer slot per sender/receiver and runs
32//! both, so encoded-frame access and per-frame metadata work on one transceiver.
33//!
34//! Building a real binary or test requires a native `libwebrtc`; set
35//! `REACTOR_WEBRTC_LIB_DIR` or `REACTOR_WEBRTC_PREBUILT_URL`. `cargo check`
36//! works without one.
37
38mod builder;
39mod chunking_times;
40mod config;
41mod dc_chunking;
42mod encoded;
43mod media;
44pub mod metadata;
45mod observer;
46mod peer_connection;
47pub mod platform;
48mod playout;
49mod sender_meta;
50
51use std::ffi::CString;
52use std::os::raw::c_int;
53use std::sync::Arc;
54
55pub use builder::PeerConnectionFactoryBuilder;
56pub use chunking_times::ChunkingStats;
57pub use config::{
58 BundlePolicy, ContinualGatheringPolicy, IceServer, IceTransportsType, RtcConfiguration,
59 TcpCandidatePolicy,
60};
61pub use dc_chunking::DcChunking;
62pub use encoded::{
63 EncodedFrame, EncodedVideoFrame, EncodedVideoTrack, EncoderFeedback, FrameAction,
64 FrameDirection, FrameTransform, H264Backend, InlineEncoderCallback, LocalVideoTrack,
65 PreEncodedOptions, RawVideoFrame, TrackVideoEncoder, VideoCodec, VideoTrackOptions,
66};
67/// What a peer declares in `a=x-reactor-dc-chunking` (see
68/// [`SessionDescription::with_dc_chunking`]).
69pub use reactor_webrtc_dc_chunking::Params as DcChunkingParams;
70/// Why a chunked data channel refused a message ([`Error::DataChannel`]).
71pub use reactor_webrtc_dc_chunking::SendError as DcSendError;
72
73/// Whether this build targets Apple (H.264 VideoToolbox backend exists).
74pub(crate) const HAVE_VIDEO_TOOLBOX: bool = cfg!(target_vendor = "apple");
75pub use media::{
76 AudioFrame, AudioTrack, AudioTrackOptions, AudioTrackSource, MediaKind, RemoteTrack, Track,
77 VideoFrame, VideoSinkTimings, VideoTrack,
78};
79pub use metadata::{
80 FrameMetadata, FrameMetadataGate, FRAME_METADATA_ATTRIBUTE, FRAME_METADATA_VERSION,
81};
82pub use observer::PeerConnectionObserver;
83pub use peer_connection::{
84 DataChannel, DataChannelState, DataChannelStats, IceCandidate, IceCandidatePairState,
85 IceCandidatePairStats, IceCandidateType, IceGatheringState, InboundRtpStats, OutboundRtpStats,
86 PeerConnection, PeerConnectionState, RelayProtocol, SctpStats, SdpType, SessionDescription,
87 StatsReport, StreamKind, TimingFrameInfo, TimingFrameReceiverTimestamps,
88 TimingFrameSenderTimestamps, Transceiver, TransceiverDirection,
89};
90pub use playout::PlayoutDelay;
91/// Runtime download/verification/caching of Cisco's OpenH264 shared library,
92/// and the required attribution string — registered with
93/// [`PeerConnectionFactoryBuilder::with_openh264`].
94#[cfg(feature = "openh264")]
95pub use reactor_webrtc_sys::openh264;
96
97/// The ABI version of the linked native build. Used to assert that the safe
98/// crate and the prebuilt `libwebrtc` agree.
99pub fn native_abi_version() -> u32 {
100 // Safe: a pure version getter with no arguments.
101 unsafe { reactor_webrtc_sys::reactor_webrtc_abi_version() }
102}
103
104/// The engine's monotonic clock, in microseconds.
105///
106/// The epoch [`Track::push_video_frame_at`], [`Track::push_video_frame_with_metadata_at`],
107/// and [`Track::push_pcm_at`] read their capture timestamps in. Read it once per
108/// unit of produced media and stamp every track with that one value: audio and
109/// video are synchronised by sharing a capture time, not by reaching the encoder
110/// at the same moment.
111pub fn time_micros() -> i64 {
112 // Safe: a pure clock read with no arguments.
113 unsafe { reactor_webrtc_sys::reactor_webrtc_time_micros() }
114}
115
116/// Errors surfaced by the WebRTC engine.
117#[derive(Debug, Clone)]
118pub enum Error {
119 /// A WebRTC operation failed (SDP, ICE, transport, …).
120 Webrtc(String),
121 /// The requested track/transceiver/data-channel was not found.
122 NotFound(String),
123 /// A chunked data channel refused a message: it is larger than the
124 /// effective max message size, or the send queue is full. Nothing of it
125 /// was sent, and the channel is still usable.
126 DataChannel(DcSendError),
127}
128
129impl std::fmt::Display for Error {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 Error::Webrtc(m) => write!(f, "webrtc error: {m}"),
133 Error::NotFound(m) => write!(f, "not found: {m}"),
134 Error::DataChannel(e) => write!(f, "data channel: {e}"),
135 }
136 }
137}
138
139impl std::error::Error for Error {}
140
141pub type Result<T> = std::result::Result<T, Error>;
142
143/// Which audio device module a [`PeerConnectionFactory`] uses.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
145pub enum AdmMode {
146 /// Headless, no audio hardware: the app pushes mic PCM via
147 /// [`PeerConnectionFactory::push_audio_frame`] and receives decoded audio
148 /// through the track sinks ([`Track::on_audio_frame`]). Right for servers /
149 /// app-driven media.
150 #[default]
151 Synthetic,
152 /// The platform audio device (CoreAudio / ALSA / WASAPI): real mic capture +
153 /// speaker playout. Right for desktop client apps.
154 Platform,
155}
156
157/// Audio Processing Module configuration passed to [`PeerConnectionFactory`].
158///
159/// All fields default to `false` (no processing). Enable selectively:
160/// ```
161/// use reactor_webrtc::ApmConfig;
162///
163/// let apm = ApmConfig { echo_canceller: true, noise_suppression: true, ..Default::default() };
164/// ```
165#[derive(Debug, Clone, Copy, Default)]
166pub struct ApmConfig {
167 /// AEC3 acoustic echo cancellation.
168 pub echo_canceller: bool,
169 /// Noise suppression (level = kHigh when enabled).
170 pub noise_suppression: bool,
171 /// Automatic gain control (gain_controller1).
172 pub agc: bool,
173 /// High-pass filter.
174 pub high_pass_filter: bool,
175}
176
177impl ApmConfig {
178 pub(crate) fn to_flags(self) -> c_int {
179 let mut f: c_int = 0;
180 if self.echo_canceller {
181 f |= 0x01;
182 }
183 if self.noise_suppression {
184 f |= 0x02;
185 }
186 if self.agc {
187 f |= 0x04;
188 }
189 if self.high_pass_filter {
190 f |= 0x08;
191 }
192 f
193 }
194}
195
196/// The native factory pointer, refcounted so every object the factory produces
197/// (a [`PeerConnection`], a [`Track`](crate::media::Track)) can hold a clone and
198/// keep the factory's signaling/worker/network threads alive for as long as it
199/// lives — including past the point where the [`PeerConnectionFactory`] value
200/// itself is dropped. Without this, the factory's `Drop` tears those threads
201/// down as soon as its own handle goes out of scope, and any object it produced
202/// that outlives it is left holding a pointer into what those threads used to
203/// back.
204pub(crate) struct FactoryHandle(*mut reactor_webrtc_sys::PeerConnectionFactory);
205
206// SAFETY: the native factory is internally thread-safe (it owns the WebRTC
207// signaling/worker/network threads).
208unsafe impl Send for FactoryHandle {}
209unsafe impl Sync for FactoryHandle {}
210
211impl FactoryHandle {
212 pub(crate) fn raw(&self) -> *mut reactor_webrtc_sys::PeerConnectionFactory {
213 self.0
214 }
215}
216
217impl Drop for FactoryHandle {
218 fn drop(&mut self) {
219 unsafe { reactor_webrtc_sys::reactor_webrtc_factory_destroy(self.0) }
220 }
221}
222
223/// Entry point: creates peer connections and tracks, and owns the audio device
224/// module (synthetic by default, or the platform device). Construct it with
225/// [`PeerConnectionFactory::builder`].
226pub struct PeerConnectionFactory {
227 handle: Arc<FactoryHandle>,
228 /// Factory-level kill switch for per-frame metadata: when `false`, every
229 /// [`PeerConnection`] from this factory behaves like one created with
230 /// `RtcConfiguration::frame_metadata` off, whatever each config says.
231 metadata_enabled: bool,
232 /// [`PeerConnectionFactoryBuilder::with_dc_chunking`]: the settings every
233 /// connection from this factory takes part with, unless its
234 /// `RtcConfiguration::dc_chunking` opts out. `None` = never offered.
235 dc_chunking: Option<DcChunking>,
236 /// Per-track encoder slots (pre-encoded / inline), wired into the native
237 /// factory at creation. Every factory has one; tracks register slots via
238 /// [`PeerConnectionFactory::create_video_track_with_options`].
239 registry: Arc<crate::encoded::EncoderRegistry>,
240 /// Whether an OpenH264 library path was registered at build
241 /// ([`PeerConnectionFactoryBuilder::with_openh264`]). Gates the explicit
242 /// [`H264Backend::OpenH264`] per-track selection. (Whether the library
243 /// itself loaded is a native-side concern — a failed load degrades to
244 /// "no OpenH264 backend", it never fails the factory.)
245 #[cfg_attr(not(feature = "openh264"), allow(dead_code))]
246 openh264_registered: bool,
247}
248
249impl PeerConnectionFactory {
250 /// Clone the refcounted native handle, so a [`PeerConnection`] or
251 /// [`Track`] created from this factory can keep its threads alive past
252 /// this value's own lifetime.
253 pub(crate) fn handle(&self) -> Arc<FactoryHandle> {
254 Arc::clone(&self.handle)
255 }
256
257 /// Start composing a factory — the replacement for the old one-shot
258 /// constructors. Chain the knobs you need, then [`build`](PeerConnectionFactoryBuilder::build):
259 ///
260 /// ```rust,ignore
261 /// let factory = PeerConnectionFactory::builder()
262 /// .with_platform_adm()
263 /// .with_metadata(false)
264 /// .build()?;
265 /// ```
266 pub fn builder() -> PeerConnectionFactoryBuilder {
267 PeerConnectionFactoryBuilder::new()
268 }
269
270 /// Shared create path: every constructor shapes a
271 /// [`reactor_webrtc_sys::ReactorFactoryOptions`] and lands here. The glue
272 /// writes a reason into `err` when it returns null; a silent null gets the
273 /// generic message.
274 pub(crate) fn create_from_options(
275 opts: &reactor_webrtc_sys::ReactorFactoryOptions,
276 metadata_enabled: bool,
277 dc_chunking: Option<DcChunking>,
278 registry: Arc<crate::encoded::EncoderRegistry>,
279 openh264_registered: bool,
280 ) -> Result<Self> {
281 let mut err = [0 as std::os::raw::c_char; 256];
282 let raw = unsafe {
283 reactor_webrtc_sys::reactor_webrtc_factory_create(
284 opts,
285 err.as_mut_ptr(),
286 err.len() as c_int,
287 )
288 };
289 if raw.is_null() {
290 let reason = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }
291 .to_string_lossy()
292 .into_owned();
293 return Err(Error::Webrtc(if reason.is_empty() {
294 "factory creation returned null".into()
295 } else {
296 format!("factory creation failed: {reason}")
297 }));
298 }
299 Ok(Self {
300 handle: Arc::new(FactoryHandle(raw)),
301 metadata_enabled,
302 dc_chunking,
303 registry,
304 openh264_registered,
305 })
306 }
307
308 /// Create a peer connection with the given configuration and observer.
309 pub fn create_peer_connection(
310 &self,
311 config: &RtcConfiguration,
312 observer: PeerConnectionObserver,
313 ) -> Result<PeerConnection> {
314 let dc_negotiation = dc_chunking::DcNegotiation::new(
315 self.dc_chunking.clone().filter(|_| config.dc_chunking),
316 );
317 let state = observer.into_state(self.handle(), Arc::clone(&dc_negotiation));
318 let callbacks = state.callbacks();
319 let native = config.to_native()?;
320 // libwebrtc reports why it rejected the configuration (an empty TURN
321 // credential, for example); the glue copies that reason in here.
322 let mut err = [0 as std::os::raw::c_char; 256];
323 let raw = unsafe {
324 reactor_webrtc_sys::reactor_webrtc_peer_connection_create(
325 self.handle.raw(),
326 &native.config(),
327 &callbacks,
328 err.as_mut_ptr(),
329 err.len() as c_int,
330 )
331 };
332 if raw.is_null() {
333 let reason = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }
334 .to_string_lossy()
335 .into_owned();
336 return Err(Error::Webrtc(if reason.is_empty() {
337 "peer connection creation returned null".into()
338 } else {
339 format!("peer connection creation failed: {reason}")
340 }));
341 }
342 Ok(PeerConnection::new(
343 raw,
344 state,
345 self.handle(),
346 config.frame_metadata && self.metadata_enabled,
347 dc_negotiation,
348 ))
349 }
350
351 /// Create a local video track backed by a push-able source
352 /// ([`Track::push_video_frame`]); libwebrtc's builtin encoder pipeline
353 /// encodes it. For encoder plumbing (pre-encoded / inline) use
354 /// [`create_video_track_with_options`](Self::create_video_track_with_options).
355 ///
356 /// Slot assignment between encoder instances and tracks is **positional**:
357 /// create tracks before (or in the same order as) the transceivers that
358 /// carry them. Slots are reserved only after native creation succeeds,
359 /// so a failed creation (bad id, native error) never leaves an orphan
360 /// slot misbinding the next track's encoder.
361 pub fn create_video_track(&self, id: &str) -> Result<VideoTrack> {
362 let track = self.create_video_track_no_slot(id, self.metadata_enabled)?;
363 self.registry.add_raw_slot(track.native_id());
364 Ok(VideoTrack::wrap(track))
365 }
366
367 /// Create a local video track with per-track [`VideoTrackOptions`] —
368 /// encoder plumbing for this track alone, alongside any number of other
369 /// raw or encoded tracks on the same factory.
370 ///
371 /// ```rust,ignore
372 /// // Pre-encoded: push already-encoded bytes whenever you produce them.
373 /// let screen = factory.create_video_track_with_options("screen", {
374 /// let mut o = VideoTrackOptions::default();
375 /// o.encoder = Some(TrackVideoEncoder::PreEncoded(PreEncodedOptions::new(1920, 1080)));
376 /// o
377 /// })?;
378 /// if let LocalVideoTrack::Encoded(enc) = screen {
379 /// enc.push_encoded_frame(frame);
380 /// }
381 /// ```
382 ///
383 /// The same positional slot-assignment rule as
384 /// [`create_video_track`](Self::create_video_track) applies, and slots are
385 /// likewise reserved only after a successful native creation.
386 ///
387 /// **One pre-encoded / inline track serves exactly one peer connection.**
388 /// Each PeerConnection layers on its own encoder instance, and the
389 /// registry binds them positionally by reservation — a second PC wired to
390 /// the same track finds no reservation and falls back to the builtin
391 /// encoder, which encodes the track's raw pushes as ordinary video
392 /// (grey/dropped output, not a copy of your bitstream). Create one
393 /// encoder-carrying track per PeerConnection instead of sharing.
394 pub fn create_video_track_with_options(
395 &self,
396 id: &str,
397 options: VideoTrackOptions,
398 ) -> Result<LocalVideoTrack> {
399 if options.encoder.is_some() && options.h264_backend.is_some() {
400 return Err(Error::Webrtc(
401 "h264_backend with a custom encoder: the track's bytes come \
402 from your own pipeline — there is no backend to route to"
403 .into(),
404 ));
405 }
406 match options.encoder {
407 None => {
408 let pref = self.h264_backend_pref(options.h264_backend)?;
409 let track = VideoTrack::wrap(self.create_video_track_no_slot(
410 id,
411 self.track_metadata_enabled(options.frame_metadata),
412 )?);
413 self.registry
414 .add_raw_slot_with_backend(track.native_id(), pref);
415 Ok(LocalVideoTrack::Raw(track))
416 }
417 Some(TrackVideoEncoder::PreEncoded(o)) => {
418 let track = VideoTrack::wrap(self.create_video_track_no_slot(
419 id,
420 self.track_metadata_enabled(options.frame_metadata),
421 )?);
422 let (queue, feedback) = self.registry.add_encoded_slot(track.native_id());
423 Ok(LocalVideoTrack::Encoded(EncodedVideoTrack::new(
424 track, queue, feedback, o.width, o.height,
425 )))
426 }
427 Some(TrackVideoEncoder::Inline(cb)) => {
428 let track = VideoTrack::wrap(self.create_video_track_no_slot(
429 id,
430 self.track_metadata_enabled(options.frame_metadata),
431 )?);
432 let feedback = self.registry.add_inline_slot(track.native_id(), cb);
433 crate::encoded::register_feedback_binding(track.native_id(), &feedback);
434 Ok(LocalVideoTrack::Raw(track))
435 }
436 }
437 }
438
439 /// Validate an explicit [`H264Backend`] choice against what this build /
440 /// this factory can actually serve, and map it to the slot preference.
441 fn h264_backend_pref(
442 &self,
443 backend: Option<H264Backend>,
444 ) -> Result<crate::encoded::H264BackendPref> {
445 use crate::encoded::H264BackendPref as Pref;
446 match backend {
447 None => Ok(Pref::Auto),
448 Some(H264Backend::VideoToolbox) if !HAVE_VIDEO_TOOLBOX => Err(Error::Webrtc(
449 "H264Backend::VideoToolbox is only available on Apple platforms".into(),
450 )),
451 Some(H264Backend::VideoToolbox) => Ok(Pref::VideoToolbox),
452 #[cfg(feature = "openh264")]
453 Some(H264Backend::OpenH264) if !self.openh264_registered => Err(Error::Webrtc(
454 "H264Backend::OpenH264 requires registering the library first \
455 (PeerConnectionFactory::builder().with_openh264(path))"
456 .into(),
457 )),
458 #[cfg(feature = "openh264")]
459 Some(H264Backend::OpenH264) => Ok(Pref::OpenH264),
460 }
461 }
462
463 /// Effective frame-metadata on a track: the factory kill switch is a
464 /// process-wide **off** that a per-track `Some(true)` can never
465 /// re-enable — `None` simply defers to the factory's setting.
466 fn track_metadata_enabled(&self, track_flag: Option<bool>) -> bool {
467 self.metadata_enabled && track_flag.unwrap_or(true)
468 }
469
470 /// Shared native side of [`create_video_track`](Self::create_video_track):
471 /// the strict FFI call, **without** touching the registry — callers
472 /// reserve their slot *after* this succeeds (so a failed create never
473 /// leaves an orphan positional slot behind).
474 fn create_video_track_no_slot(&self, id: &str, metadata_enabled: bool) -> Result<Track> {
475 let cid = CString::new(id).map_err(|_| Error::Webrtc("id contains a NUL byte".into()))?;
476 let raw = unsafe {
477 reactor_webrtc_sys::reactor_webrtc_video_track_create(self.handle.raw(), cid.as_ptr())
478 };
479 if raw.is_null() {
480 return Err(Error::Webrtc("video track creation returned null".into()));
481 }
482 Ok(Track::from_raw_with_registry(
483 raw,
484 MediaKind::Video,
485 self.handle(),
486 false,
487 metadata_enabled,
488 Some(Arc::clone(&self.registry)),
489 ))
490 }
491
492 /// Create a local audio track. Its samples come from this factory's ADM —
493 /// feed it with [`PeerConnectionFactory::push_audio_frame`]. For per-track
494 /// sources and processing constraints use
495 /// [`create_audio_track_with_options`](Self::create_audio_track_with_options).
496 pub fn create_audio_track(&self, id: &str) -> Result<AudioTrack> {
497 self.create_audio_track_with_options(id, AudioTrackOptions::default())
498 }
499
500 /// Create a local audio track with a per-track audio source, independent of
501 /// the factory ADM. Feed samples with [`Track::push_pcm`].
502 ///
503 /// **Deprecated:** retained for 0.12 source compatibility; use
504 /// [`create_audio_track_with_options`](Self::create_audio_track_with_options)
505 /// with [`AudioTrackSource::LocalPush`] instead. Accepting that the type
506 /// is now `AudioTrack` — push helpers didn't change names, though; calls
507 /// typed against [`Track`] adapt trivially (`let track: AudioTrack`).
508 #[deprecated(note = "use create_audio_track_with_options with AudioTrackSource::LocalPush")]
509 pub fn create_audio_track_with_local_source(&self, id: &str) -> Result<AudioTrack> {
510 self.create_audio_track_with_options(
511 id,
512 AudioTrackOptions {
513 source: AudioTrackSource::LocalPush,
514 ..Default::default()
515 },
516 )
517 }
518
519 /// Create a local audio track with per-track [`AudioTrackOptions`] —
520 /// choose the source (factory ADM vs independent push source) and the
521 /// per-source processing constraints (AEC / noise suppression / AGC /
522 /// high-pass), alongside any number of other audio tracks on the same
523 /// factory — the mic + music scenario, or different audio per peer.
524 ///
525 /// For [`AudioTrackSource::LocalPush`] tracks, feed samples with
526 /// [`Track::push_pcm`]; the returned [`Track`] handles both cases (push
527 /// methods are documented no-ops where they don't apply).
528 pub fn create_audio_track_with_options(
529 &self,
530 id: &str,
531 options: AudioTrackOptions,
532 ) -> Result<AudioTrack> {
533 let cid = CString::new(id).map_err(|_| Error::Webrtc("id contains a NUL byte".into()))?;
534 let tri = |v: Option<bool>| v.map_or(-1, |b| b as c_int);
535 let sys_opts = reactor_webrtc_sys::ReactorAudioTrackOptions {
536 source: match options.source {
537 AudioTrackSource::Adm => 0,
538 AudioTrackSource::LocalPush => 1,
539 },
540 echo_cancellation: tri(options.echo_cancellation),
541 noise_suppression: tri(options.noise_suppression),
542 auto_gain_control: tri(options.auto_gain_control),
543 high_pass_filter: tri(options.high_pass_filter),
544 ..Default::default()
545 };
546 let raw = unsafe {
547 reactor_webrtc_sys::reactor_webrtc_audio_track_create(
548 self.handle.raw(),
549 cid.as_ptr(),
550 &sys_opts,
551 )
552 };
553 if raw.is_null() {
554 return Err(Error::Webrtc("audio track creation returned null".into()));
555 }
556 Ok(AudioTrack::wrap(Track::from_raw(
557 raw,
558 MediaKind::Audio,
559 self.handle(),
560 false,
561 true, // metadata is a video feature; audio stays neutral
562 )))
563 }
564
565 /// Feed interleaved i16 PCM to the (synthetic) ADM, shared by all local
566 /// audio tracks. Typically called with ~10ms blocks (e.g. 480 frames @
567 /// 48kHz). No-op with the platform ADM.
568 pub fn push_audio_frame(&self, pcm: &[i16], sample_rate: u32, channels: u32) {
569 let channels = channels.max(1);
570 let samples_per_channel = (pcm.len() / channels as usize) as c_int;
571 unsafe {
572 reactor_webrtc_sys::reactor_webrtc_factory_push_audio_frame(
573 self.handle.raw(),
574 pcm.as_ptr(),
575 samples_per_channel,
576 sample_rate as c_int,
577 channels as c_int,
578 );
579 }
580 }
581
582 /// Enable/disable the synthetic ADM's playout pump (no-op for the platform
583 /// ADM). Disable to stay silent in send-only / headless scenarios.
584 pub fn set_adm_playout_enabled(&self, enabled: bool) {
585 unsafe {
586 reactor_webrtc_sys::reactor_webrtc_factory_set_adm_playout_enabled(
587 self.handle.raw(),
588 enabled as c_int,
589 )
590 }
591 }
592}