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
+46 -1
View File
@@ -51,6 +51,10 @@ pub struct PsPacket {
pub dts: Option<u64>,
/// Elementary stream payload data.
pub data: Vec<u8>,
/// Source position of this PES's first ES byte, stamped at the demux seam
/// from the producer's known stream offset. `None` when the demuxer was fed
/// without a base offset.
pub source: Option<crate::pes::SourcePos>,
}
/// Canonical DVD video PID. DVD-Video carries a single MPEG-2 video
@@ -129,6 +133,12 @@ impl PsPacket {
/// Handles non-aligned input by buffering leftover bytes between calls.
pub struct PsDemuxer {
buffer: Vec<u8>,
/// Absolute source byte offset of `buffer[0]` — the running base that turns
/// an in-buffer unit position into a [`crate::pes::SourcePos`]. Advanced as
/// the buffer drains. `has_base` gates stamping so non-provenance callers
/// stay byte-identical.
buffer_base: u64,
has_base: bool,
}
impl Default for PsDemuxer {
@@ -142,6 +152,8 @@ impl PsDemuxer {
pub fn new() -> Self {
Self {
buffer: Vec::with_capacity(64 * 1024),
buffer_base: 0,
has_base: false,
}
}
@@ -151,6 +163,24 @@ impl PsDemuxer {
self.extract_packets(false)
}
/// Like [`feed`](Self::feed) but records the absolute source byte offset of
/// `data[0]`, so every PES this call completes is stamped with a
/// [`crate::pes::SourcePos`]. The provenance-stamping entry point; the
/// highway calls this with each batch's known source offset. The base must
/// be the offset of the FIRST byte appended (i.e. of `data[0]`), which lines
/// up with the current buffer tail.
pub fn feed_at(&mut self, base_offset: u64, data: &[u8]) -> Vec<PsPacket> {
if !self.has_base {
// First base seen: the offset of data[0] is base_offset, and data[0]
// lands at buffer[buffer.len()], so buffer[0] is base_offset minus
// the bytes already buffered.
self.buffer_base = base_offset.saturating_sub(self.buffer.len() as u64);
self.has_base = true;
}
self.buffer.extend_from_slice(data);
self.extract_packets(false)
}
/// Flush remaining buffered data, returning any final PES packets.
pub fn flush(&mut self) -> Vec<PsPacket> {
// At EOF, an unbounded (length 0) PES with no trailing start code is
@@ -255,7 +285,11 @@ impl PsDemuxer {
e
};
if let Some(pkt) = parse_pes_packet(&self.buffer[sc..end]) {
if let Some(mut pkt) = parse_pes_packet(&self.buffer[sc..end]) {
if self.has_base {
pkt.source =
Some(crate::pes::SourcePos::at_byte(self.buffer_base + sc as u64));
}
packets.push(pkt);
}
pos = end;
@@ -269,6 +303,11 @@ impl PsDemuxer {
if pos > 0 {
self.buffer.drain(..pos);
// Advance the absolute base past the drained bytes so subsequent
// units stamp from the correct offset.
if self.has_base {
self.buffer_base += pos as u64;
}
}
packets
@@ -339,6 +378,9 @@ fn parse_pes_packet(data: &[u8]) -> Option<PsPacket> {
pts: None,
dts: None,
data: payload.to_vec(),
// Stamped by the demuxer (extract_packets) when a source base is
// threaded; the free function has no absolute offset of its own.
source: None,
});
}
@@ -393,6 +435,8 @@ fn parse_pes_packet(data: &[u8]) -> Option<PsPacket> {
pts,
dts,
data: es_data,
// Stamped by the demuxer (extract_packets) when a source base is threaded.
source: None,
})
}
@@ -788,6 +832,7 @@ mod tests {
pts: None,
dts: None,
data: vec![0xAA],
source: None,
}
}