use bytes::Bytes;
use scuffle_h265::SpsNALUnit;
use super::{Error, split::nal_unit_type};
use crate::Result;
use crate::catalog::hang::CatalogExt;
use crate::codec::annexb::NalIterator;
use crate::container::Frame;
pub struct Import<E: CatalogExt = ()> {
hvc1: bool,
track: crate::container::Producer<crate::catalog::hang::Container>,
rendition: crate::catalog::VideoTrack<E>,
catalog: crate::codec::video::Catalog,
last_sps: Option<Bytes>,
}
impl<E: CatalogExt> Import<E> {
pub fn new(
track: moq_net::track::Producer,
reserved: crate::catalog::Reserved<E>,
hint: crate::catalog::VideoHint,
) -> crate::Result<Self> {
let rendition = reserved.video(track.name());
let catalog = crate::codec::video::Catalog::new(&reserved, track.name(), hint)?;
let mut import = Self {
hvc1: false,
track: reserved
.producer()
.media_producer(track, crate::catalog::hang::Container::Legacy)?,
rendition,
catalog,
last_sps: None,
};
if let Some(config) = import.catalog.initial_config() {
import.apply_config(config);
}
Ok(import)
}
pub fn initialize(&mut self, buf: &[u8]) -> Result<()> {
if crate::codec::annexb::is_config_record(buf) {
self.initialize_hvc1(buf)
} else {
self.initialize_hev1(buf)
}
}
fn initialize_hvc1(&mut self, hvcc_bytes: &[u8]) -> Result<()> {
let config = super::config_from_hvcc(hvcc_bytes)?;
self.hvc1 = true;
self.apply_config(config);
Ok(())
}
fn initialize_hev1(&mut self, buf: &[u8]) -> Result<()> {
let mut scan = Bytes::copy_from_slice(buf);
let mut nals = NalIterator::new(&mut scan);
while let Some(nal) = nals.next().transpose()? {
if is_sps(&nal) {
self.configure_from_sps(&nal)?;
}
}
if let Some(nal) = nals.flush()?
&& is_sps(&nal)
{
self.configure_from_sps(&nal)?;
}
Ok(())
}
pub fn name(&self) -> &str {
self.track.name()
}
pub fn demand(&self) -> moq_net::track::Demand {
self.track.track().demand()
}
pub fn finish(&mut self) -> Result<()> {
self.rendition.record_group_end(None);
self.track.finish()?;
Ok(())
}
pub fn abort(self, err: moq_net::Error) {
self.track.abort(err);
}
pub fn cut(&mut self, end: Option<moq_net::Timestamp>) -> Result<()> {
self.rendition.record_group_end(end);
self.track.cut(end)?;
Ok(())
}
pub fn seek(&mut self, sequence: u64) -> Result<()> {
self.rendition.record_group_end(None);
self.track.seek(sequence)?;
Ok(())
}
pub fn observe_reorder(&mut self, reorder: moq_net::Timestamp) {
self.rendition.record_reorder(reorder);
}
fn configure_from_sps(&mut self, sps_nal: &Bytes) -> Result<()> {
if self.last_sps.as_ref() == Some(sps_nal) {
return Ok(());
}
let sps = SpsNALUnit::parse(&mut &sps_nal[..]).map_err(|_| Error::SpsParse)?;
let profile = &sps.rbsp.profile_tier_level.general_profile;
let vui_data = sps.rbsp.vui_parameters.as_ref().map(VuiData::new).unwrap_or_default();
let mut config = hang::catalog::VideoConfig::new(hang::catalog::H265 {
in_band: true, 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: super::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.framerate = vui_data.framerate;
config.display_aspect_width = vui_data.display_ratio_width;
config.display_aspect_height = vui_data.display_ratio_height;
config.container = hang::catalog::Container::Legacy;
self.last_sps = Some(sps_nal.clone());
self.apply_config(config);
Ok(())
}
fn apply_config(&mut self, config: hang::catalog::VideoConfig) {
self.catalog.publish(&mut self.rendition, config);
}
fn write_frames(&mut self, frames: impl IntoIterator<Item = Frame>) -> Result<()> {
for frame in frames {
if !self.hvc1
&& frame.keyframe
&& let Some(sps) = find_sps(&frame.payload)
{
self.configure_from_sps(&sps)?;
}
if frame.keyframe && !self.catalog.configured() {
return Err(Error::MissingSps.into());
}
if frame.keyframe {
self.rendition.record_group_end(Some(frame.timestamp));
}
let pts = frame.timestamp;
let bytes = frame.payload.len();
self.track.write(frame)?;
self.rendition.record_frame(pts, bytes);
}
Ok(())
}
pub fn decode(&mut self, frames: impl IntoIterator<Item = Frame>) -> Result<()> {
self.write_frames(frames)
}
}
fn is_sps(nal: &[u8]) -> bool {
nal.first()
.is_some_and(|h| nal_unit_type(*h) == scuffle_h265::NALUnitType::SpsNut)
}
fn find_sps(payload: &[u8]) -> Option<Bytes> {
let mut buf = Bytes::copy_from_slice(payload);
let mut nals = NalIterator::new(&mut buf);
while let Some(Ok(nal)) = nals.next() {
if is_sps(&nal) {
return Some(nal);
}
}
nals.flush().ok().flatten().filter(|nal| is_sps(nal))
}
#[derive(Default)]
struct VuiData {
framerate: Option<f64>,
display_ratio_width: Option<u32>,
display_ratio_height: Option<u32>,
}
impl VuiData {
fn new(vui: &scuffle_h265::VuiParameters) -> Self {
let framerate = vui
.vui_timing_info
.as_ref()
.map(|t| t.time_scale.get() as f64 / t.num_units_in_tick.get() as f64);
let (display_ratio_width, display_ratio_height) = match &vui.aspect_ratio_info {
scuffle_h265::AspectRatioInfo::ExtendedSar { sar_width, sar_height } => {
(Some(*sar_width as u32), Some(*sar_height as u32))
}
scuffle_h265::AspectRatioInfo::Predefined(idc) => aspect_ratio_from_idc(*idc)
.map(|(w, h)| (Some(w), Some(h)))
.unwrap_or((None, None)),
};
VuiData {
framerate,
display_ratio_width,
display_ratio_height,
}
}
}
fn aspect_ratio_from_idc(idc: scuffle_h265::AspectRatioIdc) -> Option<(u32, u32)> {
match idc {
scuffle_h265::AspectRatioIdc::Unspecified => None,
scuffle_h265::AspectRatioIdc::Square => Some((1, 1)),
scuffle_h265::AspectRatioIdc::Aspect12_11 => Some((12, 11)),
scuffle_h265::AspectRatioIdc::Aspect10_11 => Some((10, 11)),
scuffle_h265::AspectRatioIdc::Aspect16_11 => Some((16, 11)),
scuffle_h265::AspectRatioIdc::Aspect40_33 => Some((40, 33)),
scuffle_h265::AspectRatioIdc::Aspect24_11 => Some((24, 11)),
scuffle_h265::AspectRatioIdc::Aspect20_11 => Some((20, 11)),
scuffle_h265::AspectRatioIdc::Aspect32_11 => Some((32, 11)),
scuffle_h265::AspectRatioIdc::Aspect80_33 => Some((80, 33)),
scuffle_h265::AspectRatioIdc::Aspect18_11 => Some((18, 11)),
scuffle_h265::AspectRatioIdc::Aspect15_11 => Some((15, 11)),
scuffle_h265::AspectRatioIdc::Aspect64_33 => Some((64, 33)),
scuffle_h265::AspectRatioIdc::Aspect160_99 => Some((160, 99)),
scuffle_h265::AspectRatioIdc::Aspect4_3 => Some((4, 3)),
scuffle_h265::AspectRatioIdc::Aspect3_2 => Some((3, 2)),
scuffle_h265::AspectRatioIdc::Aspect2_1 => Some((2, 1)),
scuffle_h265::AspectRatioIdc::ExtendedSar => None,
_ => None, }
}
#[cfg(test)]
mod tests {
use bytes::BytesMut;
use super::*;
use crate::codec::h265::{Split, fixtures};
fn setup(name: &str) -> (moq_net::track::Producer, crate::catalog::Producer) {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap();
let track = broadcast.create_track(name, hang::container::track_info()).unwrap();
(track, catalog)
}
#[tokio::test(start_paused = true)]
async fn initialize_hvcc_lands_in_catalog() {
let hvcc = fixtures::hvcc();
let (track, catalog) = setup("video");
let mut import = Import::new(track, catalog.reserve(), Default::default()).unwrap();
let buf = BytesMut::from(hvcc.as_ref());
import.initialize(&buf).expect("initialize hvcC");
assert_eq!(buf.len(), hvcc.len(), "initialize must not consume the buffer");
let snapshot = catalog.snapshot();
let cfg = snapshot.video.renditions.get("video").expect("rendition");
let hang::catalog::VideoCodec::H265(h265) = &cfg.codec else {
panic!("expected H.265 codec")
};
assert!(!h265.in_band, "hvc1 source should land as in_band=false");
assert_eq!(cfg.coded_width, Some(1280));
assert_eq!(cfg.coded_height, Some(720));
assert_eq!(cfg.description.as_deref(), Some(hvcc.as_ref()));
}
#[tokio::test(start_paused = true)]
async fn hvc1_length_prefix_is_not_scanned_as_annexb() {
let hvcc = fixtures::hvcc();
let (track, catalog) = setup("video");
let mut import = Import::new(track, catalog.reserve(), Default::default()).unwrap();
import.initialize(&hvcc).expect("initialize hvcC");
let mut nal = vec![0x26, 0x01, 0x80, 0xaa]; nal.resize(322, 0x80);
let mut au = Vec::new();
au.extend_from_slice(&(nal.len() as u32).to_be_bytes());
au.extend_from_slice(&nal);
assert_eq!(
&au[..4],
&[0, 0, 1, 0x42],
"the length prefix must look like a start code"
);
let pts = moq_net::Timestamp::from_micros(0).unwrap();
let frame = crate::codec::h265::hvc1_frame(&au, 4, pts).unwrap();
assert!(frame.keyframe);
import.decode([frame]).expect("hvc1 keyframe");
let snapshot = catalog.snapshot();
let cfg = snapshot.video.renditions.get("video").expect("rendition");
let hang::catalog::VideoCodec::H265(h265) = &cfg.codec else {
panic!("expected H.265 codec")
};
assert!(!h265.in_band);
assert_eq!(cfg.description.as_deref(), Some(hvcc.as_ref()));
}
#[tokio::test(start_paused = true)]
async fn hev1_self_initializes_from_first_keyframe() {
let idr: &[u8] = &[0x26, 0x01, 0x80, 0xaa]; let mut annexb = BytesMut::new();
for nal in [fixtures::VPS, fixtures::SPS, fixtures::PPS, idr] {
annexb.extend_from_slice(&[0, 0, 0, 1]);
annexb.extend_from_slice(nal);
}
let mut split = Split::new();
let (track, catalog) = setup("video");
let mut import = Import::new(track, catalog.reserve(), Default::default()).unwrap();
assert!(
catalog.snapshot().video.renditions.is_empty(),
"no config before any frame"
);
let pts = moq_net::Timestamp::from_micros(0).unwrap();
let mut frames = split.decode(&annexb, pts).expect("split keyframe");
frames.extend(split.flush(pts).expect("flush keyframe"));
import.decode(frames).expect("decode keyframe");
let snapshot = catalog.snapshot();
let cfg = snapshot.video.renditions.get("video").expect("rendition");
let hang::catalog::VideoCodec::H265(h265) = &cfg.codec else {
panic!("expected H.265 codec")
};
assert!(h265.in_band, "hev1 source should land as in_band=true");
assert!(cfg.description.is_none(), "hev1 has no out-of-band description");
assert_eq!(cfg.coded_width, Some(1280));
assert_eq!(cfg.coded_height, Some(720));
}
}