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:
+72
-6
@@ -26,6 +26,10 @@ pub struct PesPacket {
|
||||
pub dts: Option<i64>,
|
||||
/// Elementary stream data (video frame, audio frame, subtitle segment, etc.).
|
||||
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 (callers that don't need provenance).
|
||||
pub source: Option<crate::pes::SourcePos>,
|
||||
}
|
||||
|
||||
/// Per-PID PES reassembly state.
|
||||
@@ -51,6 +55,11 @@ struct PesAssembler {
|
||||
/// partial PES would inject corrupt bytes. The partial PES is dropped and
|
||||
/// the assembler resyncs on the next PUSI. `None` until the first packet.
|
||||
last_cc: Option<u8>,
|
||||
/// Absolute source byte offset of the in-progress PES's first byte (the
|
||||
/// PUSI packet that began it), or `None` when no source base is threaded.
|
||||
/// Stamped at PES start, emitted on the completed packet — provenance is
|
||||
/// carried, never reconstructed downstream.
|
||||
pes_source: Option<crate::pes::SourcePos>,
|
||||
}
|
||||
|
||||
/// Initial capacity for a fresh PES buffer. Sized to cover the
|
||||
@@ -84,17 +93,26 @@ impl PesAssembler {
|
||||
active: false,
|
||||
header_remaining: 0,
|
||||
last_cc: None,
|
||||
pes_source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new PES packet. Returns the completed previous packet (if any).
|
||||
fn start(&mut self, pts: Option<i64>, dts: Option<i64>) -> Option<PesPacket> {
|
||||
/// `source` is the absolute source position of the new PES's first byte
|
||||
/// (carried onto the completed packet at the next start / flush).
|
||||
fn start(
|
||||
&mut self,
|
||||
pts: Option<i64>,
|
||||
dts: Option<i64>,
|
||||
source: Option<crate::pes::SourcePos>,
|
||||
) -> Option<PesPacket> {
|
||||
let completed = if self.active && !self.buffer.is_empty() {
|
||||
Some(PesPacket {
|
||||
pid: self.pid,
|
||||
pts: self.pts,
|
||||
dts: self.dts,
|
||||
data: std::mem::replace(&mut self.buffer, Vec::with_capacity(PES_BUFFER_INIT_CAP)),
|
||||
source: self.pes_source,
|
||||
})
|
||||
} else {
|
||||
self.buffer.clear();
|
||||
@@ -103,6 +121,7 @@ impl PesAssembler {
|
||||
self.pts = pts;
|
||||
self.dts = dts;
|
||||
self.active = true;
|
||||
self.pes_source = source;
|
||||
completed
|
||||
}
|
||||
|
||||
@@ -139,6 +158,7 @@ impl PesAssembler {
|
||||
pts: self.pts,
|
||||
dts: self.dts,
|
||||
data: std::mem::take(&mut self.buffer),
|
||||
source: self.pes_source,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -151,6 +171,14 @@ pub struct TsDemuxer {
|
||||
assemblers: Vec<PesAssembler>,
|
||||
pid_index: Vec<i16>, // PID → index into assemblers, -1 = not tracked
|
||||
remainder: Vec<u8>, // leftover bytes from previous feed() call
|
||||
/// Absolute source byte offset of the NEXT byte to be fed — the running
|
||||
/// base that turns an in-buffer packet offset into a source position.
|
||||
/// Advanced by each `feed` by the bytes consumed; `feed` (no base) leaves
|
||||
/// it at 0 so non-provenance callers stamp `None`.
|
||||
feed_base: u64,
|
||||
/// True once a caller has threaded a source base via [`feed_at`]. Until
|
||||
/// then no `SourcePos` is stamped (keeps existing callers byte-identical).
|
||||
has_base: bool,
|
||||
}
|
||||
|
||||
impl TsDemuxer {
|
||||
@@ -185,6 +213,8 @@ impl TsDemuxer {
|
||||
assemblers,
|
||||
pid_index,
|
||||
remainder: Vec::new(),
|
||||
feed_base: 0,
|
||||
has_base: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +231,27 @@ 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> {
|
||||
self.feed_inner(data)
|
||||
}
|
||||
|
||||
/// Like [`feed`](Self::feed) but records the absolute source byte offset of
|
||||
/// `data[0]` first, so every PES this batch completes is stamped with a
|
||||
/// [`crate::pes::SourcePos`]. The single provenance-stamping entry point;
|
||||
/// the highway calls this with each batch's known source offset.
|
||||
pub fn feed_at(&mut self, base_offset: u64, data: &[u8]) -> Vec<PesPacket> {
|
||||
self.feed_base = base_offset;
|
||||
self.has_base = true;
|
||||
self.feed_inner(data)
|
||||
}
|
||||
|
||||
/// Source position for a packet whose first byte is at `buf_offset` within
|
||||
/// the current feed buffer — `None` until a base has been threaded.
|
||||
fn pkt_source(&self, buf_offset: usize) -> Option<crate::pes::SourcePos> {
|
||||
self.has_base
|
||||
.then(|| crate::pes::SourcePos::at_byte(self.feed_base + buf_offset as u64))
|
||||
}
|
||||
|
||||
fn feed_inner(&mut self, data: &[u8]) -> Vec<PesPacket> {
|
||||
let mut completed = Vec::with_capacity(4);
|
||||
let mut offset = 0;
|
||||
|
||||
@@ -218,15 +269,26 @@ impl TsDemuxer {
|
||||
boundary[..self.remainder.len()].copy_from_slice(&self.remainder);
|
||||
boundary[self.remainder.len()..].copy_from_slice(&data[..need]);
|
||||
self.remainder.clear();
|
||||
self.process_packet(&boundary, &mut completed);
|
||||
// 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)));
|
||||
self.process_packet(&boundary, src, &mut completed);
|
||||
offset = need;
|
||||
}
|
||||
|
||||
// Aligned-packets fast path — reads directly out of `data`.
|
||||
while offset + BD_TS_PACKET_SIZE <= data.len() {
|
||||
let packet = &data[offset..offset + BD_TS_PACKET_SIZE];
|
||||
let src = self.pkt_source(offset);
|
||||
offset += BD_TS_PACKET_SIZE;
|
||||
self.process_packet(packet, &mut completed);
|
||||
self.process_packet(packet, src, &mut completed);
|
||||
}
|
||||
// Advance the running base past every byte consumed this feed so the
|
||||
// next batch stamps from the correct absolute offset.
|
||||
if self.has_base {
|
||||
self.feed_base += offset as u64;
|
||||
}
|
||||
|
||||
// Save leftover bytes for next call (cap at one packet to
|
||||
@@ -248,7 +310,12 @@ impl TsDemuxer {
|
||||
/// `PesAssembler`; completed PES packets are pushed onto
|
||||
/// `completed` so the caller's allocation amortises across the
|
||||
/// batch.
|
||||
fn process_packet(&mut self, packet: &[u8], completed: &mut Vec<PesPacket>) {
|
||||
fn process_packet(
|
||||
&mut self,
|
||||
packet: &[u8],
|
||||
source: Option<crate::pes::SourcePos>,
|
||||
completed: &mut Vec<PesPacket>,
|
||||
) {
|
||||
// Sync byte check skips malformed packets.
|
||||
if packet[4] != SYNC_BYTE {
|
||||
return;
|
||||
@@ -331,7 +398,7 @@ impl TsDemuxer {
|
||||
// `header_len` is the FULL (uncapped) PES-header length:
|
||||
// 0 = malformed (payload is not a PES start), else 6/9+N.
|
||||
let (pts, dts, header_len) = parse_pes_header(payload);
|
||||
if let Some(prev) = asm.start(pts, dts) {
|
||||
if let Some(prev) = asm.start(pts, dts, source) {
|
||||
completed.push(prev);
|
||||
}
|
||||
if header_len == 0 {
|
||||
@@ -694,7 +761,6 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
|
||||
display_aspect: None,
|
||||
secondary: false,
|
||||
label: String::new(),
|
||||
top_field_first: None,
|
||||
measured_cicp: None,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user