mod export;
mod import;
mod split;
pub use export::*;
pub use import::*;
pub use split::*;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use scuffle_h265::{NALUnitType, SpsNALUnit};
pub(crate) fn hvc1_frame(
data: impl moq_net::IntoBytes,
length_size: usize,
pts: moq_net::Timestamp,
) -> crate::Result<crate::container::Frame> {
let keyframe = hvc1_is_keyframe(data.as_ref(), length_size);
Ok(crate::container::Frame {
timestamp: pts,
payload: data.into_bytes(),
keyframe,
duration: None,
})
}
fn hvc1_is_keyframe(data: &[u8], length_size: usize) -> bool {
let Ok(nals) = crate::codec::annexb::length_prefixed_nals(data, length_size) else {
return false;
};
nals.map_while(std::result::Result::ok)
.any(|nal| nal.first().is_some_and(|header| is_irap(split::nal_unit_type(*header))))
}
pub(crate) fn is_irap(nal_type: NALUnitType) -> bool {
matches!(
nal_type,
NALUnitType::IdrWRadl
| NALUnitType::IdrNLp
| NALUnitType::BlaNLp
| NALUnitType::BlaWRadl
| NALUnitType::BlaWLp
| NALUnitType::CraNut
)
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("NAL unit is too short")]
NalTooShort,
#[error("{0} too large for hvcC length field ({1} > {max})", max = u16::MAX)]
NalTooLargeForHvcc(&'static str, usize),
#[error("too many {0} for hvcC ({1} > {max})", max = u16::MAX)]
TooManyNals(&'static str, usize),
#[error("NAL too large for 4-byte length prefix")]
NalTooLarge,
#[error("failed to parse SPS NAL unit")]
SpsParse,
#[error("missing level_idc in SPS")]
MissingLevelIdc,
#[error("forbidden zero bit is not zero")]
ForbiddenZeroBit,
#[error("not initialized")]
NotInitialized,
#[error("expected SPS before any frames")]
MissingSps,
#[error("missing timestamp")]
MissingTimestamp,
#[error("HEVCDecoderConfigurationRecord too short")]
HvccTooShort,
#[error("HEVCDecoderConfigurationRecord truncated")]
HvccTruncated,
#[error("hvc1 description for rendition {name:?} is missing VPS, SPS, or PPS (vps={vps}, sps={sps}, pps={pps})")]
MissingParamSets {
name: String,
vps: usize,
sps: usize,
pps: usize,
},
#[error("annexb: {0}")]
Annexb(#[from] crate::codec::annexb::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Hvcc {
pub length_size: usize,
pub vps: Vec<Bytes>,
pub sps: Vec<Bytes>,
pub pps: Vec<Bytes>,
}
impl Hvcc {
pub fn parse(hvcc: &[u8]) -> Result<Self> {
if hvcc.len() < 23 {
return Err(Error::HvccTooShort);
}
let length_size = (hvcc[21] & 0x3) as usize + 1;
let num_arrays = hvcc[22] as usize;
let mut vps = Vec::new();
let mut sps = Vec::new();
let mut pps = Vec::new();
let mut pos: usize = 23;
for _ in 0..num_arrays {
let after_hdr = pos.checked_add(3).ok_or(Error::HvccTruncated)?;
if hvcc.len() < after_hdr {
return Err(Error::HvccTruncated);
}
let nal_type = hvcc[pos] & 0x3f;
let num_nalus = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]) as usize;
pos = after_hdr;
for _ in 0..num_nalus {
let after_len = pos.checked_add(2).ok_or(Error::HvccTruncated)?;
if hvcc.len() < after_len {
return Err(Error::HvccTruncated);
}
let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
let after_nal = after_len.checked_add(len).ok_or(Error::HvccTruncated)?;
if hvcc.len() < after_nal {
return Err(Error::HvccTruncated);
}
let bytes = Bytes::copy_from_slice(&hvcc[after_len..after_nal]);
pos = after_nal;
match NALUnitType::from(nal_type) {
NALUnitType::VpsNut => vps.push(bytes),
NALUnitType::SpsNut => sps.push(bytes),
NALUnitType::PpsNut => pps.push(bytes),
_ => {}
}
}
}
Ok(Self {
length_size,
vps,
sps,
pps,
})
}
}
pub(crate) fn config_from_hvcc(hvcc: &[u8]) -> Result<hang::catalog::VideoConfig> {
let params = Hvcc::parse(hvcc)?;
let sps_nal = params.sps.first().ok_or(Error::MissingSps)?;
let sps = SpsNALUnit::parse(&mut &sps_nal[..]).map_err(|_| Error::SpsParse)?;
let profile = &sps.rbsp.profile_tier_level.general_profile;
let mut config = hang::catalog::VideoConfig::new(hang::catalog::H265 {
in_band: false,
profile_space: profile.profile_space,
profile_idc: profile.profile_idc,
profile_compatibility_flags: profile.profile_compatibility_flag.bits().to_be_bytes(),
tier_flag: profile.tier_flag,
level_idc: profile.level_idc.ok_or(Error::MissingLevelIdc)?,
constraint_flags: pack_constraint_flags(profile),
});
config.coded_width = Some(sps.rbsp.cropped_width() as u32);
config.coded_height = Some(sps.rbsp.cropped_height() as u32);
config.description = Some(Bytes::copy_from_slice(hvcc));
config.container = hang::catalog::Container::Legacy;
Ok(config)
}
pub struct Hvc1 {
hvcc: Option<Bytes>,
vps: Vec<Bytes>,
sps: Vec<Bytes>,
pps: Vec<Bytes>,
}
impl Default for Hvc1 {
fn default() -> Self {
Self::new()
}
}
impl Hvc1 {
pub fn new() -> Self {
Self {
hvcc: None,
vps: Vec::new(),
sps: Vec::new(),
pps: Vec::new(),
}
}
pub fn hvcc(&self) -> Option<&Bytes> {
self.hvcc.as_ref()
}
pub fn transform(&mut self, payload: Bytes) -> Result<Option<Bytes>> {
let mut buf = payload.clone();
let mut nal_iter = crate::codec::annexb::NalIterator::new(&mut buf);
let mut out = BytesMut::with_capacity(payload.remaining());
let mut frame_vps: Vec<Bytes> = Vec::new();
let mut frame_sps: Vec<Bytes> = Vec::new();
let mut frame_pps: Vec<Bytes> = Vec::new();
let mut emitted_any_slice = false;
loop {
let nal = match nal_iter.next() {
Some(Ok(n)) => n,
Some(Err(e)) => return Err(e.into()),
None => break,
};
if process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)? {
emitted_any_slice = true;
}
}
if let Some(nal) = nal_iter.flush()?
&& process_nal(&nal, &mut out, &mut frame_vps, &mut frame_sps, &mut frame_pps)?
{
emitted_any_slice = true;
}
let mut changed = false;
if !frame_vps.is_empty() && frame_vps != self.vps {
self.vps = frame_vps;
changed = true;
}
if !frame_sps.is_empty() && frame_sps != self.sps {
self.sps = frame_sps;
changed = true;
}
if !frame_pps.is_empty() && frame_pps != self.pps {
self.pps = frame_pps;
changed = true;
}
if changed {
self.rebuild_hvcc()?;
}
if !emitted_any_slice {
return Ok(None);
}
Ok(Some(out.freeze()))
}
fn rebuild_hvcc(&mut self) -> Result<()> {
if self.vps.is_empty() || self.sps.is_empty() || self.pps.is_empty() {
return Ok(());
}
self.hvcc = Some(build_hvcc(&self.vps, &self.sps, &self.pps)?);
Ok(())
}
}
fn process_nal(
nal: &Bytes,
out: &mut BytesMut,
frame_vps: &mut Vec<Bytes>,
frame_sps: &mut Vec<Bytes>,
frame_pps: &mut Vec<Bytes>,
) -> Result<bool> {
if nal.is_empty() {
return Ok(false);
}
match NALUnitType::from((nal[0] >> 1) & 0x3f) {
NALUnitType::VpsNut => {
crate::codec::annexb::push_distinct(frame_vps, nal);
Ok(false)
}
NALUnitType::SpsNut => {
crate::codec::annexb::push_distinct(frame_sps, nal);
Ok(false)
}
NALUnitType::PpsNut => {
crate::codec::annexb::push_distinct(frame_pps, nal);
Ok(false)
}
_ => {
let len = u32::try_from(nal.len()).map_err(|_| Error::NalTooLarge)?;
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(nal);
Ok(true)
}
}
}
pub(crate) fn build_hvcc(vps_nals: &[Bytes], sps_nals: &[Bytes], pps_nals: &[Bytes]) -> Result<Bytes> {
let first_sps = sps_nals.first().ok_or(Error::MissingSps)?;
for (label, nals) in [("VPS", vps_nals), ("SPS", sps_nals), ("PPS", pps_nals)] {
if nals.len() > u16::MAX as usize {
return Err(Error::TooManyNals(label, nals.len()));
}
for nal in nals {
if nal.len() > u16::MAX as usize {
return Err(Error::NalTooLargeForHvcc(label, nal.len()));
}
}
}
let sps = SpsNALUnit::parse(&mut &first_sps[..]).map_err(|_| Error::SpsParse)?;
let profile = &sps.rbsp.profile_tier_level.general_profile;
let level_idc = profile.level_idc.ok_or(Error::MissingLevelIdc)?;
let constraint_flags = pack_constraint_flags(profile);
let compat = profile.profile_compatibility_flag.bits().to_be_bytes();
let num_temporal_layers = sps.rbsp.sps_max_sub_layers_minus1 + 1;
let params_len: usize = vps_nals
.iter()
.chain(sps_nals)
.chain(pps_nals)
.map(|n| 2 + n.len())
.sum();
let mut out = BytesMut::with_capacity(23 + 3 * 3 + params_len);
out.put_u8(1); out.put_u8(((profile.profile_space & 0x3) << 6) | ((profile.tier_flag as u8) << 5) | (profile.profile_idc & 0x1f));
out.put_slice(&compat);
out.put_slice(&constraint_flags);
out.put_u8(level_idc);
out.put_u16(0xf000); out.put_u8(0xfc); out.put_u8(0xfc | (sps.rbsp.chroma_format_idc & 0x3));
out.put_u8(0xf8 | (sps.rbsp.bit_depth_luma_minus8 & 0x7));
out.put_u8(0xf8 | (sps.rbsp.bit_depth_chroma_minus8 & 0x7));
out.put_u16(0); out.put_u8(((num_temporal_layers & 0x7) << 3) | ((sps.rbsp.sps_temporal_id_nesting_flag as u8) << 2) | 0x3);
out.put_u8(3);
for (nal_type, nals) in [
(u8::from(NALUnitType::VpsNut), vps_nals),
(u8::from(NALUnitType::SpsNut), sps_nals),
(u8::from(NALUnitType::PpsNut), pps_nals),
] {
out.put_u8(0x80 | (nal_type & 0x3f)); out.put_u16(nals.len() as u16); for nal in nals {
out.put_u16(nal.len() as u16);
out.put_slice(nal);
}
}
Ok(out.freeze())
}
pub(crate) fn hvcc_params(hvcc: &[u8]) -> anyhow::Result<(usize, Vec<Bytes>)> {
anyhow::ensure!(hvcc.len() >= 23, "HEVCDecoderConfigurationRecord too short");
let length_size = (hvcc[21] & 0x03) as usize + 1;
let num_arrays = hvcc[22];
let mut params = Vec::new();
let mut pos = 23;
for _ in 0..num_arrays {
anyhow::ensure!(hvcc.len() >= pos + 3, "truncated hvcC NAL array header");
pos += 1;
let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]);
pos += 2;
for _ in 0..num_nalus {
anyhow::ensure!(hvcc.len() >= pos + 2, "truncated hvcC NAL length");
let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
pos += 2;
anyhow::ensure!(hvcc.len() >= pos + len, "hvcC NAL exceeds buffer");
params.push(Bytes::copy_from_slice(&hvcc[pos..pos + len]));
pos += len;
}
}
Ok((length_size, params))
}
pub(crate) fn pack_constraint_flags(profile: &scuffle_h265::Profile) -> [u8; 6] {
let mut flags = [0u8; 6];
flags[0] = ((profile.progressive_source_flag as u8) << 7)
| ((profile.interlaced_source_flag as u8) << 6)
| ((profile.non_packed_constraint_flag as u8) << 5)
| ((profile.frame_only_constraint_flag as u8) << 4);
flags
}
#[cfg(test)]
pub(crate) mod fixtures {
use bytes::Bytes;
pub(crate) const VPS: &[u8] = &[
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00,
0x03, 0x00, 0x5d, 0x95, 0x98, 0x09,
];
pub(crate) const SPS: &[u8] = &[
0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d,
0xa0, 0x02, 0x80, 0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x02, 0x00, 0x00, 0x03, 0x00,
0x02, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x10,
];
pub(crate) const PPS: &[u8] = &[0x44, 0x01, 0xc1, 0x72, 0xb4, 0x62, 0x40];
pub(crate) fn hvcc() -> Bytes {
super::build_hvcc(
&[Bytes::from_static(VPS)],
&[Bytes::from_static(SPS)],
&[Bytes::from_static(PPS)],
)
.expect("real parameter sets must build an hvcC")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn length_prefixed(nals: &[&[u8]]) -> Vec<u8> {
let mut au = Vec::new();
for nal in nals {
au.extend_from_slice(&(nal.len() as u32).to_be_bytes());
au.extend_from_slice(nal);
}
au
}
fn ts() -> moq_net::Timestamp {
moq_net::Timestamp::from_micros(0).unwrap()
}
#[test]
fn hvc1_frame_keyframe() {
let sei: &[u8] = &[0x4e, 0x01, 0x05, 0xff]; let idr: &[u8] = &[0x26, 0x01, 0x80, 0xaa]; let au = length_prefixed(&[sei, idr]);
let frame = hvc1_frame(&au, 4, ts()).unwrap();
assert!(frame.keyframe);
assert_eq!(frame.payload, au);
}
#[test]
fn hvc1_frame_cra_keyframe() {
let cra: &[u8] = &[0x2a, 0x01, 0x80, 0x55]; let au = length_prefixed(&[cra]);
let frame = hvc1_frame(&au, 4, ts()).unwrap();
assert!(frame.keyframe);
}
#[test]
fn hvc1_frame_delta() {
let trail: &[u8] = &[0x02, 0x01, 0x80, 0x33]; let au = length_prefixed(&[trail]);
let frame = hvc1_frame(&au, 4, ts()).unwrap();
assert!(!frame.keyframe);
}
#[test]
fn hvc1_frame_tsa_delta_not_h264_idr() {
let tsa: &[u8] = &[0x05, 0x01, 0x80, 0x33]; let au = length_prefixed(&[tsa]);
let frame = hvc1_frame(&au, 4, ts()).unwrap();
assert!(!frame.keyframe);
}
#[test]
fn config_from_hvcc_resolves_real_sps() {
let hvcc = fixtures::hvcc();
let config = config_from_hvcc(&hvcc).unwrap();
let hang::catalog::VideoCodec::H265(h265) = &config.codec else {
panic!("expected H.265 codec")
};
assert!(!h265.in_band, "hvcC config is out-of-band");
assert_eq!(config.coded_width, Some(1280));
assert_eq!(config.coded_height, Some(720));
assert_eq!(config.description.as_deref(), Some(hvcc.as_ref()));
}
#[test]
fn hvcc_params_parses_vps_sps_pps() {
let vps = &[0x40, 0x01, 0x0c][..]; let sps = &[0x42, 0x01, 0x01, 0x60][..]; let pps = &[0x44, 0x01, 0xc0][..];
let mut hvcc = BytesMut::new();
hvcc.extend_from_slice(&[0u8; 21]); hvcc.put_u8(0xfc | 0x03); hvcc.put_u8(3); for (nal_type, nal) in [
(u8::from(NALUnitType::VpsNut), vps),
(u8::from(NALUnitType::SpsNut), sps),
(u8::from(NALUnitType::PpsNut), pps),
] {
hvcc.put_u8(0x80 | (nal_type & 0x3f));
hvcc.put_u16(1); hvcc.put_u16(nal.len() as u16);
hvcc.put_slice(nal);
}
let (length_size, params) = hvcc_params(&hvcc).unwrap();
assert_eq!(length_size, 4);
assert_eq!(params.len(), 3);
assert_eq!(params[0].as_ref(), vps);
assert_eq!(params[1].as_ref(), sps);
assert_eq!(params[2].as_ref(), pps);
}
}