moq_mux/codec/mp3.rs
1//! MP3 (MPEG-1/2/2.5 Audio Layer III).
2//!
3//! Audio carried verbatim: each frame is published whole. The header is parsed
4//! only for the catalog config (sample rate, channels); the audio is never
5//! decoded and there is no out-of-band configuration record. [`Import`] publishes
6//! raw MP3 frames to a moq broadcast.
7
8use crate::catalog::hang::CatalogExt;
9use crate::container::Frame;
10use moq_net::Timestamp;
11
12/// MP3 parsing errors.
13#[derive(Debug, Clone, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16 /// The buffer was shorter than the 4-byte MPEG audio frame header.
17 #[error("MP3 frame header must be at least 4 bytes")]
18 HeaderTooShort,
19
20 /// The 11-bit frame sync (`0xFFE`) was missing.
21 #[error("missing MP3 frame sync")]
22 MissingSync,
23
24 /// The MPEG version field was the reserved value `01`.
25 #[error("reserved MPEG version")]
26 ReservedVersion,
27
28 /// The layer field was not Layer III, so this is not an MP3 frame (Layer I/II
29 /// are MP1/MP2, the reserved value is invalid).
30 #[error("not an MPEG Layer III (MP3) frame")]
31 NotLayer3,
32
33 /// The sample-rate index was the reserved value `11`.
34 #[error("reserved MP3 sample rate")]
35 ReservedSampleRate,
36}
37
38pub type Result<T> = std::result::Result<T, Error>;
39
40/// Typed MP3 configuration parsed from an MPEG audio frame header.
41pub struct Config {
42 /// Sampling frequency in Hz.
43 pub sample_rate: u32,
44 /// Channel count (1 for the mono channel mode, 2 otherwise).
45 pub channel_count: u32,
46}
47
48impl Config {
49 /// Parse the catalog config from the start of an MPEG Layer III frame.
50 ///
51 /// Reads the 4-byte frame header (ISO/IEC 11172-3 ยง2.4.1.2): verifies the
52 /// frame sync and that the layer is III, then derives the sample rate from
53 /// the version + sample-rate index and the channel count from the channel
54 /// mode. The buffer is not advanced; the frame is published whole.
55 pub fn parse(data: &[u8]) -> Result<Self> {
56 if data.len() < 4 {
57 return Err(Error::HeaderTooShort);
58 }
59
60 // 11-bit frame sync: all of byte 0 plus the top 3 bits of byte 1.
61 if data[0] != 0xFF || (data[1] & 0xE0) != 0xE0 {
62 return Err(Error::MissingSync);
63 }
64
65 let version = (data[1] >> 3) & 0x03;
66 let layer = (data[1] >> 1) & 0x03;
67 // Layer is encoded inverted: 0b01 == Layer III.
68 if layer != 0b01 {
69 return Err(Error::NotLayer3);
70 }
71
72 let sr_index = ((data[2] >> 2) & 0x03) as usize;
73 if sr_index == 0b11 {
74 return Err(Error::ReservedSampleRate);
75 }
76
77 let sample_rate = match version {
78 0b11 => [44100, 48000, 32000][sr_index], // MPEG-1
79 0b10 => [22050, 24000, 16000][sr_index], // MPEG-2
80 0b00 => [11025, 12000, 8000][sr_index], // MPEG-2.5
81 _ => return Err(Error::ReservedVersion),
82 };
83
84 // Channel mode 0b11 is single channel (mono); the rest are two-channel.
85 let channel_count = if (data[3] >> 6) & 0x03 == 0b11 { 1 } else { 2 };
86
87 Ok(Self {
88 sample_rate,
89 channel_count,
90 })
91 }
92}
93
94/// MP3 importer.
95///
96/// Publishes raw MP3 frames to a single moq track. Build it with [`new`](Self::new),
97/// passing the track producer and the [`catalog::Reserved`](crate::catalog::Reserved)
98/// it reserves its rendition from.
99///
100/// Every MP3 frame is independently decodable, so [`decode`](Self::decode) marks only the first frame
101/// of each group a keyframe (the rest extend it): frames accumulate into the current group until the
102/// caller [`cut`](Self::cut)s or [`seek`](Self::seek)s. The [`import::Track`](crate::import::Track)
103/// facade passes that through, so boundaries are the caller's either way: cut per frame for one
104/// group per frame, or at a segment cadence to align with video. MP3 carries its config in band, so
105/// the rendition has no out-of-band description.
106pub struct Import {
107 track: crate::container::Producer<crate::catalog::hang::Container, hang::catalog::AudioConfig>,
108}
109
110impl Import {
111 /// Publish on an existing track producer with a resolved catalog config.
112 ///
113 /// Build one from a frame header with [`config`], or from an out-of-band [`Config`] via `into()`.
114 /// The rendition publishes immediately.
115 pub fn new<E: CatalogExt>(
116 track: moq_net::track::Producer,
117 reserved: crate::catalog::Reserved<E>,
118 config: hang::catalog::AudioConfig,
119 ) -> crate::Result<Self> {
120 tracing::debug!(name = ?track.name(), ?config, "starting track");
121 // The caller's config names the container; the writer is built from that same value so the
122 // wire cannot disagree with what the rendition advertises.
123 let wire = crate::catalog::hang::Container::try_from(&config)?;
124 // Build the writer before advertising the rendition: it is fallible (enrolling the track in
125 // the broadcast timeline can collide), and a rendition published for a track we then fail to
126 // produce would be advertised to consumers but never served.
127 let track = reserved.audio(track, wire, config)?;
128 Ok(Self { track })
129 }
130
131 /// A watch-only handle to this track's subscriber demand.
132 pub fn demand(&self) -> moq_net::track::Demand {
133 self.track.track().demand()
134 }
135
136 /// Record a locally encoded frame's transport handoff for catalog jitter measurement.
137 pub fn flush(&mut self, timestamp: moq_net::Timestamp, now: std::time::Instant) -> crate::Result<()> {
138 self.track.flush(timestamp, now)
139 }
140
141 /// Finish the track, flushing the current group.
142 pub fn finish(&mut self) -> crate::Result<()> {
143 self.track.finish()?;
144 Ok(())
145 }
146
147 /// Abort the track with `err` instead of finishing it cleanly, so subscribers
148 /// see the real cause rather than [`moq_net::Error::Dropped`]. Consumes this importer.
149 pub fn abort(self, err: moq_net::Error) {
150 self.track.abort(err);
151 }
152
153 /// Publish what the track measured (bitrate, jitter) into the catalog rendition, filling only
154 /// the fields its config didn't supply.
155 /// Cut the current group at `end` without finishing the track.
156 pub fn cut(&mut self, end: Option<moq_net::Timestamp>) -> crate::Result<()> {
157 self.track.cut(end)?;
158 Ok(())
159 }
160
161 /// Mark a timeline break and restart measurement without lowering advertised values.
162 pub fn discontinuity(&mut self) -> crate::Result<()> {
163 self.track.discontinuity()
164 }
165
166 /// Close the current group and open the next one at `sequence`.
167 pub fn seek(&mut self, sequence: u64) -> crate::Result<()> {
168 self.track.seek(sequence)?;
169 Ok(())
170 }
171
172 /// Publish one MP3 frame, stamping `pts` or a wall clock when absent.
173 ///
174 /// MP3 is independently decodable, so the frame is marked a keyframe only when it starts a group
175 /// (see [`Producer::needs_keyframe`](crate::container::Producer::needs_keyframe)); otherwise it
176 /// extends the current group. The caller bounds groups via [`cut`](Self::cut) / [`seek`](Self::seek).
177 pub fn decode<B: moq_net::IntoBytes>(&mut self, frame: B, pts: Option<Timestamp>) -> crate::Result<()> {
178 let timestamp = self.track.timestamp(pts)?;
179 // Only the first frame of each group is a keyframe, so the group spans until the caller cuts
180 // instead of opening one group (one QUIC stream) per packet.
181 let keyframe = self.track.needs_keyframe();
182 self.track.write(Frame {
183 timestamp,
184 payload: frame.into_bytes(),
185 keyframe,
186 duration: None,
187 })?;
188 Ok(())
189 }
190}
191
192/// Build a catalog config from an MP3 frame header. Errors on a malformed or empty buffer.
193pub fn config(init: &[u8]) -> crate::Result<hang::catalog::AudioConfig> {
194 Ok(Config::parse(init)?.into())
195}
196
197impl From<Config> for hang::catalog::AudioConfig {
198 /// Build a catalog config from a config resolved out of band (e.g. gstreamer caps).
199 fn from(config: Config) -> Self {
200 let mut audio =
201 hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Mp3, config.sample_rate, config.channel_count);
202 audio.container = hang::catalog::Container::Legacy;
203 audio
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn parses_mpeg1_stereo() {
213 // MPEG-1 Layer III, 128 kbps, 44.1 kHz, joint stereo.
214 let header = [0xFF, 0xFB, 0x90, 0x44];
215 let cfg = Config::parse(&header).unwrap();
216 assert_eq!(cfg.sample_rate, 44100);
217 assert_eq!(cfg.channel_count, 2);
218 }
219
220 #[test]
221 fn parses_mpeg1_mono() {
222 // Same header but channel mode 0b11 (mono) in the top bits of byte 3.
223 let header = [0xFF, 0xFB, 0x90, 0xC4];
224 let cfg = Config::parse(&header).unwrap();
225 assert_eq!(cfg.channel_count, 1);
226 }
227
228 #[test]
229 fn parses_mpeg2_sample_rate() {
230 // MPEG-2 (version 0b10), Layer III, sample-rate index 0 -> 22.05 kHz.
231 let header = [0xFF, 0xF3, 0x90, 0x44];
232 let cfg = Config::parse(&header).unwrap();
233 assert_eq!(cfg.sample_rate, 22050);
234 }
235
236 #[test]
237 fn rejects_layer2() {
238 // Layer II is 0b10, i.e. an MP2 (not MP3) frame.
239 let header = [0xFF, 0xFD, 0x90, 0x44];
240 assert!(matches!(Config::parse(&header), Err(Error::NotLayer3)));
241 }
242
243 #[test]
244 fn rejects_missing_sync() {
245 assert!(matches!(
246 Config::parse(&[0x00, 0x00, 0x00, 0x00]),
247 Err(Error::MissingSync)
248 ));
249 }
250
251 #[test]
252 fn rejects_short() {
253 assert!(matches!(Config::parse(&[0xFF, 0xFB]), Err(Error::HeaderTooShort)));
254 }
255}