media_plane/lib.rs
1//! `media-plane` — the media-plane integration layer.
2//!
3//! The workspace's ingress/egress architecture
4//! (`docs/superpowers/specs/2026-07-26-media-plane-architecture.md` in the
5//! `rust-broadcast` repository) is four layers, not one pipeline glued
6//! together per protocol:
7//!
8//! ```text
9//! Dialer|Listener ──► [ByteStage]* ──► IngestSession ──► [IrTransform]* ──► TrunkWriter
10//! (N sources) byte→byte demux IR→IR │
11//! ▼
12//! ┌──────── Trunk ────────┐
13//! │ sample ring │
14//! │ segment log │
15//! │ EVENT log (90 kHz) │
16//! └───────────────────────┘
17//! subscribe() ─► SampleCursor ─► PushEgress (defined shape; no in-tree impl)
18//! subscribe() ─► SegmentCursor ─► SegmentEgress (DVR, MABR, ROUTE, Smooth)
19//! resolve() ─────────────────► ServedEgress (LL-HLS, DASH, catch-up)
20//! ```
21//!
22//! `media-plane` is where that shape lives in code: it is the crate that ties
23//! ingress, the byte layer, container demux (`transmux`), IR transforms, the
24//! `Trunk`, and the three egress shapes together into one runnable pipeline.
25//! It depends on `broadcast-common` for the shared drive contract
26//! ([`broadcast_common::Stage`]) and clock/backpressure types
27//! ([`broadcast_common::Timestamp`], [`broadcast_common::Demand`]).
28//!
29//! The byte layer, the whole [`Trunk`] (samples, segments, the 90 kHz event
30//! log, live parts, reader wake), [`ingress`] (`Dialer`/`Listener`/
31//! `IngestSession`/[`IngestDriver`]), and [`retention`] (hot/cold tiering over
32//! a caller-supplied [`SegmentSink`]) are all exercised end to end by real
33//! callers. Of the three egress shapes: [`ServedEgress`] and [`SegmentEgress`]
34//! each have real production implementors — `hls-runtime`'s `HlsOrigin`
35//! (`ServedEgress`) and multimux's `DashOrigin`/`LlDashOrigin`/
36//! `SmoothManifestOrigin`/`SmoothFragmentOrigin`/`DvrRecorder`
37//! (`ServedEgress`/`SegmentEgress`). [`PushEgress`] is a defined trait shape
38//! with **no in-tree production implementor** — its only impls
39//! (`RecordingPushEgress`, `WhepLikePushEgress`) live in this module's own
40//! `#[cfg(test)]` block. multimux's real push path (SRT/RTMP/RTSP relay,
41//! issue #744) does not use this trait at all; it drives its own
42//! `multimux::push::PushTransport` directly off a `Trunk`'s sample cursor.
43//! Step 3f added the acceptance furniture this doc describes (fuzz targets,
44//! examples, the release lane) without changing any of that behaviour.
45//!
46//! # `no_std` note — the byte layer only, not the whole crate
47//!
48//! **Only [`byte_stage`]/[`byte_tap`]/[`byte_merge`] are `no_std` + `alloc`.**
49//! [`Trunk`] and everything built on it ([`ingress`], [`egress`],
50//! [`retention`]) are gated behind, and require, the `std` feature —
51//! [`Trunk`] itself needs `std::sync::Mutex`/`Arc`/`Condvar` for cross-thread
52//! sharing (see the [`trunk`] module docs for why that beats a `no_std`
53//! spinlock crate here). Saying just "the plane is `no_std`-capable" without
54//! this qualifier would be true of a third of the crate and false of the
55//! rest, so it is stated plainly here rather than implied by the crate-level
56//! `no_std` attribute alone. `std` is a default feature; `--no-default-features`
57//! builds only the byte layer.
58//!
59//! # Recorded deviations (not defects — read before filing one)
60//!
61//! - [`byte_merge::MergePolicy`] deliberately has **no** `Hitless2022_7`
62//! variant yet — SMPTE ST 2022-7 seamless switching needs an RTP
63//! sequence-number parse this layer does not have; see the
64//! [`byte_merge`] module docs. Tracked as #752.
65//! - Pull sources (HLS/DASH/Smooth) are request-driven, not stream-driven,
66//! and [`IngestSession::poll_transmit`] has no way to express "issue a GET
67//! for this URL" yet — a recorded seam, not solved here; see the
68//! [`ingress`] module docs' "Known seam" section.
69//!
70//! # The byte layer ([`byte_stage`], [`byte_tap`], [`byte_merge`])
71//!
72//! A byte stage is pre-demux, byte-to-byte, deadline-driven work: CAM
73//! descramble, TS continuity/PCR repair, T2-MI/BBFrame inner-TS recovery,
74//! program-PID filtering. See the [`byte_stage`] module docs for why it is
75//! defined as a `Stage` specialisation rather than a second trait, and for the
76//! exact form that was validated to compile.
77//!
78//! [`ByteTap`] sits alongside the byte stages, not in their chain: a
79//! non-blocking positional observer that lets analysis (`dvb-conformance`,
80//! `media-doctor watch`, #737's T-STD) see bytes a demuxer would reject. See
81//! the [`byte_tap`] module docs for the non-blocking/`Lagged` trade and why it
82//! is not a `Stage`.
83//!
84//! [`ByteMerge`] is the one place `N` byte sources reduce to one stream —
85//! everything above the byte layer stays strictly single-input. See the
86//! [`byte_merge`] module docs for why it operates on discrete messages, its
87//! two policies, and why ST 2022-7 hitless switching is deliberately absent
88//! rather than stubbed.
89//!
90//! # `Trunk`, the writer, and the cursors ([`trunk`], `std`-only)
91//!
92//! Above the byte layer and demux sits [`Trunk`]: the bounded sample ring and
93//! segment log one [`TrunkWriter`] publishes into and any number of
94//! [`SampleCursor`]/[`SegmentCursor`]s read from. It requires the `std`
95//! feature (`Arc`/`Mutex`/`Condvar` for cross-thread sharing) — see the
96//! [`trunk`] module docs for why that is the right line to draw rather than
97//! reaching for a `no_std` spinlock crate, the benchmark
98//! (`spikes/trunk-bench`) that shaped the design, and — critically, before
99//! calling [`Trunk::subscribe`]/[`Trunk::subscribe_segments`] once per
100//! connection — why supported reader count is single-digit by design.
101//!
102//! The segment log resolves a real contradiction: a DVR/archive consumer
103//! must never miss a segment, but the writer must never block. See the
104//! [`trunk`] module docs' "DVR contradiction" section for why the answer is
105//! retention (a pinning cursor from [`Trunk::pin_segments`]), not
106//! back-pressure, and for the three-way [`ArchiveOverrun`] trade a pinning
107//! cursor's caller makes explicit when the retention bound is finally hit.
108//!
109//! The event log carries [`timed_metadata::TimedEvent`] on the trunk's own
110//! 90 kHz absolute clock, addressable both by media time
111//! ([`Trunk::events_between`]) and by segment
112//! ([`Trunk::events_in_segment`]) — and, critically, never fabricates a
113//! media time for an event that is only segment-relative (`emsg` v0) or
114//! wall-clock-only (SCTE-35 `splice_schedule`) until the boundary or
115//! [`timed_metadata::TimeAnchor`] it actually needs arrives. See the
116//! [`trunk`] module docs' event-log section for the full B1 story.
117//!
118//! # Retention and `SegmentSink` ([`retention`], `std`-only)
119//!
120//! [`Retention`] is the hot/cold archive policy layered on top of the
121//! segment log — [`Retention::HotOnly`] (the segment log alone) or
122//! [`Retention::Tiered`], where a [`RetentionDriver`] drains a pinning
123//! segment cursor into a caller-supplied, sans-IO [`SegmentSink`]. The
124//! concrete disk/object-store adapter behind that sink is deliberately **not**
125//! this crate's job — staying sans-IO is what lets the retention engine be
126//! driven and tested without touching a filesystem, so the caller supplies
127//! the IO. See the
128//! [`retention`] module docs for why this reuses [`ArchiveOverrun`] verbatim
129//! rather than inventing a parallel policy, why the pending hand-off queue
130//! is bounded to exactly one in-flight segment, and the "cold, ask the
131//! sink" answer [`RetentionDriver::locate`] gives for a catch-up request
132//! against an evicted-from-hot segment (issue #746, DVR/catch-up).
133
134#![cfg_attr(not(feature = "std"), no_std)]
135#![cfg_attr(docsrs, feature(doc_cfg))]
136#![doc(html_root_url = "https://docs.rs/media-plane")]
137
138extern crate alloc;
139
140pub mod byte_merge;
141pub mod byte_stage;
142pub mod byte_tap;
143#[cfg(feature = "std")]
144#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
145pub mod egress;
146#[cfg(feature = "std")]
147#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
148pub mod ingress;
149#[cfg(feature = "std")]
150#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
151pub mod retention;
152#[cfg(feature = "std")]
153#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
154pub mod trunk;
155
156pub use byte_merge::{ByteMerge, MergeError, MergePolicy, SourceId};
157pub use byte_stage::ByteStage;
158pub use byte_tap::{ByteTap, TapItem, TapPoint};
159#[cfg(feature = "std")]
160#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
161pub use egress::{
162 AwaitPolicy, CachePolicy, EgressResponse, NegotiationOutcome, PushEgress, SegmentEgress,
163 ServedEgress, TrackSelection,
164};
165#[cfg(feature = "std")]
166#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
167pub use ingress::{
168 AcceptOutcome, DEFAULT_MAX_PROGRAMS, DialAttempt, DialSupervisor, Dialer, HandshakePolicy,
169 HealthState, IngestDriver, IngestSession, ListenDriver, Listener, ProgramId, ReconnectPolicy,
170 SessionEvent, SessionId, run_dial, run_listen,
171};
172#[cfg(feature = "std")]
173#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
174pub use retention::{Retention, RetentionDriver, SegmentLocation, SegmentSink, SinkOutcome};
175#[cfg(feature = "std")]
176#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
177pub use trunk::{
178 ArchiveOverrun, EventAnchor, EventCursor, EventCursorItem, EventEntry, RetentionClass,
179 SampleCursor, SampleCursorItem, SegmentCursor, SegmentCursorItem, SegmentEntry, Trunk,
180 TrunkConfig, TrunkWriter,
181};