use crate::media::{Media, Track};
pub const MPEG_TS_WRAP: u64 = 1 << 33;
pub fn rebase_to_zero(media: &mut Media) {
for track in &mut media.tracks {
track.start_decode_time = 0;
}
}
pub fn apply_offset(media: &mut Media, delta_ticks: i64) {
for track in &mut media.tracks {
track.start_decode_time = track.start_decode_time.saturating_add_signed(delta_ticks);
}
}
const MPEG_TS_WRAP_HALF: u64 = MPEG_TS_WRAP / 2;
pub fn unroll_33bit_wraps(media: &mut Media) {
for track in &mut media.tracks {
if track.samples.is_empty() {
continue;
}
let mut folded: alloc::vec::Vec<u64> = alloc::vec::Vec::with_capacity(track.samples.len());
let mut acc = track.start_decode_time % MPEG_TS_WRAP;
folded.push(acc);
for s in track.samples.iter().take(track.samples.len() - 1) {
acc = (acc + s.duration as u64) % MPEG_TS_WRAP;
folded.push(acc);
}
let mut unwrapped: alloc::vec::Vec<u64> = alloc::vec::Vec::with_capacity(folded.len());
unwrapped.push(folded[0]);
for i in 1..folded.len() {
let prev = unwrapped[i - 1];
let cur_folded = folded[i];
let base = (prev / MPEG_TS_WRAP) * MPEG_TS_WRAP;
let mut cur = base + cur_folded;
if cur + MPEG_TS_WRAP_HALF < prev {
cur += MPEG_TS_WRAP;
}
unwrapped.push(cur);
}
track.start_decode_time = unwrapped[0];
for i in 0..track.samples.len() {
let delta = if i + 1 < unwrapped.len() {
unwrapped[i + 1] - unwrapped[i]
} else {
track.samples[i].duration as u64
};
track.samples[i].duration = delta as u32;
}
}
}
pub fn insert_discontinuity_gap(track: &mut Track, at_sample_index: usize, gap_ticks: u32) {
if track.samples.is_empty() {
track.start_decode_time = track.start_decode_time.saturating_add(gap_ticks as u64);
return;
}
if at_sample_index == 0 {
track.start_decode_time = track.start_decode_time.saturating_add(gap_ticks as u64);
return;
}
let idx = at_sample_index.min(track.samples.len()) - 1;
let dur = &mut track.samples[idx].duration;
*dur = dur.saturating_add(gap_ticks);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::media::Track;
use crate::pipeline::{Sample, TrackSpec};
use alloc::vec;
use alloc::vec::Vec;
fn sample(duration: u32) -> Sample {
Sample {
data: vec![0u8; 4],
duration,
is_sync: true,
composition_offset: 0,
source_timing: None,
}
}
fn track_at(start: u64, durs: &[u32]) -> Track {
let samples = durs.iter().map(|&d| sample(d)).collect();
Track::new_at(spec(), samples, start)
}
fn spec() -> TrackSpec {
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::nalu_types::{AvcPps, AvcSps};
use crate::pipeline::CodecConfig;
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 66,
profile_compatibility: 0,
level_indication: 30,
length_size_minus_one: 3,
sps: vec![AvcSps(vec![0x67, 0x42, 0x00, 0x1e])],
pps: vec![AvcPps(vec![0x68, 0xce, 0x3c, 0x80])],
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: vec![],
};
TrackSpec::new(
1,
90_000,
CodecConfig::Avc {
config: AVCConfigurationBox::new(record),
width: 16,
height: 16,
},
)
}
#[test]
fn rebase_to_zero_clears_anchor() {
let mut m = Media::new(vec![track_at(90_000, &[3000, 3000])], 1000);
rebase_to_zero(&mut m);
assert_eq!(m.tracks[0].start_decode_time, 0);
rebase_to_zero(&mut m);
assert_eq!(m.tracks[0].start_decode_time, 0);
}
#[test]
fn apply_offset_saturates() {
let mut m = Media::new(vec![track_at(100, &[10])], 1000);
apply_offset(&mut m, 50);
assert_eq!(m.tracks[0].start_decode_time, 150);
apply_offset(&mut m, -1000);
assert_eq!(m.tracks[0].start_decode_time, 0, "underflow saturates at 0");
}
#[test]
fn unroll_identity_on_monotonic() {
let mut m = Media::new(vec![track_at(1_000_000, &[3000, 3000, 3000])], 1000);
unroll_33bit_wraps(&mut m);
assert_eq!(m.tracks[0].start_decode_time, 1_000_000);
assert_eq!(
m.tracks[0]
.samples
.iter()
.map(|s| s.duration)
.collect::<Vec<_>>(),
vec![3000, 3000, 3000]
);
}
#[test]
fn unroll_lifts_timeline_across_boundary() {
let start = MPEG_TS_WRAP - 3000;
let mut m = Media::new(vec![track_at(start, &[3000, 3000, 3000])], 1000);
unroll_33bit_wraps(&mut m);
let t = &m.tracks[0];
assert_eq!(t.start_decode_time, MPEG_TS_WRAP - 3000);
let mut dts = t.start_decode_time;
let expected = [MPEG_TS_WRAP - 3000, MPEG_TS_WRAP, MPEG_TS_WRAP + 3000];
for (i, s) in t.samples.iter().enumerate() {
assert_eq!(dts, expected[i], "sample {i} DTS");
dts += s.duration as u64;
}
}
#[test]
fn gap_extends_preceding_sample() {
let mut t = track_at(0, &[100, 100, 100]);
let span_before: u32 = t.samples.iter().map(|s| s.duration).sum();
insert_discontinuity_gap(&mut t, 2, 500);
let span_after: u32 = t.samples.iter().map(|s| s.duration).sum();
assert_eq!(span_after - span_before, 500);
assert_eq!(t.samples[1].duration, 600, "preceding sample extended");
assert_eq!(t.samples[0].duration, 100, "earlier sample unchanged");
}
#[test]
fn gap_at_index_zero_shifts_anchor() {
let mut t = track_at(1000, &[100]);
insert_discontinuity_gap(&mut t, 0, 250);
assert_eq!(t.start_decode_time, 1250);
assert_eq!(t.samples[0].duration, 100);
}
}