diff --git a/src/error.rs b/src/error.rs index f90605a..11690f0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,4 +24,6 @@ pub enum Error { EntryInStblNotFound(u32, BoxType, u32), #[error("traf[{0}].trun.{1}.entry[{2}] not found")] EntryInTrunNotFound(u32, BoxType, u32), + #[error("{0} version {1} is not supported")] + UnsupportedBoxVersion(BoxType, u8), } diff --git a/src/mp4box/data.rs b/src/mp4box/data.rs new file mode 100644 index 0000000..ac254df --- /dev/null +++ b/src/mp4box/data.rs @@ -0,0 +1,30 @@ +use std::{ + convert::TryFrom, + io::{Read, Seek}, +}; + +use serde::Serialize; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Default, Serialize)] +pub struct DataBox { + pub data: Vec, + pub data_type: DataType, +} + +impl ReadBox<&mut R> for DataBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let data_type = DataType::try_from(reader.read_u32::()?)?; + + reader.read_u32::()?; // reserved = 0 + + let current = reader.seek(SeekFrom::Current(0))?; + let mut data = vec![0u8; (start + size - current) as usize]; + reader.read_exact(&mut data)?; + + Ok(DataBox { data, data_type }) + } +} diff --git a/src/mp4box/ilst.rs b/src/mp4box/ilst.rs new file mode 100644 index 0000000..bda3ee1 --- /dev/null +++ b/src/mp4box/ilst.rs @@ -0,0 +1,132 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::io::{Read, Seek}; + +use byteorder::ByteOrder; +use serde::Serialize; + +use crate::mp4box::data::DataBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Default, Serialize)] +pub struct IlstBox { + pub items: HashMap, +} + +impl ReadBox<&mut R> for IlstBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut items = HashMap::new(); + + let mut current = reader.seek(SeekFrom::Current(0))?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::NameBox => { + items.insert(MetadataKey::Title, IlstItemBox::read_box(reader, s)?); + } + BoxType::DayBox => { + items.insert(MetadataKey::Year, IlstItemBox::read_box(reader, s)?); + } + BoxType::CovrBox => { + items.insert(MetadataKey::Poster, IlstItemBox::read_box(reader, s)?); + } + BoxType::DescBox => { + items.insert(MetadataKey::Summary, IlstItemBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.seek(SeekFrom::Current(0))?; + } + + skip_bytes_to(reader, start + size)?; + + Ok(IlstBox { items }) + } +} + +#[derive(Debug, Clone, PartialEq, Default, Serialize)] +pub struct IlstItemBox { + pub data: DataBox, +} + +impl ReadBox<&mut R> for IlstItemBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut data = None; + + let mut current = reader.seek(SeekFrom::Current(0))?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::DataBox => { + data = Some(DataBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.seek(SeekFrom::Current(0))?; + } + + if data.is_none() { + return Err(Error::BoxNotFound(BoxType::DataBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(IlstItemBox { + data: data.unwrap(), + }) + } +} + +impl<'a> Metadata<'a> for IlstBox { + fn title(&self) -> Option> { + self.items.get(&MetadataKey::Title).map(item_to_str) + } + + fn year(&self) -> Option { + self.items.get(&MetadataKey::Year).and_then(item_to_u32) + } + + fn poster(&self) -> Option<&[u8]> { + self.items.get(&MetadataKey::Poster).map(item_to_bytes) + } + + fn summary(&self) -> Option> { + self.items.get(&MetadataKey::Summary).map(item_to_str) + } +} + +fn item_to_bytes(item: &IlstItemBox) -> &[u8] { + &item.data.data +} + +fn item_to_str(item: &IlstItemBox) -> Cow { + String::from_utf8_lossy(&item.data.data) +} + +fn item_to_u32(item: &IlstItemBox) -> Option { + match item.data.data_type { + DataType::Binary if item.data.data.len() == 4 => Some(BigEndian::read_u32(&item.data.data)), + DataType::Text => String::from_utf8_lossy(&item.data.data).parse::().ok(), + _ => None, + } +} diff --git a/src/mp4box/meta.rs b/src/mp4box/meta.rs new file mode 100644 index 0000000..bd48493 --- /dev/null +++ b/src/mp4box/meta.rs @@ -0,0 +1,52 @@ +use std::io::{Read, Seek}; + +use serde::Serialize; + +use crate::mp4box::ilst::IlstBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Default, Serialize)] +pub struct MetaBox { + #[serde(skip_serializing_if = "Option::is_none")] + pub ilst: Option, +} + +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 as u8, + )); + } + + let mut ilst = None; + + let mut current = reader.seek(SeekFrom::Current(0))?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::IlstBox => { + ilst = Some(IlstBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.seek(SeekFrom::Current(0))?; + } + + skip_bytes_to(reader, start + size)?; + + Ok(MetaBox { ilst }) + } +} diff --git a/src/mp4box/mod.rs b/src/mp4box/mod.rs index 9c35cfb..bb7ac8a 100644 --- a/src/mp4box/mod.rs +++ b/src/mp4box/mod.rs @@ -13,6 +13,10 @@ //! ftyp //! moov //! mvhd +//! udta +//! meta +//! ilst +//! data //! trak //! tkhd //! mdia @@ -60,6 +64,7 @@ use crate::*; pub(crate) mod avc1; pub(crate) mod co64; pub(crate) mod ctts; +pub(crate) mod data; pub(crate) mod dinf; pub(crate) mod edts; pub(crate) mod elst; @@ -67,9 +72,11 @@ pub(crate) mod emsg; pub(crate) mod ftyp; pub(crate) mod hdlr; pub(crate) mod hev1; +pub(crate) mod ilst; pub(crate) mod mdhd; pub(crate) mod mdia; pub(crate) mod mehd; +pub(crate) mod meta; pub(crate) mod mfhd; pub(crate) mod minf; pub(crate) mod moof; @@ -92,6 +99,7 @@ pub(crate) mod trak; pub(crate) mod trex; pub(crate) mod trun; pub(crate) mod tx3g; +pub(crate) mod udta; pub(crate) mod vmhd; pub(crate) mod vp09; pub(crate) mod vpcc; @@ -167,6 +175,7 @@ boxtype! { TrafBox => 0x74726166, TrunBox => 0x7472756E, UdtaBox => 0x75647461, + MetaBox => 0x6d657461, DinfBox => 0x64696e66, DrefBox => 0x64726566, UrlBox => 0x75726C20, @@ -179,7 +188,13 @@ boxtype! { EsdsBox => 0x65736473, Tx3gBox => 0x74783367, VpccBox => 0x76706343, - Vp09Box => 0x76703039 + Vp09Box => 0x76703039, + DataBox => 0x64617461, + IlstBox => 0x696c7374, + NameBox => 0xa96e616d, + DayBox => 0xa9646179, + CovrBox => 0x636f7672, + DescBox => 0x64657363 } pub trait Mp4Box: Sized { diff --git a/src/mp4box/moov.rs b/src/mp4box/moov.rs index ad8aa8d..881c0c4 100644 --- a/src/mp4box/moov.rs +++ b/src/mp4box/moov.rs @@ -2,7 +2,7 @@ use serde::Serialize; use std::io::{Read, Seek, SeekFrom, Write}; use crate::mp4box::*; -use crate::mp4box::{mvex::MvexBox, mvhd::MvhdBox, trak::TrakBox}; +use crate::mp4box::{mvex::MvexBox, mvhd::MvhdBox, trak::TrakBox, udta::UdtaBox}; #[derive(Debug, Clone, PartialEq, Default, Serialize)] pub struct MoovBox { @@ -13,6 +13,9 @@ pub struct MoovBox { #[serde(rename = "trak")] pub traks: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub udta: Option, } impl MoovBox { @@ -53,6 +56,7 @@ impl ReadBox<&mut R> for MoovBox { let start = box_start(reader)?; let mut mvhd = None; + let mut udta = None; let mut mvex = None; let mut traks = Vec::new(); @@ -75,8 +79,7 @@ impl ReadBox<&mut R> for MoovBox { traks.push(trak); } BoxType::UdtaBox => { - // XXX warn!() - skip_box(reader, s)?; + udta = Some(UdtaBox::read_box(reader, s)?); } _ => { // XXX warn!() @@ -95,6 +98,7 @@ impl ReadBox<&mut R> for MoovBox { Ok(MoovBox { mvhd: mvhd.unwrap(), + udta, mvex, traks, }) diff --git a/src/mp4box/udta.rs b/src/mp4box/udta.rs new file mode 100644 index 0000000..0960915 --- /dev/null +++ b/src/mp4box/udta.rs @@ -0,0 +1,44 @@ +use std::io::{Read, Seek}; + +use serde::Serialize; + +use crate::mp4box::meta::MetaBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Default, Serialize)] +pub struct UdtaBox { + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +impl ReadBox<&mut R> for UdtaBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut meta = None; + + let mut current = reader.seek(SeekFrom::Current(0))?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::MetaBox => { + meta = Some(MetaBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.seek(SeekFrom::Current(0))?; + } + + skip_bytes_to(reader, start + size)?; + + Ok(UdtaBox { meta }) + } +} diff --git a/src/reader.rs b/src/reader.rs index c3e9bd0..ebb3721 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -167,3 +167,12 @@ impl Mp4Reader { } } } + +impl Mp4Reader { + pub fn metadata(&self) -> impl Metadata<'_> { + self.moov + .udta + .as_ref() + .and_then(|udta| udta.meta.as_ref().and_then(|meta| meta.ilst.as_ref())) + } +} diff --git a/src/types.rs b/src/types.rs index 7561592..404c67c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ use serde::Serialize; +use std::borrow::Cow; use std::convert::TryFrom; use std::fmt; @@ -655,3 +656,85 @@ pub fn creation_time(creation_time: u64) -> u64 { creation_time } } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum DataType { + 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 { + match value { + 0x000000 => Ok(DataType::Binary), + 0x000001 => Ok(DataType::Text), + 0x00000D => Ok(DataType::Image), + 0x000015 => Ok(DataType::TempoCpil), + _ => Err(Error::InvalidData("invalid data type")), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub enum MetadataKey { + Title, + Year, + Poster, + Summary, +} + +pub trait Metadata<'a> { + /// The video's title + fn title(&self) -> Option>; + /// The video's release year + fn year(&self) -> Option; + /// The video's poster (cover art) + fn poster(&self) -> Option<&[u8]>; + /// The video's summary + fn summary(&self) -> Option>; +} + +impl<'a, T: Metadata<'a>> Metadata<'a> for &'a T { + fn title(&self) -> Option> { + (**self).title() + } + + fn year(&self) -> Option { + (**self).year() + } + + fn poster(&self) -> Option<&[u8]> { + (**self).poster() + } + + fn summary(&self) -> Option> { + (**self).summary() + } +} + +impl<'a, T: Metadata<'a>> Metadata<'a> for Option { + fn title(&self) -> Option> { + self.as_ref().and_then(|t| t.title()) + } + + fn year(&self) -> Option { + self.as_ref().and_then(|t| t.year()) + } + + fn poster(&self) -> Option<&[u8]> { + self.as_ref().and_then(|t| t.poster()) + } + + fn summary(&self) -> Option> { + self.as_ref().and_then(|t| t.summary()) + } +} diff --git a/tests/lib.rs b/tests/lib.rs index 39662d7..c24ce1e 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,7 +1,8 @@ use mp4::{ - AudioObjectType, AvcProfile, ChannelConfig, MediaType, Mp4Reader, SampleFreqIndex, TrackType, + AudioObjectType, AvcProfile, ChannelConfig, MediaType, Metadata, Mp4Reader, SampleFreqIndex, + TrackType, }; -use std::fs::File; +use std::fs::{self, File}; use std::io::BufReader; use std::time::Duration; @@ -159,3 +160,19 @@ fn get_reader(path: &str) -> Mp4Reader> { mp4::Mp4Reader::read_header(reader, f_size).unwrap() } + +#[test] +fn test_read_metadata() { + let want_poster = fs::read("tests/samples/big_buck_bunny.jpg").unwrap(); + let want_summary = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue."; + let mp4 = get_reader("tests/samples/big_buck_bunny_metadata.m4v"); + let metadata = mp4.metadata(); + assert_eq!(metadata.title(), Some("Big Buck Bunny".into())); + assert_eq!(metadata.year(), Some(2008)); + assert_eq!(metadata.summary(), Some(want_summary.into())); + + assert!(metadata.poster().is_some()); + let poster = metadata.poster().unwrap(); + assert_eq!(poster.len(), want_poster.len()); + assert_eq!(poster, want_poster.as_slice()); +} diff --git a/tests/samples/big_buck_bunny.jpg b/tests/samples/big_buck_bunny.jpg new file mode 100644 index 0000000..8c30bc6 Binary files /dev/null and b/tests/samples/big_buck_bunny.jpg differ diff --git a/tests/samples/big_buck_bunny_metadata.m4v b/tests/samples/big_buck_bunny_metadata.m4v new file mode 100644 index 0000000..6002e06 Binary files /dev/null and b/tests/samples/big_buck_bunny_metadata.m4v differ