Skip to main content

moq_mux/codec/vp9/
mod.rs

1//! VP9.
2//!
3//! Parses the VP9 uncompressed frame header (VP9 bitstream spec §6.2, §7.2) to
4//! detect key frames and read the dimensions, profile, bit depth, and color
5//! config, and provides an [`Import`] that publishes a raw VP9 bitstream (one
6//! frame per buffer) to a moq broadcast.
7//!
8//! VP9 stores its codec config in a `vpcC` box, so `vpcc` builds that record
9//! for the fMP4 exporter. It's the exact inverse of the `Vp09` import mapping.
10
11mod import;
12
13pub use import::*;
14
15use hang::catalog::VP9;
16
17/// VP9 parsing errors.
18#[derive(Debug, Clone, thiserror::Error)]
19#[non_exhaustive]
20pub enum Error {
21	#[error("invalid VP9 frame marker")]
22	InvalidFrameMarker,
23
24	#[error("invalid VP9 sync code")]
25	InvalidSyncCode,
26
27	#[error("VP9 header truncated")]
28	Truncated,
29
30	#[error("empty VP9 frame")]
31	EmptyFrame,
32}
33
34/// A Result type alias for VP9 parsing.
35pub type Result<T> = std::result::Result<T, Error>;
36
37/// VP9 key-frame sync code (VP9 spec §6.2, `frame_sync_code`).
38const SYNC_CODE: u32 = 0x49_8342;
39
40/// Fields parsed from a VP9 uncompressed frame header.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub(crate) struct FrameHeader {
43	/// True for a key frame (`frame_type == 0`).
44	pub keyframe: bool,
45	/// Encoded `(width, height)` and color config, present only on key frames.
46	pub key: Option<KeyFrame>,
47}
48
49/// The parts of a VP9 key-frame header we surface to the catalog.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub(crate) struct KeyFrame {
52	pub width: u16,
53	pub height: u16,
54	pub profile: u8,
55	pub bit_depth: u8,
56	/// `vpcC` chroma subsampling enum (0 = 4:2:0 vertical, 1 = 4:2:0 colocated,
57	/// 2 = 4:2:2, 3 = 4:4:4).
58	pub chroma_subsampling: u8,
59	/// CICP matrix coefficients derived from the VP9 `color_space`.
60	pub matrix_coefficients: u8,
61	pub full_range: bool,
62}
63
64impl FrameHeader {
65	/// Parse the VP9 uncompressed header.
66	///
67	/// Reads only as far as the frame size (the last field we care about);
68	/// everything after it is left untouched.
69	pub fn parse(data: &[u8]) -> Result<Self> {
70		let mut r = BitReader::new(data);
71
72		if r.read(2)? != 0b10 {
73			return Err(Error::InvalidFrameMarker);
74		}
75
76		let profile_low = r.read(1)?;
77		let profile_high = r.read(1)?;
78		let profile = ((profile_high << 1) | profile_low) as u8;
79		if profile == 3 {
80			r.skip(1)?; // reserved_zero
81		}
82
83		// show_existing_frame: displays a previously decoded frame, no header follows.
84		if r.read(1)? == 1 {
85			r.skip(3)?; // frame_to_show_map_idx
86			return Ok(Self {
87				keyframe: false,
88				key: None,
89			});
90		}
91
92		let keyframe = r.read(1)? == 0; // frame_type: 0 = KEY_FRAME
93		r.skip(2)?; // show_frame, error_resilient_mode
94
95		if !keyframe {
96			return Ok(Self {
97				keyframe: false,
98				key: None,
99			});
100		}
101
102		if r.read(24)? != SYNC_CODE {
103			return Err(Error::InvalidSyncCode);
104		}
105
106		// color_config (VP9 spec §6.2.2).
107		let bit_depth = if profile >= 2 {
108			if r.read(1)? == 1 { 12 } else { 10 }
109		} else {
110			8
111		};
112		let color_space = r.read(3)? as u8;
113
114		const CS_RGB: u8 = 7;
115		let (subsampling_x, subsampling_y, full_range);
116		if color_space != CS_RGB {
117			full_range = r.read(1)? == 1;
118			if profile == 1 || profile == 3 {
119				subsampling_x = r.read(1)? == 1;
120				subsampling_y = r.read(1)? == 1;
121				r.skip(1)?; // reserved_zero
122			} else {
123				// Profiles 0 and 2 are 4:2:0 only.
124				(subsampling_x, subsampling_y) = (true, true);
125			}
126		} else {
127			// CS_RGB is full-range 4:4:4, allowed only with profile 1 or 3.
128			full_range = true;
129			(subsampling_x, subsampling_y) = (false, false);
130			if profile == 1 || profile == 3 {
131				r.skip(1)?; // reserved_zero
132			}
133		}
134
135		// frame_size (VP9 spec §6.2.3): each field is `value - 1`.
136		let width = (r.read(16)? + 1) as u16;
137		let height = (r.read(16)? + 1) as u16;
138
139		Ok(Self {
140			keyframe: true,
141			key: Some(KeyFrame {
142				width,
143				height,
144				profile,
145				bit_depth,
146				chroma_subsampling: chroma_subsampling(subsampling_x, subsampling_y),
147				matrix_coefficients: matrix_coefficients(color_space),
148				full_range,
149			}),
150		})
151	}
152}
153
154impl KeyFrame {
155	/// Build the catalog VP9 config from a parsed key frame.
156	///
157	/// Color primaries and transfer characteristics aren't recoverable from the
158	/// VP9 `color_space` alone, so they're left unspecified (2), matching what
159	/// remuxers like ffmpeg do. `level` is the minimum that fits the picture
160	/// size (see [`level_for`]).
161	pub fn to_catalog(self) -> VP9 {
162		VP9 {
163			profile: self.profile,
164			level: level_for(self.width, self.height),
165			bit_depth: self.bit_depth,
166			chroma_subsampling: self.chroma_subsampling,
167			color_primaries: 2,
168			transfer_characteristics: 2,
169			matrix_coefficients: self.matrix_coefficients,
170			full_range: self.full_range,
171		}
172	}
173}
174
175/// Build a catalog [`VideoConfig`](hang::catalog::VideoConfig) from a VP9 frame,
176/// or `None` if the frame is not a key frame.
177///
178/// Used by the enhanced-RTMP / FLV importer. VP9 carries its config in band (the
179/// uncompressed key-frame header), so unlike H.264/H.265/AV1 there is no
180/// out-of-band record to pass through as `description`; the config is read from
181/// the key frame itself.
182pub(crate) fn config_from_keyframe(data: &[u8]) -> Result<Option<hang::catalog::VideoConfig>> {
183	let Some(key) = FrameHeader::parse(data)?.key else {
184		return Ok(None);
185	};
186	let (width, height) = (key.width, key.height);
187	let mut config = hang::catalog::VideoConfig::new(key.to_catalog());
188	config.coded_width = Some(width as u32);
189	config.coded_height = Some(height as u32);
190	config.container = hang::catalog::Container::Legacy;
191	Ok(Some(config))
192}
193
194/// `vpcC` chroma subsampling enum from the VP9 `subsampling_x`/`subsampling_y`
195/// flags. VP9 doesn't signal chroma siting, so 4:2:0 maps to "colocated" (1),
196/// the value encoders conventionally write.
197fn chroma_subsampling(x: bool, y: bool) -> u8 {
198	match (x, y) {
199		(true, true) => 1,   // 4:2:0
200		(true, false) => 2,  // 4:2:2
201		(false, false) => 3, // 4:4:4
202		(false, true) => 0,  // 4:4:0 (rare)
203	}
204}
205
206/// CICP matrix coefficients for a VP9 `color_space` (VP9 spec Table in §7.2.2),
207/// matching ffmpeg's `vp9_colorspaces`. Unknown/reserved map to unspecified (2).
208fn matrix_coefficients(color_space: u8) -> u8 {
209	match color_space {
210		1 => 5, // CS_BT_601  -> BT.470BG
211		2 => 1, // CS_BT_709  -> BT.709
212		3 => 6, // CS_SMPTE_170 -> SMPTE 170M
213		4 => 7, // CS_SMPTE_240 -> SMPTE 240M
214		5 => 9, // CS_BT_2020 -> BT.2020 non-constant luminance
215		7 => 0, // CS_RGB     -> identity
216		_ => 2, // CS_UNKNOWN / CS_RESERVED -> unspecified
217	}
218}
219
220/// The lowest VP9 level whose `MaxLumaPictureSize` fits `width * height` (VP9
221/// spec Annex A). Framerate and bitrate aren't known from the header, so this
222/// is a picture-size lower bound rather than the exact encoded level.
223fn level_for(width: u16, height: u16) -> u8 {
224	let area = width as u64 * height as u64;
225	// (level, MaxLumaPictureSize), ascending.
226	const LEVELS: &[(u8, u64)] = &[
227		(10, 36_864),
228		(11, 73_728),
229		(20, 122_880),
230		(21, 245_760),
231		(30, 552_960),
232		(31, 983_040),
233		(40, 2_228_224),
234		(50, 8_912_896),
235		(60, 35_651_584),
236		(62, 70_254_592),
237	];
238	LEVELS
239		.iter()
240		.find(|(_, max)| area <= *max)
241		.map(|(level, _)| *level)
242		.unwrap_or(62)
243}
244
245/// Build the `vpcC` configuration record for VP9.
246///
247/// The exact inverse of the `Vp09` -> catalog mapping in the fMP4 importer.
248pub(crate) fn vpcc(vp9: &VP9) -> mp4_atom::VpcC {
249	mp4_atom::VpcC {
250		profile: vp9.profile,
251		level: vp9.level,
252		bit_depth: vp9.bit_depth,
253		chroma_subsampling: vp9.chroma_subsampling,
254		video_full_range_flag: vp9.full_range,
255		color_primaries: vp9.color_primaries,
256		transfer_characteristics: vp9.transfer_characteristics,
257		matrix_coefficients: vp9.matrix_coefficients,
258		codec_initialization_data: Vec::new(),
259	}
260}
261
262/// Minimal MSB-first bit reader over a byte slice.
263struct BitReader<'a> {
264	data: &'a [u8],
265	bit: usize,
266}
267
268impl<'a> BitReader<'a> {
269	fn new(data: &'a [u8]) -> Self {
270		Self { data, bit: 0 }
271	}
272
273	fn read(&mut self, n: u32) -> Result<u32> {
274		let mut value = 0;
275		for _ in 0..n {
276			let byte = self.bit / 8;
277			if byte >= self.data.len() {
278				return Err(Error::Truncated);
279			}
280			let shift = 7 - (self.bit % 8);
281			value = (value << 1) | u32::from((self.data[byte] >> shift) & 1);
282			self.bit += 1;
283		}
284		Ok(value)
285	}
286
287	fn skip(&mut self, n: u32) -> Result<()> {
288		self.read(n).map(|_| ())
289	}
290}
291
292#[cfg(test)]
293mod tests {
294	use super::*;
295
296	// A real-shape VP9 key frame: profile 0, show_frame, 8-bit, CS_BT_601,
297	// studio range, 4:2:0, 320x240. Bytes after the frame size are irrelevant.
298	const KEYFRAME_320X240: &[u8] = &[0x82, 0x49, 0x83, 0x42, 0x20, 0x13, 0xf0, 0x0e, 0xf0, 0x00];
299
300	#[test]
301	fn parses_keyframe() {
302		let header = FrameHeader::parse(KEYFRAME_320X240).expect("parse key frame");
303		assert!(header.keyframe);
304		let key = header.key.expect("key frame fields");
305		assert_eq!((key.width, key.height), (320, 240));
306		assert_eq!(key.profile, 0);
307		assert_eq!(key.bit_depth, 8);
308		assert_eq!(key.chroma_subsampling, 1); // 4:2:0
309		assert_eq!(key.matrix_coefficients, 5); // CS_BT_601 -> BT.470BG
310		assert!(!key.full_range);
311	}
312
313	#[test]
314	fn keyframe_to_catalog() {
315		let key = FrameHeader::parse(KEYFRAME_320X240).unwrap().key.unwrap();
316		let vp9 = key.to_catalog();
317		assert_eq!(vp9.profile, 0);
318		assert_eq!(vp9.bit_depth, 8);
319		assert_eq!(vp9.level, 20); // 320x240 = 76800, above level 1.1's 73728
320		assert_eq!(vp9.color_primaries, 2); // unspecified
321	}
322
323	#[test]
324	fn parses_interframe() {
325		// Non-key frame: marker(10) profile(00) show_existing(0) frame_type(1) ...
326		// 0b10_00_0_1_00 = 0x84.
327		let header = FrameHeader::parse(&[0x84, 0x00, 0x00]).expect("parse interframe");
328		assert!(!header.keyframe);
329		assert!(header.key.is_none());
330	}
331
332	#[test]
333	fn parses_show_existing() {
334		// marker(10) profile(00) show_existing(1) idx(000) = 0b10_00_1_000 = 0x88.
335		let header = FrameHeader::parse(&[0x88]).expect("parse show_existing");
336		assert!(!header.keyframe);
337		assert!(header.key.is_none());
338	}
339
340	#[test]
341	fn rejects_bad_sync() {
342		let mut frame = KEYFRAME_320X240.to_vec();
343		frame[1] = 0x00; // corrupt the sync code
344		assert!(FrameHeader::parse(&frame).is_err());
345	}
346
347	#[test]
348	fn vpcc_round_trips_catalog() {
349		let vp9 = VP9 {
350			profile: 2,
351			level: 31,
352			bit_depth: 10,
353			chroma_subsampling: 1,
354			color_primaries: 9,
355			transfer_characteristics: 16,
356			matrix_coefficients: 9,
357			full_range: true,
358		};
359		let vpcc = vpcc(&vp9);
360		assert_eq!(vpcc.profile, 2);
361		assert_eq!(vpcc.bit_depth, 10);
362		assert_eq!(vpcc.matrix_coefficients, 9);
363		assert!(vpcc.video_full_range_flag);
364	}
365}