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
+17
View File
@@ -115,6 +115,8 @@ impl CodecParser for Ac3Parser {
let duration_ns = frame_duration_ns(remaining, bsid);
frames.push(Frame {
coding: None,
source: None,
pts_ns: frame_pts_ns,
keyframe: true,
data: data[start..start + frame_size].to_vec(),
@@ -199,6 +201,8 @@ impl CodecParser for Ac3Parser {
}
let duration_ns = frame_duration_ns(frame, bsid);
vec![Frame {
coding: None,
source: None,
pts_ns: self.flush_pts_ns,
keyframe: true,
data: buf[off..off + frame_size].to_vec(),
@@ -441,6 +445,7 @@ mod tests {
fn parse_empty_pes() {
let mut parser = Ac3Parser::new();
let pes = PesPacket {
source: None,
pid: 0,
pts: None,
dts: None,
@@ -454,6 +459,7 @@ mod tests {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2); // 48kHz, 80 words = 160 bytes
let pes = PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,
@@ -472,6 +478,7 @@ mod tests {
// First PES: first half of frame
let pes1 = PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,
@@ -482,6 +489,7 @@ mod tests {
// Second PES: second half
let pes2 = PesPacket {
source: None,
pid: 0,
pts: Some(93000),
dts: None,
@@ -499,6 +507,7 @@ mod tests {
let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; // garbage
data.extend_from_slice(&frame_data);
let pes = PesPacket {
source: None,
pid: 0,
pts: None,
dts: None,
@@ -521,6 +530,7 @@ mod tests {
let mut pes1_data = frame_data.clone();
pes1_data.push(0x0B);
let pes1 = PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,
@@ -533,6 +543,7 @@ mod tests {
let mut pes2_data = vec![0x77];
pes2_data.extend_from_slice(&frame_data[2..]);
let pes2 = PesPacket {
source: None,
pid: 0,
pts: Some(93000),
dts: None,
@@ -558,6 +569,7 @@ mod tests {
*data.last_mut().unwrap() = 0x0B;
}
let pes = PesPacket {
source: None,
pid: 0,
pts: None,
dts: None,
@@ -585,6 +597,7 @@ mod tests {
let mut parser = Ac3Parser::new();
let data = vec![0x00, 0x00, 0x0B];
let pes = PesPacket {
source: None,
pid: 0,
pts: None,
dts: None,
@@ -621,6 +634,7 @@ mod tests {
let mut data = frame_data.clone();
data.extend_from_slice(&frame_data[..40]); // partial frame 2 held
let pes = PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,
@@ -652,6 +666,7 @@ mod tests {
let mut data = frame_data.clone();
data.extend_from_slice(&frame_data);
let pes = PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,
@@ -694,6 +709,7 @@ mod tests {
let good = make_ac3_frame(0, 2);
data.extend_from_slice(&good);
let pes = PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,
@@ -1152,6 +1168,7 @@ mod tests {
// helper: PES with a generic pts for E-AC-3 tests
fn make_eac3_pes(data: Vec<u8>) -> PesPacket {
PesPacket {
source: None,
pid: 0,
pts: Some(90000),
dts: None,