moq_mux/container/mod.rs
1//! Container formats.
2//!
3//! A container decides how a media frame is laid out inside a moq-lite
4//! frame: framing overhead, whether multiple samples can share one moq
5//! frame, and whether the same encoding doubles as a file format on disk.
6//!
7//! Each submodule implements one format. The wire-level ones implement
8//! the [`Container`] trait, so [`Producer<C>`] and [`Consumer<C>`] can
9//! be generic over the choice. The catalog announces a container per
10//! track; [`catalog::hang::Container`](crate::catalog::hang::Container)
11//! dispatches the right implementation at runtime.
12
13use std::task::Poll;
14
15use bytes::Bytes;
16
17mod consumer;
18mod group;
19mod producer;
20mod source;
21#[cfg(test)]
22pub(crate) mod test_util;
23
24pub mod flv;
25pub mod fmp4;
26pub mod legacy;
27pub mod loc;
28pub mod mkv;
29pub mod ts;
30
31pub use consumer::Consumer;
32pub use group::GroupConsumer;
33pub use producer::Producer;
34pub(crate) use source::ExportSource;
35
36/// The media role that determines how a container represents frame durations.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Kind {
39 /// Audio packets carry codec-defined durations.
40 Audio,
41 /// Video frames can need a duration marker at group end.
42 Video,
43 /// Opaque data has no sample-duration semantics.
44 Data,
45}
46
47/// A decoded media frame: timestamp, payload bytes, keyframe flag.
48///
49/// `payload` is the raw codec bitstream that gets handed to the decoder.
50/// The exact shape depends on the codec (Annex B for H.264/H.265, OBU for
51/// AV1, and so on).
52#[derive(Clone, Debug)]
53pub struct Frame {
54 /// Presentation timestamp.
55 ///
56 /// Each container picks its own native scale: fmp4 uses the source
57 /// `mdhd.timescale`, mkv uses nanoseconds, legacy is fixed at microseconds.
58 /// LOC defaults to microseconds but a decoded frame keeps whatever per-frame
59 /// timescale the wire carried, so an exporter can re-emit without forcing
60 /// micros. Frames within a track must be in *decode* order, not display
61 /// order. B-frames may have non-monotonic presentation timestamps.
62 pub timestamp: moq_net::Timestamp,
63
64 /// Sample duration in the frame's own scale, when the container reports it.
65 ///
66 /// CMAF carries a per-sample duration (trun sample-duration). Legacy and LOC
67 /// can fill it from a duration marker when reading a fetched group. Streaming
68 /// muxers receive the later endpoint separately, so media stays immediately available.
69 /// The [`Consumer`] adds it to `timestamp` to learn how far a group has
70 /// presented, so it can advance to a newer group as soon as the gap is
71 /// covered instead of waiting out the max age budget.
72 pub duration: Option<moq_net::Timestamp>,
73
74 /// Encoded codec payload.
75 pub payload: Bytes,
76
77 /// Whether this frame opens a group, or is a video keyframe.
78 ///
79 /// Containers that carry the bit on the wire (CMAF reads it from
80 /// trun sample-flags) set it for video; containers that don't (Legacy,
81 /// LOC) leave it `false`. The wrapping [`Consumer`] still asserts
82 /// "first frame in a group is a keyframe" as a fallback, so the
83 /// Legacy/LOC case lands correctly without anyone having to know. For
84 /// audio, whose samples are all independently decodable, that fallback
85 /// is the only source: the bit marks the group boundary the publisher
86 /// drew, never a per-sample sync flag.
87 pub keyframe: bool,
88}
89
90/// Stamp `frame` with the duration that ends at `bound`, unless it already has one.
91pub(crate) fn close_duration(frame: &mut Frame, bound: moq_net::Timestamp) {
92 if frame.duration.is_some() {
93 return;
94 }
95 let Some(delta) = bound.as_micros().checked_sub(frame.timestamp.as_micros()) else {
96 return;
97 };
98 if delta == 0 {
99 return;
100 }
101 let Ok(delta) = u64::try_from(delta) else {
102 return;
103 };
104 if let Ok(duration) = moq_net::Timestamp::from_micros(delta) {
105 frame.duration = Some(duration);
106 }
107}
108
109/// A non-keyframe frame arrived with no open group.
110///
111/// A track must open with a keyframe (and so must the frame after
112/// [`cut`](Producer::cut) / [`seek`](Producer::seek)).
113/// [`Producer::write`] returns this so a caller joining mid-stream can skip
114/// frames until the first keyframe instead of treating it as fatal.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
116#[error("missing keyframe: a group must open on a keyframe")]
117pub struct MissingKeyframe;
118
119/// An explicit endpoint precedes the last frame of an ordered video group.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
121#[error("video group endpoint precedes its last frame")]
122pub struct InvalidEnd;
123
124/// A frame's timestamp sits below the live edge earlier groups reached.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
126#[error("frame timestamp is below the live edge")]
127pub struct TimestampRewind;
128
129/// Encode and decode media frames over a moq-lite group.
130///
131/// Implementors decide how many [`Frame`]s map onto one moq-lite frame:
132/// Legacy and LOC write one media frame per moq-lite frame; CMAF can
133/// pack many samples into a single moof+mdat fragment.
134pub trait Container {
135 /// Container-specific error. Must be convertible from [`moq_net::Error`]
136 /// (so IO errors propagate), [`MissingKeyframe`], [`InvalidEnd`], and
137 /// [`TimestampRewind`] (so the producer can reject invalid group boundaries).
138 type Error: std::error::Error
139 + Send
140 + Sync
141 + Unpin
142 + From<moq_net::Error>
143 + From<MissingKeyframe>
144 + From<InvalidEnd>
145 + From<TimestampRewind>;
146
147 /// Encode one or more frames into a single moq-lite frame appended to `group`.
148 fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error>;
149
150 /// Poll the next moq-lite frame from `group` and decode it into media
151 /// frames. A single call may produce multiple media frames (e.g. all samples
152 /// in a CMAF fragment).
153 ///
154 /// Only `Ok(None)` signals the end of the group. `Ok(Some(batch))` may carry
155 /// an empty `batch`: a wire frame was consumed but decoded to no media frames
156 /// (e.g. a CMAF fragment with zero samples). That is not end-of-group; poll
157 /// again for the next batch. Callers accumulating frames must not treat an
158 /// empty batch as completion.
159 fn poll_read(
160 &self,
161 group: &mut moq_net::group::Consumer,
162 waiter: &kio::Waiter,
163 ) -> Poll<Result<Option<Vec<Frame>>, Self::Error>>;
164
165 /// Return the endpoint timestamp when `frame` carries empty-payload metadata.
166 ///
167 /// For video this bounds the preceding frame; for audio it bounds source samples
168 /// before terminal codec packets. A consumer never submits the marker to a decoder.
169 /// Formats without endpoint metadata use the default.
170 fn end(&self, _frame: &Frame) -> Option<moq_net::Timestamp> {
171 None
172 }
173
174 /// The media role, when this format represents an audio or video track.
175 fn kind(&self) -> Kind {
176 Kind::Data
177 }
178
179 /// Write any format-specific endpoint before the producer closes the group.
180 fn finish_group(
181 &self,
182 _group: &mut moq_net::group::Producer,
183 _end: Option<moq_net::Timestamp>,
184 ) -> Result<(), Self::Error> {
185 Ok(())
186 }
187
188 /// Async wrapper around [`Self::poll_read`]. Carries the same contract: only
189 /// `Ok(None)` ends the group, and `Ok(Some(batch))` may hand back an empty
190 /// `batch` (poll again for more), so a caller loop must key completion off
191 /// `None`, not an empty batch.
192 fn read(
193 &self,
194 group: &mut moq_net::group::Consumer,
195 ) -> impl std::future::Future<Output = Result<Option<Vec<Frame>>, Self::Error>>
196 where
197 Self: Sync,
198 {
199 async { kio::wait(|waiter| self.poll_read(group, waiter)).await }
200 }
201}