use broadcast_common::{Parse, Serialize};
use transmux::{
CuePayloadBox, SampleDescriptionBox, SampleEntryVariant, VttCueBox, VttEmptyCueBox,
WebVttConfigurationBox, WvttSampleEntry, XmlSubtitleSampleEntry,
};
fn find_box_recursive<'a>(data: &'a [u8], target: &[u8; 4]) -> Option<&'a [u8]> {
let mut pos = 0usize;
while pos + 8 <= data.len() {
let sz =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
if sz < 8 {
break;
}
let end = (pos + sz).min(data.len());
let fourcc = &data[pos + 4..pos + 8];
if fourcc == target {
return Some(&data[pos..end]);
}
match fourcc {
b"moov" | b"trak" | b"mdia" | b"minf" | b"stbl" | b"udta" => {
if let Some(found) = find_box_recursive(&data[pos + 8..end], target) {
return Some(found);
}
}
_ => {}
}
pos += sz;
}
None
}
#[test]
fn stpp_parse_and_round_trip() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/mp4/stpp.mp4");
let data = std::fs::read(path).expect("stpp.mp4 fixture must exist");
let stsd_box = find_box_recursive(&data, b"stsd").expect("stsd box must exist in stpp.mp4");
let stsd = SampleDescriptionBox::parse(stsd_box).expect("stsd must parse");
assert_eq!(stsd.entries.len(), 1, "stsd should have exactly 1 entry");
let entry = match &stsd.entries[0] {
SampleEntryVariant::Stpp(s) => s,
other => panic!("expected SampleEntryVariant::Stpp, got {other:?}"),
};
assert!(
!entry.namespace.is_empty(),
"namespace must be non-empty, got {:?}",
entry.namespace
);
let stpp_box = &stsd_box[16..];
let sz = u32::from_be_bytes([stpp_box[0], stpp_box[1], stpp_box[2], stpp_box[3]]) as usize;
let stpp_bytes = &stpp_box[..sz];
assert_eq!(&stpp_bytes[4..8], b"stpp", "stpp fourcc must match");
let parsed = XmlSubtitleSampleEntry::bare_parse(stpp_bytes)
.expect("XmlSubtitleSampleEntry::bare_parse must succeed");
assert_eq!(parsed.namespace, entry.namespace);
let serialized = parsed.to_bytes();
assert_eq!(
serialized.as_slice(),
stpp_bytes,
"round-trip must be byte-identical"
);
let mut mutated = parsed.clone();
mutated.namespace = String::from("http://example.com/mutated");
let mutated_bytes = mutated.to_bytes();
assert_ne!(
mutated_bytes.as_slice(),
stpp_bytes,
"mutating namespace must change serialized bytes"
);
}
#[test]
fn stpp_namespace_is_ttml() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/mp4/stpp.mp4");
let data = std::fs::read(path).expect("stpp.mp4 fixture must exist");
let stsd_box = find_box_recursive(&data, b"stsd").expect("stsd");
let stsd = SampleDescriptionBox::parse(stsd_box).unwrap();
let entry = match &stsd.entries[0] {
SampleEntryVariant::Stpp(s) => s,
_ => panic!("expected Stpp"),
};
assert!(
entry.namespace.contains("ttml"),
"namespace should reference TTML: {:?}",
entry.namespace
);
}
#[test]
fn wvtt_spec_vector_round_trip() {
let entry = WvttSampleEntry::new("WEBVTT");
let bytes = entry.to_bytes();
assert_eq!(&bytes[4..8], b"wvtt", "fourcc must be wvtt");
let parsed = WvttSampleEntry::bare_parse(&bytes).expect("wvtt must parse");
assert_eq!(parsed.config.config, "WEBVTT");
assert_eq!(parsed.data_reference_index, 1);
assert!(parsed.extra_boxes.is_empty());
let reser = parsed.to_bytes();
assert_eq!(
reser.as_slice(),
bytes.as_slice(),
"wvtt round-trip must be byte-identical"
);
let mut mutated = parsed.clone();
mutated.config.config = String::from("WEBVTT\n\nNOTE mutated");
let mutated_bytes = mutated.to_bytes();
assert_ne!(
mutated_bytes.as_slice(),
bytes.as_slice(),
"mutating vttC config must change serialized bytes"
);
}
#[test]
fn vttc_payl_spec_vector_round_trip() {
let cue = VttCueBox::new("Hello, World!");
let bytes = cue.to_bytes();
assert_eq!(&bytes[4..8], b"vttc", "fourcc must be vttc");
let parsed = VttCueBox::bare_parse(&bytes).expect("vttc must parse");
assert_eq!(parsed.payload.cue_text, "Hello, World!");
assert!(parsed.settings.is_none());
assert!(parsed.cue_id.is_none());
let reser = parsed.to_bytes();
assert_eq!(
reser.as_slice(),
bytes.as_slice(),
"vttc round-trip must be byte-identical"
);
}
#[test]
fn vtte_spec_vector_round_trip() {
let vtte = VttEmptyCueBox;
let bytes = vtte.to_bytes();
assert_eq!(bytes.len(), 8, "vtte must be exactly 8 bytes");
assert_eq!(&bytes[4..8], b"vtte", "fourcc must be vtte");
let parsed = VttEmptyCueBox::bare_parse(&bytes).expect("vtte must parse");
let reser = parsed.to_bytes();
assert_eq!(
reser.as_slice(),
bytes.as_slice(),
"vtte round-trip must be byte-identical"
);
}
#[test]
fn wvtt_complete_spec_vector() {
let mut entry = WvttSampleEntry::new("WEBVTT");
entry.data_reference_index = 1;
let se_bytes = entry.to_bytes();
assert_eq!(&se_bytes[4..8], b"wvtt");
assert_eq!(&se_bytes[8..14], &[0u8; 6], "reserved must be zeros");
let dri = u16::from_be_bytes([se_bytes[14], se_bytes[15]]);
assert_eq!(dri, 1, "data_reference_index must be 1");
assert_eq!(&se_bytes[20..24], b"vttC", "vttC fourcc at offset 20");
let cue = VttCueBox::new("00:00:01.000 --> 00:00:03.000\nHello WebVTT!");
let sample_bytes = cue.to_bytes();
let parsed_cue = VttCueBox::bare_parse(&sample_bytes).unwrap();
assert!(parsed_cue.payload.cue_text.contains("Hello WebVTT!"));
assert_eq!(parsed_cue.to_bytes().as_slice(), sample_bytes.as_slice());
let parsed_se = WvttSampleEntry::bare_parse(&se_bytes).unwrap();
assert_eq!(parsed_se.config.config, "WEBVTT");
assert_eq!(parsed_se.to_bytes().as_slice(), se_bytes.as_slice());
}
#[test]
fn vttc_config_box_round_trip() {
let cfg = WebVttConfigurationBox::new("WEBVTT");
let bytes = cfg.to_bytes();
assert_eq!(&bytes[4..8], b"vttC");
assert_eq!(bytes.len(), 14);
let parsed = WebVttConfigurationBox::bare_parse(&bytes).unwrap();
assert_eq!(parsed.config, "WEBVTT");
assert_eq!(parsed.to_bytes().as_slice(), bytes.as_slice());
}
#[test]
fn cue_payload_box_round_trip() {
let payl = CuePayloadBox::new("Test subtitle text");
let bytes = payl.to_bytes();
assert_eq!(&bytes[4..8], b"payl");
let parsed = CuePayloadBox::bare_parse(&bytes).unwrap();
assert_eq!(parsed.cue_text, "Test subtitle text");
assert_eq!(parsed.to_bytes().as_slice(), bytes.as_slice());
}