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}