mirror of
https://github.com/alfg/mp4-rust.git
synced 2024-12-21 19:46:27 +00:00
read metadata from udta (#77)
This introduces the 'Metadata' trait to enable access to common video metadata such title, year, cover art and more. Reading 'title', 'description', 'poster' and 'year' metadata is implemented here.
This commit is contained in:
parent
5d648f1a72
commit
ace2799c75
12 changed files with 394 additions and 6 deletions
|
@ -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),
|
||||
}
|
||||
|
|
30
src/mp4box/data.rs
Normal file
30
src/mp4box/data.rs
Normal file
|
@ -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<u8>,
|
||||
pub data_type: DataType,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ReadBox<&mut R> for DataBox {
|
||||
fn read_box(reader: &mut R, size: u64) -> Result<Self> {
|
||||
let start = box_start(reader)?;
|
||||
|
||||
let data_type = DataType::try_from(reader.read_u32::<BigEndian>()?)?;
|
||||
|
||||
reader.read_u32::<BigEndian>()?; // 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 })
|
||||
}
|
||||
}
|
132
src/mp4box/ilst.rs
Normal file
132
src/mp4box/ilst.rs
Normal file
|
@ -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<MetadataKey, IlstItemBox>,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ReadBox<&mut R> for IlstBox {
|
||||
fn read_box(reader: &mut R, size: u64) -> Result<Self> {
|
||||
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<R: Read + Seek> ReadBox<&mut R> for IlstItemBox {
|
||||
fn read_box(reader: &mut R, size: u64) -> Result<Self> {
|
||||
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<Cow<str>> {
|
||||
self.items.get(&MetadataKey::Title).map(item_to_str)
|
||||
}
|
||||
|
||||
fn year(&self) -> Option<u32> {
|
||||
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<Cow<str>> {
|
||||
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<str> {
|
||||
String::from_utf8_lossy(&item.data.data)
|
||||
}
|
||||
|
||||
fn item_to_u32(item: &IlstItemBox) -> Option<u32> {
|
||||
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::<u32>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
52
src/mp4box/meta.rs
Normal file
52
src/mp4box/meta.rs
Normal file
|
@ -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<IlstBox>,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ReadBox<&mut R> for MetaBox {
|
||||
fn read_box(reader: &mut R, size: u64) -> Result<Self> {
|
||||
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 })
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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<TrakBox>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub udta: Option<UdtaBox>,
|
||||
}
|
||||
|
||||
impl MoovBox {
|
||||
|
@ -53,6 +56,7 @@ impl<R: Read + Seek> 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<R: Read + Seek> 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<R: Read + Seek> ReadBox<&mut R> for MoovBox {
|
|||
|
||||
Ok(MoovBox {
|
||||
mvhd: mvhd.unwrap(),
|
||||
udta,
|
||||
mvex,
|
||||
traks,
|
||||
})
|
||||
|
|
44
src/mp4box/udta.rs
Normal file
44
src/mp4box/udta.rs
Normal file
|
@ -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<MetaBox>,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ReadBox<&mut R> for UdtaBox {
|
||||
fn read_box(reader: &mut R, size: u64) -> Result<Self> {
|
||||
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 })
|
||||
}
|
||||
}
|
|
@ -167,3 +167,12 @@ impl<R: Read + Seek> Mp4Reader<R> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Mp4Reader<R> {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
|
83
src/types.rs
83
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<u32> for DataType {
|
||||
type Error = Error;
|
||||
fn try_from(value: u32) -> Result<DataType> {
|
||||
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<Cow<str>>;
|
||||
/// The video's release year
|
||||
fn year(&self) -> Option<u32>;
|
||||
/// The video's poster (cover art)
|
||||
fn poster(&self) -> Option<&[u8]>;
|
||||
/// The video's summary
|
||||
fn summary(&self) -> Option<Cow<str>>;
|
||||
}
|
||||
|
||||
impl<'a, T: Metadata<'a>> Metadata<'a> for &'a T {
|
||||
fn title(&self) -> Option<Cow<str>> {
|
||||
(**self).title()
|
||||
}
|
||||
|
||||
fn year(&self) -> Option<u32> {
|
||||
(**self).year()
|
||||
}
|
||||
|
||||
fn poster(&self) -> Option<&[u8]> {
|
||||
(**self).poster()
|
||||
}
|
||||
|
||||
fn summary(&self) -> Option<Cow<str>> {
|
||||
(**self).summary()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Metadata<'a>> Metadata<'a> for Option<T> {
|
||||
fn title(&self) -> Option<Cow<str>> {
|
||||
self.as_ref().and_then(|t| t.title())
|
||||
}
|
||||
|
||||
fn year(&self) -> Option<u32> {
|
||||
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<Cow<str>> {
|
||||
self.as_ref().and_then(|t| t.summary())
|
||||
}
|
||||
}
|
||||
|
|
21
tests/lib.rs
21
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<BufReader<File>> {
|
|||
|
||||
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());
|
||||
}
|
||||
|
|
BIN
tests/samples/big_buck_bunny.jpg
Normal file
BIN
tests/samples/big_buck_bunny.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
BIN
tests/samples/big_buck_bunny_metadata.m4v
Normal file
BIN
tests/samples/big_buck_bunny_metadata.m4v
Normal file
Binary file not shown.
Loading…
Reference in a new issue