mux: codec-agnostic PictureInfo + provenance; measure field order, never guess

Carry per-picture truth and byte-exact source provenance THROUGH the stream so
the muxer (and the upcoming video index) read MEASURED facts instead of
assuming them. Honest data in, honest data out.

- codec/coding.rs: codec-agnostic PictureInfo (CodingType / FieldOrder + the
  accessors field_order/coding_type/nb_fields/progressive/keyframe). Each codec
  folds its raw signals in; consumers use only accessors, never branch on codec.
- mpeg2: builds PictureInfo from the picture coding extension and carries it +
  SourcePos (source_marks, parallel to pts_marks) on every emitted frame.
- pes / codec::Frame: additive `coding` + `source`, forwarded through the
  highway; None for audio/subtitle and the network/stdio deserialize hop.
- mkvstream: DEFER muxer construction until the first coded picture, set the
  video track's FieldOrder from the MEASURED value, THEN write the header —
  right the first time, no guess, no seek-back. An interlaced track that arrives
  with no measured order is LOGGED loudly and left UNDETERMINED, never faked.
- mkv: MkvTrack::video no longer guesses TFF (a bitstream property the scan
  cannot know is UNDETERMINED at build). Removed VideoStream::top_field_first
  (the dead scan-time guess) crate-wide.
- Tests: parser population (every PictureInfo facet + per-PES source carry) and
  mux-stream consumption (measured -> correct; missing -> UNDETERMINED, not
  faked). Two obsolete tests updated only after confirming (their own comments)
  they existed to enforce the deleted hardcoded-TFF.
This commit is contained in:
Matthew Jackson
2026-06-25 20:13:55 -07:00
parent 43fb97f71f
commit e3dbafcebd
32 changed files with 1188 additions and 192 deletions
+20 -1
View File
@@ -832,7 +832,6 @@ mod tests {
display_aspect: None,
secondary: false,
label: String::new(),
top_field_first: None,
measured_cicp: None,
})
}
@@ -874,6 +873,8 @@ mod tests {
let mut w = AnnexBWriter::new(Codec::H264, None);
let mut out = Vec::new();
let f = PesFrame {
coding: None,
source: None,
track: 0,
pts: 0,
keyframe: true,
@@ -901,6 +902,8 @@ mod tests {
let mut w = AnnexBWriter::new(Codec::H264, Some(&rec));
let mut out = Vec::new();
let f1 = PesFrame {
coding: None,
source: None,
track: 0,
pts: 0,
keyframe: true,
@@ -920,6 +923,8 @@ mod tests {
// Second frame: NO param re-prepend.
let mut out2 = Vec::new();
let f2 = PesFrame {
coding: None,
source: None,
track: 0,
pts: 0,
keyframe: false,
@@ -1000,6 +1005,8 @@ mod tests {
pcs.extend_from_slice(&[0x07, 0x80, 0x04, 0x38]); // 1920x1080
pcs.extend_from_slice(&[0x10, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01]); // 1 object
let f = PesFrame {
coding: None,
source: None,
track: 0,
pts: 1_000_000_000, // 1s
keyframe: true,
@@ -1043,6 +1050,8 @@ mod tests {
fn pgs_frame_without_duration_emits_no_clear() {
// No duration → no synthetic clear (the subtitle's wipe time is unknown).
let f = PesFrame {
coding: None,
source: None,
track: 0,
pts: 0,
keyframe: true,
@@ -1080,6 +1089,8 @@ mod tests {
let mut w = VobSubWriter::new(idx.clone(), Some(b"palette: 000000, ffffff"), "eng");
let mut sub = Vec::new();
let f1 = PesFrame {
coding: None,
source: None,
track: 0,
pts: 0,
keyframe: true,
@@ -1087,6 +1098,8 @@ mod tests {
duration_ns: None,
};
let f2 = PesFrame {
coding: None,
source: None,
track: 0,
pts: 1_000_000_000,
keyframe: true,
@@ -1176,6 +1189,8 @@ mod tests {
// Video frame (track 0) and audio frame (track 1).
sink.write(&PesFrame {
coding: None,
source: None,
track: 0,
pts: 0,
keyframe: true,
@@ -1184,6 +1199,8 @@ mod tests {
})
.unwrap();
sink.write(&PesFrame {
coding: None,
source: None,
track: 1,
pts: 100_000_000, // audio 100ms late
keyframe: true,
@@ -1216,6 +1233,8 @@ mod tests {
};
let mut sink = DemuxSink::create(&dir, &title, &opts).unwrap();
sink.write(&PesFrame {
coding: None,
source: None,
track: 1,
pts: 0,
keyframe: true,