Skip to main content

moq_mux/codec/h265/
mod.rs

1//! H.265 / HEVC.
2//!
3//! The H.265 analogue of [`crate::codec::h264`]. Parses SPS NAL units
4//! and HEVCDecoderConfigurationRecord blobs. The [`Hvc1`] transmuxer
5//! rewrites Annex-B input (inline VPS/SPS/PPS) as length-prefixed NALU
6//! + out-of-band hvcC. [`Export`] is the single-rendition Annex-B
7//!   exporter; [`Import`] takes either shape, driven by a
8//!   [`Split`] for hev1 or by `hvc1_frame` for hvc1.
9
10mod export;
11mod import;
12mod split;
13
14pub use export::*;
15pub use import::*;
16pub use split::*;
17
18use bytes::{Buf, BufMut, Bytes, BytesMut};
19use scuffle_h265::{NALUnitType, SpsNALUnit};
20
21/// Wrap one hvc1 (length-prefixed NALU) access unit as a single
22/// [`Frame`](crate::container::Frame), with the keyframe flag set when it
23/// carries an IRAP NAL.
24///
25/// hvc1 is not a stream: each access unit arrives whole with its NALU
26/// `length_size` known out-of-band from the hvcC (`super::Hvcc::parse(hvcc).length_size`).
27/// The payload is passed through verbatim.
28pub(crate) fn hvc1_frame(
29	data: impl moq_net::IntoBytes,
30	length_size: usize,
31	pts: moq_net::Timestamp,
32) -> crate::Result<crate::container::Frame> {
33	let keyframe = hvc1_is_keyframe(data.as_ref(), length_size);
34	Ok(crate::container::Frame {
35		timestamp: pts,
36		payload: data.into_bytes(),
37		keyframe,
38		duration: None,
39	})
40}
41
42/// Detect whether an hvc1-shaped (length-prefixed) buffer contains an IRAP slice.
43fn hvc1_is_keyframe(data: &[u8], length_size: usize) -> bool {
44	let Ok(nals) = crate::codec::annexb::length_prefixed_nals(data, length_size) else {
45		return false;
46	};
47	nals.map_while(std::result::Result::ok)
48		.any(|nal| nal.first().is_some_and(|header| is_irap(split::nal_unit_type(*header))))
49}
50
51/// True for the IRAP (intra random access point) NAL types, the ones that open a group.
52/// HEVC random access is broader than H.264's single IDR type.
53pub(crate) fn is_irap(nal_type: NALUnitType) -> bool {
54	matches!(
55		nal_type,
56		NALUnitType::IdrWRadl
57			| NALUnitType::IdrNLp
58			| NALUnitType::BlaNLp
59			| NALUnitType::BlaWRadl
60			| NALUnitType::BlaWLp
61			| NALUnitType::CraNut
62	)
63}
64
65/// H.265 parsing and transform errors.
66#[derive(Debug, Clone, thiserror::Error)]
67#[non_exhaustive]
68pub enum Error {
69	#[error("NAL unit is too short")]
70	NalTooShort,
71
72	#[error("{0} too large for hvcC length field ({1} > {max})", max = u16::MAX)]
73	NalTooLargeForHvcc(&'static str, usize),
74
75	#[error("too many {0} for hvcC ({1} > {max})", max = u16::MAX)]
76	TooManyNals(&'static str, usize),
77
78	#[error("NAL too large for 4-byte length prefix")]
79	NalTooLarge,
80
81	#[error("failed to parse SPS NAL unit")]
82	SpsParse,
83
84	#[error("missing level_idc in SPS")]
85	MissingLevelIdc,
86
87	#[error("forbidden zero bit is not zero")]
88	ForbiddenZeroBit,
89
90	#[error("not initialized")]
91	NotInitialized,
92
93	#[error("expected SPS before any frames")]
94	MissingSps,
95
96	#[error("missing timestamp")]
97	MissingTimestamp,
98
99	#[error("HEVCDecoderConfigurationRecord too short")]
100	HvccTooShort,
101
102	#[error("HEVCDecoderConfigurationRecord truncated")]
103	HvccTruncated,
104
105	#[error("hvc1 description for rendition {name:?} is missing VPS, SPS, or PPS (vps={vps}, sps={sps}, pps={pps})")]
106	MissingParamSets {
107		name: String,
108		vps: usize,
109		sps: usize,
110		pps: usize,
111	},
112
113	#[error("annexb: {0}")]
114	Annexb(#[from] crate::codec::annexb::Error),
115}
116
117pub type Result<T> = std::result::Result<T, Error>;
118
119/// The parameter sets carried out-of-band in an HEVCDecoderConfigurationRecord,
120/// split by NAL type.
121#[derive(Debug, Clone)]
122#[non_exhaustive]
123pub struct Hvcc {
124	/// NALU length size in bytes (typically 4).
125	pub length_size: usize,
126	/// VPS NAL units carried out-of-band in the record.
127	pub vps: Vec<Bytes>,
128	/// SPS NAL units carried out-of-band in the record.
129	pub sps: Vec<Bytes>,
130	/// PPS NAL units carried out-of-band in the record.
131	pub pps: Vec<Bytes>,
132}
133
134impl Hvcc {
135	/// Parse an HEVCDecoderConfigurationRecord, sorting the VPS/SPS/PPS NAL units
136	/// by type. The HEVC analogue of [`super::h264::Avcc::parse`].
137	pub fn parse(hvcc: &[u8]) -> Result<Self> {
138		if hvcc.len() < 23 {
139			return Err(Error::HvccTooShort);
140		}
141		let length_size = (hvcc[21] & 0x3) as usize + 1;
142		let num_arrays = hvcc[22] as usize;
143
144		let mut vps = Vec::new();
145		let mut sps = Vec::new();
146		let mut pps = Vec::new();
147		let mut pos: usize = 23;
148
149		for _ in 0..num_arrays {
150			let after_hdr = pos.checked_add(3).ok_or(Error::HvccTruncated)?;
151			if hvcc.len() < after_hdr {
152				return Err(Error::HvccTruncated);
153			}
154			let nal_type = hvcc[pos] & 0x3f;
155			let num_nalus = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]) as usize;
156			pos = after_hdr;
157
158			for _ in 0..num_nalus {
159				let after_len = pos.checked_add(2).ok_or(Error::HvccTruncated)?;
160				if hvcc.len() < after_len {
161					return Err(Error::HvccTruncated);
162				}
163				let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
164				let after_nal = after_len.checked_add(len).ok_or(Error::HvccTruncated)?;
165				if hvcc.len() < after_nal {
166					return Err(Error::HvccTruncated);
167				}
168				let bytes = Bytes::copy_from_slice(&hvcc[after_len..after_nal]);
169				pos = after_nal;
170
171				match NALUnitType::from(nal_type) {
172					NALUnitType::VpsNut => vps.push(bytes),
173					NALUnitType::SpsNut => sps.push(bytes),
174					NALUnitType::PpsNut => pps.push(bytes),
175					_ => {}
176				}
177			}
178		}
179
180		Ok(Self {
181			length_size,
182			vps,
183			sps,
184			pps,
185		})
186	}
187}
188
189/// Build a catalog [`VideoConfig`](hang::catalog::VideoConfig) for the `hvc1`
190/// shape from an HEVCDecoderConfigurationRecord (hvcC).
191///
192/// The H.265 analogue of [`crate::codec::h264::Avcc::parse`] feeding a
193/// `VideoConfig`. Used by the enhanced-RTMP / FLV importer, where the hvcC
194/// arrives out of band in the sequence-header tag and the coded samples are
195/// already length-prefixed NALU, so the record passes straight through as the
196/// catalog `description` (`in_band: false`).
197pub(crate) fn config_from_hvcc(hvcc: &[u8]) -> Result<hang::catalog::VideoConfig> {
198	let params = Hvcc::parse(hvcc)?;
199	let sps_nal = params.sps.first().ok_or(Error::MissingSps)?;
200	let sps = SpsNALUnit::parse(&mut &sps_nal[..]).map_err(|_| Error::SpsParse)?;
201	let profile = &sps.rbsp.profile_tier_level.general_profile;
202
203	let mut config = hang::catalog::VideoConfig::new(hang::catalog::H265 {
204		in_band: false,
205		profile_space: profile.profile_space,
206		profile_idc: profile.profile_idc,
207		profile_compatibility_flags: profile.profile_compatibility_flag.bits().to_be_bytes(),
208		tier_flag: profile.tier_flag,
209		level_idc: profile.level_idc.ok_or(Error::MissingLevelIdc)?,
210		constraint_flags: pack_constraint_flags(profile),
211	});
212	config.coded_width = Some(sps.rbsp.cropped_width() as u32);
213	config.coded_height = Some(sps.rbsp.cropped_height() as u32);
214	config.description = Some(Bytes::copy_from_slice(hvcc));
215	config.container = hang::catalog::Container::Legacy;
216	Ok(config)
217}
218
219/// Annex-B → length-prefixed transmuxer; the H.265 analogue of
220/// [`crate::codec::h264::Avc1`].
221///
222/// The active VPS/SPS/PPS set is scoped to the latest keyframe: a frame that
223/// carries parameter sets redefines them, so a mid-stream reconfiguration drops
224/// the superseded ones instead of accumulating them forever.
225pub struct Hvc1 {
226	hvcc: Option<Bytes>,
227	/// The active VPS NALs (from the most recent keyframe that carried them).
228	vps: Vec<Bytes>,
229	/// The active SPS NALs.
230	sps: Vec<Bytes>,
231	/// The active PPS NALs.
232	pps: Vec<Bytes>,
233}
234
235impl Default for Hvc1 {
236	fn default() -> Self {
237		Self::new()
238	}
239}
240
241impl Hvc1 {
242	/// Build a new transform for a hev1 source.
243	pub fn new() -> Self {
244		Self {
245			hvcc: None,
246			vps: Vec::new(),
247			sps: Vec::new(),
248			pps: Vec::new(),
249		}
250	}
251
252	/// The HEVCDecoderConfigurationRecord, available once VPS+SPS+PPS have been observed.
253	pub fn hvcc(&self) -> Option<&Bytes> {
254		self.hvcc.as_ref()
255	}
256
257	/// Convert one decoded frame's payload to the hvc1 wire shape.
258	///
259	/// Returns:
260	/// - `Ok(Some(payload))` if a length-prefixed sample is ready to emit.
261	/// - `Ok(None)` if the input contained only parameter sets and the
262	///   transform is still waiting for slice NALs (hvcC may have been
263	///   built as a side effect).
264	pub fn transform(&mut self, payload: Bytes) -> Result<Option<Bytes>> {
265		let mut buf = payload.clone();
266		let mut nal_iter = crate::codec::annexb::NalIterator::new(&mut buf);
267
268		let mut out = BytesMut::with_capacity(payload.remaining());
269		let mut frame_vps: Vec<Bytes> = Vec::new();
270		let mut frame_sps: Vec<Bytes> = Vec::new();
271		let mut frame_pps: Vec<Bytes> = Vec::new();
272		let mut emitted_any_slice = false;
273
274		loop {
275			let nal = match nal_iter.next() {
276				Some(Ok(n)) => n,
277				Some(Err(e)) => return Err(e.into()),
278				None => break,
279			};
280			if process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)? {
281				emitted_any_slice = true;
282			}
283		}
284
285		if let Some(nal) = nal_iter.flush()?
286			&& process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)?
287		{
288			emitted_any_slice = true;
289		}
290
291		// A frame that carries parameter sets (a keyframe) redefines the active
292		// set; adopt it so a superseded configuration's VPS/SPS/PPS are dropped
293		// rather than lingering in the hvcC. Per type, so a frame that updates only
294		// one kind keeps the others.
295		let mut changed = false;
296		if !frame_vps.is_empty() && frame_vps != self.vps {
297			self.vps = frame_vps;
298			changed = true;
299		}
300		if !frame_sps.is_empty() && frame_sps != self.sps {
301			self.sps = frame_sps;
302			changed = true;
303		}
304		if !frame_pps.is_empty() && frame_pps != self.pps {
305			self.pps = frame_pps;
306			changed = true;
307		}
308		if changed {
309			self.rebuild_hvcc()?;
310		}
311
312		if !emitted_any_slice {
313			return Ok(None);
314		}
315
316		Ok(Some(out.freeze()))
317	}
318
319	fn rebuild_hvcc(&mut self) -> Result<()> {
320		if self.vps.is_empty() || self.sps.is_empty() || self.pps.is_empty() {
321			return Ok(());
322		}
323		self.hvcc = Some(build_hvcc(&self.vps, &self.sps, &self.pps)?);
324		Ok(())
325	}
326}
327
328/// Process one NAL: VPS/SPS/PPS are collected (distinctly) into this frame's
329/// sets, everything else is length-prefixed and appended to `out`. Returns true
330/// if the NAL was a slice (i.e. produced sample bytes).
331fn process_nal(
332	nal: &Bytes,
333	out: &mut BytesMut,
334	frame_vps: &mut Vec<Bytes>,
335	frame_sps: &mut Vec<Bytes>,
336	frame_pps: &mut Vec<Bytes>,
337) -> Result<bool> {
338	if nal.is_empty() {
339		return Ok(false);
340	}
341	// HEVC NAL header is 2 bytes; type is bits 1..=6 of byte 0.
342	match NALUnitType::from((nal[0] >> 1) & 0x3f) {
343		NALUnitType::VpsNut => {
344			crate::codec::annexb::push_distinct(frame_vps, nal);
345			Ok(false)
346		}
347		NALUnitType::SpsNut => {
348			crate::codec::annexb::push_distinct(frame_sps, nal);
349			Ok(false)
350		}
351		NALUnitType::PpsNut => {
352			crate::codec::annexb::push_distinct(frame_pps, nal);
353			Ok(false)
354		}
355		_ => {
356			let len = u32::try_from(nal.len()).map_err(|_| Error::NalTooLarge)?;
357			out.extend_from_slice(&len.to_be_bytes());
358			out.extend_from_slice(nal);
359			Ok(true)
360		}
361	}
362}
363
364/// Build an HEVCDecoderConfigurationRecord (ISO/IEC 14496-15 §8.3.3).
365/// Single-layer streams only. Each NAL array (VPS, SPS, PPS) carries every
366/// distinct parameter set the stream defined, in arrival order; the profile/tier
367/// fields are read from the first SPS.
368pub(crate) fn build_hvcc(vps_nals: &[Bytes], sps_nals: &[Bytes], pps_nals: &[Bytes]) -> Result<Bytes> {
369	let first_sps = sps_nals.first().ok_or(Error::MissingSps)?;
370	for (label, nals) in [("VPS", vps_nals), ("SPS", sps_nals), ("PPS", pps_nals)] {
371		if nals.len() > u16::MAX as usize {
372			return Err(Error::TooManyNals(label, nals.len()));
373		}
374		for nal in nals {
375			if nal.len() > u16::MAX as usize {
376				return Err(Error::NalTooLargeForHvcc(label, nal.len()));
377			}
378		}
379	}
380
381	let sps = SpsNALUnit::parse(&mut &first_sps[..]).map_err(|_| Error::SpsParse)?;
382	let profile = &sps.rbsp.profile_tier_level.general_profile;
383	let level_idc = profile.level_idc.ok_or(Error::MissingLevelIdc)?;
384	let constraint_flags = pack_constraint_flags(profile);
385	let compat = profile.profile_compatibility_flag.bits().to_be_bytes();
386	let num_temporal_layers = sps.rbsp.sps_max_sub_layers_minus1 + 1;
387
388	let params_len: usize = vps_nals
389		.iter()
390		.chain(sps_nals)
391		.chain(pps_nals)
392		.map(|n| 2 + n.len())
393		.sum();
394	let mut out = BytesMut::with_capacity(23 + 3 * 3 + params_len);
395	out.put_u8(1); // configurationVersion
396	out.put_u8(((profile.profile_space & 0x3) << 6) | ((profile.tier_flag as u8) << 5) | (profile.profile_idc & 0x1f));
397	out.put_slice(&compat);
398	out.put_slice(&constraint_flags);
399	out.put_u8(level_idc);
400	out.put_u16(0xf000); // min_spatial_segmentation_idc unknown
401	out.put_u8(0xfc); // parallelismType mixed
402	out.put_u8(0xfc | (sps.rbsp.chroma_format_idc & 0x3));
403	out.put_u8(0xf8 | (sps.rbsp.bit_depth_luma_minus8 & 0x7));
404	out.put_u8(0xf8 | (sps.rbsp.bit_depth_chroma_minus8 & 0x7));
405	out.put_u16(0); // avgFrameRate unspecified
406	out.put_u8(((num_temporal_layers & 0x7) << 3) | ((sps.rbsp.sps_temporal_id_nesting_flag as u8) << 2) | 0x3);
407	out.put_u8(3); // numOfArrays (VPS, SPS, PPS)
408
409	for (nal_type, nals) in [
410		(u8::from(NALUnitType::VpsNut), vps_nals),
411		(u8::from(NALUnitType::SpsNut), sps_nals),
412		(u8::from(NALUnitType::PpsNut), pps_nals),
413	] {
414		out.put_u8(0x80 | (nal_type & 0x3f)); // array_completeness = 1
415		out.put_u16(nals.len() as u16); // numNalus
416		for nal in nals {
417			out.put_u16(nal.len() as u16);
418			out.put_slice(nal);
419		}
420	}
421
422	Ok(out.freeze())
423}
424
425/// Extract the parameter-set NALs (VPS, SPS, PPS in array order) and the NALU
426/// length size from an HEVCDecoderConfigurationRecord. The inverse of
427/// [`build_hvcc`]; used to re-emit out-of-band hvc1 parameter sets as inline
428/// Annex-B (e.g. for MPEG-TS).
429pub(crate) fn hvcc_params(hvcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
430	anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
431	let length_size = (hvcc[21] & 0x03) as usize + 1;
432	let num_arrays = hvcc[22];
433
434	let mut params = Vec::new();
435	let mut pos = 23;
436	for _ in 0..num_arrays {
437		// Skip the array_completeness | NAL_unit_type byte.
438		anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
439		pos += 1;
440		let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]);
441		pos += 2;
442		for _ in 0..num_nalus {
443			anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
444			let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
445			pos += 2;
446			anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
447			params.push(Bytes::copy_from_slice(&hvcc[pos..pos + len]));
448			pos += len;
449		}
450	}
451
452	Ok((length_size, params))
453}
454
455/// Pack the constraint flags from ITU H.265 V10 §7.3.3 Profile, tier and level syntax.
456pub(crate) fn pack_constraint_flags(profile: &scuffle_h265::Profile) -> [u8; 6] {
457	let mut flags = [0u8; 6];
458	flags[0] = ((profile.progressive_source_flag as u8) << 7)
459		| ((profile.interlaced_source_flag as u8) << 6)
460		| ((profile.non_packed_constraint_flag as u8) << 5)
461		| ((profile.frame_only_constraint_flag as u8) << 4);
462	flags
463}
464
465/// Real parameter sets from a single-frame x265 encode (1280x720, Main profile),
466/// for tests that need an SPS that scuffle_h265 can actually parse.
467#[cfg(test)]
468pub(crate) mod fixtures {
469	use bytes::Bytes;
470
471	pub(crate) const VPS: &[u8] = &[
472		0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00,
473		0x03, 0x00, 0x5d, 0x95, 0x98, 0x09,
474	];
475	pub(crate) const SPS: &[u8] = &[
476		0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d,
477		0xa0, 0x02, 0x80, 0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x02, 0x00, 0x00, 0x03, 0x00,
478		0x02, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x10,
479	];
480	pub(crate) const PPS: &[u8] = &[0x44, 0x01, 0xc1, 0x72, 0xb4, 0x62, 0x40];
481
482	/// An hvcC record built from the real parameter sets (4-byte NALU lengths).
483	pub(crate) fn hvcc() -> Bytes {
484		super::build_hvcc(
485			&[Bytes::from_static(VPS)],
486			&[Bytes::from_static(SPS)],
487			&[Bytes::from_static(PPS)],
488		)
489		.expect("real parameter sets must build an hvcC")
490	}
491}
492
493#[cfg(test)]
494mod tests {
495	use super::*;
496
497	/// Length-prefix each NAL with a 4-byte big-endian length, the hvc1 wire shape.
498	fn length_prefixed(nals: &[&[u8]]) -> Vec<u8> {
499		let mut au = Vec::new();
500		for nal in nals {
501			au.extend_from_slice(&(nal.len() as u32).to_be_bytes());
502			au.extend_from_slice(nal);
503		}
504		au
505	}
506
507	fn ts() -> moq_net::Timestamp {
508		moq_net::Timestamp::from_micros(0).unwrap()
509	}
510
511	/// hvc1: a length-prefixed access unit with an IDR slice wraps as one keyframe;
512	/// the payload is passed through verbatim. The leading prefix-SEI NAL exercises
513	/// the walk past non-slice NALs.
514	#[test]
515	fn hvc1_frame_keyframe() {
516		let sei: &[u8] = &[0x4e, 0x01, 0x05, 0xff]; // PrefixSeiNut (39)
517		let idr: &[u8] = &[0x26, 0x01, 0x80, 0xaa]; // IdrWRadl (19)
518		let au = length_prefixed(&[sei, idr]);
519
520		let frame = hvc1_frame(&au, 4, ts()).unwrap();
521		assert!(frame.keyframe);
522		assert_eq!(frame.payload, au);
523	}
524
525	/// hvc1: CRA opens a group too; HEVC random access is broader than IDR.
526	#[test]
527	fn hvc1_frame_cra_keyframe() {
528		let cra: &[u8] = &[0x2a, 0x01, 0x80, 0x55]; // CraNut (21)
529		let au = length_prefixed(&[cra]);
530
531		let frame = hvc1_frame(&au, 4, ts()).unwrap();
532		assert!(frame.keyframe);
533	}
534
535	/// hvc1: a trailing delta slice is not a keyframe.
536	#[test]
537	fn hvc1_frame_delta() {
538		let trail: &[u8] = &[0x02, 0x01, 0x80, 0x33]; // TrailR (1)
539		let au = length_prefixed(&[trail]);
540
541		let frame = hvc1_frame(&au, 4, ts()).unwrap();
542		assert!(!frame.keyframe);
543	}
544
545	/// hvc1: a NAL header whose low five bits read as an H.264 IDR (0x05) is a
546	/// TsaN delta slice under the 2-byte HEVC header and must not be flagged a
547	/// keyframe.
548	#[test]
549	fn hvc1_frame_tsa_delta_not_h264_idr() {
550		let tsa: &[u8] = &[0x05, 0x01, 0x80, 0x33]; // TsaN (2)
551		let au = length_prefixed(&[tsa]);
552
553		let frame = hvc1_frame(&au, 4, ts()).unwrap();
554		assert!(!frame.keyframe);
555	}
556
557	/// The real-SPS fixture resolves a full out-of-band config: dimensions from
558	/// the parsed SPS and the hvcC itself as the catalog `description`.
559	#[test]
560	fn config_from_hvcc_resolves_real_sps() {
561		let hvcc = fixtures::hvcc();
562		let config = config_from_hvcc(&hvcc).unwrap();
563
564		let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
565			panic!("expected H.265 codec")
566		};
567		assert!(!h265.in_band, "hvcC config is out-of-band");
568		assert_eq!(config.coded_width, Some(1280));
569		assert_eq!(config.coded_height, Some(720));
570		assert_eq!(config.description.as_deref(), Some(hvcc.as_ref()));
571	}
572
573	/// Hand-build an hvcC (the layout `build_hvcc` emits) and assert the
574	/// parameter sets and length size are recovered. Built by hand rather than
575	/// via `build_hvcc` so it doesn't need a real, parseable HEVC SPS.
576	#[test]
577	fn hvcc_params_parses_vps_sps_pps() {
578		let vps = &[0x40, 0x01, 0x0c][..]; // NAL type 32
579		let sps = &[0x42, 0x01, 0x01, 0x60][..]; // NAL type 33
580		let pps = &[0x44, 0x01, 0xc0][..]; // NAL type 34
581
582		let mut hvcc = BytesMut::new();
583		hvcc.extend_from_slice(&[0u8; 21]); // fixed fields up to (but not including) byte 21
584		hvcc.put_u8(0xfc | 0x03); // byte 21: ...| lengthSizeMinusOne = 3 -> length_size 4
585		hvcc.put_u8(3); // numOfArrays
586		for (nal_type, nal) in [
587			(u8::from(NALUnitType::VpsNut), vps),
588			(u8::from(NALUnitType::SpsNut), sps),
589			(u8::from(NALUnitType::PpsNut), pps),
590		] {
591			hvcc.put_u8(0x80 | (nal_type & 0x3f));
592			hvcc.put_u16(1); // numNalus
593			hvcc.put_u16(nal.len() as u16);
594			hvcc.put_slice(nal);
595		}
596
597		let (length_size, params) = hvcc_params(&hvcc).unwrap();
598		assert_eq!(length_size, 4);
599		assert_eq!(params.len(), 3);
600		assert_eq!(params[0].as_ref(), vps);
601		assert_eq!(params[1].as_ref(), sps);
602		assert_eq!(params[2].as_ref(), pps);
603	}
604}