tests: mux sinks, header gate, m2ts LPCM PTS, mpg fit ids, stale names

This commit is contained in:
Matthew Jackson
2026-09-29 20:10:25 -07:00
parent 148e0f6989
commit 3ac519d209
7 changed files with 201 additions and 30 deletions
+1 -1
View File
@@ -598,7 +598,7 @@ mod tests {
let minf = child(&mdia[8..], b"minf").expect("minf");
// ── mdhd. Version 0 layout: vflags(4) creation(4) modification(4)
// timescale(4) duration(4) language(2) pre_defined(2) = 20 bytes.
// timescale(4) duration(4) language(2) pre_defined(2) = 24 bytes.
let mdhd = child(&mdia[8..], b"mdhd").expect("mdia must carry mdhd");
let body = &mdhd[8..];
assert_eq!(body[0], 0, "mdhd version 0 (32-bit times)");
+36 -27
View File
@@ -214,17 +214,6 @@ mod tests {
use crate::mux::codec::coding::{CodingType, Mpeg2Coding};
use crate::mux::videomap::Medium;
use crate::pes::SourcePos;
use std::path::PathBuf;
/// Tiny unique temp dir helper (avoids a dev-dependency on `tempfile`).
fn tempdir() -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let p = std::env::temp_dir().join(format!("fmkv_fvi_test_{}_{}", std::process::id(), n));
std::fs::create_dir_all(&p).unwrap();
p
}
fn mpeg2_title() -> DiscTitle {
let mut t = DiscTitle::empty();
@@ -301,18 +290,21 @@ mod tests {
#[test]
fn sink_is_write_only() {
let dir = tempdir();
let mut sink =
FviSink::create(&dir.join("x.fvi"), &mpeg2_title(), SourceInfo::default()).unwrap();
let dir = tempfile::tempdir().unwrap();
let mut sink = FviSink::create(
&dir.path().join("x.fvi"),
&mpeg2_title(),
SourceInfo::default(),
)
.unwrap();
let err = Stream::read(&mut sink).expect_err("read must error");
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sink_writes_header_and_only_video_records() {
let dir = tempdir();
let path = dir.join("movie.fvi");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("movie.fvi");
let mut sink = FviSink::create(
&path,
&mpeg2_title(),
@@ -366,27 +358,46 @@ mod tests {
rec.get("gop").is_none(),
"no GOP-closure signal → gop omitted"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn secondary_video_stream_is_not_indexed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pip.fvi");
let mut title = mpeg2_title();
let mut second = title.streams[0].clone();
if let DiscStream::Video(v) = &mut second {
v.pid = 0x1012;
v.secondary = true;
}
title.streams.push(second);
let mut sink = FviSink::create(&path, &title, SourceInfo::default()).unwrap();
sink.write(&vframe(0, Some(i_pic()), Some(SourcePos::at_byte(0))))
.unwrap();
sink.write(&vframe(1, Some(i_pic()), Some(SourcePos::at_byte(2048))))
.unwrap();
sink.finish().unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert_eq!(text.lines().count(), 2, "header + primary record only");
}
#[test]
fn empty_title_still_emits_valid_header() {
let dir = tempdir();
let path = dir.join("empty.fvi");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.fvi");
let mut sink = FviSink::create(&path, &mpeg2_title(), SourceInfo::default()).unwrap();
sink.finish().unwrap(); // no frames
let text = std::fs::read_to_string(&path).unwrap();
assert_eq!(text.lines().count(), 1, "header only");
let header: serde_json::Value = serde_json::from_str(text.lines().next().unwrap()).unwrap();
assert_eq!(header["format"], "freemkv/video-index");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn extension_jsonl_is_still_json_lines() {
// Output is always JSON Lines regardless of extension (one format today).
let dir = tempdir();
let path = dir.join("idx.jsonl");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("idx.jsonl");
let mut sink = FviSink::create(&path, &mpeg2_title(), SourceInfo::default()).unwrap();
sink.write(&vframe(0, Some(i_pic()), None)).unwrap();
sink.finish().unwrap();
@@ -394,7 +405,6 @@ mod tests {
// null src on a provenance-absent frame.
let rec: serde_json::Value = serde_json::from_str(text.lines().nth(1).unwrap()).unwrap();
assert_eq!(rec["src"], serde_json::Value::Null);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
@@ -402,8 +412,8 @@ mod tests {
// A non-MPEG2 stream (coding None) whose parser sets keyframe must still
// produce USEFUL records: key/type from the frame keyframe flag, src +
// pts populated, and NO mpeg2-only field members.
let dir = tempdir();
let path = dir.join("uhd.fvi");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("uhd.fvi");
let mut sink = FviSink::create(
&path,
&hevc_title(),
@@ -438,6 +448,5 @@ mod tests {
assert_eq!(recs[1]["key"], false);
assert_eq!(recs[1]["type"], "P");
assert_eq!(recs[1]["src"]["sector"], 10); // 20480 / 2048
let _ = std::fs::remove_dir_all(&dir);
}
}
+23
View File
@@ -268,4 +268,27 @@ mod tests {
}));
assert!(!g.ready(&t, |_| None));
}
#[test]
fn secondary_video_never_gates() {
let mut t = DiscTitle::empty();
for (pid, secondary) in [(0x1011, false), (0x1012, true)] {
t.streams.push(Stream::Video(crate::disc::VideoStream {
pid,
codec: Codec::Hevc,
resolution: crate::disc::Resolution::R2160p,
frame_rate: crate::disc::FrameRate::F23_976,
hdr: crate::disc::HdrFormat::Hdr10,
color_space: crate::disc::ColorSpace::Bt2020,
display_aspect: None,
secondary,
label: String::new(),
measured_cicp: None,
}));
}
let g = HeaderGate::default();
// Only the primary's config is awaited; the PiP / DV-EL track has none.
assert!(g.ready(&t, |i| (i == 0).then(|| vec![1])));
assert!(!g.ready(&t, |i| (i == 1).then(|| vec![1])));
}
}
+6
View File
@@ -462,6 +462,12 @@ mod tests {
let mut demux = crate::mux::ts::TsDemuxer::new(&[AUDIO_PID]);
let mut pes = demux.feed(ts);
pes.extend(demux.flush());
// Each 5 ms BD sub-payload carries its own offset PTS, so none repeat.
let pts: Vec<i64> = pes.iter().map(|p| p.pts.expect("PES PTS")).collect();
assert!(
pts.windows(2).all(|w| w[0] < w[1]),
"{channels:?} x{samples}: PES PTS strictly increasing, got {pts:?}"
);
let mut parser = crate::mux::codec::lpcm::LpcmParser::new();
let got: Vec<u8> = pes
.iter()
+106
View File
@@ -561,4 +561,110 @@ mod tests {
assert_eq!(sample_rates(SampleRate::S48_192), vec![48_000, 192_000]);
assert!(sample_rates(SampleRate::Unknown).is_empty());
}
/// Two streams of each kind, interleaved: every per-kind cursor must advance,
/// or the second stream's fields land on the first's entry.
#[test]
fn json_per_kind_cursors_and_title_fields() {
use crate::disc::{
AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, DiscTitle,
FrameRate, HdrFormat, LabelPurpose, LabelQualifier, Resolution, SampleRate,
Stream as DiscStream, SubtitleStream, VideoStream,
};
let vid = |pid| {
DiscStream::Video(VideoStream {
pid,
codec: Codec::Hevc,
resolution: Resolution::R2160p,
frame_rate: FrameRate::F23_976,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt2020,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
})
};
let aud = |pid| {
DiscStream::Audio(AudioStream {
pid,
codec: Codec::Ac3,
channels: AudioChannels::Stereo,
language: "eng".into(),
sample_rate: SampleRate::S48,
secondary: false,
purpose: LabelPurpose::Normal,
label: String::new(),
})
};
let sub = |pid| {
DiscStream::Subtitle(SubtitleStream {
pid,
codec: Codec::Pgs,
language: "eng".into(),
forced: false,
qualifier: LabelQualifier::None,
codec_data: None,
})
};
let mut t = DiscTitle::empty();
t.streams = vec![
aud(0x1100),
vid(0x1011),
aud(0x1101),
sub(0x1200),
sub(0x1201),
vid(0x1012),
];
t.content_format = ContentFormat::BdTs;
t.playlist_id = 801;
t.clips = vec![Clip {
feed_span: None,
clip_id: "00007".into(),
in_time: 0,
out_time: 45_000,
duration_secs: 1.0,
source_packets: 42,
}];
let v = title_json(&t);
for (kind, pids) in [
("video", [0x1011, 0x1012]),
("audio", [0x1100, 0x1101]),
("subtitles", [0x1200, 0x1201]),
] {
assert_eq!(v[kind][0]["pid"], pids[0], "{kind}[0]");
assert_eq!(v[kind][1]["pid"], pids[1], "{kind}[1]");
}
assert_eq!(v["format"], "BdTs");
assert_eq!(v["playlist_id"], 801);
assert_eq!(v["clips"][0]["clip_id"], "00007");
assert_eq!(v["clips"][0]["source_packets"], 42);
assert_eq!(v["clips"][0]["duration_secs"], 1.0);
}
/// create() output read back: the extension selects the chapter format and
/// the JSON file holds the title document.
#[test]
fn chapters_and_json_create_write_readable_files() {
let dir = tempfile::tempdir().unwrap();
let mut t = DiscTitle::empty();
t.chapters = chaps();
t.playlist = "MAIN".into();
for (name, marker) in [
("c.txt", "CHAPTER01="),
("c.VTT", "WEBVTT"),
("c.xml", "<Chapters>"),
] {
let path = dir.path().join(name);
ChaptersSink::create(&path, &t).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains(marker), "{name}: {text}");
}
let path = dir.path().join("t.json");
JsonSink::create(&path, &t).unwrap();
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(v["playlist"], "MAIN");
assert_eq!(v["chapter_marks"][1]["n"], 2);
}
}
+3 -2
View File
@@ -197,13 +197,14 @@ mod tests {
}
#[test]
fn is_disc_source_only_for_disc_and_iso() {
fn is_disc_source_only_for_disc_iso_and_dir() {
// is_disc_source gates the "raw sector copy" path. Per the doc table
// only disc:// and iso:// are disc sources; mkv/m2ts/network/etc must
// only disc://, iso:// and dir:// are disc sources; mkv/m2ts/network/etc must
// NOT be (they are container/stream formats, not raw sector media).
assert!(parse_url("disc://").is_disc_source());
assert!(parse_url("disc:///dev/sg1").is_disc_source());
assert!(parse_url("iso://x.iso").is_disc_source());
assert!(parse_url("dir://x").is_disc_source());
assert!(!parse_url("m2ts://x").is_disc_source());
assert!(!parse_url("mkv://x").is_disc_source());
assert!(!parse_url("network://h:1").is_disc_source());
+26
View File
@@ -353,6 +353,32 @@ mod tests {
);
}
// Pass 1 keeps a source sub-id only inside the codec's own range and only once: an AC-3
// track under a DTS-range id is re-allocated, and a duplicate id takes the lowest free.
#[test]
fn source_sub_ids_out_of_range_or_duplicated_are_reallocated() {
let t = title(vec![
video(Codec::Mpeg2),
audio(0xBD89, Codec::Ac3, SampleRate::S48, ""),
audio(0xBD81, Codec::Ac3, SampleRate::S48, ""),
audio(0xBD81, Codec::Ac3, SampleRate::S48, ""),
]);
let p = plan(&t);
let ac3 = |sub_id| {
Some(Carriage::Private {
sub_id,
kind: PrivateKind::Ac3,
})
};
assert_eq!(p.carriage[1], ac3(0x80), "0x89 is DTS range, not kept");
assert_eq!(p.carriage[2], ac3(0x81), "first 0x81 keeps its id");
assert_eq!(
p.carriage[3],
ac3(0x82),
"duplicate 0x81 takes the lowest free id"
);
}
// J23: a declared extension whose base is not carried is in neither half (seen-only).
#[test]
fn an_extension_without_a_carried_base_is_not_planned_out() {