diff --git a/Cargo.toml b/Cargo.toml index bd5f479..ec35a0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mp4" -version = "0.13.0" +version = "0.14.0" authors = ["Alf "] edition = "2018" description = "MP4 reader and writer library in Rust." diff --git a/README.md b/README.md index e011773..91778ec 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ cargo add mp4 ``` or add to your `Cargo.toml`: ```toml -mp4 = "0.13.0" +mp4 = "0.14.0" ``` #### Documentation diff --git a/src/mp4box/avc1.rs b/src/mp4box/avc1.rs index b2a514f..990bcee 100644 --- a/src/mp4box/avc1.rs +++ b/src/mp4box/avc1.rs @@ -97,30 +97,37 @@ impl ReadBox<&mut R> for Avc1Box { let depth = reader.read_u16::()?; reader.read_i16::()?; // pre-defined - let header = BoxHeader::read(reader)?; - let BoxHeader { name, size: s } = header; - if s > size { - return Err(Error::InvalidData( - "avc1 box contains a box with a larger size than it", - )); - } - if name == BoxType::AvcCBox { - let avcc = AvcCBox::read_box(reader, s)?; + let end = start + size; + loop { + let current = reader.stream_position()?; + if current >= end { + return Err(Error::InvalidData("avcc not found")); + } + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "avc1 box contains a box with a larger size than it", + )); + } + if name == BoxType::AvcCBox { + let avcc = AvcCBox::read_box(reader, s)?; - skip_bytes_to(reader, start + size)?; + skip_bytes_to(reader, start + size)?; - Ok(Avc1Box { - data_reference_index, - width, - height, - horizresolution, - vertresolution, - frame_count, - depth, - avcc, - }) - } else { - Err(Error::InvalidData("avcc not found")) + return Ok(Avc1Box { + data_reference_index, + width, + height, + horizresolution, + vertresolution, + frame_count, + depth, + avcc, + }); + } else { + skip_bytes_to(reader, current + s)?; + } } } } diff --git a/src/mp4box/dinf.rs b/src/mp4box/dinf.rs index af6fbc2..e4bb153 100644 --- a/src/mp4box/dinf.rs +++ b/src/mp4box/dinf.rs @@ -246,22 +246,16 @@ impl ReadBox<&mut R> for UrlBox { let (version, flags) = read_box_header_ext(reader)?; - let location = if size.saturating_sub(HEADER_SIZE + HEADER_EXT_SIZE) > 0 { - let buf_size = size - HEADER_SIZE - HEADER_EXT_SIZE - 1; - let mut buf = vec![0u8; buf_size as usize]; - reader.read_exact(&mut buf)?; - match String::from_utf8(buf) { - Ok(t) => { - if t.len() != buf_size as usize { - return Err(Error::InvalidData("string too small")); - } - t - } - _ => String::default(), - } - } else { - String::default() - }; + let buf_size = size + .checked_sub(HEADER_SIZE + HEADER_EXT_SIZE) + .ok_or(Error::InvalidData("url size too small"))?; + + let mut buf = vec![0u8; buf_size as usize]; + reader.read_exact(&mut buf)?; + if let Some(end) = buf.iter().position(|&b| b == b'\0') { + buf.truncate(end); + } + let location = String::from_utf8(buf).unwrap_or_default(); skip_bytes_to(reader, start + size)?; diff --git a/src/mp4box/hdlr.rs b/src/mp4box/hdlr.rs index b5d9f5b..ceb8bee 100644 --- a/src/mp4box/hdlr.rs +++ b/src/mp4box/hdlr.rs @@ -49,20 +49,15 @@ impl ReadBox<&mut R> for HdlrBox { skip_bytes(reader, 12)?; // reserved let buf_size = size - .checked_sub(HEADER_SIZE + HEADER_EXT_SIZE + 20 + 1) + .checked_sub(HEADER_SIZE + HEADER_EXT_SIZE + 20) .ok_or(Error::InvalidData("hdlr size too small"))?; + let mut buf = vec![0u8; buf_size as usize]; reader.read_exact(&mut buf)?; - - let handler_string = match String::from_utf8(buf) { - Ok(t) => { - if t.len() != buf_size as usize { - return Err(Error::InvalidData("string too small")); - } - t - } - _ => String::from("null"), - }; + if let Some(end) = buf.iter().position(|&b| b == b'\0') { + buf.truncate(end); + } + let handler_string = String::from_utf8(buf).unwrap_or_default(); skip_bytes_to(reader, start + size)?; @@ -123,4 +118,52 @@ mod tests { let dst_box = HdlrBox::read_box(&mut reader, header.size).unwrap(); assert_eq!(src_box, dst_box); } + + #[test] + fn test_hdlr_empty() { + let src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::new(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::HdlrBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = HdlrBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_hdlr_extra() { + let real_src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::from("Good"), + }; + let src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::from_utf8(b"Good\0Bad".to_vec()).unwrap(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::HdlrBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = HdlrBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(real_src_box, dst_box); + } } diff --git a/src/mp4box/hev1.rs b/src/mp4box/hev1.rs index 4472add..e662e54 100644 --- a/src/mp4box/hev1.rs +++ b/src/mp4box/hev1.rs @@ -154,15 +154,33 @@ impl WriteBox<&mut W> for Hev1Box { } } -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)] pub struct HvcCBox { pub configuration_version: u8, + pub general_profile_space: u8, + pub general_tier_flag: bool, + pub general_profile_idc: u8, + pub general_profile_compatibility_flags: u32, + pub general_constraint_indicator_flag: u64, + pub general_level_idc: u8, + pub min_spatial_segmentation_idc: u16, + pub parallelism_type: u8, + pub chroma_format_idc: u8, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub avg_frame_rate: u16, + pub constant_frame_rate: u8, + pub num_temporal_layers: u8, + pub temporal_id_nested: bool, + pub length_size_minus_one: u8, + pub arrays: Vec, } impl HvcCBox { pub fn new() -> Self { Self { configuration_version: 1, + ..Default::default() } } } @@ -173,25 +191,118 @@ impl Mp4Box for HvcCBox { } fn box_size(&self) -> u64 { - HEADER_SIZE + 1 + HEADER_SIZE + + 23 + + self + .arrays + .iter() + .map(|a| 3 + a.nalus.iter().map(|x| 2 + x.data.len() as u64).sum::()) + .sum::() } fn summary(&self) -> Result { - let s = format!("configuration_version={}", self.configuration_version); - Ok(s) + Ok(format!("configuration_version={} general_profile_space={} general_tier_flag={} general_profile_idc={} general_profile_compatibility_flags={} general_constraint_indicator_flag={} general_level_idc={} min_spatial_segmentation_idc={} parallelism_type={} chroma_format_idc={} bit_depth_luma_minus8={} bit_depth_chroma_minus8={} avg_frame_rate={} constant_frame_rate={} num_temporal_layers={} temporal_id_nested={} length_size_minus_one={}", + self.configuration_version, + self.general_profile_space, + self.general_tier_flag, + self.general_profile_idc, + self.general_profile_compatibility_flags, + self.general_constraint_indicator_flag, + self.general_level_idc, + self.min_spatial_segmentation_idc, + self.parallelism_type, + self.chroma_format_idc, + self.bit_depth_luma_minus8, + self.bit_depth_chroma_minus8, + self.avg_frame_rate, + self.constant_frame_rate, + self.num_temporal_layers, + self.temporal_id_nested, + self.length_size_minus_one + )) } } +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct HvcCArrayNalu { + pub size: u16, + pub data: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct HvcCArray { + pub completeness: bool, + pub nal_unit_type: u8, + pub nalus: Vec, +} + impl ReadBox<&mut R> for HvcCBox { - fn read_box(reader: &mut R, size: u64) -> Result { - let start = box_start(reader)?; - + fn read_box(reader: &mut R, _size: u64) -> Result { let configuration_version = reader.read_u8()?; + let params = reader.read_u8()?; + let general_profile_space = params & 0b11000000 >> 6; + let general_tier_flag = (params & 0b00100000 >> 5) > 0; + let general_profile_idc = params & 0b00011111; - skip_bytes_to(reader, start + size)?; + let general_profile_compatibility_flags = reader.read_u32::()?; + let general_constraint_indicator_flag = reader.read_u48::()?; + let general_level_idc = reader.read_u8()?; + let min_spatial_segmentation_idc = reader.read_u16::()? & 0x0FFF; + let parallelism_type = reader.read_u8()? & 0b11; + let chroma_format_idc = reader.read_u8()? & 0b11; + let bit_depth_luma_minus8 = reader.read_u8()? & 0b111; + let bit_depth_chroma_minus8 = reader.read_u8()? & 0b111; + let avg_frame_rate = reader.read_u16::()?; + + let params = reader.read_u8()?; + let constant_frame_rate = params & 0b11000000 >> 6; + let num_temporal_layers = params & 0b00111000 >> 3; + let temporal_id_nested = (params & 0b00000100 >> 2) > 0; + let length_size_minus_one = params & 0b000011; + + let num_of_arrays = reader.read_u8()?; + + let mut arrays = Vec::with_capacity(num_of_arrays as _); + for _ in 0..num_of_arrays { + let params = reader.read_u8()?; + let num_nalus = reader.read_u16::()?; + let mut nalus = Vec::with_capacity(num_nalus as usize); + + for _ in 0..num_nalus { + let size = reader.read_u16::()?; + let mut data = vec![0; size as usize]; + + reader.read_exact(&mut data)?; + + nalus.push(HvcCArrayNalu { size, data }) + } + + arrays.push(HvcCArray { + completeness: (params & 0b10000000) > 0, + nal_unit_type: params & 0b111111, + nalus, + }); + } Ok(HvcCBox { configuration_version, + general_profile_space, + general_tier_flag, + general_profile_idc, + general_profile_compatibility_flags, + general_constraint_indicator_flag, + general_level_idc, + min_spatial_segmentation_idc, + parallelism_type, + chroma_format_idc, + bit_depth_luma_minus8, + bit_depth_chroma_minus8, + avg_frame_rate, + constant_frame_rate, + num_temporal_layers, + temporal_id_nested, + length_size_minus_one, + arrays, }) } } @@ -202,6 +313,40 @@ impl WriteBox<&mut W> for HvcCBox { BoxHeader::new(self.box_type(), size).write(writer)?; writer.write_u8(self.configuration_version)?; + let general_profile_space = (self.general_profile_space & 0b11) << 6; + let general_tier_flag = u8::from(self.general_tier_flag) << 5; + let general_profile_idc = self.general_profile_idc & 0b11111; + + writer.write_u8(general_profile_space | general_tier_flag | general_profile_idc)?; + writer.write_u32::(self.general_profile_compatibility_flags)?; + writer.write_u48::(self.general_constraint_indicator_flag)?; + writer.write_u8(self.general_level_idc)?; + + writer.write_u16::(self.min_spatial_segmentation_idc & 0x0FFF)?; + writer.write_u8(self.parallelism_type & 0b11)?; + writer.write_u8(self.chroma_format_idc & 0b11)?; + writer.write_u8(self.bit_depth_luma_minus8 & 0b111)?; + writer.write_u8(self.bit_depth_chroma_minus8 & 0b111)?; + writer.write_u16::(self.avg_frame_rate)?; + + let constant_frame_rate = (self.constant_frame_rate & 0b11) << 6; + let num_temporal_layers = (self.num_temporal_layers & 0b111) << 3; + let temporal_id_nested = u8::from(self.temporal_id_nested) << 2; + let length_size_minus_one = self.length_size_minus_one & 0b11; + writer.write_u8( + constant_frame_rate | num_temporal_layers | temporal_id_nested | length_size_minus_one, + )?; + writer.write_u8(self.arrays.len() as u8)?; + for arr in &self.arrays { + writer.write_u8((arr.nal_unit_type & 0b111111) | u8::from(arr.completeness) << 7)?; + writer.write_u16::(arr.nalus.len() as _)?; + + for nalu in &arr.nalus { + writer.write_u16::(nalu.size)?; + writer.write_all(&nalu.data)?; + } + } + Ok(size) } } @@ -224,6 +369,7 @@ mod tests { depth: 24, hvcc: HvcCBox { configuration_version: 1, + ..Default::default() }, }; let mut buf = Vec::new(); diff --git a/src/mp4box/meta.rs b/src/mp4box/meta.rs index ad44120..4fb51bc 100644 --- a/src/mp4box/meta.rs +++ b/src/mp4box/meta.rs @@ -21,7 +21,7 @@ pub enum MetaBox { hdlr: HdlrBox, #[serde(skip)] - data: Vec, + data: Vec<(BoxType, Vec)>, }, } @@ -41,7 +41,13 @@ impl MetaBox { size += ilst.box_size(); } } - Self::Unknown { hdlr, data } => size += hdlr.box_size() + data.len() as u64, + Self::Unknown { hdlr, data } => { + size += hdlr.box_size() + + data + .iter() + .map(|(_, data)| data.len() as u64 + HEADER_SIZE) + .sum::() + } } size } @@ -80,22 +86,56 @@ impl ReadBox<&mut R> for MetaBox { fn read_box(reader: &mut R, size: u64) -> Result { let start = box_start(reader)?; - let (version, _) = read_box_header_ext(reader)?; - if version != 0 { - return Err(Error::UnsupportedBoxVersion(BoxType::UdtaBox, version)); + let extended_header = reader.read_u32::()?; + if extended_header != 0 { + // ISO mp4 requires this header (version & flags) to be 0. Some + // files skip the extended header and directly start the hdlr box. + let possible_hdlr = BoxType::from(reader.read_u32::()?); + if possible_hdlr == BoxType::HdlrBox { + // This file skipped the extended header! Go back to start. + reader.seek(SeekFrom::Current(-8))?; + } else { + // Looks like we actually have a bad version number or flags. + let v = (extended_header >> 24) as u8; + return Err(Error::UnsupportedBoxVersion(BoxType::MetaBox, v)); + } } - let hdlr_header = BoxHeader::read(reader)?; - if hdlr_header.name != BoxType::HdlrBox { - return Err(Error::BoxNotFound(BoxType::HdlrBox)); - } - let hdlr = HdlrBox::read_box(reader, hdlr_header.size)?; - - let mut ilst = None; - let mut current = reader.stream_position()?; let end = start + size; + let content_start = current; + + // find the hdlr box + let mut hdlr = None; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::HdlrBox => { + hdlr = Some(HdlrBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + let Some(hdlr) = hdlr else { + return Err(Error::BoxNotFound(BoxType::HdlrBox)); + }; + + // rewind and handle the other boxes + reader.seek(SeekFrom::Start(content_start))?; + current = reader.stream_position()?; + + let mut ilst = None; + match hdlr.handler_type { MDIR => { while current < end { @@ -119,8 +159,27 @@ impl ReadBox<&mut R> for MetaBox { Ok(MetaBox::Mdir { ilst }) } _ => { - let mut data = vec![0u8; (end - current) as usize]; - reader.read_exact(&mut data)?; + let mut data = Vec::new(); + + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::HdlrBox => { + skip_box(reader, s)?; + } + _ => { + let mut box_data = vec![0; (s - HEADER_SIZE) as usize]; + reader.read_exact(&mut box_data)?; + + data.push((name, box_data)); + } + } + + current = reader.stream_position()?; + } Ok(MetaBox::Unknown { hdlr, data }) } @@ -150,7 +209,12 @@ impl WriteBox<&mut W> for MetaBox { ilst.write_box(writer)?; } } - Self::Unknown { data, .. } => writer.write_all(data)?, + Self::Unknown { data, .. } => { + for (box_type, data) in data { + BoxHeader::new(*box_type, data.len() as u64 + HEADER_SIZE).write(writer)?; + writer.write_all(data)?; + } + } } Ok(size) } @@ -198,16 +262,35 @@ mod tests { assert_eq!(dst_box, src_box); } + #[test] + fn test_meta_hdrl_non_first() { + let data = b"\x00\x00\x00\x7fmeta\x00\x00\x00\x00\x00\x00\x00Qilst\x00\x00\x00I\xa9too\x00\x00\x00Adata\x00\x00\x00\x01\x00\x00\x00\x00TMPGEnc Video Mastering Works 7 Version 7.0.15.17\x00\x00\x00\"hdlr\x00\x00\x00\x00\x00\x00\x00\x00mdirappl\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let mut reader = Cursor::new(data); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + + let meta_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + + // this contains \xa9too box in the ilst + // it designates the tool that created the file, but is not yet supported by this crate + assert_eq!( + meta_box, + MetaBox::Mdir { + ilst: Some(IlstBox::default()) + } + ); + } + #[test] fn test_meta_unknown() { let src_hdlr = HdlrBox { handler_type: FourCC::from(*b"test"), ..Default::default() }; - let src_data = b"123"; + let src_data = (BoxType::UnknownBox(0x42494241), b"123".to_vec()); let src_box = MetaBox::Unknown { hdlr: src_hdlr, - data: src_data.to_vec(), + data: vec![src_data], }; let mut buf = Vec::new(); diff --git a/src/mp4box/mod.rs b/src/mp4box/mod.rs index 3f85c3e..c4f041a 100644 --- a/src/mp4box/mod.rs +++ b/src/mp4box/mod.rs @@ -107,10 +107,49 @@ pub(crate) mod vmhd; pub(crate) mod vp09; pub(crate) mod vpcc; +pub use avc1::Avc1Box; +pub use co64::Co64Box; +pub use ctts::CttsBox; +pub use data::DataBox; +pub use dinf::DinfBox; +pub use edts::EdtsBox; +pub use elst::ElstBox; pub use emsg::EmsgBox; pub use ftyp::FtypBox; +pub use hdlr::HdlrBox; +pub use hev1::Hev1Box; +pub use ilst::IlstBox; +pub use mdhd::MdhdBox; +pub use mdia::MdiaBox; +pub use mehd::MehdBox; +pub use meta::MetaBox; +pub use mfhd::MfhdBox; +pub use minf::MinfBox; pub use moof::MoofBox; pub use moov::MoovBox; +pub use mp4a::Mp4aBox; +pub use mvex::MvexBox; +pub use mvhd::MvhdBox; +pub use smhd::SmhdBox; +pub use stbl::StblBox; +pub use stco::StcoBox; +pub use stsc::StscBox; +pub use stsd::StsdBox; +pub use stss::StssBox; +pub use stsz::StszBox; +pub use stts::SttsBox; +pub use tfdt::TfdtBox; +pub use tfhd::TfhdBox; +pub use tkhd::TkhdBox; +pub use traf::TrafBox; +pub use trak::TrakBox; +pub use trex::TrexBox; +pub use trun::TrunBox; +pub use tx3g::Tx3gBox; +pub use udta::UdtaBox; +pub use vmhd::VmhdBox; +pub use vp09::Vp09Box; +pub use vpcc::VpccBox; pub const HEADER_SIZE: u64 = 8; // const HEADER_LARGE_SIZE: u64 = 16; @@ -199,7 +238,8 @@ boxtype! { DayBox => 0xa9646179, CovrBox => 0x636f7672, DescBox => 0x64657363, - WideBox => 0x77696465 + WideBox => 0x77696465, + WaveBox => 0x77617665 } pub trait Mp4Box: Sized { diff --git a/src/mp4box/mp4a.rs b/src/mp4box/mp4a.rs index f8f411d..db1dd5c 100644 --- a/src/mp4box/mp4a.rs +++ b/src/mp4box/mp4a.rs @@ -78,16 +78,28 @@ impl ReadBox<&mut R> for Mp4aBox { reader.read_u32::()?; // reserved reader.read_u16::()?; // reserved let data_reference_index = reader.read_u16::()?; - - reader.read_u64::()?; // reserved + let version = reader.read_u16::()?; + reader.read_u16::()?; // reserved + reader.read_u32::()?; // reserved let channelcount = reader.read_u16::()?; let samplesize = reader.read_u16::()?; reader.read_u32::()?; // pre-defined, reserved let samplerate = FixedPointU16::new_raw(reader.read_u32::()?); + if version == 1 { + // Skip QTFF + reader.read_u64::()?; + reader.read_u64::()?; + } + + // Find esds in mp4a or wave let mut esds = None; - let current = reader.stream_position()?; - if current < start + size { + let end = start + size; + loop { + let current = reader.stream_position()?; + if current >= end { + break; + } let header = BoxHeader::read(reader)?; let BoxHeader { name, size: s } = header; if s > size { @@ -95,13 +107,20 @@ impl ReadBox<&mut R> for Mp4aBox { "mp4a box contains a box with a larger size than it", )); } - if name == BoxType::EsdsBox { esds = Some(EsdsBox::read_box(reader, s)?); + break; + } else if name == BoxType::WaveBox { + // Typically contains frma, mp4a, esds, and a terminator atom + } else { + // Skip boxes + let skip_to = current + s; + skip_bytes_to(reader, skip_to)?; } - skip_bytes_to(reader, start + size)?; } + skip_bytes_to(reader, end)?; + Ok(Mp4aBox { data_reference_index, channelcount, @@ -577,9 +596,9 @@ impl ReadDesc<&mut R> for SLConfigDescriptor { impl WriteDesc<&mut W> for SLConfigDescriptor { fn write_desc(&self, writer: &mut W) -> Result { let size = Self::desc_size(); - write_desc(writer, Self::desc_tag(), size - 1)?; + write_desc(writer, Self::desc_tag(), size)?; - writer.write_u8(0)?; // pre-defined + writer.write_u8(2)?; // pre-defined Ok(size) } } diff --git a/src/mp4box/stsc.rs b/src/mp4box/stsc.rs index 44834e4..4371c39 100644 --- a/src/mp4box/stsc.rs +++ b/src/mp4box/stsc.rs @@ -81,7 +81,7 @@ impl ReadBox<&mut R> for StscBox { let mut sample_id = 1; for i in 0..entry_count { let (first_chunk, samples_per_chunk) = { - let mut entry = entries.get_mut(i as usize).unwrap(); + let entry = entries.get_mut(i as usize).unwrap(); entry.first_sample = sample_id; (entry.first_chunk, entry.samples_per_chunk) }; diff --git a/src/mp4box/tfhd.rs b/src/mp4box/tfhd.rs index 10b8d86..19298a4 100644 --- a/src/mp4box/tfhd.rs +++ b/src/mp4box/tfhd.rs @@ -22,6 +22,8 @@ impl TfhdBox { pub const FLAG_DEFAULT_SAMPLE_DURATION: u32 = 0x08; pub const FLAG_DEFAULT_SAMPLE_SIZE: u32 = 0x10; pub const FLAG_DEFAULT_SAMPLE_FLAGS: u32 = 0x20; + pub const FLAG_DURATION_IS_EMPTY: u32 = 0x10000; + pub const FLAG_DEFAULT_BASE_IS_MOOF: u32 = 0x20000; pub fn get_type(&self) -> BoxType { BoxType::TfhdBox diff --git a/src/mp4box/traf.rs b/src/mp4box/traf.rs index 2c21290..6afc4d6 100644 --- a/src/mp4box/traf.rs +++ b/src/mp4box/traf.rs @@ -19,6 +19,9 @@ impl TrafBox { pub fn get_size(&self) -> u64 { let mut size = HEADER_SIZE; size += self.tfhd.box_size(); + if let Some(ref tfdt) = self.tfdt { + size += tfdt.box_size(); + } if let Some(ref trun) = self.trun { size += trun.box_size(); } @@ -100,6 +103,12 @@ impl WriteBox<&mut W> for TrafBox { BoxHeader::new(self.box_type(), size).write(writer)?; self.tfhd.write_box(writer)?; + if let Some(ref tfdt) = self.tfdt { + tfdt.write_box(writer)?; + } + if let Some(ref trun) = self.trun { + trun.write_box(writer)?; + } Ok(size) } diff --git a/src/reader.rs b/src/reader.rs index 88d86bf..3c9f6c4 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -24,6 +24,7 @@ impl Mp4Reader { let mut ftyp = None; let mut moov = None; let mut moofs = Vec::new(); + let mut moof_offsets = Vec::new(); let mut emsgs = Vec::new(); let mut current = start; @@ -57,8 +58,10 @@ impl Mp4Reader { moov = Some(MoovBox::read_box(&mut reader, s)?); } BoxType::MoofBox => { + let moof_offset = reader.stream_position()? - 8; let moof = MoofBox::read_box(&mut reader, s)?; moofs.push(moof); + moof_offsets.push(moof_offset); } BoxType::EmsgBox => { let emsg = EmsgBox::read_box(&mut reader, s)?; @@ -92,11 +95,12 @@ impl Mp4Reader { default_sample_duration = mvex.trex.default_sample_duration } - for moof in moofs.iter() { + for (moof, moof_offset) in moofs.iter().zip(moof_offsets) { for traf in moof.trafs.iter() { let track_id = traf.tfhd.track_id; if let Some(track) = tracks.get_mut(&track_id) { track.default_sample_duration = default_sample_duration; + track.moof_offsets.push(moof_offset); track.trafs.push(traf.clone()) } else { return Err(Error::TrakNotFound(track_id)); @@ -116,6 +120,92 @@ impl Mp4Reader { }) } + pub fn read_fragment_header( + &self, + mut reader: FR, + size: u64, + ) -> Result> { + let start = reader.stream_position()?; + + let mut moofs = Vec::new(); + let mut moof_offsets = Vec::new(); + + let mut current = start; + while current < size { + // Get box header. + let header = BoxHeader::read(&mut reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "file contains a box with a larger size than it", + )); + } + + // Break if size zero BoxHeader, which can result in dead-loop. + if s == 0 { + break; + } + + // Match and parse the atom boxes. + match name { + BoxType::MdatBox => { + skip_box(&mut reader, s)?; + } + BoxType::MoofBox => { + let moof_offset = reader.stream_position()? - 8; + let moof = MoofBox::read_box(&mut reader, s)?; + moofs.push(moof); + moof_offsets.push(moof_offset); + } + _ => { + // XXX warn!() + skip_box(&mut reader, s)?; + } + } + current = reader.stream_position()?; + } + + if moofs.is_empty() { + return Err(Error::BoxNotFound(BoxType::MoofBox)); + } + + let size = current - start; + let mut tracks: HashMap = self + .moov + .traks + .iter() + .map(|trak| (trak.tkhd.track_id, Mp4Track::from(trak))) + .collect(); + + let mut default_sample_duration = 0; + if let Some(ref mvex) = &self.moov.mvex { + default_sample_duration = mvex.trex.default_sample_duration + } + + for (moof, moof_offset) in moofs.iter().zip(moof_offsets) { + for traf in moof.trafs.iter() { + let track_id = traf.tfhd.track_id; + if let Some(track) = tracks.get_mut(&track_id) { + track.default_sample_duration = default_sample_duration; + track.moof_offsets.push(moof_offset); + track.trafs.push(traf.clone()) + } else { + return Err(Error::TrakNotFound(track_id)); + } + } + } + + Ok(Mp4Reader { + reader, + ftyp: self.ftyp.clone(), + moov: self.moov.clone(), + moofs, + emsgs: Vec::new(), + tracks, + size, + }) + } + pub fn size(&self) -> u64 { self.size } @@ -163,6 +253,14 @@ impl Mp4Reader { Err(Error::TrakNotFound(track_id)) } } + + pub fn sample_offset(&mut self, track_id: u32, sample_id: u32) -> Result { + if let Some(track) = self.tracks.get(&track_id) { + track.sample_offset(sample_id) + } else { + Err(Error::TrakNotFound(track_id)) + } + } } impl Mp4Reader { diff --git a/src/track.rs b/src/track.rs index 946eba4..9221f76 100644 --- a/src/track.rs +++ b/src/track.rs @@ -6,6 +6,7 @@ use std::time::Duration; use crate::mp4box::traf::TrafBox; use crate::mp4box::trak::TrakBox; +use crate::mp4box::trun::TrunBox; use crate::mp4box::{ avc1::Avc1Box, co64::Co64Box, ctts::CttsBox, ctts::CttsEntry, hev1::Hev1Box, mp4a::Mp4aBox, smhd::SmhdBox, stco::StcoBox, stsc::StscEntry, stss::StssBox, stts::SttsEntry, tx3g::Tx3gBox, @@ -92,6 +93,7 @@ impl From for TrackConfig { pub struct Mp4Track { pub trak: TrakBox, pub trafs: Vec, + pub moof_offsets: Vec, // Fragmented Tracks Defaults. pub default_sample_duration: u32, @@ -103,6 +105,7 @@ impl Mp4Track { Self { trak, trafs: Vec::new(), + moof_offsets: Vec::new(), default_sample_duration: 0, } } @@ -164,11 +167,11 @@ impl Mp4Track { } pub fn frame_rate(&self) -> f64 { - let dur_msec = self.duration().as_millis() as u64; - if dur_msec > 0 { - ((self.sample_count() as u64 * 1000) / dur_msec) as f64 - } else { + let dur = self.duration(); + if dur.is_zero() { 0.0 + } else { + self.sample_count() as f64 / dur.as_secs_f64() } } @@ -219,12 +222,12 @@ impl Mp4Track { } // mp4a.esds.es_desc.dec_config.avg_bitrate } else { - let dur_sec = self.duration().as_secs(); - if dur_sec > 0 { - let bitrate = self.total_sample_size() * 8 / dur_sec; - bitrate as u32 - } else { + let dur = self.duration(); + if dur.is_zero() { 0 + } else { + let bitrate = self.total_sample_size() as f64 * 8.0 / dur.as_secs_f64(); + bitrate as u32 } } } @@ -443,10 +446,34 @@ impl Mp4Track { } } - fn sample_offset(&self, sample_id: u32) -> Result { + pub fn sample_offset(&self, sample_id: u32) -> Result { if !self.trafs.is_empty() { - if let Some((traf_idx, _sample_idx)) = self.find_traf_idx_and_sample_idx(sample_id)? { - Ok(self.trafs[traf_idx].tfhd.base_data_offset.unwrap_or(0)) + if let Ok(Some((traf_idx, sample_idx))) = self.find_traf_idx_and_sample_idx(sample_id) { + let mut sample_offset = self.trafs[traf_idx] + .tfhd + .base_data_offset + .unwrap_or(self.moof_offsets[traf_idx]); + + if let Some(data_offset) = self.trafs[traf_idx] + .trun + .as_ref() + .and_then(|trun| trun.data_offset) + { + sample_offset = sample_offset.checked_add_signed(data_offset as i64).ok_or( + Error::InvalidData("attempt to calculate trun sample offset with overflow"), + )?; + } + + let first_sample_in_trun = sample_id - sample_idx as u32; + for i in first_sample_in_trun..sample_id { + sample_offset = sample_offset + .checked_add(self.sample_size(i)? as u64) + .ok_or(Error::InvalidData( + "attempt to calculate trun entry sample offset with overflow", + ))?; + } + + Ok(sample_offset) } else { Err(Error::BoxInTrafNotFound(self.track_id(), BoxType::TrafBox)) } @@ -488,15 +515,38 @@ impl Mp4Track { } fn sample_time(&self, sample_id: u32) -> Result<(u64, u32)> { - let stts = &self.trak.mdia.minf.stbl.stts; - - let mut sample_count: u32 = 1; - let mut elapsed = 0; - if !self.trafs.is_empty() { - let start_time = ((sample_id - 1) * self.default_sample_duration) as u64; - Ok((start_time, self.default_sample_duration)) + let mut base_start_time = 0; + let mut default_sample_duration = self.default_sample_duration; + if let Ok(Some((traf_idx, sample_idx))) = self.find_traf_idx_and_sample_idx(sample_id) { + let traf = &self.trafs[traf_idx]; + if let Some(tfdt) = &traf.tfdt { + base_start_time = tfdt.base_media_decode_time; + } + if let Some(duration) = traf.tfhd.default_sample_duration { + default_sample_duration = duration; + } + if let Some(trun) = &traf.trun { + if TrunBox::FLAG_SAMPLE_DURATION & trun.flags != 0 { + let mut start_offset = 0u64; + for duration in &trun.sample_durations[..sample_idx] { + start_offset = start_offset.checked_add(*duration as u64).ok_or( + Error::InvalidData("attempt to sum sample durations with overflow"), + )?; + } + let duration = trun.sample_durations[sample_idx]; + return Ok((base_start_time + start_offset, duration)); + } + } + } + let start_offset = ((sample_id - 1) * default_sample_duration) as u64; + Ok((base_start_time + start_offset, default_sample_duration)) } else { + let stts = &self.trak.mdia.minf.stbl.stts; + + let mut sample_count: u32 = 1; + let mut elapsed = 0; + for entry in stts.entries.iter() { let new_sample_count = sample_count @@ -523,7 +573,17 @@ impl Mp4Track { } fn sample_rendering_offset(&self, sample_id: u32) -> Result { - if let Some(ref ctts) = self.trak.mdia.minf.stbl.ctts { + if !self.trafs.is_empty() { + if let Ok(Some((traf_idx, sample_idx))) = self.find_traf_idx_and_sample_idx(sample_id) { + if let Some(cts) = self.trafs[traf_idx] + .trun + .as_ref() + .and_then(|trun| trun.sample_cts.get(sample_idx)) + { + return Ok(*cts as i32); + } + } + } else if let Some(ref ctts) = self.trak.mdia.minf.stbl.ctts { if let Ok((ctts_index, _)) = self.ctts_index(sample_id) { let ctts_entry = ctts.entries diff --git a/src/types.rs b/src/types.rs index 0983468..540f7fb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -657,15 +657,21 @@ pub fn creation_time(creation_time: u64) -> u64 { } } -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum DataType { - #[default] Binary = 0x000000, Text = 0x000001, Image = 0x00000D, TempoCpil = 0x000015, } +#[allow(clippy::derivable_impls)] +impl std::default::Default for DataType { + fn default() -> Self { + DataType::Binary + } +} + impl TryFrom for DataType { type Error = Error; fn try_from(value: u32) -> Result { diff --git a/tests/lib.rs b/tests/lib.rs index c24ce1e..7c81f95 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -99,8 +99,8 @@ fn test_read_mp4() { assert_eq!(track1.video_profile().unwrap(), AvcProfile::AvcHigh); assert_eq!(track1.width(), 320); assert_eq!(track1.height(), 240); - assert_eq!(track1.bitrate(), 0); // XXX - assert_eq!(track1.frame_rate(), 25.00); // XXX + assert_eq!(track1.bitrate(), 150200); + assert_eq!(track1.frame_rate(), 25.00); // track #2 let track2 = mp4.tracks().get(&2).unwrap(); @@ -176,3 +176,36 @@ fn test_read_metadata() { assert_eq!(poster.len(), want_poster.len()); assert_eq!(poster, want_poster.as_slice()); } + +#[test] +fn test_read_fragments() { + let mp4 = get_reader("tests/samples/minimal_init.mp4"); + + assert_eq!(692, mp4.size()); + assert_eq!(5, mp4.compatible_brands().len()); + + let sample_count = mp4.sample_count(1).unwrap(); + assert_eq!(sample_count, 0); + + let f = File::open("tests/samples/minimal_fragment.m4s").unwrap(); + let f_size = f.metadata().unwrap().len(); + let frag_reader = BufReader::new(f); + + let mut mp4_fragment = mp4.read_fragment_header(frag_reader, f_size).unwrap(); + let sample_count = mp4_fragment.sample_count(1).unwrap(); + assert_eq!(sample_count, 1); + let sample_1_1 = mp4_fragment.read_sample(1, 1).unwrap().unwrap(); + assert_eq!(sample_1_1.bytes.len(), 751); + assert_eq!( + sample_1_1, + mp4::Mp4Sample { + start_time: 0, + duration: 512, + rendering_offset: 0, + is_sync: true, + bytes: mp4::Bytes::from(vec![0x0u8; 751]), + } + ); + let eos = mp4_fragment.read_sample(1, 2); + assert!(eos.is_err()); +} diff --git a/tests/samples/minimal_fragment.m4s b/tests/samples/minimal_fragment.m4s new file mode 100644 index 0000000..25532bc Binary files /dev/null and b/tests/samples/minimal_fragment.m4s differ diff --git a/tests/samples/minimal_init.mp4 b/tests/samples/minimal_init.mp4 new file mode 100644 index 0000000..fcfe892 Binary files /dev/null and b/tests/samples/minimal_init.mp4 differ