use std::{
io::{Read, Seek},
ops::ControlFlow,
};
use crate::{
DecodeResult, GaplessInfo,
mp4::{ItunSmpb, Mp4EditListEntry, Mp4MediaTiming, Mp4Visitor, scan_mp4},
traits::DecoderInput,
};
pub fn probe_mp4_gapless<R: Read + Seek + Send + Sync>(
reader: &mut R,
) -> DecodeResult<Option<GaplessInfo>> {
probe_mp4_gapless_dyn(reader)
}
pub(crate) fn probe_mp4_gapless_dyn(
reader: &mut dyn DecoderInput,
) -> DecodeResult<Option<GaplessInfo>> {
let mut probe = GaplessProbe::default();
match scan_mp4(reader, &mut probe) {
Ok(()) => Ok(probe.into()),
Err(crate::mp4::Mp4MetadataError::Io(error)) => Err(error.into()),
Err(crate::mp4::Mp4MetadataError::InvalidData(_)) => Ok(None),
}
}
#[derive(Default)]
struct GaplessProbe {
current: Option<TrackState>,
elst_derived: Option<GaplessInfo>,
itunsmpb: Option<GaplessInfo>,
movie_timescale: Option<u32>,
}
#[derive(Default)]
struct TrackState {
first_edit: Option<Mp4EditListEntry>,
media: Option<Mp4MediaTiming>,
sample_rate: Option<u32>,
}
impl From<GaplessProbe> for Option<GaplessInfo> {
fn from(probe: GaplessProbe) -> Self {
probe.elst_derived.or(probe.itunsmpb)
}
}
impl Mp4Visitor for GaplessProbe {
fn on_itunsmpb(&mut self, info: ItunSmpb) -> ControlFlow<()> {
if info.leading_frames == 0 && info.trailing_frames == 0 {
return ControlFlow::Continue(());
}
self.itunsmpb = Some(GaplessInfo {
leading_frames: info.leading_frames,
trailing_frames: info.trailing_frames,
});
ControlFlow::Break(())
}
fn on_movie_timescale(&mut self, timescale: u32) -> ControlFlow<()> {
self.movie_timescale = Some(timescale);
ControlFlow::Continue(())
}
fn on_track_begin(&mut self) -> ControlFlow<()> {
self.current = Some(TrackState::default());
ControlFlow::Continue(())
}
fn on_track_edit_list(&mut self, entries: &[Mp4EditListEntry]) -> ControlFlow<()> {
if let Some(track) = &mut self.current {
track.first_edit = entries.first().copied();
}
ControlFlow::Continue(())
}
fn on_track_end(&mut self) -> ControlFlow<()> {
let Some(track) = self.current.take() else {
return ControlFlow::Continue(());
};
if let Some(info) = derive_from_track(&track, self.movie_timescale) {
self.elst_derived = Some(info);
return ControlFlow::Break(());
}
ControlFlow::Continue(())
}
fn on_track_media_timing(&mut self, timing: Mp4MediaTiming) -> ControlFlow<()> {
if let Some(track) = &mut self.current {
track.media = Some(timing);
}
ControlFlow::Continue(())
}
fn on_track_sample_rate(&mut self, sample_rate: u32) -> ControlFlow<()> {
if let Some(track) = &mut self.current {
track.sample_rate = Some(sample_rate);
}
ControlFlow::Continue(())
}
}
fn derive_from_track(track: &TrackState, movie_timescale: Option<u32>) -> Option<GaplessInfo> {
let movie_timescale = movie_timescale?;
let sample_rate = track.sample_rate?;
let media = track.media?;
let edit = track.first_edit?;
let media_time = u64::try_from(edit.media_time).ok()?;
let leading = scale_frames(media_time, sample_rate, media.timescale)?;
let valid = scale_frames(edit.segment_duration, sample_rate, movie_timescale)?;
let total = scale_frames(media.duration, sample_rate, media.timescale)?;
let trailing = total.checked_sub(leading.saturating_add(valid))?;
if leading == 0 && trailing == 0 {
return None;
}
Some(GaplessInfo {
leading_frames: leading,
trailing_frames: trailing,
})
}
fn scale_frames(value: u64, numerator: u32, denominator: u32) -> Option<u64> {
if denominator == 0 {
return None;
}
let scaled = u128::from(value)
.saturating_mul(u128::from(numerator))
.saturating_add(u128::from(denominator / 2))
/ u128::from(denominator);
u64::try_from(scaled).ok()
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use kithara_test_utils::kithara;
use super::probe_mp4_gapless;
use crate::GaplessInfo;
fn atom(kind: [u8; 4], payload: &[u8]) -> Vec<u8> {
let size = u32::try_from(payload.len() + 8).unwrap_or(u32::MAX);
let mut out = Vec::with_capacity(payload.len() + 8);
out.extend_from_slice(&size.to_be_bytes());
out.extend_from_slice(&kind);
out.extend_from_slice(payload);
out
}
fn full_box(kind: [u8; 4], version: u8, body: &[u8]) -> Vec<u8> {
let mut payload = vec![version, 0, 0, 0];
payload.extend_from_slice(body);
atom(kind, &payload)
}
fn mvhd(movie_timescale: u32) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&movie_timescale.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
full_box(*b"mvhd", 0, &body)
}
fn mdhd(media_timescale: u32, media_duration: u32) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&media_timescale.to_be_bytes());
body.extend_from_slice(&media_duration.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
full_box(*b"mdhd", 0, &body)
}
fn elst_v0(segment_duration: u32, media_time: i32) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&segment_duration.to_be_bytes());
body.extend_from_slice(&media_time.to_be_bytes());
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
full_box(*b"elst", 0, &body)
}
fn audio_sample_entry(codec: [u8; 4], sample_rate: u32) -> Vec<u8> {
let mut entry = vec![0; 6];
entry.extend_from_slice(&1u16.to_be_bytes());
entry.extend_from_slice(&[0; 8]);
entry.extend_from_slice(&2u16.to_be_bytes());
entry.extend_from_slice(&16u16.to_be_bytes());
entry.extend_from_slice(&0u16.to_be_bytes());
entry.extend_from_slice(&0u16.to_be_bytes());
entry.extend_from_slice(&(sample_rate << 16).to_be_bytes());
atom(codec, &entry)
}
fn stsd(sample_rate: u32) -> Vec<u8> {
let entry = audio_sample_entry(*b"mp4a", sample_rate);
let mut body = Vec::new();
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&entry);
full_box(*b"stsd", 0, &body)
}
fn data_box(data_type: u32, value: &[u8]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&data_type.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(value);
atom(*b"data", &body)
}
fn freeform_text_box(kind: [u8; 4], text: &str) -> Vec<u8> {
let mut body = vec![0, 0, 0, 1];
body.extend_from_slice(text.as_bytes());
atom(kind, &body)
}
fn freeform_itunsmpb(text: &str) -> Vec<u8> {
let mut freeform = Vec::new();
freeform.extend_from_slice(&freeform_text_box(*b"mean", "com.apple.iTunes"));
freeform.extend_from_slice(&freeform_text_box(*b"name", "iTunSMPB"));
freeform.extend_from_slice(&data_box(1, text.as_bytes()));
atom(*b"----", &freeform)
}
fn track_with_elst(
sample_rate: u32,
media_timescale: u32,
media_duration: u32,
segment_duration: u32,
media_time: i32,
) -> Vec<u8> {
let mut stbl = Vec::new();
stbl.extend_from_slice(&stsd(sample_rate));
let minf = atom(*b"minf", &atom(*b"stbl", &stbl));
let mut mdia = Vec::new();
mdia.extend_from_slice(&mdhd(media_timescale, media_duration));
mdia.extend_from_slice(&minf);
let edts = atom(*b"edts", &elst_v0(segment_duration, media_time));
let mut trak = Vec::new();
trak.extend_from_slice(&atom(*b"mdia", &mdia));
trak.extend_from_slice(&edts);
atom(*b"trak", &trak)
}
#[kithara::test]
fn derives_gapless_from_edit_list() {
let mut moov = Vec::new();
moov.extend_from_slice(&mvhd(1_000));
moov.extend_from_slice(&track_with_elst(48_000, 48_000, 96_000, 1_916, 2_112));
let mut reader = Cursor::new(atom(*b"moov", &moov));
assert_eq!(
probe_mp4_gapless(&mut reader).expect("BUG: probe"),
Some(GaplessInfo {
leading_frames: 2_112,
trailing_frames: 1_920,
})
);
}
#[kithara::test]
fn derives_gapless_from_itunsmpb_when_elst_missing() {
let ilst = atom(
*b"ilst",
&freeform_itunsmpb(" 00000000 00000840 00000048 0000000000000000"),
);
let mut meta_payload = vec![0, 0, 0, 0];
meta_payload.extend_from_slice(&ilst);
let mut moov = Vec::new();
moov.extend_from_slice(&mvhd(1_000));
moov.extend_from_slice(&atom(*b"udta", &atom(*b"meta", &meta_payload)));
let mut reader = Cursor::new(atom(*b"moov", &moov));
assert_eq!(
probe_mp4_gapless(&mut reader).expect("BUG: probe"),
Some(GaplessInfo {
leading_frames: 0x840,
trailing_frames: 0x48,
})
);
}
#[kithara::test]
fn elst_takes_priority_over_itunsmpb() {
let ilst = atom(
*b"ilst",
&freeform_itunsmpb(" 00000000 00000010 00000020 0000000000000000"),
);
let mut meta_payload = vec![0, 0, 0, 0];
meta_payload.extend_from_slice(&ilst);
let mut moov = Vec::new();
moov.extend_from_slice(&mvhd(1_000));
moov.extend_from_slice(&track_with_elst(48_000, 48_000, 96_000, 1_916, 2_112));
moov.extend_from_slice(&atom(*b"udta", &atom(*b"meta", &meta_payload)));
let mut reader = Cursor::new(atom(*b"moov", &moov));
assert_eq!(
probe_mp4_gapless(&mut reader).expect("BUG: probe"),
Some(GaplessInfo {
leading_frames: 2_112,
trailing_frames: 1_920,
})
);
}
#[kithara::test]
fn returns_none_without_gapless_metadata() {
let mut moov = Vec::new();
moov.extend_from_slice(&mvhd(1_000));
let mut reader = Cursor::new(atom(*b"moov", &moov));
assert_eq!(probe_mp4_gapless(&mut reader).expect("BUG: probe"), None);
}
#[kithara::test]
fn returns_none_for_zero_padding_itunsmpb() {
let ilst = atom(
*b"ilst",
&freeform_itunsmpb(" 00000000 00000000 00000000 0000000000000000"),
);
let mut meta_payload = vec![0, 0, 0, 0];
meta_payload.extend_from_slice(&ilst);
let mut moov = Vec::new();
moov.extend_from_slice(&mvhd(1_000));
moov.extend_from_slice(&atom(*b"udta", &atom(*b"meta", &meta_payload)));
let mut reader = Cursor::new(atom(*b"moov", &moov));
assert_eq!(probe_mp4_gapless(&mut reader).expect("BUG: probe"), None);
}
}