Skip to main content

broadcast_common/
stage.rs

1//! [`Stage`] — the incremental-drive contract every streaming stage in the
2//! workspace will adopt (media-plane migration step 1).
3//!
4//! # Why
5//!
6//! Today the workspace has four incompatible "keep feeding me bytes/samples"
7//! APIs, each shaped slightly differently:
8//!
9//! - `transmux::StreamingTsDemux::feed` returns `()` — output is pulled through
10//!   a separate accessor.
11//! - `transmux::StreamingFlvDemux::feed` returns `Result<(), Error>` — but still
12//!   no inline output.
13//! - `transmux::StreamingTsHlsSegmenter::push` returns the completed segment
14//!   inline from the feed call itself.
15//! - `hls_runtime`'s `LlHlsSegmenter` splits draining into two separate
16//!   methods, `take_ready_parts` and `take_ready_segments`.
17//!
18//! None of these agree on whether output comes back from `feed`, from a
19//! separate pull, or from two separate pulls — and none of them carry a clock,
20//! which blocks deadline-driven stages (rate-scheduled SI emission, RTCP
21//! timeout, segment-boundary timers) from ever sharing a driver loop with the
22//! byte-shovelling stages. `Stage` unifies the shape: push input in with
23//! [`Stage::feed`], pull typed output out with [`Stage::poll`] (repeatable,
24//! decoupled from `feed`), signal end-of-input with [`Stage::finish`], and let
25//! time-driven work happen via [`Stage::next_deadline`] / [`Stage::on_deadline`]
26//! — all without requiring a full media IR or any concrete codec type.
27//!
28//! # `In`: a per-implementor input type, not hardcoded bytes
29//!
30//! The container-demux family's real input is bytes (`&[u8]`), but a
31//! sample-consuming stage's real input is a typed `(track_id, Sample)` — there
32//! is no useful byte encoding of a `Sample` that any caller wants, so forcing
33//! `feed(&[u8], _)` on a segmenter would mean either inventing a fake wire
34//! format nobody consumes or silently discarding real structure. [`Stage::In`]
35//! is a generic associated type precisely so each implementor states its own
36//! honest input shape — `&'a [u8]` for byte-stream stages, `(u32, Sample)` for
37//! the segmenters — while [`Stage::Out`]/[`Stage::Error`] stay per-implementor
38//! too. This is what lets one driver loop, generic only over `S: Stage`, span
39//! both families (see `transmux/tests/it/stage.rs`'s `drive` helper).
40//!
41//! This module defines the trait plus its two small supporting types; it does
42//! not migrate any existing implementor (that is the workspace's media-plane
43//! migration step 2 onward — see `docs/superpowers/specs/2026-07-26-media-plane-architecture.md`).
44//!
45//! # Clock: why a clock parameter at all
46//!
47//! An audit of the wider workspace found a clockless `Stage` would only fit the
48//! container-demux family. Several existing drive loops already take a clock
49//! and cannot be expressed without one:
50//!
51//! - `dvb_conformance::ConformanceMonitor::feed(pkt, t: Duration)` — TR 101 290
52//!   timing indicators need packet arrival time.
53//! - `mpeg_ts::mux::SiMux::poll_into(now: Duration, out)` — PSI/SI re-emission
54//!   is rate-scheduled, not event-driven.
55//! - `media_doctor::WatchState::feed_datagram(payload, clock: Duration)` —
56//!   datagram-loss/jitter watch state needs wall-clock deltas.
57//!
58//! So the clock is on the trait from the start, not bolted on later.
59//!
60//! # `Timestamp`, not `std::time::Instant`
61//!
62//! `broadcast-common` is `#![no_std]` without the `std` feature, and
63//! `std::time::Instant` cannot exist in that build. [`Timestamp`] is a plain
64//! `u64` nanosecond count from an epoch the *driver* chooses (not the stage) —
65//! nanoseconds because milliseconds are the wrong unit for SRT pacing and
66//! catastrophically wrong for ST 2110-21 timing. A driver that does have `std`
67//! can derive one from a pair of `Instant`s via [`Timestamp::from_instant`].
68//! All arithmetic on `Timestamp` saturates instead of panicking, since a stage
69//! must never crash on a clock that runs backwards or wraps.
70
71use core::time::Duration;
72
73/// Monotonic nanoseconds from an arbitrary epoch chosen by the driver.
74///
75/// Nanoseconds, not milliseconds: milliseconds are too coarse for SRT pacing
76/// and catastrophically wrong for ST 2110-21. The epoch (what `0` means) is
77/// entirely up to whatever is driving the [`Stage`] — stages must treat
78/// `Timestamp` as an opaque, monotonically non-decreasing counter and never
79/// assume it relates to wall-clock time.
80///
81/// All arithmetic saturates rather than panicking: a [`Stage`] must not crash
82/// because a driver's clock underflowed or overflowed.
83#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Default)]
84pub struct Timestamp(pub u64);
85
86impl Timestamp {
87    /// The zero timestamp — whatever epoch the driver has chosen.
88    pub const ZERO: Timestamp = Timestamp(0);
89
90    /// Construct from a raw nanosecond count.
91    pub const fn from_nanos(nanos: u64) -> Self {
92        Timestamp(nanos)
93    }
94
95    /// The raw nanosecond count since the driver's chosen epoch.
96    pub const fn as_nanos(self) -> u64 {
97        self.0
98    }
99
100    /// `self - other`, saturating at zero instead of underflowing.
101    pub const fn saturating_sub(self, other: Timestamp) -> Duration {
102        Duration::from_nanos(self.0.saturating_sub(other.0))
103    }
104
105    /// `self + nanos`, saturating at `u64::MAX` instead of overflowing.
106    pub const fn saturating_add_nanos(self, nanos: u64) -> Self {
107        Timestamp(self.0.saturating_add(nanos))
108    }
109
110    /// `self + duration`, saturating at `u64::MAX` instead of overflowing.
111    ///
112    /// A `Duration` in excess of `u64::MAX` nanoseconds saturates the same way.
113    pub fn saturating_add(self, duration: Duration) -> Self {
114        let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
115        self.saturating_add_nanos(nanos)
116    }
117}
118
119#[cfg(feature = "std")]
120impl Timestamp {
121    /// Derive a [`Timestamp`] from a `std::time::Instant` pair: `now - base`,
122    /// expressed as nanoseconds since `base`.
123    ///
124    /// `base` is whatever fixed instant the driver picked as its epoch (e.g.
125    /// "when this stage was constructed"); every subsequent call converts an
126    /// `Instant` to a `Timestamp` on the same epoch. This is a `std`-only
127    /// convenience so `std` callers are not forced to hand-roll the
128    /// subtraction; `no_std` drivers construct `Timestamp` directly.
129    pub fn from_instant(base: std::time::Instant, now: std::time::Instant) -> Self {
130        let elapsed = now.saturating_duration_since(base);
131        let nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
132        Timestamp(nanos)
133    }
134}
135
136/// A stage's hint about how much more input it can usefully accept right now.
137///
138/// This is advisory backpressure, not enforcement: a driver may call
139/// [`Stage::feed`] with more than `want_bytes`, or while `saturated` is `true`,
140/// and a well-behaved stage must still handle it correctly (buffering,
141/// erroring, or blocking as its own contract dictates) — `Demand` only lets a
142/// cooperative driver avoid doing so needlessly.
143#[non_exhaustive]
144#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
145pub struct Demand {
146    /// How many more bytes this stage would like handed to [`Stage::feed`]
147    /// before it is called again. `0` carries no meaning beyond "no
148    /// particular preference"; it is not a demand for zero bytes.
149    pub want_bytes: usize,
150    /// `true` if the stage is currently full and would prefer not to receive
151    /// more input until it has been polled / has had time to drain.
152    pub saturated: bool,
153}
154
155impl Demand {
156    /// Build a [`Demand`] requesting `want_bytes` more input, not saturated.
157    pub const fn new(want_bytes: usize) -> Self {
158        Demand {
159            want_bytes,
160            saturated: false,
161        }
162    }
163
164    /// A [`Demand`] indicating the stage currently wants no more input.
165    pub const fn saturated() -> Self {
166        Demand {
167            want_bytes: 0,
168            saturated: true,
169        }
170    }
171
172    /// [`Demand::saturated`] when `full`, otherwise the "no preference"
173    /// [`Demand::default`].
174    pub const fn saturated_if(full: bool) -> Self {
175        if full {
176            Self::saturated()
177        } else {
178            Demand::new(0)
179        }
180    }
181}
182
183/// The incremental-drive shape every streaming stage in the workspace adopts.
184///
185/// A `Stage` consumes input via [`feed`](Stage::feed) (the shape of that input
186/// is [`In`](Stage::In), chosen per implementor — see the [module docs](self)),
187/// produces typed output via repeated [`poll`](Stage::poll) calls (decoupled
188/// from `feed` — a single `feed` may unlock zero, one, or many outputs), is
189/// told there is no more input via [`finish`](Stage::finish), and may need to
190/// act purely on the passage of time via [`next_deadline`](Stage::next_deadline)
191/// / [`on_deadline`](Stage::on_deadline) (e.g. rate-scheduled re-emission with
192/// no new input at all). [`demand`](Stage::demand) lets a driver ask before
193/// feeding more.
194///
195/// See the [module docs](self) for the four divergent APIs this contract
196/// unifies and the reasoning behind the `Timestamp` clock parameter.
197///
198/// # Drive loop
199///
200/// A typical driver loop:
201///
202/// ```
203/// use broadcast_common::stage::{Demand, Stage, Timestamp};
204/// use std::collections::VecDeque;
205///
206/// /// A stage that reverses each fed chunk and hands it back on poll.
207/// struct Reverser {
208///     queue: VecDeque<Vec<u8>>,
209/// }
210///
211/// impl Stage for Reverser {
212///     type In<'a> = &'a [u8];
213///     type Out = Vec<u8>;
214///     type Error = core::convert::Infallible;
215///
216///     fn feed(&mut self, input: &[u8], _now: Timestamp) -> Result<(), Self::Error> {
217///         let mut chunk = input.to_vec();
218///         chunk.reverse();
219///         self.queue.push_back(chunk);
220///         Ok(())
221///     }
222///
223///     fn poll(&mut self) -> Option<Self::Out> {
224///         self.queue.pop_front()
225///     }
226///
227///     fn finish(&mut self) -> Result<(), Self::Error> {
228///         Ok(())
229///     }
230///
231///     fn next_deadline(&self) -> Option<Timestamp> {
232///         None
233///     }
234///
235///     fn on_deadline(&mut self, _now: Timestamp) {}
236///
237///     fn demand(&self) -> Demand {
238///         Demand::new(4096)
239///     }
240/// }
241///
242/// let mut stage = Reverser { queue: Default::default() };
243/// let mut outputs = Vec::new();
244///
245/// // feed → drain everything poll() currently has ready → repeat.
246/// stage.feed(b"abc", Timestamp::from_nanos(0)).unwrap();
247/// stage.feed(b"de", Timestamp::from_nanos(1_000)).unwrap();
248/// while let Some(out) = stage.poll() {
249///     outputs.push(out);
250/// }
251///
252/// // No more input: flush anything the stage was holding back.
253/// stage.finish().unwrap();
254/// while let Some(out) = stage.poll() {
255///     outputs.push(out);
256/// }
257///
258/// assert_eq!(outputs, vec![vec![b'c', b'b', b'a'], vec![b'e', b'd']]);
259/// ```
260pub trait Stage {
261    /// The shape of input this stage consumes via [`feed`](Stage::feed).
262    ///
263    /// A generic associated type, not a hardcoded `&[u8]`, so each
264    /// implementor states its own honest input: byte-stream stages use
265    /// `&'a [u8]`; sample-consuming stages (e.g. a segmenter) use an owned
266    /// typed input such as `(u32, Sample)` that does not need to borrow
267    /// anything, and can simply not use the `'a` parameter. See the
268    /// [module docs](self) for why this is a GAT rather than a second
269    /// `feed`-like method or an invented byte encoding.
270    type In<'a>;
271    /// The type of output this stage produces, pulled via [`poll`](Stage::poll).
272    type Out;
273    /// The error type this stage returns from [`feed`](Stage::feed) and
274    /// [`finish`](Stage::finish).
275    type Error;
276
277    /// Feed more input into the stage at time `now`.
278    ///
279    /// May unlock output retrievable via subsequent [`poll`](Stage::poll)
280    /// calls; a single `feed` call does not itself return output.
281    fn feed(&mut self, input: Self::In<'_>, now: Timestamp) -> Result<(), Self::Error>;
282
283    /// Pull one unit of ready output, if any is available.
284    ///
285    /// Callers should drain this in a `while let Some(_) = poll()` loop after
286    /// every [`feed`](Stage::feed)/[`finish`](Stage::finish)/
287    /// [`on_deadline`](Stage::on_deadline) call, since any of them may unlock
288    /// more than one output.
289    fn poll(&mut self) -> Option<Self::Out>;
290
291    /// Signal that no more input will ever be fed.
292    ///
293    /// Lets the stage flush any output it was withholding (e.g. waiting for a
294    /// boundary that will now never arrive). Idempotent: calling it more than
295    /// once must not error or reprocess.
296    fn finish(&mut self) -> Result<(), Self::Error>;
297
298    /// The next point in time (on the same clock as [`feed`](Stage::feed)'s
299    /// `now`) at which this stage has time-driven work to do, if any.
300    ///
301    /// A driver should call [`on_deadline`](Stage::on_deadline) once `now` has
302    /// reached this value, even if no new input has arrived. `None` means the
303    /// stage has nothing scheduled and only reacts to `feed`/`finish`, which
304    /// is the default.
305    fn next_deadline(&self) -> Option<Timestamp> {
306        None
307    }
308
309    /// Let the stage act purely on the passage of time (no new bytes), e.g. a
310    /// rate-scheduled re-emission or a timeout.
311    ///
312    /// May unlock output retrievable via subsequent [`poll`](Stage::poll)
313    /// calls, exactly like [`feed`](Stage::feed). The default does nothing, for
314    /// a stage whose [`next_deadline`](Stage::next_deadline) is always `None`.
315    fn on_deadline(&mut self, _now: Timestamp) {}
316
317    /// A hint about how much more input this stage would like right now.
318    ///
319    /// Advisory only — see [`Demand`]'s docs for what a driver may and may not
320    /// assume from it. The default is [`Demand::default`], no preference.
321    fn demand(&self) -> Demand {
322        Demand::default()
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    #[test]
329    fn saturated_if_selects_saturated_or_default() {
330        assert_eq!(
331            super::Demand::saturated_if(true),
332            super::Demand::saturated()
333        );
334        assert_eq!(super::Demand::saturated_if(false), super::Demand::default());
335    }
336
337    use super::*;
338    use alloc::collections::VecDeque;
339    use alloc::vec::Vec;
340    use core::convert::Infallible;
341
342    // A minimal `Stage` implementor using only `core`/`alloc` (no `std`),
343    // proving the trait is genuinely usable from a `no_std` crate. It counts
344    // fed bytes and, once a configured threshold is crossed OR a deadline
345    // fires, emits the running total.
346    struct ByteCounter {
347        total: u64,
348        threshold: u64,
349        emitted_up_to: u64,
350        pending: VecDeque<u64>,
351        deadline: Option<Timestamp>,
352        finished: bool,
353    }
354
355    impl ByteCounter {
356        fn new(threshold: u64, deadline: Option<Timestamp>) -> Self {
357            ByteCounter {
358                total: 0,
359                threshold,
360                emitted_up_to: 0,
361                pending: VecDeque::new(),
362                deadline,
363                finished: false,
364            }
365        }
366
367        fn maybe_emit(&mut self) {
368            if self.total.saturating_sub(self.emitted_up_to) >= self.threshold {
369                self.pending.push_back(self.total);
370                self.emitted_up_to = self.total;
371            }
372        }
373    }
374
375    impl Stage for ByteCounter {
376        type In<'a> = &'a [u8];
377        type Out = u64;
378        type Error = Infallible;
379
380        fn feed(&mut self, input: &[u8], _now: Timestamp) -> Result<(), Self::Error> {
381            self.total += input.len() as u64;
382            self.maybe_emit();
383            Ok(())
384        }
385
386        fn poll(&mut self) -> Option<Self::Out> {
387            self.pending.pop_front()
388        }
389
390        fn finish(&mut self) -> Result<(), Self::Error> {
391            if !self.finished && self.total > self.emitted_up_to {
392                self.pending.push_back(self.total);
393                self.emitted_up_to = self.total;
394            }
395            self.finished = true;
396            Ok(())
397        }
398
399        fn next_deadline(&self) -> Option<Timestamp> {
400            self.deadline
401        }
402
403        fn on_deadline(&mut self, now: Timestamp) {
404            if Some(now) >= self.deadline && self.total > self.emitted_up_to {
405                self.pending.push_back(self.total);
406                self.emitted_up_to = self.total;
407                self.deadline = None;
408            }
409        }
410
411        fn demand(&self) -> Demand {
412            if self.total.saturating_sub(self.emitted_up_to) >= self.threshold {
413                Demand::saturated()
414            } else {
415                Demand::new(self.threshold as usize)
416            }
417        }
418    }
419
420    #[test]
421    fn no_std_implementor_drives_via_feed_poll() {
422        let mut stage = ByteCounter::new(4, None);
423        let mut outs: Vec<u64> = Vec::new();
424
425        stage.feed(&[1, 2], Timestamp::from_nanos(0)).unwrap();
426        assert_eq!(stage.poll(), None); // below threshold, nothing yet
427
428        stage
429            .feed(&[3, 4, 5], Timestamp::from_nanos(1_000))
430            .unwrap();
431        while let Some(out) = stage.poll() {
432            outs.push(out);
433        }
434        assert_eq!(outs, alloc::vec![5]); // crossed threshold at total=5
435
436        stage.finish().unwrap();
437        // Nothing left unflushed since the last emission caught everything.
438        assert_eq!(stage.poll(), None);
439    }
440
441    #[test]
442    fn no_std_implementor_finish_flushes_remainder() {
443        let mut stage = ByteCounter::new(100, None);
444        stage.feed(&[1, 2, 3], Timestamp::from_nanos(0)).unwrap();
445        assert_eq!(stage.poll(), None); // well below threshold
446
447        stage.finish().unwrap();
448        assert_eq!(stage.poll(), Some(3));
449        assert_eq!(stage.poll(), None);
450    }
451
452    #[test]
453    fn no_std_implementor_on_deadline_fires_without_new_input() {
454        let deadline = Timestamp::from_nanos(5_000);
455        let mut stage = ByteCounter::new(100, Some(deadline));
456        stage.feed(&[1, 2, 3], Timestamp::from_nanos(0)).unwrap();
457        assert_eq!(stage.next_deadline(), Some(deadline));
458        assert_eq!(stage.poll(), None);
459
460        stage.on_deadline(deadline);
461        assert_eq!(stage.poll(), Some(3));
462        assert_eq!(stage.next_deadline(), None);
463    }
464
465    #[test]
466    fn demand_default_and_constructors() {
467        let d = Demand::default();
468        assert_eq!(d.want_bytes, 0);
469        assert!(!d.saturated);
470
471        let want = Demand::new(1024);
472        assert_eq!(want.want_bytes, 1024);
473        assert!(!want.saturated);
474
475        let full = Demand::saturated();
476        assert_eq!(full.want_bytes, 0);
477        assert!(full.saturated);
478    }
479
480    #[test]
481    fn timestamp_arithmetic_saturates_instead_of_panicking() {
482        let zero = Timestamp::ZERO;
483        let small = Timestamp::from_nanos(5);
484        let big = Timestamp::from_nanos(u64::MAX);
485
486        // Subtracting a larger timestamp from a smaller one saturates to 0,
487        // never underflows/panics.
488        assert_eq!(
489            small.saturating_sub(Timestamp::from_nanos(100)),
490            Duration::from_nanos(0)
491        );
492        assert_eq!(
493            Timestamp::from_nanos(100).saturating_sub(small),
494            Duration::from_nanos(95)
495        );
496
497        // Adding past u64::MAX saturates rather than wrapping/panicking.
498        assert_eq!(
499            big.saturating_add_nanos(10),
500            Timestamp::from_nanos(u64::MAX)
501        );
502        assert_eq!(zero.saturating_add_nanos(10), Timestamp::from_nanos(10));
503
504        // Duration-based saturating_add, including a Duration whose
505        // nanoseconds exceed u64::MAX.
506        assert_eq!(
507            zero.saturating_add(Duration::from_nanos(10)),
508            Timestamp::from_nanos(10)
509        );
510        assert_eq!(
511            big.saturating_add(Duration::from_secs(1)),
512            Timestamp::from_nanos(u64::MAX)
513        );
514        let huge = Duration::from_secs(u64::MAX);
515        assert_eq!(zero.saturating_add(huge), Timestamp::from_nanos(u64::MAX));
516    }
517
518    #[test]
519    fn timestamp_ordering_and_default() {
520        assert!(Timestamp::from_nanos(1) < Timestamp::from_nanos(2));
521        assert_eq!(Timestamp::default(), Timestamp::ZERO);
522    }
523
524    #[cfg(feature = "std")]
525    #[test]
526    fn from_instant_std_convenience() {
527        let base = std::time::Instant::now();
528        let later = base + Duration::from_millis(5);
529        let ts = Timestamp::from_instant(base, later);
530        assert_eq!(ts, Timestamp::from_nanos(5_000_000));
531
532        // A `now` before `base` saturates to zero rather than panicking.
533        let earlier = base.checked_sub(Duration::from_millis(1)).unwrap_or(base);
534        let ts2 = Timestamp::from_instant(base, earlier);
535        assert_eq!(ts2, Timestamp::ZERO);
536    }
537}