Skip to main content

moq_mux/codec/vp8/
mod.rs

1//! VP8.
2//!
3//! Parses the VP8 uncompressed frame header (RFC 6386 §9.1, §19.1) to detect
4//! key frames and read the encoded dimensions, and provides an [`Import`] that
5//! publishes a raw VP8 bitstream (one frame per buffer) to a moq broadcast.
6//!
7//! VP8 carries no out-of-band configuration record, so `vpcc` synthesizes the
8//! informational `vpcC` box the fMP4 exporter needs.
9
10mod import;
11
12pub use import::*;
13
14/// VP8 parsing errors.
15#[derive(Debug, Clone, thiserror::Error)]
16#[non_exhaustive]
17pub enum Error {
18	#[error("VP8 frame too short for tag")]
19	FrameTooShort,
20
21	#[error("VP8 key frame too short for header")]
22	KeyframeHeaderTooShort,
23
24	#[error("VP8 key frame start code mismatch")]
25	StartCodeMismatch,
26
27	#[error("empty VP8 frame")]
28	EmptyFrame,
29}
30
31/// A Result type alias for VP8 parsing.
32pub type Result<T> = std::result::Result<T, Error>;
33
34/// Fields parsed from a VP8 frame tag (RFC 6386 §9.1) plus, for key frames, the
35/// key-frame header (§19.1).
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub(crate) struct FrameHeader {
38	/// True for a key frame (frame_type bit clear), false for an interframe.
39	pub keyframe: bool,
40	/// Encoded `(width, height)`, present only on key frames.
41	pub dimensions: Option<(u16, u16)>,
42}
43
44impl FrameHeader {
45	/// Parse the leading bytes of a VP8 frame.
46	///
47	/// Reads the 3-byte frame tag, and on a key frame the 7-byte header that
48	/// follows (start code + dimensions). Interframes carry neither a start code
49	/// nor dimensions.
50	pub fn parse(data: &[u8]) -> Result<Self> {
51		if data.len() < 3 {
52			return Err(Error::FrameTooShort);
53		}
54
55		// 24-bit little-endian frame tag. Bit 0 is frame_type: 0 = key frame.
56		let tag = u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16);
57		let keyframe = tag & 0x1 == 0;
58
59		if !keyframe {
60			return Ok(Self {
61				keyframe: false,
62				dimensions: None,
63			});
64		}
65
66		if data.len() < 10 {
67			return Err(Error::KeyframeHeaderTooShort);
68		}
69		if !(data[3] == 0x9d && data[4] == 0x01 && data[5] == 0x2a) {
70			return Err(Error::StartCodeMismatch);
71		}
72
73		// 14-bit dimensions; the top 2 bits of each field are the scaling factor.
74		let width = (u16::from(data[6]) | (u16::from(data[7]) << 8)) & 0x3fff;
75		let height = (u16::from(data[8]) | (u16::from(data[9]) << 8)) & 0x3fff;
76
77		Ok(Self {
78			keyframe: true,
79			dimensions: Some((width, height)),
80		})
81	}
82}
83
84/// Build the informational `vpcC` configuration record for VP8.
85///
86/// VP8 is always 8-bit 4:2:0 with no out-of-band parameters, so the box carries
87/// fixed placeholders. Profile 0 / level 0 are the standard "unspecified"
88/// values. See https://www.webmproject.org/vp9/mp4/.
89pub(crate) fn vpcc() -> mp4_atom::VpcC {
90	mp4_atom::VpcC {
91		profile: 0,
92		level: 0,
93		bit_depth: 8,
94		..Default::default()
95	}
96}
97
98#[cfg(test)]
99mod tests {
100	use super::*;
101
102	#[test]
103	fn parses_keyframe_dimensions() {
104		// Key frame tag (bit 0 = 0) + start code + 320x240.
105		let frame = [0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00];
106		let header = FrameHeader::parse(&frame).expect("parse key frame");
107		assert!(header.keyframe);
108		assert_eq!(header.dimensions, Some((320, 240)));
109	}
110
111	#[test]
112	fn parses_interframe() {
113		// Interframe tag (bit 0 = 1); no start code or dimensions follow.
114		let frame = [0x31, 0x00, 0x00];
115		let header = FrameHeader::parse(&frame).expect("parse interframe");
116		assert!(!header.keyframe);
117		assert_eq!(header.dimensions, None);
118	}
119
120	#[test]
121	fn rejects_bad_start_code() {
122		let frame = [0x10, 0x00, 0x00, 0xde, 0xad, 0xbe, 0x40, 0x01, 0xf0, 0x00];
123		assert!(FrameHeader::parse(&frame).is_err());
124	}
125
126	#[test]
127	fn rejects_short_frame() {
128		assert!(FrameHeader::parse(&[0x10, 0x00]).is_err());
129	}
130}