Skip to content

Commit 9a4a05d

Browse files
committed
EBML: Support duration and bitrate calculation
1 parent 283a47c commit 9a4a05d

File tree

7 files changed

+305
-20
lines changed

7 files changed

+305
-20
lines changed

lofty/src/ebml/element_reader.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ ebml_master_elements! {
109109
children: [
110110
// SeekHead: { 0x114D_9B74, Master },
111111
Info: { 0x1549_A966, Master },
112+
Cluster: { 0x1F43_B675, Master },
112113
Tracks: { 0x1654_AE6B, Master },
113114
Tags: { 0x1254_C367, Master },
114115
Attachments: { 0x1941_A469, Master },
@@ -131,9 +132,28 @@ ebml_master_elements! {
131132
TimecodeScale: { 0x2AD7_B1, UnsignedInt },
132133
MuxingApp: { 0x4D80, Utf8 },
133134
WritingApp: { 0x5741, Utf8 },
135+
Duration: { 0x4489, Float },
134136
],
135137
},
136138

139+
// segment.cluster
140+
Cluster: {
141+
id: 0x1F43_B675,
142+
children: [
143+
Timestamp: { 0xE7, UnsignedInt },
144+
SimpleBlock: { 0xA3, Binary },
145+
BlockGroup: { 0xA0, Master },
146+
],
147+
},
148+
149+
// segment.cluster.blockGroup
150+
BlockGroup: {
151+
id: 0xA0,
152+
children: [
153+
Block: { 0xA1, Binary },
154+
]
155+
},
156+
137157
// segment.tracks
138158
Tracks: {
139159
id: 0x1654_AE6B,
@@ -726,6 +746,15 @@ where
726746
}
727747
}
728748

749+
impl<'a, R> Read for ElementChildIterator<'a, R>
750+
where
751+
R: Read,
752+
{
753+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
754+
self.reader.read(buf)
755+
}
756+
}
757+
729758
impl<'a, R> Deref for ElementChildIterator<'a, R>
730759
where
731760
R: Read,

lofty/src/ebml/properties.rs

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use super::Language;
22
use crate::properties::FileProperties;
33

4+
use std::time::Duration;
5+
46
/// Properties from the EBML header
57
///
68
/// These are present for all EBML formats.
@@ -77,6 +79,7 @@ pub struct SegmentInfo {
7779
pub(crate) timestamp_scale: u64,
7880
pub(crate) muxing_app: String,
7981
pub(crate) writing_app: String,
82+
pub(crate) duration: Option<Duration>,
8083
}
8184

8285
impl SegmentInfo {
@@ -100,6 +103,14 @@ impl SegmentInfo {
100103
pub fn writing_app(&self) -> &str {
101104
&self.writing_app
102105
}
106+
107+
/// The duration of the segment
108+
///
109+
/// NOTE: This information is not always present in the segment, in which case
110+
/// [`EbmlProperties::duration`] should be used.
111+
pub fn duration(&self) -> Option<Duration> {
112+
self.duration
113+
}
103114
}
104115

105116
impl Default for SegmentInfo {
@@ -109,6 +120,7 @@ impl Default for SegmentInfo {
109120
timestamp_scale: 1_000_000,
110121
muxing_app: String::new(),
111122
writing_app: String::new(),
123+
duration: None,
112124
}
113125
}
114126
}
@@ -210,11 +222,15 @@ impl AudioTrackDescriptor {
210222
/// Settings for an audio track
211223
#[derive(Debug, Clone, PartialEq, Default)]
212224
pub struct AudioTrackSettings {
225+
// Provided to us for free
213226
pub(crate) sampling_frequency: f64,
214227
pub(crate) output_sampling_frequency: f64,
215228
pub(crate) channels: u8,
216229
pub(crate) bit_depth: Option<u8>,
217230
pub(crate) emphasis: Option<EbmlAudioTrackEmphasis>,
231+
232+
// Need to be calculated
233+
pub(crate) bitrate: Option<u32>,
218234
}
219235

220236
impl AudioTrackSettings {
@@ -322,27 +338,67 @@ impl EbmlProperties {
322338

323339
/// Information about the default audio track
324340
///
325-
/// The information is extracted from the first audio track with its default flag set
326-
/// in the `\Segment\Tracks` element.
341+
/// The "default" track is selected as:
342+
/// 1. The first audio track with its `default` flag set
343+
/// 2. If 1 fails, just grab the first audio track with its `enabled` flag set
327344
pub fn default_audio_track(&self) -> Option<&AudioTrackDescriptor> {
328-
self.audio_tracks.iter().find(|track| track.default)
345+
if let Some(position) = self.default_audio_track_position() {
346+
return self.audio_tracks.get(position);
347+
}
348+
349+
None
350+
}
351+
352+
// TODO: Actually calculate from cluster
353+
/// The duration of the default audio track
354+
///
355+
/// NOTE: see [`EbmlProperties::default_audio_track`]
356+
///
357+
/// This will always use the duration written in `\Segment\Info` if present. Otherwise, it will
358+
/// be manually calculated using `\Segment\Cluster` data.
359+
pub fn duration(&self) -> Duration {
360+
self.segment_info.duration().unwrap()
361+
}
362+
363+
/// Audio bitrate (kbps)
364+
///
365+
/// NOTE: This is the bitrate of the default audio track see [`EbmlProperties::default_audio_track`]
366+
/// for what this means.
367+
pub fn bitrate(&self) -> Option<u32> {
368+
self.default_audio_track()
369+
.and_then(|track| track.settings.bitrate)
370+
}
371+
372+
pub(crate) fn default_audio_track_position(&self) -> Option<usize> {
373+
self.audio_tracks
374+
.iter()
375+
.position(|track| track.default)
376+
.or_else(|| {
377+
// Otherwise, it's normal to just pick the first enabled track
378+
self.audio_tracks.iter().position(|track| track.enabled)
379+
})
329380
}
330381
}
331382

332383
impl From<EbmlProperties> for FileProperties {
333384
fn from(input: EbmlProperties) -> Self {
334385
let Some(default_audio_track) = input.default_audio_track() else {
335-
return FileProperties::default();
386+
let mut properties = FileProperties::default();
387+
if let Some(duration) = input.segment_info.duration {
388+
properties.duration = duration;
389+
}
390+
391+
return properties;
336392
};
337393

338394
Self {
339-
duration: todo!("Support duration"),
340-
overall_bitrate: todo!("Support bitrate"),
341-
audio_bitrate: todo!("Support bitrate"),
395+
duration: input.duration(),
396+
overall_bitrate: input.bitrate(),
397+
audio_bitrate: input.bitrate(),
342398
sample_rate: Some(default_audio_track.settings.sampling_frequency as u32),
343399
bit_depth: default_audio_track.settings.bit_depth,
344400
channels: Some(default_audio_track.settings.channels),
345-
channel_mask: todo!("Channel mask"),
401+
channel_mask: None, // TODO: Will require reading into track data
346402
}
347403
}
348404
}

lofty/src/ebml/read.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod segment;
22
mod segment_attachments;
33
mod segment_chapters;
4+
mod segment_cluster;
45
mod segment_info;
56
mod segment_tags;
67
mod segment_tracks;

lofty/src/ebml/read/segment.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{segment_attachments, segment_info, segment_tags, segment_tracks};
1+
use super::{segment_attachments, segment_cluster, segment_info, segment_tags, segment_tracks};
22
use crate::config::ParseOptions;
33
use crate::ebml::element_reader::{ElementHeader, ElementIdent, ElementReader, ElementReaderYield};
44
use crate::ebml::properties::EbmlProperties;
@@ -30,6 +30,13 @@ where
3030
properties,
3131
)?;
3232
},
33+
ElementIdent::Cluster if parse_options.read_properties => {
34+
segment_cluster::read_from(
35+
&mut children_reader.children(),
36+
parse_options,
37+
properties,
38+
)?
39+
},
3340
ElementIdent::Tracks if parse_options.read_properties => {
3441
segment_tracks::read_from(
3542
&mut children_reader.children(),
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use crate::config::ParseOptions;
2+
use crate::ebml::element_reader::{
3+
ChildElementDescriptor, ElementChildIterator, ElementIdent, ElementReaderYield,
4+
};
5+
use crate::ebml::properties::EbmlProperties;
6+
use crate::ebml::{AudioTrackDescriptor, VInt};
7+
use crate::error::Result;
8+
9+
use std::io::{Read, Seek};
10+
11+
pub(super) fn read_from<R>(
12+
children_reader: &mut ElementChildIterator<'_, R>,
13+
parse_options: ParseOptions,
14+
properties: &mut EbmlProperties,
15+
) -> Result<()>
16+
where
17+
R: Read + Seek,
18+
{
19+
// TODO: Support Tracks appearing after Cluster (should implement SeekHead first)
20+
let Some(default_audio_track_position) = properties.default_audio_track_position() else {
21+
log::warn!(
22+
"No default audio track found (does \\Segment\\Cluster appear before \
23+
\\Segment\\Tracks?)"
24+
);
25+
children_reader.exhaust_current_master()?;
26+
return Ok(());
27+
};
28+
29+
let default_audio_track = &properties.audio_tracks[default_audio_track_position];
30+
31+
let target_track_number = default_audio_track.number();
32+
let mut total_audio_data_size = 0u64;
33+
34+
while let Some(child) = children_reader.next()? {
35+
let ident;
36+
let size;
37+
match child {
38+
ElementReaderYield::Master((master_ident, master_size)) => {
39+
ident = master_ident;
40+
size = master_size;
41+
},
42+
ElementReaderYield::Child((descriptor, child_size)) => {
43+
ident = descriptor.ident;
44+
size = child_size;
45+
},
46+
ElementReaderYield::Unknown(unknown) => {
47+
children_reader.skip_element(unknown)?;
48+
continue;
49+
},
50+
ElementReaderYield::Eof => break,
51+
}
52+
53+
match ident {
54+
ElementIdent::Timestamp => {
55+
// TODO: Fancy timestamp durations
56+
children_reader.skip(size.value())?;
57+
continue;
58+
},
59+
ElementIdent::SimpleBlock => {
60+
let (block_is_applicable, header_size) = check_block(
61+
children_reader,
62+
parse_options,
63+
size.value(),
64+
target_track_number,
65+
properties.header.max_size_length,
66+
)?;
67+
68+
if !block_is_applicable {
69+
continue;
70+
}
71+
72+
total_audio_data_size += (size.value() - header_size as u64);
73+
},
74+
ElementIdent::BlockGroup => read_block_group(
75+
&mut children_reader.children(),
76+
parse_options,
77+
properties,
78+
target_track_number,
79+
&mut total_audio_data_size,
80+
)?,
81+
_ => unreachable!("Unhandled child element in \\Segment\\Cluster: {child:?}"),
82+
}
83+
}
84+
85+
if total_audio_data_size == 0 {
86+
log::warn!("No audio data found, audio bitrate will be 0, duration may be 0");
87+
return Ok(());
88+
}
89+
90+
let duration_millis = properties.duration().as_secs() as u128;
91+
if duration_millis == 0 {
92+
log::warn!("Duration is zero, cannot calculate bitrate");
93+
return Ok(());
94+
}
95+
96+
let default_audio_track = &mut properties.audio_tracks[default_audio_track_position]; // TODO
97+
98+
let bitrate_bps = (((total_audio_data_size as u128) * 8) / duration_millis) as u32;
99+
default_audio_track.settings.bitrate = Some(bitrate_bps / 1000);
100+
101+
Ok(())
102+
}
103+
104+
fn read_block_group<R>(
105+
children_reader: &mut ElementChildIterator<'_, R>,
106+
parse_options: ParseOptions,
107+
properties: &mut EbmlProperties,
108+
target_track_number: u64,
109+
total_audio_data_size: &mut u64,
110+
) -> Result<()>
111+
where
112+
R: Read + Seek,
113+
{
114+
while let Some(child) = children_reader.next()? {
115+
let size;
116+
match child {
117+
ElementReaderYield::Child((
118+
ChildElementDescriptor {
119+
ident: ElementIdent::Block,
120+
..
121+
},
122+
child_size,
123+
)) => {
124+
size = child_size;
125+
},
126+
ElementReaderYield::Unknown(unknown) => {
127+
children_reader.skip_element(unknown)?;
128+
continue;
129+
},
130+
_ => unimplemented!(
131+
"Unhandled child element in \\Segment\\Cluster\\BlockGroup: {child:?}"
132+
),
133+
}
134+
135+
let (block_is_applicable, header_size) = check_block(
136+
children_reader,
137+
parse_options,
138+
size.value(),
139+
target_track_number,
140+
properties.header.max_size_length,
141+
)?;
142+
143+
if !block_is_applicable {
144+
continue;
145+
}
146+
147+
*total_audio_data_size += (size.value() - header_size as u64);
148+
}
149+
150+
Ok(())
151+
}
152+
153+
fn check_block<R>(
154+
children_reader: &mut ElementChildIterator<'_, R>,
155+
_parse_options: ParseOptions,
156+
block_size: u64,
157+
target_track_number: u64,
158+
max_size_length: u8,
159+
) -> Result<(bool, u8)>
160+
where
161+
R: Read + Seek,
162+
{
163+
// The block header is Track number (variable), timestamp (i16), and flags (u8)
164+
const NON_VARIABLE_BLOCK_HEADER_SIZE: u8 = 2 /* Timestamp */ + 1 /* Flags */;
165+
166+
let track_number = VInt::<u64>::parse(children_reader, max_size_length)?;
167+
let track_number_octets = track_number.octet_length();
168+
169+
children_reader.skip(block_size - track_number_octets as u64)?;
170+
if track_number != target_track_number {
171+
return Ok((false, track_number_octets + NON_VARIABLE_BLOCK_HEADER_SIZE));
172+
}
173+
174+
Ok((true, track_number_octets + NON_VARIABLE_BLOCK_HEADER_SIZE))
175+
}

0 commit comments

Comments
 (0)