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	config.container = hang::catalog::Container::Legacy;
76	Ok(config)
77}
78
79/// Map a parsed `mp4_atom::Av1c` (AV1CodecConfigurationRecord) to the
80/// hang catalog's AV1 codec struct.
81///
82/// Fills in profile, level, bit depth, and chroma sampling info. Color/HDR
83/// fields default to unspecified.
84pub(crate) fn av1_from_av1c(av1c: &mp4_atom::Av1c) -> AV1 {
85	AV1 {
86		profile: av1c.seq_profile,
87		level: av1c.seq_level_idx_0,
88		bitdepth: bitdepth(av1c.twelve_bit, av1c.high_bitdepth),
89		mono_chrome: av1c.monochrome,
90		chroma_subsampling_x: av1c.chroma_subsampling_x,
91		chroma_subsampling_y: av1c.chroma_subsampling_y,
92		chroma_sample_position: av1c.chroma_sample_position,
93		..Default::default()
94	}
95}
96
97/// Build an `mp4_atom::Av1c` (AV1CodecConfigurationRecord) from the hang
98/// catalog's AV1 codec struct, the inverse of [`av1_from_av1c`].
99///
100/// `config_obus` is left empty: moq-video publishes AV1 with the sequence
101/// header inline in the bitstream (the `.av01` in-band case, analogous to
102/// `hev1`/`avc3`), so the decoder reads it from the keyframe rather than the
103/// out-of-band config record. The catalog's color fields (color primaries,
104/// transfer characteristics, matrix coefficients, full range) have no slot in
105/// av1C; they live in the sequence header OBU instead.
106pub(crate) fn av1c_from_av1(av1: &AV1) -> mp4_atom::Av1c {
107	let (twelve_bit, high_bitdepth) = bitdepth_flags(av1.bitdepth);
108	mp4_atom::Av1c {
109		seq_profile: av1.profile,
110		seq_level_idx_0: av1.level,
111		seq_tier_0: av1.tier == 'H',
112		high_bitdepth,
113		twelve_bit,
114		monochrome: av1.mono_chrome,
115		chroma_subsampling_x: av1.chroma_subsampling_x,
116		chroma_subsampling_y: av1.chroma_subsampling_y,
117		chroma_sample_position: av1.chroma_sample_position,
118		initial_presentation_delay: None,
119		config_obus: Vec::new(),
120	}
121}
122
123/// Bit depth from the (twelve_bit, high_bitdepth) av1C flag pair
124/// (ISO/IEC 14496-15 + av1-isobmff ยง2.3.3).
125///
126/// Computes `8 + 2*high_bitdepth + 2*twelve_bit`.
127pub(crate) fn bitdepth(twelve_bit: bool, high_bitdepth: bool) -> u8 {
128	8 + 2 * u8::from(high_bitdepth) + 2 * u8::from(twelve_bit)
129}
130
131/// The (twelve_bit, high_bitdepth) av1C flag pair for a given bit depth, the
132/// inverse of [`bitdepth`]. 8-bit -> (false, false), 10-bit -> (false, true),
133/// 12-bit -> (true, true).
134pub(crate) fn bitdepth_flags(bitdepth: u8) -> (bool, bool) {
135	(bitdepth >= 12, bitdepth >= 10)
136}
137
138#[cfg(test)]
139mod tests {
140	use super::{av1_from_av1c, av1c_from_av1, bitdepth, bitdepth_flags};
141	use hang::catalog::AV1;
142
143	#[test]
144	fn maps_bitdepth_flags() {
145		assert_eq!(bitdepth(false, false), 8);
146		assert_eq!(bitdepth(false, true), 10);
147		assert_eq!(bitdepth(true, true), 12);
148		// twelve_bit=true with high_bitdepth=false is not a valid combination
149		// per the spec, but the additive formula still gives a defined answer.
150		assert_eq!(bitdepth(true, false), 10);
151	}
152
153	#[test]
154	fn bitdepth_flags_round_trip() {
155		for (depth, flags) in [(8, (false, false)), (10, (false, true)), (12, (true, true))] {
156			assert_eq!(bitdepth_flags(depth), flags);
157			let (twelve_bit, high_bitdepth) = flags;
158			assert_eq!(bitdepth(twelve_bit, high_bitdepth), depth);
159		}
160	}
161
162	#[test]
163	fn av1c_round_trips_catalog_fields() {
164		let av1 = AV1 {
165			profile: 0,
166			level: 8,
167			tier: 'H',
168			bitdepth: 10,
169			mono_chrome: false,
170			chroma_subsampling_x: true,
171			chroma_subsampling_y: true,
172			chroma_sample_position: 2,
173			// Color fields have no av1C slot; they live in the sequence header.
174			..Default::default()
175		};
176
177		let av1c = av1c_from_av1(&av1);
178		assert_eq!(av1c.seq_profile, 0);
179		assert_eq!(av1c.seq_level_idx_0, 8);
180		assert!(av1c.seq_tier_0);
181		assert!(av1c.high_bitdepth);
182		assert!(!av1c.twelve_bit);
183		assert!(av1c.chroma_subsampling_x);
184		assert!(av1c.chroma_subsampling_y);
185		assert_eq!(av1c.chroma_sample_position, 2);
186		assert!(av1c.config_obus.is_empty());
187
188		// The av1C-backed fields survive a round trip back to the catalog.
189		let back = av1_from_av1c(&av1c);
190		assert_eq!(back.profile, av1.profile);
191		assert_eq!(back.level, av1.level);
192		assert_eq!(back.bitdepth, av1.bitdepth);
193		assert_eq!(back.mono_chrome, av1.mono_chrome);
194		assert_eq!(back.chroma_subsampling_x, av1.chroma_subsampling_x);
195		assert_eq!(back.chroma_subsampling_y, av1.chroma_subsampling_y);
196		assert_eq!(back.chroma_sample_position, av1.chroma_sample_position);
197	}
198}