1
0
Fork 0
mirror of https://github.com/alfg/mp4-rust.git synced 2024-05-08 19:43:03 +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:
Ririsoft 2022-07-21 04:05:38 +02:00 committed by GitHub
parent 5d648f1a72
commit ace2799c75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 394 additions and 6 deletions

View file

@ -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
View 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
View 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
View 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 })
}
}

View file

@ -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 {

View file

@ -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
View 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 })
}
}

View file

@ -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()))
}
}

View file

@ -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())
}
}

View file

@ -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());
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.