DVD vob_start absolute rebase + rc.5.3 audit fixes

- ifo.rs: rebase VTS title VOBS to absolute disc LBA (file_start_lba +
  vtstt_vobs); fixes DVD rips opening on the menu region instead of the
  movie (e.g. SOTL). Adds absolute-placement regression test.
- aacs/boil.rs: add mk_from_pk primitive (PK -> MK via MKB walk).
- dvdnav/: nav-VM command decoder + start-cell resolver seam, parked
  behind USE_NAV_RESOLVER (kept compiled, never executed).
- mux: FVI src.byte within-sector per spec; Unknown colour -> CICP
  unspecified (2,2,2,1); demux clear PCS -> NORMAL; ts.rs feed() base
  reset + boundary provenance fix.
- Assorted audit fixes (doc/comment/test accuracy) across the crate.
This commit is contained in:
Matthew Jackson
2026-06-26 17:03:58 -07:00
parent d8c323bf9f
commit 835cc990ad
31 changed files with 1149 additions and 129 deletions
+11 -4
View File
@@ -181,8 +181,13 @@ impl PictureInfo {
match self.detail {
CodingDetail::Mpeg2(m) => {
if !m.frame_picture {
// A single field picture is inherently interlaced; the
// top_field_first bit names which field this picture is.
// A single field picture is inherently interlaced. Which
// field it actually codes is given by picture_structure
// (top/bottom), not by top_field_first — §6.3.10 constrains
// top_field_first to 0 for field pictures, so it is not the
// spec source here. picture_structure is not retained on
// this carrier, so top_field_first is used only as the lone
// field hint available (best-effort, not spec-derived).
Some(if m.top_field_first {
FieldOrder::Tff
} else {
@@ -203,8 +208,10 @@ impl PictureInfo {
/// Number of field-display periods this picture occupies — the basis for
/// soft-telecine (2:3 pulldown) timing. MPEG-2 (ISO/IEC 13818-2 §6.3.10,
/// ffmpeg `nb_fields = repeat_pict + 2`): a field picture occupies 1 field,
/// a normal frame 2, a `repeat_first_field` frame 3 (or 4/6 in a progressive
/// sequence). Codecs without pulldown signalling report the normal 2 fields.
/// a normal frame 2, a `repeat_first_field` progressive-frame 3 (or 4/6 in a
/// progressive sequence); an rff bit on a non-progressive interlaced frame is
/// spec-forbidden (§6.3.10) and is treated as 2. Codecs without pulldown
/// signalling report the normal 2 fields.
pub fn nb_fields(&self) -> u8 {
match self.detail {
CodingDetail::Mpeg2(m) => {
+2 -1
View File
@@ -325,7 +325,8 @@ impl CodecParser for H264Parser {
}
/// Parse `(chroma_format_idc, bit_depth_luma_minus8, bit_depth_chroma_minus8)` from
/// a High-Profile SPS NAL (profile_idc ∈ {100, 110, 122, 144}).
/// a High-Profile SPS NAL (profile_idc ∈ `HIGH_PROFILES` — the 14 chroma/bit-depth
/// extended profiles `codec_private` invokes this for).
///
/// SPS RBSP layout (ITU-T H.264 §7.3.2.1.1) up to the fields we need:
/// byte 0 NAL header (already known to be type 7)
+1 -4
View File
@@ -56,10 +56,7 @@ fn hevc_num_extra_slice_header_bits(pps_nal: &[u8]) -> Option<u32> {
br.read_ue()?; // pps_pic_parameter_set_id
br.read_ue()?; // pps_seq_parameter_set_id
br.skip_bits(2)?; // dependent_slice_segments_enabled_flag, output_flag_present_flag
let mut n = 0u32;
for _ in 0..3 {
n = (n << 1) | br.read_bit()?;
}
let n = br.read_bits(3)?;
Some(n)
}
+3 -3
View File
@@ -273,8 +273,8 @@ const SUP_HEADER_LEN: usize = SUP_MAGIC.len() + 4 + 4;
const SEG_PCS: u8 = 0x16;
/// PGS segment type: END of display set.
const SEG_END: u8 = 0x80;
/// PCS `composition_state` value: Epoch Start (a fresh display).
const PCS_COMPOSITION_STATE_EPOCH_START: u8 = 0x80;
/// PCS `composition_state` value: Normal (an update to the current epoch).
const PCS_COMPOSITION_STATE_NORMAL: u8 = 0x00;
/// PGS segment header on the wire (inside `frame.data`): type(1) + size(2 BE).
const PGS_SEG_HEADER_LEN: usize = 3;
/// Byte offset of `width`/`height` within a PCS segment (after type+size).
@@ -346,7 +346,7 @@ impl PgsSupWriter {
PCS_FRAME_RATE,
0x00,
0x00, // composition_number
PCS_COMPOSITION_STATE_EPOCH_START,
PCS_COMPOSITION_STATE_NORMAL,
0x00, // palette_update_flag
0x00, // palette_id
PCS_NO_OBJECTS,
+13 -3
View File
@@ -82,8 +82,16 @@ fn write_fvi_record(w: &mut dyn Write, r: &PictureRecord) -> io::Result<()> {
// `src` is REQUIRED by the record schema (Appendix A); when provenance is
// absent the member is still emitted as null — a reader treats null as
// "position unknown".
//
// Per `docs/FVI_FORMAT.md` §9, `src.byte` is the offset of the AU's first
// byte WITHIN its `sector` (not the absolute source offset). `SourcePos.byte`
// is the absolute offset, so reduce it modulo the sector size; `sector`
// already carries the whole-sector count.
let src = match r.source {
Some(s) => serde_json::json!({ "sector": s.sector, "byte": s.byte }),
Some(s) => serde_json::json!({
"sector": s.sector,
"byte": s.byte % u64::from(FVI_SECTOR_SIZE),
}),
None => serde_json::Value::Null,
};
@@ -355,8 +363,9 @@ mod tests {
let dir = tempdir();
let path = dir.join("movie.fvi");
let mut sink = FviSink::create(&path, &mpeg2_title(), "iso://m.iso".into(), 1).unwrap();
// Video frame on track 0 → indexed.
sink.write(&vframe(0, Some(i_pic()), Some(SourcePos::at_byte(2048))))
// Video frame on track 0 → indexed. Offset 2148 = sector 1, byte 100
// within that sector (exercises the within-sector `src.byte`, §9).
sink.write(&vframe(0, Some(i_pic()), Some(SourcePos::at_byte(2148))))
.unwrap();
// Audio frame on a non-video track → ignored.
sink.write(&vframe(7, None, Some(SourcePos::at_byte(9999))))
@@ -387,6 +396,7 @@ mod tests {
assert_eq!(rec["nb_fields"], 2);
assert_eq!(rec["pts"], 0);
assert_eq!(rec["src"]["sector"], 1);
assert_eq!(rec["src"]["byte"], 100); // 2148 % 2048 → within-sector (§9)
assert!(rec.get("dts").is_none(), "no DTS on a frame → omitted");
assert!(
rec.get("gop").is_none(),
+4 -1
View File
@@ -254,7 +254,10 @@ impl<W: Write> M2tsMux<W> {
// step — e.g. a leading audio frame ahead of the first video keyframe),
// which still floors to 0 per the documented behavior.
let delta = raw_90k.wrapping_sub(base) & 0x1_FFFF_FFFF;
if delta > (1 << 32) { 0 } else { delta }
// Signed 33-bit: the sign bit is bit 32 (value 2^32), so the entire
// upper half [2^32, 2^33) is negative (frame before base) and floors
// to 0. delta == 2^32 is the most-negative value (-2^32), hence `>=`.
if delta >= (1 << 32) { 0 } else { delta }
}
/// Emit one PES payload as a chain of TS packets on `pid`. If `pcr`
+53 -1
View File
@@ -28,6 +28,9 @@ const CICP_PRIMARIES_BT470BG: u8 = 5;
const CICP_PRIMARIES_BT601_525: u8 = 6;
/// ColourPrimaries = 9 (BT.2020 / BT.2100) — ITU-T H.273 Table 2.
const CICP_PRIMARIES_BT2020: u8 = 9;
/// ColourPrimaries = 2 ("unspecified" — colorimetry unknown) — ITU-T H.273
/// Table 2.
const CICP_PRIMARIES_UNSPECIFIED: u8 = 2;
/// TransferCharacteristics = 1 (BT.709) — ITU-T H.273 Table 3.
const CICP_TRANSFER_BT709: u8 = 1;
@@ -41,6 +44,9 @@ const CICP_TRANSFER_PQ: u8 = 16;
/// TransferCharacteristics = 18 (ARIB STD-B67 / Hybrid Log-Gamma) — ITU-T H.273
/// Table 3.
const CICP_TRANSFER_HLG: u8 = 18;
/// TransferCharacteristics = 2 ("unspecified" — transfer unknown) — ITU-T H.273
/// Table 3.
const CICP_TRANSFER_UNSPECIFIED: u8 = 2;
/// MatrixCoefficients = 1 (BT.709) — ITU-T H.273 Table 4.
const CICP_MATRIX_BT709: u8 = 1;
@@ -50,6 +56,8 @@ const CICP_MATRIX_BT470BG: u8 = 5;
const CICP_MATRIX_BT601_525: u8 = 6;
/// MatrixCoefficients = 9 (BT.2020 non-constant luminance) — ITU-T H.273 Table 4.
const CICP_MATRIX_BT2020NC: u8 = 9;
/// MatrixCoefficients = 2 ("unspecified" — matrix unknown) — ITU-T H.273 Table 4.
const CICP_MATRIX_UNSPECIFIED: u8 = 2;
/// Matroska Colour/Range = 1 (broadcast / studio-swing "limited" range). RFC
/// 9559 Range element. (0 = unspecified, 2 = full.)
@@ -104,7 +112,16 @@ pub(crate) fn cicp_for_video(v: &VideoStream) -> (u8, u8, u8, u8) {
CICP_PRIMARIES_BT601_525,
COLOUR_RANGE_LIMITED,
),
ColorSpace::Unknown => (0, 0, 0, 0),
// Unknown colorimetry → CICP "unspecified" (code point 2) for matrix,
// transfer, and primaries, with limited range (the disc norm). Both the
// MKV sink and the FVI sidecar emit 2 so the two sinks of one title
// agree (matches `Colour::from_color_space`'s Unknown mapping).
ColorSpace::Unknown => (
CICP_MATRIX_UNSPECIFIED,
CICP_TRANSFER_UNSPECIFIED,
CICP_PRIMARIES_UNSPECIFIED,
COLOUR_RANGE_LIMITED,
),
};
// Override the transfer for HDR signalled by the HdrFormat (the coarse enum
// can't express PQ/HLG). Only applies on the enum fallback; a measured CICP
@@ -1549,6 +1566,41 @@ mod tests {
);
}
/// Unknown colorimetry with no measured CICP and no HDR must emit CICP
/// "unspecified" (code point 2) for matrix/transfer/primaries — never 0 — so
/// the MKV sink agrees with the FVI sidecar (`Colour::from_color_space`).
#[test]
fn unknown_color_space_emits_unspecified_cicp() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Hevc,
resolution: Resolution::R1080p,
frame_rate: crate::disc::FrameRate::F24,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Unknown,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert_eq!(
(
t.colour_matrix,
t.colour_transfer,
t.colour_primaries,
t.colour_range
),
(
CICP_MATRIX_UNSPECIFIED,
CICP_TRANSFER_UNSPECIFIED,
CICP_PRIMARIES_UNSPECIFIED,
COLOUR_RANGE_LIMITED
),
"Unknown colorimetry must emit CICP 'unspecified' (2), not 0"
);
}
/// Helper: search for a 4-byte big-endian EBML ID in a byte slice.
fn find_id(data: &[u8], id: u32) -> Option<usize> {
let bytes = id.to_be_bytes();
+68 -24
View File
@@ -101,8 +101,12 @@ enum WriteMode {
/// Header written; muxing live. Boxed (MkvMuxer is large) to keep the enum
/// small (clippy::large_enum_variant).
Active(Box<MkvMuxer<Box<dyn WriteSeek + Send>>>),
/// Transient placeholder held only across the Pending → Active swap; never
/// observed by `read` / `write` / `finish`.
/// Sentinel held in `self.mode` while the muxer is being built (across the
/// Pending → Active swap). It is also the terminal state left behind after
/// `finish()` swaps the muxer out, and the degraded state left behind if
/// `activate()` fails partway (the first error still surfaces via `?`). In
/// that terminal state a subsequent `write()` no-ops (`Ok(())`) and `finish()`
/// does not re-finalize.
Building,
}
@@ -188,7 +192,11 @@ impl MkvStream {
/// available), then write the header and replay buffered frames. A no-op if
/// not pending. The muxer only ever muxes the track it is given — this routes
/// the parser's measured value onto that track first.
fn activate(&mut self, coding: Option<crate::mux::codec::PictureInfo>) -> io::Result<()> {
fn activate(
&mut self,
coding: Option<crate::mux::codec::PictureInfo>,
video_picture_seen: bool,
) -> io::Result<()> {
let mut pending = match std::mem::replace(&mut self.mode, Mode::Write(WriteMode::Building))
{
Mode::Write(WriteMode::Pending(p)) => p,
@@ -199,7 +207,7 @@ impl MkvStream {
}
};
if let Some(vt) = pending.video_track {
apply_coding_to_track(&mut pending.tracks[vt], coding);
apply_coding_to_track(&mut pending.tracks[vt], coding, video_picture_seen);
}
// --log-level 3: dump the FINAL TrackEntry metadata (field order set).
for (i, track) in pending.tracks.iter().enumerate() {
@@ -240,12 +248,20 @@ impl MkvStream {
/// Set a video track's `FieldOrder` from the MEASURED coding of the first coded
/// picture — the parser's value, the first time, never a guess.
///
/// A progressive track has no field order (left UNDETERMINED — expected). An
/// INTERLACED track that reaches here with no measured field order is a
/// A progressive track — or a progressive picture on an interlaced-flagged track
/// — has no field order (left UNDETERMINED — expected). An INTERLACED track that
/// reaches here WITH a video picture but no measured field order is a
/// parser/source gap (MPEG-2 carries `top_field_first` on every interlaced
/// picture, so it should never be missing): LOG it loudly so the source can be
/// debugged, and leave UNDETERMINED — a muxer never fabricates a source fact.
fn apply_coding_to_track(track: &mut MkvTrack, coding: Option<crate::mux::codec::PictureInfo>) {
/// `video_picture_seen == false` (an empty title finalized with no frames, or a
/// cap-triggered build that never saw the video frame) is NOT a defect — the
/// missing coding is expected there, so log it quietly.
fn apply_coding_to_track(
track: &mut MkvTrack,
coding: Option<crate::mux::codec::PictureInfo>,
video_picture_seen: bool,
) {
// HDR10 static metadata measured from the bitstream (HEVC SEI). Applied for
// ANY track type that carries it (independent of interlace): the first coded
// picture's PictureInfo holds it once both HDR10 SEI messages were seen.
@@ -260,17 +276,32 @@ fn apply_coding_to_track(track: &mut MkvTrack, coding: Option<crate::mux::codec:
match coding.and_then(|c| c.field_order()) {
Some(FieldOrder::Tff) => track.field_order = ebml::FIELD_ORDER_TFF,
Some(FieldOrder::Bff) => track.field_order = ebml::FIELD_ORDER_BFF,
other => {
// A progressive picture on an interlaced-flagged track carries no field
// order. Leave UNDETERMINED (not a guess) — there is no parser gap here.
Some(FieldOrder::Progressive) => {
track.field_order = ebml::FIELD_ORDER_UNDETERMINED;
}
None if video_picture_seen => {
tracing::warn!(
target: "mux",
"interlaced video track reached the muxer with NO measured field order \
(field_order={:?}, coding_present={}); writing FieldOrder=UNDETERMINED \
NOT a guess. Debug why the source/parser did not set top_field_first.",
other,
"interlaced video track had a video picture but NO usable field order \
(coding_present={}); writing FieldOrder=UNDETERMINED NOT a guess. \
Debug why the source/parser did not set top_field_first.",
coding.is_some(),
);
track.field_order = ebml::FIELD_ORDER_UNDETERMINED;
}
None => {
// No video picture was ever measured (empty title finalized with no
// frames, or a cap-triggered build before the first video frame).
// Coding is legitimately absent, not a parser defect — log quietly.
tracing::debug!(
target: "mux",
"interlaced video track activated with no video picture \
(empty/buffered-only title); writing FieldOrder=UNDETERMINED.",
);
track.field_order = ebml::FIELD_ORDER_UNDETERMINED;
}
}
}
@@ -427,7 +458,7 @@ impl crate::pes::Stream for MkvStream {
// Pass the trigger frame's coding only when it IS the video frame; a
// cap-triggered build never saw the video frame, so nothing measured
// is passed (apply_coding_to_track then logs + leaves UNDETERMINED).
self.activate(if use_coding { frame.coding } else { None })?;
self.activate(if use_coding { frame.coding } else { None }, use_coding)?;
if let Mode::Write(WriteMode::Active(m)) = &mut self.mode {
return m.write_frame(
frame.track,
@@ -450,7 +481,10 @@ impl crate::pes::Stream for MkvStream {
// A title that produced no frames (or only buffered ones) is still
// finalized into a valid MKV: activate now with no measured coding.
if matches!(self.mode, Mode::Write(WriteMode::Pending(_))) {
self.activate(None)?;
// No video picture was ever measured for this title (it produced no
// frames, or only buffered non-video ones): coding is legitimately
// absent, not a parser defect — `video_picture_seen=false`.
self.activate(None, false)?;
}
if let Mode::Write(WriteMode::Active(m)) =
std::mem::replace(&mut self.mode, Mode::Write(WriteMode::Building))
@@ -923,7 +957,7 @@ mod tests {
// MEASURED bottom-field-first → BFF (6). The red-flag fix.
let mut t = interlaced_track();
apply_coding_to_track(&mut t, Some(pic(false, false)));
apply_coding_to_track(&mut t, Some(pic(false, false)), true);
assert_eq!(
t.field_order,
ebml::FIELD_ORDER_BFF,
@@ -932,27 +966,37 @@ mod tests {
// MEASURED top-field-first → TFF (1).
let mut t = interlaced_track();
apply_coding_to_track(&mut t, Some(pic(true, false)));
apply_coding_to_track(&mut t, Some(pic(true, false)), true);
assert_eq!(
t.field_order,
ebml::FIELD_ORDER_TFF,
"measured TFF → FieldOrder=1"
);
// Interlaced track, NO measured coding → UNDETERMINED (logged loudly,
// never faked).
// Interlaced track, a video picture but NO usable field order →
// UNDETERMINED (logged loudly, never faked).
let mut t = interlaced_track();
apply_coding_to_track(&mut t, None);
apply_coding_to_track(&mut t, None, true);
assert_eq!(
t.field_order,
ebml::FIELD_ORDER_UNDETERMINED,
"no measured value → UNDETERMINED, never a guess"
);
// Interlaced track activated with NO video picture (empty/buffered-only
// title) → UNDETERMINED, logged quietly (not a parser defect).
let mut t = interlaced_track();
apply_coding_to_track(&mut t, None, false);
assert_eq!(
t.field_order,
ebml::FIELD_ORDER_UNDETERMINED,
"empty title → UNDETERMINED, never a guess"
);
// Progressive picture on an interlaced-flagged track → UNDETERMINED (no
// field order applies; not faked to TFF/BFF).
let mut t = interlaced_track();
apply_coding_to_track(&mut t, Some(pic(true, true)));
apply_coding_to_track(&mut t, Some(pic(true, true)), true);
assert_eq!(t.field_order, ebml::FIELD_ORDER_UNDETERMINED);
// A PROGRESSIVE track is never touched — field order stays UNDETERMINED.
@@ -969,7 +1013,7 @@ mod tests {
measured_cicp: None,
});
assert!(!prog.interlaced);
apply_coding_to_track(&mut prog, Some(pic(false, false)));
apply_coding_to_track(&mut prog, Some(pic(false, false)), true);
assert_eq!(prog.field_order, ebml::FIELD_ORDER_UNDETERMINED);
}
@@ -1011,18 +1055,18 @@ mod tests {
let mut t = make();
assert!(t.hdr10.is_none(), "fresh track has no HDR10");
let pic = PictureInfo::coding_type_only(CodingType::I).with_hdr10(Some(h));
apply_coding_to_track(&mut t, Some(pic));
apply_coding_to_track(&mut t, Some(pic), true);
assert_eq!(t.hdr10, Some(h), "measured HDR10 must reach the track");
// Picture without HDR10 → track stays None (never fabricated).
let mut t = make();
let pic = PictureInfo::coding_type_only(CodingType::I);
apply_coding_to_track(&mut t, Some(pic));
apply_coding_to_track(&mut t, Some(pic), true);
assert!(t.hdr10.is_none(), "no measured HDR10 → track stays None");
// No coding at all → None.
let mut t = make();
apply_coding_to_track(&mut t, None);
apply_coding_to_track(&mut t, None, true);
assert!(t.hdr10.is_none());
}
+25
View File
@@ -12,6 +12,7 @@
//! | network:// | Yes (listen) | Yes (connect) | host:port (required) |
//! | stdio:// | Yes (stdin) | Yes (stdout) | empty |
//! | null:// | -- | Yes | empty |
//! | demux:// | -- | Yes | directory path (required) — per-track ES demux |
//! | fvi:// | -- | Yes | file path (required) — per-picture video index |
//!
//! Bare paths without a scheme are rejected.
@@ -920,6 +921,30 @@ mod tests {
!parse_url("dir://x").is_disc_source(),
"dir:// is a sink, never a disc source"
);
// fvi:// parses to Fvi with the raw remainder as the path, and is a
// sink (never a disc source) — parallel to the demux:// coverage above.
match parse_url("fvi://out/movie.fvi") {
StreamUrl::Fvi { path } => {
assert_eq!(path, PathBuf::from("out/movie.fvi"));
}
other => panic!("fvi:// must parse to Fvi, got {other:?}"),
}
assert_eq!(parse_url("fvi://x").scheme(), "fvi");
assert_eq!(parse_url("fvi://x/y.fvi").path_str(), "x/y.fvi");
assert!(
!parse_url("fvi://x").is_disc_source(),
"fvi:// is a sink, never a disc source"
);
}
/// `fvi://` is output-only: `input()` rejects it with StreamWriteOnly
/// (E9001 → Unsupported), mirroring `null://` / `demux://`.
#[test]
fn input_fvi_url_is_write_only() {
assert_eq!(
input_err_kind("fvi://out/movie.fvi"),
std::io::ErrorKind::Unsupported
);
}
/// `dir://` is output-only: `input()` rejects it (StreamWriteOnly →
+88 -7
View File
@@ -229,6 +229,12 @@ impl TsDemuxer {
/// `data` in place. Zero-copy on the bulk path; one 192-byte copy
/// on the boundary.
pub fn feed(&mut self, data: &[u8]) -> Vec<PesPacket> {
// A plain feed carries no provenance. Reset any base a prior
// `feed_at` left behind so mixing the two entry points is safe:
// after this call no `SourcePos` is stamped, and the stale running
// base can't leak a wrong offset into the boundary packet.
self.feed_base = 0;
self.has_base = false;
self.feed_inner(data)
}
@@ -263,15 +269,20 @@ impl TsDemuxer {
self.remainder.extend_from_slice(data);
return completed;
}
// Capture the remainder length before clearing — it's how many of
// the boundary packet's bytes lived in the PREVIOUS feed buffer,
// and `feed_base` currently points at the FIRST byte of THIS buffer.
let rem_len = self.remainder.len();
let mut boundary = [0u8; BD_SOURCE_PACKET_BYTES];
boundary[..self.remainder.len()].copy_from_slice(&self.remainder);
boundary[self.remainder.len()..].copy_from_slice(&data[..need]);
boundary[..rem_len].copy_from_slice(&self.remainder);
boundary[rem_len..].copy_from_slice(&data[..need]);
self.remainder.clear();
// The boundary packet began in the PREVIOUS feed buffer; stamp it
// with the offset just before this buffer (its first bytes' base).
let src = self
.has_base
.then(|| crate::pes::SourcePos::at_byte(self.feed_base.saturating_sub(1)));
// The boundary packet's first byte sat `rem_len` bytes before the
// current feed_base (in the previous buffer). Stamp it there — not
// at `feed_base - 1`, which would be wrong by `rem_len - 1` bytes.
let src = self.has_base.then(|| {
crate::pes::SourcePos::at_byte(self.feed_base.saturating_sub(rem_len as u64))
});
self.process_packet(&boundary, src, &mut completed);
offset = need;
}
@@ -931,6 +942,76 @@ mod tests {
assert!(result.is_empty());
}
/// A boundary packet (one split across two feeds) must be stamped with the
/// source offset of its FIRST byte, which sat `remainder.len()` bytes before
/// the current feed's base — not at `feed_base - 1`. We feed two 192-byte
/// packets via `feed_at`, splitting mid-second-packet so the second packet
/// is reassembled at the boundary, and assert its provenance lands exactly
/// on its first byte.
#[test]
fn boundary_packet_source_is_first_byte_not_base_minus_one() {
let pid = 0x1011;
let base: u64 = 20480; // sector-aligned (10 × 2048)
let mut demux = TsDemuxer::new(&[pid]);
let pkt0 = ts_payload_packet(pid, true, 0, &pes_start(b"AAAA"));
let pkt1 = ts_payload_packet(pid, true, 1, &pes_start(b"BBBB"));
let mut full = pkt0;
full.extend_from_slice(&pkt1);
// Split mid-pkt1 → pkt1 is reassembled from a 100-byte remainder + the
// next feed's head. pkt1's first byte is at absolute offset base + 192.
let split = BD_SOURCE_PACKET_BYTES + 100;
let out1 = demux.feed_at(base, &full[..split]);
assert!(out1.is_empty(), "pkt0's PES is still open");
// Second feed carries the rest; data[0] is at absolute base + split.
let out2 = demux.feed_at(base + split as u64, &full[split..]);
// pkt1 (PUSI) flushes pkt0's "AAAA" PES, stamped at pkt0's first byte.
assert_eq!(out2.len(), 1, "pkt0's PES completes when pkt1 starts");
assert_eq!(
out2[0].source.map(|s| s.byte),
Some(base),
"AAAA PES provenance is pkt0's first byte"
);
// Flush emits pkt1's "BBBB" PES — its source is the boundary stamp.
let out3 = demux.flush();
assert_eq!(out3.len(), 1, "pkt1's PES flushes out");
assert_eq!(
out3[0].source.map(|s| s.byte),
Some(base + BD_SOURCE_PACKET_BYTES as u64),
"boundary packet provenance must be its first byte (base + 192), \
not feed_base - 1"
);
}
/// Owner decision #7: a plain `feed()` must reset/ignore any base a prior
/// `feed_at()` left behind, so mixing the two is safe. After a `feed_at`
/// primes a base, the next plain `feed` must stamp `None` on PES packets it
/// begins.
#[test]
fn plain_feed_resets_prior_feed_at_base() {
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
// Prime a base via feed_at; pkt0's PES stays open.
let out1 = demux.feed_at(20480, &ts_payload_packet(pid, true, 0, &pes_start(b"AAAA")));
assert!(out1.is_empty());
// Plain feed must clear the base. pkt1 (PUSI) flushes "AAAA" (which was
// stamped during feed_at) and starts "BBBB" with NO provenance.
let out2 = demux.feed(&ts_payload_packet(pid, true, 1, &pes_start(b"BBBB")));
assert_eq!(out2.len(), 1, "AAAA completes");
let out3 = demux.flush();
assert_eq!(out3.len(), 1, "BBBB flushes");
assert_eq!(
out3[0].source, None,
"PES begun by a plain feed must carry no source after a prior feed_at"
);
}
// ── scan_streams PMT parsing ──────────────────────────────────────────
/// Wrap a 188-byte TS packet body in a 192-byte BD-TS packet
+15
View File
@@ -551,6 +551,21 @@ mod tests {
},
"measured CICP must override the coarse color_space enum"
);
// Unknown colorimetry, SDR, no measured CICP → all code points map to
// "unspecified" (2), matching `from_color_space(Unknown)`. Both sinks of
// one title must emit 2, never 0.
let c = Colour::from_video(&mk(HdrFormat::Sdr, ColorSpace::Unknown, None));
assert_eq!(
c,
Colour {
primaries: 2,
transfer: 2,
matrix: 2,
full_range: false,
},
"Unknown colorimetry must emit CICP 'unspecified' (2), not 0"
);
}
#[test]