use h264_parser::{bitreader::BitReader, eg::read_ue};
use scuffle_h265::{NALUnitType, SpsNALUnit};
use super::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SpsHead {
pub profile_space: u8,
pub tier_flag: bool,
pub profile_idc: u8,
pub profile_compatibility_flags: [u8; 4],
pub constraint_flags: [u8; 6],
pub level_idc: u8,
pub max_sub_layers_minus1: u8,
pub temporal_id_nesting_flag: bool,
pub chroma_format_idc: u8,
pub bit_depth_luma_minus8: u8,
pub bit_depth_chroma_minus8: u8,
pub width: u32,
pub height: u32,
}
pub(crate) struct Sps {
pub head: SpsHead,
pub full: Option<scuffle_h265::SpsRbsp>,
}
impl Sps {
pub(crate) fn parse(nal: &[u8]) -> Result<Self> {
match SpsNALUnit::parse(&mut &nal[..]) {
Ok(sps) => Ok(Self {
head: SpsHead::from_rbsp(&sps.rbsp)?,
full: Some(sps.rbsp),
}),
Err(err) if err.kind() == std::io::ErrorKind::InvalidData => {
let head = SpsHead::parse(nal).ok_or(Error::SpsParse)?;
tracing::warn!(%err, "H.265 SPS refused by scuffle_h265; using its head, without the VUI");
Ok(Self { head, full: None })
}
Err(_) => Err(Error::SpsParse),
}
}
}
impl SpsHead {
fn from_rbsp(rbsp: &scuffle_h265::SpsRbsp) -> Result<Self> {
let profile = &rbsp.profile_tier_level.general_profile;
Ok(Self {
profile_space: profile.profile_space,
tier_flag: profile.tier_flag,
profile_idc: profile.profile_idc,
profile_compatibility_flags: profile.profile_compatibility_flag.bits().to_be_bytes(),
constraint_flags: super::pack_constraint_flags(profile),
level_idc: profile.level_idc.ok_or(Error::MissingLevelIdc)?,
max_sub_layers_minus1: rbsp.sps_max_sub_layers_minus1,
temporal_id_nesting_flag: rbsp.sps_temporal_id_nesting_flag,
chroma_format_idc: rbsp.chroma_format_idc,
bit_depth_luma_minus8: rbsp.bit_depth_luma_minus8,
bit_depth_chroma_minus8: rbsp.bit_depth_chroma_minus8,
width: rbsp.cropped_width() as u32,
height: rbsp.cropped_height() as u32,
})
}
fn parse(nal: &[u8]) -> Option<Self> {
let [header, header2, payload @ ..] = nal else {
return None;
};
if header & 0x80 != 0 || super::split::nal_unit_type(*header) != NALUnitType::SpsNut || header2 & 0x07 != 1 {
return None;
}
let rbsp = h264_parser::nal::ebsp_to_rbsp(payload);
Self::read(&mut BitReader::new(&rbsp)).ok()?
}
fn read(r: &mut BitReader<'_>) -> h264_parser::Result<Option<Self>> {
r.skip_bits(4)?; let max_sub_layers_minus1 = r.read_bits(3)? as u8;
let temporal_id_nesting_flag = r.read_flag()?;
if max_sub_layers_minus1 > 6 || (max_sub_layers_minus1 == 0 && !temporal_id_nesting_flag) {
return Ok(None);
}
let profile_space = r.read_bits(2)? as u8;
let tier_flag = r.read_flag()?;
let profile_idc = r.read_bits(5)? as u8;
let profile_compatibility_flags = r.read_bits(32)?.to_be_bytes();
let constraint_flags = [(r.read_bits(4)? as u8) << 4, 0, 0, 0, 0, 0];
r.skip_bits(44)?; let level_idc = r.read_u8()?;
let sub_layers = usize::from(max_sub_layers_minus1);
let mut present = [(false, false); 6];
for flags in &mut present[..sub_layers] {
*flags = (r.read_flag()?, r.read_flag()?);
}
if sub_layers > 0 {
r.skip_bits(2 * (8 - u32::from(max_sub_layers_minus1)))?; }
for &(profile, level) in &present[..sub_layers] {
r.skip_bits(88 * u32::from(profile) + 8 * u32::from(level))?;
}
let sps_seq_parameter_set_id = read_ue(r)?;
let chroma_format_idc = read_ue(r)?;
if sps_seq_parameter_set_id > 15 || chroma_format_idc > 3 {
return Ok(None);
}
if chroma_format_idc == 3 {
r.skip_bits(1)?; }
let pic_width = read_ue(r)?;
let pic_height = read_ue(r)?;
let [left, right, top, bottom] = if r.read_flag()? {
[read_ue(r)?, read_ue(r)?, read_ue(r)?, read_ue(r)?]
} else {
[0; 4]
};
let bit_depth_luma_minus8 = read_ue(r)?;
let bit_depth_chroma_minus8 = read_ue(r)?;
if bit_depth_luma_minus8 > 8 || bit_depth_chroma_minus8 > 8 {
return Ok(None);
}
let sub_width_c = if matches!(chroma_format_idc, 1 | 2) { 2 } else { 1 };
let sub_height_c = if chroma_format_idc == 1 { 2 } else { 1 };
let width = pic_width.checked_sub(left.saturating_add(right).saturating_mul(sub_width_c));
let height = pic_height.checked_sub(top.saturating_add(bottom).saturating_mul(sub_height_c));
let (Some(width @ 1..), Some(height @ 1..)) = (width, height) else {
return Ok(None);
};
Ok(Some(Self {
profile_space,
tier_flag,
profile_idc,
profile_compatibility_flags,
constraint_flags,
level_idc,
max_sub_layers_minus1,
temporal_id_nesting_flag,
chroma_format_idc: chroma_format_idc as u8,
bit_depth_luma_minus8: bit_depth_luma_minus8 as u8,
bit_depth_chroma_minus8: bit_depth_chroma_minus8 as u8,
width,
height,
}))
}
}
#[cfg(test)]
mod tests {
use super::super::fixtures;
use super::*;
const SPS_SUB_LAYERS: &[u8] = &[
0x42, 0x01, 0x05, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d,
0x90, 0x00, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d, 0xa0,
0x02, 0x80, 0x80, 0x2d, 0x16, 0x51, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x02, 0x00, 0x00, 0x03, 0x00, 0x02,
0x00, 0x00, 0x03, 0x00, 0x3c, 0x10,
];
const SPS_SUB_LAYER_PROFILE_AND_LEVEL: &[u8] = &[
0x42, 0x01, 0x05, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d,
0xd0, 0x00, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5a, 0x5d,
0xa0, 0x02, 0x80, 0x80, 0x2d, 0x16, 0x51, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x02, 0x00, 0x00, 0x03, 0x00,
0x02, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x10,
];
#[test]
fn head_matches_the_full_parse() {
for nal in [fixtures::SPS, fixtures::SPS_PYRAMID, SPS_SUB_LAYERS] {
let sps = Sps::parse(nal).unwrap();
assert!(sps.full.is_some(), "scuffle_h265 accepts this SPS");
assert_eq!(SpsHead::parse(nal), Some(sps.head));
}
assert_eq!(SpsHead::parse(SPS_SUB_LAYERS).unwrap().max_sub_layers_minus1, 2);
}
#[test]
fn sub_layer_profile_and_level() {
let expected = SpsHead {
max_sub_layers_minus1: 2,
..SpsHead::parse(fixtures::SPS).unwrap()
};
assert_eq!(Sps::parse(SPS_SUB_LAYER_PROFILE_AND_LEVEL).unwrap().head, expected);
}
#[test]
fn zeroed_colour_description_falls_back_to_the_head() {
let sps = Sps::parse(fixtures::SPS_ZERO_COLOUR).unwrap();
assert!(sps.full.is_none(), "scuffle_h265 refuses this SPS");
assert_eq!(
sps.head,
SpsHead {
profile_space: 0,
tier_flag: false,
profile_idc: 1,
profile_compatibility_flags: [0x60, 0, 0, 0],
constraint_flags: [0; 6],
level_idc: 150,
max_sub_layers_minus1: 0,
temporal_id_nesting_flag: true,
chroma_format_idc: 1,
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
width: 1920,
height: 1080,
}
);
}
#[test]
fn truncated_or_invalid_head_is_still_an_error() {
assert!(Sps::parse(&fixtures::SPS[..8]).is_err());
assert!(Sps::parse(&fixtures::SPS_ZERO_COLOUR[..20]).is_err());
assert!(Sps::parse(&fixtures::SPS[..fixtures::SPS.len() - 6]).is_err());
for (index, byte) in [(1, 0x02), (2, 0x00)] {
let mut nal = fixtures::SPS_ZERO_COLOUR.to_vec();
nal[index] = byte;
assert!(Sps::parse(&nal).is_err());
}
assert_eq!(SpsHead::parse(fixtures::PPS), None);
assert_eq!(SpsHead::parse(fixtures::VPS), None);
}
}