Files
libfreemkv/src/mux/au_assembly.rs
T
Matthew Jackson 5559987325 test(mux): replace a false-green discontinuity test with the property it named
a_signalled_discontinuity_survives_a_backstop_discard asserted that a
SOURCE-signalled discontinuity on discarded bytes still reaches the AU
that follows. It did not test that. Deleting the disc_marks push, or the
mark-retirement loop inside discard_gap_before, left it passing.

The mechanism: the `discontinuity = true` rode the FIRST over-cap push,
which still has the next AU's delimiter at buf[0] — so it force-flushes
as an over-long AU rather than discarding, and THAT AU consumes the
mark. The assertion's `.find(|x| x.data.contains(&0x22))` then filters
it out, and the flag it reads comes entirely from `pending_gap`, set by
the second push's backstop. Behaviourally identical to the test 40 lines
above it, under a name promising something else.

I wrote it this morning, in the same commit that fixed a different test
for having a fixture that never reached the code it named, while
cataloguing that exact shape. Third instance today of writing the bug I
was hunting.

The two mechanisms cannot be isolated in one fixture — a fragment that
trips the backstop sets pending_gap regardless — so they now get one
test each. The replacement drives disc_marks end to end with no backstop
involved: a flagged fragment that carries a complete AU and is emitted,
not discarded. Nothing else pinned that path. Removing the disc_marks
push reds it.

Found by the round-9 opus escalation over test quality, dispatched
because the sonnet pass over the same 17,000 lines of new test code
returned zero findings.
2026-07-30 20:07:11 -07:00

1407 lines
62 KiB
Rust

//! Access-unit assembly — a codec-parser helper.
//!
//! The contract a codec parser converts is `PES → access units (Frames)`. A
//! *transport* stream hands the parser one AU per PES for free (BD aligns one
//! access unit per PES; the TS demuxer reassembles to the
//! `payload_unit_start_indicator`). A *program* stream does not — the PS muxer
//! chops the elementary stream into fixed-size PES fragments with no AU
//! alignment, and only the first fragment of an AU carries a PTS. So a parser
//! that assumes one-AU-per-PES (h264/hevc/vc1, written against TS) mis-frames a
//! program stream, while `mpeg2` — the DVD/PS codec — must reassemble across PES.
//!
//! [`AuAssembler`] is that reassembly, factored out so EVERY program-stream video
//! parser shares one implementation instead of hand-rolling the buffer. The
//! h264/hevc/vc1 parsers ([`Mode::StartCode`] / [`Mode::Vc1`]) and the MPEG-2
//! parser ([`Mode::Mpeg2`], via [`AuAssembler::mpeg2`]) all drive it. It buffers
//! PES-fragment bytes and emits one AU per codec AU boundary, carrying the
//! AU-start timing/source forward. Since the boundary is a codec start code, it
//! lives with the codec parser (which picks the marker); only the generic
//! buffering + timing-carry is shared here.
//!
//! This is *inside* the parser, not a pipeline stage: the pipeline stays
//! `Demuxer → PES → Parser → Frames`, and the demuxer stays codec-agnostic. Every
//! stream a parser sees runs through one of these — self-framing codecs (MPEG-2,
//! audio) use [`Mode::Passthrough`] so the parser code path is uniform.
use crate::disc::Codec;
use crate::pes::SourcePos;
use std::collections::VecDeque;
/// Safety cap on a single in-progress access unit. A real coded picture is far
/// below this; a stream that never yields a second AU boundary is force-flushed
/// at the cap rather than buffering without bound on hostile/corrupt input.
const MAX_AU_BUFFER: usize = 8 * 1024 * 1024;
/// Cap on buffered timing/discontinuity marks. A real access unit spans a few
/// hundred PES fragments at most; this bounds the mark deques so a run of
/// zero-length (or start-code-free) timed fragments — which grow no buffer bytes
/// and so never trip the `MAX_AU_BUFFER` mark-prune — cannot accumulate marks
/// without bound on hostile/corrupt disc input.
const MAX_MARKS: usize = 64 * 1024;
/// One AU-complete unit drained from the buffer: its elementary-stream bytes plus
/// the timing/source/discontinuity of the fragment that opened the AU.
pub(crate) struct AssembledAu {
pub data: Vec<u8>,
pub pts: Option<i64>,
pub dts: Option<i64>,
pub source: Option<SourcePos>,
pub discontinuity: bool,
}
/// VC-1 (SMPTE 421M Annex E) BDU start-code suffixes, `00 00 01 <type>`.
const VC1_FRAME: u8 = 0x0D; // coded picture
const VC1_ENTRY: u8 = 0x0E; // entry-point header
const VC1_SEQ: u8 = 0x0F; // sequence header
/// MPEG-2 (ISO/IEC 13818-2) start-code suffixes, `00 00 01 <type>`.
const MP2_PICTURE: u8 = 0x00; // picture_start_code
const MP2_SEQ: u8 = 0xB3; // sequence_header_code
const MP2_GOP: u8 = 0xB8; // group_start_code
/// How a stream's fragments become AU-complete units.
#[derive(Clone, Copy)]
enum Mode {
/// Split the elementary stream on the codec's single AU-delimiter start code
/// `00 00 01 <marker>` (H.264 AUD `0x09`, HEVC AUD `0x46`). Every AU opens with
/// exactly that code, so a plain split is correct.
StartCode(u8),
/// VC-1 has no single AU delimiter: an access unit is a `[sequence header?]
/// [entry point?][frame][slices…]` group. The sequence-header (`0x0F`) and
/// entry-point (`0x0E`) BDUs precede the frame (`0x0D`) they belong to, so a
/// plain `0x0D` split would glue them onto the *previous* AU and strip every
/// I-frame of its headers. The boundary is instead the next `0x0F`/`0x0E`/`0x0D`
/// start code that follows a frame already seen in the current AU.
Vc1,
/// MPEG-2 access unit: `[sequence header?][GOP header?][picture][slices…]`.
/// Structurally identical to [`Mode::Vc1`] — the sequence (`0xB3`) and GOP
/// (`0xB8`) headers precede the picture (`0x00`) they introduce, so the
/// boundary is the next picture / sequence / GOP start code that follows a
/// picture already seen. Slice (`0x01..=0xAF`), extension (`0xB5`),
/// user-data (`0xB2`) and sequence-end (`0xB7`) codes are NOT boundaries.
Mpeg2,
/// The codec self-frames (MPEG-2 reassembles in its own parser; audio resyncs
/// on syncwords), so each fragment passes straight through as one unit. Lets
/// the caller run EVERY stream through an assembler with no per-codec branch.
Passthrough,
}
/// A timing/source mark taken at the absolute stream offset of a fragment that
/// carried it, so it survives `buf.drain(..)` and can be attributed to the AU
/// whose byte range contains it.
struct Mark {
off: u64,
pts: Option<i64>,
dts: Option<i64>,
source: Option<SourcePos>,
}
/// Reassembles PES fragments into AU-complete units. One per stream; stateful
/// across `push` calls.
pub(crate) struct AuAssembler {
mode: Mode,
/// Buffered elementary-stream bytes not yet emitted as a complete AU.
buf: Vec<u8>,
/// Absolute stream offset of `buf[0]`, so marks (taken at absolute offsets)
/// survive `buf.drain(..)`.
base: u64,
/// Timing/source marks, in fragment order.
marks: VecDeque<Mark>,
/// Absolute offsets of fragments flagged with an upstream discontinuity.
disc_marks: VecDeque<u64>,
/// A `MAX_AU_BUFFER` backstop discard happened and no AU has been emitted
/// since. Sticky rather than an offset mark, because the bytes it refers to
/// no longer exist: the discard is followed by a pre-sync trim that would
/// retire any mark placed at the new base, and the gap must outlive that.
/// Consumed by the next AU to emit. See `discard_gap_before`.
pending_gap: bool,
/// Incremental boundary-scan cursor: the offset into `buf` up to which the
/// current AU has already been searched for its end without finding one. Each
/// `push` resumes the boundary search from here instead of rescanning the
/// whole buffer, so reassembling one AU split across N PES fragments costs
/// O(AU bytes) total, not O(AU bytes²/fragment). Reset to 0 whenever `buf[0]`
/// moves (an AU drained, or leading bytes dropped).
scan_pos: usize,
/// Whether the current AU has already contained a coded frame/picture — the
/// state the VC-1/MPEG-2 boundary rule carries across a resumed scan (their
/// boundary is "the next opener after a frame is already seen"). Meaningless
/// for `Mode::StartCode`. Reset with `scan_pos`.
seen_unit: bool,
/// Pre-sync opener-search cursor: the offset up to which the buffer has been
/// searched for the FIRST AU opener with none found. Resumes the opener scan
/// so a long run of junk with no start code (hostile/corrupt input) costs
/// O(bytes) total, not O(buffer) per push. Reset when `buf[0]` moves.
opener_pos: usize,
/// Test-only: how many times `take_front` fell back to the COPY path. The
/// handover is the whole point of `take_front`, so "did it actually fire" is a
/// property to MEASURE, not to reason about. See
/// `handover_survives_a_large_au_instead_of_copying_every_later_one`.
#[cfg(test)]
copy_path_hits: usize,
}
impl AuAssembler {
/// An assembler for `codec`. Video codecs whose parsers assume AU-complete PES
/// (H.264 / HEVC / VC-1) get a [`Mode::StartCode`] assembler; MPEG-2 (self-
/// reassembles) and audio/subtitle codecs (self-framing) get [`Mode::Passthrough`]
/// so callers can run every stream through this uniformly.
pub(crate) fn for_codec(codec: Codec) -> Self {
let mode = match codec {
Codec::H264 => Mode::StartCode(0x09), // access_unit_delimiter NAL (type 9)
Codec::Hevc => Mode::StartCode(0x46), // AUD NAL (type 35 → (35 << 1) = 0x46)
Codec::Vc1 => Mode::Vc1, // frame + preceding seq/entry headers
_ => Mode::Passthrough,
};
Self {
mode,
// Passthrough never writes `buf` (one fragment → one unit); only the
// reassembling modes need reserve. Avoids ~256 KiB per audio/subtitle
// stream (and every TS/BD stream, which never feeds the assembler).
buf: match mode {
Mode::Passthrough => Vec::new(),
_ => Vec::with_capacity(256 * 1024),
},
base: 0,
marks: VecDeque::new(),
disc_marks: VecDeque::new(),
pending_gap: false,
scan_pos: 0,
seen_unit: false,
opener_pos: 0,
#[cfg(test)]
copy_path_hits: 0,
}
}
/// An assembler that reassembles MPEG-2 access units. The MPEG-2 parser owns
/// one of these directly (rather than hand-rolling the buffer): the demux
/// layer runs MPEG-2 through [`Mode::Passthrough`] and hands each fragment to
/// the parser, which feeds them here to be reframed on picture boundaries.
pub(crate) fn mpeg2() -> Self {
Self {
mode: Mode::Mpeg2,
buf: Vec::with_capacity(128 * 1024),
base: 0,
marks: VecDeque::new(),
disc_marks: VecDeque::new(),
pending_gap: false,
scan_pos: 0,
seen_unit: false,
opener_pos: 0,
#[cfg(test)]
copy_path_hits: 0,
}
}
/// Feed one PES fragment the caller OWNS; return every AU now complete. For
/// a self-framing (`Passthrough`) stream the payload is MOVED straight into
/// the emitted unit with no copy — the common DVD/HD-DVD case (MPEG-2 video,
/// all audio). A buffering mode copies into `buf` exactly as [`Self::push`].
pub(crate) fn push_owned(
&mut self,
data: Vec<u8>,
pts: Option<i64>,
dts: Option<i64>,
source: Option<SourcePos>,
discontinuity: bool,
) -> Vec<AssembledAu> {
if matches!(self.mode, Mode::Passthrough) {
return vec![AssembledAu {
data,
pts,
dts,
source,
discontinuity,
}];
}
self.push(&data, pts, dts, source, discontinuity)
}
/// Feed one PES fragment (borrowed); return every AU that is now complete.
pub(crate) fn push(
&mut self,
data: &[u8],
pts: Option<i64>,
dts: Option<i64>,
source: Option<SourcePos>,
discontinuity: bool,
) -> Vec<AssembledAu> {
// Self-framing codecs pass through unchanged — one fragment, one unit,
// its own timing. (This is exactly today's behaviour for mpeg2/audio.)
if matches!(self.mode, Mode::Passthrough) {
return vec![AssembledAu {
data: data.to_vec(),
pts,
dts,
source,
discontinuity,
}];
}
let off = self.base + self.buf.len() as u64;
if pts.is_some() || dts.is_some() || source.is_some() {
self.marks.push_back(Mark {
off,
pts,
dts,
source,
});
// Backstop: the `buf`-size cap prunes marks only when bytes accumulate.
// A run of zero-length (or start-code-free) timed fragments grows no
// bytes, so bound the deque directly — drop the oldest (stalest) mark,
// which belongs to an already-emitted or lost AU. A real AU spans far
// fewer fragments than this cap.
if self.marks.len() > MAX_MARKS {
self.marks.pop_front();
}
}
if discontinuity {
self.disc_marks.push_back(off);
if self.disc_marks.len() > MAX_MARKS {
self.disc_marks.pop_front();
}
}
self.buf.extend_from_slice(data);
self.drain(false)
}
/// Emit the trailing in-progress AU at end of stream (no following boundary).
pub(crate) fn flush(&mut self) -> Vec<AssembledAu> {
if matches!(self.mode, Mode::Passthrough) {
return Vec::new();
}
self.drain(true)
}
fn drain(&mut self, force: bool) -> Vec<AssembledAu> {
if matches!(self.mode, Mode::Passthrough) {
return Vec::new();
}
let mut out = Vec::new();
loop {
// Locate the AU start code that opens the buffered run (resumes from
// opener_pos so an unsynced junk run is scanned once, not per push).
let Some(a0) = self.au_opener_resumable() else {
// No AU boundary buffered. Bound memory: drop all but a 3-byte
// tail (enough to catch a start-code prefix straddling the cut)
// once over the cap; otherwise wait for more data.
if self.buf.len() > MAX_AU_BUFFER {
let drop = self.buf.len() - 3;
self.buf.drain(..drop);
self.base += drop as u64;
self.reset_scan();
self.discard_gap_before(self.base);
}
break;
};
if a0 > 0 {
// Leading bytes before the first AU boundary are a partial AU from
// before we synced (or junk) — discard them and any stale marks.
self.buf.drain(..a0);
self.base += a0 as u64;
self.reset_scan();
self.drop_marks_before(self.base);
continue;
}
// The AU runs from here (buf[0]) to the NEXT AU boundary. The search
// resumes from `scan_pos` (bytes already searched with no boundary),
// so one AU spread across many fragments is scanned once, not per push.
let end = match self.au_boundary_resumable() {
Some(next) => next,
// No next boundary yet: on EOF (or over-cap backstop) the rest of
// the buffer is this AU; otherwise wait for more data.
None if force => self.buf.len(),
None if self.buf.len() > MAX_AU_BUFFER => self.buf.len(),
None => break,
};
if end == 0 {
break;
}
let end_abs = self.base + end as u64;
// The AU's own timing/source: take the FIRST Some of each field
// across every mark in this AU's range [base, end_abs), independently
// — one PES fragment may carry the source while a later fragment of
// the same AU carries the PTS (and vice versa), so reading only the
// front mark would drop the other field. This restores the semantics
// of the pre-consolidation separate pts/source mark deques.
let (mut pts, mut dts, mut source) = (None, None, None);
while self.marks.front().is_some_and(|m| m.off < end_abs) {
let m = self.marks.pop_front().unwrap();
pts = pts.or(m.pts);
dts = dts.or(m.dts);
source = source.or(m.source);
}
// A backstop discard is a gap in its own right, independent of any
// upstream signal: bytes were thrown away, so this AU does not
// continue the last one emitted.
let mut discontinuity = std::mem::take(&mut self.pending_gap);
if self.disc_marks.front().is_some_and(|&o| o < end_abs) {
discontinuity = true;
}
while self.disc_marks.front().is_some_and(|&o| o < end_abs) {
self.disc_marks.pop_front();
}
let data = self.take_front(end);
self.base += end as u64;
self.reset_scan();
out.push(AssembledAu {
data,
pts,
dts,
source,
discontinuity,
});
}
out
}
/// Detach `buf[..end]` as the emitted AU's own `Vec` and leave `buf` holding
/// the tail.
///
/// The AU's bytes are HANDED OVER — `buf`'s allocation becomes the returned
/// `Vec` and a fresh buffer (pre-sized to the same capacity, so the next AU
/// accumulates without re-growing) takes its place holding only the short
/// tail. `buf[..end].to_vec()` + `drain(..end)` instead copied every AU out
/// in full: on a UHD HEVC title that is a whole-frame memcpy (hundreds of KB)
/// per coded picture, ~200k times, for bytes that are about to be discarded
/// from `buf` anyway.
///
/// The allocation COUNT is unchanged (one per AU either way — the frame `Vec`
/// before, the replacement buffer now), so the only difference is the copy
/// that no longer happens. Nothing depends on `buf` keeping its identity: the
/// only state tied to `buf[0]`'s position is `base`/`scan_pos`/`opener_pos`,
/// which the caller updates immediately after.
///
/// Falls back to a copy when the buffer's capacity is far larger than the AU
/// (a small AU after a multi-MB one): handing over would otherwise attach an
/// oversized idle allocation to a small frame for as long as the frame queues
/// downstream, trading a copy for resident memory.
///
/// That fallback must not become permanent. `buf`'s capacity used to be a
/// one-way high-water mark — the replacement buffer was created with
/// `cap.max(tail_len)`, and the copy path's `drain` also preserves `cap` — so
/// once ONE large AU had been assembled, every later smaller AU satisfied
/// `cap > 2*end` and took the copy path forever. On a UHD HEVC title the first
/// IDR grows `buf` to ~4-8 MB, after which each ~200-400 KB P/B AU paid a
/// whole-AU allocation plus a whole-AU memcpy plus a tail memmove for ~99% of
/// the ~200,000 coded pictures — tens of GB of exactly the memcpy this handover
/// exists to remove. So the copy path now also RELEASES the high-water
/// capacity, which re-arms the handover for the next AU: one copy after a size
/// step down, not one per frame forever.
fn take_front(&mut self, end: usize) -> Vec<u8> {
let cap = self.buf.capacity();
let tail_len = self.buf.len() - end;
if cap > end.saturating_mul(2) {
#[cfg(test)]
{
self.copy_path_hits += 1;
}
let data = self.buf[..end].to_vec();
self.buf.drain(..end);
// Shrink toward what this AU actually needed (the tail plus room for
// another AU of about this size). Only the short tail is copied, and it
// brings `cap` back under the `2*end` threshold so the next AU of this
// size hands over instead of copying.
self.buf.shrink_to(end.max(tail_len));
return data;
}
// Replacement buffer: enough for the tail plus room to accumulate the next
// AU of about this size. NOT `cap`, which would re-pin the high-water mark.
let mut tail = Vec::with_capacity(end.max(tail_len));
tail.extend_from_slice(&self.buf[end..]);
let mut data = std::mem::replace(&mut self.buf, tail);
data.truncate(end);
data
}
/// Reset the incremental boundary-scan cursor. Called whenever `buf[0]` moves
/// (an AU drained, or leading bytes discarded) so the next scan starts fresh
/// from the new AU opener.
fn reset_scan(&mut self) {
self.scan_pos = 0;
self.seen_unit = false;
self.opener_pos = 0;
}
/// Locate the first AU opener in `buf`, resuming the search from `opener_pos`
/// (bytes already searched with no opener) so a long unsynced run costs
/// O(bytes) total, not O(buffer) per push. Advances `opener_pos` on a miss.
fn au_opener_resumable(&mut self) -> Option<usize> {
match au_opener_from(self.mode, &self.buf, self.opener_pos) {
Some(o) => Some(o),
None => {
// Nothing yet; next call resumes here (back up 3 for a straddling
// start-code prefix). Never advance past what is searchable.
self.opener_pos = self.buf.len().saturating_sub(3).max(self.opener_pos);
None
}
}
}
/// Find the end of the AU that opens at `buf[0]`, resuming from `scan_pos`
/// (and, for VC-1/MPEG-2, the carried `seen_unit`) instead of rescanning the
/// whole buffer. On no boundary yet, advances `scan_pos`/`seen_unit` so the
/// next call continues where this one stopped. Equivalent result to a
/// from-scratch whole-buffer scan, but O(total AU bytes) across all pushes.
fn au_boundary_resumable(&mut self) -> Option<usize> {
match self.mode {
Mode::StartCode(marker) => {
// Stateless: the AU ends at the next delimiter after the opener at
// buf[0]. Resume from the furthest searched offset (never before 4,
// to skip the opening delimiter). find_start_code needs 4 bytes, so
// back up 3 to catch a code straddling the previous buffer end.
let from = self.scan_pos.max(4);
match find_start_code(&self.buf, from, marker) {
Some(e) => Some(e),
None => {
self.scan_pos = self.buf.len().saturating_sub(3).max(from);
None
}
}
}
Mode::Vc1 => self.scan_unit_boundary(VC1_FRAME, &[VC1_ENTRY, VC1_SEQ]),
Mode::Mpeg2 => self.scan_unit_boundary(MP2_PICTURE, &[MP2_SEQ, MP2_GOP]),
Mode::Passthrough => None,
}
}
/// Resumable form of the VC-1/MPEG-2 boundary rule: scan from `scan_pos`,
/// carrying `seen_unit`; the AU ends at the next `frame` / `header` start code
/// once a frame is already seen. Advances `scan_pos`/`seen_unit` when no
/// boundary is found so the next push continues, not restarts.
fn scan_unit_boundary(&mut self, frame: u8, headers: &[u8]) -> Option<usize> {
let buf = &self.buf;
let mut i = self.scan_pos;
let mut seen = self.seen_unit;
while i + 4 <= buf.len() {
if buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 {
let c = buf[i + 3];
let is_frame = c == frame;
if (is_frame || headers.contains(&c)) && i > 0 && seen {
// The AU ends at the next frame/header once a frame is seen.
return Some(i);
}
if is_frame {
seen = true;
}
i += 4;
} else {
i += 1;
}
}
// No boundary yet. Persist the scan state so the next append resumes here
// rather than rescanning from 0 (the i+=4 stride is preserved exactly).
self.scan_pos = i;
self.seen_unit = seen;
None
}
/// Retire every mark that falls before `off`, timing and discontinuity
/// alike.
///
/// This is the STREAM-START case: bytes ahead of the first AU boundary are
/// the tail of an access unit that began before we had sync, and there is
/// no prior AU for them to be discontinuous *from*. Carrying a mark forward
/// here would arm the resync gate at the head of every title and drop its
/// first GOP.
fn drop_marks_before(&mut self, off: u64) {
while self.marks.front().is_some_and(|m| m.off < off) {
self.marks.pop_front();
}
while self.disc_marks.front().is_some_and(|&o| o < off) {
self.disc_marks.pop_front();
}
}
/// Retire stale timing marks before `off` and record that a GAP occurred
/// there.
///
/// This is the BACKSTOP case: `MAX_AU_BUFFER` bytes accumulated with no AU
/// start code in them, so the run is unusable and gets thrown away. Unlike
/// the stream-start trim above, there IS a prior AU here, and whatever
/// follows definitively does not continue it — a decoder handed the next
/// picture would resolve its references against frames separated from it by
/// megabytes of discarded data.
///
/// So the discard is itself a discontinuity, whether or not the source
/// signalled one. It is recorded as a sticky flag rather than an offset
/// mark because a mark placed at the new base would be retired moments
/// later by the pre-sync trim that follows resync — the gap has to outlive
/// the bytes that caused it. It arms the resync gate, which drops to the
/// next keyframe instead of emitting a picture with dangling references.
///
/// Timing marks before `off` are still retired — they describe bytes that
/// no longer exist, and the AU that eventually emits takes its PTS from the
/// fragment that actually opened it.
fn discard_gap_before(&mut self, off: u64) {
self.drop_marks_before(off);
self.pending_gap = true;
}
}
/// Offset of the start code that opens the next AU in `buf` (at or after 0), or
/// `None` if no AU-opening start code is buffered yet.
fn au_opener_from(mode: Mode, buf: &[u8], from: usize) -> Option<usize> {
match mode {
Mode::StartCode(marker) => find_start_code(buf, from, marker),
// Any of the three AU-opening BDU types opens a VC-1 access unit.
Mode::Vc1 => find_vc1_start(buf, from),
// A sequence header, GOP header, or picture opens an MPEG-2 access unit.
Mode::Mpeg2 => find_mpeg2_start(buf, from),
Mode::Passthrough => None,
}
}
/// Find the next `00 00 01 <marker>` start code at or after `from`.
fn find_start_code(buf: &[u8], from: usize, marker: u8) -> Option<usize> {
let mut i = from;
while i + 4 <= buf.len() {
if buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 && buf[i + 3] == marker {
return Some(i);
}
i += 1;
}
None
}
/// Find the next VC-1 AU-opening BDU start code (`00 00 01` followed by a
/// sequence header, entry point, or frame) at or after `from`.
fn find_vc1_start(buf: &[u8], from: usize) -> Option<usize> {
let mut i = from;
while i + 4 <= buf.len() {
if buf[i] == 0
&& buf[i + 1] == 0
&& buf[i + 2] == 1
&& matches!(buf[i + 3], VC1_FRAME | VC1_ENTRY | VC1_SEQ)
{
return Some(i);
}
i += 1;
}
None
}
/// Find the next MPEG-2 AU-opening start code (`00 00 01` followed by a picture,
/// sequence header, or GOP header) at or after `from`.
fn find_mpeg2_start(buf: &[u8], from: usize) -> Option<usize> {
let mut i = from;
while i + 4 <= buf.len() {
if buf[i] == 0
&& buf[i + 1] == 0
&& buf[i + 2] == 1
&& matches!(buf[i + 3], MP2_PICTURE | MP2_SEQ | MP2_GOP)
{
return Some(i);
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const AUD: &[u8] = &[0x00, 0x00, 0x01, 0x09]; // H.264 access-unit delimiter
fn au(payload: u8, len: usize) -> Vec<u8> {
let mut v = AUD.to_vec();
v.extend(std::iter::repeat_n(payload, len));
v
}
#[test]
fn self_framing_codecs_pass_through_each_fragment_unchanged() {
// MPEG-2 (self-reassembles in its parser) and audio (syncword resync) run
// through a Passthrough assembler: every fragment emerges immediately as
// one unit with its own timing — byte-identical to today's path.
for codec in [Codec::Mpeg2, Codec::Ac3Plus, Codec::Dts, Codec::Lpcm] {
let mut a = AuAssembler::for_codec(codec);
let out = a.push(&[1, 2, 3, 4], Some(42), None, None, false);
assert_eq!(
out.len(),
1,
"{codec:?} passes each fragment straight through"
);
assert_eq!(out[0].data, vec![1, 2, 3, 4]);
assert_eq!(out[0].pts, Some(42));
assert!(a.flush().is_empty(), "passthrough buffers nothing");
}
}
#[test]
fn video_codecs_reassemble_across_fragments() {
// H.264 buffers: one fragment is NOT a complete AU on its own.
let mut a = AuAssembler::for_codec(Codec::H264);
assert!(
a.push(&[0, 0, 1, 0x09, 0xAB], Some(1), None, None, false)
.is_empty(),
"holds an AU until the next boundary"
);
}
#[test]
fn one_au_split_across_fragments_reassembles_with_start_pts() {
// A single AU (AUD + 100 bytes) arrives as three fragments; only the
// first carries a PTS. It must emit exactly ONE AU with that PTS.
let mut a = AuAssembler::for_codec(Codec::H264);
let full = au(0xAB, 100);
assert!(
a.push(&full[..40], Some(9000), None, None, false)
.is_empty()
);
assert!(a.push(&full[40..80], None, None, None, false).is_empty());
assert!(a.push(&full[80..], None, None, None, false).is_empty());
let out = a.flush();
assert_eq!(out.len(), 1);
assert_eq!(
out[0].pts,
Some(9000),
"AU carries its START pts, not 0/None"
);
assert_eq!(out[0].data, full);
}
#[test]
fn two_aus_emit_when_the_second_boundary_arrives() {
let mut a = AuAssembler::for_codec(Codec::H264);
let au1 = au(0x11, 50);
let au2 = au(0x22, 60);
let mut buf = au1.clone();
buf.extend_from_slice(&au2);
// AU1 + AU2's opening AUD → AU1 completes, tagged pts1.
let out = a.push(&buf[..au1.len() + 4], Some(1000), None, None, false);
assert_eq!(out.len(), 1);
assert_eq!(out[0].data, au1);
assert_eq!(out[0].pts, Some(1000));
a.push(&buf[au1.len() + 4..], None, None, None, false);
let out2 = a.flush();
assert_eq!(out2.len(), 1);
assert_eq!(out2[0].data, au2);
}
#[test]
fn au_merges_pts_and_source_from_different_fragments() {
// One fragment of an AU may carry the source stamp while a later fragment
// of the SAME AU carries the PTS (each PES gets a source; only the anchor
// gets a PTS). The AU must keep BOTH — reading only the front mark would
// drop whichever field the first fragment lacked.
let src = crate::pes::SourcePos::at_byte(4242);
let mut a = AuAssembler::for_codec(Codec::H264);
let full = au(0xAB, 80);
// Fragment 1: source only, no PTS.
assert!(a.push(&full[..30], None, None, Some(src), false).is_empty());
// Fragment 2 (same AU): PTS only, no source.
assert!(
a.push(&full[30..], Some(9000), None, None, false)
.is_empty()
);
let out = a.flush();
assert_eq!(out.len(), 1);
assert_eq!(out[0].pts, Some(9000), "PTS from the 2nd fragment retained");
assert_eq!(
out[0].source.map(|s| s.byte),
Some(4242),
"source from the 1st fragment retained"
);
}
#[test]
fn discontinuity_flag_attaches_to_the_au_it_opens() {
// A discontinuity-flagged fragment opens AU2; that flag must land on AU2,
// not AU1 (the B1 resync gate keys off it).
let mut a = AuAssembler::for_codec(Codec::H264);
let au1 = au(0x11, 30);
let au2 = au(0x22, 30);
a.push(&au1, Some(1), None, None, false);
// AU2 arrives flagged; its opening AUD completes AU1 first.
let out = a.push(&au2, Some(2), None, None, true);
assert_eq!(out.len(), 1, "AU1 completes when AU2's boundary arrives");
assert!(!out[0].discontinuity, "AU1 is NOT the discontinuity");
let out2 = a.flush();
assert_eq!(out2.len(), 1);
assert!(out2[0].discontinuity, "AU2 carries the discontinuity");
}
#[test]
fn leading_bytes_before_first_au_are_discarded() {
let mut a = AuAssembler::for_codec(Codec::H264);
let mut buf = vec![0xFF, 0xFF, 0xFF, 0xFF];
buf.extend_from_slice(&au(0x33, 20));
a.push(&buf, Some(500), None, None, false);
let out = a.flush();
assert_eq!(out.len(), 1);
assert_eq!(out[0].data, au(0x33, 20), "leading junk dropped, AU intact");
}
// ── VC-1 AU grouping ──────────────────────────────────────────────────
fn bdu(ty: u8, payload: u8, len: usize) -> Vec<u8> {
let mut v = vec![0x00, 0x00, 0x01, ty];
v.extend(std::iter::repeat_n(payload, len));
v
}
#[test]
fn vc1_i_frame_keeps_its_preceding_seq_and_entry_headers() {
// An I-frame AU is [seq 0x0F][entry 0x0E][frame 0x0D][slices]; a following
// P-frame is just [frame 0x0D][slices]. A plain 0x0D split would strand the
// seq/entry headers on the P-frame's AU — the decode bug. The VC-1 mode must
// group them with the I-frame that follows them.
let mut a = AuAssembler::for_codec(Codec::Vc1);
let mut iframe = bdu(VC1_SEQ, 0xAA, 8);
iframe.extend(bdu(VC1_ENTRY, 0xBB, 6));
iframe.extend(bdu(VC1_FRAME, 0xCC, 20)); // frame + slice bytes
let pframe = bdu(VC1_FRAME, 0xDD, 15);
// Feed the I-frame; it stays open until the P-frame's boundary arrives.
assert!(a.push(&iframe, Some(9000), None, None, false).is_empty());
let out = a.push(&pframe, Some(9376), None, None, false);
assert_eq!(out.len(), 1, "I-frame AU completes at the P-frame boundary");
assert_eq!(out[0].data, iframe, "I-frame AU retains seq+entry+frame");
assert_eq!(out[0].pts, Some(9000));
let tail = a.flush();
assert_eq!(tail.len(), 1);
assert_eq!(tail[0].data, pframe, "P-frame is its own AU");
assert_eq!(tail[0].pts, Some(9376));
}
#[test]
fn vc1_consecutive_frames_split_one_per_au() {
// Back-to-back frames with no headers between them each form their own AU.
let mut a = AuAssembler::for_codec(Codec::Vc1);
let f1 = bdu(VC1_FRAME, 0x11, 30);
let f2 = bdu(VC1_FRAME, 0x22, 40);
let mut both = f1.clone();
both.extend_from_slice(&f2);
both.extend(bdu(VC1_FRAME, 0x33, 4)); // opening boundary of a 3rd frame
let out = a.push(&both, Some(1), None, None, false);
assert_eq!(out.len(), 2, "two complete frames emit");
assert_eq!(out[0].data, f1);
assert_eq!(out[1].data, f2);
}
#[test]
fn vc1_entry_point_without_seq_header_still_groups_with_frame() {
// Mid-GOP open points can carry an entry-point header with no sequence
// header; it must still attach to the frame that follows it.
let mut a = AuAssembler::for_codec(Codec::Vc1);
let mut au = bdu(VC1_ENTRY, 0xEE, 5);
au.extend(bdu(VC1_FRAME, 0xFF, 12));
let mut done = a.push(&au, Some(500), None, None, false);
// Next frame's opening boundary closes the entry+frame AU.
done.extend(a.push(&bdu(VC1_FRAME, 0x00, 4), None, None, None, false));
done.extend(a.flush());
assert_eq!(done.len(), 2);
assert_eq!(done[0].data, au, "entry+frame grouped");
assert_eq!(done[0].pts, Some(500));
}
// ── MPEG-2 AU grouping ────────────────────────────────────────────────
#[test]
fn mpeg2_keeps_seq_and_gop_headers_with_their_picture() {
// A GOP-opening AU is [seq 0xB3][gop 0xB8][picture 0x00][slices]; the next
// picture (no headers) is its own AU. The seq/GOP headers must stay with
// the picture they introduce, not glue onto the previous AU.
let mut a = AuAssembler::mpeg2();
let mut gop = bdu(MP2_SEQ, 0xAA, 10);
gop.extend(bdu(MP2_GOP, 0xBB, 8));
gop.extend(bdu(MP2_PICTURE, 0xCC, 20)); // picture + slice bytes
let pic2 = bdu(MP2_PICTURE, 0xDD, 15);
assert!(a.push(&gop, Some(9000), None, None, false).is_empty());
let out = a.push(&pic2, Some(9376), None, None, false);
assert_eq!(
out.len(),
1,
"first AU completes at the next picture boundary"
);
assert_eq!(out[0].data, gop, "AU retains seq + GOP + picture");
assert_eq!(out[0].pts, Some(9000));
let tail = a.flush();
assert_eq!(tail.len(), 1);
assert_eq!(tail[0].data, pic2, "second picture is its own AU");
assert_eq!(tail[0].pts, Some(9376));
}
#[test]
fn mpeg2_slice_codes_are_not_au_boundaries() {
// Slice start codes (0x01..=0xAF) inside a picture must not split the AU.
let mut a = AuAssembler::mpeg2();
let mut pic = bdu(MP2_PICTURE, 0x11, 4);
pic.extend(bdu(0x01, 0x22, 10)); // slice 1
pic.extend(bdu(0xAF, 0x33, 10)); // slice 175 (max slice code)
let next = bdu(MP2_PICTURE, 0x44, 4); // opening boundary of the next AU
let out = a.push(&[pic.clone(), next].concat(), Some(1), None, None, false);
assert_eq!(out.len(), 1, "slices stay inside the one picture AU");
assert_eq!(out[0].data, pic, "AU spans the picture and all its slices");
}
#[test]
fn mpeg2_reassembles_one_picture_split_across_fragments() {
// A picture split across three PES fragments; only the first carries a PTS.
let mut a = AuAssembler::mpeg2();
let full = bdu(MP2_PICTURE, 0xEE, 100);
assert!(a.push(&full[..40], Some(500), None, None, false).is_empty());
assert!(a.push(&full[40..80], None, None, None, false).is_empty());
assert!(a.push(&full[80..], None, None, None, false).is_empty());
let out = a.flush();
assert_eq!(out.len(), 1);
assert_eq!(out[0].pts, Some(500), "AU carries its START pts");
assert_eq!(out[0].data, full);
}
/// Split `stream` into fragments of `frag` bytes, push them through the given
/// assembler mode, and return the reassembled AU byte-payloads.
fn reassemble_with(mut a: AuAssembler, stream: &[u8], frag: usize) -> Vec<Vec<u8>> {
let mut out = Vec::new();
let mut i = 0;
while i < stream.len() {
let end = (i + frag).min(stream.len());
for au in a.push(&stream[i..end], None, None, None, false) {
out.push(au.data);
}
i = end;
}
for au in a.flush() {
out.push(au.data);
}
out
}
#[test]
fn resumable_boundary_matches_from_scratch_across_all_fragmentations() {
// The incremental scan_pos cursor must produce byte-identical AUs to a
// whole-buffer rescan, at EVERY fragment granularity (this is what makes
// the O(n) resume equivalent to the old O(n^2) from-scratch scan). Build a
// multi-AU stream per codec, reassemble it fed 1 byte at a time up to
// whole, and require one canonical result.
let h264 = {
let mut s = au(0x11, 40); // AU1 (AUD + payload)
s.extend(au(0x22, 70)); // AU2
s.extend(au(0x33, 25)); // AU3
s
};
let vc1 = {
let mut s = bdu(VC1_SEQ, 0xAA, 8);
s.extend(bdu(VC1_ENTRY, 0xBB, 6));
s.extend(bdu(VC1_FRAME, 0xCC, 50)); // I-frame AU
s.extend(bdu(VC1_FRAME, 0xDD, 30)); // P-frame AU
s.extend(bdu(VC1_FRAME, 0xEE, 20)); // P-frame AU
s
};
let mpeg2 = {
let mut s = bdu(MP2_SEQ, 0xAA, 10);
s.extend(bdu(MP2_GOP, 0xBB, 8));
s.extend(bdu(MP2_PICTURE, 0xCC, 60)); // GOP-opening picture AU
s.extend(bdu(MP2_PICTURE, 0xDD, 40)); // picture AU
s
};
// (label, stream, assembler factory). MPEG-2 uses the dedicated mpeg2()
// assembler (Mode::Mpeg2); the AUD/VC-1 codecs use for_codec().
type MakeAsm = fn() -> AuAssembler;
let cases: [(&str, &[u8], MakeAsm); 3] = [
("h264", &h264, || AuAssembler::for_codec(Codec::H264)),
("vc1", &vc1, || AuAssembler::for_codec(Codec::Vc1)),
("mpeg2", &mpeg2, AuAssembler::mpeg2),
];
for (label, stream, make) in cases {
let whole = reassemble_with(make(), stream, stream.len());
assert!(!whole.is_empty(), "{label}: baseline produced AUs");
for frag in 1..=stream.len() {
let got = reassemble_with(make(), stream, frag);
assert_eq!(
got, whole,
"{label}: fragmented at {frag} differs from whole-buffer reassembly"
);
}
}
}
#[test]
fn marks_deques_stay_bounded_on_zero_length_timed_fragments() {
// A run of zero-length fragments that each carry a PTS (or a
// discontinuity) grows no buffer bytes, so the buf-size cap never prunes
// the mark deques. The MAX_MARKS backstop must bound them regardless.
let mut a = AuAssembler::for_codec(Codec::H264);
for i in 0..(MAX_MARKS * 2) {
a.push(&[], Some(i as i64), None, None, true);
}
assert!(
a.marks.len() <= MAX_MARKS,
"marks bounded at MAX_MARKS, got {}",
a.marks.len()
);
assert!(
a.disc_marks.len() <= MAX_MARKS,
"disc_marks bounded at MAX_MARKS, got {}",
a.disc_marks.len()
);
}
/// The 8 MiB backstop throws away a start-code-free run as unusable. The
/// AU that eventually emits after that discard MUST be marked
/// discontinuous, whether or not the source ever signalled a
/// discontinuity: megabytes of the stream are simply gone, so the next
/// picture cannot resolve its references against the last one that was
/// emitted.
///
/// `discontinuity` is what arms the resync gate downstream
/// (`resync.rs`, driven from `mux/disc.rs`), which drops to the next
/// keyframe rather than emitting a picture with dangling references. If the
/// flag is retired with the discarded bytes, the gate never arms and the
/// broken picture goes out — a silent corruption, which is the one class of
/// loss this crate refuses to have.
#[test]
fn a_backstop_discard_marks_the_next_au_discontinuous() {
let mut a = AuAssembler::for_codec(Codec::H264);
// A clean AU first, so there IS a prior AU to be discontinuous from.
let first = au(0x11, 64);
let mut stream = first.clone();
stream.extend_from_slice(AUD);
let out = a.push(&stream, Some(1000), None, None, false);
assert_eq!(out.len(), 1, "the first AU emits normally");
assert!(
!out[0].discontinuity,
"an ordinary AU at the head of a clean run is continuous"
);
// Now start-code-free junk past the cap. The FIRST over-cap run still
// has the next AU's delimiter at buf[0], so it is force-flushed as an
// (over-long) access unit — nothing is discarded and nothing is lost.
// Only once the buffer holds no opener at all does the backstop throw
// bytes away, which is the case this test is about.
let junk = vec![0xAB; MAX_AU_BUFFER + 4096];
a.push(&junk, Some(2000), None, None, false);
a.push(&junk, Some(2100), None, None, false);
// Resync: a fresh AU, followed by the delimiter that closes it.
let mut resumed = au(0x22, 64);
resumed.extend_from_slice(AUD);
let out = a.push(&resumed, Some(3000), None, None, false);
let au2 = out
.iter()
.find(|x| x.data.contains(&0x22))
.expect("the post-gap AU must emit");
assert!(
au2.discontinuity,
"the AU following an 8 MiB backstop discard follows a gap and must \
say so; without the flag the resync gate never arms and a picture \
with dangling references is emitted as if it were sound"
);
}
/// The opposite case, and the reason the two call sites are separate.
///
/// Bytes ahead of the FIRST access-unit delimiter are the tail of an AU
/// that began before we had sync. There is no prior AU for them to be
/// discontinuous from, so retiring the marks there is right — and
/// necessary: marking the first AU of every title discontinuous would arm
/// the resync gate at the head of each one and drop its opening GOP.
#[test]
fn a_stream_start_trim_does_not_mark_the_first_au_discontinuous() {
let mut a = AuAssembler::for_codec(Codec::H264);
// Junk BEFORE the first delimiter — a partial AU from before sync.
// Small enough that the backstop never fires; this is the a0 > 0 path.
let mut stream = vec![0xCD; 512];
stream.extend_from_slice(&au(0x33, 64));
stream.extend_from_slice(AUD);
let out = a.push(&stream, Some(1000), None, None, false);
let first = out.first().expect("the first synced AU must emit");
assert!(
!first.discontinuity,
"trimming pre-sync bytes at stream start is not a gap in the \
stream; flagging it would drop the opening GOP of every title"
);
}
/// A source-signalled discontinuity reaches the AU it opens.
///
/// This is the `disc_marks` path — the ORIGINAL mechanism, distinct from
/// the sticky `pending_gap` the backstop sets. Nothing else pins it: the
/// two tests above drive `pending_gap`, and a mark placed on a fragment
/// that is later discarded is retired by design.
///
/// Deliberately NOT combined with the backstop. A previous version of this
/// test signalled the discontinuity on the first over-cap push and asserted
/// the flag on the AU after the discard — but that first run still has the
/// next AU's delimiter at `buf[0]`, so it force-flushes as an over-long AU,
/// and THAT AU consumes the mark. The assertion was then satisfied entirely
/// by `pending_gap`, making the test a duplicate of the one above it under
/// a name promising something else. The two mechanisms cannot be isolated
/// in one fixture, so they get one test each.
#[test]
fn a_source_signalled_discontinuity_reaches_the_au_it_opens() {
let mut a = AuAssembler::for_codec(Codec::H264);
// A clean AU first, so there is a prior AU and the gate has somewhere
// to be discontinuous FROM.
let mut first = au(0x11, 64);
first.extend_from_slice(AUD);
let out = a.push(&first, Some(1000), None, None, false);
assert_eq!(out.len(), 1);
assert!(!out[0].discontinuity, "a clean run is continuous");
// The source flags this fragment as following a gap. It carries the
// body of the next AU and its closing delimiter, so it is emitted
// rather than discarded — the mark must ride through to it.
let mut second = au(0x22, 64);
second.extend_from_slice(AUD);
let out = a.push(&second, Some(2000), None, None, true);
let au2 = out
.iter()
.find(|x| x.data.contains(&0x22))
.expect("the flagged AU must emit");
assert!(
au2.discontinuity,
"a discontinuity the SOURCE signalled must reach the AU whose bytes \
carried it; this is the disc_marks path and no other test drives it"
);
}
#[test]
fn over_cap_without_boundary_force_flushes() {
let mut a = AuAssembler::for_codec(Codec::H264);
let big = au(0x44, MAX_AU_BUFFER + 16);
let emitted = a.push(&big, Some(1), None, None, false);
assert!(
!emitted.is_empty(),
"over-cap AU is force-flushed, not buffered forever"
);
}
/// MEASURED: a drained AU must be HANDED the accumulation buffer's
/// allocation, not copied out of it. The emitted `Vec`'s data pointer is the
/// buffer's own pointer — which is only true if no full-frame copy happened.
/// (`buf[..end].to_vec()` allocates fresh, so the pointers differ.) One
/// whole-AU memcpy per coded picture is ~200k memcpys of a few hundred KB
/// each on a UHD feature.
#[test]
fn drained_au_takes_over_the_buffer_allocation_without_copying() {
let mut a = AuAssembler::for_codec(Codec::H264);
// An AU large enough that the buffer's capacity is not >2x its size (the
// small-AU copy path exists so a small frame cannot carry an oversized
// idle allocation downstream).
let au1 = au(0x11, 400 * 1024);
let au2 = au(0x22, 400 * 1024);
let mut stream = au1.clone();
stream.extend_from_slice(&au2);
// Push everything except the final byte of AU2's delimiter, so no AU has
// been emitted yet but the buffer holds the whole of AU1.
a.push(&stream[..au1.len() + 3], Some(1), None, None, false);
let before = a.buf.as_ptr();
let cap_before = a.buf.capacity();
let out = a.push(
&stream[au1.len() + 3..au1.len() + 4],
None,
None,
None,
false,
);
assert_eq!(out.len(), 1);
assert_eq!(
out[0].data, au1,
"handover must preserve the AU bytes exactly"
);
assert_eq!(
out[0].data.as_ptr(),
before,
"the emitted AU must own the buffer's allocation (no whole-frame copy)"
);
// The replacement buffer keeps room for another AU of about this size, so
// the next AU does not re-grow — but it is NOT pinned to the OLD capacity,
// which would make `buf` a permanent high-water mark and send every later
// smaller AU down the copy path (see
// `handover_survives_a_large_au_instead_of_copying_every_later_one`).
assert!(
a.buf.capacity() >= au1.len(),
"replacement buffer must fit another AU of this size: {} < {}",
a.buf.capacity(),
au1.len()
);
assert!(
a.buf.capacity() <= cap_before,
"replacement buffer must never EXCEED the old capacity"
);
assert_eq!(a.buf.len(), 4, "the buffer holds only AU2's delimiter tail");
}
/// MEASURED: `take_front`'s copy fallback must not become permanent.
///
/// `buf`'s capacity used to be a one-way high-water mark, and the copy path's
/// `drain` preserves it, so after ONE large AU every later smaller AU satisfied
/// `cap > 2*end` and copied forever. On a UHD HEVC title the first IDR grows
/// `buf` to multiple MB, after which ~99% of the ~200,000 coded pictures each
/// paid a whole-AU allocation + whole-AU memcpy + tail memmove — tens of GB of
/// exactly the copy the handover exists to remove. Counted at the copy path
/// itself: one copy is expected right after the size step down; a per-frame
/// copy is the bug.
#[test]
fn handover_survives_a_large_au_instead_of_copying_every_later_one() {
let mut a = AuAssembler::for_codec(Codec::H264);
// Production shape: BD-TS aligns one access unit per PES, so each `push`
// carries about one AU and the buffer holds ~one AU at a time. One large AU
// (the IDR) followed by a run of much smaller ones (P/B frames). Each AU is
// pushed with the NEXT AU's opener so the previous one closes.
const SMALL: usize = 64 * 1024;
let mut pending = au(0x11, 2 * 1024 * 1024);
for i in 0..20u8 {
let next = au(0x30 + i, SMALL);
// Append the next AU's 4-byte opener to close `pending`, push, and
// carry the rest of `next` forward.
pending.extend_from_slice(&next[..4]);
a.push(&pending, Some(1), None, None, false);
pending = next[4..].to_vec();
}
let hits = a.copy_path_hits;
assert!(
hits <= 2,
"the copy fallback must re-arm the handover, not fire for every AU \
after a large one: {hits} copies over 20 access units"
);
}
// ── AU-opener detection: the per-mode start-code rule ─────────────────
//
// `au_opener_from` is the SECOND implementation of a rule each codec parser
// also encodes (h264 `NAL_AUD`, hevc `NAL_AUD`, vc1 `SC_*`, mpeg2
// `PICTURE_CODE`/`SEQ_HEADER_CODE`/`GOP_CODE`). Two independent copies of one
// rule drift; these cases pin this copy to the normative byte values and to
// the codes that are explicitly NOT openers, so a drift shows up here.
/// The opener offset must be the position of the real start code, never a
/// fixed 0. A constant `Some(0)` makes every pre-sync run of junk bytes look
/// like the head of an access unit, so the first AU of every stream that does
/// not begin exactly on a start code is emitted with junk glued to its front.
#[test]
fn au_opener_from_locates_the_real_start_code_per_codec() {
// Junk that contains a start-code PREFIX but no opener suffix, so a
// scanner that stopped at `00 00 01` alone would answer wrongly.
let junk: &[u8] = &[0xFF, 0x00, 0x00, 0x01, 0x67, 0xAA];
let cases: &[(Mode, u8, &str)] = &[
// ISO/IEC 14496-10 §7.4.1: nal_unit_type 9 = access unit delimiter,
// and nal_ref_idc shall be 0 for it, so the header byte is 0x09.
(Mode::StartCode(0x09), 0x09, "H.264 AUD"),
// ITU-T H.265 §7.4.2.2: nal_unit_type 35 = AUD_NUT. The first NAL
// header byte is forbidden_zero_bit(1) | nal_unit_type(6) |
// nuh_layer_id MSB(1) = (35 << 1) = 0x46 on the base layer.
(Mode::StartCode(0x46), 0x46, "HEVC AUD"),
// SMPTE 421M Annex E BDU types.
(Mode::Vc1, VC1_SEQ, "VC-1 sequence header"),
(Mode::Vc1, VC1_ENTRY, "VC-1 entry point"),
(Mode::Vc1, VC1_FRAME, "VC-1 frame"),
// ISO/IEC 13818-2 §6.2.1 Table 6-1 start code values.
(Mode::Mpeg2, MP2_PICTURE, "MPEG-2 picture"),
(Mode::Mpeg2, MP2_SEQ, "MPEG-2 sequence header"),
(Mode::Mpeg2, MP2_GOP, "MPEG-2 GOP header"),
];
for &(mode, code, what) in cases {
let mut buf = junk.to_vec();
buf.extend_from_slice(&[0x00, 0x00, 0x01, code, 0x5A]);
assert_eq!(
au_opener_from(mode, &buf, 0),
Some(junk.len()),
"{what}: opener must be found at the start code, not at 0"
);
// `from` must actually skip: searching past the only opener finds none.
assert_eq!(
au_opener_from(mode, &buf, junk.len() + 1),
None,
"{what}: the resume cursor must be honoured"
);
}
}
/// Start codes that are NOT access-unit openers must not be reported as one.
/// Treating a slice or an extension header as an AU start splits one coded
/// picture into several frames, each missing its picture header.
#[test]
fn non_opening_start_codes_are_not_au_openers() {
// ISO/IEC 13818-2 Table 6-1: slice (0x01..=0xAF), user data (0xB2),
// extension (0xB5), sequence end (0xB7) all appear INSIDE an access unit.
for code in [0x01u8, 0xAF, 0xB2, 0xB5, 0xB7] {
let buf = [0x00, 0x00, 0x01, code, 0x11, 0x22];
assert_eq!(
au_opener_from(Mode::Mpeg2, &buf, 0),
None,
"MPEG-2 start code {code:#04x} must not open an access unit"
);
}
// SMPTE 421M: slice (0x0B) and field (0x0C) BDUs belong to the frame
// already in progress; end-of-sequence (0x0A) opens nothing.
for code in [0x0Au8, 0x0B, 0x0C] {
let buf = [0x00, 0x00, 0x01, code, 0x11, 0x22];
assert_eq!(
au_opener_from(Mode::Vc1, &buf, 0),
None,
"VC-1 BDU {code:#04x} must not open an access unit"
);
}
// H.264: an SPS (7) / PPS (8) / IDR slice (5) is not the AU DELIMITER the
// StartCode mode splits on.
for code in [0x05u8, 0x67, 0x68] {
let buf = [0x00, 0x00, 0x01, code, 0x11, 0x22];
assert_eq!(au_opener_from(Mode::StartCode(0x09), &buf, 0), None);
}
// Passthrough never frames — the codec self-frames.
assert_eq!(
au_opener_from(Mode::Passthrough, &[0, 0, 1, 0x09, 0xAA], 0),
None
);
}
/// `au_opener_resumable` must return the true offset AND advance
/// `opener_pos` only over bytes that cannot hide a straddling start code.
/// A constant `Some(0)` short-circuits both.
#[test]
fn au_opener_resumable_reports_the_real_offset_and_resumes_safely() {
let mut a = AuAssembler::for_codec(Codec::H264);
// A junk run with no opener: None, and the cursor parks 3 bytes back so a
// start code split across the append boundary is still found.
a.buf.extend_from_slice(&[0xFFu8; 32]);
assert_eq!(a.au_opener_resumable(), None, "no opener in a junk run");
assert_eq!(
a.opener_pos, 29,
"resume 3 bytes back for a straddling code"
);
// Now append a start code that STRADDLES the previous end: the first three
// bytes of `00 00 01 09` land at offsets 29..32.
a.buf.truncate(29);
a.buf.extend_from_slice(&[0x00, 0x00, 0x01, 0x09, 0x77]);
assert_eq!(
a.au_opener_resumable(),
Some(29),
"a start code straddling the previous scan end must still be found"
);
}
/// After the pre-sync bytes are discarded, the emitted AU must take the
/// timing of the fragment that ACTUALLY opened it. `drop_marks_before` is
/// what retires the discarded fragment's marks; a no-op there stamps the
/// first real access unit with the PTS and source of bytes that were thrown
/// away — a whole-title A/V sync offset, since every later frame is timed
/// relative to it.
#[test]
fn discarded_pre_sync_marks_do_not_time_the_first_access_unit() {
let src = |b: u64| SourcePos {
byte: b,
..Default::default()
};
let mut a = AuAssembler::for_codec(Codec::H264);
// Fragment 1: pre-sync junk, no start code. Carries its own PTS/source.
assert!(
a.push(&[0xFFu8; 24], Some(1_000), Some(900), Some(src(11)), false)
.is_empty()
);
// Fragment 2: the first real AU opener, with the timing that belongs to it.
assert!(
a.push(
&au(0x33, 40),
Some(2_000),
Some(1_900),
Some(src(22)),
false
)
.is_empty()
);
// Fragment 3: a second AU, closing the first.
let out = a.push(
&au(0x44, 40),
Some(3_000),
Some(2_900),
Some(src(33)),
false,
);
assert_eq!(out.len(), 1, "the first AU closes on the second opener");
assert_eq!(out[0].data, au(0x33, 40), "junk discarded, AU intact");
assert_eq!(
out[0].pts,
Some(2_000),
"the AU must take the opening fragment's PTS, not the discarded junk's"
);
assert_eq!(out[0].dts, Some(1_900), "same for DTS");
assert_eq!(
out[0].source.map(|s| s.byte),
Some(22),
"same for the source position used by the recovery map"
);
let tail = a.flush();
assert_eq!(tail.len(), 1);
assert_eq!(tail[0].pts, Some(3_000), "the second AU keeps its own PTS");
}
/// `for_codec` is the dispatch that decides whether a stream is REASSEMBLED
/// across PES fragments or passed straight through. Getting it wrong is
/// silent: an H.264/HEVC/VC-1 stream routed to `Passthrough` on a program
/// source emits one "frame" per PES fragment — a few hundred bytes of a
/// coded picture, framed as a whole access unit — and the output plays as
/// corruption, not as an error.
///
/// Each mode is identified BEHAVIOURALLY (feed a two-AU stream in two halves
/// and see whether it reassembles), so the case cannot pass by matching a
/// constant.
#[test]
fn for_codec_routes_each_video_codec_to_its_reassembly_mode() {
// Buffering codecs: a stream split mid-AU must NOT emit until the second
// AU's opener arrives, and must then emit the FIRST AU whole.
let buffering: &[(Codec, u8)] = &[
(Codec::H264, 0x09), // ISO/IEC 14496-10 §7.4.1 AUD
(Codec::Hevc, 0x46), // ITU-T H.265 §7.4.2.2 AUD_NUT, (35 << 1)
];
for &(codec, marker) in buffering {
let mut a = AuAssembler::for_codec(codec);
let mut unit = vec![0x00, 0x00, 0x01, marker];
unit.extend(std::iter::repeat_n(0x5Au8, 30));
// First half of AU 1: nothing complete yet.
assert!(
a.push(&unit[..20], Some(1), None, None, false).is_empty(),
"{codec:?} must buffer a partial access unit, not emit it"
);
assert!(
a.push(&unit[20..], None, None, None, false).is_empty(),
"{codec:?} must hold AU 1 until the next opener"
);
// AU 2's opener closes AU 1.
let out = a.push(&unit, Some(2), None, None, false);
assert_eq!(out.len(), 1, "{codec:?} emits exactly one AU");
assert_eq!(out[0].data, unit, "{codec:?} reassembles AU 1 whole");
assert_eq!(out[0].pts, Some(1), "{codec:?} carries the AU-start PTS");
}
// VC-1 buffers too, on its own boundary rule (no single AU delimiter).
let mut a = AuAssembler::for_codec(Codec::Vc1);
let frame = bdu(VC1_FRAME, 0x77, 30);
assert!(a.push(&frame, Some(1), None, None, false).is_empty());
assert_eq!(
a.push(&frame, Some(2), None, None, false).len(),
1,
"VC-1 emits AU 1 when the next frame BDU opens AU 2"
);
// Self-framing codecs pass each fragment through immediately — the same
// half-AU input that the buffering modes held back comes straight out.
for codec in [Codec::Mpeg2, Codec::Ac3, Codec::TrueHd, Codec::Pgs] {
let mut a = AuAssembler::for_codec(codec);
let out = a.push(&[0x00, 0x00, 0x01, 0x09, 0xAA], Some(7), None, None, false);
assert_eq!(out.len(), 1, "{codec:?} must pass through, not buffer");
assert_eq!(out[0].pts, Some(7));
assert!(a.flush().is_empty(), "{codec:?} buffers nothing at EOF");
}
}
}