mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2025-01-18 23:23:53 +00:00
Add MP4 property reading
This commit is contained in:
parent
fafda6243d
commit
cb785ddf95
7 changed files with 322 additions and 14 deletions
|
@ -5,6 +5,7 @@ use std::io::{Read, Seek, SeekFrom};
|
|||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) struct Atom {
|
||||
pub(crate) start: u64,
|
||||
pub(crate) len: u64,
|
||||
pub(crate) extended: bool,
|
||||
pub(crate) ident: String,
|
||||
|
@ -15,6 +16,8 @@ impl Atom {
|
|||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let start = data.seek(SeekFrom::Current(0))?;
|
||||
|
||||
let len = data.read_u32::<BigEndian>()?;
|
||||
|
||||
let mut ident = [0; 4];
|
||||
|
@ -51,6 +54,7 @@ impl Atom {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
start,
|
||||
len,
|
||||
extended,
|
||||
ident,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod atom;
|
||||
mod ilst;
|
||||
mod moov;
|
||||
mod properties;
|
||||
pub(crate) mod read;
|
||||
mod trak;
|
||||
|
||||
|
|
|
@ -5,8 +5,9 @@ use super::trak::Trak;
|
|||
use crate::error::Result;
|
||||
use crate::types::tag::Tag;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub(crate) struct Moov {
|
||||
pub(crate) traks: Vec<Trak>,
|
||||
|
@ -24,7 +25,7 @@ impl Moov {
|
|||
|
||||
while let Ok(atom) = Atom::read(data) {
|
||||
match &*atom.ident {
|
||||
//"trak" => traks.push(Trak::parse(data, &atom)?),
|
||||
"trak" => traks.push(Trak::parse(data, &atom)?),
|
||||
"udta" => {
|
||||
meta = meta_from_udta(data, atom.len - 8)?;
|
||||
},
|
||||
|
|
248
src/logic/mp4/properties.rs
Normal file
248
src/logic/mp4/properties.rs
Normal file
|
@ -0,0 +1,248 @@
|
|||
use super::atom::Atom;
|
||||
use super::read::nested_atom;
|
||||
use super::read::skip_unneeded;
|
||||
use super::trak::Trak;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::properties::FileProperties;
|
||||
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) fn read_properties<R>(data: &mut R, traks: &[Trak]) -> Result<FileProperties>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
println!("hi");
|
||||
// We need the mdhd and minf atoms from the audio track
|
||||
let mut audio_track = false;
|
||||
let mut mdhd = None;
|
||||
let mut minf = None;
|
||||
|
||||
// We have to search through the traks with a mdia atom to find the audio track
|
||||
for mdia in traks.iter().filter_map(|trak| trak.mdia.as_ref()) {
|
||||
if audio_track {
|
||||
break;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Start(mdia.start + 8))?;
|
||||
|
||||
let mut read = 8;
|
||||
|
||||
while read < mdia.len {
|
||||
let atom = Atom::read(data)?;
|
||||
|
||||
match &*atom.ident {
|
||||
"mdhd" => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
mdhd = Some(atom)
|
||||
},
|
||||
"hdlr" => {
|
||||
// The hdlr atom is followed by 8 zeros
|
||||
data.seek(SeekFrom::Current(8))?;
|
||||
|
||||
let mut handler_type = [0; 4];
|
||||
data.read_exact(&mut handler_type)?;
|
||||
|
||||
if &handler_type == b"soun" {
|
||||
audio_track = true
|
||||
}
|
||||
|
||||
skip_unneeded(data, atom.extended, atom.len - 12)?;
|
||||
},
|
||||
"minf" => minf = Some(atom),
|
||||
_ => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
read += atom.len
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !audio_track {
|
||||
return Err(LoftyError::Mp4("File contains no audio tracks"));
|
||||
}
|
||||
|
||||
let duration = match mdhd {
|
||||
Some(mdhd) => {
|
||||
data.seek(SeekFrom::Start(mdhd.start + 8))?;
|
||||
|
||||
let version = data.read_u8()?;
|
||||
let _flags = data.read_uint::<BigEndian>(3)?;
|
||||
|
||||
let (timescale, duration) = if version == 1 {
|
||||
// We don't care about these two values
|
||||
let _creation_time = data.read_u64::<BigEndian>()?;
|
||||
let _modification_time = data.read_u64::<BigEndian>()?;
|
||||
|
||||
let timescale = data.read_u32::<BigEndian>()?;
|
||||
let duration = data.read_u64::<BigEndian>()?;
|
||||
|
||||
(timescale, duration)
|
||||
} else {
|
||||
let _creation_time = data.read_u32::<BigEndian>()?;
|
||||
let _modification_time = data.read_u32::<BigEndian>()?;
|
||||
|
||||
let timescale = data.read_u32::<BigEndian>()?;
|
||||
let duration = data.read_u32::<BigEndian>()?;
|
||||
|
||||
(timescale, u64::from(duration))
|
||||
};
|
||||
|
||||
Duration::from_millis(duration * 1000 / u64::from(timescale))
|
||||
},
|
||||
None => return Err(LoftyError::BadAtom("Expected atom \"trak.mdia.mdhd\"")),
|
||||
};
|
||||
|
||||
// We create the properties here, since it is possible the other information isn't available
|
||||
let mut properties = FileProperties {
|
||||
duration,
|
||||
bitrate: None,
|
||||
sample_rate: None,
|
||||
channels: None,
|
||||
};
|
||||
|
||||
if let Some(minf) = minf {
|
||||
data.seek(SeekFrom::Start(minf.start + 8))?;
|
||||
|
||||
if let Some(stbl) = nested_atom(data, minf.len, "stbl")? {
|
||||
if let Some(stsd) = nested_atom(data, stbl.len, "stsd")? {
|
||||
let mut stsd = vec![0; (stsd.len - 8) as usize];
|
||||
data.read_exact(&mut stsd)?;
|
||||
|
||||
let mut stsd_reader = Cursor::new(&*stsd);
|
||||
|
||||
// There are 8 bytes we don't care about
|
||||
// Version (1)
|
||||
// Flags (3)
|
||||
// Number of entries (4)
|
||||
stsd_reader.seek(SeekFrom::Start(8))?;
|
||||
|
||||
let atom = Atom::read(&mut stsd_reader)?;
|
||||
|
||||
match &*atom.ident {
|
||||
"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties)?,
|
||||
"alac" => alac_properties(&mut stsd_reader, &mut properties)?,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(properties)
|
||||
}
|
||||
|
||||
fn mp4a_properties<R>(data: &mut R, properties: &mut FileProperties) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
// There are 16 bytes we don't care about
|
||||
// Reserved (6)
|
||||
// Data reference index (2)
|
||||
// Version (2)
|
||||
// Revision level (2)
|
||||
// Vendor (4)
|
||||
data.seek(SeekFrom::Current(16))?;
|
||||
|
||||
properties.channels = Some(data.read_u16::<BigEndian>()? as u8);
|
||||
|
||||
// There are 4 bytes we don't care about
|
||||
// Sample size (2)
|
||||
// Compression ID (2)
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
|
||||
properties.sample_rate = Some(data.read_u32::<BigEndian>()?);
|
||||
|
||||
data.seek(SeekFrom::Current(2))?;
|
||||
|
||||
// This information is often followed by an esds (elementary stream descriptor) atom containing the bitrate
|
||||
if let Ok(esds) = Atom::read(data) {
|
||||
// There are 4 bytes we expect to be zeroed out
|
||||
// Version (1)
|
||||
// Flags (3)
|
||||
if &*esds.ident == "esds" && data.read_u32::<BigEndian>()? == 0 {
|
||||
let mut descriptor = [0; 4];
|
||||
data.read_exact(&mut descriptor)?;
|
||||
|
||||
// [0x03, 0x80, 0x80, 0x80] marks the start of the elementary stream descriptor.
|
||||
// 0x03 being the object descriptor
|
||||
if descriptor == [0x03, 0x80, 0x80, 0x80] {
|
||||
// There are 4 bytes here we don't care about
|
||||
// Descriptor length (1)
|
||||
// Elementary stream ID (2)
|
||||
// Flags (1)
|
||||
let _info = data.read_u32::<BigEndian>()?;
|
||||
|
||||
// There is another descriptor embedded in the previous one
|
||||
let mut specific_config = [0; 4];
|
||||
data.read_exact(&mut specific_config)?;
|
||||
|
||||
// [0x04, 0x80, 0x80, 0x80] marks the start of the descriptor configuration
|
||||
if specific_config == [0x04, 0x80, 0x80, 0x80] {
|
||||
// There are 10 bytes here we don't care about
|
||||
// Descriptor length (1)
|
||||
// MPEG4 Audio (1)
|
||||
// Stream type (1)
|
||||
// Buffer size (3)
|
||||
// Max bitrate (4)
|
||||
let mut info = [0; 10];
|
||||
data.read_exact(&mut info)?;
|
||||
|
||||
let average_bitrate = data.read_u32::<BigEndian>()?;
|
||||
|
||||
if average_bitrate > 0 {
|
||||
properties.bitrate = Some(average_bitrate / 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn alac_properties<R>(data: &mut R, properties: &mut FileProperties) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
// With ALAC, we can expect the length to be exactly 88 (64 here since we removed the size and identifier)
|
||||
if data.seek(SeekFrom::End(0))? != 80 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Unlike the mp4a atom, we cannot read the data that immediately follows it
|
||||
// For ALAC, we have to skip the first "alac" atom entirely, and read the one that
|
||||
// immediately follows it.
|
||||
//
|
||||
// We are skipping over 44 bytes total
|
||||
// stsd information/alac atom header (16, see `read_properties`)
|
||||
// First alac atom's content (28)
|
||||
data.seek(SeekFrom::Start(44))?;
|
||||
|
||||
if let Ok(alac) = Atom::read(data) {
|
||||
if &*alac.ident == "alac" {
|
||||
// There are 13 bytes we don't care about
|
||||
// Version (4)
|
||||
// Samples per frame (4)
|
||||
// Compatible version (1)
|
||||
// Sample size (1)
|
||||
// Rice history mult (1)
|
||||
// Rice initial history (1)
|
||||
// Rice parameter limit (1)
|
||||
data.seek(SeekFrom::Current(13))?;
|
||||
|
||||
properties.channels = Some(data.read_u8()?);
|
||||
|
||||
// There are 6 bytes we don't care about
|
||||
// Max run (2)
|
||||
// Max frame size (4)
|
||||
data.seek(SeekFrom::Current(6))?;
|
||||
|
||||
properties.bitrate = Some(data.read_u32::<BigEndian>()?);
|
||||
properties.sample_rate = Some(data.read_u32::<BigEndian>()?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
use super::atom::Atom;
|
||||
use super::moov::Moov;
|
||||
use super::trak::Trak;
|
||||
use super::properties::read_properties;
|
||||
use super::Mp4File;
|
||||
use crate::types::properties::FileProperties;
|
||||
use crate::error::{LoftyError, Result};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
@ -26,11 +25,6 @@ where
|
|||
.map_err(|_| LoftyError::BadAtom("Unable to parse \"ftyp\"'s major brand"))
|
||||
}
|
||||
|
||||
fn read_properties<R>(data: &mut R, traks: &[Trak]) -> Result<FileProperties>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{}
|
||||
|
||||
#[allow(clippy::similar_names)]
|
||||
pub(crate) fn read_from<R>(data: &mut R) -> Result<Mp4File>
|
||||
where
|
||||
|
@ -58,7 +52,7 @@ where
|
|||
Ok(Mp4File {
|
||||
ftyp,
|
||||
ilst: moov.meta,
|
||||
properties: Default::default(),
|
||||
properties: read_properties(data, &moov.traks)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -80,3 +74,28 @@ where
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn nested_atom<R>(data: &mut R, len: u64, expected: &str) -> Result<Option<Atom>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut read = 8;
|
||||
let mut ret = None;
|
||||
|
||||
while read < len {
|
||||
let atom = Atom::read(data)?;
|
||||
|
||||
match &*atom.ident {
|
||||
ident if ident == expected => {
|
||||
ret = Some(atom);
|
||||
break;
|
||||
},
|
||||
_ => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
read += atom.len
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
|
35
src/logic/mp4/trak.rs
Normal file
35
src/logic/mp4/trak.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use super::atom::Atom;
|
||||
use super::read::skip_unneeded;
|
||||
use crate::error::Result;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub(crate) struct Trak {
|
||||
pub(crate) mdia: Option<Atom>,
|
||||
}
|
||||
|
||||
impl Trak {
|
||||
pub(crate) fn parse<R>(data: &mut R, trak: &Atom) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut mdia = None;
|
||||
|
||||
let mut read = 8;
|
||||
|
||||
while read < trak.len {
|
||||
let atom = Atom::read(data)?;
|
||||
|
||||
if &*atom.ident == "mdia" {
|
||||
mdia = Some(atom);
|
||||
data.seek(SeekFrom::Current((trak.len - read) as i64))?;
|
||||
break;
|
||||
}
|
||||
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
read += atom.len;
|
||||
}
|
||||
|
||||
Ok(Self { mdia })
|
||||
}
|
||||
}
|
|
@ -2,10 +2,10 @@ use std::time::Duration;
|
|||
|
||||
/// Various *immutable* audio properties
|
||||
pub struct FileProperties {
|
||||
duration: Duration,
|
||||
bitrate: Option<u32>,
|
||||
sample_rate: Option<u32>,
|
||||
channels: Option<u8>,
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) bitrate: Option<u32>,
|
||||
pub(crate) sample_rate: Option<u32>,
|
||||
pub(crate) channels: Option<u8>,
|
||||
}
|
||||
|
||||
impl Default for FileProperties {
|
||||
|
|
Loading…
Reference in a new issue