2
0
Fork 0
mirror of https://github.com/Serial-ATA/lofty-rs synced 2024-12-14 06:32:33 +00:00

EBML: Support duration and bitrate calculation

This commit is contained in:
Serial 2024-10-15 09:03:23 -04:00
parent c3c16fce47
commit e45a036dba
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
7 changed files with 305 additions and 20 deletions

View file

@ -109,6 +109,7 @@ ebml_master_elements! {
children: [ children: [
// SeekHead: { 0x114D_9B74, Master }, // SeekHead: { 0x114D_9B74, Master },
Info: { 0x1549_A966, Master }, Info: { 0x1549_A966, Master },
Cluster: { 0x1F43_B675, Master },
Tracks: { 0x1654_AE6B, Master }, Tracks: { 0x1654_AE6B, Master },
Tags: { 0x1254_C367, Master }, Tags: { 0x1254_C367, Master },
Attachments: { 0x1941_A469, Master }, Attachments: { 0x1941_A469, Master },
@ -131,9 +132,28 @@ ebml_master_elements! {
TimecodeScale: { 0x2AD7_B1, UnsignedInt }, TimecodeScale: { 0x2AD7_B1, UnsignedInt },
MuxingApp: { 0x4D80, Utf8 }, MuxingApp: { 0x4D80, Utf8 },
WritingApp: { 0x5741, Utf8 }, WritingApp: { 0x5741, Utf8 },
Duration: { 0x4489, Float },
], ],
}, },
// segment.cluster
Cluster: {
id: 0x1F43_B675,
children: [
Timestamp: { 0xE7, UnsignedInt },
SimpleBlock: { 0xA3, Binary },
BlockGroup: { 0xA0, Master },
],
},
// segment.cluster.blockGroup
BlockGroup: {
id: 0xA0,
children: [
Block: { 0xA1, Binary },
]
},
// segment.tracks // segment.tracks
Tracks: { Tracks: {
id: 0x1654_AE6B, id: 0x1654_AE6B,
@ -726,6 +746,15 @@ where
} }
} }
impl<'a, R> Read for ElementChildIterator<'a, R>
where
R: Read,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
}
impl<'a, R> Deref for ElementChildIterator<'a, R> impl<'a, R> Deref for ElementChildIterator<'a, R>
where where
R: Read, R: Read,

View file

@ -1,6 +1,8 @@
use super::Language; use super::Language;
use crate::properties::FileProperties; use crate::properties::FileProperties;
use std::time::Duration;
/// Properties from the EBML header /// Properties from the EBML header
/// ///
/// These are present for all EBML formats. /// These are present for all EBML formats.
@ -77,6 +79,7 @@ pub struct SegmentInfo {
pub(crate) timestamp_scale: u64, pub(crate) timestamp_scale: u64,
pub(crate) muxing_app: String, pub(crate) muxing_app: String,
pub(crate) writing_app: String, pub(crate) writing_app: String,
pub(crate) duration: Option<Duration>,
} }
impl SegmentInfo { impl SegmentInfo {
@ -100,6 +103,14 @@ impl SegmentInfo {
pub fn writing_app(&self) -> &str { pub fn writing_app(&self) -> &str {
&self.writing_app &self.writing_app
} }
/// The duration of the segment
///
/// NOTE: This information is not always present in the segment, in which case
/// [`EbmlProperties::duration`] should be used.
pub fn duration(&self) -> Option<Duration> {
self.duration
}
} }
impl Default for SegmentInfo { impl Default for SegmentInfo {
@ -109,6 +120,7 @@ impl Default for SegmentInfo {
timestamp_scale: 1_000_000, timestamp_scale: 1_000_000,
muxing_app: String::new(), muxing_app: String::new(),
writing_app: String::new(), writing_app: String::new(),
duration: None,
} }
} }
} }
@ -210,11 +222,15 @@ impl AudioTrackDescriptor {
/// Settings for an audio track /// Settings for an audio track
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct AudioTrackSettings { pub struct AudioTrackSettings {
// Provided to us for free
pub(crate) sampling_frequency: f64, pub(crate) sampling_frequency: f64,
pub(crate) output_sampling_frequency: f64, pub(crate) output_sampling_frequency: f64,
pub(crate) channels: u8, pub(crate) channels: u8,
pub(crate) bit_depth: Option<u8>, pub(crate) bit_depth: Option<u8>,
pub(crate) emphasis: Option<EbmlAudioTrackEmphasis>, pub(crate) emphasis: Option<EbmlAudioTrackEmphasis>,
// Need to be calculated
pub(crate) bitrate: Option<u32>,
} }
impl AudioTrackSettings { impl AudioTrackSettings {
@ -322,27 +338,67 @@ impl EbmlProperties {
/// Information about the default audio track /// Information about the default audio track
/// ///
/// The information is extracted from the first audio track with its default flag set /// The "default" track is selected as:
/// in the `\Segment\Tracks` element. /// 1. The first audio track with its `default` flag set
/// 2. If 1 fails, just grab the first audio track with its `enabled` flag set
pub fn default_audio_track(&self) -> Option<&AudioTrackDescriptor> { pub fn default_audio_track(&self) -> Option<&AudioTrackDescriptor> {
self.audio_tracks.iter().find(|track| track.default) if let Some(position) = self.default_audio_track_position() {
return self.audio_tracks.get(position);
}
None
}
// TODO: Actually calculate from cluster
/// The duration of the default audio track
///
/// NOTE: see [`EbmlProperties::default_audio_track`]
///
/// This will always use the duration written in `\Segment\Info` if present. Otherwise, it will
/// be manually calculated using `\Segment\Cluster` data.
pub fn duration(&self) -> Duration {
self.segment_info.duration().unwrap()
}
/// Audio bitrate (kbps)
///
/// NOTE: This is the bitrate of the default audio track see [`EbmlProperties::default_audio_track`]
/// for what this means.
pub fn bitrate(&self) -> Option<u32> {
self.default_audio_track()
.and_then(|track| track.settings.bitrate)
}
pub(crate) fn default_audio_track_position(&self) -> Option<usize> {
self.audio_tracks
.iter()
.position(|track| track.default)
.or_else(|| {
// Otherwise, it's normal to just pick the first enabled track
self.audio_tracks.iter().position(|track| track.enabled)
})
} }
} }
impl From<EbmlProperties> for FileProperties { impl From<EbmlProperties> for FileProperties {
fn from(input: EbmlProperties) -> Self { fn from(input: EbmlProperties) -> Self {
let Some(default_audio_track) = input.default_audio_track() else { let Some(default_audio_track) = input.default_audio_track() else {
return FileProperties::default(); let mut properties = FileProperties::default();
if let Some(duration) = input.segment_info.duration {
properties.duration = duration;
}
return properties;
}; };
Self { Self {
duration: todo!("Support duration"), duration: input.duration(),
overall_bitrate: todo!("Support bitrate"), overall_bitrate: input.bitrate(),
audio_bitrate: todo!("Support bitrate"), audio_bitrate: input.bitrate(),
sample_rate: Some(default_audio_track.settings.sampling_frequency as u32), sample_rate: Some(default_audio_track.settings.sampling_frequency as u32),
bit_depth: default_audio_track.settings.bit_depth, bit_depth: default_audio_track.settings.bit_depth,
channels: Some(default_audio_track.settings.channels), channels: Some(default_audio_track.settings.channels),
channel_mask: todo!("Channel mask"), channel_mask: None, // TODO: Will require reading into track data
} }
} }
} }

View file

@ -1,6 +1,7 @@
mod segment; mod segment;
mod segment_attachments; mod segment_attachments;
mod segment_chapters; mod segment_chapters;
mod segment_cluster;
mod segment_info; mod segment_info;
mod segment_tags; mod segment_tags;
mod segment_tracks; mod segment_tracks;

View file

@ -1,4 +1,4 @@
use super::{segment_attachments, segment_info, segment_tags, segment_tracks}; use super::{segment_attachments, segment_cluster, segment_info, segment_tags, segment_tracks};
use crate::config::ParseOptions; use crate::config::ParseOptions;
use crate::ebml::element_reader::{ElementHeader, ElementIdent, ElementReader, ElementReaderYield}; use crate::ebml::element_reader::{ElementHeader, ElementIdent, ElementReader, ElementReaderYield};
use crate::ebml::properties::EbmlProperties; use crate::ebml::properties::EbmlProperties;
@ -30,6 +30,13 @@ where
properties, properties,
)?; )?;
}, },
ElementIdent::Cluster if parse_options.read_properties => {
segment_cluster::read_from(
&mut children_reader.children(),
parse_options,
properties,
)?
},
ElementIdent::Tracks if parse_options.read_properties => { ElementIdent::Tracks if parse_options.read_properties => {
segment_tracks::read_from( segment_tracks::read_from(
&mut children_reader.children(), &mut children_reader.children(),

View file

@ -0,0 +1,175 @@
use crate::config::ParseOptions;
use crate::ebml::element_reader::{
ChildElementDescriptor, ElementChildIterator, ElementIdent, ElementReaderYield,
};
use crate::ebml::properties::EbmlProperties;
use crate::ebml::{AudioTrackDescriptor, VInt};
use crate::error::Result;
use std::io::{Read, Seek};
pub(super) fn read_from<R>(
children_reader: &mut ElementChildIterator<'_, R>,
parse_options: ParseOptions,
properties: &mut EbmlProperties,
) -> Result<()>
where
R: Read + Seek,
{
// TODO: Support Tracks appearing after Cluster (should implement SeekHead first)
let Some(default_audio_track_position) = properties.default_audio_track_position() else {
log::warn!(
"No default audio track found (does \\Segment\\Cluster appear before \
\\Segment\\Tracks?)"
);
children_reader.exhaust_current_master()?;
return Ok(());
};
let default_audio_track = &properties.audio_tracks[default_audio_track_position];
let target_track_number = default_audio_track.number();
let mut total_audio_data_size = 0u64;
while let Some(child) = children_reader.next()? {
let ident;
let size;
match child {
ElementReaderYield::Master((master_ident, master_size)) => {
ident = master_ident;
size = master_size;
},
ElementReaderYield::Child((descriptor, child_size)) => {
ident = descriptor.ident;
size = child_size;
},
ElementReaderYield::Unknown(unknown) => {
children_reader.skip_element(unknown)?;
continue;
},
ElementReaderYield::Eof => break,
}
match ident {
ElementIdent::Timestamp => {
// TODO: Fancy timestamp durations
children_reader.skip(size.value())?;
continue;
},
ElementIdent::SimpleBlock => {
let (block_is_applicable, header_size) = check_block(
children_reader,
parse_options,
size.value(),
target_track_number,
properties.header.max_size_length,
)?;
if !block_is_applicable {
continue;
}
total_audio_data_size += (size.value() - header_size as u64);
},
ElementIdent::BlockGroup => read_block_group(
&mut children_reader.children(),
parse_options,
properties,
target_track_number,
&mut total_audio_data_size,
)?,
_ => unreachable!("Unhandled child element in \\Segment\\Cluster: {child:?}"),
}
}
if total_audio_data_size == 0 {
log::warn!("No audio data found, audio bitrate will be 0, duration may be 0");
return Ok(());
}
let duration_millis = properties.duration().as_secs() as u128;
if duration_millis == 0 {
log::warn!("Duration is zero, cannot calculate bitrate");
return Ok(());
}
let default_audio_track = &mut properties.audio_tracks[default_audio_track_position]; // TODO
let bitrate_bps = (((total_audio_data_size as u128) * 8) / duration_millis) as u32;
default_audio_track.settings.bitrate = Some(bitrate_bps / 1000);
Ok(())
}
fn read_block_group<R>(
children_reader: &mut ElementChildIterator<'_, R>,
parse_options: ParseOptions,
properties: &mut EbmlProperties,
target_track_number: u64,
total_audio_data_size: &mut u64,
) -> Result<()>
where
R: Read + Seek,
{
while let Some(child) = children_reader.next()? {
let size;
match child {
ElementReaderYield::Child((
ChildElementDescriptor {
ident: ElementIdent::Block,
..
},
child_size,
)) => {
size = child_size;
},
ElementReaderYield::Unknown(unknown) => {
children_reader.skip_element(unknown)?;
continue;
},
_ => unimplemented!(
"Unhandled child element in \\Segment\\Cluster\\BlockGroup: {child:?}"
),
}
let (block_is_applicable, header_size) = check_block(
children_reader,
parse_options,
size.value(),
target_track_number,
properties.header.max_size_length,
)?;
if !block_is_applicable {
continue;
}
*total_audio_data_size += (size.value() - header_size as u64);
}
Ok(())
}
fn check_block<R>(
children_reader: &mut ElementChildIterator<'_, R>,
_parse_options: ParseOptions,
block_size: u64,
target_track_number: u64,
max_size_length: u8,
) -> Result<(bool, u8)>
where
R: Read + Seek,
{
// The block header is Track number (variable), timestamp (i16), and flags (u8)
const NON_VARIABLE_BLOCK_HEADER_SIZE: u8 = 2 /* Timestamp */ + 1 /* Flags */;
let track_number = VInt::<u64>::parse(children_reader, max_size_length)?;
let track_number_octets = track_number.octet_length();
children_reader.skip(block_size - track_number_octets as u64)?;
if track_number != target_track_number {
return Ok((false, track_number_octets + NON_VARIABLE_BLOCK_HEADER_SIZE));
}
Ok((true, track_number_octets + NON_VARIABLE_BLOCK_HEADER_SIZE))
}

View file

@ -5,6 +5,7 @@ use crate::error::Result;
use crate::macros::decode_err; use crate::macros::decode_err;
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use std::time::Duration;
pub(super) fn read_from<R>( pub(super) fn read_from<R>(
children_reader: &mut ElementChildIterator<'_, R>, children_reader: &mut ElementChildIterator<'_, R>,
@ -14,6 +15,10 @@ pub(super) fn read_from<R>(
where where
R: Read + Seek, R: Read + Seek,
{ {
// Deal with duration after parsing, in case the timestamp scale appears after it
// for some reason.
let mut duration = None;
while let Some(child) = children_reader.next()? { while let Some(child) = children_reader.next()? {
match child { match child {
ElementReaderYield::Master((id, size)) => { ElementReaderYield::Master((id, size)) => {
@ -29,21 +34,17 @@ where
ElementIdent::TimecodeScale => { ElementIdent::TimecodeScale => {
properties.segment_info.timestamp_scale = properties.segment_info.timestamp_scale =
children_reader.read_unsigned_int(size.value())?; children_reader.read_unsigned_int(size.value())?;
if properties.segment_info.timestamp_scale == 0 {
log::warn!("Segment.Info.TimecodeScale is 0, which is invalid");
if parse_options.parsing_mode == ParsingMode::Strict {
decode_err!(@BAIL Ebml, "Segment.Info.TimecodeScale must be nonzero");
}
}
}, },
ElementIdent::MuxingApp => { ElementIdent::MuxingApp => {
properties.segment_info.muxing_app = let muxing_app = children_reader.read_utf8(size.value())?;
children_reader.read_utf8(size.value())? properties.segment_info.muxing_app = muxing_app;
}, },
ElementIdent::WritingApp => { ElementIdent::WritingApp => {
properties.segment_info.writing_app = let writing_app = children_reader.read_utf8(size.value())?;
children_reader.read_utf8(size.value())? properties.segment_info.writing_app = writing_app;
},
ElementIdent::Duration => {
duration = Some(children_reader.read_float(size.value())?);
}, },
_ => { _ => {
// We do not end up using information from all of the segment // We do not end up using information from all of the segment
@ -63,5 +64,19 @@ where
} }
} }
if properties.segment_info.timestamp_scale == 0 {
log::warn!("Segment.Info.TimecodeScale is 0, which is invalid");
if parse_options.parsing_mode == ParsingMode::Strict {
decode_err!(@BAIL Ebml, "Segment.Info.TimecodeScale must be non-zero");
}
return Ok(());
}
if let Some(duration) = duration {
let scaled_duration = duration * properties.segment_info.timestamp_scale as f64;
properties.segment_info.duration = Some(Duration::from_nanos(scaled_duration as u64));
}
Ok(()) Ok(())
} }

View file

@ -88,6 +88,7 @@ fn MKA_PROPERTIES() -> EbmlProperties {
timestamp_scale: 1000000, timestamp_scale: 1000000,
muxing_app: String::from("Lavf60.3.100"), muxing_app: String::from("Lavf60.3.100"),
writing_app: String::from("Lavf60.3.100"), writing_app: String::from("Lavf60.3.100"),
duration: Some(Duration::from_millis(1431)),
}, },
audio_tracks: vec![AudioTrackDescriptor { audio_tracks: vec![AudioTrackDescriptor {
number: 1, number: 1,
@ -105,6 +106,7 @@ fn MKA_PROPERTIES() -> EbmlProperties {
channels: 2, channels: 2,
bit_depth: Some(32), bit_depth: Some(32),
emphasis: None, emphasis: None,
bitrate: Some(99), // TODO: FFmpeg reports 97, not bad
}, },
}], }],
} }