tests: header gate rebase and subtitle, fvi header provenance, hvcC cut arrays, FMKV stream fields, json mvc and cue times

This commit is contained in:
Matthew Jackson
2026-10-01 15:07:14 -07:00
parent cd208c8c18
commit c37ef82dbf
5 changed files with 183 additions and 0 deletions
+18
View File
@@ -338,6 +338,24 @@ mod tests {
assert_eq!(n, [0, 1]);
}
// Known provenance (playlist, volume id) and picture count are carried into the header.
#[test]
fn header_carries_playlist_volume_id_and_picture_count_when_known() {
let source = SourceInfo {
playlist: "00800.mpls".into(),
volume_id: "DISC_LABEL".into(),
..SourceInfo::default()
};
let mut header = MapHeader::from_title(&mpeg2_title(), source);
header.picture_count = Some(7);
let mut out = Vec::new();
write_fvi_header(&mut out, &header).unwrap();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["source"]["playlist"], "00800.mpls");
assert_eq!(v["source"]["volume_id"], "DISC_LABEL");
assert_eq!(v["picture_count"], 7);
}
#[test]
fn sink_writes_header_and_only_video_records() {
let dir = tempfile::tempdir().unwrap();
+26
View File
@@ -183,6 +183,32 @@ mod tests {
assert!(waiting(&g));
}
// After a forward discontinuity the clock rebases: normal PTS past the jump counts again.
#[test]
fn the_clock_runs_again_after_a_forward_pts_jump() {
let mut g = HeaderGate::default();
for pts in [0, S, 100 * S, 101 * S, 102 * S, 103 * S, 104 * S] {
g.account(0, pts, 0);
}
assert!(!waiting(&g), "5 s of content on either side of the jump");
}
// A subtitle track has no codec_private and never holds the headers.
#[test]
fn subtitle_tracks_never_gate() {
let mut t = DiscTitle::empty();
t.streams
.push(Stream::Subtitle(crate::disc::SubtitleStream {
pid: 0x1200,
codec: Codec::Pgs,
language: "eng".into(),
forced: false,
qualifier: crate::disc::LabelQualifier::None,
codec_data: None,
}));
assert!(HeaderGate::default().ready(&t, |_| None));
}
// Decode-order reordering (B-frames) must not inflate elapsed time.
#[test]
fn reordered_pts_count_once() {
+17
View File
@@ -495,6 +495,23 @@ mod tests {
assert_eq!(&out[..], &[0x00, 0x00, 0x00, 0x01, 0x40, 0x01]);
}
// An hvcC ending 1-2 bytes into the next array header is truncated, not read past its end.
#[test]
fn hvcc_cut_inside_an_array_header_is_truncated_not_out_of_bounds() {
for tail in [&[32u8][..], &[32, 0]] {
let mut hvcc = vec![0u8; 22];
hvcc.push(2); // numArrays = 2
hvcc.push(32);
hvcc.extend_from_slice(&1u16.to_be_bytes());
hvcc.extend_from_slice(&2u16.to_be_bytes());
hvcc.extend_from_slice(&[0x40, 0x01]);
hvcc.extend_from_slice(tail);
let (out, truncated) = hvcc_parse(&hvcc);
assert!(truncated, "tail {tail:?}");
assert_eq!(out.unwrap(), [0x00, 0x00, 0x00, 0x01, 0x40, 0x01]);
}
}
// --- length_prefixed_to_annex_b additional branches ---
#[test]
+88
View File
@@ -675,6 +675,94 @@ mod tests {
assert_eq!(cursor.position() as usize % BD_SOURCE_PACKET_BYTES, 0);
}
// Every stream field the receiver acts on survives an FMKV hop (network://, stdio://, m2ts://).
#[test]
fn stream_fields_round_trip_through_the_header() {
use crate::disc::{
AudioChannels, AudioStream, LabelPurpose, LabelQualifier, MVC_DEPENDENT_LABEL,
SampleRate, SubtitleStream,
};
let mut t = video_title(HdrFormat::Hdr10, ColorSpace::Bt2020);
if let Stream::Video(v) = &mut t.streams[0] {
v.label = MVC_DEPENDENT_LABEL.into();
v.secondary = true;
}
for (i, purpose) in [
LabelPurpose::Commentary,
LabelPurpose::Descriptive,
LabelPurpose::Score,
LabelPurpose::Ime,
]
.into_iter()
.enumerate()
{
t.streams.push(Stream::Audio(AudioStream {
pid: 0x1100 + i as u16,
codec: Codec::Dts,
channels: AudioChannels::Surround71,
language: "fra".into(),
sample_rate: SampleRate::S96,
secondary: true,
purpose,
label: format!("audio {i}"),
}));
}
for qualifier in [
LabelQualifier::Sdh,
LabelQualifier::DescriptiveService,
LabelQualifier::Forced,
] {
t.streams.push(Stream::Subtitle(SubtitleStream {
pid: 0x1200,
codec: Codec::Pgs,
language: "deu".into(),
forced: true,
qualifier,
codec_data: None,
}));
}
let back = M2tsMeta::from_title(&t).to_title();
let Stream::Video(v) = &back.streams[0] else {
panic!("video first")
};
assert!(v.is_mvc_dependent());
let dbg = |s: &crate::disc::Stream| format!("{s:?}");
let (want, got): (Vec<_>, Vec<_>) = t
.streams
.iter()
.zip(&back.streams)
.map(|(a, b)| (dbg(a), dbg(b)))
.unzip();
assert_eq!(got, want);
}
// The wire track is a u8: a header declaring more than 256 streams is refused.
#[test]
fn a_header_with_more_than_256_streams_is_refused() {
let title_of = |n: usize| {
let mut t = DiscTitle::empty();
for pid in 0..n {
t.streams
.push(Stream::Subtitle(crate::disc::SubtitleStream {
pid: pid as u16,
codec: Codec::Pgs,
language: String::new(),
forced: false,
qualifier: crate::disc::LabelQualifier::None,
codec_data: None,
}));
}
t
};
let read = |n| {
let mut buf = Vec::new();
write_header(&mut buf, &M2tsMeta::from_title(&title_of(n))).unwrap();
read_header(&mut io::Cursor::new(buf))
};
assert!(read(256).unwrap().is_some());
assert!(read(257).is_err());
}
#[test]
fn audio_and_subtitle_codec_private_round_trip() {
use crate::disc::{AudioChannels, AudioStream, LabelPurpose, SampleRate, SubtitleStream};
+34
View File
@@ -362,6 +362,40 @@ mod tests {
assert!(vtt.contains("a &amp; &lt;b&gt;"), "{vtt}");
}
#[test]
fn vtt_times_carry_hours_minutes_and_seconds() {
assert_eq!(vtt_time(3725.5), "01:02:05.500");
assert_eq!(vtt_time(36_000.0), "10:00:00.000");
}
// json:// marks the MVC dependent view and only that.
#[test]
fn json_marks_the_mvc_dependent_view() {
use crate::disc::{
Codec, ColorSpace, DiscTitle, FrameRate, HdrFormat, MVC_DEPENDENT_LABEL, Resolution,
Stream as DiscStream, VideoStream,
};
let video = |label: &str| {
DiscStream::Video(VideoStream {
pid: 0x1011,
codec: Codec::H264,
resolution: Resolution::R1080p,
frame_rate: FrameRate::F23_976,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
display_aspect: None,
secondary: false,
label: label.into(),
measured_cicp: None,
})
};
let mut t = DiscTitle::empty();
t.streams = vec![video(""), video(MVC_DEPENDENT_LABEL)];
let v = title_json(&t);
assert_eq!(v["video"][0]["mvc"], false);
assert_eq!(v["video"][1]["mvc"], true);
}
#[test]
fn chapters_format_selected_by_extension() {
let xml = chapters_content(&chaps(), Some("xml"));