use oxideav_core::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SampleGroupGroupingType {
Roll,
Prol,
RateShare,
AlternativeStartup,
RandomAccessPoint,
TemporalLevel,
Sap,
}
impl SampleGroupGroupingType {
pub fn from_fourcc(fourcc: &[u8; 4]) -> Option<Self> {
match fourcc {
b"roll" => Some(Self::Roll),
b"prol" => Some(Self::Prol),
b"rash" => Some(Self::RateShare),
b"alst" => Some(Self::AlternativeStartup),
b"rap " => Some(Self::RandomAccessPoint),
b"tele" => Some(Self::TemporalLevel),
b"sap " => Some(Self::Sap),
_ => None,
}
}
pub fn fourcc(self) -> [u8; 4] {
match self {
Self::Roll => *b"roll",
Self::Prol => *b"prol",
Self::RateShare => *b"rash",
Self::AlternativeStartup => *b"alst",
Self::RandomAccessPoint => *b"rap ",
Self::TemporalLevel => *b"tele",
Self::Sap => *b"sap ",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SampleGroupEntry {
Roll(RollRecoveryEntry),
RateShare(RateShareEntry),
AlternativeStartup(AlternativeStartupEntry),
RandomAccessPoint(VisualRandomAccessEntry),
TemporalLevel(TemporalLevelEntry),
Sap(SapEntry),
}
pub fn decode_sample_group_entry(
grouping_type: &[u8; 4],
blob: &[u8],
) -> Result<Option<SampleGroupEntry>> {
let Some(gt) = SampleGroupGroupingType::from_fourcc(grouping_type) else {
return Ok(None);
};
let entry = match gt {
SampleGroupGroupingType::Roll | SampleGroupGroupingType::Prol => {
SampleGroupEntry::Roll(parse_roll(blob)?)
}
SampleGroupGroupingType::RateShare => SampleGroupEntry::RateShare(parse_rash(blob)?),
SampleGroupGroupingType::AlternativeStartup => {
SampleGroupEntry::AlternativeStartup(parse_alst(blob)?)
}
SampleGroupGroupingType::RandomAccessPoint => {
SampleGroupEntry::RandomAccessPoint(parse_rap(blob)?)
}
SampleGroupGroupingType::TemporalLevel => {
SampleGroupEntry::TemporalLevel(parse_tele(blob)?)
}
SampleGroupGroupingType::Sap => SampleGroupEntry::Sap(parse_sap(blob)?),
};
Ok(Some(entry))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RollRecoveryEntry {
pub roll_distance: i16,
}
pub fn parse_roll(blob: &[u8]) -> Result<RollRecoveryEntry> {
let b = blob
.get(0..2)
.ok_or_else(|| Error::invalid("sgpd roll/prol entry: need 2 bytes for roll_distance"))?;
Ok(RollRecoveryEntry {
roll_distance: i16::from_be_bytes([b[0], b[1]]),
})
}
pub fn build_roll(e: &RollRecoveryEntry) -> Vec<u8> {
e.roll_distance.to_be_bytes().to_vec()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RateShareOperationPoint {
pub available_bitrate: u32,
pub target_rate_share: u16,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RateShareEntry {
pub single_target_rate_share: Option<u16>,
pub operation_points: Vec<RateShareOperationPoint>,
pub maximum_bitrate: u32,
pub minimum_bitrate: u32,
pub discard_priority: u8,
}
pub fn parse_rash(blob: &[u8]) -> Result<RateShareEntry> {
let read_u16 =
|o: usize| -> Option<u16> { blob.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]])) };
let read_u32 = |o: usize| -> Option<u32> {
blob.get(o..o + 4)
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
};
let op_count = read_u16(0)
.ok_or_else(|| Error::invalid("sgpd rash entry: operation_point_count truncated"))?;
if op_count == 0 {
return Err(Error::invalid(
"sgpd rash entry: operation_point_count must be non-zero (§10.2.2.3)",
));
}
let mut off = 2;
let (single_target_rate_share, operation_points) = if op_count == 1 {
let t = read_u16(off)
.ok_or_else(|| Error::invalid("sgpd rash entry: target_rate_share truncated"))?;
off += 2;
(Some(t), Vec::new())
} else {
let mut pts = Vec::with_capacity(op_count as usize);
for _ in 0..op_count {
let ab = read_u32(off)
.ok_or_else(|| Error::invalid("sgpd rash entry: available_bitrate truncated"))?;
off += 4;
let t = read_u16(off)
.ok_or_else(|| Error::invalid("sgpd rash entry: target_rate_share truncated"))?;
off += 2;
pts.push(RateShareOperationPoint {
available_bitrate: ab,
target_rate_share: t,
});
}
(None, pts)
};
let maximum_bitrate = read_u32(off)
.ok_or_else(|| Error::invalid("sgpd rash entry: maximum_bitrate truncated"))?;
off += 4;
let minimum_bitrate = read_u32(off)
.ok_or_else(|| Error::invalid("sgpd rash entry: minimum_bitrate truncated"))?;
off += 4;
let discard_priority = *blob
.get(off)
.ok_or_else(|| Error::invalid("sgpd rash entry: discard_priority truncated"))?;
Ok(RateShareEntry {
single_target_rate_share,
operation_points,
maximum_bitrate,
minimum_bitrate,
discard_priority,
})
}
pub fn build_rash(e: &RateShareEntry) -> Vec<u8> {
let mut out = Vec::new();
if let Some(t) = e.single_target_rate_share {
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&t.to_be_bytes());
} else if e.operation_points.is_empty() {
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
} else {
let count = e.operation_points.len().min(u16::MAX as usize) as u16;
out.extend_from_slice(&count.to_be_bytes());
for p in e.operation_points.iter().take(count as usize) {
out.extend_from_slice(&p.available_bitrate.to_be_bytes());
out.extend_from_slice(&p.target_rate_share.to_be_bytes());
}
}
out.extend_from_slice(&e.maximum_bitrate.to_be_bytes());
out.extend_from_slice(&e.minimum_bitrate.to_be_bytes());
out.push(e.discard_priority);
out
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AlstOutputRatePiece {
pub num_output_samples: u16,
pub num_total_samples: u16,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AlternativeStartupEntry {
pub first_output_sample: u16,
pub sample_offsets: Vec<u32>,
pub output_rate_pieces: Vec<AlstOutputRatePiece>,
}
pub fn parse_alst(blob: &[u8]) -> Result<AlternativeStartupEntry> {
let read_u16 =
|o: usize| -> Option<u16> { blob.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]])) };
let read_u32 = |o: usize| -> Option<u32> {
blob.get(o..o + 4)
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
};
let roll_count = read_u16(0)
.ok_or_else(|| Error::invalid("sgpd alst entry: roll_count truncated"))?
as usize;
let first_output_sample = read_u16(2)
.ok_or_else(|| Error::invalid("sgpd alst entry: first_output_sample truncated"))?;
let mut off = 4;
let mut sample_offsets = Vec::with_capacity(roll_count);
for _ in 0..roll_count {
let v = read_u32(off)
.ok_or_else(|| Error::invalid("sgpd alst entry: sample_offset truncated"))?;
sample_offsets.push(v);
off += 4;
}
let tail = &blob[off..];
if tail.len() % 4 != 0 {
return Err(Error::invalid(
"sgpd alst entry: output-rate tail not a whole number of (out,total) pieces",
));
}
let mut output_rate_pieces = Vec::with_capacity(tail.len() / 4);
for chunk in tail.chunks_exact(4) {
output_rate_pieces.push(AlstOutputRatePiece {
num_output_samples: u16::from_be_bytes([chunk[0], chunk[1]]),
num_total_samples: u16::from_be_bytes([chunk[2], chunk[3]]),
});
}
Ok(AlternativeStartupEntry {
first_output_sample,
sample_offsets,
output_rate_pieces,
})
}
pub fn build_alst(e: &AlternativeStartupEntry) -> Vec<u8> {
let roll_count = e.sample_offsets.len().min(u16::MAX as usize) as u16;
let mut out =
Vec::with_capacity(4 + e.sample_offsets.len() * 4 + e.output_rate_pieces.len() * 4);
out.extend_from_slice(&roll_count.to_be_bytes());
out.extend_from_slice(&e.first_output_sample.to_be_bytes());
for &o in e.sample_offsets.iter().take(roll_count as usize) {
out.extend_from_slice(&o.to_be_bytes());
}
for p in &e.output_rate_pieces {
out.extend_from_slice(&p.num_output_samples.to_be_bytes());
out.extend_from_slice(&p.num_total_samples.to_be_bytes());
}
out
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VisualRandomAccessEntry {
pub num_leading_samples_known: bool,
pub num_leading_samples: u8,
}
pub fn parse_rap(blob: &[u8]) -> Result<VisualRandomAccessEntry> {
let b = *blob
.first()
.ok_or_else(|| Error::invalid("sgpd rap entry: need 1 byte"))?;
Ok(VisualRandomAccessEntry {
num_leading_samples_known: (b & 0x80) != 0,
num_leading_samples: b & 0x7F,
})
}
pub fn build_rap(e: &VisualRandomAccessEntry) -> Vec<u8> {
let known = if e.num_leading_samples_known { 0x80 } else { 0 };
vec![known | (e.num_leading_samples & 0x7F)]
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TemporalLevelEntry {
pub level_independently_decodable: bool,
}
pub fn parse_tele(blob: &[u8]) -> Result<TemporalLevelEntry> {
let b = *blob
.first()
.ok_or_else(|| Error::invalid("sgpd tele entry: need 1 byte"))?;
Ok(TemporalLevelEntry {
level_independently_decodable: (b & 0x80) != 0,
})
}
pub fn build_tele(e: &TemporalLevelEntry) -> Vec<u8> {
vec![if e.level_independently_decodable {
0x80
} else {
0
}]
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SapEntry {
pub dependent_flag: bool,
pub sap_type: u8,
}
pub fn parse_sap(blob: &[u8]) -> Result<SapEntry> {
let b = *blob
.first()
.ok_or_else(|| Error::invalid("sgpd sap entry: need 1 byte"))?;
Ok(SapEntry {
dependent_flag: (b & 0x80) != 0,
sap_type: b & 0x0F,
})
}
pub fn build_sap(e: &SapEntry) -> Vec<u8> {
let dep = if e.dependent_flag { 0x80 } else { 0 };
vec![dep | (e.sap_type & 0x0F)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roll_roundtrip_positive() {
let e = RollRecoveryEntry { roll_distance: 4 };
let blob = build_roll(&e);
assert_eq!(blob, vec![0x00, 0x04]);
assert_eq!(parse_roll(&blob).unwrap(), e);
}
#[test]
fn roll_roundtrip_negative() {
let e = RollRecoveryEntry { roll_distance: -3 };
let blob = build_roll(&e);
assert_eq!(blob, (-3i16).to_be_bytes());
assert_eq!(parse_roll(&blob).unwrap(), e);
}
#[test]
fn roll_tolerates_trailing_bytes() {
let blob = [0x00, 0x05, 0xDE, 0xAD];
assert_eq!(parse_roll(&blob).unwrap().roll_distance, 5);
}
#[test]
fn roll_rejects_short() {
assert!(parse_roll(&[0x00]).is_err());
assert!(parse_roll(&[]).is_err());
}
#[test]
fn grouping_type_fourcc_roundtrip() {
for gt in [
SampleGroupGroupingType::Roll,
SampleGroupGroupingType::Prol,
SampleGroupGroupingType::RateShare,
SampleGroupGroupingType::AlternativeStartup,
SampleGroupGroupingType::RandomAccessPoint,
SampleGroupGroupingType::TemporalLevel,
SampleGroupGroupingType::Sap,
] {
assert_eq!(SampleGroupGroupingType::from_fourcc(>.fourcc()), Some(gt));
}
assert_eq!(SampleGroupGroupingType::from_fourcc(b"sync"), None);
assert_eq!(SampleGroupGroupingType::from_fourcc(b"seig"), None);
}
#[test]
fn rap_roundtrip_known() {
let e = VisualRandomAccessEntry {
num_leading_samples_known: true,
num_leading_samples: 3,
};
let blob = build_rap(&e);
assert_eq!(blob, vec![0x83]); assert_eq!(parse_rap(&blob).unwrap(), e);
}
#[test]
fn rap_roundtrip_unknown() {
let e = VisualRandomAccessEntry {
num_leading_samples_known: false,
num_leading_samples: 0x7F,
};
let blob = build_rap(&e);
assert_eq!(blob, vec![0x7F]);
assert_eq!(parse_rap(&blob).unwrap(), e);
}
#[test]
fn rap_masks_7bit() {
let e = VisualRandomAccessEntry {
num_leading_samples_known: false,
num_leading_samples: 0xFF,
};
assert_eq!(build_rap(&e), vec![0x7F]);
}
#[test]
fn rap_rejects_empty() {
assert!(parse_rap(&[]).is_err());
}
#[test]
fn tele_roundtrip() {
for indep in [false, true] {
let e = TemporalLevelEntry {
level_independently_decodable: indep,
};
let blob = build_tele(&e);
assert_eq!(blob, vec![if indep { 0x80 } else { 0 }]);
assert_eq!(parse_tele(&blob).unwrap(), e);
}
}
#[test]
fn tele_ignores_reserved_bits() {
assert!(parse_tele(&[0xFF]).unwrap().level_independently_decodable);
assert!(!parse_tele(&[0x7F]).unwrap().level_independently_decodable);
}
#[test]
fn sap_roundtrip() {
let e = SapEntry {
dependent_flag: true,
sap_type: 6,
};
let blob = build_sap(&e);
assert_eq!(blob, vec![0x86]); assert_eq!(parse_sap(&blob).unwrap(), e);
}
#[test]
fn sap_ignores_reserved_and_masks() {
let parsed = parse_sap(&[0xF5]).unwrap(); assert!(parsed.dependent_flag);
assert_eq!(parsed.sap_type, 5);
let e = SapEntry {
dependent_flag: false,
sap_type: 0xFF,
};
assert_eq!(build_sap(&e), vec![0x0F]);
}
#[test]
fn sap_rejects_empty() {
assert!(parse_sap(&[]).is_err());
}
#[test]
fn alst_roundtrip_no_tail() {
let e = AlternativeStartupEntry {
first_output_sample: 2,
sample_offsets: vec![0, 10, 20],
output_rate_pieces: vec![],
};
let blob = build_alst(&e);
assert_eq!(blob.len(), 16);
assert_eq!(&blob[0..2], &3u16.to_be_bytes());
assert_eq!(&blob[2..4], &2u16.to_be_bytes());
assert_eq!(parse_alst(&blob).unwrap(), e);
}
#[test]
fn alst_roundtrip_with_tail() {
let e = AlternativeStartupEntry {
first_output_sample: 1,
sample_offsets: vec![0, 5],
output_rate_pieces: vec![
AlstOutputRatePiece {
num_output_samples: 4,
num_total_samples: 8,
},
AlstOutputRatePiece {
num_output_samples: 2,
num_total_samples: 3,
},
],
};
let blob = build_alst(&e);
assert_eq!(blob.len(), 20);
assert_eq!(parse_alst(&blob).unwrap(), e);
}
#[test]
fn alst_roundtrip_roll_count_zero() {
let e = AlternativeStartupEntry {
first_output_sample: 0,
sample_offsets: vec![],
output_rate_pieces: vec![],
};
let blob = build_alst(&e);
assert_eq!(blob.len(), 4);
assert_eq!(parse_alst(&blob).unwrap(), e);
}
#[test]
fn alst_rejects_truncated_offset_array() {
let mut blob = Vec::new();
blob.extend_from_slice(&2u16.to_be_bytes());
blob.extend_from_slice(&0u16.to_be_bytes());
blob.extend_from_slice(&7u32.to_be_bytes());
assert!(parse_alst(&blob).is_err());
}
#[test]
fn alst_rejects_odd_tail() {
let mut blob = Vec::new();
blob.extend_from_slice(&1u16.to_be_bytes());
blob.extend_from_slice(&1u16.to_be_bytes());
blob.extend_from_slice(&0u32.to_be_bytes());
blob.extend_from_slice(&9u16.to_be_bytes());
assert!(parse_alst(&blob).is_err());
}
#[test]
fn rash_roundtrip_single_point() {
let e = RateShareEntry {
single_target_rate_share: Some(60),
operation_points: vec![],
maximum_bitrate: 2000,
minimum_bitrate: 100,
discard_priority: 128,
};
let blob = build_rash(&e);
assert_eq!(blob.len(), 13);
assert_eq!(&blob[0..2], &1u16.to_be_bytes()); assert_eq!(parse_rash(&blob).unwrap(), e);
}
#[test]
fn rash_roundtrip_multi_point() {
let e = RateShareEntry {
single_target_rate_share: None,
operation_points: vec![
RateShareOperationPoint {
available_bitrate: 500,
target_rate_share: 40,
},
RateShareOperationPoint {
available_bitrate: 1500,
target_rate_share: 70,
},
],
maximum_bitrate: 3000,
minimum_bitrate: 200,
discard_priority: 64,
};
let blob = build_rash(&e);
assert_eq!(blob.len(), 23);
assert_eq!(&blob[0..2], &2u16.to_be_bytes());
assert_eq!(parse_rash(&blob).unwrap(), e);
}
#[test]
fn rash_rejects_count_zero() {
let mut blob = Vec::new();
blob.extend_from_slice(&0u16.to_be_bytes()); blob.extend_from_slice(&[0u8; 9]); assert!(parse_rash(&blob).is_err());
}
#[test]
fn rash_rejects_truncated_points() {
let mut blob = Vec::new();
blob.extend_from_slice(&2u16.to_be_bytes());
blob.extend_from_slice(&500u32.to_be_bytes());
blob.extend_from_slice(&40u16.to_be_bytes());
assert!(parse_rash(&blob).is_err());
}
#[test]
fn decode_dispatch_routes_each_type() {
let roll_blob = build_roll(&RollRecoveryEntry { roll_distance: -2 });
assert_eq!(
decode_sample_group_entry(b"roll", &roll_blob).unwrap(),
Some(SampleGroupEntry::Roll(RollRecoveryEntry {
roll_distance: -2
}))
);
assert_eq!(
decode_sample_group_entry(b"prol", &roll_blob).unwrap(),
Some(SampleGroupEntry::Roll(RollRecoveryEntry {
roll_distance: -2
}))
);
let rap_blob = build_rap(&VisualRandomAccessEntry {
num_leading_samples_known: true,
num_leading_samples: 1,
});
assert!(matches!(
decode_sample_group_entry(b"rap ", &rap_blob).unwrap(),
Some(SampleGroupEntry::RandomAccessPoint(_))
));
let tele_blob = build_tele(&TemporalLevelEntry {
level_independently_decodable: true,
});
assert!(matches!(
decode_sample_group_entry(b"tele", &tele_blob).unwrap(),
Some(SampleGroupEntry::TemporalLevel(_))
));
let sap_blob = build_sap(&SapEntry {
dependent_flag: false,
sap_type: 3,
});
assert!(matches!(
decode_sample_group_entry(b"sap ", &sap_blob).unwrap(),
Some(SampleGroupEntry::Sap(_))
));
let alst_blob = build_alst(&AlternativeStartupEntry {
first_output_sample: 1,
sample_offsets: vec![0],
output_rate_pieces: vec![],
});
assert!(matches!(
decode_sample_group_entry(b"alst", &alst_blob).unwrap(),
Some(SampleGroupEntry::AlternativeStartup(_))
));
let rash_blob = build_rash(&RateShareEntry {
single_target_rate_share: Some(50),
operation_points: vec![],
maximum_bitrate: 0,
minimum_bitrate: 0,
discard_priority: 1,
});
assert!(matches!(
decode_sample_group_entry(b"rash", &rash_blob).unwrap(),
Some(SampleGroupEntry::RateShare(_))
));
}
#[test]
fn decode_unknown_type_is_none() {
assert_eq!(
decode_sample_group_entry(b"sync", &[0x12, 0x34]).unwrap(),
None
);
assert_eq!(
decode_sample_group_entry(b"seig", &[0u8; 20]).unwrap(),
None
);
}
#[test]
fn decode_malformed_base_type_errors() {
assert!(decode_sample_group_entry(b"roll", &[]).is_err());
assert!(decode_sample_group_entry(b"rap ", &[]).is_err());
}
#[test]
fn rash_build_empty_degrades_to_single_zero() {
let e = RateShareEntry {
single_target_rate_share: None,
operation_points: vec![],
maximum_bitrate: 0,
minimum_bitrate: 0,
discard_priority: 128,
};
let blob = build_rash(&e);
assert_eq!(&blob[0..2], &1u16.to_be_bytes());
let parsed = parse_rash(&blob).unwrap();
assert_eq!(parsed.single_target_rate_share, Some(0));
}
}