Skip to main content

reactor_webrtc/
media.rs

1//! Media tracks and decoded frames.
2//!
3//! A [`Track`] wraps a native media track (local or remote, audio or video).
4//! Local tracks created via [`crate::PeerConnectionFactory::create_video_track`]
5//! / [`create_audio_track`](crate::PeerConnectionFactory::create_audio_track)
6//! can push frames; remote tracks delivered to
7//! [`PeerConnectionObserver::on_track`](crate::PeerConnectionObserver) can have
8//! a frame sink attached. Audio send is via the factory ADM
9//! ([`crate::PeerConnectionFactory::push_audio_frame`]).
10
11use crate::{Error, FactoryHandle, Result};
12use std::collections::{HashMap, VecDeque};
13use std::ffi::c_void;
14use std::os::raw::c_int;
15use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
16use std::sync::{Arc, Mutex};
17
18const SENDER_META_CAP: usize = 300;
19
20/// Audio vs. video.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum MediaKind {
23    Audio,
24    Video,
25    Unknown,
26}
27
28impl MediaKind {
29    pub(crate) fn from_raw(kind: c_int) -> Self {
30        match kind {
31            0 => MediaKind::Audio,
32            1 => MediaKind::Video,
33            _ => MediaKind::Unknown,
34        }
35    }
36}
37
38/// A decoded video frame, borrowed for the duration of the callback. The buffer
39/// is BGRA (`width*height*4`, B-G-R-A); copy it if you need to keep it.
40pub struct VideoFrame<'a> {
41    pub bgra: &'a [u8],
42    pub width: u32,
43    pub height: u32,
44    /// Metadata attached by the sender via
45    /// [`Track::push_video_frame_with_metadata`], decoded from the packet trailer.
46    ///
47    /// `None` when no trailer was present — which includes every frame from a peer
48    /// that did not negotiate the capability, and every frame on a connection built
49    /// with [`RtcConfiguration::frame_metadata`](crate::RtcConfiguration) off.
50    pub metadata: Option<crate::metadata::FrameMetadata>,
51}
52
53/// A chunk of decoded audio, borrowed for the duration of the callback:
54/// interleaved signed 16-bit PCM (`frames * channels` samples).
55pub struct AudioFrame<'a> {
56    pub pcm: &'a [i16],
57    pub sample_rate: u32,
58    pub channels: u32,
59    pub frames: u32,
60}
61
62/// Where an audio track's samples come from
63/// ([`create_audio_track_with_options`](crate::PeerConnectionFactory::create_audio_track_with_options)).
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum AudioTrackSource {
67    /// The factory's ADM: the platform microphone + speaker (real devices on
68    /// desktop) or the shared synthetic pipe fed by
69    /// [`PeerConnectionFactory::push_audio_frame`](crate::PeerConnectionFactory::push_audio_frame).
70    /// Every ADM-sourced track carries the **same** signal.
71    ///
72    /// There is exactly one ADM per factory — per-track choice is "tap it or
73    /// bypass it", not "which ADM".
74    ///
75    /// On the **platform** device, the render path is fully automatic: every
76    /// inbound audio track decodes to the speaker by default — you cannot
77    /// route one remote track to the speaker and another elsewhere through
78    /// plumbing. `on_frame` taps them, it doesn't divert. Nothing plays
79    /// automatically through the synthetic device.
80    #[default]
81    Adm,
82    /// A per-track push source, fully independent of the ADM: feed it with
83    /// [`Track::push_pcm`]. The way to send per-track audio — e.g. a music
84    /// track next to the mic — or to route different audio to different
85    /// peers from one factory.
86    LocalPush,
87}
88
89/// Options for
90/// [`PeerConnectionFactory::create_audio_track_with_options`].
91///
92/// All fields optional; `Default::default()` produces exactly what
93/// [`PeerConnectionFactory::create_audio_track`] produces. The struct is
94/// `#[non_exhaustive]` — construct via `Default` and assign fields.
95///
96/// The processing flags are **constraints refining what this track asks of
97/// the factory's APM chain** (the one
98/// [`PeerConnectionFactoryBuilder::with_apm`] built — default: none):
99/// `None` inherits the chain's state, `Some(v)` forces the stage on/off for
100/// this track's source. They act **one level above the flags list** — with
101/// libwebrtc semantics this crate honors: a flag set per constraint only
102/// takes effect by engaging part of an APM the factory already has.
103/// **On a factory without an APM (the default) they are inert** — there is
104/// nothing in the capture pipeline for them to toggle; that is not a bug,
105/// the DSP chain genuinely doesn't exist. They constrain the capture/send
106/// side only — received audio is never processed.
107///
108/// A [`LocalPush`](AudioTrackSource::LocalPush) source bypasses the ADM
109/// device path entirely, so these flags only ever matter on
110/// [`AudioTrackSource::Adm`] — pushed audio has no echo to cancel.
111///
112/// ```rust,ignore
113/// let mic = factory.create_audio_track_with_options("mic", {
114///     let mut o = AudioTrackOptions::default();
115///     o.echo_cancellation = Some(true);      // target the APM chain
116///     o.noise_suppression = Some(true);
117///     o
118/// })?;
119/// let music = factory.create_audio_track_with_options("music", {
120///     let mut o = AudioTrackOptions::default();
121///     o.source = AudioTrackSource::LocalPush;
122///     o
123/// })?;
124/// ```
125#[derive(Default)]
126#[non_exhaustive]
127pub struct AudioTrackOptions {
128    /// Where this track's samples come from. Default: [`AudioTrackSource::Adm`].
129    pub source: AudioTrackSource,
130    /// AEC3 echo cancellation for this track's source.
131    pub echo_cancellation: Option<bool>,
132    /// Noise suppression for this track's source.
133    pub noise_suppression: Option<bool>,
134    /// Automatic gain control for this track's source.
135    pub auto_gain_control: Option<bool>,
136    /// High-pass filter for this track's source.
137    pub high_pass_filter: Option<bool>,
138}
139
140type VideoSinkCb = Box<dyn for<'a> FnMut(VideoFrame<'a>, std::time::Duration) + Send>;
141type AudioSinkCb = Box<dyn for<'a> FnMut(AudioFrame<'a>) + Send>;
142
143// sender: HashMap keyed by capture_time_ms (no-erase for simulcast), evicted
144// FIFO when the 300-entry cap is reached.
145type SenderMetaMap = Mutex<(HashMap<i64, crate::metadata::FrameMetadata>, VecDeque<i64>)>;
146
147/// A [`Track`]'s outgoing metadata, keyed by the frame's capture timestamp.
148///
149/// Registered in [`crate::sender_meta`] under the native track's identity so that
150/// `set_remote_description` can find it when it installs the sender transform.
151#[derive(Default)]
152pub(crate) struct CaptureTimeMeta(SenderMetaMap);
153
154impl crate::sender_meta::SenderMetaSource for CaptureTimeMeta {
155    fn take(&self, frame: &crate::encoded::EncodedFrame) -> Option<crate::metadata::FrameMetadata> {
156        // Look up rather than remove, and only with a real timestamp: simulcast
157        // layers of one frame share capture_time_ms and each must find the entry.
158        if frame.capture_time_ms <= 0 {
159            return None;
160        }
161        self.0
162            .lock()
163            .ok()
164            .and_then(|g| g.0.get(&frame.capture_time_ms).cloned())
165    }
166}
167
168// receiver: queue written by the strip transform and drained by video_sink_tramp,
169// which pairs by SSRC and RTP timestamp so a frame dropped before render can't
170// shift it.
171use crate::sender_meta::ReceiverMetaQueue;
172
173/// How long a remote video track's sink took to hand its decoded frames on,
174/// added up since the sink was attached ([`VideoTrack::sink_timings`]).
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
176pub struct VideoSinkTimings {
177    /// From libwebrtc handing a decoded frame to the sink, before its conversion
178    /// to BGRA, until the frame reaches the `on_frame` closure.
179    pub convert: std::time::Duration,
180    /// The frames the closure received.
181    pub frames: u64,
182}
183
184/// A sink's totals, updated and read as one pair so a reading's time and count
185/// describe the same frames.
186#[derive(Default)]
187struct SinkTimingTotals(Mutex<VideoSinkTimings>);
188
189// Heap-pinned sink state behind the C userdata pointer.
190struct VideoSinkState {
191    cb: Mutex<VideoSinkCb>,
192    // Shared with Track::video_sink_timings, which sink_timings reads.
193    timings: Arc<SinkTimingTotals>,
194    // The track's inbound metadata queue, always present. It is simply empty when
195    // no strip transform is installed or the sender attached no trailer, so there
196    // is no state to switch on and no back-fill ordering to get right.
197    receiver_meta: Arc<ReceiverMetaQueue>,
198}
199struct AudioSinkState {
200    cb: Mutex<AudioSinkCb>,
201}
202
203extern "C" fn video_sink_tramp(
204    ud: *mut c_void,
205    bgra: *const u8,
206    width: c_int,
207    height: c_int,
208    ssrc: u32,
209    rtp_timestamp: u32,
210    convert_ns: i64,
211) {
212    // The native sink timed its own share; this one adds the work here, until
213    // the callback, on a clock as fine.
214    let entered = std::time::Instant::now();
215    let st = unsafe { &*(ud as *const VideoSinkState) };
216    let len = (width as usize) * (height as usize) * 4;
217    let slice = unsafe { std::slice::from_raw_parts(bgra, len) };
218    let metadata = crate::sender_meta::take_receiver_meta(&st.receiver_meta, ssrc, rtp_timestamp);
219    if let Ok(mut cb) = st.cb.lock() {
220        let here_ns = u64::try_from(entered.elapsed().as_nanos()).unwrap_or(u64::MAX);
221        let convert = (convert_ns.max(0) as u64).saturating_add(here_ns);
222        if let Ok(mut totals) = st.timings.0.lock() {
223            totals.convert = totals
224                .convert
225                .saturating_add(std::time::Duration::from_nanos(convert));
226            totals.frames += 1;
227        }
228        cb(
229            VideoFrame {
230                bgra: slice,
231                width: width as u32,
232                height: height as u32,
233                metadata,
234            },
235            std::time::Duration::from_nanos(convert),
236        );
237    }
238}
239
240extern "C" fn audio_sink_tramp(
241    ud: *mut c_void,
242    pcm: *const i16,
243    sample_rate: c_int,
244    channels: c_int,
245    frames: c_int,
246) {
247    let st = unsafe { &*(ud as *const AudioSinkState) };
248    let len = (frames as usize) * (channels as usize);
249    let slice = unsafe { std::slice::from_raw_parts(pcm, len) };
250    if let Ok(mut cb) = st.cb.lock() {
251        cb(AudioFrame {
252            pcm: slice,
253            sample_rate: sample_rate as u32,
254            channels: channels as u32,
255            frames: frames as u32,
256        });
257    }
258}
259
260/// A media track (local or remote). Dropping it detaches any sink and releases
261/// the native track.
262pub struct Track {
263    raw: *mut reactor_webrtc_sys::MediaStreamTrack,
264    kind: MediaKind,
265    // Mutex, not a plain field, so on_video_frame/on_audio_frame can take &self:
266    // Transceiver.set_track needs to share a Track behind an Arc (for the Python
267    // binding's spawn_blocking future), which rules out ever taking &mut self.
268    video_sink: Mutex<Option<Box<VideoSinkState>>>,
269    // The totals of the video sink attached last, apart from `video_sink`:
270    // attaching holds that lock while it waits on the worker thread, which runs
271    // the frame callbacks, so a callback reading the totals through it could
272    // deadlock. This lock is only ever held to swap or clone the Arc.
273    video_sink_timings: Mutex<Arc<SinkTimingTotals>>,
274    audio_sink: Mutex<Option<Box<AudioSinkState>>>,
275    // Always present: push_video_frame_with_metadata fills it, and the sender
276    // transform the peer connection installs after negotiation reads it. There is
277    // nothing to switch on — an empty map simply yields no metadata.
278    sender_meta: Arc<CaptureTimeMeta>,
279    // Identity of the native track, which is how `sender_meta` is registered and
280    // therefore how a transceiver finds it. Cached because Drop needs it.
281    native_id: usize,
282    // Always present, shared with VideoSinkState and with the strip transform the
283    // peer connection installs after negotiation.
284    receiver_meta: Arc<ReceiverMetaQueue>,
285    // Monotonic per-track frame counter; wraps from u64::MAX to 1 (0 = unset).
286    frame_counter: AtomicU64,
287    // Last capture_ms allocated by alloc_send_capture_us; ensures strictly-
288    // increasing keys in the sender map even when two pushes land in the
289    // same millisecond.
290    last_send_ms: AtomicI64,
291    // Keeps the factory's signaling/worker/network threads alive for as long
292    // as this track exists — destroying a track dispatches onto them, whether
293    // the track is a local one this factory produced or a remote one the
294    // observer delivered over a connection this factory created.
295    _factory: Arc<FactoryHandle>,
296    // Tracks delivered by the observer are inbound-only: pushing frames onto
297    // them is an error (surfaced by the media wrappers).
298    is_remote: bool,
299    // Per-track frame-metadata switch: off tracks never get a trailer writer
300    // installed and drop `user_data` on pushes (the frame itself still flows).
301    // Defaults from the factory kill switch; overridable per track via
302    // `VideoTrackOptions::frame_metadata`.
303    metadata_enabled: bool,
304    // The factory's encoder registry for local tracks — dropped slots must be
305    // retracted when the track dies ([`EncoderRegistry::retract`]). None for
306    // remote tracks: their slots never existed.
307    registry: Option<Arc<crate::encoded::EncoderRegistry>>,
308}
309
310// SAFETY: the native track is internally thread-safe; sink callbacks and sink
311// (re)attachment are both guarded by a Mutex, and the other `&self` methods
312// (frame push) go through WebRTC's internally-locked broadcaster, so a
313// `&Track` may be shared and used concurrently across threads.
314unsafe impl Send for Track {}
315unsafe impl Sync for Track {}
316
317impl Track {
318    pub(crate) fn from_raw(
319        raw: *mut reactor_webrtc_sys::MediaStreamTrack,
320        kind: MediaKind,
321        factory: Arc<FactoryHandle>,
322        is_remote: bool,
323        metadata_enabled: bool,
324    ) -> Self {
325        Self::from_raw_with_registry(raw, kind, factory, is_remote, metadata_enabled, None)
326    }
327
328    pub(crate) fn from_raw_with_registry(
329        raw: *mut reactor_webrtc_sys::MediaStreamTrack,
330        kind: MediaKind,
331        factory: Arc<FactoryHandle>,
332        is_remote: bool,
333        metadata_enabled: bool,
334        registry: Option<Arc<crate::encoded::EncoderRegistry>>,
335    ) -> Self {
336        let sender_meta = Arc::new(CaptureTimeMeta::default());
337        let receiver_meta: Arc<ReceiverMetaQueue> = Arc::new(ReceiverMetaQueue::default());
338        let native_id = unsafe { reactor_webrtc_sys::reactor_webrtc_media_stream_track_id(raw) };
339        // Video only: audio carries no metadata, so registering it would just put
340        // an entry in the way of nothing.
341        if kind == MediaKind::Video {
342            let source: Arc<dyn crate::sender_meta::SenderMetaSource> = sender_meta.clone();
343            crate::sender_meta::register(native_id, &source);
344            crate::sender_meta::register_receiver(native_id, &receiver_meta);
345            crate::sender_meta::register_allowed(native_id, metadata_enabled);
346        }
347        Self {
348            raw,
349            kind,
350            video_sink: Mutex::new(None),
351            video_sink_timings: Mutex::default(),
352            audio_sink: Mutex::new(None),
353            sender_meta,
354            native_id,
355            receiver_meta,
356            frame_counter: AtomicU64::new(0),
357            last_send_ms: AtomicI64::new(0),
358            _factory: factory,
359            is_remote,
360            metadata_enabled,
361            registry,
362        }
363    }
364
365    /// Whether this track came from the remote side of a peer connection.
366    /// Remote tracks only receive — pushing frames onto one is an error.
367    pub fn is_remote(&self) -> bool {
368        self.is_remote
369    }
370
371    /// Whether this track carries a frame-metadata trailer (`user_data`
372    /// survives the push and reaches the receiver).
373    pub(crate) fn metadata_enabled(&self) -> bool {
374        self.metadata_enabled
375    }
376
377    pub(crate) fn raw(&self) -> *mut reactor_webrtc_sys::MediaStreamTrack {
378        self.raw
379    }
380
381    /// Identity of the native track, comparable with
382    /// `reactor_webrtc_rtp_transceiver_sender_track_id`.
383    pub(crate) fn native_id(&self) -> usize {
384        self.native_id
385    }
386
387    pub fn kind(&self) -> MediaKind {
388        self.kind
389    }
390
391    /// Push a BGRA frame captured *now* — the untimestamped native path.
392    /// Called by [`VideoTrack::push_frame`]. No-op for non-video tracks.
393    pub(crate) fn push_video_frame_now(&self, bgra: &[u8], width: u32, height: u32) {
394        if self.kind != MediaKind::Video {
395            return;
396        }
397        unsafe {
398            reactor_webrtc_sys::reactor_webrtc_video_track_push_frame(
399                self.raw,
400                bgra.as_ptr(),
401                width as c_int,
402                height as c_int,
403            );
404        }
405    }
406
407    /// Shared body of [`VideoTrack::push_frame_with_metadata` and `_at`]:
408    /// `capture_time_us` is the caller's declared time, or `None` for the clock.
409    ///
410    /// `capture_time_us` does two jobs, and they want different things from it.
411    /// The trailer carries it **exactly**, so the receiver reads the number the
412    /// caller put on the frame — that is the caller's own timeline, and rounding
413    /// it would be this library editing a value it does not own. Playout
414    /// synchronisation gets it too, so a frame the model annotated still lines up
415    /// with the audio produced alongside it: the trailer must not cost the caller
416    /// its timestamp.
417    ///
418    /// Synchronisation is where the rounding lives. The trailer is matched to its
419    /// frame by capture millisecond, so two frames stamped inside the same
420    /// millisecond would collide; the second is nudged to the following one —
421    /// far below the resolution any synchronisation cares about, and only
422    /// reachable above 1000 fps. Neither the truncation nor the nudge reaches the
423    /// trailer, so the value delivered is the value passed, and several tracks
424    /// pushed with one stamp deliver that one stamp.
425    pub(crate) fn push_metadata_frame(
426        &self,
427        bgra: &[u8],
428        width: u32,
429        height: u32,
430        user_data: &[u8],
431        capture_time_us: Option<i64>,
432    ) {
433        if self.kind != MediaKind::Video {
434            return;
435        }
436        // One reading, two uses. The trailer carries it exactly as it is, because
437        // that is the caller's own timeline and rounding it would be this library
438        // editing a number it does not own. The join key and the capture
439        // timestamp libwebrtc gets are derived from the same value, and the
440        // millisecond truncation and uniqueness nudge they need stop there.
441        let declared = capture_time_us.unwrap_or_else(crate::time_micros);
442        let capture_us = self.alloc_send_capture_us(Some(declared));
443        let meta = crate::metadata::FrameMetadata {
444            frame_id: self.next_frame_id(),
445            // The wire field is unsigned and 0 is its "unset", so a negative
446            // reading has nowhere to land and says nothing instead of a wrapped
447            // number that would read as 584 thousand years from now.
448            capture_time_us: declared.max(0) as u64,
449            user_data: user_data.to_vec(),
450        };
451        self.insert_sender_meta(capture_us / 1000, meta);
452        self.push_video_frame_ts(bgra, width, height, capture_us);
453    }
454
455    /// Return a strictly-increasing capture timestamp in microseconds for use
456    /// as the sender-map key and `push_video_frame_ts` argument.
457    ///
458    /// `requested` is the caller's own capture time, or `None` to read the
459    /// clock. Either way two frames in the same millisecond would produce the
460    /// same `capture_ms` key and share one metadata entry, so the counter
461    /// advances by 1 ms when the source hasn't moved, guaranteeing unique keys
462    /// regardless of call frequency.
463    pub(crate) fn alloc_send_capture_us(&self, requested: Option<i64>) -> i64 {
464        let raw_ms = match requested {
465            Some(us) => us / 1000,
466            None => crate::time_micros() / 1000,
467        };
468        next_unique_capture_ms(&self.last_send_ms, raw_ms) * 1000
469    }
470
471    /// Advance the per-track frame counter and return the new value (1-based,
472    /// wraps from `u64::MAX` back to 1; 0 is reserved to mean "unset").
473    pub(crate) fn next_frame_id(&self) -> u64 {
474        let raw = self.frame_counter.fetch_add(1, Ordering::Relaxed);
475        if raw == u64::MAX {
476            1
477        } else {
478            raw + 1
479        }
480    }
481
482    /// Insert a metadata entry keyed by `capture_ms` into the sender map.
483    ///
484    /// Always accepted, whether or not a sender transform is installed and whether
485    /// or not the peer declared support. The map is capacity-bounded and evicts in
486    /// insertion order, so entries nobody reads are reclaimed rather than
487    /// accumulated.
488    pub(crate) fn insert_sender_meta(&self, capture_ms: i64, meta: crate::metadata::FrameMetadata) {
489        if let Ok(mut guard) = self.sender_meta.0.lock() {
490            let (ref mut hmap, ref mut order) = *guard;
491            if order.len() >= SENDER_META_CAP {
492                if let Some(old_key) = order.pop_front() {
493                    hmap.remove(&old_key);
494                }
495            }
496            // No-erase on insert: simulcast layers sharing the same
497            // capture_time_ms must all find the same entry.
498            hmap.entry(capture_ms).or_insert(meta);
499            order.push_back(capture_ms);
500        }
501    }
502
503    /// Push a BGRA frame with an explicit WebRTC capture timestamp (µs).
504    /// Used internally to correlate metadata with the sender FrameTransform.
505    pub(crate) fn push_video_frame_ts(
506        &self,
507        bgra: &[u8],
508        width: u32,
509        height: u32,
510        capture_us: i64,
511    ) {
512        unsafe {
513            reactor_webrtc_sys::reactor_webrtc_video_track_push_frame_ts(
514                self.raw,
515                bgra.as_ptr(),
516                width as c_int,
517                height as c_int,
518                capture_us,
519            );
520        }
521    }
522
523    /// Register a decoded-frame sink, replacing any previous one. Called by
524    /// [`VideoTrack::on_frame`].
525    ///
526    /// [`VideoFrame::metadata`] is populated whenever the sender included a
527    /// metadata trailer and the peer connection installed the strip transform —
528    /// which it does automatically, on both peers, once the capability has been
529    /// negotiated. Nothing here has to be arranged for it.
530    pub(crate) fn attach_video_sink(
531        &self,
532        cb: impl for<'a> FnMut(VideoFrame<'a>, std::time::Duration) + Send + 'static,
533    ) {
534        let timings = Arc::new(SinkTimingTotals::default());
535        let state = Box::new(VideoSinkState {
536            cb: Mutex::new(Box::new(cb)),
537            timings: Arc::clone(&timings),
538            receiver_meta: self.receiver_meta.clone(),
539        });
540        let ud = &*state as *const VideoSinkState as *mut c_void;
541        // Held across both the native registration and the Rust-side store: two
542        // concurrent callers must not interleave their FFI call with the other's
543        // store, or the native side can end up pointing at whichever Box the
544        // *other* caller's store just dropped — freed memory the callback thread
545        // then reads. Holding the lock over both serializes each caller's pair of
546        // steps, so the last one to run leaves a native pointer and a stored Box
547        // that always agree with each other.
548        let mut guard = self.video_sink.lock().expect("video_sink mutex poisoned");
549        unsafe {
550            reactor_webrtc_sys::reactor_webrtc_video_track_add_sink(self.raw, ud, video_sink_tramp);
551        }
552        // Drops the previous sink Box, if any, same as an `Option` field replace.
553        *guard = Some(state);
554        *self
555            .video_sink_timings
556            .lock()
557            .expect("video_sink_timings mutex poisoned") = timings;
558    }
559
560    /// Shared body of [`AudioTrack::push_frame`] / [`push_frame_at`]:
561    /// `capture_time_ms = 0` means "unknown / stamp at arrival".
562    pub(crate) fn push_pcm_inner(
563        &self,
564        pcm: &[i16],
565        sample_rate: u32,
566        channels: u32,
567        capture_time_ms: i64,
568    ) -> Result<()> {
569        if channels == 0 {
570            return Err(Error::Webrtc("channels must be at least 1".to_owned()));
571        }
572        if !pcm.len().is_multiple_of(channels as usize) {
573            return Err(Error::Webrtc(format!(
574                "pcm length ({}) is not a multiple of channels ({})",
575                pcm.len(),
576                channels
577            )));
578        }
579        let channels = channels as c_int;
580        let samples_per_channel = (pcm.len() / channels as usize) as c_int;
581        unsafe {
582            reactor_webrtc_sys::reactor_webrtc_audio_track_push_pcm_ts(
583                self.raw,
584                pcm.as_ptr(),
585                samples_per_channel,
586                sample_rate as c_int,
587                channels,
588                capture_time_ms,
589            );
590        }
591        Ok(())
592    }
593
594    /// Register a decoded-PCM sink, replacing any previous one. Called by
595    /// [`AudioTrack::on_frame`].
596    pub(crate) fn attach_audio_sink(
597        &self,
598        cb: impl for<'a> FnMut(AudioFrame<'a>) + Send + 'static,
599    ) {
600        let state = Box::new(AudioSinkState {
601            cb: Mutex::new(Box::new(cb)),
602        });
603        let ud = &*state as *const AudioSinkState as *mut c_void;
604        // Same reasoning as `on_video_frame`: the lock spans both steps so the
605        // two can never interleave across callers.
606        let mut guard = self.audio_sink.lock().expect("audio_sink mutex poisoned");
607        unsafe {
608            reactor_webrtc_sys::reactor_webrtc_audio_track_add_sink(self.raw, ud, audio_sink_tramp);
609        }
610        // Drops the previous sink Box, if any, same as an `Option` field replace.
611        *guard = Some(state);
612    }
613}
614
615impl Drop for Track {
616    fn drop(&mut self) {
617        // Guarded on identity inside deregister: an EncodedVideoTrack wrapping this
618        // track will have replaced the entry with its own FIFO source, and that one
619        // must survive this drop.
620        if self.kind == MediaKind::Video {
621            let source: Arc<dyn crate::sender_meta::SenderMetaSource> = self.sender_meta.clone();
622            crate::sender_meta::deregister(self.native_id, &source);
623            crate::sender_meta::deregister_receiver(self.native_id, &self.receiver_meta);
624            crate::sender_meta::deregister_allowed(self.native_id);
625            crate::encoded::deregister_feedback_binding(self.native_id);
626        }
627        // Video-only: this track may have reserved encoder slots — reclaim
628        // them so nothing else can be routed through the dead lane.
629        if let Some(registry) = &self.registry {
630            if self.kind == MediaKind::Video {
631                registry.retract(self.native_id());
632            }
633        }
634        // Detaches the C++ sink before the sink-state boxes are freed.
635        unsafe { reactor_webrtc_sys::reactor_webrtc_media_stream_track_destroy(self.raw) }
636    }
637}
638
639// ── Media-typed wrappers ─────────────────────────────────────────────────────
640//
641// `Track` is the untyped core: the native handle, lifetime, metadata plumbing
642// and the transceiver/FFI surface. The public frame API lives on these
643// wrappers instead — pushing a video frame onto an audio track is a compile
644// error, not a runtime no-op. Everything derefs to `&Track`, so
645// `transceiver.set_track(&wrapper)` just works.
646
647use std::ops::Deref;
648
649/// A video [`Track`] — local (raw) or remote. `on_frame` receives decoded
650/// video; `push_frame` family feeds a local track (`Err` on a remote track).
651pub struct VideoTrack(Track);
652
653/// An audio [`Track`] — local or remote. `on_frame` receives decoded PCM;
654/// `push_frame` family feeds a local [`AudioTrackSource::LocalPush`] track
655/// (`Err` on a remote track; no-op semantics on ADM-backed locals match the
656/// native ADM).
657pub struct AudioTrack(Track);
658
659impl VideoTrack {
660    pub(crate) fn wrap(track: Track) -> Self {
661        debug_assert_eq!(track.kind(), MediaKind::Video);
662        Self(track)
663    }
664
665    /// Subscribe to decoded frames. Replaces any previous sink. The closure
666    /// runs on a WebRTC thread.
667    pub fn on_frame(&self, mut cb: impl for<'a> FnMut(VideoFrame<'a>) + Send + 'static) {
668        self.0.attach_video_sink(move |frame, _| cb(frame));
669    }
670
671    /// [`on_frame`](Self::on_frame), with each frame's share of
672    /// [`VideoSinkTimings::convert`]: how long it took from libwebrtc handing it
673    /// to the sink until the closure. For a caller adding up its own totals
674    /// alongside that time, so they all count the same frames.
675    pub fn on_frame_timed(
676        &self,
677        cb: impl for<'a> FnMut(VideoFrame<'a>, std::time::Duration) + Send + 'static,
678    ) {
679        self.0.attach_video_sink(cb);
680    }
681
682    /// How long the sink took to hand decoded frames to the [`on_frame`](Self::on_frame)
683    /// closure, added up since it was attached. Zero before any sink is attached.
684    /// It can be called from the closure.
685    pub fn sink_timings(&self) -> VideoSinkTimings {
686        let timings = Arc::clone(
687            &self
688                .0
689                .video_sink_timings
690                .lock()
691                .expect("video_sink_timings mutex poisoned"),
692        );
693        let totals = *timings.0.lock().expect("sink timings mutex poisoned");
694        totals
695    }
696
697    /// Push a BGRA frame (`width*height*4` bytes), captured now — on a local
698    /// track only (a remote track returns `Err`). What a
699    /// [`TrackVideoEncoder::Inline`](crate::TrackVideoEncoder) callback sees on
700    /// its slot comes straight from these pushes.
701    pub fn push_frame(&self, frame: VideoFrame<'_>) -> Result<()> {
702        self.guard_remote()?;
703        self.0
704            .push_video_frame_now(frame.bgra, frame.width, frame.height);
705        Ok(())
706    }
707
708    /// Push a BGRA frame captured at `capture_time_us` ([`time_micros`]'s
709    /// epoch) rather than whenever it reaches the encoder.
710    ///
711    /// A track's RTP timestamp otherwise counts only the samples handed to it,
712    /// which says how much media exists but not when it was captured.
713    /// Supplying the same timestamp used for the audio/video produced
714    /// alongside it is what lets the receiver play them together.
715    pub fn push_frame_at(&self, frame: VideoFrame<'_>, capture_time_us: i64) -> Result<()> {
716        self.guard_remote()?;
717        self.0
718            .push_video_frame_ts(frame.bgra, frame.width, frame.height, capture_time_us);
719        Ok(())
720    }
721
722    /// Push a BGRA frame with arbitrary `user_data` embedded as a protobuf
723    /// trailer, captured now. `frame_id` (monotonic) and `capture_time_us`
724    /// are filled in internally.
725    pub fn push_frame_with_metadata(&self, frame: VideoFrame<'_>, user_data: &[u8]) -> Result<()> {
726        self.guard_remote()?;
727        if self.0.metadata_enabled() {
728            self.0
729                .push_metadata_frame(frame.bgra, frame.width, frame.height, user_data, None);
730        } else {
731            // user_data dropped by contract; the frame itself still flows.
732            self.0
733                .push_video_frame_now(frame.bgra, frame.width, frame.height);
734        }
735        Ok(())
736    }
737
738    /// Same as [`push_frame_with_metadata`](Self::push_frame_with_metadata)
739    /// with the capture time declared by the caller. The trailer carries the
740    /// value **exactly**; the millisecond-uniqueness nudge lives elsewhere and
741    /// never reaches the wire value.
742    pub fn push_frame_with_metadata_at(
743        &self,
744        frame: VideoFrame<'_>,
745        user_data: &[u8],
746        capture_time_us: i64,
747    ) -> Result<()> {
748        self.guard_remote()?;
749        if self.0.metadata_enabled() {
750            self.0.push_metadata_frame(
751                frame.bgra,
752                frame.width,
753                frame.height,
754                user_data,
755                Some(capture_time_us),
756            );
757        } else {
758            // user_data dropped by contract; the frame still flows, stamped.
759            self.0
760                .push_video_frame_ts(frame.bgra, frame.width, frame.height, capture_time_us);
761        }
762        Ok(())
763    }
764
765    fn guard_remote(&self) -> Result<()> {
766        if self.0.is_remote() {
767            return Err(Error::Webrtc(
768                "cannot push frames onto a remote track".to_owned(),
769            ));
770        }
771        Ok(())
772    }
773
774    /// Listen for encoder feedback
775    /// ([`EncoderFeedback::RateUpdate`](crate::EncoderFeedback::RateUpdate)) on
776    /// a track created with [`TrackVideoEncoder::Inline`]. Inline tracks see
777    /// keyframe demands inside the [`RawVideoFrame`] their callback receives
778    /// (`request_key_frame`); the other feedback — rate control from the
779    /// congestion controller — surfaces here. Latest registration wins.
780    ///
781    /// Only meaningful on inline-encoder tracks: returns an error on raw
782    /// (builtin-encoder) tracks, where BWE adaptation is internal to
783    /// libwebrtc, and on remote tracks.
784    pub fn on_encoder_feedback(
785        &self,
786        cb: impl FnMut(crate::encoded::EncoderFeedback) + Send + 'static,
787    ) -> Result<()> {
788        self.guard_remote()?;
789        let Some(listeners) = crate::encoded::feedback_binding(self.0.native_id()) else {
790            return Err(Error::Webrtc(
791                "on_encoder_feedback is only valid on tracks created with                  TrackVideoEncoder::Inline — builtin-encoder tracks adapt internally"
792                    .to_owned(),
793            ));
794        };
795        listeners.set(Box::new(cb));
796        Ok(())
797    }
798}
799
800impl AudioTrack {
801    pub(crate) fn wrap(track: Track) -> Self {
802        debug_assert_eq!(track.kind(), MediaKind::Audio);
803        Self(track)
804    }
805
806    /// Subscribe to decoded PCM. Replaces any previous sink. The closure runs
807    /// on a WebRTC thread.
808    pub fn on_frame(&self, cb: impl for<'a> FnMut(AudioFrame<'a>) + Send + 'static) {
809        self.0.attach_audio_sink(cb);
810    }
811
812    /// Push interleaved i16 PCM, timestamped on arrival. Only meaningful on a
813    /// local [`AudioTrackSource::LocalPush`] track — it's a no-op on
814    /// ADM-backed locals and an `Err` on remote tracks. A remote→relay use
815    /// case passes the received [`AudioFrame`] straight back in — same struct
816    /// both ways.
817    ///
818    /// `pcm.len()` must be `frames * channels`; a partial trailing frame is an
819    /// error. Only one thread should call this at a time for a given track —
820    /// concurrent callers produce interleaved, garbage audio.
821    pub fn push_frame(&self, frame: AudioFrame<'_>) -> Result<()> {
822        self.guard_remote()?;
823        self.0
824            .push_pcm_inner(frame.pcm, frame.sample_rate, frame.channels, 0)
825    }
826
827    /// Push PCM captured at `capture_time_us` ([`time_micros`]'s epoch) rather
828    /// than whenever it reaches the encoder — see
829    /// [`VideoTrack::push_frame_at`] for why the shared stamp matters.
830    pub fn push_frame_at(&self, frame: AudioFrame<'_>, capture_time_us: i64) -> Result<()> {
831        self.guard_remote()?;
832        self.0.push_pcm_inner(
833            frame.pcm,
834            frame.sample_rate,
835            frame.channels,
836            capture_time_us / 1000,
837        )
838    }
839
840    fn guard_remote(&self) -> Result<()> {
841        if self.0.is_remote() {
842            return Err(Error::Webrtc(
843                "cannot push frames onto a remote track".to_owned(),
844            ));
845        }
846        Ok(())
847    }
848}
849
850impl Deref for VideoTrack {
851    type Target = Track;
852    fn deref(&self) -> &Track {
853        &self.0
854    }
855}
856impl Deref for AudioTrack {
857    type Target = Track;
858    fn deref(&self) -> &Track {
859        &self.0
860    }
861}
862
863// SAFETY: wraps a Track; its own fields add no state.
864unsafe impl Send for VideoTrack {}
865unsafe impl Sync for VideoTrack {}
866unsafe impl Send for AudioTrack {}
867unsafe impl Sync for AudioTrack {}
868
869/// A track delivered by [`PeerConnectionObserver::on_track`] — media-typed,
870/// so `RemoteTrack::Video(v).on_frame(...)` and `RemoteTrack::Audio(a)
871/// .on_frame(...)` each take the matching frame type and nothing else.
872pub enum RemoteTrack {
873    /// A remote video track.
874    Video(VideoTrack),
875    /// A remote audio track.
876    Audio(AudioTrack),
877}
878
879impl RemoteTrack {
880    /// Audio vs video.
881    pub fn kind(&self) -> MediaKind {
882        match self {
883            Self::Video(_) => MediaKind::Video,
884            Self::Audio(_) => MediaKind::Audio,
885        }
886    }
887
888    /// `Some` when this is a video track.
889    pub fn as_video(&self) -> Option<&VideoTrack> {
890        match self {
891            Self::Video(t) => Some(t),
892            Self::Audio(_) => None,
893        }
894    }
895
896    /// `Some` when this is an audio track.
897    pub fn as_audio(&self) -> Option<&AudioTrack> {
898        match self {
899            Self::Audio(t) => Some(t),
900            Self::Video(_) => None,
901        }
902    }
903
904    /// Unwrap a video track (panics on audio tracks).
905    pub fn into_video(self) -> VideoTrack {
906        match self {
907            Self::Video(t) => t,
908            Self::Audio(_) => panic!("into_video on an audio track"),
909        }
910    }
911
912    /// Unwrap an audio track (panics on video tracks).
913    pub fn into_audio(self) -> AudioTrack {
914        match self {
915            Self::Audio(t) => t,
916            Self::Video(_) => panic!("into_audio on a video track"),
917        }
918    }
919}
920
921impl VideoFrame<'_> {
922    /// A push-side frame: BGRA pixels, no metadata (outgoing trailers go
923    /// through [`VideoTrack::push_frame_with_metadata(_at)`] with `user_data`).
924    /// The same struct `on_frame` delivers — relay use cases repack nothing.
925    /// `metadata` is populated only on receive.
926    pub const fn new(bgra: &'_ [u8], width: u32, height: u32) -> VideoFrame<'_> {
927        VideoFrame {
928            bgra,
929            width,
930            height,
931            metadata: None,
932        }
933    }
934}
935
936impl AudioFrame<'_> {
937    /// A push-side frame: interleaved i16 PCM + format. `frames` is derived
938    /// (`pcm.len() / channels`) — the push validates it against `pcm.len()`.
939    /// The same struct `on_frame` delivers.
940    pub fn new(pcm: &'_ [i16], sample_rate: u32, channels: u32) -> AudioFrame<'_> {
941        AudioFrame {
942            pcm,
943            sample_rate,
944            channels,
945            frames: if channels == 0 {
946                0
947            } else {
948                (pcm.len() / channels as usize) as u32
949            },
950        }
951    }
952}
953
954/// Claim `raw_ms` as a capture millisecond, advancing past `last` when the
955/// source has not moved.
956///
957/// The sender metadata map is keyed by capture millisecond, so two frames
958/// claiming the same one would share a trailer. Nudging the second forward
959/// keeps keys unique whether the millisecond came from the clock or from a
960/// caller stamping its own capture time.
961fn next_unique_capture_ms(last: &AtomicI64, raw_ms: i64) -> i64 {
962    let mut prev = last.load(Ordering::Relaxed);
963    loop {
964        let next = raw_ms.max(prev + 1);
965        match last.compare_exchange_weak(prev, next, Ordering::SeqCst, Ordering::Relaxed) {
966            Ok(_) => return next,
967            Err(actual) => prev = actual,
968        }
969    }
970}
971
972#[cfg(test)]
973mod tests {
974    use super::*;
975
976    #[test]
977    fn a_caller_timestamp_is_used_as_given() {
978        let last = AtomicI64::new(0);
979        assert_eq!(next_unique_capture_ms(&last, 1_700), 1_700);
980    }
981
982    #[test]
983    fn a_second_frame_in_the_same_millisecond_is_nudged_forward() {
984        let last = AtomicI64::new(0);
985        assert_eq!(next_unique_capture_ms(&last, 1_700), 1_700);
986        assert_eq!(next_unique_capture_ms(&last, 1_700), 1_701);
987        assert_eq!(next_unique_capture_ms(&last, 1_700), 1_702);
988    }
989
990    #[test]
991    fn a_timestamp_past_the_nudge_wins() {
992        let last = AtomicI64::new(0);
993        next_unique_capture_ms(&last, 1_700);
994        next_unique_capture_ms(&last, 1_700);
995        // 40 ms later — a real frame interval — lands where the caller asked.
996        assert_eq!(next_unique_capture_ms(&last, 1_740), 1_740);
997    }
998
999    #[test]
1000    fn keys_stay_unique_across_threads() {
1001        let last = Arc::new(AtomicI64::new(0));
1002        let seen = Arc::new(Mutex::new(Vec::new()));
1003        let mut handles = Vec::new();
1004        for _ in 0..8 {
1005            let last = Arc::clone(&last);
1006            let seen = Arc::clone(&seen);
1007            handles.push(std::thread::spawn(move || {
1008                for _ in 0..100 {
1009                    let ms = next_unique_capture_ms(&last, 5_000);
1010                    seen.lock().unwrap().push(ms);
1011                }
1012            }));
1013        }
1014        for h in handles {
1015            h.join().unwrap();
1016        }
1017        let mut all = seen.lock().unwrap().clone();
1018        all.sort_unstable();
1019        let before = all.len();
1020        all.dedup();
1021        assert_eq!(all.len(), before, "capture milliseconds collided");
1022    }
1023}