Skip to main content

moq_mux/codec/h264/
mod.rs

1//! H.264 / AVC.
2//!
3//! Parses SPS NAL units and AVCDecoderConfigurationRecord blobs into
4//! catalog-ready fields. The [`Avc1`] transmuxer rewrites Annex-B input
5//! (inline SPS/PPS) as length-prefixed NALU + out-of-band avcC, which is
6//! what every CMAF and MKV consumer expects. [`Export`] subscribes to a
7//! catalog-narrowed H.264 rendition and emits an Annex-B elementary
8//! stream; [`Split`] does the byte-level framing for the Annex-B (avc3)
9//! wire shape and [`Import`] is the pure frame publisher that resolves the
10//! catalog. avc1 (length-prefixed NALU) has no stream framing; wrap one
11//! access unit with `avc1_frame`.
12
13mod export;
14mod import;
15mod split;
16
17pub use export::*;
18pub use import::*;
19pub use split::*;
20
21use bytes::{Buf, BufMut, Bytes, BytesMut};
22
23// H.264 NAL unit types (ISO/IEC 14496-10 §7.4.1).
24const NAL_TYPE_SPS: u8 = 7;
25const NAL_TYPE_PPS: u8 = 8;
26
27/// Wrap one avc1 (length-prefixed NALU) access unit as a single
28/// [`Frame`](crate::container::Frame), with the keyframe flag set when it
29/// carries an IDR slice (NAL type 5).
30///
31/// avc1 is not a stream: each access unit arrives whole with its NALU
32/// `length_size` known out-of-band from the avcC (`super::Avcc::parse(avcc).length_size`).
33/// The payload is passed through verbatim.
34pub(crate) fn avc1_frame(
35	data: impl moq_net::IntoBytes,
36	length_size: usize,
37	pts: moq_net::Timestamp,
38) -> crate::Result<crate::container::Frame> {
39	let keyframe = avc1_is_keyframe(data.as_ref(), length_size);
40	Ok(crate::container::Frame {
41		timestamp: pts,
42		payload: data.into_bytes(),
43		keyframe,
44		duration: None,
45	})
46}
47
48/// Detect whether an avc1-shaped (length-prefixed) buffer contains an IDR slice.
49fn avc1_is_keyframe(data: &[u8], length_size: usize) -> bool {
50	let Ok(nals) = crate::codec::annexb::length_prefixed_nals(data, length_size) else {
51		return false;
52	};
53	nals.map_while(std::result::Result::ok)
54		.any(|nal| nal.first().is_some_and(|header| header & 0x1f == 5))
55}
56
57/// H.264 parsing and transform errors.
58#[derive(Debug, Clone, thiserror::Error)]
59#[non_exhaustive]
60pub enum Error {
61	#[error("SPS NAL too short")]
62	SpsTooShort,
63
64	#[error("failed to parse SPS")]
65	SpsParse,
66
67	#[error("AVCDecoderConfigurationRecord too short")]
68	AvccTooShort,
69
70	#[error("AVCDecoderConfigurationRecord truncated")]
71	AvccTruncated,
72
73	#[error("avc1 description for rendition {name:?} is missing SPS or PPS (sps={sps}, pps={pps})")]
74	MissingParamSets { name: String, sps: usize, pps: usize },
75
76	#[error("SPS too large for avcC length field ({0} > {max})", max = u16::MAX)]
77	SpsTooLarge(usize),
78
79	#[error("PPS too large for avcC length field ({0} > {max})", max = u16::MAX)]
80	PpsTooLarge(usize),
81
82	#[error("avcC requires at least one SPS")]
83	MissingSps,
84
85	#[error("too many SPS for avcC ({0} > 31)")]
86	TooManySps(usize),
87
88	#[error("too many PPS for avcC ({0} > 255)")]
89	TooManyPps(usize),
90
91	#[error("NAL too large for 4-byte length prefix")]
92	NalTooLarge,
93
94	#[error("NAL unit is too short")]
95	NalTooShort,
96
97	#[error("forbidden zero bit is not zero")]
98	ForbiddenZeroBit,
99
100	#[error("not initialized")]
101	NotInitialized,
102
103	#[error("avc3 track not created")]
104	Avc3TrackNotCreated,
105
106	#[error("missing timestamp")]
107	MissingTimestamp,
108
109	#[error("annexb: {0}")]
110	Annexb(#[from] crate::codec::annexb::Error),
111}
112
113pub type Result<T> = std::result::Result<T, Error>;
114
115/// Parsed H.264 SPS (Sequence Parameter Set) NAL.
116///
117/// Wraps [`h264_parser::Sps`] with the codec-config fields that the hang
118/// catalog records: profile_idc, level_idc, and the packed constraint_set
119/// flags. The first byte of `nal` must be the NAL header.
120#[derive(Debug, Clone)]
121pub struct Sps {
122	pub profile: u8,
123	pub constraints: u8,
124	pub level: u8,
125	pub coded_width: u32,
126	pub coded_height: u32,
127}
128
129impl Sps {
130	/// Parse an SPS NAL unit.
131	pub fn parse(nal: &[u8]) -> Result<Self> {
132		if nal.len() < 4 {
133			return Err(Error::SpsTooShort);
134		}
135		let rbsp = h264_parser::nal::ebsp_to_rbsp(&nal[1..]);
136		let sps = h264_parser::Sps::parse(&rbsp).map_err(|_| Error::SpsParse)?;
137		Ok(Self {
138			profile: sps.profile_idc,
139			constraints: pack_constraint_flags(&sps),
140			level: sps.level_idc,
141			coded_width: sps.width,
142			coded_height: sps.height,
143		})
144	}
145}
146
147/// The reordering an SPS NAL unit declares in its VUI: `max_num_reorder_frames` from the
148/// bitstream restriction, and the frame period when `fixed_frame_rate_flag` is set.
149///
150/// `None` when the SPS carries no VUI or no bitstream restriction, or fails to parse.
151pub(crate) fn sps_reorder(nal: &[u8]) -> Option<crate::codec::video::Reorder> {
152	let vui = sps_vui(nal)?;
153	Some(crate::codec::video::Reorder {
154		depth: vui.depth?,
155		period: vui.period,
156	})
157}
158
159/// The frame rate an SPS NAL unit fixes with `fixed_frame_rate_flag`.
160///
161/// Without the flag the VUI tick only bounds the rate from above, so it is left to measurement.
162/// A field-capable SPS is left out too: `pic_struct` in the slice header then decides whether a
163/// tick counts a field or a frame (ITU-T H.264 E.2.1, Table E-6), and only measurement settles it.
164pub(crate) fn sps_framerate(nal: &[u8]) -> Option<f64> {
165	let vui = sps_vui(nal)?;
166	if !vui.frame_only {
167		return None;
168	}
169	let (units, scale) = vui.period?;
170	Some(scale as f64 / units as f64)
171}
172
173/// What an SPS VUI declares about picture timing.
174struct Vui {
175	/// `frame_mbs_only_flag`: every picture is a frame, so two ticks are one frame.
176	frame_only: bool,
177	/// One frame's duration as `(units, scale)` when `fixed_frame_rate_flag` is set.
178	period: Option<(u64, u64)>,
179	/// `max_num_reorder_frames`, when the bitstream restriction is present.
180	depth: Option<u32>,
181}
182
183/// `None` when the SPS carries no VUI or fails to parse.
184fn sps_vui(nal: &[u8]) -> Option<Vui> {
185	if nal.len() < 4 {
186		return None;
187	}
188	let rbsp = h264_parser::nal::ebsp_to_rbsp(&nal[1..]);
189	let sps = h264_parser::Sps::parse(&rbsp).ok()?;
190	if !sps.vui_parameters_present_flag {
191		return None;
192	}
193	vui(&rbsp).ok().flatten()
194}
195
196/// Walk an SPS RBSP to its VUI (ITU-T H.264 7.3.2.1.1, E.1.1). `h264_parser` stops at
197/// `vui_parameters_present_flag` without exposing its position, so this re-reads the fields
198/// before it.
199fn vui(rbsp: &[u8]) -> h264_parser::Result<Option<Vui>> {
200	use h264_parser::bitreader::BitReader;
201	use h264_parser::eg::{read_se, read_ue};
202
203	let mut r = BitReader::new(rbsp);
204	let profile_idc = r.read_u8()?;
205	r.skip_bits(16)?; // constraint flags, reserved bits, level_idc
206	read_ue(&mut r)?; // seq_parameter_set_id
207	if matches!(
208		profile_idc,
209		100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
210	) {
211		let chroma_format_idc = read_ue(&mut r)?;
212		if chroma_format_idc == 3 {
213			r.skip_bits(1)?; // separate_colour_plane_flag
214		}
215		read_ue(&mut r)?; // bit_depth_luma_minus8
216		read_ue(&mut r)?; // bit_depth_chroma_minus8
217		r.skip_bits(1)?; // qpprime_y_zero_transform_bypass_flag
218		if r.read_flag()? {
219			let lists = if chroma_format_idc == 3 { 12 } else { 8 };
220			for i in 0..lists {
221				if r.read_flag()? {
222					let size = if i < 6 { 16 } else { 64 };
223					let (mut last, mut next) = (8i32, 8i32);
224					for _ in 0..size {
225						if next != 0 {
226							next = (last + read_se(&mut r)?).rem_euclid(256);
227						}
228						if next != 0 {
229							last = next;
230						}
231					}
232				}
233			}
234		}
235	}
236	read_ue(&mut r)?; // log2_max_frame_num_minus4
237	match read_ue(&mut r)? {
238		0 => {
239			read_ue(&mut r)?; // log2_max_pic_order_cnt_lsb_minus4
240		}
241		1 => {
242			r.skip_bits(1)?; // delta_pic_order_always_zero_flag
243			read_se(&mut r)?; // offset_for_non_ref_pic
244			read_se(&mut r)?; // offset_for_top_to_bottom_field
245			for _ in 0..read_ue(&mut r)? {
246				read_se(&mut r)?; // offset_for_ref_frame
247			}
248		}
249		_ => {}
250	}
251	read_ue(&mut r)?; // max_num_ref_frames
252	r.skip_bits(1)?; // gaps_in_frame_num_value_allowed_flag
253	read_ue(&mut r)?; // pic_width_in_mbs_minus1
254	read_ue(&mut r)?; // pic_height_in_map_units_minus1
255	let frame_only = r.read_flag()?; // frame_mbs_only_flag
256	if !frame_only {
257		r.skip_bits(1)?; // mb_adaptive_frame_field_flag
258	}
259	r.skip_bits(1)?; // direct_8x8_inference_flag
260	if r.read_flag()? {
261		for _ in 0..4 {
262			read_ue(&mut r)?; // frame_crop_*_offset
263		}
264	}
265	if !r.read_flag()? {
266		return Ok(None);
267	}
268
269	// vui_parameters()
270	if r.read_flag()? && r.read_u8()? == 255 {
271		r.skip_bits(32)?; // sar_width, sar_height
272	}
273	if r.read_flag()? {
274		r.skip_bits(1)?; // overscan_appropriate_flag
275	}
276	if r.read_flag()? {
277		r.skip_bits(4)?; // video_format, video_full_range_flag
278		if r.read_flag()? {
279			r.skip_bits(24)?; // colour_primaries, transfer_characteristics, matrix_coefficients
280		}
281	}
282	if r.read_flag()? {
283		read_ue(&mut r)?; // chroma_sample_loc_type_top_field
284		read_ue(&mut r)?; // chroma_sample_loc_type_bottom_field
285	}
286	let mut period = None;
287	if r.read_flag()? {
288		let units = r.read_bits(32)?;
289		let scale = r.read_bits(32)?;
290		// With a fixed rate, a frame lasts two ticks (E.2.1).
291		if r.read_flag()? && units > 0 && scale > 0 {
292			period = Some((2 * u64::from(units), u64::from(scale)));
293		}
294	}
295	let nal_hrd = r.read_flag()?;
296	if nal_hrd {
297		skip_hrd(&mut r)?;
298	}
299	let vcl_hrd = r.read_flag()?;
300	if vcl_hrd {
301		skip_hrd(&mut r)?;
302	}
303	if nal_hrd || vcl_hrd {
304		r.skip_bits(1)?; // low_delay_hrd_flag
305	}
306	r.skip_bits(1)?; // pic_struct_present_flag
307	if !r.read_flag()? {
308		return Ok(Some(Vui {
309			frame_only,
310			period,
311			depth: None,
312		}));
313	}
314	r.skip_bits(1)?; // motion_vectors_over_pic_boundaries_flag
315	for _ in 0..4 {
316		read_ue(&mut r)?; // max_bytes_per_pic_denom .. log2_max_mv_length_vertical
317	}
318	let depth = read_ue(&mut r)?; // max_num_reorder_frames
319	Ok(Some(Vui {
320		frame_only,
321		period,
322		depth: Some(depth),
323	}))
324}
325
326/// Skip `hrd_parameters()` (ITU-T H.264 E.1.2).
327fn skip_hrd(r: &mut h264_parser::bitreader::BitReader) -> h264_parser::Result<()> {
328	use h264_parser::eg::read_ue;
329
330	let cpb_cnt_minus1 = read_ue(r)?;
331	if cpb_cnt_minus1 > 31 {
332		return Err(h264_parser::Error::MalformedSps("cpb_cnt_minus1 out of range".into()));
333	}
334	r.skip_bits(8)?; // bit_rate_scale, cpb_size_scale
335	for _ in 0..=cpb_cnt_minus1 {
336		read_ue(r)?; // bit_rate_value_minus1
337		read_ue(r)?; // cpb_size_value_minus1
338		r.skip_bits(1)?; // cbr_flag
339	}
340	r.skip_bits(20) // four delay/offset lengths
341}
342
343/// Parsed AVCDecoderConfigurationRecord (ISO/IEC 14496-15 §5.3.3.1.2).
344///
345/// Just the codec-config fields that the hang catalog records. The original
346/// avcC bytes are still what gets stored as the catalog `description`; this
347/// struct is for the field extraction.
348#[derive(Debug, Clone)]
349#[non_exhaustive]
350pub struct Avcc {
351	/// AVC profile indication (`profile_idc`) from the record.
352	pub profile: u8,
353	/// Packed constraint-set flags byte from the record.
354	pub constraints: u8,
355	/// AVC level indication (`level_idc`) from the record.
356	pub level: u8,
357	/// NALU length size in bytes (typically 4).
358	pub length_size: usize,
359	/// SPS NAL units carried out-of-band in the record.
360	pub sps: Vec<Bytes>,
361	/// PPS NAL units carried out-of-band in the record.
362	pub pps: Vec<Bytes>,
363	/// Resolution from the embedded SPS, if one was present and parseable.
364	pub coded_width: Option<u32>,
365	pub coded_height: Option<u32>,
366}
367
368impl Avcc {
369	/// Parse an AVCDecoderConfigurationRecord buffer.
370	pub fn parse(avcc: &[u8]) -> Result<Self> {
371		if avcc.len() < 7 {
372			return Err(Error::AvccTooShort);
373		}
374
375		let profile = avcc[1];
376		let constraints = avcc[2];
377		let level = avcc[3];
378		let length_size = (avcc[4] & 0x03) as usize + 1;
379		let num_sps = (avcc[5] & 0x1f) as usize;
380
381		let mut pos = 6;
382		let sps = read_param_sets(avcc, &mut pos, num_sps)?;
383
384		if avcc.len() <= pos {
385			return Err(Error::AvccTruncated);
386		}
387		let num_pps = avcc[pos] as usize;
388		pos += 1;
389		let pps = read_param_sets(avcc, &mut pos, num_pps)?;
390
391		// Resolution from the first parseable SPS.
392		let (mut coded_width, mut coded_height) = (None, None);
393		if let Some(first) = sps.first()
394			&& first.len() > 1
395			&& let Ok(parsed) = Sps::parse(first)
396		{
397			coded_width = Some(parsed.coded_width);
398			coded_height = Some(parsed.coded_height);
399		}
400
401		Ok(Self {
402			profile,
403			constraints,
404			level,
405			length_size,
406			sps,
407			pps,
408			coded_width,
409			coded_height,
410		})
411	}
412}
413
414fn pack_constraint_flags(sps: &h264_parser::Sps) -> u8 {
415	((sps.constraint_set0_flag as u8) << 7)
416		| ((sps.constraint_set1_flag as u8) << 6)
417		| ((sps.constraint_set2_flag as u8) << 5)
418		| ((sps.constraint_set3_flag as u8) << 4)
419		| ((sps.constraint_set4_flag as u8) << 3)
420		| ((sps.constraint_set5_flag as u8) << 2)
421}
422
423/// Build an AVCDecoderConfigurationRecord (ISO/IEC 14496-15 §5.3.3.1.2) from the
424/// given SPS and PPS NALs. At least one SPS is required; the profile/level fields
425/// are read from the first SPS. A stream may legitimately carry several distinct
426/// SPS/PPS (slices reference them by id), so the record holds an ordered list of
427/// each rather than a single one.
428pub(crate) fn build_avcc(sps_nals: &[Bytes], pps_nals: &[Bytes]) -> Result<Bytes> {
429	let first_sps = sps_nals.first().ok_or(Error::MissingSps)?;
430	if first_sps.len() < 4 {
431		return Err(Error::SpsTooShort);
432	}
433	// numOfSequenceParameterSets is a 5-bit field, numOfPictureParameterSets a byte.
434	if sps_nals.len() > 0x1f {
435		return Err(Error::TooManySps(sps_nals.len()));
436	}
437	if pps_nals.len() > u8::MAX as usize {
438		return Err(Error::TooManyPps(pps_nals.len()));
439	}
440	for sps in sps_nals {
441		if sps.len() > u16::MAX as usize {
442			return Err(Error::SpsTooLarge(sps.len()));
443		}
444	}
445	for pps in pps_nals {
446		if pps.len() > u16::MAX as usize {
447			return Err(Error::PpsTooLarge(pps.len()));
448		}
449	}
450
451	let profile_idc = first_sps[1];
452	let constraints = first_sps[2];
453	let level_idc = first_sps[3];
454
455	let payload: usize = sps_nals.iter().chain(pps_nals).map(|n| 2 + n.len()).sum();
456	let mut out = BytesMut::with_capacity(7 + payload);
457	out.put_u8(1); // configurationVersion
458	out.put_u8(profile_idc);
459	out.put_u8(constraints);
460	out.put_u8(level_idc);
461	out.put_u8(0xff); // reserved (6 bits) | lengthSizeMinusOne (2 bits = 3)
462	out.put_u8(0xe0 | sps_nals.len() as u8); // reserved (3 bits) | numOfSequenceParameterSets
463	for sps in sps_nals {
464		out.put_u16(sps.len() as u16);
465		out.put_slice(sps);
466	}
467	out.put_u8(pps_nals.len() as u8); // numOfPictureParameterSets
468	for pps in pps_nals {
469		out.put_u16(pps.len() as u16);
470		out.put_slice(pps);
471	}
472	Ok(out.freeze())
473}
474
475/// Read `count` length-prefixed (u16) NAL units from `buf` starting at `*pos`,
476/// advancing `*pos` past the last one. All arithmetic is checked so malformed
477/// configs surface as errors rather than panics.
478fn read_param_sets(buf: &[u8], pos: &mut usize, count: usize) -> Result<Vec<Bytes>> {
479	let mut out = Vec::with_capacity(count);
480	for _ in 0..count {
481		let after_len = pos.checked_add(2).ok_or(Error::AvccTruncated)?;
482		if buf.len() < after_len {
483			return Err(Error::AvccTruncated);
484		}
485		let len = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]) as usize;
486		let after_nal = after_len.checked_add(len).ok_or(Error::AvccTruncated)?;
487		if buf.len() < after_nal {
488			return Err(Error::AvccTruncated);
489		}
490		out.push(Bytes::copy_from_slice(&buf[after_len..after_nal]));
491		*pos = after_nal;
492	}
493	Ok(out)
494}
495
496/// Extract the parameter-set NALs (SPS then PPS) and the NALU length size from
497/// an AVCDecoderConfigurationRecord. The inverse of [`build_avcc`]; used to
498/// re-emit out-of-band avc1 parameter sets as inline Annex-B (e.g. for MPEG-TS).
499pub(crate) fn avcc_params(avcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
500	anyhow::ensure!(avcc.len() >= 6, "AVCDecoderConfigurationRecord too short");
501	let length_size = (avcc[4] & 0x03) as usize + 1;
502
503	let mut params = Vec::new();
504	let num_sps = avcc[5] & 0x1f;
505	let mut pos = read_param_set_array(avcc, 6, num_sps as usize, &mut params)?;
506
507	anyhow::ensure!(avcc.len() > pos, "avcC missing PPS count");
508	let num_pps = avcc[pos];
509	pos += 1;
510	read_param_set_array(avcc, pos, num_pps as usize, &mut params)?;
511
512	Ok((length_size, params))
513}
514
515/// Read `count` u16-length-prefixed NALs starting at `pos`, appending each to
516/// `params`. Returns the offset just past the last NAL read.
517fn read_param_set_array(buf: &[u8], mut pos: usize, count: usize, params: &mut Vec<Bytes>) -> anyhow::Result<usize> {
518	for _ in 0..count {
519		anyhow::ensure!(buf.len() >= pos + 2, "truncated parameter-set length");
520		let len = u16::from_be_bytes([buf[pos], buf[pos + 1]]) as usize;
521		pos += 2;
522		anyhow::ensure!(buf.len() >= pos + len, "parameter-set NAL exceeds buffer");
523		params.push(Bytes::copy_from_slice(&buf[pos..pos + len]));
524		pos += len;
525	}
526	Ok(pos)
527}
528
529/// Transform H.264 frames from Annex-B (inline SPS/PPS, "avc3") to
530/// length-prefixed NALU (out-of-band AVCDecoderConfigurationRecord, "avc1").
531///
532/// The avcC is synthesized from the active SPS+PPS and exposed via
533/// [`Self::avcc`]. Once it returns `Some`, all subsequent calls to
534/// [`Self::transform`] return length-prefixed sample data suitable for an avc1
535/// container (e.g. MKV `V_MPEG4/ISO/AVC` with the avcC in CodecPrivate).
536///
537/// The active set is scoped to the latest keyframe: a frame that carries
538/// parameter sets redefines them, so a mid-stream reconfiguration drops the
539/// superseded SPS/PPS instead of accumulating them forever.
540pub struct Avc1 {
541	avcc: Option<Bytes>,
542	/// The active SPS NALs (from the most recent keyframe that carried them).
543	sps: Vec<Bytes>,
544	/// The active PPS NALs.
545	pps: Vec<Bytes>,
546}
547
548impl Default for Avc1 {
549	fn default() -> Self {
550		Self::new()
551	}
552}
553
554impl Avc1 {
555	/// Build a new transform for an avc3 source.
556	pub fn new() -> Self {
557		Self {
558			avcc: None,
559			sps: Vec::new(),
560			pps: Vec::new(),
561		}
562	}
563
564	/// The AVCDecoderConfigurationRecord, available once SPS+PPS have been observed.
565	pub fn avcc(&self) -> Option<&Bytes> {
566		self.avcc.as_ref()
567	}
568
569	/// Convert one decoded frame's payload to the avc1 wire shape.
570	///
571	/// Returns:
572	/// - `Ok(Some(payload))` if a length-prefixed sample is ready to emit.
573	/// - `Ok(None)` if the input contained only parameter sets and the
574	///   transform is still waiting for slice NALs (avcC may have been built
575	///   as a side effect).
576	pub fn transform(&mut self, payload: Bytes) -> Result<Option<Bytes>> {
577		// Parse Annex-B NALs, collect this frame's SPS/PPS, length-prefix the
578		// rest. NalIterator advances the Bytes cursor; the trailing NAL has to be
579		// pulled separately via flush().
580		let mut buf = payload.clone();
581		let mut nal_iter = crate::codec::annexb::NalIterator::new(&mut buf);
582
583		let mut out = BytesMut::with_capacity(payload.remaining());
584		let mut frame_sps: Vec<Bytes> = Vec::new();
585		let mut frame_pps: Vec<Bytes> = Vec::new();
586		let mut emitted_any_slice = false;
587
588		loop {
589			let nal = match nal_iter.next() {
590				Some(Ok(n)) => n,
591				Some(Err(e)) => return Err(e.into()),
592				None => break,
593			};
594			if process_nal(&nal, &mut out, &mut frame_sps, &mut frame_pps)? {
595				emitted_any_slice = true;
596			}
597		}
598
599		if let Some(nal) = nal_iter.flush()?
600			&& process_nal(&nal, &mut out, &mut frame_sps, &mut frame_pps)?
601		{
602			emitted_any_slice = true;
603		}
604
605		// A frame that carries parameter sets (a keyframe) redefines the active
606		// set; adopt it so SPS/PPS from a superseded configuration are dropped
607		// rather than lingering in the avcC. Per type, so a frame that updates only
608		// one of SPS/PPS keeps the other.
609		let mut changed = false;
610		if !frame_sps.is_empty() && frame_sps != self.sps {
611			self.sps = frame_sps;
612			changed = true;
613		}
614		if !frame_pps.is_empty() && frame_pps != self.pps {
615			self.pps = frame_pps;
616			changed = true;
617		}
618		if changed {
619			self.rebuild_avcc()?;
620		}
621
622		if !emitted_any_slice {
623			return Ok(None);
624		}
625
626		Ok(Some(out.freeze()))
627	}
628
629	fn rebuild_avcc(&mut self) -> Result<()> {
630		if self.sps.is_empty() || self.pps.is_empty() {
631			return Ok(());
632		}
633		self.avcc = Some(build_avcc(&self.sps, &self.pps)?);
634		Ok(())
635	}
636}
637
638/// Process one NAL: SPS/PPS are collected (distinctly) into this frame's sets,
639/// everything else is length-prefixed and appended to `out`. Returns true if the
640/// NAL was a slice (i.e. produced sample bytes).
641fn process_nal(
642	nal: &Bytes,
643	out: &mut BytesMut,
644	frame_sps: &mut Vec<Bytes>,
645	frame_pps: &mut Vec<Bytes>,
646) -> Result<bool> {
647	if nal.is_empty() {
648		return Ok(false);
649	}
650	match nal[0] & 0x1f {
651		NAL_TYPE_SPS => {
652			crate::codec::annexb::push_distinct(frame_sps, nal);
653			Ok(false)
654		}
655		NAL_TYPE_PPS => {
656			crate::codec::annexb::push_distinct(frame_pps, nal);
657			Ok(false)
658		}
659		_ => {
660			let len = u32::try_from(nal.len()).map_err(|_| Error::NalTooLarge)?;
661			out.extend_from_slice(&len.to_be_bytes());
662			out.extend_from_slice(nal);
663			Ok(true)
664		}
665	}
666}
667
668/// Real SPS NAL units from 64x64, 25 fps x264 encodes (High profile, 50 Hz VUI tick), for
669/// tests that need a VUI with a bitstream restriction.
670#[cfg(test)]
671pub(crate) mod fixtures {
672	/// `bframes=1:force-cfr=1`: one B-frame per reference (`max_num_reorder_frames` 1) with
673	/// `fixed_frame_rate_flag` set.
674	pub(crate) const SPS_IPB: &[u8] = &[
675		0x67, 0x64, 0x00, 0x0a, 0xac, 0xe4, 0x10, 0x9b, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03,
676		0x03, 0x28, 0xf1, 0x22, 0x51, 0x20,
677	];
678	/// `bframes=3:b-pyramid=2:force-cfr=1`: a B-pyramid (`max_num_reorder_frames` 2) with
679	/// `fixed_frame_rate_flag` set.
680	pub(crate) const SPS_PYRAMID: &[u8] = &[
681		0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, 0x44, 0x26, 0xc0, 0x44, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03,
682		0x00, 0xca, 0x3c, 0x48, 0x96, 0x58,
683	];
684	/// `bframes=1` at x264's default: `max_num_reorder_frames` 1 without `fixed_frame_rate_flag`.
685	pub(crate) const SPS_IPB_VARIABLE: &[u8] = &[
686		0x67, 0x64, 0x00, 0x0a, 0xac, 0xe4, 0x10, 0x9b, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03,
687		0x03, 0x20, 0xf1, 0x22, 0x51, 0x20,
688	];
689	/// `interlaced=1:tff=1:bframes=0` at 320x240, with `fixed_frame_rate_flag` forced on: the same
690	/// 50 Hz tick, but `frame_mbs_only_flag` 0 leaves `pic_struct` to the slice header.
691	pub(crate) const SPS_FIELD: &[u8] = &[
692		0x67, 0xf4, 0x00, 0x15, 0x91, 0x9d, 0x02, 0x82, 0x1f, 0x89, 0xc0, 0x44, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00,
693		0x00, 0x03, 0x00, 0xca, 0x7c, 0x50, 0xaa, 0x80,
694	];
695	pub(crate) const PPS: &[u8] = &[0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0];
696}
697
698#[cfg(test)]
699mod tests {
700	use super::*;
701
702	const SC4: &[u8] = &[0, 0, 0, 1];
703
704	#[test]
705	fn sps_reorder_reads_the_vui() {
706		use crate::codec::video::Reorder;
707
708		let fixed = Some((2, 50));
709		assert_eq!(
710			sps_reorder(fixtures::SPS_IPB),
711			Some(Reorder {
712				depth: 1,
713				period: fixed
714			})
715		);
716		assert_eq!(
717			sps_reorder(fixtures::SPS_PYRAMID),
718			Some(Reorder {
719				depth: 2,
720				period: fixed
721			})
722		);
723		assert_eq!(
724			sps_reorder(fixtures::SPS_IPB_VARIABLE),
725			Some(Reorder { depth: 1, period: None })
726		);
727		// The field-capable SPS still declares its depth and tick; only the frame rate needs
728		// `pic_struct` to read it.
729		assert_eq!(
730			sps_reorder(fixtures::SPS_FIELD),
731			Some(Reorder {
732				depth: 0,
733				period: Some((2, 50))
734			})
735		);
736		// Baseline with no VUI declares nothing.
737		assert_eq!(sps_reorder(&[0x67, 0x42, 0xc0, 0x1f, 0xde]), None);
738		// A VUI cut short is no declaration rather than a wrong one.
739		assert_eq!(sps_reorder(&fixtures::SPS_IPB[..fixtures::SPS_IPB.len() - 3]), None);
740	}
741
742	#[test]
743	fn sps_framerate_needs_a_fixed_rate() {
744		assert_eq!(sps_framerate(fixtures::SPS_IPB), Some(25.0));
745		// The same 50 Hz tick without `fixed_frame_rate_flag` is only a ceiling.
746		assert_eq!(sps_framerate(fixtures::SPS_IPB_VARIABLE), None);
747		// A field-capable SPS makes the tick a field or a frame depending on `pic_struct`.
748		assert_eq!(sps_framerate(fixtures::SPS_FIELD), None);
749		assert_eq!(sps_framerate(&[0x67, 0x42, 0xc0, 0x1f, 0xde]), None);
750	}
751
752	fn annexb_frame(nals: &[&[u8]]) -> Bytes {
753		let mut buf = BytesMut::new();
754		for nal in nals {
755			buf.extend_from_slice(SC4);
756			buf.extend_from_slice(nal);
757		}
758		buf.freeze()
759	}
760
761	/// avc1: a length-prefixed access unit with an IDR slice wraps as one keyframe;
762	/// the payload is passed through verbatim.
763	#[test]
764	fn avc1_frame_keyframe() {
765		let idr: &[u8] = &[0x65, 0x88, 0x84, 0x21];
766		let mut au = BytesMut::new();
767		au.extend_from_slice(&(idr.len() as u32).to_be_bytes());
768		au.extend_from_slice(idr);
769
770		let frame = avc1_frame(&au, 4, moq_net::Timestamp::from_micros(0).unwrap()).unwrap();
771		assert!(frame.keyframe);
772		assert_eq!(frame.payload[4..], *idr);
773	}
774
775	/// avc1: a length-prefixed access unit with a non-IDR slice is a delta frame.
776	#[test]
777	fn avc1_frame_delta() {
778		let pslice: &[u8] = &[0x61, 0xe0, 0x12, 0x34];
779		let mut au = BytesMut::new();
780		au.extend_from_slice(&(pslice.len() as u32).to_be_bytes());
781		au.extend_from_slice(pslice);
782
783		let frame = avc1_frame(&au, 4, moq_net::Timestamp::from_micros(0).unwrap()).unwrap();
784		assert!(!frame.keyframe);
785	}
786
787	#[test]
788	fn avc3_strips_sps_pps_and_builds_avcc() {
789		let sps = &[0x67, 0x42, 0xc0, 0x1f, 0xde][..];
790		let pps = &[0x68, 0xce, 0x3c, 0x80][..];
791		let idr = &[0x65, 0x88, 0x84, 0x21][..];
792
793		let mut tx = Avc1::new();
794		assert!(tx.avcc().is_none());
795
796		let frame = annexb_frame(&[sps, pps, idr]);
797		let out = tx.transform(frame).expect("transform").expect("expected output");
798
799		let avcc = tx.avcc().expect("avcC available").clone();
800		assert_eq!(avcc[0], 1);
801		assert_eq!(avcc[1], sps[1]);
802		assert_eq!(avcc[3], sps[3]);
803
804		let mut expected = BytesMut::new();
805		expected.extend_from_slice(&(idr.len() as u32).to_be_bytes());
806		expected.extend_from_slice(idr);
807		assert_eq!(out.as_ref(), expected.as_ref());
808	}
809
810	#[test]
811	fn avcc_params_roundtrips_build_avcc() {
812		let sps = Bytes::from_static(&[0x67, 0x42, 0xc0, 0x1f, 0xde]);
813		let pps = Bytes::from_static(&[0x68, 0xce, 0x3c, 0x80]);
814
815		let avcc = build_avcc(std::slice::from_ref(&sps), std::slice::from_ref(&pps)).unwrap();
816		let (length_size, params) = avcc_params(&avcc).unwrap();
817
818		assert_eq!(length_size, 4);
819		assert_eq!(params.len(), 2);
820		assert_eq!(params[0], sps);
821		assert_eq!(params[1], pps);
822	}
823
824	#[test]
825	fn build_avcc_carries_multiple_pps() {
826		// A source with one SPS and two PPS (ids 0 and 1): the avcC must keep both,
827		// in order, so slices referencing either id stay decodable.
828		let sps = Bytes::from_static(&[0x67, 0x42, 0xc0, 0x1f, 0xde]);
829		let pps0 = Bytes::from_static(&[0x68, 0xce, 0x3c, 0x80]);
830		let pps1 = Bytes::from_static(&[0x68, 0xce, 0x3c, 0x81]);
831
832		let avcc = build_avcc(std::slice::from_ref(&sps), &[pps0.clone(), pps1.clone()]).unwrap();
833		// numOfSequenceParameterSets is the low 5 bits of byte 5.
834		assert_eq!(avcc[5] & 0x1f, 1);
835
836		let (_, params) = avcc_params(&avcc).unwrap();
837		assert_eq!(params, vec![sps, pps0, pps1]);
838	}
839
840	#[test]
841	fn avc3_keyframe_with_two_pps_keeps_both() {
842		// One keyframe carrying both PPS: the synthesized avcC keeps both, in order.
843		let sps = &[0x67, 0x42, 0xc0, 0x1f, 0xde][..];
844		let pps0 = &[0x68, 0xce, 0x3c, 0x80][..];
845		let pps1 = &[0x68, 0xce, 0x3c, 0x81][..];
846		let idr = &[0x65, 0x88][..];
847
848		let mut tx = Avc1::new();
849		tx.transform(annexb_frame(&[sps, pps0, pps1, idr])).unwrap();
850
851		let avcc = tx.avcc().expect("avcC available");
852		let (_, params) = avcc_params(avcc).unwrap();
853		assert_eq!(
854			params.iter().map(|p| p.as_ref()).collect::<Vec<_>>(),
855			vec![sps, pps0, pps1]
856		);
857	}
858
859	#[test]
860	fn avc3_reinit_drops_superseded_pps() {
861		// A later keyframe presents a different PPS set: the avcC adopts the new set
862		// and drops the old one rather than accumulating both forever.
863		let sps = &[0x67, 0x42, 0xc0, 0x1f, 0xde][..];
864		let pps0 = &[0x68, 0xce, 0x3c, 0x80][..];
865		let pps1 = &[0x68, 0xce, 0x3c, 0x81][..];
866		let idr = &[0x65, 0x88][..];
867
868		let mut tx = Avc1::new();
869		tx.transform(annexb_frame(&[sps, pps0, idr])).unwrap();
870		tx.transform(annexb_frame(&[sps, pps1, idr])).unwrap();
871
872		let avcc = tx.avcc().expect("avcC available");
873		let (_, params) = avcc_params(avcc).unwrap();
874		assert_eq!(
875			params.iter().map(|p| p.as_ref()).collect::<Vec<_>>(),
876			vec![sps, pps1],
877			"reinit must drop the superseded PPS"
878		);
879	}
880
881	#[test]
882	fn avc3_parameter_only_frame_returns_none() {
883		let sps = &[0x67, 0x42, 0xc0, 0x1f, 0xde][..];
884		let pps = &[0x68, 0xce, 0x3c, 0x80][..];
885
886		let mut tx = Avc1::new();
887		let frame = annexb_frame(&[sps, pps]);
888		assert!(tx.transform(frame).unwrap().is_none());
889		assert!(tx.avcc().is_some());
890	}
891
892	#[test]
893	fn avc3_subsequent_frame_uses_cached_avcc() {
894		let sps = &[0x67, 0x42, 0xc0, 0x1f, 0xde][..];
895		let pps = &[0x68, 0xce, 0x3c, 0x80][..];
896		let idr = &[0x65, 0x88][..];
897		let p = &[0x61, 0xe0, 0x12][..];
898
899		let mut tx = Avc1::new();
900		tx.transform(annexb_frame(&[sps, pps, idr])).unwrap();
901		let avcc_v1 = tx.avcc().unwrap().clone();
902
903		let out = tx.transform(annexb_frame(&[p])).unwrap().unwrap();
904		assert_eq!(tx.avcc().unwrap(), &avcc_v1);
905		let mut expected = BytesMut::new();
906		expected.extend_from_slice(&(p.len() as u32).to_be_bytes());
907		expected.extend_from_slice(p);
908		assert_eq!(out.as_ref(), expected.as_ref());
909	}
910
911	#[test]
912	fn avc3_export_e2e_payload_shape() {
913		// Mirror the byte shapes used by the export integration test so any
914		// divergence surfaces here in isolation.
915		let sps = &[0x67u8, 0x42, 0xc0, 0x1f, 0xde, 0xad, 0xbe, 0xef][..];
916		let pps = &[0x68u8, 0xce, 0x3c, 0x80][..];
917		let idr = &[0x65u8, 0x88, 0x84, 0x21, 0x00, 0x11, 0x22, 0x33][..];
918		let pslice = &[0x61u8, 0xe0, 0x12, 0x34][..];
919
920		let mut tx = Avc1::new();
921		let key = annexb_frame(&[sps, pps, idr]);
922		let key_out = tx.transform(key).expect("transform key").expect("output");
923		assert!(tx.avcc().is_some());
924
925		assert_eq!(key_out.len(), 4 + idr.len());
926		assert_eq!(&key_out[4..], idr);
927
928		let p = annexb_frame(&[pslice]);
929		let p_out = tx.transform(p).expect("transform p").expect("output");
930		assert_eq!(p_out.len(), 4 + pslice.len());
931		assert_eq!(&p_out[4..], pslice);
932	}
933}