use oxideav_mp4::demux::{parse_mfra_box, parse_prft_box, parse_sidx_box};
fn boxed(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(fourcc);
out.extend_from_slice(body);
out
}
fn build_sidx_v0(
reference_id: u32,
timescale: u32,
ept: u32,
first_offset: u32,
refs: &[(bool, u32, u32, bool, u8)],
) -> Vec<u8> {
let mut body = vec![0u8; 4]; body.extend_from_slice(&reference_id.to_be_bytes());
body.extend_from_slice(×cale.to_be_bytes());
body.extend_from_slice(&ept.to_be_bytes());
body.extend_from_slice(&first_offset.to_be_bytes());
body.extend_from_slice(&[0, 0]); body.extend_from_slice(&(refs.len() as u16).to_be_bytes());
for &(is_sidx, sz, dur, sap, sap_t) in refs {
let r0 = if is_sidx { 0x8000_0000 } else { 0 } | (sz & 0x7FFF_FFFF);
body.extend_from_slice(&r0.to_be_bytes());
body.extend_from_slice(&dur.to_be_bytes());
let r2 = if sap { 0x8000_0000 } else { 0 } | (((sap_t as u32) & 0x7) << 28);
body.extend_from_slice(&r2.to_be_bytes());
}
body
}
fn build_tfra_v0(
track_id: u32,
len_traf: u8,
len_trun: u8,
len_sample: u8,
entries: &[(u32, u32, u32, u32, u32)],
) -> Vec<u8> {
let mut body = vec![0u8; 4]; body.extend_from_slice(&track_id.to_be_bytes());
let lengths: u32 =
(((len_traf - 1) as u32) << 4) | (((len_trun - 1) as u32) << 2) | ((len_sample - 1) as u32);
body.extend_from_slice(&lengths.to_be_bytes());
body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for &(t, m, traf, trun, samp) in entries {
body.extend_from_slice(&t.to_be_bytes());
body.extend_from_slice(&m.to_be_bytes());
push_var(&mut body, traf, len_traf);
push_var(&mut body, trun, len_trun);
push_var(&mut body, samp, len_sample);
}
body
}
fn push_var(out: &mut Vec<u8>, value: u32, n: u8) {
let bytes = value.to_be_bytes();
out.extend_from_slice(&bytes[(4 - n as usize)..]);
}
fn build_mfra(tfras: &[Vec<u8>]) -> Vec<u8> {
let mut mfra_body = Vec::new();
for t in tfras {
mfra_body.extend_from_slice(&boxed(b"tfra", t));
}
let mfro_body = {
let mut b = vec![0u8; 4];
b.extend_from_slice(&0u32.to_be_bytes());
b
};
mfra_body.extend_from_slice(&boxed(b"mfro", &mfro_body));
mfra_body
}
#[test]
fn sidx_v0_single_reference_round_trips() {
let body = build_sidx_v0(
1, 48_000, 0, 16, &[(false, 4096, 1024, true, 1)], );
let sidx_end = 200u64;
let r = parse_sidx_box(&body, sidx_end).unwrap().unwrap();
assert_eq!(r.reference_id, 1);
assert_eq!(r.timescale, 48_000);
assert_eq!(r.earliest_presentation_time, 0);
assert_eq!(r.first_byte_offset, 200 + 16);
assert_eq!(r.references.len(), 1);
let rr = r.references[0];
assert!(!rr.is_sidx);
assert_eq!(rr.referenced_size, 4096);
assert_eq!(rr.subsegment_duration, 1024);
assert!(rr.starts_with_sap);
assert_eq!(rr.sap_type, 1);
}
#[test]
fn sidx_v0_multi_reference_round_trips() {
let body = build_sidx_v0(
2,
90_000,
12_345,
0,
&[
(false, 1000, 90, true, 1),
(false, 2000, 180, true, 1),
(false, 1500, 90, true, 1),
],
);
let r = parse_sidx_box(&body, 0).unwrap().unwrap();
assert_eq!(r.references.len(), 3);
assert_eq!(r.earliest_presentation_time, 12_345);
let total_dur: u32 = r.references.iter().map(|x| x.subsegment_duration).sum();
assert_eq!(total_dur, 360);
}
#[test]
fn sidx_v1_64bit_fields_round_trip() {
let mut body = vec![0u8; 4];
body[0] = 1; body.extend_from_slice(&7u32.to_be_bytes()); body.extend_from_slice(&30u32.to_be_bytes()); body.extend_from_slice(&0xDEAD_BEEF_u64.to_be_bytes()); body.extend_from_slice(&100u64.to_be_bytes()); body.extend_from_slice(&[0, 0]);
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&30u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
let r = parse_sidx_box(&body, 1000).unwrap().unwrap();
assert_eq!(r.reference_id, 7);
assert_eq!(r.earliest_presentation_time, 0xDEAD_BEEF);
assert_eq!(r.first_byte_offset, 1000 + 100);
}
#[test]
fn sidx_truncated_returns_invalid() {
let body = vec![0u8; 4]; assert!(parse_sidx_box(&body, 0).is_err());
}
#[test]
fn sidx_hierarchical_marks_is_sidx() {
let body = build_sidx_v0(
1,
1000,
0,
0,
&[
(true, 200, 10, false, 0), (false, 100, 5, true, 1), ],
);
let r = parse_sidx_box(&body, 0).unwrap().unwrap();
assert!(r.references[0].is_sidx);
assert!(!r.references[1].is_sidx);
}
#[test]
fn tfra_v0_round_trips() {
let tfra_body = build_tfra_v0(
1,
1,
1,
1,
&[
(0, 1024, 1, 1, 1),
(4800, 2048, 1, 1, 1),
(9600, 3072, 1, 1, 1),
],
);
let mfra_body = build_mfra(&[tfra_body]);
let tfras = parse_mfra_box(&mfra_body).unwrap();
assert_eq!(tfras.len(), 1);
let t = &tfras[0];
assert_eq!(t.track_id, 1);
assert_eq!(t.entries.len(), 3);
assert_eq!(t.entries[0].time, 0);
assert_eq!(t.entries[0].moof_offset, 1024);
assert_eq!(t.entries[1].time, 4800);
assert_eq!(t.entries[2].moof_offset, 3072);
}
#[test]
fn tfra_v1_64bit_fields_round_trip() {
let mut body = vec![0u8; 4];
body[0] = 1; body.extend_from_slice(&5u32.to_be_bytes()); body.extend_from_slice(&0x0000_001Bu32.to_be_bytes());
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&0x1234_5678_9ABC_DEF0u64.to_be_bytes());
body.extend_from_slice(&0xCAFE_BABE_DEAD_BEEFu64.to_be_bytes());
body.extend_from_slice(&[0x12, 0x34]); body.extend_from_slice(&[0xAA, 0xBB, 0xCC]); body.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
let mfra_body = boxed(b"tfra", &body);
let tfras = parse_mfra_box(&mfra_body).unwrap();
assert_eq!(tfras.len(), 1);
let e = tfras[0].entries[0];
assert_eq!(e.time, 0x1234_5678_9ABC_DEF0);
assert_eq!(e.moof_offset, 0xCAFE_BABE_DEAD_BEEF);
assert_eq!(e.traf_number, 0x1234);
assert_eq!(e.trun_number, 0x00AA_BBCC);
assert_eq!(e.sample_number, 0x0102_0304);
}
#[test]
fn mfra_with_two_tracks_returns_two_tfras() {
let t1 = build_tfra_v0(1, 1, 1, 1, &[(0, 100, 1, 1, 1)]);
let t2 = build_tfra_v0(2, 1, 1, 1, &[(0, 200, 1, 1, 1)]);
let mfra_body = build_mfra(&[t1, t2]);
let tfras = parse_mfra_box(&mfra_body).unwrap();
assert_eq!(tfras.len(), 2);
assert_eq!(tfras[0].track_id, 1);
assert_eq!(tfras[1].track_id, 2);
assert_eq!(tfras[0].entries[0].moof_offset, 100);
assert_eq!(tfras[1].entries[0].moof_offset, 200);
}
#[test]
fn empty_mfra_returns_no_tfras() {
let mfra_body = build_mfra(&[]);
let tfras = parse_mfra_box(&mfra_body).unwrap();
assert!(tfras.is_empty());
}
#[test]
fn tfra_truncated_returns_invalid() {
let body = vec![0u8; 8]; let mfra_body = boxed(b"tfra", &body);
assert!(parse_mfra_box(&mfra_body).is_err());
}
#[test]
fn tfra_drives_seek_to_correct_keyframe() {
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
use std::io::Cursor;
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mp4,
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(1),
styp: None,
emit_random_access_indexes: false,
}),
write_edit_list: true,
track_sample_groups: Vec::new(),
};
let path = std::env::temp_dir().join("oxideav-mp4-tfra-seek.mp4");
let mut frag_byte_offsets: Vec<u64> = Vec::new();
{
let f = std::fs::File::create(&path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_with_options(ws, std::slice::from_ref(&stream), opts).unwrap();
mux.write_header().unwrap();
for i in 0..5i64 {
let mut pkt = Packet::new(0, stream.time_base, vec![i as u8; 32]);
pkt.pts = Some(i * 1024);
pkt.dts = Some(i * 1024);
pkt.duration = Some(1024);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let bytes = std::fs::read(&path).unwrap();
let mut i = 0usize;
while i + 8 <= bytes.len() {
let sz = u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]) as usize;
let fc = &bytes[i + 4..i + 8];
if fc == b"moof" {
frag_byte_offsets.push(i as u64);
}
if sz == 0 {
break;
}
i += sz;
}
assert_eq!(frag_byte_offsets.len(), 5);
let entries: Vec<(u32, u32, u32, u32, u32)> = frag_byte_offsets
.iter()
.enumerate()
.map(|(i, &off)| ((i as u32) * 1024, off as u32, 1, 1, 1))
.collect();
let tfra_body = build_tfra_v0(1, 1, 1, 1, &entries);
let mfra_body = build_mfra(&[tfra_body]);
let mut new_bytes = bytes.clone();
new_bytes.extend_from_slice(&boxed(b"mfra", &mfra_body));
std::fs::write(&path, &new_bytes).unwrap();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(new_bytes));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let landed_pts = dmx.seek_to(0, 2500).unwrap();
assert_eq!(landed_pts, 2048, "tfra should pick the pts=2048 keyframe");
let pkt = dmx.next_packet().unwrap();
assert_eq!(pkt.pts, Some(2048));
assert_eq!(pkt.data, vec![2u8; 32]);
}
#[test]
fn mfra_in_fragmented_file_does_not_break_demux() {
use std::io::Cursor;
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mp4,
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(1),
styp: None,
emit_random_access_indexes: false,
}),
write_edit_list: true,
track_sample_groups: Vec::new(),
};
let path = std::env::temp_dir().join("oxideav-mp4-mfra-smoke.mp4");
{
let f = std::fs::File::create(&path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_with_options(ws, std::slice::from_ref(&stream), opts).unwrap();
mux.write_header().unwrap();
for i in 0..3i64 {
let mut pkt = Packet::new(0, stream.time_base, vec![0u8; 16]);
pkt.pts = Some(i * 1024);
pkt.dts = Some(i * 1024);
pkt.duration = Some(1024);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let mut bytes = std::fs::read(&path).unwrap();
let tfra_body = build_tfra_v0(
1,
1,
1,
1,
&[
(0, 100, 1, 1, 1),
(1024, 200, 1, 1, 1),
(2048, 300, 1, 1, 1),
],
);
let mfra_body = build_mfra(&[tfra_body]);
bytes.extend_from_slice(&boxed(b"mfra", &mfra_body));
std::fs::write(&path, &bytes).unwrap();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let mut count = 0;
while dmx.next_packet().is_ok() {
count += 1;
}
assert_eq!(count, 3, "still demuxes 3 packets with mfra appended");
}
fn build_prft_v0(track_id: u32, ntp: u64, media_time: u32) -> Vec<u8> {
let mut body = vec![0u8; 4]; body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&ntp.to_be_bytes());
body.extend_from_slice(&media_time.to_be_bytes());
body
}
fn build_prft_v1(track_id: u32, ntp: u64, media_time: u64) -> Vec<u8> {
let mut body = vec![0u8; 4];
body[0] = 1; body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&ntp.to_be_bytes());
body.extend_from_slice(&media_time.to_be_bytes());
body
}
#[test]
fn prft_v0_round_trips() {
let ntp: u64 = (3_900_000_000u64 << 32) | 0x8000_0000;
let body = build_prft_v0(1, ntp, 48_000);
let r = parse_prft_box(&body).unwrap().unwrap();
assert_eq!(r.reference_track_id, 1);
assert_eq!(r.ntp_timestamp, ntp);
assert_eq!(r.media_time, 48_000);
assert_eq!(r.version, 0);
}
#[test]
fn prft_v1_64bit_media_time_round_trips() {
let ntp: u64 = 0x0123_4567_89AB_CDEF;
let mt: u64 = 0xFEDC_BA98_7654_3210;
let body = build_prft_v1(7, ntp, mt);
let r = parse_prft_box(&body).unwrap().unwrap();
assert_eq!(r.reference_track_id, 7);
assert_eq!(r.ntp_timestamp, ntp);
assert_eq!(r.media_time, mt);
assert_eq!(r.version, 1);
}
#[test]
fn prft_truncated_v0_returns_invalid() {
let mut body = vec![0u8; 4];
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&0u64.to_be_bytes());
assert!(parse_prft_box(&body).is_err());
}
#[test]
fn prft_truncated_below_floor_returns_invalid() {
let body = vec![0u8; 12];
assert!(parse_prft_box(&body).is_err());
}
#[test]
fn prft_truncated_v1_returns_invalid() {
let mut body = vec![0u8; 4];
body[0] = 1; body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); assert!(parse_prft_box(&body).is_err());
}
#[test]
fn prft_in_file_is_surfaced_as_metadata() {
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
use std::io::Cursor;
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(1);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mp4,
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(2),
styp: None,
emit_random_access_indexes: false,
}),
write_edit_list: true,
track_sample_groups: Vec::new(),
};
let path = std::env::temp_dir().join("oxideav-mp4-prft-meta.mp4");
{
let f = std::fs::File::create(&path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_with_options(ws, std::slice::from_ref(&stream), opts).unwrap();
mux.write_header().unwrap();
for i in 0..4i64 {
let mut pkt = Packet::new(0, stream.time_base, vec![i as u8; 4]);
pkt.pts = Some(i);
pkt.dts = Some(i);
pkt.duration = Some(1);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let bytes = std::fs::read(&path).unwrap();
let prft0_body = build_prft_v0(1, 0x1234_5678_9ABC_DEF0, 12_345);
let prft0_box = boxed(b"prft", &prft0_body);
let prft1_body = build_prft_v1(1, 0x1122_3344_5566_7788, 0xFFFF_FFFF_FFFFu64);
let prft1_box = boxed(b"prft", &prft1_body);
let splice_off = find_first_box_offset(&bytes, &[b"moof", b"styp"])
.expect("fragmented file must have a moof / styp");
let mut spliced = Vec::with_capacity(bytes.len() + prft0_box.len() + prft1_box.len());
spliced.extend_from_slice(&bytes[..splice_off]);
spliced.extend_from_slice(&prft0_box);
spliced.extend_from_slice(&prft1_box);
spliced.extend_from_slice(&bytes[splice_off..]);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(spliced));
let dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let md = dmx.metadata();
let prft0 = md
.iter()
.find(|(k, _)| k == "prft_0")
.expect("prft_0 metadata key missing");
assert_eq!(
prft0.1,
format!("1 {} {}", 0x1234_5678_9ABC_DEF0u64, 12_345)
);
let prft1 = md
.iter()
.find(|(k, _)| k == "prft_1")
.expect("prft_1 metadata key missing");
assert_eq!(
prft1.1,
format!("1 {} {}", 0x1122_3344_5566_7788u64, 0xFFFF_FFFF_FFFFu64)
);
}
fn find_first_box_offset(bytes: &[u8], wanted: &[&[u8; 4]]) -> Option<usize> {
let mut off = 0usize;
while off + 8 <= bytes.len() {
let size = u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]]);
let fourcc = &bytes[off + 4..off + 8];
if wanted.iter().any(|w| fourcc == w.as_slice()) {
return Some(off);
}
let advance = match size {
0 => return None, 1 => {
if off + 16 > bytes.len() {
return None;
}
u64::from_be_bytes([
bytes[off + 8],
bytes[off + 9],
bytes[off + 10],
bytes[off + 11],
bytes[off + 12],
bytes[off + 13],
bytes[off + 14],
bytes[off + 15],
]) as usize
}
n => n as usize,
};
if advance < 8 {
return None;
}
off += advance;
}
None
}
#[test]
fn sidx_drives_seek_to_correct_keyframe_when_no_mfra() {
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
use std::io::Cursor;
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mp4,
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(1),
styp: None,
emit_random_access_indexes: true,
}),
write_edit_list: true,
track_sample_groups: Vec::new(),
};
let path = std::env::temp_dir().join("oxideav-mp4-sidx-seek.mp4");
{
let f = std::fs::File::create(&path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_with_options(ws, std::slice::from_ref(&stream), opts).unwrap();
mux.write_header().unwrap();
for i in 0..5i64 {
let mut pkt = Packet::new(0, stream.time_base, vec![i as u8; 32]);
pkt.pts = Some(i * 1024);
pkt.dts = Some(i * 1024);
pkt.duration = Some(1024);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let bytes = std::fs::read(&path).unwrap();
let mut mfra_off: Option<usize> = None;
let mut sidx_count = 0usize;
let mut off = 0usize;
while off + 8 <= bytes.len() {
let sz = u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
as usize;
let fc = &bytes[off + 4..off + 8];
if fc == b"sidx" {
sidx_count += 1;
}
if fc == b"mfra" {
mfra_off = Some(off);
break;
}
if sz < 8 {
break;
}
off += sz;
}
assert!(
sidx_count >= 1,
"muxer should have emitted at least one sidx with emit_random_access_indexes=true"
);
let mfra_off = mfra_off.expect("muxer should have emitted a trailing mfra");
let sidx_only = bytes[..mfra_off].to_vec();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(sidx_only.clone()));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let landed = dmx.seek_to(0, 2500).unwrap();
assert_eq!(
landed, 2048,
"sidx fast-path should pick the pts=2048 keyframe"
);
let pkt = dmx.next_packet().unwrap();
assert_eq!(pkt.pts, Some(2048));
assert_eq!(pkt.data, vec![2u8; 32]);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(sidx_only.clone()));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let landed = dmx.seek_to(0, -100).unwrap();
assert_eq!(landed, 0, "sidx fast-path should snap negative pts to 0");
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(sidx_only.clone()));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let landed = dmx.seek_to(0, 3072).unwrap();
assert_eq!(landed, 3072, "exact-pts sidx hit");
}
#[test]
fn seek_to_still_works_without_sidx_or_mfra() {
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
use std::io::Cursor;
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mp4,
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(1),
styp: None,
emit_random_access_indexes: false,
}),
write_edit_list: true,
track_sample_groups: Vec::new(),
};
let path = std::env::temp_dir().join("oxideav-mp4-noindex-seek.mp4");
{
let f = std::fs::File::create(&path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_with_options(ws, std::slice::from_ref(&stream), opts).unwrap();
mux.write_header().unwrap();
for i in 0..5i64 {
let mut pkt = Packet::new(0, stream.time_base, vec![i as u8; 32]);
pkt.pts = Some(i * 1024);
pkt.dts = Some(i * 1024);
pkt.duration = Some(1024);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let bytes = std::fs::read(&path).unwrap();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let landed = dmx.seek_to(0, 2500).unwrap();
assert_eq!(
landed, 2048,
"linear-scan fallback should pick the pts=2048 keyframe"
);
let pkt = dmx.next_packet().unwrap();
assert_eq!(pkt.pts, Some(2048));
assert_eq!(pkt.data, vec![2u8; 32]);
}
#[test]
fn sidx_with_wrong_reference_id_is_ignored() {
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
use std::io::Cursor;
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mp4,
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(1),
styp: None,
emit_random_access_indexes: true,
}),
write_edit_list: true,
track_sample_groups: Vec::new(),
};
let path = std::env::temp_dir().join("oxideav-mp4-sidx-wrong-id.mp4");
{
let f = std::fs::File::create(&path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_with_options(ws, std::slice::from_ref(&stream), opts).unwrap();
mux.write_header().unwrap();
for i in 0..5i64 {
let mut pkt = Packet::new(0, stream.time_base, vec![i as u8; 32]);
pkt.pts = Some(i * 1024);
pkt.dts = Some(i * 1024);
pkt.duration = Some(1024);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let mut bytes = std::fs::read(&path).unwrap();
let mut mfra_off: Option<usize> = None;
let mut off = 0usize;
while off + 8 <= bytes.len() {
let sz = u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
as usize;
let fc = &bytes[off + 4..off + 8];
if fc == b"mfra" {
mfra_off = Some(off);
break;
}
if sz < 8 {
break;
}
off += sz;
}
bytes.truncate(mfra_off.expect("trailing mfra missing"));
let mut off = 0usize;
let mut patched = 0;
while off + 8 <= bytes.len() {
let sz = u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
as usize;
let fc = &bytes[off + 4..off + 8];
if fc == b"sidx" {
let rid_off = off + 8 + 4; bytes[rid_off..rid_off + 4].copy_from_slice(&99u32.to_be_bytes());
patched += 1;
}
if sz < 8 {
break;
}
off += sz;
}
assert!(patched >= 1, "should have patched at least one sidx");
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).unwrap();
let landed = dmx.seek_to(0, 2500).unwrap();
assert_eq!(landed, 2048);
}