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`] is the Annex-B importer.
8
9mod export;
10mod import;
11mod split;
12
13pub use export::*;
14pub use import::*;
15pub use split::*;
16
17use bytes::{Buf, BufMut, Bytes, BytesMut};
18use scuffle_h265::{NALUnitType, SpsNALUnit};
19
20/// H.265 parsing and transform errors.
21#[derive(Debug, Clone, thiserror::Error)]
22#[non_exhaustive]
23pub enum Error {
24	#[error("NAL unit is too short")]
25	NalTooShort,
26
27	#[error("{0} too large for hvcC length field ({1} > {max})", max = u16::MAX)]
28	NalTooLargeForHvcc(&'static str, usize),
29
30	#[error("too many {0} for hvcC ({1} > {max})", max = u16::MAX)]
31	TooManyNals(&'static str, usize),
32
33	#[error("NAL too large for 4-byte length prefix")]
34	NalTooLarge,
35
36	#[error("failed to parse SPS NAL unit")]
37	SpsParse,
38
39	#[error("missing level_idc in SPS")]
40	MissingLevelIdc,
41
42	#[error("forbidden zero bit is not zero")]
43	ForbiddenZeroBit,
44
45	#[error("not initialized")]
46	NotInitialized,
47
48	#[error("expected SPS before any frames")]
49	MissingSps,
50
51	#[error("missing timestamp")]
52	MissingTimestamp,
53
54	#[error("HEVCDecoderConfigurationRecord too short")]
55	HvccTooShort,
56
57	#[error("HEVCDecoderConfigurationRecord truncated")]
58	HvccTruncated,
59
60	#[error("hvc1 description for rendition {name:?} is missing VPS, SPS, or PPS (vps={vps}, sps={sps}, pps={pps})")]
61	MissingParamSets {
62		name: String,
63		vps: usize,
64		sps: usize,
65		pps: usize,
66	},
67
68	#[error("annexb: {0}")]
69	Annexb(#[from] crate::codec::annexb::Error),
70}
71
72pub type Result<T> = std::result::Result<T, Error>;
73
74/// The parameter sets carried out-of-band in an HEVCDecoderConfigurationRecord,
75/// split by NAL type.
76#[derive(Debug, Clone)]
77#[non_exhaustive]
78pub struct Hvcc {
79	/// NALU length size in bytes (typically 4).
80	pub length_size: usize,
81	/// VPS NAL units carried out-of-band in the record.
82	pub vps: Vec<Bytes>,
83	/// SPS NAL units carried out-of-band in the record.
84	pub sps: Vec<Bytes>,
85	/// PPS NAL units carried out-of-band in the record.
86	pub pps: Vec<Bytes>,
87}
88
89impl Hvcc {
90	/// Parse an HEVCDecoderConfigurationRecord, sorting the VPS/SPS/PPS NAL units
91	/// by type. The HEVC analogue of [`super::h264::Avcc::parse`].
92	pub fn parse(hvcc: &[u8]) -> Result<Self> {
93		if hvcc.len() < 23 {
94			return Err(Error::HvccTooShort);
95		}
96		let length_size = (hvcc[21] & 0x3) as usize + 1;
97		let num_arrays = hvcc[22] as usize;
98
99		let mut vps = Vec::new();
100		let mut sps = Vec::new();
101		let mut pps = Vec::new();
102		let mut pos: usize = 23;
103
104		for _ in 0..num_arrays {
105			let after_hdr = pos.checked_add(3).ok_or(Error::HvccTruncated)?;
106			if hvcc.len() < after_hdr {
107				return Err(Error::HvccTruncated);
108			}
109			let nal_type = hvcc[pos] & 0x3f;
110			let num_nalus = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]) as usize;
111			pos = after_hdr;
112
113			for _ in 0..num_nalus {
114				let after_len = pos.checked_add(2).ok_or(Error::HvccTruncated)?;
115				if hvcc.len() < after_len {
116					return Err(Error::HvccTruncated);
117				}
118				let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
119				let after_nal = after_len.checked_add(len).ok_or(Error::HvccTruncated)?;
120				if hvcc.len() < after_nal {
121					return Err(Error::HvccTruncated);
122				}
123				let bytes = Bytes::copy_from_slice(&hvcc[after_len..after_nal]);
124				pos = after_nal;
125
126				match NALUnitType::from(nal_type) {
127					NALUnitType::VpsNut => vps.push(bytes),
128					NALUnitType::SpsNut => sps.push(bytes),
129					NALUnitType::PpsNut => pps.push(bytes),
130					_ => {}
131				}
132			}
133		}
134
135		Ok(Self {
136			length_size,
137			vps,
138			sps,
139			pps,
140		})
141	}
142}
143
144/// Build a catalog [`VideoConfig`](hang::catalog::VideoConfig) for the `hvc1`
145/// shape from an HEVCDecoderConfigurationRecord (hvcC).
146///
147/// The H.265 analogue of [`crate::codec::h264::Avcc::parse`] feeding a
148/// `VideoConfig`. Used by the enhanced-RTMP / FLV importer, where the hvcC
149/// arrives out of band in the sequence-header tag and the coded samples are
150/// already length-prefixed NALU, so the record passes straight through as the
151/// catalog `description` (`in_band: false`).
152pub(crate) fn config_from_hvcc(hvcc: &[u8]) -> Result<hang::catalog::VideoConfig> {
153	let params = Hvcc::parse(hvcc)?;
154	let sps_nal = params.sps.first().ok_or(Error::MissingSps)?;
155	let sps = SpsNALUnit::parse(&mut &sps_nal[..]).map_err(|_| Error::SpsParse)?;
156	let profile = &sps.rbsp.profile_tier_level.general_profile;
157
158	let mut config = hang::catalog::VideoConfig::new(hang::catalog::H265 {
159		in_band: false,
160		profile_space: profile.profile_space,
161		profile_idc: profile.profile_idc,
162		profile_compatibility_flags: profile.profile_compatibility_flag.bits().to_be_bytes(),
163		tier_flag: profile.tier_flag,
164		level_idc: profile.level_idc.ok_or(Error::MissingLevelIdc)?,
165		constraint_flags: pack_constraint_flags(profile),
166	});
167	config.coded_width = Some(sps.rbsp.cropped_width() as u32);
168	config.coded_height = Some(sps.rbsp.cropped_height() as u32);
169	config.description = Some(Bytes::copy_from_slice(hvcc));
170	config.container = hang::catalog::Container::Legacy;
171	Ok(config)
172}
173
174/// Annex-B → length-prefixed transmuxer; the H.265 analogue of
175/// [`crate::codec::h264::Avc1`].
176///
177/// The active VPS/SPS/PPS set is scoped to the latest keyframe: a frame that
178/// carries parameter sets redefines them, so a mid-stream reconfiguration drops
179/// the superseded ones instead of accumulating them forever.
180pub struct Hvc1 {
181	hvcc: Option<Bytes>,
182	/// The active VPS NALs (from the most recent keyframe that carried them).
183	vps: Vec<Bytes>,
184	/// The active SPS NALs.
185	sps: Vec<Bytes>,
186	/// The active PPS NALs.
187	pps: Vec<Bytes>,
188}
189
190impl Default for Hvc1 {
191	fn default() -> Self {
192		Self::new()
193	}
194}
195
196impl Hvc1 {
197	/// Build a new transform for a hev1 source.
198	pub fn new() -> Self {
199		Self {
200			hvcc: None,
201			vps: Vec::new(),
202			sps: Vec::new(),
203			pps: Vec::new(),
204		}
205	}
206
207	/// The HEVCDecoderConfigurationRecord, available once VPS+SPS+PPS have been observed.
208	pub fn hvcc(&self) -> Option<&Bytes> {
209		self.hvcc.as_ref()
210	}
211
212	/// Convert one decoded frame's payload to the hvc1 wire shape.
213	///
214	/// Returns:
215	/// - `Ok(Some(payload))` if a length-prefixed sample is ready to emit.
216	/// - `Ok(None)` if the input contained only parameter sets and the
217	///   transform is still waiting for slice NALs (hvcC may have been
218	///   built as a side effect).
219	pub fn transform(&mut self, payload: Bytes) -> Result<Option<Bytes>> {
220		let mut buf = payload.clone();
221		let mut nal_iter = crate::codec::annexb::NalIterator::new(&mut buf);
222
223		let mut out = BytesMut::with_capacity(payload.remaining());
224		let mut frame_vps: Vec<Bytes> = Vec::new();
225		let mut frame_sps: Vec<Bytes> = Vec::new();
226		let mut frame_pps: Vec<Bytes> = Vec::new();
227		let mut emitted_any_slice = false;
228
229		loop {
230			let nal = match nal_iter.next() {
231				Some(Ok(n)) => n,
232				Some(Err(e)) => return Err(e.into()),
233				None => break,
234			};
235			if process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)? {
236				emitted_any_slice = true;
237			}
238		}
239
240		if let Some(nal) = nal_iter.flush()? {
241			if process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)? {
242				emitted_any_slice = true;
243			}
244		}
245
246		// A frame that carries parameter sets (a keyframe) redefines the active
247		// set; adopt it so a superseded configuration's VPS/SPS/PPS are dropped
248		// rather than lingering in the hvcC. Per type, so a frame that updates only
249		// one kind keeps the others.
250		let mut changed = false;
251		if !frame_vps.is_empty() && frame_vps != self.vps {
252			self.vps = frame_vps;
253			changed = true;
254		}
255		if !frame_sps.is_empty() && frame_sps != self.sps {
256			self.sps = frame_sps;
257			changed = true;
258		}
259		if !frame_pps.is_empty() && frame_pps != self.pps {
260			self.pps = frame_pps;
261			changed = true;
262		}
263		if changed {
264			self.rebuild_hvcc()?;
265		}
266
267		if !emitted_any_slice {
268			return Ok(None);
269		}
270
271		Ok(Some(out.freeze()))
272	}
273
274	fn rebuild_hvcc(&mut self) -> Result<()> {
275		if self.vps.is_empty() || self.sps.is_empty() || self.pps.is_empty() {
276			return Ok(());
277		}
278		self.hvcc = Some(build_hvcc(&self.vps, &self.sps, &self.pps)?);
279		Ok(())
280	}
281}
282
283/// Process one NAL: VPS/SPS/PPS are collected (distinctly) into this frame's
284/// sets, everything else is length-prefixed and appended to `out`. Returns true
285/// if the NAL was a slice (i.e. produced sample bytes).
286fn process_nal(
287	nal: &Bytes,
288	out: &mut BytesMut,
289	frame_vps: &mut Vec<Bytes>,
290	frame_sps: &mut Vec<Bytes>,
291	frame_pps: &mut Vec<Bytes>,
292) -> Result<bool> {
293	if nal.is_empty() {
294		return Ok(false);
295	}
296	// HEVC NAL header is 2 bytes; type is bits 1..=6 of byte 0.
297	match NALUnitType::from((nal[0] >> 1) & 0x3f) {
298		NALUnitType::VpsNut => {
299			crate::codec::annexb::push_distinct(frame_vps, nal);
300			Ok(false)
301		}
302		NALUnitType::SpsNut => {
303			crate::codec::annexb::push_distinct(frame_sps, nal);
304			Ok(false)
305		}
306		NALUnitType::PpsNut => {
307			crate::codec::annexb::push_distinct(frame_pps, nal);
308			Ok(false)
309		}
310		_ => {
311			let len = u32::try_from(nal.len()).map_err(|_| Error::NalTooLarge)?;
312			out.extend_from_slice(&len.to_be_bytes());
313			out.extend_from_slice(nal);
314			Ok(true)
315		}
316	}
317}
318
319/// Build an HEVCDecoderConfigurationRecord (ISO/IEC 14496-15 §8.3.3).
320/// Single-layer streams only. Each NAL array (VPS, SPS, PPS) carries every
321/// distinct parameter set the stream defined, in arrival order; the profile/tier
322/// fields are read from the first SPS.
323pub(crate) fn build_hvcc(vps_nals: &[Bytes], sps_nals: &[Bytes], pps_nals: &[Bytes]) -> Result<Bytes> {
324	let first_sps = sps_nals.first().ok_or(Error::MissingSps)?;
325	for (label, nals) in [("VPS", vps_nals), ("SPS", sps_nals), ("PPS", pps_nals)] {
326		if nals.len() > u16::MAX as usize {
327			return Err(Error::TooManyNals(label, nals.len()));
328		}
329		for nal in nals {
330			if nal.len() > u16::MAX as usize {
331				return Err(Error::NalTooLargeForHvcc(label, nal.len()));
332			}
333		}
334	}
335
336	let sps = SpsNALUnit::parse(&mut &first_sps[..]).map_err(|_| Error::SpsParse)?;
337	let profile = &sps.rbsp.profile_tier_level.general_profile;
338	let level_idc = profile.level_idc.ok_or(Error::MissingLevelIdc)?;
339	let constraint_flags = pack_constraint_flags(profile);
340	let compat = profile.profile_compatibility_flag.bits().to_be_bytes();
341	let num_temporal_layers = sps.rbsp.sps_max_sub_layers_minus1 + 1;
342
343	let params_len: usize = vps_nals
344		.iter()
345		.chain(sps_nals)
346		.chain(pps_nals)
347		.map(|n| 2 + n.len())
348		.sum();
349	let mut out = BytesMut::with_capacity(23 + 3 * 3 + params_len);
350	out.put_u8(1); // configurationVersion
351	out.put_u8(((profile.profile_space & 0x3) << 6) | ((profile.tier_flag as u8) << 5) | (profile.profile_idc & 0x1f));
352	out.put_slice(&compat);
353	out.put_slice(&constraint_flags);
354	out.put_u8(level_idc);
355	out.put_u16(0xf000); // min_spatial_segmentation_idc unknown
356	out.put_u8(0xfc); // parallelismType mixed
357	out.put_u8(0xfc | (sps.rbsp.chroma_format_idc & 0x3));
358	out.put_u8(0xf8 | (sps.rbsp.bit_depth_luma_minus8 & 0x7));
359	out.put_u8(0xf8 | (sps.rbsp.bit_depth_chroma_minus8 & 0x7));
360	out.put_u16(0); // avgFrameRate unspecified
361	out.put_u8(((num_temporal_layers & 0x7) << 3) | ((sps.rbsp.sps_temporal_id_nesting_flag as u8) << 2) | 0x3);
362	out.put_u8(3); // numOfArrays (VPS, SPS, PPS)
363
364	for (nal_type, nals) in [
365		(u8::from(NALUnitType::VpsNut), vps_nals),
366		(u8::from(NALUnitType::SpsNut), sps_nals),
367		(u8::from(NALUnitType::PpsNut), pps_nals),
368	] {
369		out.put_u8(0x80 | (nal_type & 0x3f)); // array_completeness = 1
370		out.put_u16(nals.len() as u16); // numNalus
371		for nal in nals {
372			out.put_u16(nal.len() as u16);
373			out.put_slice(nal);
374		}
375	}
376
377	Ok(out.freeze())
378}
379
380/// Extract the parameter-set NALs (VPS, SPS, PPS in array order) and the NALU
381/// length size from an HEVCDecoderConfigurationRecord. The inverse of
382/// [`build_hvcc`]; used to re-emit out-of-band hvc1 parameter sets as inline
383/// Annex-B (e.g. for MPEG-TS).
384pub(crate) fn hvcc_params(hvcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
385	anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
386	let length_size = (hvcc[21] & 0x03) as usize + 1;
387	let num_arrays = hvcc[22];
388
389	let mut params = Vec::new();
390	let mut pos = 23;
391	for _ in 0..num_arrays {
392		// Skip the array_completeness | NAL_unit_type byte.
393		anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
394		pos += 1;
395		let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]);
396		pos += 2;
397		for _ in 0..num_nalus {
398			anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
399			let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
400			pos += 2;
401			anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
402			params.push(Bytes::copy_from_slice(&hvcc[pos..pos + len]));
403			pos += len;
404		}
405	}
406
407	Ok((length_size, params))
408}
409
410/// Pack the constraint flags from ITU H.265 V10 §7.3.3 Profile, tier and level syntax.
411pub(crate) fn pack_constraint_flags(profile: &scuffle_h265::Profile) -> [u8; 6] {
412	let mut flags = [0u8; 6];
413	flags[0] = ((profile.progressive_source_flag as u8) << 7)
414		| ((profile.interlaced_source_flag as u8) << 6)
415		| ((profile.non_packed_constraint_flag as u8) << 5)
416		| ((profile.frame_only_constraint_flag as u8) << 4);
417	flags
418}
419
420#[cfg(test)]
421mod tests {
422	use super::*;
423
424	/// Hand-build an hvcC (the layout `build_hvcc` emits) and assert the
425	/// parameter sets and length size are recovered. Built by hand rather than
426	/// via `build_hvcc` so it doesn't need a real, parseable HEVC SPS.
427	#[test]
428	fn hvcc_params_parses_vps_sps_pps() {
429		let vps = &[0x40, 0x01, 0x0c][..]; // NAL type 32
430		let sps = &[0x42, 0x01, 0x01, 0x60][..]; // NAL type 33
431		let pps = &[0x44, 0x01, 0xc0][..]; // NAL type 34
432
433		let mut hvcc = BytesMut::new();
434		hvcc.extend_from_slice(&[0u8; 21]); // fixed fields up to (but not including) byte 21
435		hvcc.put_u8(0xfc | 0x03); // byte 21: ...| lengthSizeMinusOne = 3 -> length_size 4
436		hvcc.put_u8(3); // numOfArrays
437		for (nal_type, nal) in [
438			(u8::from(NALUnitType::VpsNut), vps),
439			(u8::from(NALUnitType::SpsNut), sps),
440			(u8::from(NALUnitType::PpsNut), pps),
441		] {
442			hvcc.put_u8(0x80 | (nal_type & 0x3f));
443			hvcc.put_u16(1); // numNalus
444			hvcc.put_u16(nal.len() as u16);
445			hvcc.put_slice(nal);
446		}
447
448		let (length_size, params) = hvcc_params(&hvcc).unwrap();
449		assert_eq!(length_size, 4);
450		assert_eq!(params.len(), 3);
451		assert_eq!(params[0].as_ref(), vps);
452		assert_eq!(params[1].as_ref(), sps);
453		assert_eq!(params[2].as_ref(), pps);
454	}
455}