Every parser now carries provenance, on the one shared buffer
dts, ac3 and truehd assembled access units across PES packets with three private implementations of the same bookkeeping, and none of them carried the source byte offset. They now hold a PesBuf, so a unit takes the timestamp AND the source of the packet covering its first byte, from the same mark, and no codec can answer that question its own way again. dts is the reference case: its pts_marks already implemented the rule correctly, and all 61 of its existing tests -- including the PTS attribution ones -- pass unchanged on the shared type. That is the evidence the type preserves the behaviour dts had right. ac3 kept a single carry-over timestamp and one anchor offset, so it could only attribute the first unit in a call; it now resolves each unit at its own offset. truehd had no attribution at all beyond a running clock. New tests cover the case that motivated this: a unit whose first bytes arrive in one packet and whose remainder arrives in the next keeps the FIRST packet's offset. At a clip seam those two packets belong to different clips, and taking the later one places the audio in the wrong one. Clippy on the pinned toolchain caught an empty `if` block left where dts used to clear stale marks -- restored as an explicit clear, with why it is still needed once drain keeps the covering mark.
This commit is contained in:
+81
-26
@@ -62,8 +62,11 @@ const AC3_SAMPLES_PER_FRAME: u32 = 1536;
|
|||||||
const MAX_AC3_BUF: usize = 1024 * 1024;
|
const MAX_AC3_BUF: usize = 1024 * 1024;
|
||||||
|
|
||||||
pub struct Ac3Parser {
|
pub struct Ac3Parser {
|
||||||
/// Leftover bytes from previous PES (incomplete frame at end).
|
/// Leftover bytes from previous PES (incomplete frame at end), each still
|
||||||
buf: Vec<u8>,
|
/// 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.
|
/// 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.
|
/// the running per-frame PTS at the point the partial tail was retained.
|
||||||
/// Used by `flush()` to time the final buffered frame at EOS.
|
/// Used by `flush()` to time the final buffered frame at EOS.
|
||||||
@@ -92,7 +95,7 @@ impl Default for Ac3Parser {
|
|||||||
impl Ac3Parser {
|
impl Ac3Parser {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buf: Vec::with_capacity(4096),
|
acc: super::pesbuf::PesBuf::with_capacity(4096),
|
||||||
flush_pts_ns: 0,
|
flush_pts_ns: 0,
|
||||||
tally: super::dropgate::DropTally::new("ac3"),
|
tally: super::dropgate::DropTally::new("ac3"),
|
||||||
saw_extension: false,
|
saw_extension: false,
|
||||||
@@ -137,6 +140,7 @@ impl Ac3Parser {
|
|||||||
base_pts_ns: i64,
|
base_pts_ns: i64,
|
||||||
anchor: Option<PtsAnchor>,
|
anchor: Option<PtsAnchor>,
|
||||||
at_eos: bool,
|
at_eos: bool,
|
||||||
|
marks: &[(usize, super::pesbuf::PesFacts)],
|
||||||
) -> (Vec<Frame>, usize, i64) {
|
) -> (Vec<Frame>, usize, i64) {
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
let mut pos = 0usize;
|
let mut pos = 0usize;
|
||||||
@@ -219,7 +223,7 @@ impl Ac3Parser {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(au) = pending.take() {
|
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
|
// First access unit that starts in this PES's own bytes: adopt
|
||||||
// this PES's timestamp so a genuine PTS jump is followed instead
|
// this PES's timestamp so a genuine PTS jump is followed instead
|
||||||
@@ -264,7 +268,7 @@ impl Ac3Parser {
|
|||||||
frame_pts_ns = au.pts_ns;
|
frame_pts_ns = au.pts_ns;
|
||||||
hold_from = Some(au.start);
|
hold_from = Some(au.start);
|
||||||
} else {
|
} 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,
|
tally: &mut super::dropgate::DropTally,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
au: &PendingAu,
|
au: &PendingAu,
|
||||||
|
marks: &[(usize, super::pesbuf::PesFacts)],
|
||||||
out: &mut Vec<Frame>,
|
out: &mut Vec<Frame>,
|
||||||
) {
|
) {
|
||||||
if let Some(reason) = au.drop_reason {
|
if let Some(reason) = au.drop_reason {
|
||||||
@@ -344,7 +349,9 @@ fn close_access_unit(
|
|||||||
out.push(Frame {
|
out.push(Frame {
|
||||||
discontinuity: false,
|
discontinuity: false,
|
||||||
coding: None,
|
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,
|
pts_ns: au.pts_ns,
|
||||||
keyframe: true,
|
keyframe: true,
|
||||||
data: data[au.start..au.end].to_vec(),
|
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
|
// never be stranded by an empty post-gap PES (the demuxer only emits
|
||||||
// non-empty PES today; this is defensive for any future caller).
|
// non-empty PES today; this is defensive for any future caller).
|
||||||
if pes.discontinuity {
|
if pes.discontinuity {
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
}
|
}
|
||||||
if pes.data.is_empty() {
|
if pes.data.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -480,7 +487,7 @@ impl CodecParser for Ac3Parser {
|
|||||||
// no anchor the running cadence simply continues. The discontinuity-
|
// no anchor the running cadence simply continues. The discontinuity-
|
||||||
// carrying PES is a PUSI with a PTS in practice, so this is
|
// carrying PES is a PUSI with a PTS in practice, so this is
|
||||||
// defense-in-depth.
|
// defense-in-depth.
|
||||||
let carry_len = self.buf.len();
|
let carry_len = self.acc.len();
|
||||||
let anchor = pes.pts.map(|p| PtsAnchor {
|
let anchor = pes.pts.map(|p| PtsAnchor {
|
||||||
at: carry_len,
|
at: carry_len,
|
||||||
pts_ns: pts_to_ns(p),
|
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
|
// 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
|
// 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.
|
// borrowed from `self`). The unconsumed tail is written back at the end.
|
||||||
self.buf.extend_from_slice(&pes.data);
|
self.acc.push(pes);
|
||||||
let buf = std::mem::take(&mut self.buf);
|
// 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 data = &buf;
|
||||||
let (frames, keep_from, frame_pts_ns) =
|
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() {
|
if keep_from < data.len() {
|
||||||
let tail = &data[keep_from..];
|
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",
|
"ac3: carry-over buffer exceeded {} bytes without a frame; dropping and resyncing",
|
||||||
MAX_AC3_BUF
|
MAX_AC3_BUF
|
||||||
);
|
);
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
// Advance the cadence, as both sibling branches below do, so the
|
// Advance the cadence, as both sibling branches below do, so the
|
||||||
// three paths out of this block cannot disagree. Defensive: no
|
// three paths out of this block cannot disagree. Defensive: no
|
||||||
// input reaching this parser was found that both parses frames and
|
// 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.
|
// prevents is not currently reachable and has no regression test.
|
||||||
self.flush_pts_ns = frame_pts_ns;
|
self.flush_pts_ns = frame_pts_ns;
|
||||||
} else {
|
} else {
|
||||||
self.buf = tail.to_vec();
|
self.acc.drain(keep_from);
|
||||||
// The carried bytes, when later completed and emitted (next call
|
// The carried bytes, when later completed and emitted (next call
|
||||||
// or by flush() at EOS), are timed at the PTS the scanner reached
|
// 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
|
// 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;
|
self.flush_pts_ns = frame_pts_ns;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
// Nothing carried, but keep the cadence so a following PES with no
|
// Nothing carried, but keep the cadence so a following PES with no
|
||||||
// PTS (no anchor) continues the timeline instead of reusing a stale
|
// PTS (no anchor) continues the timeline instead of reusing a stale
|
||||||
// value.
|
// value.
|
||||||
@@ -539,9 +550,11 @@ impl CodecParser for Ac3Parser {
|
|||||||
// with no following PES to close it, and without this drain the last
|
// 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
|
// ~32 ms of audio is lost. `at_eos` closes the trailing access unit
|
||||||
// instead of holding it; a partial/garbage tail yields nothing.
|
// 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
|
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;
|
.0;
|
||||||
// Aggregate drop report at end-of-stream (warn-level, always visible).
|
// Aggregate drop report at end-of-stream (warn-level, always visible).
|
||||||
self.tally.log_summary();
|
self.tally.log_summary();
|
||||||
@@ -1030,15 +1043,15 @@ mod tests {
|
|||||||
let frames = parser.parse(&pes);
|
let frames = parser.parse(&pes);
|
||||||
assert!(frames.is_empty());
|
assert!(frames.is_empty());
|
||||||
assert!(
|
assert!(
|
||||||
parser.buf.len() <= MAX_AC3_BUF,
|
parser.acc.len() <= MAX_AC3_BUF,
|
||||||
"buffer grew to {} (cap {})",
|
"buffer grew to {} (cap {})",
|
||||||
parser.buf.len(),
|
parser.acc.len(),
|
||||||
MAX_AC3_BUF
|
MAX_AC3_BUF
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// After all that garbage the retained tail is at most a single partial
|
// After all that garbage the retained tail is at most a single partial
|
||||||
// syncword byte — never an accumulation of whole PES packets.
|
// 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]
|
#[test]
|
||||||
@@ -1057,7 +1070,11 @@ mod tests {
|
|||||||
discontinuity: false,
|
discontinuity: false,
|
||||||
};
|
};
|
||||||
assert!(parser.parse(&pes).is_empty());
|
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]
|
#[test]
|
||||||
@@ -1067,14 +1084,14 @@ mod tests {
|
|||||||
// ac3 inherited the no-op default flush and dropped the last frame.
|
// ac3 inherited the no-op default flush and dropped the last frame.
|
||||||
let mut parser = Ac3Parser::new();
|
let mut parser = Ac3Parser::new();
|
||||||
let frame_data = make_ac3_frame(0, 2);
|
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);
|
parser.flush_pts_ns = pts_to_ns(99000);
|
||||||
let f = parser.flush();
|
let f = parser.flush();
|
||||||
assert_eq!(f.len(), 1, "complete buffered frame drained at EOS");
|
assert_eq!(f.len(), 1, "complete buffered frame drained at EOS");
|
||||||
assert_eq!(f[0].data.len(), 160);
|
assert_eq!(f[0].data.len(), 160);
|
||||||
assert_eq!(f[0].pts_ns, pts_to_ns(99000), "flush uses carried PTS");
|
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!(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]
|
#[test]
|
||||||
@@ -1107,7 +1124,7 @@ mod tests {
|
|||||||
// emitted truncated.
|
// emitted truncated.
|
||||||
let mut parser = Ac3Parser::new();
|
let mut parser = Ac3Parser::new();
|
||||||
let frame_data = make_ac3_frame(0, 2);
|
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");
|
assert!(parser.flush().is_empty(), "partial tail dropped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1511,7 +1528,7 @@ mod tests {
|
|||||||
// 100 bytes.
|
// 100 bytes.
|
||||||
let mut parser = Ac3Parser::new();
|
let mut parser = Ac3Parser::new();
|
||||||
let frame = make_ac3_frame(0, 2); // sizes to 160
|
let frame = make_ac3_frame(0, 2); // sizes to 160
|
||||||
parser.buf = frame[..100].to_vec();
|
parser.acc.seed(&frame[..100]);
|
||||||
assert!(
|
assert!(
|
||||||
parser.flush().is_empty(),
|
parser.flush().is_empty(),
|
||||||
"incomplete frame must not be emitted truncated at flush"
|
"incomplete frame must not be emitted truncated at flush"
|
||||||
@@ -1522,7 +1539,7 @@ mod tests {
|
|||||||
fn flush_with_no_sync_is_empty() {
|
fn flush_with_no_sync_is_empty() {
|
||||||
// flush on a buffer with no syncword yields nothing and clears.
|
// flush on a buffer with no syncword yields nothing and clears.
|
||||||
let mut parser = Ac3Parser::new();
|
let mut parser = Ac3Parser::new();
|
||||||
parser.buf = vec![0xAA, 0xBB, 0xCC];
|
parser.acc.seed(&[0xAA, 0xBB, 0xCC]);
|
||||||
assert!(parser.flush().is_empty());
|
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.pts_ns, pts_to_ns(90000) + i as i64 * 32_000_000);
|
||||||
assert_eq!(fr.duration_ns, Some(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");
|
assert!(parser.flush().is_empty(), "flush has nothing left to drain");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2110,4 +2127,42 @@ mod tests {
|
|||||||
discontinuity: false,
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,4 +276,15 @@ mod tests {
|
|||||||
let f = p.parse(&make_pes(vec![0xFF, 0xF1, 0x50], Some(0)));
|
let f = p.parse(&make_pes(vec![0xFF, 0xF1, 0x50], Some(0)));
|
||||||
assert_eq!(f.len(), 1, "too short to validate → kept");
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-110
@@ -6,7 +6,7 @@
|
|||||||
//! are emitted complete.
|
//! are emitted complete.
|
||||||
|
|
||||||
use super::startcode::BitReader;
|
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];
|
const DTS_CORE_SYNC: [u8; 4] = [0x7F, 0xFE, 0x80, 0x01];
|
||||||
/// DTS-HD extension substream syncword. An access unit is delimited by the next
|
/// 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
|
/// This preserves the lossless extension data instead of downgrading to lossy
|
||||||
/// core (the lossy-core downgrade bug).
|
/// core (the lossy-core downgrade bug).
|
||||||
pub struct DtsParser {
|
pub struct DtsParser {
|
||||||
buf: Vec<u8>,
|
/// 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
|
/// PTS of the access unit currently being assembled in `buf` (the unit
|
||||||
/// starting at the first buffered core sync). Captured when that core
|
/// starting at the first buffered core sync). Captured when that core
|
||||||
/// frame's PES first arrived; the trailing extension-substream PES
|
/// frame's PES first arrived; the trailing extension-substream PES
|
||||||
/// packets carry their own (later) PTS which must NOT override it.
|
/// packets carry their own (later) PTS which must NOT override it.
|
||||||
pending_pts: i64,
|
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
|
/// 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
|
/// 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
|
/// unchanged, this AU shares the previous AU's PES → advance one frame
|
||||||
@@ -66,9 +59,8 @@ impl Default for DtsParser {
|
|||||||
impl DtsParser {
|
impl DtsParser {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buf: Vec::with_capacity(32768),
|
acc: super::pesbuf::PesBuf::with_capacity(32768),
|
||||||
pending_pts: 0,
|
pending_pts: 0,
|
||||||
pts_marks: std::collections::VecDeque::new(),
|
|
||||||
last_front_pts: PTS_UNSET,
|
last_front_pts: PTS_UNSET,
|
||||||
next_pts_ns: PTS_UNSET,
|
next_pts_ns: PTS_UNSET,
|
||||||
tally: super::dropgate::DropTally::new("dts"),
|
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
|
/// 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
|
/// a drop leaves the following audio on its true timeline — a gap, not a
|
||||||
/// shift. Every drop is logged (fail-loud, never silent).
|
/// shift. Every drop is logged (fail-loud, never silent).
|
||||||
fn emit_or_drop(&mut self, au: Vec<u8>, au_pts: i64, dur_ns: i64, out: &mut Vec<Frame>) {
|
fn emit_or_drop(
|
||||||
|
&mut self,
|
||||||
|
au: Vec<u8>,
|
||||||
|
au_pts: i64,
|
||||||
|
dur_ns: i64,
|
||||||
|
src: Option<crate::pes::SourcePos>,
|
||||||
|
out: &mut Vec<Frame>,
|
||||||
|
) {
|
||||||
let verdict = if self.tally.is_poisoned() {
|
let verdict = if self.tally.is_poisoned() {
|
||||||
Err(DropReason::TrackPoisoned)
|
Err(DropReason::TrackPoisoned)
|
||||||
} else {
|
} else {
|
||||||
@@ -104,7 +103,9 @@ impl DtsParser {
|
|||||||
out.push(Frame {
|
out.push(Frame {
|
||||||
discontinuity: false,
|
discontinuity: false,
|
||||||
coding: None,
|
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,
|
pts_ns: au_pts,
|
||||||
keyframe: true,
|
keyframe: true,
|
||||||
data: au,
|
data: au,
|
||||||
@@ -148,41 +149,25 @@ impl DtsParser {
|
|||||||
base
|
base
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop `n` bytes from the front of `buf` and rebase the PTS markers so
|
/// Drop `n` bytes from the front, rebasing attribution onto the new front.
|
||||||
/// 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.
|
|
||||||
fn drain_front(&mut self, n: usize) {
|
fn drain_front(&mut self, n: usize) {
|
||||||
if n == 0 {
|
self.acc.drain(n);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PTS that should be stamped on an access unit currently at the front of
|
/// PTS for the access unit at the front of the buffer: the facts of the
|
||||||
/// `buf` (offset 0): the most recent marker at offset 0, falling back to
|
/// packet covering offset 0, falling back to the unit's captured base.
|
||||||
/// `pending_pts`.
|
|
||||||
fn front_pts(&self) -> i64 {
|
fn front_pts(&self) -> i64 {
|
||||||
self.pts_marks
|
self.acc
|
||||||
.iter()
|
.front()
|
||||||
.rev()
|
.presentation_ns()
|
||||||
.find(|&&(off, _)| off == 0)
|
|
||||||
.map(|&(_, pts)| pts)
|
|
||||||
.unwrap_or(self.pending_pts)
|
.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<crate::pes::SourcePos> {
|
||||||
|
self.acc.front().source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hard cap on a buffered access unit (core + all its extension substreams).
|
/// 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
|
// never be stranded by an empty post-gap PES (defensive; the demuxer only
|
||||||
// emits non-empty PES today).
|
// emits non-empty PES today).
|
||||||
if pes.discontinuity {
|
if pes.discontinuity {
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
self.pts_marks.clear();
|
|
||||||
self.pending_pts = PTS_UNSET;
|
self.pending_pts = PTS_UNSET;
|
||||||
// A concealed gap is a timeline discontinuity: let the post-gap AU
|
// 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.
|
// re-base to its own PES PTS rather than the pre-gap cursor.
|
||||||
@@ -242,16 +226,14 @@ impl CodecParser for DtsParser {
|
|||||||
// guard at a post-gap continuation) must NOT reset the timeline to 0;
|
// guard at a post-gap continuation) must NOT reset the timeline to 0;
|
||||||
// continue from the most recent known base. Defense-in-depth: the
|
// continue from the most recent known base. Defense-in-depth: the
|
||||||
// discontinuity-carrying PES is a PUSI with a PTS in practice.
|
// discontinuity-carrying PES is a PUSI with a PTS in practice.
|
||||||
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or_else(|| {
|
// A PES with no PTS (rare for audio, but legal) must NOT reset the
|
||||||
self.pts_marks
|
// timeline to 0 — continue from the most recent known base.
|
||||||
.back()
|
let pts_ns = super::pesbuf::PesFacts::of(pes)
|
||||||
.map(|&(_, p)| p)
|
.presentation_ns()
|
||||||
.filter(|&p| p >= 0)
|
|
||||||
.unwrap_or(if self.pending_pts >= 0 {
|
.unwrap_or(if self.pending_pts >= 0 {
|
||||||
self.pending_pts
|
self.pending_pts
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// On Blu-ray, a DTS-HD MA/HRA access unit is a DTS core frame
|
// On Blu-ray, a DTS-HD MA/HRA access unit is a DTS core frame
|
||||||
@@ -272,7 +254,7 @@ impl CodecParser for DtsParser {
|
|||||||
// prior forced (safety-valve) flush left it invalidated — in the
|
// prior forced (safety-valve) flush left it invalidated — in the
|
||||||
// forced case the bytes still in `buf` are not a real core frame, so
|
// 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.
|
// 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;
|
self.pending_pts = pts_ns;
|
||||||
}
|
}
|
||||||
// Mark where THIS PES's bytes begin in the buffer, with its PTS. The
|
// 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
|
// (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
|
// that core's timestamp even when its extensions / the following core
|
||||||
// arrive (with a later PTS) in this same parse() call.
|
// arrive (with a later PTS) in this same parse() call.
|
||||||
// (pts_marks is bounded implicitly: an empty PES returns above without
|
// `pts_ns` is this packet's own timestamp, or the carried-forward base
|
||||||
// pushing a mark, and a non-empty run grows `buf`, which is cleared —
|
// when it had none; the source offset is always this packet's.
|
||||||
// along with pts_marks — once it exceeds MAX_AU_BYTES.)
|
self.acc.push_with(
|
||||||
self.pts_marks.push_back((self.buf.len(), pts_ns));
|
&pes.data,
|
||||||
self.buf.extend_from_slice(&pes.data);
|
super::pesbuf::PesFacts::of(pes).with_pts_ns(pts_ns),
|
||||||
|
);
|
||||||
|
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Resync to the first core sync; drop any leading junk.
|
// 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
|
// 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.
|
// sync split across PES packets can still be found next time.
|
||||||
if self.buf.len() > 3 {
|
if self.acc.len() > 3 {
|
||||||
let tail = self.buf.len() - 3;
|
let tail = self.acc.len() - 3;
|
||||||
self.drain_front(tail);
|
self.drain_front(tail);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -305,17 +288,17 @@ impl CodecParser for DtsParser {
|
|||||||
// offset 0 by construction, so a re-scan would be a redundant
|
// offset 0 by construction, so a re-scan would be a redundant
|
||||||
// O(buf_len) walk per iteration; assert the invariant instead.
|
// O(buf_len) walk per iteration; assert the invariant instead.
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
find_sync(&self.buf, &DTS_CORE_SYNC),
|
find_sync(self.acc.as_slice(), &DTS_CORE_SYNC),
|
||||||
Some(0),
|
Some(0),
|
||||||
"drain_front(start) must leave the core sync at offset 0"
|
"drain_front(start) must leave the core sync at offset 0"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need the core header to size the core frame.
|
// 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;
|
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
|
// `dts_core_frame_size` returns a 14-bit `fsize + 1`, so it is
|
||||||
// always in [1, 16384]; the bare `== 0` / `> MAX_AU_BYTES` checks
|
// always in [1, 16384]; the bare `== 0` / `> MAX_AU_BYTES` checks
|
||||||
// can never fire. A real DTS core frame is at least
|
// can never fire. A real DTS core frame is at least
|
||||||
@@ -330,7 +313,7 @@ impl CodecParser for DtsParser {
|
|||||||
self.drain_front(4);
|
self.drain_front(4);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if self.buf.len() < core_size {
|
if self.acc.len() < core_size {
|
||||||
break; // core frame not fully buffered yet — wait
|
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
|
// flush is an extension-substream PES, carrying its own later
|
||||||
// timestamp) must NOT become the next unit's PTS base.
|
// timestamp) must NOT become the next unit's PTS base.
|
||||||
let mut forced = false;
|
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::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 => {
|
NextCore::NeedMore => {
|
||||||
// A candidate boundary exists but is not fully buffered. Normally
|
// A candidate boundary exists but is not fully buffered. Normally
|
||||||
// we wait for more PES; but once the buffer exceeds the AU cap,
|
// 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
|
// stream that keeps a boundary perpetually incomplete can't grow
|
||||||
// `buf` without bound (the `break` above never reaches it).
|
// `buf` without bound (the `break` above never reaches it).
|
||||||
forced = true;
|
forced = true;
|
||||||
(self.buf.len(), true)
|
(self.acc.len(), true)
|
||||||
}
|
}
|
||||||
NextCore::None => {
|
NextCore::None => {
|
||||||
// No next core sync buffered yet. The trailing extension
|
// 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
|
// them rather than emit a core-only (lossy) frame — unless
|
||||||
// the buffer has grown unreasonably large, in which case
|
// the buffer has grown unreasonably large, in which case
|
||||||
// emit what we have to guarantee forward progress.
|
// emit what we have to guarantee forward progress.
|
||||||
if self.buf.len() <= MAX_AU_BYTES {
|
if self.acc.len() <= MAX_AU_BYTES {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
forced = true;
|
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
|
// rather than shipping it. A recognized-but-unsizeable extension
|
||||||
// (`ext_clean == true`) is preserved in full (lossless).
|
// (`ext_clean == true`) is preserved in full (lossless).
|
||||||
let emit_end = if ext_clean { au_end } else { core_size };
|
let emit_end = if ext_clean { au_end } else { core_size };
|
||||||
let au: Vec<u8> = self.buf[..emit_end].to_vec();
|
let au: Vec<u8> = self.acc.as_slice()[..emit_end].to_vec();
|
||||||
// The AU's own core PES PTS (the PES covering its first byte, even if
|
// 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
|
// that PES preceded the one(s) carrying its extensions or the next
|
||||||
// core), stamped monotonically: honored when it advances past the
|
// 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,
|
// 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.
|
// never a shift. `emit_or_drop` decides whether to actually push it.
|
||||||
let au_pts = self.stamp_pts(self.front_pts(), dur_ns);
|
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);
|
self.drain_front(au_end);
|
||||||
// After draining, the marker covering the new front (if any) carries
|
// After draining, the marker covering the new front (if any) carries
|
||||||
// the next AU's PTS; `pending_pts` is only the fallback when no
|
// 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
|
// regardless of buffer state, rather than inheriting this
|
||||||
// (non-core) PES's timestamp.
|
// (non-core) PES's timestamp.
|
||||||
self.pending_pts = PTS_UNSET;
|
self.pending_pts = PTS_UNSET;
|
||||||
self.pts_marks.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discard markers that no longer reference live buffer bytes (everything
|
// An empty buffer holds no bytes for a mark to attribute, so drop the
|
||||||
// past the buffer end can't happen, but collapse duplicates at offset 0
|
// marks with them. `drain` deliberately keeps the mark covering the new
|
||||||
// and drop a stale empty-buffer marker set).
|
// front — correct while bytes remain, stale once none do.
|
||||||
if self.buf.is_empty() {
|
if self.acc.is_empty() {
|
||||||
self.pts_marks.clear();
|
self.acc.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
frames
|
frames
|
||||||
@@ -440,26 +425,28 @@ impl DtsParser {
|
|||||||
/// streaming), gated through the decodability check. Require a complete core
|
/// streaming), gated through the decodability check. Require a complete core
|
||||||
/// frame; drop a bare partial sync tail.
|
/// frame; drop a bare partial sync tail.
|
||||||
fn flush_tail(&mut self) -> Vec<Frame> {
|
fn flush_tail(&mut self) -> Vec<Frame> {
|
||||||
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();
|
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
|
// `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()`.
|
// old `== 0` check was dead; reject a sub-minimum core like `parse()`.
|
||||||
if core_size < MIN_CORE_FRAME_BYTES || self.buf.len() < core_size {
|
if core_size < MIN_CORE_FRAME_BYTES || self.acc.len() < core_size {
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
// The final AU's PTS is the PES covering the buffer front (its core's
|
// 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.
|
// 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 dur_ns = dts_core_duration_ns(&au) as i64;
|
||||||
let pts_ns = self.stamp_pts(self.front_pts(), dur_ns);
|
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();
|
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
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -824,6 +811,7 @@ fn core_header_drop_reason(au: &[u8]) -> Option<DropReason> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::mux::codec::pts_to_ns;
|
||||||
use crate::mux::ts::PesPacket;
|
use crate::mux::ts::PesPacket;
|
||||||
|
|
||||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||||
@@ -899,15 +887,19 @@ mod tests {
|
|||||||
fn drain_front_collapses_offset_zero_markers_instead_of_leaking() {
|
fn drain_front_collapses_offset_zero_markers_instead_of_leaking() {
|
||||||
let mut parser = DtsParser::new();
|
let mut parser = DtsParser::new();
|
||||||
for i in 0..200i64 {
|
for i in 0..200i64 {
|
||||||
parser.buf.extend_from_slice(&[0u8; 5]);
|
parser.acc.append_unattributed(&[0u8; 5]);
|
||||||
parser.pts_marks.push_back((5, i));
|
parser
|
||||||
parser.pts_marks.push_back((5, i));
|
.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);
|
parser.drain_front(5);
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
parser.pts_marks.len() <= 2,
|
parser.acc.mark_count() <= 2,
|
||||||
"pts_marks must stay bounded across repeated drains, got {}",
|
"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"
|
"NeedMore past the AU cap must force-emit, not stall and balloon the buffer"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
parser.buf.is_empty(),
|
parser.acc.is_empty(),
|
||||||
"the forced flush drains the buffer instead of growing it unbounded"
|
"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
|
// The first core's bytes are still buffered awaiting the verdict — not
|
||||||
// dropped, not emitted.
|
// dropped, not emitted.
|
||||||
assert!(
|
assert!(
|
||||||
parser.buf.len() >= 512,
|
parser.acc.len() >= 512,
|
||||||
"core1 retained while candidate boundary is undecided"
|
"core1 retained while candidate boundary is undecided"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1809,8 +1801,8 @@ mod tests {
|
|||||||
let mut parser = DtsParser::new();
|
let mut parser = DtsParser::new();
|
||||||
let f = parser.parse(&make_pes(vec![0x11, 0x22, 0x33, 0x44], Some(90000)));
|
let f = parser.parse(&make_pes(vec![0x11, 0x22, 0x33, 0x44], Some(90000)));
|
||||||
assert!(f.is_empty());
|
assert!(f.is_empty());
|
||||||
assert_eq!(parser.buf.len(), 3, "only a 3-byte resync tail retained");
|
assert_eq!(parser.acc.len(), 3, "only a 3-byte resync tail retained");
|
||||||
assert_eq!(parser.buf, vec![0x22, 0x33, 0x44]);
|
assert_eq!(parser.acc.as_slice(), &[0x22, 0x33, 0x44]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1825,7 +1817,7 @@ mod tests {
|
|||||||
.parse(&make_pes(core[..3].to_vec(), Some(90000)))
|
.parse(&make_pes(core[..3].to_vec(), Some(90000)))
|
||||||
.is_empty()
|
.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.
|
// PES 2: the 4th sync byte + the rest of core1, then a 2nd core to close.
|
||||||
let mut rest = core[3..].to_vec();
|
let mut rest = core[3..].to_vec();
|
||||||
rest.extend_from_slice(&make_dts_core(640));
|
rest.extend_from_slice(&make_dts_core(640));
|
||||||
@@ -1847,7 +1839,7 @@ mod tests {
|
|||||||
let mut data = DTS_CORE_SYNC.to_vec();
|
let mut data = DTS_CORE_SYNC.to_vec();
|
||||||
data.extend_from_slice(&[0x00, 0x00, 0x00]); // only 7 bytes total < 10
|
data.extend_from_slice(&[0x00, 0x00, 0x00]); // only 7 bytes total < 10
|
||||||
assert!(parser.parse(&make_pes(data, Some(90000))).is_empty());
|
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]
|
#[test]
|
||||||
@@ -1859,7 +1851,7 @@ mod tests {
|
|||||||
let mut d = vec![0u8; 17];
|
let mut d = vec![0u8; 17];
|
||||||
d[0..4].copy_from_slice(&DTS_CORE_SYNC);
|
d[0..4].copy_from_slice(&DTS_CORE_SYNC);
|
||||||
d[6] = 0x01; // fsize → 16 → size 17
|
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");
|
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).
|
// declared size must be dropped (never emit fewer bytes than declared).
|
||||||
let mut parser = DtsParser::new();
|
let mut parser = DtsParser::new();
|
||||||
let core = make_dts_core(512);
|
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!(
|
assert!(
|
||||||
parser.flush().is_empty(),
|
parser.flush().is_empty(),
|
||||||
"incomplete core not emitted truncated"
|
"incomplete core not emitted truncated"
|
||||||
@@ -1886,9 +1878,9 @@ mod tests {
|
|||||||
fn flush_partial_sync_tail_dropped() {
|
fn flush_partial_sync_tail_dropped() {
|
||||||
// A bare partial-sync tail (not at offset 0 / not a full core) is dropped.
|
// A bare partial-sync tail (not at offset 0 / not a full core) is dropped.
|
||||||
let mut parser = DtsParser::new();
|
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.flush().is_empty());
|
||||||
assert!(parser.buf.is_empty(), "buffer cleared on flush");
|
assert!(parser.acc.is_empty(), "buffer cleared on flush");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2311,13 +2303,13 @@ mod tests {
|
|||||||
let out = parser.parse(&make_pes(d, Some(90_000)));
|
let out = parser.parse(&make_pes(d, Some(90_000)));
|
||||||
assert!(out.is_empty(), "a false sync emits nothing");
|
assert!(out.is_empty(), "a false sync emits nothing");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parser.buf.len(),
|
parser.acc.len(),
|
||||||
3,
|
3,
|
||||||
"the false sync was decoded, drained and resynced past — leaving only \
|
"the false sync was decoded, drained and resynced past — leaving only \
|
||||||
the 3-byte split-sync carry-over"
|
the 3-byte split-sync carry-over"
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
find_sync(&parser.buf, &DTS_CORE_SYNC),
|
find_sync(parser.acc.as_slice(), &DTS_CORE_SYNC),
|
||||||
Some(0),
|
Some(0),
|
||||||
"and the bogus sync is no longer at the front of the buffer"
|
"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() {
|
fn flush_emits_a_core_that_exactly_fills_the_buffer() {
|
||||||
let mut parser = DtsParser::new();
|
let mut parser = DtsParser::new();
|
||||||
let core = make_dts_core(512);
|
let core = make_dts_core(512);
|
||||||
parser.buf = core.clone();
|
parser.acc.seed(&core.clone());
|
||||||
parser.pending_pts = 90_000;
|
parser.pending_pts = 90_000;
|
||||||
let out = parser.flush();
|
let out = parser.flush();
|
||||||
assert_eq!(out.len(), 1, "the final AU is emitted, not dropped");
|
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");
|
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.
|
// One byte short is still refused — the bound is not simply absent.
|
||||||
let mut parser = DtsParser::new();
|
let mut parser = DtsParser::new();
|
||||||
parser.buf = core[..511].to_vec();
|
parser.acc.seed(&core[..511]);
|
||||||
parser.pending_pts = 90_000;
|
parser.pending_pts = 90_000;
|
||||||
assert!(
|
assert!(
|
||||||
parser.flush().is_empty(),
|
parser.flush().is_empty(),
|
||||||
@@ -2368,18 +2360,18 @@ mod tests {
|
|||||||
Some(0),
|
Some(0),
|
||||||
"the fixture really has no core sync at the front"
|
"the fixture really has no core sync at the front"
|
||||||
);
|
);
|
||||||
parser.buf = broken;
|
parser.acc.seed(&broken);
|
||||||
parser.pending_pts = 90_000;
|
parser.pending_pts = 90_000;
|
||||||
assert!(
|
assert!(
|
||||||
parser.flush().is_empty(),
|
parser.flush().is_empty(),
|
||||||
"a buffer whose front is not a core sync is discarded, not size-decoded"
|
"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
|
// The other half of the disjunction: a buffer too short to size, whose
|
||||||
// front IS a core sync, is discarded too.
|
// front IS a core sync, is discarded too.
|
||||||
let mut parser = DtsParser::new();
|
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;
|
parser.pending_pts = 90_000;
|
||||||
assert!(parser.flush().is_empty(), "a bare sync tail is not an AU");
|
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 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-11
@@ -34,10 +34,10 @@ use crate::pes::SourcePos;
|
|||||||
/// changed those semantics silently while fixing provenance.
|
/// changed those semantics silently while fixing provenance.
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||||
pub(crate) struct PesFacts {
|
pub(crate) struct PesFacts {
|
||||||
/// Presentation timestamp in 90kHz ticks, as carried.
|
/// Presentation time in NANOSECONDS, already derived (see `of`). Stored
|
||||||
pub pts: Option<i64>,
|
/// derived rather than raw so there is one derivation and no caller can
|
||||||
/// Decode timestamp in 90kHz ticks, as carried.
|
/// pick a different one.
|
||||||
pub dts: Option<i64>,
|
pub pts_ns: Option<i64>,
|
||||||
/// Byte offset of this PES's first ES byte within the title's feed — what
|
/// 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
|
/// identifies the clip a frame came from. `None` when the demuxer was fed
|
||||||
/// without a base offset.
|
/// without a base offset.
|
||||||
@@ -56,13 +56,23 @@ impl PesFacts {
|
|||||||
/// for a parser whose unit begins in the packet it is handed.
|
/// for a parser whose unit begins in the packet it is handed.
|
||||||
pub(crate) fn of(pes: &PesPacket) -> Self {
|
pub(crate) fn of(pes: &PesPacket) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pts: pes.pts,
|
pts_ns: pes.pts.or(pes.dts).map(pts_to_ns),
|
||||||
dts: pes.dts,
|
|
||||||
source: pes.source,
|
source: pes.source,
|
||||||
discontinuity: pes.discontinuity,
|
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.
|
/// 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,
|
/// 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
|
/// for a missing field, not a second rule: dvdsub read `pts` alone and
|
||||||
/// returned 0 for a packet that carried only DTS.
|
/// returned 0 for a packet that carried only DTS.
|
||||||
pub(crate) fn presentation_ns(&self) -> Option<i64> {
|
pub(crate) fn presentation_ns(&self) -> Option<i64> {
|
||||||
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
|
/// Bytes accumulated across PES packets, each byte attributable to the packet
|
||||||
/// that carried it.
|
/// that carried it.
|
||||||
pub(crate) struct PesBuf {
|
pub(crate) struct PesBuf {
|
||||||
@@ -112,10 +137,14 @@ impl PesBuf {
|
|||||||
self.buf.extend_from_slice(&pes.data);
|
self.buf.extend_from_slice(&pes.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append raw bytes attributed to the SAME PES as the bytes already at the
|
/// Append a payload under facts the caller resolved — for a parser that
|
||||||
/// end of the buffer. For a parser that rewrites or re-frames payload
|
/// carries a timestamp forward across a packet that omitted one. Same
|
||||||
/// in-place rather than appending a packet verbatim.
|
/// attribution rule; only the timestamp differs from what the packet said.
|
||||||
pub(crate) fn push_bytes(&mut self, data: &[u8]) {
|
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);
|
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) {
|
pub(crate) fn clear(&mut self) {
|
||||||
self.buf.clear();
|
self.buf.clear();
|
||||||
self.marks.clear();
|
self.marks.clear();
|
||||||
|
|||||||
+65
-34
@@ -63,7 +63,10 @@ const AU_DURATION_NS_441: i64 = 907_029;
|
|||||||
const MAX_TRUEHD_BUF: usize = 256 * 1024;
|
const MAX_TRUEHD_BUF: usize = 256 * 1024;
|
||||||
|
|
||||||
pub struct TrueHdParser {
|
pub struct TrueHdParser {
|
||||||
buf: Vec<u8>,
|
/// 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,
|
next_pts_ns: i64,
|
||||||
/// Per-AU PTS increment. Defaults to the 48 kHz-family value (833_333) and
|
/// 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
|
/// 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 {
|
impl TrueHdParser {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buf: Vec::with_capacity(32768),
|
acc: super::pesbuf::PesBuf::with_capacity(32768),
|
||||||
next_pts_ns: 0,
|
next_pts_ns: 0,
|
||||||
au_duration_ns: AU_DURATION_NS,
|
au_duration_ns: AU_DURATION_NS,
|
||||||
tally: DropTally::new("truehd"),
|
tally: DropTally::new("truehd"),
|
||||||
@@ -195,15 +198,15 @@ impl TrueHdParser {
|
|||||||
/// single source of truth shared with the AC-3 parser; a returned `0` there
|
/// 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.
|
/// (reserved fscod or out-of-range frmsizecod) is the unmappable case.
|
||||||
fn ac3_frame_at_head(&self) -> Ac3Size {
|
fn ac3_frame_at_head(&self) -> Ac3Size {
|
||||||
if self.buf.len() < 6 {
|
if self.acc.len() < 6 {
|
||||||
return Ac3Size::NeedMore;
|
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 {
|
if frame_bytes == 0 {
|
||||||
// Reserved fscod or out-of-range frmsizecod → unmappable header.
|
// Reserved fscod or out-of-range frmsizecod → unmappable header.
|
||||||
return Ac3Size::Unmappable;
|
return Ac3Size::Unmappable;
|
||||||
}
|
}
|
||||||
if self.buf.len() < frame_bytes {
|
if self.acc.len() < frame_bytes {
|
||||||
return Ac3Size::NeedMore;
|
return Ac3Size::NeedMore;
|
||||||
}
|
}
|
||||||
Ac3Size::Frame(frame_bytes)
|
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
|
// never be stranded by an empty post-gap PES (defensive; the demuxer only
|
||||||
// emits non-empty PES today).
|
// emits non-empty PES today).
|
||||||
if pes.discontinuity {
|
if pes.discontinuity {
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
}
|
}
|
||||||
if pes.data.is_empty() {
|
if pes.data.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -376,7 +379,7 @@ impl CodecParser for TrueHdParser {
|
|||||||
// mid-AU would snap that AU's PTS backward/forward and break the
|
// 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
|
// 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.
|
// 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
|
&& let Some(pts) = pes.pts
|
||||||
{
|
{
|
||||||
// Resync to the authoritative PES PTS. TrueHD AUs are a fixed
|
// 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();
|
let mut frames = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if self.buf.len() < 4 {
|
if self.acc.len() < 4 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,19 +441,19 @@ impl CodecParser for TrueHdParser {
|
|||||||
// when its computed end is corroborated by what follows: end of
|
// when its computed end is corroborated by what follows: end of
|
||||||
// buffer (frame fills the rest), another AC-3 sync, or a plausible
|
// 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.
|
// 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() {
|
match self.ac3_frame_at_head() {
|
||||||
Ac3Size::Unmappable => {
|
Ac3Size::Unmappable => {
|
||||||
// Permanently unmappable header at the head would stall
|
// Permanently unmappable header at the head would stall
|
||||||
// the parser forever; resync by dropping 2 bytes so one
|
// the parser forever; resync by dropping 2 bytes so one
|
||||||
// bad frame costs one frame, not the whole buffer.
|
// bad frame costs one frame, not the whole buffer.
|
||||||
self.buf.drain(..2);
|
self.acc.drain(2);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Ac3Size::NeedMore => break, // wait for the rest of the frame
|
Ac3Size::NeedMore => break, // wait for the rest of the frame
|
||||||
Ac3Size::Frame(skip) => {
|
Ac3Size::Frame(skip) => {
|
||||||
if ac3_boundary_corroborated(&self.buf, skip) {
|
if ac3_boundary_corroborated(self.acc.as_slice(), skip) {
|
||||||
self.buf.drain(..skip);
|
self.acc.drain(skip);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Not corroborated — fall through and interpret the
|
// 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
|
// 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 {
|
if unit_words == 0 {
|
||||||
// A zero-length AU is malformed/padding. The AU header is 4 bytes
|
// A zero-length AU is malformed/padding. The AU header is 4 bytes
|
||||||
// (length + timing); drain the whole header, not just the length
|
// (length + timing); drain the whole header, not just the length
|
||||||
// word, otherwise the timing bytes get misread as the next
|
// word, otherwise the timing bytes get misread as the next
|
||||||
// length word and produce a spurious parse on the next iteration.
|
// length word and produce a spurious parse on the next iteration.
|
||||||
self.buf.drain(..4);
|
self.acc.drain(4);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// unit_words is masked to 12 bits, so unit_bytes <= 4095 * 2 = 8190;
|
// unit_words is masked to 12 bits, so unit_bytes <= 4095 * 2 = 8190;
|
||||||
// no separate oversize-resync guard is reachable.
|
// no separate oversize-resync guard is reachable.
|
||||||
let unit_bytes = unit_words * 2;
|
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
|
break; // incomplete access unit, wait for more data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,10 +487,10 @@ impl CodecParser for TrueHdParser {
|
|||||||
// gated on 0xBA alone in `au_check`.
|
// gated on 0xBA alone in `au_check`.
|
||||||
let is_major_sync = unit_bytes >= 8
|
let is_major_sync = unit_bytes >= 8
|
||||||
&& is_mlp_major_sync(u32::from_be_bytes([
|
&& is_mlp_major_sync(u32::from_be_bytes([
|
||||||
self.buf[4],
|
self.acc.as_slice()[4],
|
||||||
self.buf[5],
|
self.acc.as_slice()[5],
|
||||||
self.buf[6],
|
self.acc.as_slice()[6],
|
||||||
self.buf[7],
|
self.acc.as_slice()[7],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Decodability gate. MLP/TrueHD decode state persists across access
|
// 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.
|
// 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
|
// The PTS clock advances across every dropped AU so a drop is a
|
||||||
// silence gap, never a shift.
|
// 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;
|
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<bool> = None; // Some(is_keyframe) => emit
|
let mut emit_keyframe: Option<bool> = None; // Some(is_keyframe) => emit
|
||||||
let mut drop_reason: Option<(&'static str, bool)> = None; // (reason, verified)
|
let mut drop_reason: Option<(&'static str, bool)> = None; // (reason, verified)
|
||||||
|
|
||||||
@@ -560,7 +568,7 @@ impl CodecParser for TrueHdParser {
|
|||||||
frames.push(Frame {
|
frames.push(Frame {
|
||||||
discontinuity: false,
|
discontinuity: false,
|
||||||
coding: None,
|
coding: None,
|
||||||
source: None,
|
source: au_src,
|
||||||
pts_ns: pts,
|
pts_ns: pts,
|
||||||
keyframe,
|
keyframe,
|
||||||
data: au,
|
data: au,
|
||||||
@@ -575,14 +583,14 @@ impl CodecParser for TrueHdParser {
|
|||||||
.record_collateral_drop(pts, self.au_duration_ns, au.len(), reason);
|
.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;
|
self.next_pts_ns += self.au_duration_ns;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bound memory on malformed input: a stream that never yields a
|
// Bound memory on malformed input: a stream that never yields a
|
||||||
// complete frame must not grow the buffer without limit.
|
// complete frame must not grow the buffer without limit.
|
||||||
if self.buf.len() > MAX_TRUEHD_BUF {
|
if self.acc.len() > MAX_TRUEHD_BUF {
|
||||||
self.buf.clear();
|
self.acc.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
frames
|
frames
|
||||||
@@ -1455,7 +1463,7 @@ mod tests {
|
|||||||
"TrueHD AU behind a bad header is recovered"
|
"TrueHD AU behind a bad header is recovered"
|
||||||
);
|
);
|
||||||
assert_eq!(frames[0].data.len(), 200);
|
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]
|
#[test]
|
||||||
@@ -1707,7 +1715,7 @@ mod tests {
|
|||||||
f.is_empty(),
|
f.is_empty(),
|
||||||
"must not emit fewer bytes than the length field"
|
"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.
|
/// 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:
|
// The bound that actually holds is MAX_AU_BYTES, not MAX_TRUEHD_BUF:
|
||||||
// `parse`'s loop only breaks with data retained when
|
// `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
|
// `((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
|
// 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
|
// an unreachable backstop — an exhaustive sweep of all 65536 two-byte
|
||||||
@@ -1740,17 +1748,17 @@ mod tests {
|
|||||||
frag[0] = 0xFF;
|
frag[0] = 0xFF;
|
||||||
frag[1] = 0xFF;
|
frag[1] = 0xFF;
|
||||||
let _ = parser.parse(&make_pes(frag, Some(0)));
|
let _ = parser.parse(&make_pes(frag, Some(0)));
|
||||||
worst = worst.max(parser.buf.len());
|
worst = worst.max(parser.acc.len());
|
||||||
assert!(
|
assert!(
|
||||||
parser.buf.len() < MAX_AU_BYTES,
|
parser.acc.len() < MAX_AU_BYTES,
|
||||||
"reassembly buffer exceeded the AU-length ceiling: {} >= {}",
|
"reassembly buffer exceeded the AU-length ceiling: {} >= {}",
|
||||||
parser.buf.len(),
|
parser.acc.len(),
|
||||||
MAX_AU_BYTES
|
MAX_AU_BYTES
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
parser.buf.len() <= MAX_TRUEHD_BUF,
|
parser.acc.len() <= MAX_TRUEHD_BUF,
|
||||||
"reassembly buffer exceeded cap: {} > {}",
|
"reassembly buffer exceeded cap: {} > {}",
|
||||||
parser.buf.len(),
|
parser.acc.len(),
|
||||||
MAX_TRUEHD_BUF
|
MAX_TRUEHD_BUF
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1813,7 +1821,7 @@ mod tests {
|
|||||||
fn ac3_frame_at_head_needs_more_when_buffer_short() {
|
fn ac3_frame_at_head_needs_more_when_buffer_short() {
|
||||||
// < 6 bytes buffered → NeedMore (can't read the AC-3 header).
|
// < 6 bytes buffered → NeedMore (can't read the AC-3 header).
|
||||||
let mut parser = TrueHdParser::new();
|
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.
|
// Drive through parse: a short 0x0B77 head must wait, not emit.
|
||||||
let f = parser.parse(&make_pes(vec![0x0B, 0x77, 0x00], Some(0)));
|
let f = parser.parse(&make_pes(vec![0x0B, 0x77, 0x00], Some(0)));
|
||||||
assert!(f.is_empty());
|
assert!(f.is_empty());
|
||||||
@@ -2014,4 +2022,27 @@ mod tests {
|
|||||||
assert_eq!(truehd_sample_rate_hz(info.format_info), Some(96000));
|
assert_eq!(truehd_sample_rate_hz(info.format_info), Some(96000));
|
||||||
assert_eq!(info.is_atmos, Some(true));
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user