use broadcast_common::{Package, Unpackage};
use transmux::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use transmux::media::{CmafMux, Fmp4Demux, Media, Track};
use transmux::nalu_types::{AvcPps, AvcSps};
use transmux::pipeline::{
CodecConfig, FragmentTrackData, Sample, TrackSpec, build_init_segment, build_media_segment,
};
use transmux::rebase::{apply_offset, insert_discontinuity_gap, rebase_to_zero};
fn avc_spec() -> TrackSpec {
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, 0xc0, 0x1e, 0xd9, 0x00, 0x80, 0x1e, 0x24,
])],
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,
},
)
}
fn sample_at(dts: i64, duration: u32) -> Sample {
let nal = [0x65u8, 0x88, 0x84, 0x00];
let mut data = (nal.len() as u32).to_be_bytes().to_vec();
data.extend_from_slice(&nal);
Sample::new(data, Some(dts), Some(dts), Some(duration), true)
}
fn media_with_anchor(start: u64, durs: &[u32]) -> Media {
let mut dts = start as i64;
let mut samples = Vec::with_capacity(durs.len());
for &d in durs {
samples.push(sample_at(dts, d));
dts += i64::from(d);
}
Media::new(vec![Track::new_at(avc_spec(), samples, start)], 90_000)
}
fn dts_seq(media: &Media) -> Vec<Option<i64>> {
media.tracks[0].samples.iter().map(|s| s.dts).collect()
}
fn muxed_tfdt(fmp4: &[u8]) -> u64 {
use transmux::movie_fragment::MovieFragmentBox;
let mut off = 0usize;
while off + 8 <= fmp4.len() {
let sz =
u32::from_be_bytes([fmp4[off], fmp4[off + 1], fmp4[off + 2], fmp4[off + 3]]) as usize;
if sz < 8 {
break;
}
if &fmp4[off + 4..off + 8] == b"moof" {
let moof = MovieFragmentBox::parse_body(&fmp4[off + 8..off + sz]).expect("parse moof");
return moof.traf[0]
.tfdt
.as_ref()
.expect("traf must carry a tfdt")
.base_media_decode_time();
}
off += sz;
}
panic!("no moof in muxed fMP4");
}
#[test]
fn fmp4_demux_populates_start_decode_time_from_tfdt() {
const KNOWN_BASE: u64 = 123_456;
let spec = avc_spec();
let samples = [sample_at(0, 3000), sample_at(3000, 3000)];
let mut fmp4 = build_init_segment(std::slice::from_ref(&spec), 90_000).expect("init");
let frag = FragmentTrackData::new(1, KNOWN_BASE, &samples);
let media_seg = build_media_segment(1, &[frag]).expect("media segment");
fmp4.extend_from_slice(&media_seg);
let media = Fmp4Demux::new().unpackage(&fmp4).expect("demux");
assert_eq!(media.tracks.len(), 1);
assert_eq!(
media.tracks[0].start_decode_time, KNOWN_BASE,
"Fmp4Demux must set start_decode_time from the first fragment tfdt"
);
assert_eq!(
dts_seq(&media),
vec![Some(KNOWN_BASE as i64), Some(KNOWN_BASE as i64 + 3000)],
"sample dts must be absolute, anchored on the fragment tfdt"
);
}
#[test]
fn rebase_to_zero_end_to_end() {
const ANCHOR: u64 = 900_000;
let mut media = media_with_anchor(ANCHOR, &[3000, 3000, 3000]);
let before = CmafMux::default().package(&media).expect("package");
assert_eq!(
muxed_tfdt(&before),
ANCHOR,
"muxed tfdt must equal the track anchor before rebase"
);
rebase_to_zero(&mut media);
assert_eq!(media.tracks[0].start_decode_time, 0);
assert_eq!(
dts_seq(&media),
vec![Some(0), Some(3000), Some(6000)],
"rebase_to_zero must move the samples, not just the anchor"
);
let after = CmafMux::default().package(&media).expect("package");
assert_eq!(
muxed_tfdt(&after),
0,
"muxed tfdt must be 0 after rebase_to_zero"
);
}
#[test]
fn apply_offset_bites() {
const ANCHOR: u64 = 100_000;
const DELTA: i64 = 90_000;
let mut media = media_with_anchor(ANCHOR, &[3000, 3000]);
apply_offset(&mut media, DELTA);
assert_eq!(media.tracks[0].start_decode_time, ANCHOR + DELTA as u64);
assert_eq!(
dts_seq(&media),
vec![
Some(ANCHOR as i64 + DELTA),
Some(ANCHOR as i64 + DELTA + 3000)
],
"apply_offset must shift every sample's absolute dts too"
);
let fmp4 = CmafMux::default().package(&media).expect("package");
assert_eq!(
muxed_tfdt(&fmp4),
ANCHOR + DELTA as u64,
"muxed tfdt must reflect the applied offset"
);
}
#[test]
fn insert_discontinuity_gap_bites() {
const GAP: u32 = 4500;
let mut media = media_with_anchor(0, &[3000, 3000, 3000, 3000]);
let track = &mut media.tracks[0];
insert_discontinuity_gap(track, 2, GAP);
let seq: Vec<Option<i64>> = track.samples.iter().map(|s| s.dts).collect();
assert_eq!(
seq,
vec![
Some(0),
Some(3000),
Some(6000 + i64::from(GAP)),
Some(9000 + i64::from(GAP)),
],
"the gap must push the insertion point and every later sample out by exactly GAP"
);
assert_eq!(
track.start_decode_time, 0,
"a mid-track gap leaves the anchor alone"
);
}
#[test]
fn insert_discontinuity_gap_at_zero_shifts_anchor_and_samples() {
const GAP: u32 = 250;
let mut media = media_with_anchor(1000, &[100, 100]);
let track = &mut media.tracks[0];
insert_discontinuity_gap(track, 0, GAP);
assert_eq!(track.start_decode_time, 1000 + u64::from(GAP));
assert_eq!(
track.samples.iter().map(|s| s.dts).collect::<Vec<_>>(),
vec![Some(1250), Some(1350)]
);
}
#[test]
fn transforms_never_fabricate_a_timestamp() {
let mut media = media_with_anchor(5000, &[100, 100]);
media.tracks[0].samples[1].dts = None;
media.tracks[0].samples[1].pts = None;
rebase_to_zero(&mut media);
assert_eq!(media.tracks[0].samples[1].dts, None, "rebase_to_zero");
assert_eq!(media.tracks[0].samples[1].pts, None, "rebase_to_zero");
apply_offset(&mut media, 1234);
assert_eq!(media.tracks[0].samples[1].dts, None, "apply_offset");
assert_eq!(media.tracks[0].samples[1].pts, None, "apply_offset");
insert_discontinuity_gap(&mut media.tracks[0], 1, 999);
assert_eq!(
media.tracks[0].samples[1].dts, None,
"insert_discontinuity_gap"
);
assert_eq!(
media.tracks[0].samples[1].pts, None,
"insert_discontinuity_gap"
);
}