use crate::error::{Error, Result};
use crate::matroska::last_packet_duration;
use crate::mkv::mux::bytes::{invalid, refuse};
use crate::mkv::mux::copy::{PREFERRED_SCALE, PacketTiming};
use crate::mkv::mux::mp4_edit::EditPlan;
use crate::mkv::mux::spec::TrackSpec;
use crate::mkv::mux::timing::{gcd, nanos_of};
use crate::packet::Packet;
use crate::track::{Track, TrackKind};
pub(super) fn matroska_timing(
track: &Track,
packets: &[Packet],
) -> Result<Vec<Option<PacketTiming>>> {
let last = last_packet_duration(track, packets);
packets
.iter()
.enumerate()
.map(|(n, packet)| {
if packet.pts < 0 {
return Err(negative(track, packet.index, packet.pts));
}
Ok(Some(PacketTiming {
pts: packet.pts,
duration: if n + 1 == packets.len() {
last
} else {
packet.duration
},
discard_padding: packet.discard_padding,
}))
})
.collect()
}
pub(super) fn negative(track: &Track, index: u64, pts: i64) -> Error {
refuse(format!(
"packet {index} of track {track} at pts {pts}: a Matroska block timestamp is never \
negative, and an MP4 edit list holds a frame the edit cuts",
track = track.id
))
}
pub(super) fn mp4_timing(
track: &Track,
edit: &EditPlan,
framed_text: bool,
packets: &[Packet],
) -> Result<Vec<Option<PacketTiming>>> {
let mut out = Vec::with_capacity(packets.len());
let mut last = None;
let mut media_end = 0i128;
for (n, packet) in packets.iter().enumerate() {
if packet.description_index != 1 {
return Err(Error::SampleDescriptionChange {
track_id: track.id,
descriptions: u64::from(packet.description_index),
});
}
if framed_text && packet.size == 2 {
out.push(None);
continue;
}
let pts = packet
.pts
.checked_add(edit.shift)
.ok_or_else(|| negative(track, packet.index, packet.pts))?;
if pts < 0 {
return Err(negative(track, packet.index, pts));
}
let end = i128::from(packet.pts) + i128::from(packet.duration.unwrap_or(0));
media_end = media_end.max(end);
last = Some(n);
out.push(Some(PacketTiming {
pts,
duration: packet.duration,
discard_padding: None,
}));
}
let padding = end_trim(track, edit, media_end)?;
if let (Some(padding), Some(slot)) = (padding, last)
&& let Some(Some(timing)) = out.get_mut(slot)
{
let covered = timing
.duration
.is_some_and(|duration| i128::from(duration) >= i128::from(padding.ticks));
if !covered {
return Err(refuse(format!(
"the edit list of track {} cuts {} ticks from the end, more than its last \
packet lasts: `DiscardPadding` trims one block",
track.id, padding.ticks
)));
}
timing.discard_padding = Some(padding.nanos);
}
Ok(out)
}
struct EndTrim {
ticks: i64,
nanos: i64,
}
fn end_trim(track: &Track, edit: &EditPlan, media_end: i128) -> Result<Option<EndTrim>> {
let Some(gap_scaled) = end_gap(track, edit, media_end)? else {
return Ok(None);
};
let movie = edit.movie_timescale;
let nanos = (gap_scaled % movie == 0)
.then(|| nanos_of(gap_scaled / movie, track.timebase))
.flatten()
.and_then(|nanos| i64::try_from(nanos).ok());
let ticks = i64::try_from(gap_scaled / movie).ok();
let (Some(nanos), Some(ticks)) = (nanos, ticks) else {
return Err(refuse(format!(
"the edit list of track {}: its end trim of {gap_scaled} over {movie} ticks at {} \
ticks per second is no whole number of nanoseconds. `StreamCopy::plan_rounded` \
rounds it",
track.id, track.timebase.den
)));
};
Ok(Some(EndTrim { ticks, nanos }))
}
pub(super) fn end_gap(track: &Track, edit: &EditPlan, media_end: i128) -> Result<Option<i128>> {
let Some(end_scaled) = edit.end_scaled else {
return Ok(None);
};
let gap_scaled = media_end * edit.movie_timescale - end_scaled;
if gap_scaled <= i128::from(track.timebase.den) {
return Ok(None);
}
if track.kind != TrackKind::Audio {
return Err(refuse(format!(
"the edit list of track {}: it ends before the last packet does, and Matroska trims \
the end of audio only",
track.id
)));
}
Ok(Some(gap_scaled))
}
fn exact_nanos(track: &Track, what: &str, value: i128) -> Result<i128> {
nanos_of(value, track.timebase).ok_or_else(|| {
refuse(format!(
"{what} {value} of track {} at {}/{} seconds per tick: that time is no whole number \
of nanoseconds, and `TimestampScale` counts whole nanoseconds. No scale holds it \
exactly. To round it to the nearest tick, use `StreamCopy::plan_rounded`",
track.id, track.timebase.num, track.timebase.den
))
})
}
pub(super) fn set_codec_delay(track: &Track, edit: &EditPlan, spec: &mut TrackSpec) -> Result<()> {
if edit.delay <= 0 {
return Ok(());
}
let nanos = exact_nanos(track, "the edit list media time", i128::from(edit.delay))?;
spec.matroska.codec_delay = u64::try_from(nanos).map_err(|_| {
invalid(format!(
"track {}: the `CodecDelay` of {nanos} ns exceeds 64 bits. Use a shorter edit",
track.id
))
})?;
Ok(())
}
pub(super) fn uniform_duration(
track: &Track,
timing: &[Option<PacketTiming>],
) -> Result<Option<u64>> {
let Some(duration) = uniform_span(timing.iter().flatten().map(|block| block.duration)) else {
return Ok(None);
};
let nanos = exact_nanos(track, "the duration", i128::from(duration))?;
Ok(u64::try_from(nanos).ok())
}
pub(super) fn uniform_span(durations: impl Iterator<Item = Option<u64>>) -> Option<u64> {
let mut durations = durations.peekable();
let first = durations.next()?;
durations.peek()?;
let duration = first.filter(|&duration| duration > 0)?;
while let Some(next) = durations.next() {
if durations.peek().is_some() && next != Some(duration) {
return None;
}
}
Some(duration)
}
pub(super) fn exact_scale(track: &Track, timing: &[Option<PacketTiming>]) -> Result<u64> {
let mut scale = u128::from(PREFERRED_SCALE);
for block in timing.iter().flatten() {
let pts = exact_nanos(track, "the pts", i128::from(block.pts))?;
scale = gcd(scale, pts.unsigned_abs());
if let Some(duration) = block.duration {
let nanos = exact_nanos(track, "the duration", i128::from(duration))?;
scale = gcd(scale, nanos.unsigned_abs());
}
}
Ok(u64::try_from(scale).unwrap_or(1))
}