sidx 0.2.0

parse ISOBMFF sidx box
Documentation
use byteorder::{BigEndian as BE, ReadBytesExt, WriteBytesExt};
use std::io::{Cursor, Read, Write};
use std::time::Duration;
use std::ops::Range;

#[derive(thiserror::Error, Debug)]
pub enum Error {
	#[error("sidx box mismatch")] SidxBoxMismatch,
	#[error("byte read/write error: {0}")] ByteReadWriteError(#[from] std::io::Error),
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Sidx {
	pub version: u8,
	pub flags: u32,
	pub reference_id: u32,
	pub timescale: u32,
	pub earliest_presentation: u64,
	pub first_offset: u64,
	pub references: Vec<SidxEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReferenceType {
	Moof,
	Sidx,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SidxEntry {
	pub reference_type: ReferenceType,
	pub referenced_size: u32,
	pub subsegment_duration: u32,
	pub starts_with_sap: bool,
	pub sap_type: u8,
	pub sap_delta_time: u32,
}

impl Sidx {
	// all fields are named/commented appropriately, and are in parsing order, but it'd be cool to write a doc comment with a sidx breakdown later
	/// Parses ISO Base File Format (ISO/IEC 14496-12) qualified SIDX box
	///
	/// For strcture definition see 13.4 <https://www.etsi.org/deliver/etsi_ts/126200_126299/126244/10.02.00_60/ts_126244v100200p.pdf>
	/// For SAP type meaning see 4.5.2 <https://ptabdata.blob.core.windows.net/files/2020/IPR2020-01688/v67_EXHIBIT%201067%20-%20ISO-IEC%2023009-1%202019(E)%20-%20Info.%20Tech.%20-%20Dynamic%20Adaptive%20Streaming%20Over%20HTTP%20(DASH).pdf>
	#[fehler::throws]
	pub fn parse_box(media_box: &[u8]) -> Self {
		let mut reader = Cursor::new(media_box);
		let mut sidx = [0; 4];

		reader.read_u32::<BE>()?; // box size
		reader.read_exact(&mut sidx)?; // box type
		if &sidx != b"sidx" { fehler::throw!(Error::SidxBoxMismatch); }

		let version = reader.read_u8()?;
		let flags = reader.read_u24::<BE>()?;
		let reference_id = reader.read_u32::<BE>()?;
		let timescale = reader.read_u32::<BE>()?;
		let (earliest_presentation, first_offset) = if version == 0 {
			(u64::from(reader.read_u32::<BE>()?), u64::from(reader.read_u32::<BE>()?))
		} else {
			(reader.read_u64::<BE>()?, reader.read_u64::<BE>()?)
		};
		reader.read_u16::<BE>()?; // reserved space

		let reference_count = reader.read_u16::<BE>()?;
		let mut references: Vec<SidxEntry> = Vec::with_capacity(usize::from(reference_count));
		for _ in 0..reference_count {
			let reference_type_and_size = reader.read_u32::<BE>()?;
			let reference_type = if reference_type_and_size >> 31 == 0 { ReferenceType::Moof } else { ReferenceType::Sidx };
			let referenced_size = reference_type_and_size & 0x7fffffff;
			let subsegment_duration = reader.read_u32::<BE>()?;
			let starts_with_sap_and_sap_type_and_sap_delta_time = reader.read_u32::<BE>()?;
			// first bit
			let starts_with_sap = starts_with_sap_and_sap_type_and_sap_delta_time >> 31 == 1; // 0 = no; 1 = yes
			// bits 1 to 3
			let sap_type = ((starts_with_sap_and_sap_type_and_sap_delta_time >> 28) & 0b0111) as u8;
			// bits 4 to 32
			let sap_delta_time = starts_with_sap_and_sap_type_and_sap_delta_time & !(0b1111 << 28);

			references.push(SidxEntry { reference_type, referenced_size, subsegment_duration, starts_with_sap, sap_type, sap_delta_time });
		}

		Sidx { version, flags, reference_id, timescale, earliest_presentation, first_offset, references }
	}

	#[fehler::throws]
	pub fn build_box(&self) -> Vec<u8> {
		const BASE_SIZE: usize = 24;

		let size = BASE_SIZE + if self.version == 0 { 8 } else { 16 } + self.references.len() * 12;
		let mut writer = Cursor::new(Vec::with_capacity(size));
		writer.write_u32::<BE>(u32::try_from(size).unwrap())?;
		writer.write(b"sidx")?;
		writer.write_u8(self.version)?;
		writer.write_u24::<BE>(self.flags)?;
		writer.write_u32::<BE>(self.reference_id)?;
		writer.write_u32::<BE>(self.timescale)?;
		if self.version == 0 {
			writer.write_u32::<BE>(u32::try_from(self.earliest_presentation).unwrap())?;
			writer.write_u32::<BE>(u32::try_from(self.first_offset).unwrap())?;
		} else {
			writer.write_u64::<BE>(self.earliest_presentation)?;
			writer.write_u64::<BE>(self.first_offset)?;
		}
		writer.write_u16::<BE>(0)?; // reserved space
		writer.write_u16::<BE>(u16::try_from(self.references.len()).unwrap())?;
		for reference in &self.references {
			let reference_type_and_size = if reference.reference_type == ReferenceType::Moof { 0 } else { 1 << 31 } | reference.referenced_size;
			writer.write_u32::<BE>(reference_type_and_size)?;
			writer.write_u32::<BE>(reference.subsegment_duration)?;
			let starts_with_sap_and_sap_type_and_sap_delta_time = if reference.starts_with_sap { 1 << 31 } else { 0 } | (reference.sap_type as u32) << 28 | reference.sap_delta_time;
			writer.write_u32::<BE>(starts_with_sap_and_sap_type_and_sap_delta_time)?;
		}

		writer.into_inner()
	}

	/// Scan the SIDX and gets the `bytestart..byteend` range from a `time::Duration`.
	pub fn time_to_bytes(&self, time: &Range<Duration>) -> (Range<u64>, Range<Duration>) {
		let presentation_offset = self.earliest_presentation;

		let mut start_time = presentation_offset;
		let mut start_offset = 0;
		for entry in &self.references {
			if (start_time as f64 / self.timescale as f64) >= time.start.as_secs_f64() { break; }
			start_time += u64::from(entry.subsegment_duration);
			start_offset += u64::from(entry.referenced_size);
		}

		let mut end_time = presentation_offset;
		let mut end_offset = 0;
		for entry in &self.references {
			if (end_time as f64 / self.timescale as f64) > time.end.as_secs_f64() { break; }
			end_time += u64::from(entry.subsegment_duration);
			end_offset += u64::from(entry.referenced_size);
		}

		(start_offset..end_offset, Duration::from_secs_f64(start_time as f64 / self.timescale as f64)..Duration::from_secs_f64(end_time as f64 / self.timescale as f64))
	}
}