Skip to main content

moq_mux/codec/av1/
mod.rs

1//! AV1.
2//!
3//! Maps the AV1CodecConfigurationRecord (av1C) flag bits into the
4//! catalog's AV1 codec struct, and provides an [`Import`] that publishes
5//! raw AV1 bitstreams (OBU-framed) to a moq broadcast.
6
7mod import;
8mod split;
9
10pub use import::*;
11pub use split::*;
12
13use hang::catalog::AV1;
14
15/// AV1 parsing errors.
16#[derive(Debug, Clone, thiserror::Error)]
17#[non_exhaustive]
18pub enum Error {
19	#[error("OBU is too short")]
20	ObuTooShort,
21
22	#[error("OBU size too large")]
23	ObuSizeTooLarge,
24
25	#[error("not initialized")]
26	NotInitialized,
27
28	#[error("expected sequence header before any frames")]
29	MissingSequenceHeader,
30
31	#[error("missing timestamp")]
32	MissingTimestamp,
33
34	#[error("OBU header parse: {0}")]
35	ObuHeaderParse(std::sync::Arc<std::io::Error>),
36}
37
38impl From<std::io::Error> for Error {
39	fn from(err: std::io::Error) -> Self {
40		Error::ObuHeaderParse(std::sync::Arc::new(err))
41	}
42}
43
44pub type Result<T> = std::result::Result<T, Error>;
45
46/// Build a catalog [`VideoConfig`](hang::catalog::VideoConfig) for the `av01`
47/// shape from an AV1CodecConfigurationRecord (av1C).
48///
49/// Used by the enhanced-RTMP / FLV importer, where the av1C arrives out of band
50/// in the sequence-header tag (leading `0x81` marker) and the coded samples are
51/// raw OBU temporal units, so the record passes straight through as the catalog
52/// `description`. Resolution and color live in the inline sequence header, not
53/// the av1C, so `coded_width`/`coded_height` are left unset here.
54pub(crate) fn config_from_av1c(av1c: &[u8]) -> Result<hang::catalog::VideoConfig> {
55	// av1C: byte 0 = marker(1)|version(7) = 0x81, byte 1 = seq_profile(3)|seq_level_idx_0(5),
56	// byte 2 = seq_tier_0|high_bitdepth|twelve_bit|monochrome|subsampling_x|subsampling_y|sample_position(2).
57	if av1c.len() < 4 || av1c[0] != 0x81 {
58		return Err(Error::ObuTooShort);
59	}
60	let high_bitdepth = ((av1c[2] >> 6) & 0x01) == 1;
61	let twelve_bit = ((av1c[2] >> 5) & 0x01) == 1;
62
63	let mut config = hang::catalog::VideoConfig::new(AV1 {
64		profile: (av1c[1] >> 5) & 0x07,
65		level: av1c[1] & 0x1f,
66		tier: if ((av1c[2] >> 7) & 0x01) == 1 { 'H' } else { 'M' },
67		bitdepth: bitdepth(twelve_bit, high_bitdepth),
68		mono_chrome: ((av1c[2] >> 4) & 0x01) == 1,
69		chroma_subsampling_x: ((av1c[2] >> 3) & 0x01) == 1,
70		chroma_subsampling_y: ((av1c[2] >> 2) & 0x01) == 1,
71		chroma_sample_position: av1c[2] & 0x03,
72		..Default::default()
73	});
74	config.description = Some(bytes::Bytes::copy_from_slice(av1c));
75	Ok(config)
76}
77
78/// Map a parsed `mp4_atom::Av1c` (AV1CodecConfigurationRecord) to the
79/// hang catalog's AV1 codec struct.
80///
81/// Fills in profile, level, bit depth, and chroma sampling info. Color/HDR
82/// fields default to unspecified.
83pub(crate) fn av1_from_av1c(av1c: &mp4_atom::Av1c) -> AV1 {
84	AV1 {
85		profile: av1c.seq_profile,
86		level: av1c.seq_level_idx_0,
87		bitdepth: bitdepth(av1c.twelve_bit, av1c.high_bitdepth),
88		mono_chrome: av1c.monochrome,
89		chroma_subsampling_x: av1c.chroma_subsampling_x,
90		chroma_subsampling_y: av1c.chroma_subsampling_y,
91		chroma_sample_position: av1c.chroma_sample_position,
92		..Default::default()
93	}
94}
95
96/// Build an `mp4_atom::Av1c` (AV1CodecConfigurationRecord) from the hang
97/// catalog's AV1 codec struct, the inverse of [`av1_from_av1c`].
98///
99/// `config_obus` is left empty: moq-video publishes AV1 with the sequence
100/// header inline in the bitstream (the `.av01` in-band case, analogous to
101/// `hev1`/`avc3`), so the decoder reads it from the keyframe rather than the
102/// out-of-band config record. The catalog's color fields (color primaries,
103/// transfer characteristics, matrix coefficients, full range) have no slot in
104/// av1C; they live in the sequence header OBU instead.
105pub(crate) fn av1c_from_av1(av1: &AV1) -> mp4_atom::Av1c {
106	let (twelve_bit, high_bitdepth) = bitdepth_flags(av1.bitdepth);
107	mp4_atom::Av1c {
108		seq_profile: av1.profile,
109		seq_level_idx_0: av1.level,
110		seq_tier_0: av1.tier == 'H',
111		high_bitdepth,
112		twelve_bit,
113		monochrome: av1.mono_chrome,
114		chroma_subsampling_x: av1.chroma_subsampling_x,
115		chroma_subsampling_y: av1.chroma_subsampling_y,
116		chroma_sample_position: av1.chroma_sample_position,
117		initial_presentation_delay: None,
118		config_obus: Vec::new(),
119	}
120}
121
122/// Bit depth from the (twelve_bit, high_bitdepth) av1C flag pair
123/// (ISO/IEC 14496-15 + av1-isobmff ยง2.3.3).
124///
125/// Computes `8 + 2*high_bitdepth + 2*twelve_bit`.
126pub(crate) fn bitdepth(twelve_bit: bool, high_bitdepth: bool) -> u8 {
127	8 + 2 * u8::from(high_bitdepth) + 2 * u8::from(twelve_bit)
128}
129
130/// The (twelve_bit, high_bitdepth) av1C flag pair for a given bit depth, the
131/// inverse of [`bitdepth`]. 8-bit -> (false, false), 10-bit -> (false, true),
132/// 12-bit -> (true, true).
133pub(crate) fn bitdepth_flags(bitdepth: u8) -> (bool, bool) {
134	(bitdepth >= 12, bitdepth >= 10)
135}
136
137#[cfg(test)]
138mod tests {
139	use super::{av1_from_av1c, av1c_from_av1, bitdepth, bitdepth_flags};
140	use hang::catalog::AV1;
141
142	#[test]
143	fn maps_bitdepth_flags() {
144		assert_eq!(bitdepth(false, false), 8);
145		assert_eq!(bitdepth(false, true), 10);
146		assert_eq!(bitdepth(true, true), 12);
147		// twelve_bit=true with high_bitdepth=false is not a valid combination
148		// per the spec, but the additive formula still gives a defined answer.
149		assert_eq!(bitdepth(true, false), 10);
150	}
151
152	#[test]
153	fn bitdepth_flags_round_trip() {
154		for (depth, flags) in [(8, (false, false)), (10, (false, true)), (12, (true, true))] {
155			assert_eq!(bitdepth_flags(depth), flags);
156			let (twelve_bit, high_bitdepth) = flags;
157			assert_eq!(bitdepth(twelve_bit, high_bitdepth), depth);
158		}
159	}
160
161	#[test]
162	fn av1c_round_trips_catalog_fields() {
163		let av1 = AV1 {
164			profile: 0,
165			level: 8,
166			tier: 'H',
167			bitdepth: 10,
168			mono_chrome: false,
169			chroma_subsampling_x: true,
170			chroma_subsampling_y: true,
171			chroma_sample_position: 2,
172			// Color fields have no av1C slot; they live in the sequence header.
173			..Default::default()
174		};
175
176		let av1c = av1c_from_av1(&av1);
177		assert_eq!(av1c.seq_profile, 0);
178		assert_eq!(av1c.seq_level_idx_0, 8);
179		assert!(av1c.seq_tier_0);
180		assert!(av1c.high_bitdepth);
181		assert!(!av1c.twelve_bit);
182		assert!(av1c.chroma_subsampling_x);
183		assert!(av1c.chroma_subsampling_y);
184		assert_eq!(av1c.chroma_sample_position, 2);
185		assert!(av1c.config_obus.is_empty());
186
187		// The av1C-backed fields survive a round trip back to the catalog.
188		let back = av1_from_av1c(&av1c);
189		assert_eq!(back.profile, av1.profile);
190		assert_eq!(back.level, av1.level);
191		assert_eq!(back.bitdepth, av1.bitdepth);
192		assert_eq!(back.mono_chrome, av1.mono_chrome);
193		assert_eq!(back.chroma_subsampling_x, av1.chroma_subsampling_x);
194		assert_eq!(back.chroma_subsampling_y, av1.chroma_subsampling_y);
195		assert_eq!(back.chroma_sample_position, av1.chroma_sample_position);
196	}
197}