Skip to content

Commit accb0a9

Browse files
kixelatedclaude
andcommitted
feat(moq-mux): add Avcc/Hvcc param-set parsers and annexb prefix helpers
Expose the out-of-band parameter sets a length-prefixed (avc1/hvc1) stream needs to re-inject at each keyframe when converting back to Annex-B: - `h264::Avcc` gains `sps`/`pps` fields (and `#[non_exhaustive]`); `Avcc::parse` now collects the NAL lists, deriving resolution from the first SPS. - `h265::Hvcc` + `Hvcc::parse`: the HEVC analogue, sorting VPS/SPS/PPS by type. - `annexb::from_length_prefixed` (with an optional keyframe prefix) and `annexb::build_prefix`. The internal `avcc_params`/`hvcc_params` flatteners now delegate to the typed parsers, so there is a single byte-parser per codec. This also fixes `hvcc_params` to keep only VPS/SPS/PPS as its doc already claimed (it previously flattened every NAL array). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4f2ce12 commit accb0a9

3 files changed

Lines changed: 161 additions & 51 deletions

File tree

rs/moq-mux/src/codec/annexb.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::{self};
1+
use anyhow::Context;
22
use bytes::{Buf, Bytes, BytesMut};
33

44
pub const START_CODE: Bytes = Bytes::from_static(&[0, 0, 0, 1]);
@@ -148,6 +148,55 @@ pub fn find_start_code(b: &[u8]) -> Option<(usize, usize)> {
148148
}
149149
}
150150

151+
/// Convert a length-prefixed NALU payload (avc1 / hvc1 wire shape) to Annex-B,
152+
/// optionally prepending `prefix` bytes (typically VPS/SPS/PPS NAL units already
153+
/// in Annex-B form, for keyframe parameter-set injection).
154+
pub fn from_length_prefixed(payload: &[u8], length_size: usize, prefix: Option<&[u8]>) -> anyhow::Result<Bytes> {
155+
anyhow::ensure!(
156+
(1..=4).contains(&length_size),
157+
"invalid avc1/hvc1 length size {length_size}"
158+
);
159+
160+
let mut out = BytesMut::with_capacity(payload.len() + prefix.map(|p| p.len()).unwrap_or(0) + 16);
161+
if let Some(p) = prefix {
162+
out.extend_from_slice(p);
163+
}
164+
165+
let mut pos = 0;
166+
while pos < payload.len() {
167+
let after_prefix = pos
168+
.checked_add(length_size)
169+
.context("truncated length-prefixed NAL unit")?;
170+
anyhow::ensure!(payload.len() >= after_prefix, "truncated length-prefixed NAL unit");
171+
let mut len = 0usize;
172+
for byte in &payload[pos..after_prefix] {
173+
len = (len << 8) | (*byte as usize);
174+
}
175+
let after_nal = after_prefix
176+
.checked_add(len)
177+
.context("truncated length-prefixed NAL unit")?;
178+
anyhow::ensure!(payload.len() >= after_nal, "truncated length-prefixed NAL unit");
179+
out.extend_from_slice(&START_CODE);
180+
out.extend_from_slice(&payload[after_prefix..after_nal]);
181+
pos = after_nal;
182+
}
183+
184+
Ok(out.freeze())
185+
}
186+
187+
/// Concatenate `start_code | nal` for every NAL in `nals` and freeze the result.
188+
/// Used to build a keyframe parameter-set prefix for an Annex-B elementary stream.
189+
pub fn build_prefix<'a, I: IntoIterator<Item = &'a Bytes>>(nals: I) -> Bytes {
190+
let nals: Vec<&Bytes> = nals.into_iter().collect();
191+
let total: usize = nals.iter().map(|n| n.len() + START_CODE.len()).sum();
192+
let mut out = BytesMut::with_capacity(total);
193+
for nal in nals {
194+
out.extend_from_slice(&START_CODE);
195+
out.extend_from_slice(nal);
196+
}
197+
out.freeze()
198+
}
199+
151200
#[cfg(test)]
152201
mod tests {
153202
use super::*;

rs/moq-mux/src/codec/h264/mod.rs

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,17 @@ impl Sps {
5353
/// avcC bytes are still what gets stored as the catalog `description`; this
5454
/// struct is for the field extraction.
5555
#[derive(Debug, Clone)]
56+
#[non_exhaustive]
5657
pub struct Avcc {
5758
pub profile: u8,
5859
pub constraints: u8,
5960
pub level: u8,
6061
/// NALU length size in bytes (typically 4).
6162
pub length_size: usize,
63+
/// SPS NAL units carried out-of-band in the record.
64+
pub sps: Vec<Bytes>,
65+
/// PPS NAL units carried out-of-band in the record.
66+
pub pps: Vec<Bytes>,
6267
/// Resolution from the embedded SPS, if one was present and parseable.
6368
pub coded_width: Option<u32>,
6469
pub coded_height: Option<u32>,
@@ -67,33 +72,40 @@ pub struct Avcc {
6772
impl Avcc {
6873
/// Parse an AVCDecoderConfigurationRecord buffer.
6974
pub fn parse(avcc: &[u8]) -> anyhow::Result<Self> {
70-
anyhow::ensure!(avcc.len() >= 6, "AVCDecoderConfigurationRecord too short");
75+
anyhow::ensure!(avcc.len() >= 7, "AVCDecoderConfigurationRecord too short");
7176

7277
let profile = avcc[1];
7378
let constraints = avcc[2];
7479
let level = avcc[3];
7580
let length_size = (avcc[4] & 0x03) as usize + 1;
76-
let num_sps = avcc[5] & 0x1f;
81+
let num_sps = (avcc[5] & 0x1f) as usize;
7782

83+
let mut sps = Vec::with_capacity(num_sps);
84+
let mut pos = read_param_set_array(avcc, 6, num_sps, &mut sps)?;
85+
86+
anyhow::ensure!(avcc.len() > pos, "AVCDecoderConfigurationRecord truncated");
87+
let num_pps = avcc[pos] as usize;
88+
pos += 1;
89+
let mut pps = Vec::with_capacity(num_pps);
90+
read_param_set_array(avcc, pos, num_pps, &mut pps)?;
91+
92+
// Resolution from the first parseable SPS.
7893
let (mut coded_width, mut coded_height) = (None, None);
79-
if num_sps > 0 && avcc.len() >= 8 {
80-
let sps_len = u16::from_be_bytes([avcc[6], avcc[7]]) as usize;
81-
let sps_start = 8;
82-
let sps_end = sps_start + sps_len;
83-
if sps_end <= avcc.len()
84-
&& sps_len > 1
85-
&& let Ok(sps) = Sps::parse(&avcc[sps_start..sps_end])
86-
{
87-
coded_width = Some(sps.coded_width);
88-
coded_height = Some(sps.coded_height);
89-
}
94+
if let Some(first) = sps.first()
95+
&& first.len() > 1
96+
&& let Ok(parsed) = Sps::parse(first)
97+
{
98+
coded_width = Some(parsed.coded_width);
99+
coded_height = Some(parsed.coded_height);
90100
}
91101

92102
Ok(Self {
93103
profile,
94104
constraints,
95105
level,
96106
length_size,
107+
sps,
108+
pps,
97109
coded_width,
98110
coded_height,
99111
})
@@ -168,20 +180,13 @@ pub(crate) fn build_avcc(sps_nals: &[Bytes], pps_nals: &[Bytes]) -> anyhow::Resu
168180
/// Extract the parameter-set NALs (SPS then PPS) and the NALU length size from
169181
/// an AVCDecoderConfigurationRecord. The inverse of [`build_avcc`]; used to
170182
/// re-emit out-of-band avc1 parameter sets as inline Annex-B (e.g. for MPEG-TS).
183+
/// The SPS+PPS NALs (in that order) and NALU length size, flattened for callers
184+
/// that re-emit every parameter set as one Annex-B prefix (e.g. MPEG-TS export).
171185
pub(crate) fn avcc_params(avcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
172-
anyhow::ensure!(avcc.len() >= 6, "AVCDecoderConfigurationRecord too short");
173-
let length_size = (avcc[4] & 0x03) as usize + 1;
174-
175-
let mut params = Vec::new();
176-
let num_sps = avcc[5] & 0x1f;
177-
let mut pos = read_param_set_array(avcc, 6, num_sps as usize, &mut params)?;
178-
179-
anyhow::ensure!(avcc.len() > pos, "avcC missing PPS count");
180-
let num_pps = avcc[pos];
181-
pos += 1;
182-
read_param_set_array(avcc, pos, num_pps as usize, &mut params)?;
183-
184-
Ok((length_size, params))
186+
let avcc = Avcc::parse(avcc)?;
187+
let mut params = avcc.sps;
188+
params.extend(avcc.pps);
189+
Ok((avcc.length_size, params))
185190
}
186191

187192
/// Read `count` u16-length-prefixed NALs starting at `pos`, appending each to
@@ -389,6 +394,20 @@ mod tests {
389394
assert_eq!(params[1], pps);
390395
}
391396

397+
#[test]
398+
fn avcc_parse_separates_sps_and_pps() {
399+
let sps = Bytes::from_static(&[0x67, 0x42, 0xc0, 0x1f, 0xde]);
400+
let pps0 = Bytes::from_static(&[0x68, 0xce, 0x3c, 0x80]);
401+
let pps1 = Bytes::from_static(&[0x68, 0xce, 0x3c, 0x81]);
402+
403+
let avcc = build_avcc(std::slice::from_ref(&sps), &[pps0.clone(), pps1.clone()]).unwrap();
404+
let parsed = Avcc::parse(&avcc).unwrap();
405+
406+
assert_eq!(parsed.length_size, 4);
407+
assert_eq!(parsed.sps, vec![sps]);
408+
assert_eq!(parsed.pps, vec![pps0, pps1]);
409+
}
410+
392411
#[test]
393412
fn build_avcc_carries_multiple_pps() {
394413
// A source with one SPS and two PPS (ids 0 and 1): the avcC must keep both,

rs/moq-mux/src/codec/h265/mod.rs

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -282,34 +282,69 @@ pub(crate) fn build_hvcc(vps_nals: &[Bytes], sps_nals: &[Bytes], pps_nals: &[Byt
282282
Ok(out.freeze())
283283
}
284284

285-
/// Extract the parameter-set NALs (VPS, SPS, PPS in array order) and the NALU
286-
/// length size from an HEVCDecoderConfigurationRecord. The inverse of
287-
/// [`build_hvcc`]; used to re-emit out-of-band hvc1 parameter sets as inline
288-
/// Annex-B (e.g. for MPEG-TS).
285+
/// The VPS+SPS+PPS NALs (in that order) and NALU length size, flattened for
286+
/// callers that re-emit every parameter set as one Annex-B prefix (e.g. MPEG-TS
287+
/// export).
289288
pub(crate) fn hvcc_params(hvcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
290-
anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
291-
let length_size = (hvcc[21] & 0x03) as usize + 1;
292-
let num_arrays = hvcc[22];
289+
let hvcc = Hvcc::parse(hvcc)?;
290+
let mut params = hvcc.vps;
291+
params.extend(hvcc.sps);
292+
params.extend(hvcc.pps);
293+
Ok((hvcc.length_size, params))
294+
}
293295

294-
let mut params = Vec::new();
295-
let mut pos = 23;
296-
for _ in 0..num_arrays {
297-
// Skip the array_completeness | NAL_unit_type byte.
298-
anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
299-
pos += 1;
300-
let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]);
301-
pos += 2;
302-
for _ in 0..num_nalus {
303-
anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
304-
let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
305-
pos += 2;
306-
anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
307-
params.push(Bytes::copy_from_slice(&hvcc[pos..pos + len]));
308-
pos += len;
296+
/// The parameter sets carried out-of-band in an HEVCDecoderConfigurationRecord,
297+
/// split by NAL type.
298+
#[derive(Debug, Clone)]
299+
#[non_exhaustive]
300+
pub struct Hvcc {
301+
/// NALU length size in bytes (typically 4).
302+
pub length_size: usize,
303+
pub vps: Vec<Bytes>,
304+
pub sps: Vec<Bytes>,
305+
pub pps: Vec<Bytes>,
306+
}
307+
308+
impl Hvcc {
309+
/// Parse an HEVCDecoderConfigurationRecord, sorting the VPS/SPS/PPS NAL units
310+
/// by type. The HEVC analogue of [`super::h264::Avcc::parse`]: used to recover
311+
/// the Annex-B parameter sets a length-prefixed (hvc1) stream needs at each
312+
/// keyframe for in-band injection (VPS→SPS→PPS order).
313+
pub fn parse(hvcc: &[u8]) -> anyhow::Result<Self> {
314+
anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
315+
let length_size = (hvcc[21] & 0x03) as usize + 1;
316+
let num_arrays = hvcc[22];
317+
318+
let mut out = Self {
319+
length_size,
320+
vps: Vec::new(),
321+
sps: Vec::new(),
322+
pps: Vec::new(),
323+
};
324+
let mut pos = 23;
325+
for _ in 0..num_arrays {
326+
anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
327+
let nal_type = hvcc[pos] & 0x3f;
328+
let num_nalus = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]);
329+
pos += 3;
330+
for _ in 0..num_nalus {
331+
anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
332+
let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
333+
pos += 2;
334+
anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
335+
let nal = Bytes::copy_from_slice(&hvcc[pos..pos + len]);
336+
pos += len;
337+
match NALUnitType::from(nal_type) {
338+
NALUnitType::VpsNut => out.vps.push(nal),
339+
NALUnitType::SpsNut => out.sps.push(nal),
340+
NALUnitType::PpsNut => out.pps.push(nal),
341+
_ => {}
342+
}
343+
}
309344
}
310-
}
311345

312-
Ok((length_size, params))
346+
Ok(out)
347+
}
313348
}
314349

315350
/// Pack the constraint flags from ITU H.265 V10 §7.3.3 Profile, tier and level syntax.
@@ -356,5 +391,12 @@ mod tests {
356391
assert_eq!(params[0].as_ref(), vps);
357392
assert_eq!(params[1].as_ref(), sps);
358393
assert_eq!(params[2].as_ref(), pps);
394+
395+
// The typed parser keys the same NALs by type.
396+
let parsed = Hvcc::parse(&hvcc).unwrap();
397+
assert_eq!(parsed.length_size, 4);
398+
assert_eq!(parsed.vps, vec![Bytes::copy_from_slice(vps)]);
399+
assert_eq!(parsed.sps, vec![Bytes::copy_from_slice(sps)]);
400+
assert_eq!(parsed.pps, vec![Bytes::copy_from_slice(pps)]);
359401
}
360402
}

0 commit comments

Comments
 (0)