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;
let window = &rbsp.conformance_window;
let crop = |size: u64, sub: u8, start: u64, end: u64| {
let offset = start.checked_add(end)?.checked_mul(sub.into())?;
u32::try_from(size.checked_sub(offset)?).ok().filter(|size| *size > 0)
};
let width = crop(
rbsp.pic_width_in_luma_samples.get(),
rbsp.sub_width_c(),
window.conf_win_left_offset,
window.conf_win_right_offset,
)
.ok_or(Error::SpsParse)?;
let height = crop(
rbsp.pic_height_in_luma_samples.get(),
rbsp.sub_height_c(),
window.conf_win_top_offset,
window.conf_win_bottom_offset,
)
.ok_or(Error::SpsParse)?;
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,
height,
})
}
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 empty_conformance_window_is_refused() {
let rbsp = SpsNALUnit::parse(&mut &fixtures::SPS[..]).unwrap().rbsp;
let width = rbsp.pic_width_in_luma_samples.get() / u64::from(rbsp.sub_width_c());
for right in [width, width + 1] {
let mut rbsp = rbsp.clone();
rbsp.conformance_window.conf_win_left_offset = 0;
rbsp.conformance_window.conf_win_right_offset = right;
assert!(SpsHead::from_rbsp(&rbsp).is_err());
}
let height = rbsp.pic_height_in_luma_samples.get() / u64::from(rbsp.sub_height_c());
let mut rbsp = rbsp.clone();
rbsp.conformance_window.conf_win_top_offset = 0;
rbsp.conformance_window.conf_win_bottom_offset = height;
assert!(SpsHead::from_rbsp(&rbsp).is_err());
}
#[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);
}
}