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//! - `ll_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 checked_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.checked_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
173/// The incremental-drive shape every streaming stage in the workspace adopts.
174///
175/// A `Stage` consumes input via [`feed`](Stage::feed) (the shape of that input
176/// is [`In`](Stage::In), chosen per implementor — see the [module docs](self)),
177/// produces typed output via repeated [`poll`](Stage::poll) calls (decoupled
178/// from `feed` — a single `feed` may unlock zero, one, or many outputs), is
179/// told there is no more input via [`finish`](Stage::finish), and may need to
180/// act purely on the passage of time via [`next_deadline`](Stage::next_deadline)
181/// / [`on_deadline`](Stage::on_deadline) (e.g. rate-scheduled re-emission with
182/// no new input at all). [`demand`](Stage::demand) lets a driver ask before
183/// feeding more.
184///
185/// See the [module docs](self) for the four divergent APIs this contract
186/// unifies and the reasoning behind the `Timestamp` clock parameter.
187///
188/// # Drive loop
189///
190/// A typical driver loop:
191///
192/// ```
193/// use broadcast_common::stage::{Demand, Stage, Timestamp};
194/// use std::collections::VecDeque;
195///
196/// /// A stage that reverses each fed chunk and hands it back on poll.
197/// struct Reverser {
198///     queue: VecDeque<Vec<u8>>,
199/// }
200///
201/// impl Stage for Reverser {
202///     type In<'a> = &'a [u8];
203///     type Out = Vec<u8>;
204///     type Error = core::convert::Infallible;
205///
206///     fn feed(&mut self, input: &[u8], _now: Timestamp) -> Result<(), Self::Error> {
207///         let mut chunk = input.to_vec();
208///         chunk.reverse();
209///         self.queue.push_back(chunk);
210///         Ok(())
211///     }
212///
213///     fn poll(&mut self) -> Option<Self::Out> {
214///         self.queue.pop_front()
215///     }
216///
217///     fn finish(&mut self) -> Result<(), Self::Error> {
218///         Ok(())
219///     }
220///
221///     fn next_deadline(&self) -> Option<Timestamp> {
222///         None
223///     }
224///
225///     fn on_deadline(&mut self, _now: Timestamp) {}
226///
227///     fn demand(&self) -> Demand {
228///         Demand::new(4096)
229///     }
230/// }
231///
232/// let mut stage = Reverser { queue: Default::default() };
233/// let mut outputs = Vec::new();
234///
235/// // feed → drain everything poll() currently has ready → repeat.
236/// stage.feed(b"abc", Timestamp::from_nanos(0)).unwrap();
237/// stage.feed(b"de", Timestamp::from_nanos(1_000)).unwrap();
238/// while let Some(out) = stage.poll() {
239///     outputs.push(out);
240/// }
241///
242/// // No more input: flush anything the stage was holding back.
243/// stage.finish().unwrap();
244/// while let Some(out) = stage.poll() {
245///     outputs.push(out);
246/// }
247///
248/// assert_eq!(outputs, vec![vec![b'c', b'b', b'a'], vec![b'e', b'd']]);
249/// ```
250pub trait Stage {
251    /// The shape of input this stage consumes via [`feed`](Stage::feed).
252    ///
253    /// A generic associated type, not a hardcoded `&[u8]`, so each
254    /// implementor states its own honest input: byte-stream stages use
255    /// `&'a [u8]`; sample-consuming stages (e.g. a segmenter) use an owned
256    /// typed input such as `(u32, Sample)` that does not need to borrow
257    /// anything, and can simply not use the `'a` parameter. See the
258    /// [module docs](self) for why this is a GAT rather than a second
259    /// `feed`-like method or an invented byte encoding.
260    type In<'a>;
261    /// The type of output this stage produces, pulled via [`poll`](Stage::poll).
262    type Out;
263    /// The error type this stage returns from [`feed`](Stage::feed) and
264    /// [`finish`](Stage::finish).
265    type Error;
266
267    /// Feed more input into the stage at time `now`.
268    ///
269    /// May unlock output retrievable via subsequent [`poll`](Stage::poll)
270    /// calls; a single `feed` call does not itself return output.
271    fn feed(&mut self, input: Self::In<'_>, now: Timestamp) -> Result<(), Self::Error>;
272
273    /// Pull one unit of ready output, if any is available.
274    ///
275    /// Callers should drain this in a `while let Some(_) = poll()` loop after
276    /// every [`feed`](Stage::feed)/[`finish`](Stage::finish)/
277    /// [`on_deadline`](Stage::on_deadline) call, since any of them may unlock
278    /// more than one output.
279    fn poll(&mut self) -> Option<Self::Out>;
280
281    /// Signal that no more input will ever be fed.
282    ///
283    /// Lets the stage flush any output it was withholding (e.g. waiting for a
284    /// boundary that will now never arrive). Idempotent: calling it more than
285    /// once must not error or reprocess.
286    fn finish(&mut self) -> Result<(), Self::Error>;
287
288    /// The next point in time (on the same clock as [`feed`](Stage::feed)'s
289    /// `now`) at which this stage has time-driven work to do, if any.
290    ///
291    /// A driver should call [`on_deadline`](Stage::on_deadline) once `now` has
292    /// reached this value, even if no new input has arrived. `None` means the
293    /// stage has nothing scheduled and only reacts to `feed`/`finish`.
294    fn next_deadline(&self) -> Option<Timestamp>;
295
296    /// Let the stage act purely on the passage of time (no new bytes), e.g. a
297    /// rate-scheduled re-emission or a timeout.
298    ///
299    /// May unlock output retrievable via subsequent [`poll`](Stage::poll)
300    /// calls, exactly like [`feed`](Stage::feed).
301    fn on_deadline(&mut self, now: Timestamp);
302
303    /// A hint about how much more input this stage would like right now.
304    ///
305    /// Advisory only — see [`Demand`]'s docs for what a driver may and may not
306    /// assume from it.
307    fn demand(&self) -> Demand;
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use alloc::collections::VecDeque;
314    use alloc::vec::Vec;
315    use core::convert::Infallible;
316
317    // A minimal `Stage` implementor using only `core`/`alloc` (no `std`),
318    // proving the trait is genuinely usable from a `no_std` crate. It counts
319    // fed bytes and, once a configured threshold is crossed OR a deadline
320    // fires, emits the running total.
321    struct ByteCounter {
322        total: u64,
323        threshold: u64,
324        emitted_up_to: u64,
325        pending: VecDeque<u64>,
326        deadline: Option<Timestamp>,
327        finished: bool,
328    }
329
330    impl ByteCounter {
331        fn new(threshold: u64, deadline: Option<Timestamp>) -> Self {
332            ByteCounter {
333                total: 0,
334                threshold,
335                emitted_up_to: 0,
336                pending: VecDeque::new(),
337                deadline,
338                finished: false,
339            }
340        }
341
342        fn maybe_emit(&mut self) {
343            if self.total.saturating_sub(self.emitted_up_to) >= self.threshold {
344                self.pending.push_back(self.total);
345                self.emitted_up_to = self.total;
346            }
347        }
348    }
349
350    impl Stage for ByteCounter {
351        type In<'a> = &'a [u8];
352        type Out = u64;
353        type Error = Infallible;
354
355        fn feed(&mut self, input: &[u8], _now: Timestamp) -> Result<(), Self::Error> {
356            self.total += input.len() as u64;
357            self.maybe_emit();
358            Ok(())
359        }
360
361        fn poll(&mut self) -> Option<Self::Out> {
362            self.pending.pop_front()
363        }
364
365        fn finish(&mut self) -> Result<(), Self::Error> {
366            if !self.finished && self.total > self.emitted_up_to {
367                self.pending.push_back(self.total);
368                self.emitted_up_to = self.total;
369            }
370            self.finished = true;
371            Ok(())
372        }
373
374        fn next_deadline(&self) -> Option<Timestamp> {
375            self.deadline
376        }
377
378        fn on_deadline(&mut self, now: Timestamp) {
379            if Some(now) >= self.deadline && self.total > self.emitted_up_to {
380                self.pending.push_back(self.total);
381                self.emitted_up_to = self.total;
382                self.deadline = None;
383            }
384        }
385
386        fn demand(&self) -> Demand {
387            if self.total.saturating_sub(self.emitted_up_to) >= self.threshold {
388                Demand::saturated()
389            } else {
390                Demand::new(self.threshold as usize)
391            }
392        }
393    }
394
395    #[test]
396    fn no_std_implementor_drives_via_feed_poll() {
397        let mut stage = ByteCounter::new(4, None);
398        let mut outs: Vec<u64> = Vec::new();
399
400        stage.feed(&[1, 2], Timestamp::from_nanos(0)).unwrap();
401        assert_eq!(stage.poll(), None); // below threshold, nothing yet
402
403        stage
404            .feed(&[3, 4, 5], Timestamp::from_nanos(1_000))
405            .unwrap();
406        while let Some(out) = stage.poll() {
407            outs.push(out);
408        }
409        assert_eq!(outs, alloc::vec![5]); // crossed threshold at total=5
410
411        stage.finish().unwrap();
412        // Nothing left unflushed since the last emission caught everything.
413        assert_eq!(stage.poll(), None);
414    }
415
416    #[test]
417    fn no_std_implementor_finish_flushes_remainder() {
418        let mut stage = ByteCounter::new(100, None);
419        stage.feed(&[1, 2, 3], Timestamp::from_nanos(0)).unwrap();
420        assert_eq!(stage.poll(), None); // well below threshold
421
422        stage.finish().unwrap();
423        assert_eq!(stage.poll(), Some(3));
424        assert_eq!(stage.poll(), None);
425    }
426
427    #[test]
428    fn no_std_implementor_on_deadline_fires_without_new_input() {
429        let deadline = Timestamp::from_nanos(5_000);
430        let mut stage = ByteCounter::new(100, Some(deadline));
431        stage.feed(&[1, 2, 3], Timestamp::from_nanos(0)).unwrap();
432        assert_eq!(stage.next_deadline(), Some(deadline));
433        assert_eq!(stage.poll(), None);
434
435        stage.on_deadline(deadline);
436        assert_eq!(stage.poll(), Some(3));
437        assert_eq!(stage.next_deadline(), None);
438    }
439
440    #[test]
441    fn demand_default_and_constructors() {
442        let d = Demand::default();
443        assert_eq!(d.want_bytes, 0);
444        assert!(!d.saturated);
445
446        let want = Demand::new(1024);
447        assert_eq!(want.want_bytes, 1024);
448        assert!(!want.saturated);
449
450        let full = Demand::saturated();
451        assert_eq!(full.want_bytes, 0);
452        assert!(full.saturated);
453    }
454
455    #[test]
456    fn timestamp_arithmetic_saturates_instead_of_panicking() {
457        let zero = Timestamp::ZERO;
458        let small = Timestamp::from_nanos(5);
459        let big = Timestamp::from_nanos(u64::MAX);
460
461        // Subtracting a larger timestamp from a smaller one saturates to 0,
462        // never underflows/panics.
463        assert_eq!(
464            small.saturating_sub(Timestamp::from_nanos(100)),
465            Duration::from_nanos(0)
466        );
467        assert_eq!(
468            Timestamp::from_nanos(100).saturating_sub(small),
469            Duration::from_nanos(95)
470        );
471
472        // Adding past u64::MAX saturates rather than wrapping/panicking.
473        assert_eq!(big.checked_add_nanos(10), Timestamp::from_nanos(u64::MAX));
474        assert_eq!(zero.checked_add_nanos(10), Timestamp::from_nanos(10));
475
476        // Duration-based saturating_add, including a Duration whose
477        // nanoseconds exceed u64::MAX.
478        assert_eq!(
479            zero.saturating_add(Duration::from_nanos(10)),
480            Timestamp::from_nanos(10)
481        );
482        assert_eq!(
483            big.saturating_add(Duration::from_secs(1)),
484            Timestamp::from_nanos(u64::MAX)
485        );
486        let huge = Duration::from_secs(u64::MAX);
487        assert_eq!(zero.saturating_add(huge), Timestamp::from_nanos(u64::MAX));
488    }
489
490    #[test]
491    fn timestamp_ordering_and_default() {
492        assert!(Timestamp::from_nanos(1) < Timestamp::from_nanos(2));
493        assert_eq!(Timestamp::default(), Timestamp::ZERO);
494    }
495
496    #[cfg(feature = "std")]
497    #[test]
498    fn from_instant_std_convenience() {
499        let base = std::time::Instant::now();
500        let later = base + Duration::from_millis(5);
501        let ts = Timestamp::from_instant(base, later);
502        assert_eq!(ts, Timestamp::from_nanos(5_000_000));
503
504        // A `now` before `base` saturates to zero rather than panicking.
505        let earlier = base.checked_sub(Duration::from_millis(1)).unwrap_or(base);
506        let ts2 = Timestamp::from_instant(base, earlier);
507        assert_eq!(ts2, Timestamp::ZERO);
508    }
509}