use mp4_emsg::{EMSG_BOX_TYPE, EmsgBox, EmsgVersion, PresentationTime};
use transmux::{FragmentTrackData, Sample, build_media_segment, build_media_segment_with_events};
const SCTE35_SCHEME: &str = "urn:scte:scte35:2013:bin";
fn scte35_fixture_emsg_bytes() -> Vec<u8> {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../fixtures/shared/scte35_emsg_v0.bin"
);
std::fs::read(path).expect("fixtures/shared/scte35_emsg_v0.bin must be committed")
}
fn minimal_tracks() -> Vec<Sample> {
vec![Sample::new(
vec![0u8; 4],
Some(0),
Some(0),
Some(3000),
true,
)]
}
fn minimal_frag(samples: &[Sample]) -> Vec<FragmentTrackData<'_>> {
vec![FragmentTrackData::new(1, 0, samples)]
}
fn scan_boxes(data: &[u8]) -> Vec<(usize, usize, [u8; 4])> {
let mut out = Vec::new();
let mut off = 0usize;
while off + 8 <= data.len() {
let size =
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
if size < 8 {
break;
}
let mut cc = [0u8; 4];
cc.copy_from_slice(&data[off + 4..off + 8]);
let end = off + size;
if end > data.len() {
break;
}
out.push((off, end, cc));
off = end;
}
out
}
#[test]
fn emsg_present_and_round_trips_real_scte35_payload() {
let fixture_bytes = scte35_fixture_emsg_bytes();
let fixture_emsg = EmsgBox::parse(&fixture_bytes).expect("fixture must parse");
assert_eq!(fixture_emsg.scheme_id_uri, SCTE35_SCHEME);
assert_eq!(fixture_emsg.version(), EmsgVersion::SegmentRelative);
assert!(fixture_emsg.is_scte35());
assert!(
fixture_emsg.message_data.len() >= 14,
"minimum splice_info_section size"
);
assert_eq!(
fixture_emsg.message_data[0], 0xFC,
"splice_info_section table_id"
);
let samples = minimal_tracks();
let tracks = minimal_frag(&samples);
let seg = build_media_segment_with_events(1, &tracks, std::slice::from_ref(&fixture_emsg))
.expect("build must succeed");
let boxes = scan_boxes(&seg);
let box_fourccs: Vec<[u8; 4]> = boxes.iter().map(|b| b.2).collect();
let styp_pos = box_fourccs
.iter()
.position(|c| c == b"styp")
.expect("styp missing");
let emsg_pos = box_fourccs
.iter()
.position(|c| c == &EMSG_BOX_TYPE)
.expect("emsg missing");
let moof_pos = box_fourccs
.iter()
.position(|c| c == b"moof")
.expect("moof missing");
let mdat_pos = box_fourccs
.iter()
.position(|c| c == b"mdat")
.expect("mdat missing");
assert!(
styp_pos < emsg_pos,
"emsg ({emsg_pos}) must come after styp ({styp_pos})"
);
assert!(
emsg_pos < moof_pos,
"emsg ({emsg_pos}) must come before moof ({moof_pos})"
);
assert!(
moof_pos < mdat_pos,
"moof ({moof_pos}) must come before mdat ({mdat_pos})"
);
let styp_end = boxes[styp_pos].1;
let emsg_start = boxes[emsg_pos].0;
let moof_start = boxes[moof_pos].0;
assert!(
styp_end <= emsg_start,
"emsg byte start ({emsg_start}) must be >= styp end ({styp_end})"
);
assert!(
emsg_start < moof_start,
"emsg byte start ({emsg_start}) must be < moof start ({moof_start})"
);
let emsg_slice = &seg[emsg_start..boxes[emsg_pos].1];
let reparsed = EmsgBox::parse(emsg_slice).expect("emsg in segment must re-parse");
assert_eq!(reparsed.scheme_id_uri, fixture_emsg.scheme_id_uri);
assert_eq!(reparsed.value, fixture_emsg.value);
assert_eq!(reparsed.timescale, fixture_emsg.timescale);
assert_eq!(reparsed.presentation_time, fixture_emsg.presentation_time);
assert_eq!(reparsed.event_duration, fixture_emsg.event_duration);
assert_eq!(reparsed.id, fixture_emsg.id);
assert_eq!(
reparsed.message_data, fixture_emsg.message_data,
"message_data (splice_info_section) must survive the round-trip"
);
assert!(reparsed.is_scte35());
assert_eq!(
reparsed.message_data[0], 0xFC,
"splice_info_section table_id survives"
);
}
#[test]
fn two_emsgs_appear_in_order_before_moof() {
let msg_a = [0xFCu8, 0x30, 0x01];
let msg_b = [0xFCu8, 0x30, 0x02];
let emsg_a = EmsgBox {
scheme_id_uri: SCTE35_SCHEME,
value: "",
timescale: 90_000,
presentation_time: PresentationTime::Delta(0),
event_duration: 0xFFFF_FFFF,
id: 1,
message_data: &msg_a,
};
let emsg_b = EmsgBox {
scheme_id_uri: SCTE35_SCHEME,
value: "",
timescale: 90_000,
presentation_time: PresentationTime::Delta(9000),
event_duration: 0xFFFF_FFFF,
id: 2,
message_data: &msg_b,
};
let samples = minimal_tracks();
let tracks = minimal_frag(&samples);
let seg = build_media_segment_with_events(2, &tracks, &[emsg_a.clone(), emsg_b.clone()])
.expect("build must succeed");
let boxes = scan_boxes(&seg);
let box_fourccs: Vec<[u8; 4]> = boxes.iter().map(|b| b.2).collect();
let emsg_positions: Vec<usize> = box_fourccs
.iter()
.enumerate()
.filter_map(|(i, c)| if c == &EMSG_BOX_TYPE { Some(i) } else { None })
.collect();
assert_eq!(
emsg_positions.len(),
2,
"exactly two emsg boxes must be present"
);
let moof_pos = box_fourccs
.iter()
.position(|c| c == b"moof")
.expect("moof missing");
for &ep in &emsg_positions {
assert!(
ep < moof_pos,
"emsg at box-index {ep} must precede moof at {moof_pos}"
);
}
let first_emsg_bytes = &seg[boxes[emsg_positions[0]].0..boxes[emsg_positions[0]].1];
let second_emsg_bytes = &seg[boxes[emsg_positions[1]].0..boxes[emsg_positions[1]].1];
let first = EmsgBox::parse(first_emsg_bytes).expect("first emsg must parse");
let second = EmsgBox::parse(second_emsg_bytes).expect("second emsg must parse");
assert_eq!(first.id, 1, "first emsg id must be 1");
assert_eq!(second.id, 2, "second emsg id must be 2");
assert_eq!(first.message_data, msg_a.as_slice());
assert_eq!(second.message_data, msg_b.as_slice());
let moof_start = boxes[moof_pos].0;
assert!(boxes[emsg_positions[0]].1 <= moof_start);
assert!(boxes[emsg_positions[1]].1 <= moof_start);
}
#[test]
fn no_emsg_output_byte_identical_to_base_fn() {
let samples = minimal_tracks();
let tracks_a = minimal_frag(&samples);
let tracks_b = minimal_frag(&samples);
let base = build_media_segment(3, &tracks_a).expect("base build must succeed");
let with_events =
build_media_segment_with_events(3, &tracks_b, &[]).expect("with_events build must succeed");
assert_eq!(
base, with_events,
"build_media_segment_with_events with empty emsgs must be byte-identical to build_media_segment"
);
}