Skip to main content

moq_mux/codec/opus/
mod.rs

1//! Opus.
2//!
3//! RFC 7845 OpusHead parse and encode lives in [`Config`]. [`Import`]
4//! publishes raw Opus frames (no Ogg framing) to a moq broadcast.
5
6mod import;
7
8pub use import::*;
9
10use bytes::{Buf, Bytes};
11
12const OPUS_HEAD: u64 = u64::from_be_bytes(*b"OpusHead");
13
14/// Opus parsing errors.
15#[derive(Debug, Clone, thiserror::Error)]
16#[non_exhaustive]
17pub enum Error {
18	/// The OpusHead packet was shorter than the 19-byte minimum (RFC 7845 §5.1).
19	#[error("OpusHead must be at least 19 bytes")]
20	HeadTooShort,
21
22	/// The packet did not start with the `OpusHead` magic signature.
23	#[error("invalid OpusHead signature")]
24	InvalidSignature,
25
26	/// [`Config::encode`] was asked to emit an OpusHead for a channel count other
27	/// than mono or stereo; channel mapping family 0 only covers 1 or 2 channels.
28	#[error("channel mapping family 0 only supports mono/stereo (got {0} channels)")]
29	UnsupportedChannelCount(u32),
30}
31
32pub type Result<T> = std::result::Result<T, Error>;
33
34/// Typed Opus configuration mirroring the parsed fields of an OpusHead packet.
35pub struct Config {
36	pub sample_rate: u32,
37	pub channel_count: u32,
38}
39
40impl Config {
41	/// Parse an OpusHead buffer (RFC 7845 §5.1).
42	///
43	/// Verifies the magic signature; reads channel count and sample rate;
44	/// ignores pre-skip, gain, and channel mapping. Any trailing bytes are
45	/// consumed.
46	pub fn parse<T: Buf>(buf: &mut T) -> Result<Self> {
47		if buf.remaining() < 19 {
48			return Err(Error::HeadTooShort);
49		}
50		let signature = buf.get_u64();
51		if signature != OPUS_HEAD {
52			return Err(Error::InvalidSignature);
53		}
54
55		buf.advance(1); // Skip version
56		let channel_count = buf.get_u8() as u32;
57		buf.advance(2); // Skip pre-skip
58		let sample_rate = buf.get_u32_le();
59
60		// Skip gain, channel mapping until if/when we support them.
61		if buf.remaining() > 0 {
62			buf.advance(buf.remaining());
63		}
64
65		Ok(Self {
66			sample_rate,
67			channel_count,
68		})
69	}
70
71	/// Encode the minimal OpusHead packet (19 bytes; channel mapping family
72	/// 0, zero pre-skip and gain).
73	///
74	/// Errors with [`Error::UnsupportedChannelCount`] unless `channel_count` is 1
75	/// or 2 — mapping family 0 is only defined for mono/stereo per RFC 7845 §5.1.
76	/// Multi-channel streams need family 1 with a channel mapping table, which
77	/// this helper does not emit.
78	pub fn encode(&self) -> Result<Bytes> {
79		if !(1..=2).contains(&self.channel_count) {
80			return Err(Error::UnsupportedChannelCount(self.channel_count));
81		}
82		let mut head = Vec::with_capacity(19);
83		head.extend_from_slice(b"OpusHead");
84		head.push(1); // version
85		head.push(self.channel_count as u8);
86		head.extend_from_slice(&0u16.to_le_bytes()); // pre-skip
87		head.extend_from_slice(&self.sample_rate.to_le_bytes());
88		head.extend_from_slice(&0i16.to_le_bytes()); // output gain
89		head.push(0); // channel mapping family (0 = mono/stereo)
90		Ok(Bytes::from(head))
91	}
92}
93
94#[cfg(test)]
95mod tests {
96	use super::*;
97
98	#[test]
99	fn parses_valid_opus_head() {
100		let cfg = Config {
101			sample_rate: 48000,
102			channel_count: 2,
103		};
104		let encoded = cfg.encode().unwrap();
105		assert_eq!(encoded.len(), 19);
106		let parsed = Config::parse(&mut encoded.as_ref()).unwrap();
107		assert_eq!(parsed.sample_rate, 48000);
108		assert_eq!(parsed.channel_count, 2);
109	}
110
111	#[test]
112	fn parse_rejects_invalid_signature() {
113		let mut bytes = Config {
114			sample_rate: 48000,
115			channel_count: 1,
116		}
117		.encode()
118		.unwrap()
119		.to_vec();
120		bytes[0] = b'X';
121		assert!(Config::parse(&mut bytes.as_slice()).is_err());
122	}
123
124	#[test]
125	fn encode_rejects_multichannel() {
126		let err = Config {
127			sample_rate: 48000,
128			channel_count: 6,
129		}
130		.encode()
131		.unwrap_err();
132		assert!(matches!(err, Error::UnsupportedChannelCount(6)));
133	}
134}