use std::collections::VecDeque;
use crate::error::{Container, Error, Result};
use crate::mp4::mux::options::{MAX_CHUNK_BYTES, MAX_CHUNK_SAMPLES};
use crate::mp4::mux::spec::{PacketFormat, PacketIn, TrackSpec};
use crate::mp4::mux::table::SampleTable;
const EMPTY_TEXT: [u8; 2] = [0, 0];
#[derive(Debug)]
pub(crate) struct Chunk {
pub first_dts: i64,
pub data: Vec<u8>,
pub samples: u32,
}
#[derive(Clone, Copy, Debug)]
struct Pending {
dts: i64,
pts: i64,
duration: Option<u64>,
index: u64,
}
struct Accepted {
pending_delta: Option<u32>,
filler: Option<(i64, u32)>,
offset: i64,
size: u32,
}
#[derive(Debug)]
pub(crate) struct TrackState {
pub id: u32,
pub spec: TrackSpec,
pub entry: Vec<u8>,
pub table: SampleTable,
pending: Option<Pending>,
packets: u64,
text_end: i64,
chunk_ticks: u64,
pub open: Option<Chunk>,
pub closed: VecDeque<Chunk>,
last_dts: Option<i64>,
}
impl TrackState {
pub fn new(id: u32, spec: TrackSpec, entry: Vec<u8>, chunk_ms: u32) -> Self {
let chunk_ticks = (u64::from(chunk_ms) * u64::from(spec.timescale) / 1000).max(1);
Self {
id,
spec,
entry,
table: SampleTable::default(),
pending: None,
packets: 0,
text_end: 0,
chunk_ticks,
open: None,
closed: VecDeque::new(),
last_dts: None,
}
}
fn invalid(&self, reason: String) -> Error {
Error::InvalidPacket {
container: Container::Mp4,
track_id: self.id,
index: self.packets,
reason,
}
}
fn delta(&self, prev: &Pending, dts: i64) -> Result<u32> {
if dts <= prev.dts {
return Err(Error::DtsOutOfOrder {
track_id: self.id,
index: self.packets,
dts,
previous: prev.dts,
});
}
let delta = dts
.checked_sub(prev.dts)
.and_then(|d| u32::try_from(d).ok());
let Some(delta) = delta else {
return Err(self.invalid(format!(
"dts {dts} is more than 2^32 - 1 ticks after the previous dts {}, the \
largest `stts` delta",
prev.dts
)));
};
if let Some(declared) = prev.duration
&& declared != u64::from(delta)
{
return Err(Error::InvalidPacket {
container: Container::Mp4,
track_id: self.id,
index: prev.index,
reason: format!(
"the duration is {declared}, but the next packet's dts {dts} gives \
{delta}. MP4 stores durations as dts deltas. Make them agree, or \
leave the duration unset on every packet but the last"
),
});
}
Ok(delta)
}
fn offset(&self, packet: &PacketIn<'_>, signed: bool) -> Result<i64> {
let refuse = |reason: String| Error::CompositionOffset {
track_id: self.id,
index: self.packets,
pts: packet.pts,
dts: packet.dts,
reason,
};
let offset = packet
.pts
.checked_sub(packet.dts)
.ok_or_else(|| refuse("pts - dts overflows 64 bits".to_owned()))?;
if offset < 0 && !signed {
return Err(refuse(
"pts is before dts, and the `mp41` brand stores unsigned composition \
offsets. Use `Brand::Isom`, `Brand::Mp42` or `Brand::QuickTime`"
.to_owned(),
));
}
if i32::try_from(offset).is_err() {
return Err(refuse(format!(
"the composition offset {offset} is outside the range readers accept, \
-2^31 to 2^31 - 1. Lower the distance between pts and dts"
)));
}
Ok(offset)
}
fn accept(&self, packet: &PacketIn<'_>, signed: bool) -> Result<Accepted> {
let framing = match self.spec.packet_format {
PacketFormat::AsIs => 0,
PacketFormat::PlainText => 2,
};
let size = u32::try_from(packet.data.len() + framing)
.ok()
.filter(|_| framing == 0 || packet.data.len() <= usize::from(u16::MAX));
let Some(size) = size else {
return Err(self.invalid(format!(
"the packet holds {} bytes, above the sample limit of {}. Split the packet",
packet.data.len(),
if framing == 0 {
u64::from(u32::MAX)
} else {
u64::from(u16::MAX)
}
)));
};
let mut filler = None;
let mut prev = self.pending;
if self.spec.packet_format == PacketFormat::PlainText {
if packet.pts != packet.dts {
return Err(self.invalid(format!(
"a text cue has pts {} and dts {}. Text cues need pts == dts",
packet.pts, packet.dts
)));
}
if packet.duration.is_none() && self.spec.default_duration.is_none() {
return Err(Error::MissingDuration {
track_id: self.id,
index: self.packets,
});
}
if packet.dts < self.text_end {
return Err(Error::Unsupported {
container: Container::Mp4,
what: format!(
"overlapping text cues: track {} cue {} starts at {} before the \
previous cue ends at {}",
self.id, self.packets, packet.dts, self.text_end
),
use_instead: Container::Matroska,
});
}
if packet.dts > self.text_end {
let gap = u32::try_from(packet.dts - self.text_end).map_err(|_| {
self.invalid(format!(
"the gap of {} ticks before the cue exceeds 2^32 - 1. Send a cue inside the gap",
packet.dts - self.text_end
))
})?;
filler = Some((self.text_end, gap));
}
}
let mut pending_delta = None;
if let Some((filler_dts, _)) = filler {
if let Some(prev) = &prev {
pending_delta = Some(self.delta(prev, filler_dts)?);
}
prev = Some(Pending {
dts: filler_dts,
pts: filler_dts,
duration: None,
index: self.packets,
});
}
match &prev {
Some(prev) => {
let delta = self.delta(prev, packet.dts)?;
if filler.is_none() {
pending_delta = Some(delta);
}
}
None if packet.dts != 0 => {
return Err(self.invalid(format!(
"the first packet has dts {}, and MP4 decodes the first sample at 0. \
Shift the track's timestamps so its first dts is 0, and put the delay \
in the edit list",
packet.dts
)));
}
None => {}
}
let samples = u64::from(self.table.count) + 1 + u64::from(filler.is_some());
if samples > u64::from(u32::MAX) {
return Err(
self.invalid("the track holds 2^32 - 1 samples, the most `stsz` counts. Split the output across files".to_owned())
);
}
let offset = self.offset(packet, signed)?;
if let Some(duration) = packet.duration
&& u32::try_from(duration).is_err()
{
return Err(self.invalid(format!(
"the duration {duration} exceeds 2^32 - 1, the largest `stts` delta. Use a shorter duration"
)));
}
Ok(Accepted {
pending_delta,
filler,
offset,
size,
})
}
pub fn push(&mut self, packet: &PacketIn<'_>, signed: bool) -> Result<usize> {
let accepted = self.accept(packet, signed)?;
if let Some(delta) = accepted.pending_delta
&& let Some(pending) = self.pending.take()
{
self.table.push_duration(delta, pending.pts);
}
let mut added = 0;
if let Some((dts, gap)) = accepted.filler {
self.table.push_sample(2, 0, true);
self.table.push_duration(gap, dts);
added += self.append(dts, &[&EMPTY_TEXT[..]]);
}
self.table
.push_sample(accepted.size, accepted.offset, packet.sync);
added += match self.spec.packet_format {
PacketFormat::AsIs => self.append(packet.dts, &[packet.data]),
PacketFormat::PlainText => {
let len = (packet.data.len() as u16).to_be_bytes();
self.append(packet.dts, &[&len[..], packet.data])
}
};
let duration = packet
.duration
.or_else(|| self.spec.default_duration.map(u64::from));
if self.spec.packet_format == PacketFormat::PlainText {
self.text_end = packet.dts.saturating_add(duration.unwrap_or(0) as i64);
}
self.pending = Some(Pending {
dts: packet.dts,
pts: packet.pts,
duration: packet.duration,
index: self.packets,
});
self.last_dts = Some(packet.dts);
self.packets += 1;
Ok(added)
}
fn append(&mut self, dts: i64, parts: &[&[u8]]) -> usize {
if let Some(open) = &self.open
&& dts.saturating_sub(open.first_dts) as u64 >= self.chunk_ticks
{
self.close_open();
}
let chunk = self.open.get_or_insert_with(|| Chunk {
first_dts: dts,
data: Vec::new(),
samples: 0,
});
let mut added = 0;
for part in parts {
chunk.data.extend_from_slice(part);
added += part.len();
}
chunk.samples += 1;
if chunk.data.len() >= MAX_CHUNK_BYTES || chunk.samples >= MAX_CHUNK_SAMPLES {
self.close_open();
}
added
}
pub fn close_open(&mut self) {
if let Some(chunk) = self.open.take() {
self.closed.push_back(chunk);
}
}
pub fn lower_bound(&self) -> Option<i64> {
self.closed
.front()
.or(self.open.as_ref())
.map(|chunk| chunk.first_dts)
.or(self.last_dts)
}
pub fn finish(&mut self) -> Result<()> {
if let Some(pending) = self.pending {
let duration = pending
.duration
.or_else(|| self.spec.default_duration.map(u64::from))
.ok_or(Error::MissingDuration {
track_id: self.id,
index: pending.index,
})?;
let duration = u32::try_from(duration).unwrap_or(u32::MAX);
self.table.push_duration(duration, pending.pts);
self.pending = None;
}
self.close_open();
Ok(())
}
}