use crate::error::Result;
use crate::mkv::mux::bytes::{invalid, refuse};
use crate::mkv::mux::copy_timing::{
exact_scale, matroska_timing, mp4_timing, set_codec_delay, uniform_duration,
};
use crate::mkv::mux::from_mp4::{is_tx3g, spec_from_mp4};
use crate::mkv::mux::mp4_edit::edit_plan;
use crate::mkv::mux::rounded::{TimestampRounding, round_mp4};
use crate::mkv::mux::spec::{PacketIn, TrackSpec};
use crate::mkv::mux::timing::gcd;
use crate::packet::Packet;
use crate::track::{Timebase, Track};
pub(super) const PREFERRED_SCALE: u64 = 1_000_000;
impl TrackSpec {
pub fn copy_from(track: &Track) -> Result<Self> {
match &track.matroska {
Some(mkv) => Ok(Self {
timebase: track.timebase,
matroska: mkv.clone(),
metadata: track.metadata.clone(),
}),
None => spec_from_mp4(track),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PacketTiming {
pub pts: i64,
pub duration: Option<u64>,
pub discard_padding: Option<i64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct StreamCopy {
pub spec: TrackSpec,
timing: Vec<Option<PacketTiming>>,
scale: u64,
rounding: Option<TimestampRounding>,
framed_text: bool,
track_id: u32,
}
impl StreamCopy {
pub fn plan(track: &Track, packets: &[Packet]) -> Result<Self> {
check_order(track, packets, "plan")?;
let mut spec = TrackSpec::copy_from(track)?;
let framed_text = track.matroska.is_none() && is_tx3g(track);
let timing = if track.matroska.is_some() {
matroska_timing(track, packets)?
} else {
let edit = edit_plan(track)?;
set_codec_delay(track, &edit, &mut spec)?;
let timing = mp4_timing(track, &edit, framed_text, packets)?;
if !framed_text {
spec.matroska.default_duration = uniform_duration(track, &timing)?;
}
timing
};
let scale = exact_scale(track, &timing)?;
Ok(Self {
spec,
timing,
scale,
rounding: None,
framed_text,
track_id: track.id,
})
}
pub fn plan_rounded(track: &Track, packets: &[Packet], scale: u64) -> Result<Self> {
check_order(track, packets, "plan_rounded")?;
let tick = u32::try_from(scale).ok().filter(|&tick| tick > 0).ok_or_else(|| {
invalid(format!(
"`StreamCopy::plan_rounded` got scale {scale}. Use 1 to {} nanoseconds per tick",
u32::MAX
))
})?;
if track.matroska.is_some() {
return Err(invalid(format!(
"`StreamCopy::plan_rounded` got Matroska track {}. It rounds MP4 tracks. Use \
`StreamCopy::plan`",
track.id
)));
}
let mut spec = TrackSpec::copy_from(track)?;
let framed_text = is_tx3g(track);
let rounded = round_mp4(track, framed_text, packets, tick)?;
spec.timebase = Timebase::from_nanoseconds(tick);
spec.matroska.codec_delay = rounded.codec_delay;
spec.matroska.default_duration = rounded.default_duration;
Ok(Self {
spec,
timing: rounded.timing,
scale,
rounding: Some(rounded.rounding),
framed_text,
track_id: track.id,
})
}
pub fn timestamp_scale(plans: &[StreamCopy]) -> u64 {
let scale = plans.iter().fold(0u64, |scale, plan| {
u64::try_from(gcd(u128::from(scale), u128::from(plan.scale))).unwrap_or(1)
});
if scale == 0 { PREFERRED_SCALE } else { scale }
}
pub fn rounding(&self) -> Option<TimestampRounding> {
self.rounding
}
pub fn timing(&self) -> &[Option<PacketTiming>] {
&self.timing
}
pub fn packet<'a>(&self, packet: &Packet, data: &'a [u8]) -> Result<Option<PacketIn<'a>>> {
let timing = usize::try_from(packet.index)
.ok()
.and_then(|index| self.timing.get(index))
.ok_or_else(|| {
invalid(format!(
"packet {} is past the {} packets planned for track {}. Plan the copy with \
every packet of the track",
packet.index,
self.timing.len(),
self.track_id
))
})?;
let Some(timing) = timing else {
return Ok(None);
};
let data = if self.framed_text {
cue_text(data, self.track_id, packet.index)?
} else {
data
};
Ok(Some(PacketIn {
data,
pts: timing.pts,
duration: timing.duration,
sync: packet.sync,
discardable: packet.discardable,
discard_padding: timing.discard_padding,
}))
}
}
fn check_order(track: &Track, packets: &[Packet], call: &str) -> Result<()> {
for (n, packet) in packets.iter().enumerate() {
if packet.track_id != track.id || packet.index != n as u64 {
return Err(invalid(format!(
"`StreamCopy::{call}` got packet {} of track {} at position {n} for track {}. \
Pass the track's packets in decode order",
packet.index, packet.track_id, track.id
)));
}
}
Ok(())
}
fn cue_text(data: &[u8], track_id: u32, index: u64) -> Result<&[u8]> {
let [high, low, text @ ..] = data else {
return Err(invalid(format!(
"`tx3g` sample {index} of track {track_id} holds {} bytes, below its 2-byte length. \
Remux the source with `ffmpeg -c copy`",
data.len()
)));
};
let len = usize::from(u16::from_be_bytes([*high, *low]));
if text.len() != len {
return Err(refuse(format!(
"`tx3g` sample {index} of track {track_id}: {} bytes follow a text of {len} bytes. \
`S_TEXT/UTF8` holds no style boxes",
text.len()
)));
}
Ok(text)
}