Expand description
media-plane — the media-plane integration layer.
The workspace’s ingress/egress architecture
(docs/superpowers/specs/2026-07-26-media-plane-architecture.md in the
rust-broadcast repository) is four layers, not one pipeline glued
together per protocol:
Dialer|Listener ──► [ByteStage]* ──► IngestSession ──► [IrTransform]* ──► TrunkWriter
(N sources) byte→byte demux IR→IR │
▼
┌──────── Trunk ────────┐
│ sample ring │
│ segment log │
│ EVENT log (90 kHz) │
└───────────────────────┘
subscribe() ─► SampleCursor ─► PushEgress (defined shape; no in-tree impl)
subscribe() ─► SegmentCursor ─► SegmentEgress (DVR, MABR, ROUTE, Smooth)
resolve() ─────────────────► ServedEgress (LL-HLS, DASH, catch-up)media-plane is where that shape lives in code: it is the crate that ties
ingress, the byte layer, container demux (transmux), IR transforms, the
Trunk, and the three egress shapes together into one runnable pipeline.
It depends on broadcast-common for the shared drive contract
(broadcast_common::Stage) and clock/backpressure types
(broadcast_common::Timestamp, broadcast_common::Demand).
The byte layer, the whole Trunk (samples, segments, the 90 kHz event
log, live parts, reader wake), ingress (Dialer/Listener/
IngestSession/IngestDriver), and retention (hot/cold tiering over
a caller-supplied SegmentSink) are all exercised end to end by real
callers. Of the three egress shapes: ServedEgress and SegmentEgress
each have real production implementors — hls-runtime’s HlsOrigin
(ServedEgress) and multimux’s DashOrigin/LlDashOrigin/
SmoothManifestOrigin/SmoothFragmentOrigin/DvrRecorder
(ServedEgress/SegmentEgress). PushEgress is a defined trait shape
with no in-tree production implementor — its only impls
(RecordingPushEgress, WhepLikePushEgress) live in this module’s own
#[cfg(test)] block. multimux’s real push path (SRT/RTMP/RTSP relay,
issue #744) does not use this trait at all; it drives its own
multimux::push::PushTransport directly off a Trunk’s sample cursor.
Step 3f added the acceptance furniture this doc describes (fuzz targets,
examples, the release lane) without changing any of that behaviour.
§no_std note — the byte layer only, not the whole crate
Only byte_stage/byte_tap/byte_merge are no_std + alloc.
Trunk and everything built on it (ingress, egress,
retention) are gated behind, and require, the std feature —
Trunk itself needs std::sync::Mutex/Arc/Condvar for cross-thread
sharing (see the trunk module docs for why that beats a no_std
spinlock crate here). Saying just “the plane is no_std-capable” without
this qualifier would be true of a third of the crate and false of the
rest, so it is stated plainly here rather than implied by the crate-level
no_std attribute alone. std is a default feature; --no-default-features
builds only the byte layer.
§Recorded deviations (not defects — read before filing one)
byte_merge::MergePolicydeliberately has noHitless2022_7variant yet — SMPTE ST 2022-7 seamless switching needs an RTP sequence-number parse this layer does not have; see thebyte_mergemodule docs. Tracked as #752.- Pull sources (HLS/DASH/Smooth) are request-driven, not stream-driven,
and
IngestSession::poll_transmithas no way to express “issue a GET for this URL” yet — a recorded seam, not solved here; see theingressmodule docs’ “Known seam” section.
§The byte layer (byte_stage, byte_tap, byte_merge)
A byte stage is pre-demux, byte-to-byte, deadline-driven work: CAM
descramble, TS continuity/PCR repair, T2-MI/BBFrame inner-TS recovery,
program-PID filtering. See the byte_stage module docs for why it is
defined as a Stage specialisation rather than a second trait, and for the
exact form that was validated to compile.
ByteTap sits alongside the byte stages, not in their chain: a
non-blocking positional observer that lets analysis (dvb-conformance,
media-doctor watch, #737’s T-STD) see bytes a demuxer would reject. See
the byte_tap module docs for the non-blocking/Lagged trade and why it
is not a Stage.
ByteMerge is the one place N byte sources reduce to one stream —
everything above the byte layer stays strictly single-input. See the
byte_merge module docs for why it operates on discrete messages, its
two policies, and why ST 2022-7 hitless switching is deliberately absent
rather than stubbed.
§Trunk, the writer, and the cursors (trunk, std-only)
Above the byte layer and demux sits Trunk: the bounded sample ring and
segment log one TrunkWriter publishes into and any number of
SampleCursor/SegmentCursors read from. It requires the std
feature (Arc/Mutex/Condvar for cross-thread sharing) — see the
trunk module docs for why that is the right line to draw rather than
reaching for a no_std spinlock crate, the benchmark
(spikes/trunk-bench) that shaped the design, and — critically, before
calling Trunk::subscribe/Trunk::subscribe_segments once per
connection — why supported reader count is single-digit by design.
The segment log resolves a real contradiction: a DVR/archive consumer
must never miss a segment, but the writer must never block. See the
trunk module docs’ “DVR contradiction” section for why the answer is
retention (a pinning cursor from Trunk::pin_segments), not
back-pressure, and for the three-way ArchiveOverrun trade a pinning
cursor’s caller makes explicit when the retention bound is finally hit.
The event log carries timed_metadata::TimedEvent on the trunk’s own
90 kHz absolute clock, addressable both by media time
(Trunk::events_between) and by segment
(Trunk::events_in_segment) — and, critically, never fabricates a
media time for an event that is only segment-relative (emsg v0) or
wall-clock-only (SCTE-35 splice_schedule) until the boundary or
timed_metadata::TimeAnchor it actually needs arrives. See the
trunk module docs’ event-log section for the full B1 story.
§Retention and SegmentSink (retention, std-only)
Retention is the hot/cold archive policy layered on top of the
segment log — Retention::HotOnly (the segment log alone) or
Retention::Tiered, where a RetentionDriver drains a pinning
segment cursor into a caller-supplied, sans-IO SegmentSink. The
concrete disk/object-store adapter behind that sink is deliberately not
this crate’s job — staying sans-IO is what lets the retention engine be
driven and tested without touching a filesystem, so the caller supplies
the IO. See the
retention module docs for why this reuses ArchiveOverrun verbatim
rather than inventing a parallel policy, why the pending hand-off queue
is bounded to exactly one in-flight segment, and the “cold, ask the
sink” answer RetentionDriver::locate gives for a catch-up request
against an evicted-from-hot segment (issue #746, DVR/catch-up).
Re-exports§
pub use byte_merge::ByteMerge;pub use byte_merge::MergeError;pub use byte_merge::MergePolicy;pub use byte_merge::SourceId;pub use byte_stage::ByteStage;pub use byte_tap::ByteTap;pub use byte_tap::TapItem;pub use byte_tap::TapPoint;pub use egress::AwaitPolicy;stdpub use egress::CachePolicy;stdpub use egress::EgressResponse;stdpub use egress::NegotiationOutcome;stdpub use egress::PushEgress;stdpub use egress::SegmentEgress;stdpub use egress::ServedEgress;stdpub use egress::TrackSelection;stdpub use ingress::AcceptOutcome;stdpub use ingress::DEFAULT_MAX_PROGRAMS;stdpub use ingress::DialAttempt;stdpub use ingress::DialSupervisor;stdpub use ingress::Dialer;stdpub use ingress::HandshakePolicy;stdpub use ingress::HealthState;stdpub use ingress::IngestDriver;stdpub use ingress::IngestSession;stdpub use ingress::ListenDriver;stdpub use ingress::Listener;stdpub use ingress::ProgramId;stdpub use ingress::ReconnectPolicy;stdpub use ingress::SessionEvent;stdpub use ingress::SessionId;stdpub use ingress::run_dial;stdpub use ingress::run_listen;stdpub use retention::Retention;stdpub use retention::RetentionDriver;stdpub use retention::SegmentLocation;stdpub use retention::SegmentSink;stdpub use retention::SinkOutcome;stdpub use trunk::ArchiveOverrun;stdpub use trunk::EventAnchor;stdpub use trunk::EventCursor;stdpub use trunk::EventCursorItem;stdpub use trunk::EventEntry;stdpub use trunk::RetentionClass;stdpub use trunk::SampleCursor;stdpub use trunk::SampleCursorItem;stdpub use trunk::SegmentCursor;stdpub use trunk::SegmentCursorItem;stdpub use trunk::SegmentEntry;stdpub use trunk::Trunk;stdpub use trunk::TrunkConfig;stdpub use trunk::TrunkWriter;std
Modules§
- byte_
merge ByteMerge— the one bounded multi-input primitive in the byte layer.- byte_
stage ByteStage— the pre-demux byte-to-byte drive contract.- byte_
tap ByteTap— a positional, non-blocking observer of bytes in flight.- egress
std ServedEgress,PushEgress,SegmentEgress— the three egress shapes that read from acrate::Trunk(plan step 3d;docs/superpowers/specs/2026-07-26-media-plane-architecture.md§1/§3).- ingress
std Dialer/Listener/IngestSession— the ingress traits, and the genericrun_dial/run_listendrivers that pump them into acrate::Trunk(plan step 3c;docs/superpowers/specs/2026-07-26-media-plane-architecture.md§2).- retention
std RetentionandSegmentSink— the hot/cold archive policy layered on top of the segment log (plan step 3e;docs/superpowers/plans/2026-07-26-media-plane-implementation.mdStep 3e,docs/superpowers/specs/2026-07-26-media-plane-architecture.md§1.2/§3).- trunk
std Trunk— the sample ring,TrunkWriter, andSampleCursor(plan step 3b-i); the segment log,SegmentCursor, and the lossless-by-retention pinning mechanism (plan step 3b-ii); the 90 kHz event log,EventCursor, andEventAnchor(plan step 3b-iii); the live-part log and theTrunk::listenreader-wake primitive (plan step 3b-iv), closing the two gaps step 3d found while readinghls-runtime/src/server/before writing the egress traits — see The live-part log and The reader-wake primitive below; and nowSegmentWriter, splitting the single write handle 3b-i introduced by ring group so a segmenter can exist at all — see One writer per ring group, not one writer perTrunkbelow — perdocs/superpowers/specs/2026-07-26-media-plane-architecture.md§1.2.