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;
13mod sps;
14
15pub use export::*;
16pub use import::*;
17pub use split::*;
18
19use bytes::{Buf, BufMut, Bytes, BytesMut};
20use scuffle_h265::{NALUnitType, SpsNALUnit};
21use sps::Sps;
22
23/// Wrap one hvc1 (length-prefixed NALU) access unit as a single
24/// [`Frame`](crate::container::Frame), with the keyframe flag set when it
25/// carries an IRAP NAL.
26///
27/// hvc1 is not a stream: each access unit arrives whole with its NALU
28/// `length_size` known out-of-band from the hvcC (`super::Hvcc::parse(hvcc).length_size`).
29/// The payload is passed through verbatim.
30pub(crate) fn hvc1_frame(
31	data: impl moq_net::IntoBytes,
32	length_size: usize,
33	pts: moq_net::Timestamp,
34) -> crate::Result<crate::container::Frame> {
35	let keyframe = hvc1_is_keyframe(data.as_ref(), length_size);
36	Ok(crate::container::Frame {
37		timestamp: pts,
38		payload: data.into_bytes(),
39		keyframe,
40		duration: None,
41	})
42}
43
44/// Detect whether an hvc1-shaped (length-prefixed) buffer contains an IRAP slice.
45fn hvc1_is_keyframe(data: &[u8], length_size: usize) -> bool {
46	let Ok(nals) = crate::codec::annexb::length_prefixed_nals(data, length_size) else {
47		return false;
48	};
49	nals.map_while(std::result::Result::ok)
50		.any(|nal| nal.first().is_some_and(|header| is_irap(split::nal_unit_type(*header))))
51}
52
53/// True for the IRAP (intra random access point) NAL types, the ones that open a group.
54/// HEVC random access is broader than H.264's single IDR type.
55pub(crate) fn is_irap(nal_type: NALUnitType) -> bool {
56	matches!(
57		nal_type,
58		NALUnitType::IdrWRadl
59			| NALUnitType::IdrNLp
60			| NALUnitType::BlaNLp
61			| NALUnitType::BlaWRadl
62			| NALUnitType::BlaWLp
63			| NALUnitType::CraNut
64	)
65}
66
67/// H.265 parsing and transform errors.
68#[derive(Debug, Clone, thiserror::Error)]
69#[non_exhaustive]
70pub enum Error {
71	#[error("NAL unit is too short")]
72	NalTooShort,
73
74	#[error("{0} too large for hvcC length field ({1} > {max})", max = u16::MAX)]
75	NalTooLargeForHvcc(&'static str, usize),
76
77	#[error("too many {0} for hvcC ({1} > {max})", max = u16::MAX)]
78	TooManyNals(&'static str, usize),
79
80	#[error("NAL too large for 4-byte length prefix")]
81	NalTooLarge,
82
83	#[error("failed to parse SPS NAL unit")]
84	SpsParse,
85
86	#[error("missing level_idc in SPS")]
87	MissingLevelIdc,
88
89	#[error("forbidden zero bit is not zero")]
90	ForbiddenZeroBit,
91
92	#[error("not initialized")]
93	NotInitialized,
94
95	#[error("expected SPS before any frames")]
96	MissingSps,
97
98	#[error("missing timestamp")]
99	MissingTimestamp,
100
101	#[error("HEVCDecoderConfigurationRecord too short")]
102	HvccTooShort,
103
104	#[error("HEVCDecoderConfigurationRecord truncated")]
105	HvccTruncated,
106
107	#[error("hvc1 description for rendition {name:?} is missing VPS, SPS, or PPS (vps={vps}, sps={sps}, pps={pps})")]
108	MissingParamSets {
109		name: String,
110		vps: usize,
111		sps: usize,
112		pps: usize,
113	},
114
115	#[error("annexb: {0}")]
116	Annexb(#[from] crate::codec::annexb::Error),
117}
118
119pub type Result<T> = std::result::Result<T, Error>;
120
121/// The parameter sets carried out-of-band in an HEVCDecoderConfigurationRecord,
122/// split by NAL type.
123#[derive(Debug, Clone)]
124#[non_exhaustive]
125pub struct Hvcc {
126	/// NALU length size in bytes (typically 4).
127	pub length_size: usize,
128	/// VPS NAL units carried out-of-band in the record.
129	pub vps: Vec<Bytes>,
130	/// SPS NAL units carried out-of-band in the record.
131	pub sps: Vec<Bytes>,
132	/// PPS NAL units carried out-of-band in the record.
133	pub pps: Vec<Bytes>,
134}
135
136impl Hvcc {
137	/// Parse an HEVCDecoderConfigurationRecord, sorting the VPS/SPS/PPS NAL units
138	/// by type. The HEVC analogue of [`super::h264::Avcc::parse`].
139	pub fn parse(hvcc: &[u8]) -> Result<Self> {
140		if hvcc.len() < 23 {
141			return Err(Error::HvccTooShort);
142		}
143		let length_size = (hvcc[21] & 0x3) as usize + 1;
144		let num_arrays = hvcc[22] as usize;
145
146		let mut vps = Vec::new();
147		let mut sps = Vec::new();
148		let mut pps = Vec::new();
149		let mut pos: usize = 23;
150
151		for _ in 0..num_arrays {
152			let after_hdr = pos.checked_add(3).ok_or(Error::HvccTruncated)?;
153			if hvcc.len() < after_hdr {
154				return Err(Error::HvccTruncated);
155			}
156			let nal_type = hvcc[pos] & 0x3f;
157			let num_nalus = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]) as usize;
158			pos = after_hdr;
159
160			for _ in 0..num_nalus {
161				let after_len = pos.checked_add(2).ok_or(Error::HvccTruncated)?;
162				if hvcc.len() < after_len {
163					return Err(Error::HvccTruncated);
164				}
165				let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
166				let after_nal = after_len.checked_add(len).ok_or(Error::HvccTruncated)?;
167				if hvcc.len() < after_nal {
168					return Err(Error::HvccTruncated);
169				}
170				let bytes = Bytes::copy_from_slice(&hvcc[after_len..after_nal]);
171				pos = after_nal;
172
173				match NALUnitType::from(nal_type) {
174					NALUnitType::VpsNut => vps.push(bytes),
175					NALUnitType::SpsNut => sps.push(bytes),
176					NALUnitType::PpsNut => pps.push(bytes),
177					_ => {}
178				}
179			}
180		}
181
182		Ok(Self {
183			length_size,
184			vps,
185			sps,
186			pps,
187		})
188	}
189}
190
191/// Build a catalog [`VideoConfig`](hang::catalog::VideoConfig) for the `hvc1`
192/// shape from an HEVCDecoderConfigurationRecord (hvcC).
193///
194/// The H.265 analogue of [`crate::codec::h264::Avcc::parse`] feeding a
195/// `VideoConfig`. Used by the enhanced-RTMP / FLV importer, where the hvcC
196/// arrives out of band in the sequence-header tag and the coded samples are
197/// already length-prefixed NALU, so the record passes straight through as the
198/// catalog `description` (`in_band: false`).
199pub(crate) fn config_from_hvcc(hvcc: &[u8]) -> Result<hang::catalog::VideoConfig> {
200	let params = Hvcc::parse(hvcc)?;
201	let sps_nal = params.sps.first().ok_or(Error::MissingSps)?;
202	let sps = Sps::parse(sps_nal)?.head;
203
204	let mut config = hang::catalog::VideoConfig::new(hang::catalog::H265 {
205		in_band: false,
206		profile_space: sps.profile_space,
207		profile_idc: sps.profile_idc,
208		profile_compatibility_flags: sps.profile_compatibility_flags,
209		tier_flag: sps.tier_flag,
210		level_idc: sps.level_idc,
211		constraint_flags: sps.constraint_flags,
212	});
213	config.coded_width = Some(sps.width);
214	config.coded_height = Some(sps.height);
215	config.description = Some(Bytes::copy_from_slice(hvcc));
216	Ok(config)
217}
218
219/// The reordering an SPS NAL unit declares for its highest sub-layer: `sps_max_num_reorder_pics`,
220/// and the picture period when the VUI's HRD parameters fix the picture rate within the CVS
221/// (ITU-T H.265 E.3.2).
222///
223/// `None` when the SPS fails to parse.
224pub(crate) fn sps_reorder(nal: &[u8]) -> Option<crate::codec::video::Reorder> {
225	let sps = SpsNALUnit::parse(&mut &nal[..]).ok()?;
226	let highest = sps.rbsp.sps_max_sub_layers_minus1 as usize;
227	let reorder = &sps.rbsp.sub_layer_ordering_info.sps_max_num_reorder_pics;
228	let depth = *reorder.get(highest).or(reorder.last())?;
229	Some(crate::codec::video::Reorder {
230		depth: u32::try_from(depth).ok()?,
231		period: sps_period(&sps.rbsp),
232	})
233}
234
235/// One picture's duration at the highest sub-layer as `(units, scale)`, when the VUI's HRD
236/// parameters fix the picture rate within the CVS (ITU-T H.265 E.3.2). Without that the VUI tick
237/// only bounds the rate from above.
238pub(crate) fn sps_period(sps: &scuffle_h265::SpsRbsp) -> Option<(u64, u64)> {
239	let highest = sps.sps_max_sub_layers_minus1 as usize;
240	let timing = sps.vui_parameters.as_ref()?.vui_timing_info.as_ref()?;
241	let sub_layers = &timing.hrd_parameters.as_ref()?.sub_layers;
242	let elemental = sub_layers
243		.get(highest)
244		.or(sub_layers.last())?
245		.elemental_duration_in_tc_minus1?;
246	let units = elemental
247		.checked_add(1)?
248		.checked_mul(u64::from(timing.num_units_in_tick.get()))?;
249	Some((units, u64::from(timing.time_scale.get())))
250}
251
252/// Annex-B → length-prefixed transmuxer; the H.265 analogue of
253/// [`crate::codec::h264::Avc1`].
254///
255/// The active VPS/SPS/PPS set is scoped to the latest keyframe: a frame that
256/// carries parameter sets redefines them, so a mid-stream reconfiguration drops
257/// the superseded ones instead of accumulating them forever.
258pub struct Hvc1 {
259	hvcc: Option<Bytes>,
260	/// The active VPS NALs (from the most recent keyframe that carried them).
261	vps: Vec<Bytes>,
262	/// The active SPS NALs.
263	sps: Vec<Bytes>,
264	/// The active PPS NALs.
265	pps: Vec<Bytes>,
266}
267
268impl Default for Hvc1 {
269	fn default() -> Self {
270		Self::new()
271	}
272}
273
274impl Hvc1 {
275	/// Build a new transform for a hev1 source.
276	pub fn new() -> Self {
277		Self {
278			hvcc: None,
279			vps: Vec::new(),
280			sps: Vec::new(),
281			pps: Vec::new(),
282		}
283	}
284
285	/// The HEVCDecoderConfigurationRecord, available once VPS+SPS+PPS have been observed.
286	pub fn hvcc(&self) -> Option<&Bytes> {
287		self.hvcc.as_ref()
288	}
289
290	/// Convert one decoded frame's payload to the hvc1 wire shape.
291	///
292	/// Returns:
293	/// - `Ok(Some(payload))` if a length-prefixed sample is ready to emit.
294	/// - `Ok(None)` if the input contained only parameter sets and the
295	///   transform is still waiting for slice NALs (hvcC may have been
296	///   built as a side effect).
297	pub fn transform(&mut self, payload: Bytes) -> Result<Option<Bytes>> {
298		let mut buf = payload.clone();
299		let mut nal_iter = crate::codec::annexb::NalIterator::new(&mut buf);
300
301		let mut out = BytesMut::with_capacity(payload.remaining());
302		let mut frame_vps: Vec<Bytes> = Vec::new();
303		let mut frame_sps: Vec<Bytes> = Vec::new();
304		let mut frame_pps: Vec<Bytes> = Vec::new();
305		let mut emitted_any_slice = false;
306
307		loop {
308			let nal = match nal_iter.next() {
309				Some(Ok(n)) => n,
310				Some(Err(e)) => return Err(e.into()),
311				None => break,
312			};
313			if process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)? {
314				emitted_any_slice = true;
315			}
316		}
317
318		if let Some(nal) = nal_iter.flush()?
319			&& process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)?
320		{
321			emitted_any_slice = true;
322		}
323
324		// A frame that carries parameter sets (a keyframe) redefines the active
325		// set; adopt it so a superseded configuration's VPS/SPS/PPS are dropped
326		// rather than lingering in the hvcC. Per type, so a frame that updates only
327		// one kind keeps the others.
328		let mut changed = false;
329		if !frame_vps.is_empty() && frame_vps != self.vps {
330			self.vps = frame_vps;
331			changed = true;
332		}
333		if !frame_sps.is_empty() && frame_sps != self.sps {
334			self.sps = frame_sps;
335			changed = true;
336		}
337		if !frame_pps.is_empty() && frame_pps != self.pps {
338			self.pps = frame_pps;
339			changed = true;
340		}
341		if changed {
342			self.rebuild_hvcc()?;
343		}
344
345		if !emitted_any_slice {
346			return Ok(None);
347		}
348
349		Ok(Some(out.freeze()))
350	}
351
352	fn rebuild_hvcc(&mut self) -> Result<()> {
353		if self.vps.is_empty() || self.sps.is_empty() || self.pps.is_empty() {
354			return Ok(());
355		}
356		self.hvcc = Some(build_hvcc(&self.vps, &self.sps, &self.pps)?);
357		Ok(())
358	}
359}
360
361/// Process one NAL: VPS/SPS/PPS are collected (distinctly) into this frame's
362/// sets, everything else is length-prefixed and appended to `out`. Returns true
363/// if the NAL was a slice (i.e. produced sample bytes).
364fn process_nal(
365	nal: &Bytes,
366	out: &mut BytesMut,
367	frame_vps: &mut Vec<Bytes>,
368	frame_sps: &mut Vec<Bytes>,
369	frame_pps: &mut Vec<Bytes>,
370) -> Result<bool> {
371	if nal.is_empty() {
372		return Ok(false);
373	}
374	// HEVC NAL header is 2 bytes; type is bits 1..=6 of byte 0.
375	match NALUnitType::from((nal[0] >> 1) & 0x3f) {
376		NALUnitType::VpsNut => {
377			crate::codec::annexb::push_distinct(frame_vps, nal);
378			Ok(false)
379		}
380		NALUnitType::SpsNut => {
381			crate::codec::annexb::push_distinct(frame_sps, nal);
382			Ok(false)
383		}
384		NALUnitType::PpsNut => {
385			crate::codec::annexb::push_distinct(frame_pps, nal);
386			Ok(false)
387		}
388		_ => {
389			let len = u32::try_from(nal.len()).map_err(|_| Error::NalTooLarge)?;
390			out.extend_from_slice(&len.to_be_bytes());
391			out.extend_from_slice(nal);
392			Ok(true)
393		}
394	}
395}
396
397/// Build an HEVCDecoderConfigurationRecord (ISO/IEC 14496-15 §8.3.3).
398/// Single-layer streams only. Each NAL array (VPS, SPS, PPS) carries every
399/// distinct parameter set the stream defined, in arrival order; the profile/tier
400/// fields are read from the first SPS.
401pub(crate) fn build_hvcc(vps_nals: &[Bytes], sps_nals: &[Bytes], pps_nals: &[Bytes]) -> Result<Bytes> {
402	let first_sps = sps_nals.first().ok_or(Error::MissingSps)?;
403	for (label, nals) in [("VPS", vps_nals), ("SPS", sps_nals), ("PPS", pps_nals)] {
404		if nals.len() > u16::MAX as usize {
405			return Err(Error::TooManyNals(label, nals.len()));
406		}
407		for nal in nals {
408			if nal.len() > u16::MAX as usize {
409				return Err(Error::NalTooLargeForHvcc(label, nal.len()));
410			}
411		}
412	}
413
414	let sps = Sps::parse(first_sps)?.head;
415	let num_temporal_layers = sps.max_sub_layers_minus1 + 1;
416
417	let params_len: usize = vps_nals
418		.iter()
419		.chain(sps_nals)
420		.chain(pps_nals)
421		.map(|n| 2 + n.len())
422		.sum();
423	let mut out = BytesMut::with_capacity(23 + 3 * 3 + params_len);
424	out.put_u8(1); // configurationVersion
425	out.put_u8(((sps.profile_space & 0x3) << 6) | ((sps.tier_flag as u8) << 5) | (sps.profile_idc & 0x1f));
426	out.put_slice(&sps.profile_compatibility_flags);
427	out.put_slice(&sps.constraint_flags);
428	out.put_u8(sps.level_idc);
429	out.put_u16(0xf000); // min_spatial_segmentation_idc unknown
430	out.put_u8(0xfc); // parallelismType mixed
431	out.put_u8(0xfc | (sps.chroma_format_idc & 0x3));
432	out.put_u8(0xf8 | (sps.bit_depth_luma_minus8 & 0x7));
433	out.put_u8(0xf8 | (sps.bit_depth_chroma_minus8 & 0x7));
434	out.put_u16(0); // avgFrameRate unspecified
435	out.put_u8(((num_temporal_layers & 0x7) << 3) | ((sps.temporal_id_nesting_flag as u8) << 2) | 0x3);
436	out.put_u8(3); // numOfArrays (VPS, SPS, PPS)
437
438	for (nal_type, nals) in [
439		(u8::from(NALUnitType::VpsNut), vps_nals),
440		(u8::from(NALUnitType::SpsNut), sps_nals),
441		(u8::from(NALUnitType::PpsNut), pps_nals),
442	] {
443		out.put_u8(0x80 | (nal_type & 0x3f)); // array_completeness = 1
444		out.put_u16(nals.len() as u16); // numNalus
445		for nal in nals {
446			out.put_u16(nal.len() as u16);
447			out.put_slice(nal);
448		}
449	}
450
451	Ok(out.freeze())
452}
453
454/// Extract the parameter-set NALs (VPS, SPS, PPS in array order) and the NALU
455/// length size from an HEVCDecoderConfigurationRecord. The inverse of
456/// [`build_hvcc`]; used to re-emit out-of-band hvc1 parameter sets as inline
457/// Annex-B (e.g. for MPEG-TS).
458pub(crate) fn hvcc_params(hvcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
459	anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
460	let length_size = (hvcc[21] & 0x03) as usize + 1;
461	let num_arrays = hvcc[22];
462
463	let mut params = Vec::new();
464	let mut pos = 23;
465	for _ in 0..num_arrays {
466		// Skip the array_completeness | NAL_unit_type byte.
467		anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
468		pos += 1;
469		let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]);
470		pos += 2;
471		for _ in 0..num_nalus {
472			anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
473			let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
474			pos += 2;
475			anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
476			params.push(Bytes::copy_from_slice(&hvcc[pos..pos + len]));
477			pos += len;
478		}
479	}
480
481	Ok((length_size, params))
482}
483
484/// Pack the constraint flags from ITU H.265 V10 §7.3.3 Profile, tier and level syntax.
485pub(crate) fn pack_constraint_flags(profile: &scuffle_h265::Profile) -> [u8; 6] {
486	let mut flags = [0u8; 6];
487	flags[0] = ((profile.progressive_source_flag as u8) << 7)
488		| ((profile.interlaced_source_flag as u8) << 6)
489		| ((profile.non_packed_constraint_flag as u8) << 5)
490		| ((profile.frame_only_constraint_flag as u8) << 4);
491	flags
492}
493
494/// Real parameter sets, mostly from a single-frame x265 encode (1280x720, Main profile),
495/// for tests that need an SPS rather than hand-built bytes.
496#[cfg(test)]
497pub(crate) mod fixtures {
498	use bytes::Bytes;
499
500	pub(crate) const VPS: &[u8] = &[
501		0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00,
502		0x03, 0x00, 0x5d, 0x95, 0x98, 0x09,
503	];
504	pub(crate) const SPS: &[u8] = &[
505		0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d,
506		0xa0, 0x02, 0x80, 0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x02, 0x00, 0x00, 0x03, 0x00,
507		0x02, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x10,
508	];
509	pub(crate) const PPS: &[u8] = &[0x44, 0x01, 0xc1, 0x72, 0xb4, 0x62, 0x40];
510	/// SPS from a 64x64, 25 fps x265 encode with `bframes=3:b-pyramid=1` and HRD signalling
511	/// (`sps_max_num_reorder_pics` 2).
512	pub(crate) const SPS_PYRAMID: &[u8] = &[
513		0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3c,
514		0xa0, 0x20, 0x81, 0x05, 0x96, 0x56, 0x59, 0x24, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80,
515		0x00, 0x00, 0x0c, 0xb0, 0x0a, 0x48, 0x2f, 0x00, 0x07, 0xa1, 0x20, 0x00, 0xf4, 0x24, 0x40,
516	];
517
518	/// SPS from a Viewtron IP-PTZ-440 (1920x1080 Main@L5.0, 10 fps). Its VUI sets
519	/// `colour_description_present_flag` with `colour_primaries`, `transfer_characteristics`
520	/// and `matrix_coeffs` all 0 on a 4:2:0 stream. ITU-T H.265 E.3.1 forbids `matrix_coeffs`
521	/// 0 there, so scuffle_h265 refuses the SPS; libavcodec and GStreamer accept it.
522	pub(crate) const SPS_ZERO_COLOUR: &[u8] = &[
523		0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00,
524		0x96, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07, 0xcb, 0x8a, 0xad, 0x3b, 0xa2, 0x4b, 0xb9, 0x08, 0x00, 0x00, 0x03,
525		0x00, 0x20, 0x05, 0x26, 0x5c, 0x00, 0x33, 0x7f, 0x98, 0x01,
526	];
527
528	/// SPS from a 1280x720, 60 fps hevc_nvenc encode (`p1`, `ull`), the desktop host's shape. Its VUI
529	/// sets `aspect_ratio_idc` 1: square samples.
530	pub(crate) const SPS_NVENC: &[u8] = &[
531		0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78,
532		0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0x5d, 0x29, 0x08, 0x46, 0x45, 0xd5, 0x0c, 0x04, 0x04, 0x00,
533		0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf1, 0x80, 0x17, 0x79, 0x44, 0x00, 0x02, 0xdc, 0x6c, 0x00,
534		0x00, 0x2d, 0xc6, 0xc0, 0x40,
535	];
536
537	/// SPS from a 1440x1080 hevc_nvenc encode with 4:3 samples (`setsar=4/3`), shown 16:9.
538	pub(crate) const SPS_ANAMORPHIC: &[u8] = &[
539		0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78,
540		0xa0, 0x02, 0xd0, 0x80, 0x11, 0x07, 0xcb, 0x96, 0x5d, 0x29, 0x08, 0x46, 0x45, 0xd5, 0x0c, 0x38, 0x04, 0x00,
541		0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x79, 0x80, 0x17, 0x79, 0x44, 0x00, 0x02, 0xdc, 0x6c, 0x00,
542		0x00, 0xf4, 0x24, 0x04,
543	];
544
545	/// An hvcC record built from the real parameter sets (4-byte NALU lengths).
546	pub(crate) fn hvcc() -> Bytes {
547		super::build_hvcc(
548			&[Bytes::from_static(VPS)],
549			&[Bytes::from_static(SPS)],
550			&[Bytes::from_static(PPS)],
551		)
552		.expect("real parameter sets must build an hvcC")
553	}
554}
555
556#[cfg(test)]
557mod tests {
558	use super::*;
559
560	/// Length-prefix each NAL with a 4-byte big-endian length, the hvc1 wire shape.
561	fn length_prefixed(nals: &[&[u8]]) -> Vec<u8> {
562		let mut au = Vec::new();
563		for nal in nals {
564			au.extend_from_slice(&(nal.len() as u32).to_be_bytes());
565			au.extend_from_slice(nal);
566		}
567		au
568	}
569
570	fn ts() -> moq_net::Timestamp {
571		moq_net::Timestamp::from_micros(0).unwrap()
572	}
573
574	#[test]
575	fn sps_reorder_reads_the_highest_sub_layer() {
576		let reorder = sps_reorder(fixtures::SPS_PYRAMID).unwrap();
577		assert_eq!(reorder.depth, 2);
578		assert_eq!(reorder.period, Some((1, 25)));
579		assert_eq!(sps_reorder(&fixtures::SPS[..8]), None);
580	}
581
582	/// hvc1: a length-prefixed access unit with an IDR slice wraps as one keyframe;
583	/// the payload is passed through verbatim. The leading prefix-SEI NAL exercises
584	/// the walk past non-slice NALs.
585	#[test]
586	fn hvc1_frame_keyframe() {
587		let sei: &[u8] = &[0x4e, 0x01, 0x05, 0xff]; // PrefixSeiNut (39)
588		let idr: &[u8] = &[0x26, 0x01, 0x80, 0xaa]; // IdrWRadl (19)
589		let au = length_prefixed(&[sei, idr]);
590
591		let frame = hvc1_frame(&au, 4, ts()).unwrap();
592		assert!(frame.keyframe);
593		assert_eq!(frame.payload, au);
594	}
595
596	/// hvc1: CRA opens a group too; HEVC random access is broader than IDR.
597	#[test]
598	fn hvc1_frame_cra_keyframe() {
599		let cra: &[u8] = &[0x2a, 0x01, 0x80, 0x55]; // CraNut (21)
600		let au = length_prefixed(&[cra]);
601
602		let frame = hvc1_frame(&au, 4, ts()).unwrap();
603		assert!(frame.keyframe);
604	}
605
606	/// hvc1: a trailing delta slice is not a keyframe.
607	#[test]
608	fn hvc1_frame_delta() {
609		let trail: &[u8] = &[0x02, 0x01, 0x80, 0x33]; // TrailR (1)
610		let au = length_prefixed(&[trail]);
611
612		let frame = hvc1_frame(&au, 4, ts()).unwrap();
613		assert!(!frame.keyframe);
614	}
615
616	/// hvc1: a NAL header whose low five bits read as an H.264 IDR (0x05) is a
617	/// TsaN delta slice under the 2-byte HEVC header and must not be flagged a
618	/// keyframe.
619	#[test]
620	fn hvc1_frame_tsa_delta_not_h264_idr() {
621		let tsa: &[u8] = &[0x05, 0x01, 0x80, 0x33]; // TsaN (2)
622		let au = length_prefixed(&[tsa]);
623
624		let frame = hvc1_frame(&au, 4, ts()).unwrap();
625		assert!(!frame.keyframe);
626	}
627
628	/// The real-SPS fixture resolves a full out-of-band config: dimensions from
629	/// the parsed SPS and the hvcC itself as the catalog `description`.
630	#[test]
631	fn config_from_hvcc_resolves_real_sps() {
632		let hvcc = fixtures::hvcc();
633		let config = config_from_hvcc(&hvcc).unwrap();
634
635		let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
636			panic!("expected H.265 codec")
637		};
638		assert!(!h265.in_band, "hvcC config is out-of-band");
639		assert_eq!(config.coded_width, Some(1280));
640		assert_eq!(config.coded_height, Some(720));
641		assert_eq!(config.description.as_deref(), Some(hvcc.as_ref()));
642	}
643
644	/// Hand-build an hvcC (the layout `build_hvcc` emits) and assert the
645	/// parameter sets and length size are recovered. Built by hand rather than
646	/// via `build_hvcc` so it doesn't need a real, parseable HEVC SPS.
647	#[test]
648	fn hvcc_params_parses_vps_sps_pps() {
649		let vps = &[0x40, 0x01, 0x0c][..]; // NAL type 32
650		let sps = &[0x42, 0x01, 0x01, 0x60][..]; // NAL type 33
651		let pps = &[0x44, 0x01, 0xc0][..]; // NAL type 34
652
653		let mut hvcc = BytesMut::new();
654		hvcc.extend_from_slice(&[0u8; 21]); // fixed fields up to (but not including) byte 21
655		hvcc.put_u8(0xfc | 0x03); // byte 21: ...| lengthSizeMinusOne = 3 -> length_size 4
656		hvcc.put_u8(3); // numOfArrays
657		for (nal_type, nal) in [
658			(u8::from(NALUnitType::VpsNut), vps),
659			(u8::from(NALUnitType::SpsNut), sps),
660			(u8::from(NALUnitType::PpsNut), pps),
661		] {
662			hvcc.put_u8(0x80 | (nal_type & 0x3f));
663			hvcc.put_u16(1); // numNalus
664			hvcc.put_u16(nal.len() as u16);
665			hvcc.put_slice(nal);
666		}
667
668		let (length_size, params) = hvcc_params(&hvcc).unwrap();
669		assert_eq!(length_size, 4);
670		assert_eq!(params.len(), 3);
671		assert_eq!(params[0].as_ref(), vps);
672		assert_eq!(params[1].as_ref(), sps);
673		assert_eq!(params[2].as_ref(), pps);
674	}
675
676	/// hev1: an inline SPS whose VUI carries zeroed colour fields on 4:2:0 still configures the
677	/// rendition from the fields before the VUI.
678	#[test]
679	fn hev1_config_tolerates_zeroed_colour_description() {
680		let mut annexb = Vec::new();
681		for nal in [fixtures::VPS, fixtures::SPS_ZERO_COLOUR, fixtures::PPS] {
682			annexb.extend_from_slice(&[0, 0, 0, 1]);
683			annexb.extend_from_slice(nal);
684		}
685		let config = config(&annexb).expect("the SPS head is well-formed");
686		let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
687			panic!("expected H.265 codec")
688		};
689		assert!(h265.in_band);
690		assert_eq!(h265.to_string(), "hev1.1.6.L150");
691		assert_eq!((config.coded_width, config.coded_height), (Some(1920), Some(1080)));
692	}
693
694	/// hvc1: the same SPS builds an hvcC and resolves an out-of-band config. The record header
695	/// matches, byte for byte, the one GStreamer's h265parse builds from this SPS.
696	#[test]
697	fn hvcc_tolerates_zeroed_colour_description() {
698		let hvcc = build_hvcc(
699			&[Bytes::from_static(fixtures::VPS)],
700			&[Bytes::from_static(fixtures::SPS_ZERO_COLOUR)],
701			&[Bytes::from_static(fixtures::PPS)],
702		)
703		.expect("the SPS head is well-formed");
704		assert_eq!(
705			&hvcc[..23],
706			&[
707				0x01, 0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0xf0, 0x00, 0xfc, 0xfd,
708				0xf8, 0xf8, 0x00, 0x00, 0x0f, 0x03,
709			]
710		);
711		let config = config_from_hvcc(&hvcc).unwrap();
712		assert_eq!((config.coded_width, config.coded_height), (Some(1920), Some(1080)));
713	}
714
715	/// The SPS and catalog paths keep `profile_compatibility_flags` as raw
716	/// big-endian bytes, including across codec-string formatting and parsing.
717	#[test]
718	fn bitstream_and_catalog_paths_agree() {
719		use std::str::FromStr;
720
721		let config = config_from_hvcc(&fixtures::hvcc()).unwrap();
722		let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
723			panic!("expected H.265 codec")
724		};
725
726		// x265 Main profile: general_profile_compatibility_flag[1] and [2] set.
727		assert_eq!(h265.profile_compatibility_flags, [0x60, 0, 0, 0]);
728
729		let encoded = h265.to_string();
730		assert_eq!(encoded, "hvc1.1.6.L93.90");
731
732		let parsed = hang::catalog::H265::from_str(&encoded).unwrap();
733		assert_eq!(parsed.profile_compatibility_flags, h265.profile_compatibility_flags);
734	}
735}