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/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    /// Misnamed: this saturates, it does not check. Use
111    /// [`saturating_add_nanos`](Self::saturating_add_nanos).
112    #[deprecated(
113        since = "9.5.0",
114        note = "saturates, not checks; use `saturating_add_nanos`"
115    )]
116    pub const fn checked_add_nanos(self, nanos: u64) -> Self {
117        self.saturating_add_nanos(nanos)
118    }
119
120    /// `self + duration`, saturating at `u64::MAX` instead of overflowing.
121    ///
122    /// A `Duration` in excess of `u64::MAX` nanoseconds saturates the same way.
123    pub fn saturating_add(self, duration: Duration) -> Self {
124        let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
125        self.saturating_add_nanos(nanos)
126    }
127}
128
129#[cfg(feature = "std")]
130impl Timestamp {
131    /// Derive a [`Timestamp`] from a `std::time::Instant` pair: `now - base`,
132    /// expressed as nanoseconds since `base`.
133    ///
134    /// `base` is whatever fixed instant the driver picked as its epoch (e.g.
135    /// "when this stage was constructed"); every subsequent call converts an
136    /// `Instant` to a `Timestamp` on the same epoch. This is a `std`-only
137    /// convenience so `std` callers are not forced to hand-roll the
138    /// subtraction; `no_std` drivers construct `Timestamp` directly.
139    pub fn from_instant(base: std::time::Instant, now: std::time::Instant) -> Self {
140        let elapsed = now.saturating_duration_since(base);
141        let nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
142        Timestamp(nanos)
143    }
144}
145
146/// A stage's hint about how much more input it can usefully accept right now.
147///
148/// This is advisory backpressure, not enforcement: a driver may call
149/// [`Stage::feed`] with more than `want_bytes`, or while `saturated` is `true`,
150/// and a well-behaved stage must still handle it correctly (buffering,
151/// erroring, or blocking as its own contract dictates) — `Demand` only lets a
152/// cooperative driver avoid doing so needlessly.
153#[non_exhaustive]
154#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
155pub struct Demand {
156    /// How many more bytes this stage would like handed to [`Stage::feed`]
157    /// before it is called again. `0` carries no meaning beyond "no
158    /// particular preference"; it is not a demand for zero bytes.
159    pub want_bytes: usize,
160    /// `true` if the stage is currently full and would prefer not to receive
161    /// more input until it has been polled / has had time to drain.
162    pub saturated: bool,
163}
164
165impl Demand {
166    /// Build a [`Demand`] requesting `want_bytes` more input, not saturated.
167    pub const fn new(want_bytes: usize) -> Self {
168        Demand {
169            want_bytes,
170            saturated: false,
171        }
172    }
173
174    /// A [`Demand`] indicating the stage currently wants no more input.
175    pub const fn saturated() -> Self {
176        Demand {
177            want_bytes: 0,
178            saturated: true,
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`.
304    fn next_deadline(&self) -> Option<Timestamp>;
305
306    /// Let the stage act purely on the passage of time (no new bytes), e.g. a
307    /// rate-scheduled re-emission or a timeout.
308    ///
309    /// May unlock output retrievable via subsequent [`poll`](Stage::poll)
310    /// calls, exactly like [`feed`](Stage::feed).
311    fn on_deadline(&mut self, now: Timestamp);
312
313    /// A hint about how much more input this stage would like right now.
314    ///
315    /// Advisory only — see [`Demand`]'s docs for what a driver may and may not
316    /// assume from it.
317    fn demand(&self) -> Demand;
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use alloc::collections::VecDeque;
324    use alloc::vec::Vec;
325    use core::convert::Infallible;
326
327    // A minimal `Stage` implementor using only `core`/`alloc` (no `std`),
328    // proving the trait is genuinely usable from a `no_std` crate. It counts
329    // fed bytes and, once a configured threshold is crossed OR a deadline
330    // fires, emits the running total.
331    struct ByteCounter {
332        total: u64,
333        threshold: u64,
334        emitted_up_to: u64,
335        pending: VecDeque<u64>,
336        deadline: Option<Timestamp>,
337        finished: bool,
338    }
339
340    impl ByteCounter {
341        fn new(threshold: u64, deadline: Option<Timestamp>) -> Self {
342            ByteCounter {
343                total: 0,
344                threshold,
345                emitted_up_to: 0,
346                pending: VecDeque::new(),
347                deadline,
348                finished: false,
349            }
350        }
351
352        fn maybe_emit(&mut self) {
353            if self.total.saturating_sub(self.emitted_up_to) >= self.threshold {
354                self.pending.push_back(self.total);
355                self.emitted_up_to = self.total;
356            }
357        }
358    }
359
360    impl Stage for ByteCounter {
361        type In<'a> = &'a [u8];
362        type Out = u64;
363        type Error = Infallible;
364
365        fn feed(&mut self, input: &[u8], _now: Timestamp) -> Result<(), Self::Error> {
366            self.total += input.len() as u64;
367            self.maybe_emit();
368            Ok(())
369        }
370
371        fn poll(&mut self) -> Option<Self::Out> {
372            self.pending.pop_front()
373        }
374
375        fn finish(&mut self) -> Result<(), Self::Error> {
376            if !self.finished && self.total > self.emitted_up_to {
377                self.pending.push_back(self.total);
378                self.emitted_up_to = self.total;
379            }
380            self.finished = true;
381            Ok(())
382        }
383
384        fn next_deadline(&self) -> Option<Timestamp> {
385            self.deadline
386        }
387
388        fn on_deadline(&mut self, now: Timestamp) {
389            if Some(now) >= self.deadline && self.total > self.emitted_up_to {
390                self.pending.push_back(self.total);
391                self.emitted_up_to = self.total;
392                self.deadline = None;
393            }
394        }
395
396        fn demand(&self) -> Demand {
397            if self.total.saturating_sub(self.emitted_up_to) >= self.threshold {
398                Demand::saturated()
399            } else {
400                Demand::new(self.threshold as usize)
401            }
402        }
403    }
404
405    #[test]
406    fn no_std_implementor_drives_via_feed_poll() {
407        let mut stage = ByteCounter::new(4, None);
408        let mut outs: Vec<u64> = Vec::new();
409
410        stage.feed(&[1, 2], Timestamp::from_nanos(0)).unwrap();
411        assert_eq!(stage.poll(), None); // below threshold, nothing yet
412
413        stage
414            .feed(&[3, 4, 5], Timestamp::from_nanos(1_000))
415            .unwrap();
416        while let Some(out) = stage.poll() {
417            outs.push(out);
418        }
419        assert_eq!(outs, alloc::vec![5]); // crossed threshold at total=5
420
421        stage.finish().unwrap();
422        // Nothing left unflushed since the last emission caught everything.
423        assert_eq!(stage.poll(), None);
424    }
425
426    #[test]
427    fn no_std_implementor_finish_flushes_remainder() {
428        let mut stage = ByteCounter::new(100, None);
429        stage.feed(&[1, 2, 3], Timestamp::from_nanos(0)).unwrap();
430        assert_eq!(stage.poll(), None); // well below threshold
431
432        stage.finish().unwrap();
433        assert_eq!(stage.poll(), Some(3));
434        assert_eq!(stage.poll(), None);
435    }
436
437    #[test]
438    fn no_std_implementor_on_deadline_fires_without_new_input() {
439        let deadline = Timestamp::from_nanos(5_000);
440        let mut stage = ByteCounter::new(100, Some(deadline));
441        stage.feed(&[1, 2, 3], Timestamp::from_nanos(0)).unwrap();
442        assert_eq!(stage.next_deadline(), Some(deadline));
443        assert_eq!(stage.poll(), None);
444
445        stage.on_deadline(deadline);
446        assert_eq!(stage.poll(), Some(3));
447        assert_eq!(stage.next_deadline(), None);
448    }
449
450    #[test]
451    fn demand_default_and_constructors() {
452        let d = Demand::default();
453        assert_eq!(d.want_bytes, 0);
454        assert!(!d.saturated);
455
456        let want = Demand::new(1024);
457        assert_eq!(want.want_bytes, 1024);
458        assert!(!want.saturated);
459
460        let full = Demand::saturated();
461        assert_eq!(full.want_bytes, 0);
462        assert!(full.saturated);
463    }
464
465    #[test]
466    fn timestamp_arithmetic_saturates_instead_of_panicking() {
467        let zero = Timestamp::ZERO;
468        let small = Timestamp::from_nanos(5);
469        let big = Timestamp::from_nanos(u64::MAX);
470
471        // Subtracting a larger timestamp from a smaller one saturates to 0,
472        // never underflows/panics.
473        assert_eq!(
474            small.saturating_sub(Timestamp::from_nanos(100)),
475            Duration::from_nanos(0)
476        );
477        assert_eq!(
478            Timestamp::from_nanos(100).saturating_sub(small),
479            Duration::from_nanos(95)
480        );
481
482        // Adding past u64::MAX saturates rather than wrapping/panicking.
483        assert_eq!(
484            big.saturating_add_nanos(10),
485            Timestamp::from_nanos(u64::MAX)
486        );
487        assert_eq!(zero.saturating_add_nanos(10), Timestamp::from_nanos(10));
488
489        // Duration-based saturating_add, including a Duration whose
490        // nanoseconds exceed u64::MAX.
491        assert_eq!(
492            zero.saturating_add(Duration::from_nanos(10)),
493            Timestamp::from_nanos(10)
494        );
495        assert_eq!(
496            big.saturating_add(Duration::from_secs(1)),
497            Timestamp::from_nanos(u64::MAX)
498        );
499        let huge = Duration::from_secs(u64::MAX);
500        assert_eq!(zero.saturating_add(huge), Timestamp::from_nanos(u64::MAX));
501    }
502
503    #[test]
504    fn timestamp_ordering_and_default() {
505        assert!(Timestamp::from_nanos(1) < Timestamp::from_nanos(2));
506        assert_eq!(Timestamp::default(), Timestamp::ZERO);
507    }
508
509    #[cfg(feature = "std")]
510    #[test]
511    fn from_instant_std_convenience() {
512        let base = std::time::Instant::now();
513        let later = base + Duration::from_millis(5);
514        let ts = Timestamp::from_instant(base, later);
515        assert_eq!(ts, Timestamp::from_nanos(5_000_000));
516
517        // A `now` before `base` saturates to zero rather than panicking.
518        let earlier = base.checked_sub(Duration::from_millis(1)).unwrap_or(base);
519        let ts2 = Timestamp::from_instant(base, earlier);
520        assert_eq!(ts2, Timestamp::ZERO);
521    }
522}