use crate::TrackId;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::marker::PhantomData;
use broadcast_common::{Parse, Unpackage};
use crate::annexb::{NAL_LENGTH_SIZE_MINUS_ONE, normalise_nal_length_size};
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::error::{Error, Result};
use crate::hevc_config::{HEVCConfigurationBox, HEVCDecoderConfigurationRecord};
use crate::ir::InputDegradation;
use crate::media::{Media, Track};
use crate::opus::OpusSpecificBox;
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
use crate::rtp_sdp::aac_config_from_asc_bytes;
use crate::vp9::Vp9ConfigurationBox;
const EBML_HEADER: u32 = 0x1A45_DFA3;
const SEGMENT: u32 = 0x1853_8067;
const INFO: u32 = 0x1549_A966;
const TIMESTAMP_SCALE: u32 = 0x2A_D7_B1;
const TRACKS: u32 = 0x1654_AE6B;
const TRACK_ENTRY: u32 = 0xAE;
const TRACK_NUMBER: u32 = 0xD7;
const TRACK_TYPE: u32 = 0x83;
const CODEC_ID: u32 = 0x86;
const CODEC_PRIVATE: u32 = 0x63A2;
const DEFAULT_DURATION: u32 = 0x23_E3_83;
const VIDEO: u32 = 0xE0;
const PIXEL_WIDTH: u32 = 0xB0;
const PIXEL_HEIGHT: u32 = 0xBA;
const AUDIO: u32 = 0xE1;
const SAMPLING_FREQUENCY: u32 = 0xB5;
const CHANNELS: u32 = 0x9F;
const CLUSTER: u32 = 0x1F43_B675;
const CLUSTER_TIMESTAMP: u32 = 0xE7;
const SIMPLE_BLOCK: u32 = 0xA3;
const BLOCK_GROUP: u32 = 0xA0;
const BLOCK: u32 = 0xA1;
const SEEK_HEAD: u32 = 0x114D_9B74;
const CUES: u32 = 0x1C53_BB6B;
const TAGS: u32 = 0x1254_C367;
const CHAPTERS: u32 = 0x1043_A770;
const ATTACHMENTS: u32 = 0x1941_A469;
const SEGMENT_LEVEL_IDS: &[u32] = &[
INFO,
TRACKS,
CLUSTER,
SEEK_HEAD,
CUES,
TAGS,
CHAPTERS,
ATTACHMENTS,
];
const REFERENCE_BLOCK: u32 = 0xFB;
const CODEC_V_VP9: &[u8] = b"V_VP9";
const CODEC_V_VP8: &[u8] = b"V_VP8";
const CODEC_V_AVC: &[u8] = b"V_MPEG4/ISO/AVC";
const CODEC_V_HEVC: &[u8] = b"V_MPEGH/ISO/HEVC";
const CODEC_A_OPUS: &[u8] = b"A_OPUS";
const CODEC_A_VORBIS: &[u8] = b"A_VORBIS";
const CODEC_A_AAC: &[u8] = b"A_AAC";
const TRACK_TYPE_VIDEO: u64 = 1;
const TRACK_TYPE_AUDIO: u64 = 2;
const BLOCK_FLAG_KEYFRAME: u8 = 0x80;
const BLOCK_FLAG_LACING_MASK: u8 = 0x06;
const BLOCK_FLAG_LACING_XIPH: u8 = 0x02;
const BLOCK_FLAG_LACING_EBML: u8 = 0x06;
const BLOCK_FLAG_LACING_FIXED: u8 = 0x04;
const XIPH_LACE_CONTINUATION: u8 = 0xFF;
const MAX_LACED_FRAMES: usize = 256;
const fn ebml_lace_delta_bias(vint_len: usize) -> i64 {
(1i64 << (7 * vint_len - 1)) - 1
}
const DEFAULT_TIMESTAMP_SCALE_NS: u64 = 1_000_000;
pub const IR_TIMESCALE: u32 = 1000;
const NS_PER_SECOND: u64 = 1_000_000_000;
const OPUS_HEAD_MAGIC: &[u8; 8] = b"OpusHead";
const OPUS_HEAD_MIN_LEN: usize = 19;
const OPUS_OUTPUT_SAMPLE_RATE: u32 = 48_000;
const AUDIO_SAMPLE_SIZE: u16 = 16;
const VP8_FRAME_TAG_LEN: usize = 3;
const VP8_START_CODE: [u8; 3] = [0x9D, 0x01, 0x2A];
const VP8_KEYFRAME_TAG_BIT: u8 = 0x01;
const VP8_DIMENSION_MASK: u16 = 0x3FFF;
const VP8_KEYFRAME_HEADER_LEN: usize = VP8_FRAME_TAG_LEN + VP8_START_CODE.len() + 4;
const VORBIS_LACE_COUNT: u8 = 2;
const VORBIS_ID_HEADER_TYPE: u8 = 0x01;
const VORBIS_SIGNATURE: &[u8; 6] = b"vorbis";
const VORBIS_ID_CHANNELS_OFFSET: usize = 1 + 6 + 4;
const VORBIS_ID_SAMPLE_RATE_OFFSET: usize = VORBIS_ID_CHANNELS_OFFSET + 1;
const VORBIS_ID_MIN_LEN: usize = VORBIS_ID_SAMPLE_RATE_OFFSET + 4;
const VPCC_VERSION: u8 = 1;
const VP9_PROFILE_0: u8 = 0;
const VP9_LEVEL_UNSPECIFIED: u8 = 0;
const VP9_BIT_DEPTH_8: u8 = 8;
const VP9_CHROMA_420: u8 = 1;
const CICP_UNSPECIFIED: u8 = 2;
#[derive(Debug)]
struct RawBlock {
track_number: u64,
pts_ticks: i64,
is_sync: bool,
frames: Vec<Vec<u8>>,
declared_frames: usize,
}
#[derive(Default)]
struct TrackInfo {
track_number: u64,
track_type: u64,
codec_id: Vec<u8>,
codec_private: Vec<u8>,
default_duration_ns: u64,
pixel_width: u16,
pixel_height: u16,
channels: u16,
sampling_frequency: u32,
}
#[derive(Debug, Default, Clone)]
pub struct WebmDemux<'a> {
_marker: PhantomData<&'a [u8]>,
}
impl<'a> WebmDemux<'a> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
pub fn demux(&mut self, input: &'a [u8]) -> Result<Media> {
let mut r = EbmlReader::new(input);
let mut timestamp_scale_ns = DEFAULT_TIMESTAMP_SCALE_NS;
let mut tracks: Vec<TrackInfo> = Vec::new();
let mut blocks: Vec<RawBlock> = Vec::new();
let mut degraded: Vec<InputDegradation> = Vec::new();
while let Some((id, body)) = r.next_element()? {
match id {
EBML_HEADER => {}
SEGMENT => {
Self::walk_segment(
input,
body,
&mut timestamp_scale_ns,
&mut tracks,
&mut blocks,
&mut degraded,
)?;
}
_ => {}
}
}
let declared_frames: usize = blocks.iter().map(|b| b.declared_frames).sum();
if declared_frames > input.len() {
return Err(Error::InvalidInput(
"webm: declared frame count exceeds the file's byte length",
));
}
let mut media = build_media(timestamp_scale_ns, tracks, blocks)?;
media.degraded = degraded;
Ok(media)
}
fn walk_segment(
root: &[u8],
body: &[u8],
timestamp_scale_ns: &mut u64,
tracks: &mut Vec<TrackInfo>,
blocks: &mut Vec<RawBlock>,
degraded: &mut Vec<InputDegradation>,
) -> Result<()> {
let mut r = EbmlReader::new(body);
loop {
let at = r.pos;
let (id, child) = match r.next_element_bounded(SEGMENT_LEVEL_IDS) {
Ok(Some(el)) => el,
Ok(None) => break,
Err(_) => {
degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, &body[at..]),
});
match find_sibling_boundary(body, at + 1, &[CLUSTER]) {
Some(p) => {
r.pos = p;
continue;
}
None => break,
}
}
};
match id {
INFO if Self::walk_info(child, timestamp_scale_ns).is_err() => {
degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, child),
});
}
TRACKS => Self::walk_tracks(root, child, tracks, degraded),
CLUSTER => Self::walk_cluster(root, child, *timestamp_scale_ns, blocks, degraded),
_ => {}
}
}
Ok(())
}
fn walk_info(body: &[u8], timestamp_scale_ns: &mut u64) -> Result<()> {
let mut r = EbmlReader::new(body);
while let Some((id, child)) = r.next_element()? {
if id == TIMESTAMP_SCALE {
*timestamp_scale_ns = read_uint(child);
}
}
Ok(())
}
fn walk_tracks(
root: &[u8],
body: &[u8],
tracks: &mut Vec<TrackInfo>,
degraded: &mut Vec<InputDegradation>,
) {
let mut r = EbmlReader::new(body);
loop {
let (id, child) = match r.next_element() {
Ok(Some(el)) => el,
Ok(None) => break,
Err(_) => {
degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, &body[r.pos..]),
});
break;
}
};
if id == TRACK_ENTRY {
match Self::parse_track_entry(child) {
Ok(t) => tracks.push(t),
Err(_) => degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, child),
}),
}
}
}
}
fn parse_track_entry(body: &[u8]) -> Result<TrackInfo> {
let mut info = TrackInfo::default();
let mut r = EbmlReader::new(body);
while let Some((id, child)) = r.next_element()? {
match id {
TRACK_NUMBER => info.track_number = read_uint(child),
TRACK_TYPE => info.track_type = read_uint(child),
CODEC_ID => info.codec_id = child.to_vec(),
CODEC_PRIVATE => info.codec_private = child.to_vec(),
DEFAULT_DURATION => info.default_duration_ns = read_uint(child),
VIDEO => Self::parse_video(child, &mut info)?,
AUDIO => Self::parse_audio(child, &mut info)?,
_ => {}
}
}
Ok(info)
}
fn parse_video(body: &[u8], info: &mut TrackInfo) -> Result<()> {
let mut r = EbmlReader::new(body);
while let Some((id, child)) = r.next_element()? {
match id {
PIXEL_WIDTH => info.pixel_width = read_uint(child) as u16,
PIXEL_HEIGHT => info.pixel_height = read_uint(child) as u16,
_ => {}
}
}
Ok(())
}
fn parse_audio(body: &[u8], info: &mut TrackInfo) -> Result<()> {
let mut r = EbmlReader::new(body);
while let Some((id, child)) = r.next_element()? {
match id {
CHANNELS => info.channels = read_uint(child) as u16,
SAMPLING_FREQUENCY => info.sampling_frequency = read_float(child) as u32,
_ => {}
}
}
Ok(())
}
fn walk_cluster(
root: &[u8],
body: &[u8],
timestamp_scale_ns: u64,
blocks: &mut Vec<RawBlock>,
degraded: &mut Vec<InputDegradation>,
) {
let mut cluster_ts: i64 = 0;
let mut r = EbmlReader::new(body);
loop {
let (id, child) = match r.next_element() {
Ok(Some(el)) => el,
Ok(None) => break,
Err(_) => {
degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, &body[r.pos..]),
});
break;
}
};
match id {
CLUSTER_TIMESTAMP => match i64::try_from(read_uint(child)) {
Ok(ts) => cluster_ts = ts,
Err(_) => degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, child),
}),
},
SIMPLE_BLOCK => match parse_block(child, cluster_ts, timestamp_scale_ns, true) {
Ok(b) => blocks.push(b),
Err(_) => degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, child),
}),
},
BLOCK_GROUP => {
let mut block_bytes: Option<&[u8]> = None;
let mut has_reference = false;
let mut g = EbmlReader::new(child);
let mut group_ok = true;
loop {
match g.next_element() {
Ok(Some((gid, gchild))) => match gid {
BLOCK => block_bytes = Some(gchild),
REFERENCE_BLOCK => has_reference = true,
_ => {}
},
Ok(None) => break,
Err(_) => {
group_ok = false;
break;
}
}
}
let parsed = block_bytes
.filter(|_| group_ok)
.map(|b| parse_block(b, cluster_ts, timestamp_scale_ns, false));
match parsed {
Some(Ok(mut rb)) => {
rb.is_sync = !has_reference;
blocks.push(rb);
}
Some(Err(_)) => degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, child),
}),
None if !group_ok => degraded.push(InputDegradation::MalformedUnit {
position: offset_in(root, child),
}),
None => {}
}
}
_ => {}
}
}
}
}
fn offset_in(root: &[u8], part: &[u8]) -> u64 {
(part.as_ptr() as usize)
.checked_sub(root.as_ptr() as usize)
.filter(|o| *o <= root.len())
.unwrap_or(0) as u64
}
impl<'a> Unpackage for WebmDemux<'a> {
type Input = &'a [u8];
type Media = Media;
type Error = Error;
fn unpackage(&mut self, input: &'a [u8]) -> Result<Media> {
self.demux(input)
}
}
fn parse_block(
data: &[u8],
cluster_ts: i64,
timestamp_scale_ns: u64,
is_simple_block: bool,
) -> Result<RawBlock> {
let (track_number, mut off) = read_vint_value(data).ok_or(Error::InvalidInput(
"webm block: truncated track-number VINT",
))?;
if data.len() < off + 3 {
return Err(Error::BufferTooShort {
need: off + 3,
have: data.len(),
what: "webm block header (rel-ts + flags)",
});
}
let rel_ts = i16::from_be_bytes([data[off], data[off + 1]]) as i64;
let flags = data[off + 2];
off += 3;
let is_sync = if is_simple_block {
flags & BLOCK_FLAG_KEYFRAME != 0
} else {
false
};
let declared_frames = declared_lace_count(&data[off..], flags)?;
let frames = unlace(&data[off..], flags)?;
let raw_ticks = cluster_ts.checked_add(rel_ts).ok_or(Error::InvalidInput(
"webm block timestamp: cluster_ts + rel_ts overflowed i64",
))?;
let ns = raw_ticks.saturating_mul(timestamp_scale_ns as i64);
let ns_per_ir_tick = (NS_PER_SECOND / IR_TIMESCALE as u64) as i64;
let pts_ticks = ns / ns_per_ir_tick;
Ok(RawBlock {
track_number,
pts_ticks,
is_sync,
frames,
declared_frames,
})
}
fn declared_lace_count(payload: &[u8], flags: u8) -> Result<usize> {
if flags & BLOCK_FLAG_LACING_MASK == 0 {
return Ok(1);
}
let count = payload.first().ok_or(Error::BufferTooShort {
need: 1,
have: 0,
what: "webm laced block frame count",
})?;
Ok(usize::from(*count) + 1)
}
fn unlace(payload: &[u8], flags: u8) -> Result<Vec<Vec<u8>>> {
let mode = flags & BLOCK_FLAG_LACING_MASK;
if mode == 0 {
return Ok(alloc::vec![payload.to_vec()]);
}
let Some((&count_minus_one, rest)) = payload.split_first() else {
return Err(Error::BufferTooShort {
need: 1,
have: 0,
what: "webm laced block frame count",
});
};
let frame_count = usize::from(count_minus_one) + 1;
if frame_count > MAX_LACED_FRAMES {
return Err(Error::InvalidInput(
"webm block: laced frame count out of range",
));
}
let mut sizes: Vec<usize> = Vec::with_capacity(frame_count);
let mut body = rest;
match mode {
BLOCK_FLAG_LACING_XIPH => {
for _ in 0..frame_count - 1 {
let mut size = 0usize;
loop {
let Some((&byte, tail)) = body.split_first() else {
return Err(Error::BufferTooShort {
need: 1,
have: 0,
what: "webm Xiph lace size",
});
};
body = tail;
size = size
.checked_add(usize::from(byte))
.ok_or(Error::InvalidInput("webm Xiph lace size overflowed"))?;
if byte != XIPH_LACE_CONTINUATION {
break;
}
}
sizes.push(size);
}
}
BLOCK_FLAG_LACING_EBML => {
let (first, used) = read_vint_generic(body)
.ok_or(Error::InvalidInput("webm block: truncated EBML lace size"))?;
body = &body[used..];
let mut prev = usize::try_from(first)
.map_err(|_| Error::InvalidInput("webm EBML lace size out of range"))?;
sizes.push(prev);
for _ in 1..frame_count - 1 {
let (delta, used) = read_signed_vint(body)
.ok_or(Error::InvalidInput("webm block: truncated EBML lace delta"))?;
body = &body[used..];
let prev_i64 = i64::try_from(prev)
.map_err(|_| Error::InvalidInput("webm EBML lace size out of range"))?;
let next = i128::from(prev_i64) + i128::from(delta);
if next < 0 {
return Err(Error::InvalidInput("webm EBML lace size went negative"));
}
prev = usize::try_from(next)
.map_err(|_| Error::InvalidInput("webm EBML lace size out of range"))?;
sizes.push(prev);
}
}
BLOCK_FLAG_LACING_FIXED => {
let total = body.len();
if total % frame_count != 0 {
return Err(Error::InvalidInput(
"webm block: fixed lacing payload is not a multiple of the frame count",
));
}
let each = total / frame_count;
sizes.extend(core::iter::repeat_n(each, frame_count - 1));
}
_ => return Err(Error::InvalidInput("webm block: unrecognised lacing mode")),
}
for size in &sizes {
if *size == 0 {
return Err(Error::InvalidInput(
"webm block: laced frame has zero length",
));
}
}
let mut frames = Vec::with_capacity(frame_count);
for size in &sizes {
if body.len() < *size {
return Err(Error::BufferTooShort {
need: *size,
have: body.len(),
what: "webm laced frame",
});
}
let (frame, tail) = body.split_at(*size);
frames.push(frame.to_vec());
body = tail;
}
frames.push(body.to_vec());
Ok(frames)
}
fn read_vint_generic(buf: &[u8]) -> Option<(u64, usize)> {
let first = *buf.first()?;
let mut mask = 0x80u8;
let mut len = 1usize;
while first & mask == 0 {
mask >>= 1;
len += 1;
if len > 8 {
return None;
}
}
if buf.len() < len {
return None;
}
let mut value = u64::from(first & (mask - 1));
for &byte in &buf[1..len] {
value = (value << 8) | u64::from(byte);
}
Some((value, len))
}
fn read_signed_vint(buf: &[u8]) -> Option<(i64, usize)> {
let first = *buf.first()?;
let mut mask = 0x80u8;
let mut len = 1usize;
while first & mask == 0 {
mask >>= 1;
len += 1;
if len > 8 {
return None;
}
}
let (raw, used) = read_vint_generic(buf)?;
let signed = i64::try_from(raw).ok()? - ebml_lace_delta_bias(len);
Some((signed, used))
}
#[cfg(test)]
thread_local! {
static BLOCKS_VISITED: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
fn build_media(
timestamp_scale_ns: u64,
tracks: Vec<TrackInfo>,
mut blocks: Vec<RawBlock>,
) -> Result<Media> {
let _ = timestamp_scale_ns;
let mut by_track: BTreeMap<u64, Vec<RawBlock>> = BTreeMap::new();
for b in blocks.drain(..) {
#[cfg(test)]
BLOCKS_VISITED.with(|c| c.set(c.get() + 1));
by_track.entry(b.track_number).or_default().push(b);
}
let mut out_tracks: Vec<Track> = Vec::new();
let mut track_id = TrackId::new(1);
for info in &tracks {
let default_dur_ir = ns_to_ir_ticks(info.default_duration_ns);
let mut timeline: Vec<BlockSpan> = Vec::new();
let mut samples: Vec<Sample> = Vec::new();
let mut sync: Vec<bool> = Vec::new();
let mut payloads: Vec<Vec<u8>> = Vec::new();
for mut b in by_track.remove(&info.track_number).unwrap_or_default() {
#[cfg(test)]
BLOCKS_VISITED.with(|c| c.set(c.get() + 1));
timeline.push(BlockSpan {
pts_ticks: b.pts_ticks,
first_frame: payloads.len(),
frame_count: b.frames.len(),
});
sync.extend(core::iter::repeat_n(b.is_sync, b.frames.len()));
payloads.append(&mut b.frames);
}
if payloads.is_empty() {
continue;
}
let nominal = if default_dur_ir > 0 {
default_dur_ir
} else {
derive_nominal_frame_duration(&timeline)
};
let pts = lay_out_block_timestamps(&timeline, nominal);
let first_sync = sync
.iter()
.position(|&s| s)
.map(|i| payloads[i].as_slice())
.unwrap_or(payloads[0].as_slice());
let Some(codec) = codec_config_for(info, first_sync)? else {
continue;
};
let (config, nal_length_size) = codec;
let n = payloads.len();
for i in 0..n {
let delta = if i + 1 < n {
pts[i + 1] - pts[i]
} else if n >= 2 {
pts[i] - pts[i - 1]
} else {
i64::try_from(nominal).unwrap_or(i64::MAX)
};
let duration = u32::try_from(delta.max(0))
.map_err(|_| Error::InvalidInput("webm: sample duration does not fit the IR"))?;
let data = core::mem::take(&mut payloads[i]);
let data = match nal_length_size {
Some(size) => normalise_nal_length_size(&data, size)?,
None => data,
};
samples.push(Sample {
data: data.into(),
dts: Some(pts[i]),
pts: Some(pts[i]),
duration: Some(duration),
flags: crate::ir::SampleFlags::new(sync[i]),
provenance: None,
display: None,
});
}
let anchor = pts.first().map(|&p| p.max(0) as u64).unwrap_or(0);
out_tracks.push(Track::new_at(
TrackSpec::new(track_id, IR_TIMESCALE, config),
samples,
anchor,
));
track_id = track_id.next();
}
Ok(Media::new(out_tracks, IR_TIMESCALE))
}
struct BlockSpan {
pts_ticks: i64,
first_frame: usize,
frame_count: usize,
}
pub fn ns_to_ir_ticks(ns: u64) -> u64 {
let ticks = u128::from(ns) * u128::from(IR_TIMESCALE);
let half = NS_PER_SECOND as u128 / 2;
u64::try_from((ticks + half) / NS_PER_SECOND as u128).unwrap_or(u64::MAX)
}
fn derive_nominal_frame_duration(timeline: &[BlockSpan]) -> u64 {
for pair in timeline.windows(2) {
let (cur, next) = (&pair[0], &pair[1]);
let frames = (next.first_frame - cur.first_frame) as u64;
let gap = next.pts_ticks.saturating_sub(cur.pts_ticks);
if frames > 0 && gap > 0 {
return (gap as u64) / frames;
}
}
0
}
fn lay_out_block_timestamps(timeline: &[BlockSpan], nominal: u64) -> Vec<i64> {
let nominal = if nominal == 0 { 1 } else { nominal };
let total: usize = timeline
.last()
.map(|b| b.first_frame + b.frame_count)
.unwrap_or(0);
let mut out = alloc::vec![0i64; total];
for block in timeline {
for i in 0..block.frame_count {
out[block.first_frame + i] = block
.pts_ticks
.saturating_add((i as i64).saturating_mul(nominal as i64));
}
}
out
}
fn codec_config_for(
info: &TrackInfo,
first_frame: &[u8],
) -> Result<Option<(CodecConfig, Option<usize>)>> {
if info.track_type == TRACK_TYPE_VIDEO && info.codec_id == CODEC_V_VP9 {
Ok(Some((vp9_config(info), None)))
} else if info.track_type == TRACK_TYPE_VIDEO && info.codec_id == CODEC_V_VP8 {
Ok(Some((vp8_config(first_frame)?, None)))
} else if info.track_type == TRACK_TYPE_VIDEO && info.codec_id == CODEC_V_AVC {
Ok(Some(avc_config(info)?))
} else if info.track_type == TRACK_TYPE_VIDEO && info.codec_id == CODEC_V_HEVC {
Ok(Some(hevc_config(info)?))
} else if info.track_type == TRACK_TYPE_AUDIO && info.codec_id == CODEC_A_OPUS {
Ok(Some((opus_config(info)?, None)))
} else if info.track_type == TRACK_TYPE_AUDIO && info.codec_id == CODEC_A_VORBIS {
Ok(Some((vorbis_config(info)?, None)))
} else if info.track_type == TRACK_TYPE_AUDIO && info.codec_id == CODEC_A_AAC {
Ok(Some((
aac_config_from_asc_bytes(info.codec_private.clone())?,
None,
)))
} else {
Ok(None)
}
}
fn avc_config(info: &TrackInfo) -> Result<(CodecConfig, Option<usize>)> {
let mut record = AVCDecoderConfigurationRecord::parse(&info.codec_private)?;
let length_size = usize::from(record.length_size_minus_one) + 1;
record.length_size_minus_one = NAL_LENGTH_SIZE_MINUS_ONE;
Ok((
CodecConfig::Avc {
config: AVCConfigurationBox::new(record),
width: info.pixel_width,
height: info.pixel_height,
},
Some(length_size),
))
}
fn hevc_config(info: &TrackInfo) -> Result<(CodecConfig, Option<usize>)> {
let mut record = HEVCDecoderConfigurationRecord::parse(&info.codec_private)?;
let length_size = usize::from(record.length_size_minus_one) + 1;
record.length_size_minus_one = NAL_LENGTH_SIZE_MINUS_ONE;
Ok((
CodecConfig::Hevc {
config: HEVCConfigurationBox::new(record),
width: info.pixel_width,
height: info.pixel_height,
},
Some(length_size),
))
}
fn vp8_config(first_frame: &[u8]) -> Result<CodecConfig> {
if first_frame.len() < VP8_KEYFRAME_HEADER_LEN {
return Err(Error::BufferTooShort {
need: VP8_KEYFRAME_HEADER_LEN,
have: first_frame.len(),
what: "VP8 key-frame header",
});
}
if first_frame[0] & VP8_KEYFRAME_TAG_BIT != 0 {
return Err(Error::InvalidValue {
field: "VP8 key_frame",
value: (first_frame[0] & VP8_KEYFRAME_TAG_BIT) as u64,
reason: "first VP8 frame is not a key frame (key_frame bit != 0)",
});
}
let start = &first_frame[VP8_FRAME_TAG_LEN..VP8_FRAME_TAG_LEN + VP8_START_CODE.len()];
if start != VP8_START_CODE {
return Err(Error::InvalidValue {
field: "VP8 start code",
value: u32::from_be_bytes([0, start[0], start[1], start[2]]) as u64,
reason: "VP8 key-frame start code is not 0x9D012A",
});
}
let d = VP8_FRAME_TAG_LEN + VP8_START_CODE.len();
let width = u16::from_le_bytes([first_frame[d], first_frame[d + 1]]) & VP8_DIMENSION_MASK;
let height = u16::from_le_bytes([first_frame[d + 2], first_frame[d + 3]]) & VP8_DIMENSION_MASK;
Ok(CodecConfig::Vp8 { width, height })
}
fn vorbis_config(info: &TrackInfo) -> Result<CodecConfig> {
let cp = &info.codec_private;
let id = vorbis_id_header(cp)?;
if id.len() < VORBIS_ID_MIN_LEN {
return Err(Error::BufferTooShort {
need: VORBIS_ID_MIN_LEN,
have: id.len(),
what: "Vorbis identification header",
});
}
if id[0] != VORBIS_ID_HEADER_TYPE {
return Err(Error::InvalidValue {
field: "Vorbis header packet type",
value: id[0] as u64,
reason: "first Vorbis header is not the identification header (type 0x01)",
});
}
if &id[1..1 + VORBIS_SIGNATURE.len()] != VORBIS_SIGNATURE {
return Err(Error::InvalidInput(
"Vorbis identification header missing the \"vorbis\" signature",
));
}
let channels = id[VORBIS_ID_CHANNELS_OFFSET] as u16;
let sample_rate = u32::from_le_bytes([
id[VORBIS_ID_SAMPLE_RATE_OFFSET],
id[VORBIS_ID_SAMPLE_RATE_OFFSET + 1],
id[VORBIS_ID_SAMPLE_RATE_OFFSET + 2],
id[VORBIS_ID_SAMPLE_RATE_OFFSET + 3],
]);
Ok(CodecConfig::Vorbis {
codec_private: cp.clone(),
channels,
sample_rate,
})
}
fn vorbis_id_header(cp: &[u8]) -> Result<&[u8]> {
if cp.is_empty() {
return Err(Error::BufferTooShort {
need: 1,
have: 0,
what: "Vorbis CodecPrivate (Xiph lacing count)",
});
}
if cp[0] != VORBIS_LACE_COUNT {
return Err(Error::InvalidValue {
field: "Vorbis CodecPrivate lacing count",
value: cp[0] as u64,
reason: "expected numPackets-1 == 2 (three Xiph-laced Vorbis headers)",
});
}
let mut pos = 1usize;
let mut lengths = [0usize; 2];
for len in lengths.iter_mut() {
loop {
let b = *cp.get(pos).ok_or(Error::BufferTooShort {
need: pos + 1,
have: cp.len(),
what: "Vorbis CodecPrivate Xiph lacing length",
})?;
pos += 1;
*len += b as usize;
if b != 0xFF {
break;
}
}
}
let id_start = pos;
let id_end = id_start
.checked_add(lengths[0])
.filter(|&e| e <= cp.len())
.ok_or(Error::BufferTooShort {
need: id_start + lengths[0],
have: cp.len(),
what: "Vorbis identification header body",
})?;
Ok(&cp[id_start..id_end])
}
fn vp9_config(info: &TrackInfo) -> CodecConfig {
let config = Vp9ConfigurationBox {
version: VPCC_VERSION,
flags: 0,
profile: VP9_PROFILE_0,
level: VP9_LEVEL_UNSPECIFIED,
bit_depth: VP9_BIT_DEPTH_8,
chroma_subsampling: VP9_CHROMA_420,
video_full_range_flag: false,
colour_primaries: CICP_UNSPECIFIED,
transfer_characteristics: CICP_UNSPECIFIED,
matrix_coefficients: CICP_UNSPECIFIED,
codec_initialization_data: Vec::new(),
};
CodecConfig::Vp9 {
config,
width: info.pixel_width,
height: info.pixel_height,
}
}
fn opus_config(info: &TrackInfo) -> Result<CodecConfig> {
let cp = &info.codec_private;
if cp.len() < OPUS_HEAD_MIN_LEN {
return Err(Error::BufferTooShort {
need: OPUS_HEAD_MIN_LEN,
have: cp.len(),
what: "Opus CodecPrivate (OpusHead)",
});
}
if &cp[0..8] != OPUS_HEAD_MAGIC {
return Err(Error::InvalidValue {
field: "OpusHead magic",
value: u64::from_be_bytes([cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]]),
reason: "Opus CodecPrivate does not start with the \"OpusHead\" signature",
});
}
let version = cp[8];
let output_channel_count = cp[9];
let pre_skip = u16::from_le_bytes([cp[10], cp[11]]);
let input_sample_rate = u32::from_le_bytes([cp[12], cp[13], cp[14], cp[15]]);
let output_gain = i16::from_le_bytes([cp[16], cp[17]]);
let channel_mapping_family = cp[18];
let channel_mapping = if channel_mapping_family != 0 {
let need = OPUS_HEAD_MIN_LEN + 2 + output_channel_count as usize;
if cp.len() < need {
return Err(Error::BufferTooShort {
need,
have: cp.len(),
what: "OpusHead channel-mapping table",
});
}
let stream_count = cp[19];
let coupled_count = cp[20];
let map_start = 21;
let map_end = map_start + output_channel_count as usize;
Some(crate::opus::ChannelMappingTable {
stream_count,
coupled_count,
channel_mapping: cp[map_start..map_end].to_vec(),
})
} else {
None
};
let dops = OpusSpecificBox {
version,
output_channel_count,
pre_skip,
input_sample_rate,
output_gain,
channel_mapping_family,
channel_mapping,
};
Ok(CodecConfig::Opus {
config: dops,
channel_count: output_channel_count as u16,
sample_rate: OPUS_OUTPUT_SAMPLE_RATE,
sample_size: AUDIO_SAMPLE_SIZE,
})
}
struct EbmlReader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> EbmlReader<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn next_element(&mut self) -> Result<Option<(u32, &'a [u8])>> {
self.next_element_bounded(&[])
}
fn next_element_bounded(&mut self, boundary_ids: &[u32]) -> Result<Option<(u32, &'a [u8])>> {
if self.pos >= self.buf.len() {
return Ok(None);
}
let rest = &self.buf[self.pos..];
let (id, id_len) =
read_element_id(rest).ok_or(Error::InvalidInput("webm: truncated element ID"))?;
let after_id = &rest[id_len..];
let (size, size_len, unknown) = read_element_size(after_id)
.ok_or(Error::InvalidInput("webm: truncated element size"))?;
let body_start = self.pos + id_len + size_len;
let body_end = if unknown {
find_sibling_boundary(self.buf, body_start, boundary_ids).unwrap_or(self.buf.len())
} else {
let end = body_start + size as usize;
if end > self.buf.len() {
return Err(Error::BufferTooShort {
need: end,
have: self.buf.len(),
what: "webm element body",
});
}
end
};
let body = &self.buf[body_start..body_end];
self.pos = body_end;
Ok(Some((id, body)))
}
}
fn find_sibling_boundary(buf: &[u8], from: usize, boundary_ids: &[u32]) -> Option<usize> {
if boundary_ids.is_empty() {
return None;
}
let mut p = from;
while p < buf.len() {
if let Some((id, id_len)) = read_element_id(&buf[p..])
&& boundary_ids.contains(&id)
&& let Some((size, size_len, unknown)) = read_element_size(&buf[p + id_len..])
{
let body_start = p + id_len + size_len;
if unknown || body_start + size as usize <= buf.len() {
return Some(p);
}
}
p += 1;
}
None
}
fn read_element_id(buf: &[u8]) -> Option<(u32, usize)> {
let first = *buf.first()?;
if first == 0 {
return None; }
let len = first.leading_zeros() as usize + 1;
if len > 4 || buf.len() < len {
return None;
}
let mut id: u32 = 0;
for &b in &buf[..len] {
id = (id << 8) | b as u32;
}
Some((id, len))
}
fn read_element_size(buf: &[u8]) -> Option<(u64, usize, bool)> {
let (value, len, all_ones) = read_vint(buf)?;
Some((value, len, all_ones))
}
fn read_vint(buf: &[u8]) -> Option<(u64, usize, bool)> {
let first = *buf.first()?;
if first == 0 {
return None; }
let len = first.leading_zeros() as usize + 1;
if buf.len() < len {
return None;
}
let first_mask: u8 = if len >= 8 { 0 } else { 0xFF >> len };
let mut value = (first & first_mask) as u64;
for &b in &buf[1..len] {
value = (value << 8) | b as u64;
}
let data_bits = 7 * len; let max = if data_bits >= 64 {
u64::MAX
} else {
(1u64 << data_bits) - 1
};
Some((value, len, value == max))
}
fn read_vint_value(buf: &[u8]) -> Option<(u64, usize)> {
read_vint(buf).map(|(v, len, _)| (v, len))
}
fn read_uint(body: &[u8]) -> u64 {
let mut v: u64 = 0;
for &b in body.iter().take(8) {
v = (v << 8) | b as u64;
}
v
}
fn read_float(body: &[u8]) -> f64 {
match body.len() {
4 => f32::from_be_bytes([body[0], body[1], body[2], body[3]]) as f64,
8 => f64::from_be_bytes([
body[0], body[1], body[2], body[3], body[4], body[5], body[6], body[7],
]),
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_media_examines_each_block_once_not_once_per_track() {
const TRACKS: u64 = 8;
const BLOCKS: usize = 1000;
let tracks: Vec<TrackInfo> = (1..=TRACKS)
.map(|n| TrackInfo {
track_number: n,
codec_id: b"X_UNSUPPORTED".to_vec(),
..TrackInfo::default()
})
.collect();
let blocks: Vec<RawBlock> = (0..BLOCKS)
.map(|i| RawBlock {
track_number: (i as u64 % TRACKS) + 1,
pts_ticks: i as i64,
is_sync: true,
frames: vec![vec![0u8; 4]],
declared_frames: 1,
})
.collect();
BLOCKS_VISITED.with(|c| c.set(0));
build_media(1_000_000, tracks, blocks).unwrap();
let visited = BLOCKS_VISITED.with(core::cell::Cell::get);
assert_eq!(
visited,
2 * BLOCKS,
"partition + gather, not per-track rescans"
);
}
#[test]
fn vint_one_byte() {
assert_eq!(read_vint(&[0x81]), Some((1, 1, false)));
assert_eq!(read_vint(&[0xFF]), Some((127, 1, true)));
}
#[test]
fn vint_two_byte() {
assert_eq!(read_vint(&[0x40, 0x02]), Some((2, 2, false)));
}
#[test]
fn element_id_segment() {
assert_eq!(
read_element_id(&[0x18, 0x53, 0x80, 0x67]),
Some((SEGMENT, 4))
);
}
#[test]
fn element_id_track_entry() {
assert_eq!(read_element_id(&[0xAE]), Some((TRACK_ENTRY, 1)));
}
#[test]
fn uint_be() {
assert_eq!(read_uint(&[0x0F, 0x42, 0x40]), 1_000_000);
}
#[test]
fn laced_block_is_unlaced() {
let block = [0x81u8, 0x00, 0x00, 0x02, 0x01, 0x02, 0xAA, 0xBB, 0xCC, 0xDD];
let parsed = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).expect("parse");
assert_eq!(
parsed.frames,
alloc::vec![alloc::vec![0xAA, 0xBB], alloc::vec![0xCC, 0xDD]],
"a Xiph-laced block splits into its frames, sized per the lace run"
);
}
#[test]
fn unlaced_block_is_one_frame() {
let block = [0x81u8, 0x00, 0x00, 0x80, 0xAA, 0xBB];
let parsed = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).expect("parse");
assert_eq!(parsed.frames, alloc::vec![alloc::vec![0xAA, 0xBB]]);
}
#[test]
fn ebml_lacing_sizes_each_frame_from_its_delta() {
let bytes = [
0x81u8, 0x00, 0x00, 0x06, 0x02, 0x83, 0xC0, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, 0xCC, ];
let parsed = parse_block(&bytes, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).expect("parse");
assert_eq!(
parsed
.frames
.iter()
.map(|f| f.len())
.collect::<alloc::vec::Vec<_>>(),
alloc::vec![3, 4, 1],
"EBML lacing sizes each frame from the running VINT delta"
);
}
#[test]
fn fixed_lacing_needs_a_whole_multiple() {
let block = [0x81u8, 0x00, 0x00, 0x04, 0x01, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
let err = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).unwrap_err();
assert!(
matches!(err, Error::InvalidInput(_)),
"a fixed-laced payload that is not a multiple of the frame count is rejected, \
never silently truncated"
);
}
#[test]
fn zero_length_laced_frames_are_rejected() {
let block = [0x81u8, 0x00, 0x00, 0x04, 0xFF];
let err = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).unwrap_err();
assert!(
matches!(err, Error::InvalidInput(_)),
"a zero-length laced frame must be rejected, got {err:?}"
);
}
#[test]
fn xiph_laced_zero_length_frame_is_rejected() {
let block = [0x81u8, 0x00, 0x00, 0x02, 0x01, 0x00, 0xAA];
let err = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn declared_frame_count_cannot_exceed_the_file_length() {
let block_payload = [
0x81u8, 0x00, 0x00, 0x04, 0xFF, 0xAA, 0xBB, ];
let mut cluster_body = alloc::vec![0xE7, 0x81, 0x00]; cluster_body.extend_from_slice(&[0xA3, 0x87]); cluster_body.extend_from_slice(&block_payload);
let mut segment_body = alloc::vec![0x16, 0x54, 0xAE, 0x6B, 0x80]; segment_body.extend_from_slice(&[0x1F, 0x43, 0xB6, 0x75, 0x80 | cluster_body.len() as u8]);
segment_body.extend_from_slice(&cluster_body);
let mut file = alloc::vec![0x1A, 0x45, 0xDF, 0xA3, 0x80]; file.extend_from_slice(&[0x18, 0x53, 0x80, 0x67, 0x80 | segment_body.len() as u8]);
file.extend_from_slice(&segment_body);
let mut demux = WebmDemux::new();
let media = demux.demux(&file).expect("a damaged block is skipped");
assert!(
media.tracks.iter().all(|t| t.samples.is_empty()),
"a block declaring more frames than the file has bytes yields no frames"
);
assert_eq!(
media.degraded.len(),
1,
"and is reported: {:?}",
media.degraded
);
let frames = demux_frame_count(&[0x81u8, 0x00, 0x00, 0x02, 0x01, 0x00, 0xAA]);
assert_eq!(frames, None, "a zero-length lace is rejected");
let frames = demux_frame_count(&[0x81u8, 0x00, 0x00, 0x02, 0x01, 0x01, 0xAA, 0xBB]);
assert_eq!(frames, Some(2), "a two-frame Xiph lace parses");
}
fn demux_frame_count(block: &[u8]) -> Option<usize> {
parse_block(block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true)
.ok()
.map(|b| b.frames.len())
}
#[test]
fn a_full_lace_count_is_accepted_and_the_cap_is_the_format_maximum() {
assert_eq!(
MAX_LACED_FRAMES,
usize::from(u8::MAX) + 1,
"the cap is the format's own maximum frame count"
);
let mut block = alloc::vec![0x81u8, 0x00, 0x00, 0x04, 0xFF];
block.extend(core::iter::repeat_n(0xAAu8, 256));
let parsed = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).expect("parse");
assert_eq!(parsed.frames.len(), MAX_LACED_FRAMES);
assert_eq!(parsed.declared_frames, MAX_LACED_FRAMES);
assert!(parsed.frames.iter().all(|f| f.len() == 1));
}
#[test]
fn laced_frame_size_past_the_payload_is_rejected() {
let block = [0x81u8, 0x00, 0x00, 0x02, 0x01, 0x05, 0xAA, 0xBB];
let err = parse_block(&block, 0, DEFAULT_TIMESTAMP_SCALE_NS, true).unwrap_err();
assert!(matches!(err, Error::BufferTooShort { .. }));
}
#[test]
fn webm_avc_two_byte_nal_lengths_normalised_to_four() {
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::ir::{FragmentTrackData, Sample, SampleFlags};
use crate::mkv_mux::MkvMux;
use crate::nalu_types::{AvcPps, AvcSps};
use crate::pipeline::{CodecConfig, TrackSpec, build_init_segment, build_media_segment};
use broadcast_common::Package;
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 66,
profile_compatibility: 0,
level_indication: 0x1E,
length_size_minus_one: 1,
sps: alloc::vec![AvcSps(alloc::vec![0x67, 0x42, 0x00, 0x1E, 0xAB, 0x40])],
pps: alloc::vec![AvcPps(alloc::vec![0x68, 0xCE, 0x3C, 0x80])],
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: alloc::vec![],
};
let config = CodecConfig::Avc {
config: AVCConfigurationBox::new(record),
width: 640,
height: 360,
};
let sample = Sample {
data: alloc::vec![0x00, 0x05, 0x65, 0x88, 0x84, 0x00, 0x21, 0x00, 0x01, 0x41].into(),
dts: Some(0),
pts: Some(0),
duration: Some(1000),
flags: SampleFlags::new(true),
provenance: None,
display: None,
};
let media = Media::new(
alloc::vec![Track::new_at(
TrackSpec::new(TrackId::new(1), IR_TIMESCALE, config),
alloc::vec![sample],
0,
)],
IR_TIMESCALE,
);
let muxed = MkvMux::new().package(&media).expect("mkv package");
let demuxed = WebmDemux::new()
.unpackage(&muxed)
.expect("WebM demux of the 2-byte-length file");
let track = &demuxed.tracks[0];
const EXPECTED: [u8; 14] = [
0x00, 0x00, 0x00, 0x05, 0x65, 0x88, 0x84, 0x00, 0x21, 0x00, 0x00, 0x00, 0x01, 0x41, ];
assert_eq!(
track.samples[0].data.as_ref(),
EXPECTED,
"2-byte NAL lengths must be rewritten to 4-byte on the demux edge"
);
let CodecConfig::Avc { config, .. } = &track.spec.config else {
panic!("expected CodecConfig::Avc");
};
assert_eq!(
config.config.length_size_minus_one, NAL_LENGTH_SIZE_MINUS_ONE,
"the emitted avcC must declare the 4-byte length size of the samples"
);
let init = build_init_segment(core::slice::from_ref(&track.spec), IR_TIMESCALE)
.expect("init segment");
let frag = build_media_segment(
1,
&[FragmentTrackData::new(TrackId::new(1), 0, &track.samples)],
)
.expect("media segment");
let mut both = init;
both.extend_from_slice(&frag);
let reparsed = crate::media::Fmp4Demux::new()
.unpackage(&both)
.expect("re-demux of the muxed normalised IR");
assert_eq!(
reparsed.tracks[0].samples[0].data.as_ref(),
EXPECTED,
"the 4-byte-prefixed NALs must survive a CMAF mux/demux round-trip"
);
let CodecConfig::Avc { config, .. } = &reparsed.tracks[0].spec.config else {
panic!("expected CodecConfig::Avc");
};
assert_eq!(
config.config.length_size_minus_one,
NAL_LENGTH_SIZE_MINUS_ONE
);
}
#[test]
fn webm_hevc_two_byte_nal_lengths_normalised_to_four() {
use crate::hevc_config::{HEVCConfigurationBox, HEVCDecoderConfigurationRecord};
use crate::ir::{FragmentTrackData, Sample, SampleFlags};
use crate::mkv_mux::MkvMux;
use crate::nalu_types::{HevcNalArray, HevcNalUnit};
use crate::pipeline::{CodecConfig, TrackSpec, build_init_segment, build_media_segment};
use broadcast_common::Package;
let record = HEVCDecoderConfigurationRecord {
configuration_version: 1,
general_profile_space: 0,
general_tier_flag: false,
general_profile_idc: 1,
general_profile_compatibility_flags: 0,
general_constraint_indicator_flags: 0,
general_level_idc: 93,
min_spatial_segmentation_idc: 0,
parallelism_type: 0,
chroma_format_idc: 1,
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
avg_frame_rate: 0,
constant_frame_rate: 0,
num_temporal_layers: 1,
temporal_id_nested: false,
length_size_minus_one: 1,
arrays: alloc::vec![HevcNalArray {
array_completeness: true,
nal_unit_type: 32,
nalus: alloc::vec![HevcNalUnit(alloc::vec![0x40, 0x01, 0x0C, 0x01, 0xFF])],
}],
};
let config = CodecConfig::Hevc {
config: HEVCConfigurationBox::new(record),
width: 640,
height: 360,
};
let sample = Sample {
data: alloc::vec![0x00, 0x04, 0x26, 0x01, 0xAF, 0x09].into(),
dts: Some(0),
pts: Some(0),
duration: Some(1000),
flags: SampleFlags::new(true),
provenance: None,
display: None,
};
let media = Media::new(
alloc::vec![Track::new_at(
TrackSpec::new(TrackId::new(1), IR_TIMESCALE, config),
alloc::vec![sample],
0,
)],
IR_TIMESCALE,
);
let muxed = MkvMux::new().package(&media).expect("mkv package");
let demuxed = WebmDemux::new().unpackage(&muxed).expect("WebM demux");
let track = &demuxed.tracks[0];
assert_eq!(
track.samples[0].data.as_ref(),
[0x00, 0x00, 0x00, 0x04, 0x26, 0x01, 0xAF, 0x09],
"HEVC 2-byte NAL lengths must be rewritten to 4-byte"
);
let CodecConfig::Hevc { config, .. } = &track.spec.config else {
panic!("expected CodecConfig::Hevc");
};
assert_eq!(
config.config.length_size_minus_one, NAL_LENGTH_SIZE_MINUS_ONE,
"the emitted hvcC must declare the 4-byte length size"
);
let expected: [u8; 8] = [0x00, 0x00, 0x00, 0x04, 0x26, 0x01, 0xAF, 0x09];
let init =
build_init_segment(core::slice::from_ref(&track.spec), IR_TIMESCALE).expect("init");
let frag = build_media_segment(
1,
&[FragmentTrackData::new(TrackId::new(1), 0, &track.samples)],
)
.expect("media segment");
let mut both = init;
both.extend_from_slice(&frag);
let reparsed = crate::media::Fmp4Demux::new()
.unpackage(&both)
.expect("re-demux of the muxed normalised HEVC IR");
assert_eq!(
reparsed.tracks[0].samples[0].data.as_ref(),
expected,
"4-byte-prefixed HEVC NALs must survive a CMAF mux/demux round-trip"
);
let CodecConfig::Hevc { config, .. } = &reparsed.tracks[0].spec.config else {
panic!("expected CodecConfig::Hevc after re-demux");
};
assert_eq!(
config.config.length_size_minus_one,
NAL_LENGTH_SIZE_MINUS_ONE
);
}
}