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/lib.rs b/src/lib.rs index 06658cf..92319e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ //! ``` //! //! [mp4box]: https://github.com/alfg/mp4-rust/blob/master/src/mp4box/mod.rs -//! [examples]: https://github.com/alfg/mp4-rust/blob/master/src/examples +//! [examples]: https://github.com/alfg/mp4-rust/tree/master/examples #![doc(html_root_url = "https://docs.rs/mp4/*")] use std::fs::File; diff --git a/src/mp4box/meta.rs b/src/mp4box/meta.rs index df57c77..56ca816 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 } @@ -84,22 +90,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 { @@ -123,8 +163,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 }) } @@ -154,7 +213,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) } @@ -202,16 +266,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 15be3b1..4bbdd41 100644 --- a/src/mp4box/mod.rs +++ b/src/mp4box/mod.rs @@ -106,10 +106,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; diff --git a/src/mp4box/mp4a.rs b/src/mp4box/mp4a.rs index e5e8666..a80c6c4 100644 --- a/src/mp4box/mp4a.rs +++ b/src/mp4box/mp4a.rs @@ -604,9 +604,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 e5b444e..a2b034b 100644 --- a/src/mp4box/stsc.rs +++ b/src/mp4box/stsc.rs @@ -85,7 +85,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 e67e5cb..5b529e6 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 51f812d..d53d713 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(); } @@ -104,6 +107,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 dfff001..09ff288 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)?; @@ -101,11 +104,12 @@ impl Mp4Reader { } } - 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)); @@ -125,6 +129,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 } diff --git a/src/track.rs b/src/track.rs index ab2f5a5..7eada83 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, } } @@ -436,8 +439,32 @@ impl Mp4Track { 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 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)) } @@ -473,15 +500,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 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 @@ -508,7 +558,17 @@ impl Mp4Track { } fn sample_rendering_offset(&self, sample_id: u32) -> i32 { - if let Some(ref ctts) = self.trak.mdia.minf.stbl.ctts { + if !self.trafs.is_empty() { + if let 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 *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.get(ctts_index).unwrap(); return ctts_entry.sample_offset; diff --git a/src/types.rs b/src/types.rs index 19fd40b..0983468 100644 --- a/src/types.rs +++ b/src/types.rs @@ -657,20 +657,15 @@ pub fn creation_time(creation_time: u64) -> u64 { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] pub enum DataType { + #[default] Binary = 0x000000, Text = 0x000001, Image = 0x00000D, TempoCpil = 0x000015, } -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 2d17ab2..7c81f95 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -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