diff --git a/src/mux/codec/ac3.rs b/src/mux/codec/ac3.rs index 3a7378a..e8f4281 100644 --- a/src/mux/codec/ac3.rs +++ b/src/mux/codec/ac3.rs @@ -62,8 +62,11 @@ const AC3_SAMPLES_PER_FRAME: u32 = 1536; const MAX_AC3_BUF: usize = 1024 * 1024; pub struct Ac3Parser { - /// Leftover bytes from previous PES (incomplete frame at end). - buf: Vec, + /// Leftover bytes from previous PES (incomplete frame at end), each still + /// attributable to the packet that carried it — so an access unit that + /// began in an earlier packet takes THAT packet's source offset, not the + /// one that happened to complete it. + acc: super::pesbuf::PesBuf, /// PTS (ns) to stamp on the frame that begins the carry-over `buf` — i.e. /// the running per-frame PTS at the point the partial tail was retained. /// Used by `flush()` to time the final buffered frame at EOS. @@ -92,7 +95,7 @@ impl Default for Ac3Parser { impl Ac3Parser { pub fn new() -> Self { Self { - buf: Vec::with_capacity(4096), + acc: super::pesbuf::PesBuf::with_capacity(4096), flush_pts_ns: 0, tally: super::dropgate::DropTally::new("ac3"), saw_extension: false, @@ -137,6 +140,7 @@ impl Ac3Parser { base_pts_ns: i64, anchor: Option, at_eos: bool, + marks: &[(usize, super::pesbuf::PesFacts)], ) -> (Vec, usize, i64) { let mut frames = Vec::new(); let mut pos = 0usize; @@ -219,7 +223,7 @@ impl Ac3Parser { } } else { if let Some(au) = pending.take() { - close_access_unit(&mut self.tally, data, &au, &mut frames); + close_access_unit(&mut self.tally, data, &au, marks, &mut frames); } // First access unit that starts in this PES's own bytes: adopt // this PES's timestamp so a genuine PTS jump is followed instead @@ -264,7 +268,7 @@ impl Ac3Parser { frame_pts_ns = au.pts_ns; hold_from = Some(au.start); } else { - close_access_unit(&mut self.tally, data, &au, &mut frames); + close_access_unit(&mut self.tally, data, &au, marks, &mut frames); } } @@ -334,6 +338,7 @@ fn close_access_unit( tally: &mut super::dropgate::DropTally, data: &[u8], au: &PendingAu, + marks: &[(usize, super::pesbuf::PesFacts)], out: &mut Vec, ) { if let Some(reason) = au.drop_reason { @@ -344,7 +349,9 @@ fn close_access_unit( out.push(Frame { discontinuity: false, coding: None, - source: None, + // The packet covering this unit's FIRST byte — which is the packet its + // PTS came from too, when the unit began in an earlier PES. + source: super::pesbuf::facts_for(marks, au.start).source, pts_ns: au.pts_ns, keyframe: true, data: data[au.start..au.end].to_vec(), @@ -461,7 +468,7 @@ impl CodecParser for Ac3Parser { // never be stranded by an empty post-gap PES (the demuxer only emits // non-empty PES today; this is defensive for any future caller). if pes.discontinuity { - self.buf.clear(); + self.acc.clear(); } if pes.data.is_empty() { return Vec::new(); @@ -480,7 +487,7 @@ impl CodecParser for Ac3Parser { // no anchor the running cadence simply continues. The discontinuity- // carrying PES is a PUSI with a PTS in practice, so this is // defense-in-depth. - let carry_len = self.buf.len(); + let carry_len = self.acc.len(); let anchor = pes.pts.map(|p| PtsAnchor { at: carry_len, pts_ns: pts_to_ns(p), @@ -489,11 +496,15 @@ impl CodecParser for Ac3Parser { // Prepend leftover from previous PES, then take the whole buffer into a // local so the scanner can call `self.tally` (the bytes are no longer // borrowed from `self`). The unconsumed tail is written back at the end. - self.buf.extend_from_slice(&pes.data); - let buf = std::mem::take(&mut self.buf); + self.acc.push(pes); + // Copy the working bytes out so the scanner can borrow `self.tally`; + // the buffer keeps its marks, so the unconsumed tail stays attributed + // to the packet that carried it. + let buf = self.acc.as_slice().to_vec(); + let marks = self.acc.marks_snapshot(); let data = &buf; let (frames, keep_from, frame_pts_ns) = - self.scan_access_units(data, self.flush_pts_ns, anchor, false); + self.scan_access_units(data, self.flush_pts_ns, anchor, false, &marks); if keep_from < data.len() { let tail = &data[keep_from..]; @@ -506,7 +517,7 @@ impl CodecParser for Ac3Parser { "ac3: carry-over buffer exceeded {} bytes without a frame; dropping and resyncing", MAX_AC3_BUF ); - self.buf.clear(); + self.acc.clear(); // Advance the cadence, as both sibling branches below do, so the // three paths out of this block cannot disagree. Defensive: no // input reaching this parser was found that both parses frames and @@ -514,7 +525,7 @@ impl CodecParser for Ac3Parser { // prevents is not currently reachable and has no regression test. self.flush_pts_ns = frame_pts_ns; } else { - self.buf = tail.to_vec(); + self.acc.drain(keep_from); // The carried bytes, when later completed and emitted (next call // or by flush() at EOS), are timed at the PTS the scanner reached // here: the PTS of the next access unit in presentation order, or @@ -523,7 +534,7 @@ impl CodecParser for Ac3Parser { self.flush_pts_ns = frame_pts_ns; } } else { - self.buf.clear(); + self.acc.clear(); // Nothing carried, but keep the cadence so a following PES with no // PTS (no anchor) continues the timeline instead of reusing a stale // value. @@ -539,9 +550,11 @@ impl CodecParser for Ac3Parser { // with no following PES to close it, and without this drain the last // ~32 ms of audio is lost. `at_eos` closes the trailing access unit // instead of holding it; a partial/garbage tail yields nothing. - let buf = std::mem::take(&mut self.buf); + let buf = self.acc.as_slice().to_vec(); + let marks = self.acc.marks_snapshot(); + self.acc.clear(); let out = self - .scan_access_units(&buf, self.flush_pts_ns, None, true) + .scan_access_units(&buf, self.flush_pts_ns, None, true, &marks) .0; // Aggregate drop report at end-of-stream (warn-level, always visible). self.tally.log_summary(); @@ -1030,15 +1043,15 @@ mod tests { let frames = parser.parse(&pes); assert!(frames.is_empty()); assert!( - parser.buf.len() <= MAX_AC3_BUF, + parser.acc.len() <= MAX_AC3_BUF, "buffer grew to {} (cap {})", - parser.buf.len(), + parser.acc.len(), MAX_AC3_BUF ); } // After all that garbage the retained tail is at most a single partial // syncword byte — never an accumulation of whole PES packets. - assert!(parser.buf.len() <= 1, "retained {} bytes", parser.buf.len()); + assert!(parser.acc.len() <= 1, "retained {} bytes", parser.acc.len()); } #[test] @@ -1057,7 +1070,11 @@ mod tests { discontinuity: false, }; assert!(parser.parse(&pes).is_empty()); - assert_eq!(parser.buf, vec![0x0B], "lone trailing 0x0B retained"); + assert_eq!( + parser.acc.as_slice(), + vec![0x0B], + "lone trailing 0x0B retained" + ); } #[test] @@ -1067,14 +1084,14 @@ mod tests { // ac3 inherited the no-op default flush and dropped the last frame. let mut parser = Ac3Parser::new(); let frame_data = make_ac3_frame(0, 2); - parser.buf = frame_data.clone(); + parser.acc.seed(&frame_data.clone()); parser.flush_pts_ns = pts_to_ns(99000); let f = parser.flush(); assert_eq!(f.len(), 1, "complete buffered frame drained at EOS"); assert_eq!(f[0].data.len(), 160); assert_eq!(f[0].pts_ns, pts_to_ns(99000), "flush uses carried PTS"); assert!(f[0].duration_ns.is_some(), "flush sets duration"); - assert!(parser.buf.is_empty(), "buffer consumed by flush"); + assert!(parser.acc.is_empty(), "buffer consumed by flush"); } #[test] @@ -1107,7 +1124,7 @@ mod tests { // emitted truncated. let mut parser = Ac3Parser::new(); let frame_data = make_ac3_frame(0, 2); - parser.buf = frame_data[..80].to_vec(); // half a frame + parser.acc.seed(&frame_data[..80]); // half a frame assert!(parser.flush().is_empty(), "partial tail dropped"); } @@ -1511,7 +1528,7 @@ mod tests { // 100 bytes. let mut parser = Ac3Parser::new(); let frame = make_ac3_frame(0, 2); // sizes to 160 - parser.buf = frame[..100].to_vec(); + parser.acc.seed(&frame[..100]); assert!( parser.flush().is_empty(), "incomplete frame must not be emitted truncated at flush" @@ -1522,7 +1539,7 @@ mod tests { fn flush_with_no_sync_is_empty() { // flush on a buffer with no syncword yields nothing and clears. let mut parser = Ac3Parser::new(); - parser.buf = vec![0xAA, 0xBB, 0xCC]; + parser.acc.seed(&[0xAA, 0xBB, 0xCC]); assert!(parser.flush().is_empty()); } @@ -1839,7 +1856,7 @@ mod tests { assert_eq!(fr.pts_ns, pts_to_ns(90000) + i as i64 * 32_000_000); assert_eq!(fr.duration_ns, Some(32_000_000)); } - assert!(parser.buf.is_empty(), "nothing held back for plain AC-3"); + assert!(parser.acc.is_empty(), "nothing held back for plain AC-3"); assert!(parser.flush().is_empty(), "flush has nothing left to drain"); } @@ -2110,4 +2127,42 @@ mod tests { discontinuity: false, } } + + /// An access unit that began in an earlier packet keeps THAT packet's + /// source offset. The packet that completes it is a different clip at a + /// seam, and taking its offset places the audio in the wrong one. + #[test] + fn an_access_unit_carries_the_source_of_the_packet_it_began_in() { + let mut parser = Ac3Parser::new(); + let frame = make_ac3_frame(0, 4); + + let mut p1 = PesPacket { + pid: 0x1100, + pts: Some(90_000), + dts: None, + data: frame[..frame.len() / 2].to_vec(), + source: Some(crate::pes::SourcePos::at_byte(1_000)), + discontinuity: false, + }; + p1.data.truncate(frame.len() / 2); + assert!(parser.parse(&p1).is_empty(), "partial frame held"); + + let mut rest = frame[frame.len() / 2..].to_vec(); + rest.extend_from_slice(&make_ac3_frame(0, 4)); + let p2 = PesPacket { + pid: 0x1100, + pts: Some(180_000), + dts: None, + data: rest, + source: Some(crate::pes::SourcePos::at_byte(9_000)), + discontinuity: false, + }; + let frames = parser.parse(&p2); + assert!(!frames.is_empty(), "the completed unit is emitted"); + assert_eq!( + frames[0].source.map(|s| s.byte), + Some(1_000), + "the unit belongs to the packet its FIRST byte came from" + ); + } } diff --git a/src/mux/codec/adts.rs b/src/mux/codec/adts.rs index 30ed307..6d6dd3a 100644 --- a/src/mux/codec/adts.rs +++ b/src/mux/codec/adts.rs @@ -276,4 +276,15 @@ mod tests { let f = p.parse(&make_pes(vec![0xFF, 0xF1, 0x50], Some(0))); assert_eq!(f.len(), 1, "too short to validate → kept"); } + + /// One PES is one unit here, so the frame carries that packet's offset. + #[test] + fn a_frame_carries_its_packets_source() { + let mut parser = AdtsParser::new(); + let mut p = make_pes(adts_frame(64), Some(90_000)); + p.source = Some(crate::pes::SourcePos::at_byte(4_242)); + let frames = parser.parse(&p); + assert!(!frames.is_empty(), "a valid ADTS frame is emitted"); + assert_eq!(frames[0].source.map(|s| s.byte), Some(4_242)); + } } diff --git a/src/mux/codec/dts.rs b/src/mux/codec/dts.rs index f6369b1..8df4731 100644 --- a/src/mux/codec/dts.rs +++ b/src/mux/codec/dts.rs @@ -6,7 +6,7 @@ //! are emitted complete. use super::startcode::BitReader; -use super::{CodecParser, Frame, PesPacket, pts_to_ns}; +use super::{CodecParser, Frame, PesPacket}; const DTS_CORE_SYNC: [u8; 4] = [0x7F, 0xFE, 0x80, 0x01]; /// DTS-HD extension substream syncword. An access unit is delimited by the next @@ -21,23 +21,16 @@ const DTS_HD_EXT_SYNC: [u8; 4] = [0x64, 0x58, 0x20, 0x25]; /// This preserves the lossless extension data instead of downgrading to lossy /// core (the lossy-core downgrade bug). pub struct DtsParser { - buf: Vec, + /// Bytes assembled across PES packets, each attributable to the packet + /// that carried it. An emitted unit takes the facts of the packet covering + /// its FIRST byte, so an AU whose core arrived in an earlier PES keeps that + /// core's timestamp and source offset when its extensions arrive later. + acc: super::pesbuf::PesBuf, /// PTS of the access unit currently being assembled in `buf` (the unit /// starting at the first buffered core sync). Captured when that core /// frame's PES first arrived; the trailing extension-substream PES /// packets carry their own (later) PTS which must NOT override it. pending_pts: i64, - /// PTS markers attributing buffer regions to their source PES. Each entry - /// is `(buffer_offset, pts_ns)` for the PES whose bytes begin at that - /// offset. When an access unit is emitted from the front of `buf`, its PTS - /// is the marker covering offset 0 — NOT the most recent PES's PTS. This is - /// what fixes multi-AU-per-call PTS attribution: if a PES carries the - /// extension substreams (and possibly the next core) for an AU whose own - /// core arrived in an earlier PES, the emitted AU keeps its own core PES's - /// timestamp instead of the later PES's. Offsets are kept relative to the - /// current `buf` start and rebased whenever bytes are drained from the - /// front. - pts_marks: std::collections::VecDeque<(usize, i64)>, /// The `front_pts` of the PREVIOUS emitted access unit. When the current /// AU's `front_pts` differs, it began a new PES → re-base to it. When it is /// unchanged, this AU shares the previous AU's PES → advance one frame @@ -66,9 +59,8 @@ impl Default for DtsParser { impl DtsParser { pub fn new() -> Self { Self { - buf: Vec::with_capacity(32768), + acc: super::pesbuf::PesBuf::with_capacity(32768), pending_pts: 0, - pts_marks: std::collections::VecDeque::new(), last_front_pts: PTS_UNSET, next_pts_ns: PTS_UNSET, tally: super::dropgate::DropTally::new("dts"), @@ -92,7 +84,14 @@ impl DtsParser { /// PTS clock (which the caller advances whether or not the AU survives), so /// a drop leaves the following audio on its true timeline — a gap, not a /// shift. Every drop is logged (fail-loud, never silent). - fn emit_or_drop(&mut self, au: Vec, au_pts: i64, dur_ns: i64, out: &mut Vec) { + fn emit_or_drop( + &mut self, + au: Vec, + au_pts: i64, + dur_ns: i64, + src: Option, + out: &mut Vec, + ) { let verdict = if self.tally.is_poisoned() { Err(DropReason::TrackPoisoned) } else { @@ -104,7 +103,9 @@ impl DtsParser { out.push(Frame { discontinuity: false, coding: None, - source: None, + // From the SAME packet as `au_pts` — both are the facts of + // the PES covering this unit's first byte. + source: src, pts_ns: au_pts, keyframe: true, data: au, @@ -148,41 +149,25 @@ impl DtsParser { base } - /// Drop `n` bytes from the front of `buf` and rebase the PTS markers so - /// their offsets stay relative to the new buffer start. A marker that now - /// sits at or before offset 0 is clamped to 0 (it still covers the front). - /// Redundant markers all at offset 0 collapse to the last one. + /// Drop `n` bytes from the front, rebasing attribution onto the new front. fn drain_front(&mut self, n: usize) { - if n == 0 { - return; - } - self.buf.drain(..n); - for m in &mut self.pts_marks { - m.0 = m.0.saturating_sub(n); - } - // Collapse all leading markers that now sit at offset 0 to the last - // such marker — that is the PES whose data currently begins the buffer. - let last_zero = self - .pts_marks - .iter() - .rposition(|&(off, _)| off == 0) - .filter(|&i| i > 0); - if let Some(i) = last_zero { - self.pts_marks.drain(..i); - } + self.acc.drain(n); } - /// PTS that should be stamped on an access unit currently at the front of - /// `buf` (offset 0): the most recent marker at offset 0, falling back to - /// `pending_pts`. + /// PTS for the access unit at the front of the buffer: the facts of the + /// packet covering offset 0, falling back to the unit's captured base. fn front_pts(&self) -> i64 { - self.pts_marks - .iter() - .rev() - .find(|&&(off, _)| off == 0) - .map(|&(_, pts)| pts) + self.acc + .front() + .presentation_ns() .unwrap_or(self.pending_pts) } + + /// Source offset for that same unit — from the SAME packet as its PTS, + /// which is the property the shared buffer exists to guarantee. + fn front_source(&self) -> Option { + self.acc.front().source + } } /// Hard cap on a buffered access unit (core + all its extension substreams). @@ -227,8 +212,7 @@ impl CodecParser for DtsParser { // never be stranded by an empty post-gap PES (defensive; the demuxer only // emits non-empty PES today). if pes.discontinuity { - self.buf.clear(); - self.pts_marks.clear(); + self.acc.clear(); self.pending_pts = PTS_UNSET; // A concealed gap is a timeline discontinuity: let the post-gap AU // re-base to its own PES PTS rather than the pre-gap cursor. @@ -242,17 +226,15 @@ impl CodecParser for DtsParser { // guard at a post-gap continuation) must NOT reset the timeline to 0; // continue from the most recent known base. Defense-in-depth: the // discontinuity-carrying PES is a PUSI with a PTS in practice. - let pts_ns = pes.pts.map(pts_to_ns).unwrap_or_else(|| { - self.pts_marks - .back() - .map(|&(_, p)| p) - .filter(|&p| p >= 0) - .unwrap_or(if self.pending_pts >= 0 { - self.pending_pts - } else { - 0 - }) - }); + // A PES with no PTS (rare for audio, but legal) must NOT reset the + // timeline to 0 — continue from the most recent known base. + let pts_ns = super::pesbuf::PesFacts::of(pes) + .presentation_ns() + .unwrap_or(if self.pending_pts >= 0 { + self.pending_pts + } else { + 0 + }); // On Blu-ray, a DTS-HD MA/HRA access unit is a DTS core frame // (sync 0x7FFE8001) followed by one or more DTS extension substreams @@ -272,7 +254,7 @@ impl CodecParser for DtsParser { // prior forced (safety-valve) flush left it invalidated — in the // forced case the bytes still in `buf` are not a real core frame, so // the first PES to arrive after the flush carries the correct base. - if self.buf.is_empty() || self.pending_pts == PTS_UNSET { + if self.acc.is_empty() || self.pending_pts == PTS_UNSET { self.pending_pts = pts_ns; } // Mark where THIS PES's bytes begin in the buffer, with its PTS. The @@ -280,21 +262,22 @@ impl CodecParser for DtsParser { // (see `front_pts`), so an AU whose core arrived in an earlier PES keeps // that core's timestamp even when its extensions / the following core // arrive (with a later PTS) in this same parse() call. - // (pts_marks is bounded implicitly: an empty PES returns above without - // pushing a mark, and a non-empty run grows `buf`, which is cleared — - // along with pts_marks — once it exceeds MAX_AU_BYTES.) - self.pts_marks.push_back((self.buf.len(), pts_ns)); - self.buf.extend_from_slice(&pes.data); + // `pts_ns` is this packet's own timestamp, or the carried-forward base + // when it had none; the source offset is always this packet's. + self.acc.push_with( + &pes.data, + super::pesbuf::PesFacts::of(pes).with_pts_ns(pts_ns), + ); let mut frames = Vec::new(); loop { // Resync to the first core sync; drop any leading junk. - let Some(start) = find_sync(&self.buf, &DTS_CORE_SYNC) else { + let Some(start) = find_sync(self.acc.as_slice(), &DTS_CORE_SYNC) else { // No core sync at all yet — keep at most a 3-byte tail so a // sync split across PES packets can still be found next time. - if self.buf.len() > 3 { - let tail = self.buf.len() - 3; + if self.acc.len() > 3 { + let tail = self.acc.len() - 3; self.drain_front(tail); } break; @@ -305,17 +288,17 @@ impl CodecParser for DtsParser { // offset 0 by construction, so a re-scan would be a redundant // O(buf_len) walk per iteration; assert the invariant instead. debug_assert_eq!( - find_sync(&self.buf, &DTS_CORE_SYNC), + find_sync(self.acc.as_slice(), &DTS_CORE_SYNC), Some(0), "drain_front(start) must leave the core sync at offset 0" ); } // Need the core header to size the core frame. - if self.buf.len() < CORE_HEADER_MIN_BYTES { + if self.acc.len() < CORE_HEADER_MIN_BYTES { break; } - let core_size = dts_core_frame_size(&self.buf); + let core_size = dts_core_frame_size(self.acc.as_slice()); // `dts_core_frame_size` returns a 14-bit `fsize + 1`, so it is // always in [1, 16384]; the bare `== 0` / `> MAX_AU_BYTES` checks // can never fire. A real DTS core frame is at least @@ -330,7 +313,7 @@ impl CodecParser for DtsParser { self.drain_front(4); continue; } - if self.buf.len() < core_size { + if self.acc.len() < core_size { break; // core frame not fully buffered yet — wait } @@ -349,9 +332,9 @@ impl CodecParser for DtsParser { // flush is an extension-substream PES, carrying its own later // timestamp) must NOT become the next unit's PTS base. let mut forced = false; - let (au_end, ext_clean) = match next_core_boundary(&self.buf, core_size) { + let (au_end, ext_clean) = match next_core_boundary(self.acc.as_slice(), core_size) { NextCore::Found { end, ext_clean } => (end, ext_clean), - NextCore::NeedMore if self.buf.len() <= MAX_AU_BYTES => break, + NextCore::NeedMore if self.acc.len() <= MAX_AU_BYTES => break, NextCore::NeedMore => { // A candidate boundary exists but is not fully buffered. Normally // we wait for more PES; but once the buffer exceeds the AU cap, @@ -359,7 +342,7 @@ impl CodecParser for DtsParser { // stream that keeps a boundary perpetually incomplete can't grow // `buf` without bound (the `break` above never reaches it). forced = true; - (self.buf.len(), true) + (self.acc.len(), true) } NextCore::None => { // No next core sync buffered yet. The trailing extension @@ -367,11 +350,11 @@ impl CodecParser for DtsParser { // them rather than emit a core-only (lossy) frame — unless // the buffer has grown unreasonably large, in which case // emit what we have to guarantee forward progress. - if self.buf.len() <= MAX_AU_BYTES { + if self.acc.len() <= MAX_AU_BYTES { break; } forced = true; - (self.buf.len(), true) + (self.acc.len(), true) } }; @@ -384,7 +367,7 @@ impl CodecParser for DtsParser { // rather than shipping it. A recognized-but-unsizeable extension // (`ext_clean == true`) is preserved in full (lossless). let emit_end = if ext_clean { au_end } else { core_size }; - let au: Vec = self.buf[..emit_end].to_vec(); + let au: Vec = self.acc.as_slice()[..emit_end].to_vec(); // The AU's own core PES PTS (the PES covering its first byte, even if // that PES preceded the one(s) carrying its extensions or the next // core), stamped monotonically: honored when it advances past the @@ -396,7 +379,10 @@ impl CodecParser for DtsParser { // would: the following AU keeps its true PTS and the drop is a gap, // never a shift. `emit_or_drop` decides whether to actually push it. let au_pts = self.stamp_pts(self.front_pts(), dur_ns); - self.emit_or_drop(au, au_pts, dur_ns, &mut frames); + // Read BEFORE draining: after the drain the front is the NEXT + // unit's packet, not this one's. + let au_src = self.front_source(); + self.emit_or_drop(au, au_pts, dur_ns, au_src, &mut frames); self.drain_front(au_end); // After draining, the marker covering the new front (if any) carries // the next AU's PTS; `pending_pts` is only the fallback when no @@ -408,15 +394,14 @@ impl CodecParser for DtsParser { // regardless of buffer state, rather than inheriting this // (non-core) PES's timestamp. self.pending_pts = PTS_UNSET; - self.pts_marks.clear(); } } - // Discard markers that no longer reference live buffer bytes (everything - // past the buffer end can't happen, but collapse duplicates at offset 0 - // and drop a stale empty-buffer marker set). - if self.buf.is_empty() { - self.pts_marks.clear(); + // An empty buffer holds no bytes for a mark to attribute, so drop the + // marks with them. `drain` deliberately keeps the mark covering the new + // front — correct while bytes remain, stale once none do. + if self.acc.is_empty() { + self.acc.clear(); } frames @@ -440,26 +425,28 @@ impl DtsParser { /// streaming), gated through the decodability check. Require a complete core /// frame; drop a bare partial sync tail. fn flush_tail(&mut self) -> Vec { - if find_sync(&self.buf, &DTS_CORE_SYNC) != Some(0) || self.buf.len() < CORE_HEADER_MIN_BYTES + if find_sync(self.acc.as_slice(), &DTS_CORE_SYNC) != Some(0) + || self.acc.len() < CORE_HEADER_MIN_BYTES { - self.buf.clear(); + self.acc.clear(); return Vec::new(); } - let core_size = dts_core_frame_size(&self.buf); + let core_size = dts_core_frame_size(self.acc.as_slice()); // `dts_core_frame_size` returns a 14-bit `fsize + 1` (never 0), so the // old `== 0` check was dead; reject a sub-minimum core like `parse()`. - if core_size < MIN_CORE_FRAME_BYTES || self.buf.len() < core_size { - self.buf.clear(); + if core_size < MIN_CORE_FRAME_BYTES || self.acc.len() < core_size { + self.acc.clear(); return Vec::new(); } // The final AU's PTS is the PES covering the buffer front (its core's // PES). Fall back to pending_pts, clamping the sentinel to 0. - let au = std::mem::take(&mut self.buf); + let au = self.acc.as_slice().to_vec(); let dur_ns = dts_core_duration_ns(&au) as i64; let pts_ns = self.stamp_pts(self.front_pts(), dur_ns); - self.pts_marks.clear(); + let src = self.front_source(); + self.acc.clear(); let mut out = Vec::new(); - self.emit_or_drop(au, pts_ns, dur_ns, &mut out); + self.emit_or_drop(au, pts_ns, dur_ns, src, &mut out); out } } @@ -824,6 +811,7 @@ fn core_header_drop_reason(au: &[u8]) -> Option { #[cfg(test)] mod tests { use super::*; + use crate::mux::codec::pts_to_ns; use crate::mux::ts::PesPacket; fn make_pes(data: Vec, pts: Option) -> PesPacket { @@ -899,15 +887,19 @@ mod tests { fn drain_front_collapses_offset_zero_markers_instead_of_leaking() { let mut parser = DtsParser::new(); for i in 0..200i64 { - parser.buf.extend_from_slice(&[0u8; 5]); - parser.pts_marks.push_back((5, i)); - parser.pts_marks.push_back((5, i)); + parser.acc.append_unattributed(&[0u8; 5]); + parser + .acc + .mark_here(crate::mux::codec::pesbuf::PesFacts::default().with_pts_ns(i)); + parser + .acc + .mark_here(crate::mux::codec::pesbuf::PesFacts::default().with_pts_ns(i)); parser.drain_front(5); } assert!( - parser.pts_marks.len() <= 2, + parser.acc.mark_count() <= 2, "pts_marks must stay bounded across repeated drains, got {}", - parser.pts_marks.len() + parser.acc.mark_count() ); } @@ -1579,7 +1571,7 @@ mod tests { "NeedMore past the AU cap must force-emit, not stall and balloon the buffer" ); assert!( - parser.buf.is_empty(), + parser.acc.is_empty(), "the forced flush drains the buffer instead of growing it unbounded" ); } @@ -1751,7 +1743,7 @@ mod tests { // The first core's bytes are still buffered awaiting the verdict — not // dropped, not emitted. assert!( - parser.buf.len() >= 512, + parser.acc.len() >= 512, "core1 retained while candidate boundary is undecided" ); } @@ -1809,8 +1801,8 @@ mod tests { let mut parser = DtsParser::new(); let f = parser.parse(&make_pes(vec![0x11, 0x22, 0x33, 0x44], Some(90000))); assert!(f.is_empty()); - assert_eq!(parser.buf.len(), 3, "only a 3-byte resync tail retained"); - assert_eq!(parser.buf, vec![0x22, 0x33, 0x44]); + assert_eq!(parser.acc.len(), 3, "only a 3-byte resync tail retained"); + assert_eq!(parser.acc.as_slice(), &[0x22, 0x33, 0x44]); } #[test] @@ -1825,7 +1817,7 @@ mod tests { .parse(&make_pes(core[..3].to_vec(), Some(90000))) .is_empty() ); - assert_eq!(parser.buf.len(), 3, "3-byte sync prefix retained"); + assert_eq!(parser.acc.len(), 3, "3-byte sync prefix retained"); // PES 2: the 4th sync byte + the rest of core1, then a 2nd core to close. let mut rest = core[3..].to_vec(); rest.extend_from_slice(&make_dts_core(640)); @@ -1847,7 +1839,7 @@ mod tests { let mut data = DTS_CORE_SYNC.to_vec(); data.extend_from_slice(&[0x00, 0x00, 0x00]); // only 7 bytes total < 10 assert!(parser.parse(&make_pes(data, Some(90000))).is_empty()); - assert!(!parser.buf.is_empty(), "partial core header retained"); + assert!(!parser.acc.is_empty(), "partial core header retained"); } #[test] @@ -1859,7 +1851,7 @@ mod tests { let mut d = vec![0u8; 17]; d[0..4].copy_from_slice(&DTS_CORE_SYNC); d[6] = 0x01; // fsize → 16 → size 17 - parser.buf = d; + parser.acc.seed(&d); assert!(parser.flush().is_empty(), "sub-spec core rejected at flush"); } @@ -1869,7 +1861,7 @@ mod tests { // declared size must be dropped (never emit fewer bytes than declared). let mut parser = DtsParser::new(); let core = make_dts_core(512); - parser.buf = core[..300].to_vec(); // header says 512, only 300 present + parser.acc.seed(&core[..300]); // header says 512, only 300 present assert!( parser.flush().is_empty(), "incomplete core not emitted truncated" @@ -1886,9 +1878,9 @@ mod tests { fn flush_partial_sync_tail_dropped() { // A bare partial-sync tail (not at offset 0 / not a full core) is dropped. let mut parser = DtsParser::new(); - parser.buf = vec![0x7F, 0xFE, 0x80]; // 3 of 4 sync bytes + parser.acc.seed(&[0x7F, 0xFE, 0x80]); // 3 of 4 sync bytes assert!(parser.flush().is_empty()); - assert!(parser.buf.is_empty(), "buffer cleared on flush"); + assert!(parser.acc.is_empty(), "buffer cleared on flush"); } #[test] @@ -2311,13 +2303,13 @@ mod tests { let out = parser.parse(&make_pes(d, Some(90_000))); assert!(out.is_empty(), "a false sync emits nothing"); assert_eq!( - parser.buf.len(), + parser.acc.len(), 3, "the false sync was decoded, drained and resynced past — leaving only \ the 3-byte split-sync carry-over" ); assert_ne!( - find_sync(&parser.buf, &DTS_CORE_SYNC), + find_sync(parser.acc.as_slice(), &DTS_CORE_SYNC), Some(0), "and the bogus sync is no longer at the front of the buffer" ); @@ -2331,14 +2323,14 @@ mod tests { fn flush_emits_a_core_that_exactly_fills_the_buffer() { let mut parser = DtsParser::new(); let core = make_dts_core(512); - parser.buf = core.clone(); + parser.acc.seed(&core.clone()); parser.pending_pts = 90_000; let out = parser.flush(); assert_eq!(out.len(), 1, "the final AU is emitted, not dropped"); assert_eq!(out[0].data, core, "and it is the whole core frame"); // One byte short is still refused — the bound is not simply absent. let mut parser = DtsParser::new(); - parser.buf = core[..511].to_vec(); + parser.acc.seed(&core[..511]); parser.pending_pts = 90_000; assert!( parser.flush().is_empty(), @@ -2368,18 +2360,18 @@ mod tests { Some(0), "the fixture really has no core sync at the front" ); - parser.buf = broken; + parser.acc.seed(&broken); parser.pending_pts = 90_000; assert!( parser.flush().is_empty(), "a buffer whose front is not a core sync is discarded, not size-decoded" ); - assert!(parser.buf.is_empty(), "and the junk is dropped"); + assert!(parser.acc.is_empty(), "and the junk is dropped"); // The other half of the disjunction: a buffer too short to size, whose // front IS a core sync, is discarded too. let mut parser = DtsParser::new(); - parser.buf = DTS_CORE_SYNC.to_vec(); + parser.acc.seed(DTS_CORE_SYNC.as_ref()); parser.pending_pts = 90_000; assert!(parser.flush().is_empty(), "a bare sync tail is not an AU"); } @@ -2416,4 +2408,54 @@ mod tests { "the bare sync sizes nothing" ); } + + /// The whole point of the shared buffer: an access unit whose core arrived + /// in an EARLIER packet keeps that packet's source offset, not the offset + /// of whichever packet completed it. At a clip boundary the two belong to + /// different clips, and taking the later one puts the unit in the wrong + /// clip -- which is what left nine audio and subtitle tracks unplaceable. + #[test] + fn an_access_unit_carries_the_source_of_the_packet_its_core_arrived_in() { + let mut parser = DtsParser::new(); + let core = make_dts_core(512); + + // The core starts here, at byte 1000 of the feed. + let mut p1 = make_pes(core[..256].to_vec(), Some(90_000)); + p1.source = Some(crate::pes::SourcePos::at_byte(1_000)); + assert!(parser.parse(&p1).is_empty(), "partial core held"); + + // The rest arrives later, at byte 9_000, together with the next core + // that closes the unit. + let mut rest = core[256..].to_vec(); + rest.extend_from_slice(&make_dts_core(512)); + let mut p2 = make_pes(rest, Some(180_000)); + p2.source = Some(crate::pes::SourcePos::at_byte(9_000)); + let frames = parser.parse(&p2); + + assert!(!frames.is_empty(), "the completed unit is emitted"); + assert_eq!( + frames[0].source.map(|s| s.byte), + Some(1_000), + "the unit belongs to the packet its FIRST byte came from" + ); + assert_eq!( + frames[0].pts_ns, + pts_to_ns(90_000), + "and its timestamp comes from that same packet" + ); + } + + /// A unit that begins and ends in one packet takes that packet's offset — + /// the ordinary case, which must not regress while fixing the spanning one. + #[test] + fn a_self_contained_access_unit_carries_its_own_packets_source() { + let mut parser = DtsParser::new(); + let mut data = make_dts_core(512); + data.extend_from_slice(&make_dts_core(512)); + let mut p = make_pes(data, Some(90_000)); + p.source = Some(crate::pes::SourcePos::at_byte(4_242)); + let frames = parser.parse(&p); + assert!(!frames.is_empty()); + assert_eq!(frames[0].source.map(|s| s.byte), Some(4_242)); + } } diff --git a/src/mux/codec/pesbuf.rs b/src/mux/codec/pesbuf.rs index 32e153a..2d2035b 100644 --- a/src/mux/codec/pesbuf.rs +++ b/src/mux/codec/pesbuf.rs @@ -34,10 +34,10 @@ use crate::pes::SourcePos; /// changed those semantics silently while fixing provenance. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub(crate) struct PesFacts { - /// Presentation timestamp in 90kHz ticks, as carried. - pub pts: Option, - /// Decode timestamp in 90kHz ticks, as carried. - pub dts: Option, + /// Presentation time in NANOSECONDS, already derived (see `of`). Stored + /// derived rather than raw so there is one derivation and no caller can + /// pick a different one. + pub pts_ns: Option, /// Byte offset of this PES's first ES byte within the title's feed — what /// identifies the clip a frame came from. `None` when the demuxer was fed /// without a base offset. @@ -56,13 +56,23 @@ impl PesFacts { /// for a parser whose unit begins in the packet it is handed. pub(crate) fn of(pes: &PesPacket) -> Self { Self { - pts: pes.pts, - dts: pes.dts, + pts_ns: pes.pts.or(pes.dts).map(pts_to_ns), source: pes.source, discontinuity: pes.discontinuity, } } + /// The same facts with the presentation time replaced by one the parser + /// resolved itself — for a packet that carried no timestamp and whose unit + /// continues a base established earlier. The attribution is unchanged: + /// still this packet's bytes, still its source offset. + pub(crate) fn with_pts_ns(self, pts_ns: i64) -> Self { + Self { + pts_ns: Some(pts_ns), + ..self + } + } + /// This unit's presentation time in nanoseconds — the ONE derivation. /// /// PTS and DTS are not two spellings of one value: PTS is when to display, @@ -77,10 +87,25 @@ impl PesFacts { /// for a missing field, not a second rule: dvdsub read `pts` alone and /// returned 0 for a packet that carried only DTS. pub(crate) fn presentation_ns(&self) -> Option { - self.pts.or(self.dts).map(pts_to_ns) + self.pts_ns } } +/// The facts of the packet covering `off` within a [`PesBuf::marks_snapshot`]. +/// +/// The same at-or-before rule as [`PesBuf::facts_at`], for a scanner holding a +/// snapshot rather than the buffer. +pub(crate) fn facts_for(marks: &[(usize, PesFacts)], off: usize) -> PesFacts { + let mut found = PesFacts::default(); + for &(at, facts) in marks { + if at > off { + break; + } + found = facts; + } + found +} + /// Bytes accumulated across PES packets, each byte attributable to the packet /// that carried it. pub(crate) struct PesBuf { @@ -112,10 +137,14 @@ impl PesBuf { self.buf.extend_from_slice(&pes.data); } - /// Append raw bytes attributed to the SAME PES as the bytes already at the - /// end of the buffer. For a parser that rewrites or re-frames payload - /// in-place rather than appending a packet verbatim. - pub(crate) fn push_bytes(&mut self, data: &[u8]) { + /// Append a payload under facts the caller resolved — for a parser that + /// carries a timestamp forward across a packet that omitted one. Same + /// attribution rule; only the timestamp differs from what the packet said. + pub(crate) fn push_with(&mut self, data: &[u8], facts: PesFacts) { + if data.is_empty() { + return; + } + self.marks.push_back((self.buf.len(), facts)); self.buf.extend_from_slice(data); } @@ -177,6 +206,44 @@ impl PesBuf { } } + /// Seed the buffer directly with bytes carrying no packet attribution — + /// for tests that drive a parser's scanner without a demuxer in front of + /// it. Facts for these bytes default to absent, which is what an + /// unattributed byte honestly is. + #[cfg(test)] + pub(crate) fn seed(&mut self, data: &[u8]) { + self.buf.clear(); + self.marks.clear(); + self.buf.extend_from_slice(data); + } + + /// Append unattributed bytes, keeping existing content and marks. + #[cfg(test)] + pub(crate) fn append_unattributed(&mut self, data: &[u8]) { + self.buf.extend_from_slice(data); + } + + /// How many packet marks are held — a test hook for the bound on marks. + #[cfg(test)] + pub(crate) fn mark_count(&self) -> usize { + self.marks.len() + } + + /// Record a mark at the current end without appending bytes — a test hook + /// for exercising mark bookkeeping directly. + #[cfg(test)] + pub(crate) fn mark_here(&mut self, facts: PesFacts) { + self.marks.push_back((self.buf.len(), facts)); + } + + /// The marks, for a scanner that must resolve facts at several offsets + /// while the buffer's bytes are borrowed elsewhere. Use with + /// [`facts_for`], which applies the same at-or-before rule as + /// [`PesBuf::facts_at`]. + pub(crate) fn marks_snapshot(&self) -> Vec<(usize, PesFacts)> { + self.marks.iter().copied().collect() + } + pub(crate) fn clear(&mut self) { self.buf.clear(); self.marks.clear(); diff --git a/src/mux/codec/truehd.rs b/src/mux/codec/truehd.rs index 6fc1a39..7856fc9 100644 --- a/src/mux/codec/truehd.rs +++ b/src/mux/codec/truehd.rs @@ -63,7 +63,10 @@ const AU_DURATION_NS_441: i64 = 907_029; const MAX_TRUEHD_BUF: usize = 256 * 1024; pub struct TrueHdParser { - buf: Vec, + /// Bytes assembled across PES packets, each attributable to the packet + /// that carried it, so an access unit takes the timestamp AND the source + /// offset of the packet covering its first byte. + acc: super::pesbuf::PesBuf, next_pts_ns: i64, /// Per-AU PTS increment. Defaults to the 48 kHz-family value (833_333) and /// is refined to the 44.1 kHz-family value once the first major sync reveals @@ -94,7 +97,7 @@ impl Default for TrueHdParser { impl TrueHdParser { pub fn new() -> Self { Self { - buf: Vec::with_capacity(32768), + acc: super::pesbuf::PesBuf::with_capacity(32768), next_pts_ns: 0, au_duration_ns: AU_DURATION_NS, tally: DropTally::new("truehd"), @@ -195,15 +198,15 @@ impl TrueHdParser { /// single source of truth shared with the AC-3 parser; a returned `0` there /// (reserved fscod or out-of-range frmsizecod) is the unmappable case. fn ac3_frame_at_head(&self) -> Ac3Size { - if self.buf.len() < 6 { + if self.acc.len() < 6 { return Ac3Size::NeedMore; } - let frame_bytes = super::ac3::ac3_frame_size(&self.buf); + let frame_bytes = super::ac3::ac3_frame_size(self.acc.as_slice()); if frame_bytes == 0 { // Reserved fscod or out-of-range frmsizecod → unmappable header. return Ac3Size::Unmappable; } - if self.buf.len() < frame_bytes { + if self.acc.len() < frame_bytes { return Ac3Size::NeedMore; } Ac3Size::Frame(frame_bytes) @@ -363,7 +366,7 @@ impl CodecParser for TrueHdParser { // never be stranded by an empty post-gap PES (defensive; the demuxer only // emits non-empty PES today). if pes.discontinuity { - self.buf.clear(); + self.acc.clear(); } if pes.data.is_empty() { return Vec::new(); @@ -376,7 +379,7 @@ impl CodecParser for TrueHdParser { // mid-AU would snap that AU's PTS backward/forward and break the // monotonic +AU_DURATION_NS cadence (A/V drift). Once the buffer is empty // the next PES legitimately begins a new AU and seeds the base. - if self.buf.is_empty() + if self.acc.is_empty() && let Some(pts) = pes.pts { // Resync to the authoritative PES PTS. TrueHD AUs are a fixed @@ -421,12 +424,12 @@ impl CodecParser for TrueHdParser { } } - self.buf.extend_from_slice(&pes.data); + self.acc.push(pes); let mut frames = Vec::new(); loop { - if self.buf.len() < 4 { + if self.acc.len() < 4 { break; } @@ -438,19 +441,19 @@ impl CodecParser for TrueHdParser { // when its computed end is corroborated by what follows: end of // buffer (frame fills the rest), another AC-3 sync, or a plausible // TrueHD AU header. If none holds, this is treated as a TrueHD AU. - if self.buf[0] == 0x0B && self.buf[1] == 0x77 { + if self.acc.as_slice()[0] == 0x0B && self.acc.as_slice()[1] == 0x77 { match self.ac3_frame_at_head() { Ac3Size::Unmappable => { // Permanently unmappable header at the head would stall // the parser forever; resync by dropping 2 bytes so one // bad frame costs one frame, not the whole buffer. - self.buf.drain(..2); + self.acc.drain(2); continue; } Ac3Size::NeedMore => break, // wait for the rest of the frame Ac3Size::Frame(skip) => { - if ac3_boundary_corroborated(&self.buf, skip) { - self.buf.drain(..skip); + if ac3_boundary_corroborated(self.acc.as_slice(), skip) { + self.acc.drain(skip); continue; } // Not corroborated — fall through and interpret the @@ -460,19 +463,21 @@ impl CodecParser for TrueHdParser { } // TrueHD access unit: lower 12 bits of first 2 bytes = length in words - let unit_words = (((self.buf[0] as usize) << 8) | self.buf[1] as usize) & 0xFFF; + let unit_words = (((self.acc.as_slice()[0] as usize) << 8) + | self.acc.as_slice()[1] as usize) + & 0xFFF; if unit_words == 0 { // A zero-length AU is malformed/padding. The AU header is 4 bytes // (length + timing); drain the whole header, not just the length // word, otherwise the timing bytes get misread as the next // length word and produce a spurious parse on the next iteration. - self.buf.drain(..4); + self.acc.drain(4); continue; } // unit_words is masked to 12 bits, so unit_bytes <= 4095 * 2 = 8190; // no separate oversize-resync guard is reachable. let unit_bytes = unit_words * 2; - if self.buf.len() < unit_bytes { + if self.acc.len() < unit_bytes { break; // incomplete access unit, wait for more data } @@ -482,10 +487,10 @@ impl CodecParser for TrueHdParser { // gated on 0xBA alone in `au_check`. let is_major_sync = unit_bytes >= 8 && is_mlp_major_sync(u32::from_be_bytes([ - self.buf[4], - self.buf[5], - self.buf[6], - self.buf[7], + self.acc.as_slice()[4], + self.acc.as_slice()[5], + self.acc.as_slice()[6], + self.acc.as_slice()[7], ])); // Decodability gate. MLP/TrueHD decode state persists across access @@ -493,8 +498,11 @@ impl CodecParser for TrueHdParser { // major sync (the clean re-init point) rather than excised in place. // The PTS clock advances across every dropped AU so a drop is a // silence gap, never a shift. - let au = self.buf[..unit_bytes].to_vec(); + let au = self.acc.as_slice()[..unit_bytes].to_vec(); let pts = self.next_pts_ns; + // Read BEFORE the drain below: this unit's source is the packet + // covering the CURRENT front, not the next unit's. + let au_src = self.acc.front().source; let mut emit_keyframe: Option = None; // Some(is_keyframe) => emit let mut drop_reason: Option<(&'static str, bool)> = None; // (reason, verified) @@ -560,7 +568,7 @@ impl CodecParser for TrueHdParser { frames.push(Frame { discontinuity: false, coding: None, - source: None, + source: au_src, pts_ns: pts, keyframe, data: au, @@ -575,14 +583,14 @@ impl CodecParser for TrueHdParser { .record_collateral_drop(pts, self.au_duration_ns, au.len(), reason); } } - self.buf.drain(..unit_bytes); + self.acc.drain(unit_bytes); self.next_pts_ns += self.au_duration_ns; } // Bound memory on malformed input: a stream that never yields a // complete frame must not grow the buffer without limit. - if self.buf.len() > MAX_TRUEHD_BUF { - self.buf.clear(); + if self.acc.len() > MAX_TRUEHD_BUF { + self.acc.clear(); } frames @@ -1455,7 +1463,7 @@ mod tests { "TrueHD AU behind a bad header is recovered" ); assert_eq!(frames[0].data.len(), 200); - assert!(parser.buf.is_empty(), "buffer fully consumed, no stall"); + assert!(parser.acc.is_empty(), "buffer fully consumed, no stall"); } #[test] @@ -1707,7 +1715,7 @@ mod tests { f.is_empty(), "must not emit fewer bytes than the length field" ); - assert_eq!(parser.buf.len(), 100, "partial AU retained"); + assert_eq!(parser.acc.len(), 100, "partial AU retained"); } /// Largest AU the 12-bit length field can declare: 0xFFF words × 2. @@ -1720,7 +1728,7 @@ mod tests { // // The bound that actually holds is MAX_AU_BYTES, not MAX_TRUEHD_BUF: // `parse`'s loop only breaks with data retained when - // `self.buf.len() < unit_bytes`, and `unit_bytes` is + // `self.acc.len() < unit_bytes`, and `unit_bytes` is // `((buf[0] << 8 | buf[1]) & 0xFFF) * 2 <= 8190`. Every other exit // drains. So the post-loop `buf.len() > MAX_TRUEHD_BUF` cap (256 KiB) is // an unreachable backstop — an exhaustive sweep of all 65536 two-byte @@ -1740,17 +1748,17 @@ mod tests { frag[0] = 0xFF; frag[1] = 0xFF; let _ = parser.parse(&make_pes(frag, Some(0))); - worst = worst.max(parser.buf.len()); + worst = worst.max(parser.acc.len()); assert!( - parser.buf.len() < MAX_AU_BYTES, + parser.acc.len() < MAX_AU_BYTES, "reassembly buffer exceeded the AU-length ceiling: {} >= {}", - parser.buf.len(), + parser.acc.len(), MAX_AU_BYTES ); assert!( - parser.buf.len() <= MAX_TRUEHD_BUF, + parser.acc.len() <= MAX_TRUEHD_BUF, "reassembly buffer exceeded cap: {} > {}", - parser.buf.len(), + parser.acc.len(), MAX_TRUEHD_BUF ); } @@ -1813,7 +1821,7 @@ mod tests { fn ac3_frame_at_head_needs_more_when_buffer_short() { // < 6 bytes buffered → NeedMore (can't read the AC-3 header). let mut parser = TrueHdParser::new(); - parser.buf = vec![0x0B, 0x77, 0x00]; + parser.acc.seed(&[0x0B, 0x77, 0x00]); // Drive through parse: a short 0x0B77 head must wait, not emit. let f = parser.parse(&make_pes(vec![0x0B, 0x77, 0x00], Some(0))); assert!(f.is_empty()); @@ -2014,4 +2022,27 @@ mod tests { assert_eq!(truehd_sample_rate_hz(info.format_info), Some(96000)); assert_eq!(info.is_atmos, Some(true)); } + + /// Same rule for TrueHD: a unit spanning two packets belongs to the one + /// that carried its first byte. + #[test] + fn an_access_unit_carries_the_source_of_the_packet_it_began_in() { + let mut parser = TrueHdParser::new(); + let unit = make_truehd_unit(512); + + let mut p1 = make_pes(unit[..200].to_vec(), Some(90_000)); + p1.source = Some(crate::pes::SourcePos::at_byte(1_000)); + let first = parser.parse(&p1); + assert!(first.is_empty(), "partial unit held"); + + let mut p2 = make_pes(unit[200..].to_vec(), Some(180_000)); + p2.source = Some(crate::pes::SourcePos::at_byte(9_000)); + let frames = parser.parse(&p2); + assert!(!frames.is_empty(), "the completed unit is emitted"); + assert_eq!( + frames[0].source.map(|s| s.byte), + Some(1_000), + "the unit belongs to the packet its FIRST byte came from" + ); + } }