mux: make B1 concealment decode-clean on every gap shape
Closes the three residual holes where a concealed/lost gap could still let a dangling-reference frame reach the muxer (degraded/undecryptable-disc path only; clean rips are byte-identical and untouched). Root cause: the discontinuity signal was reconstructed from the 4-bit continuity counter and applied per-PES, both of which are lossy. Three coordinated changes: 1. CC-INDEPENDENT marker. fill_null_ts_unit now tags its NULL packets with an adaptation-field discontinuity_indicator; the demuxer recognises a 0x1FFF packet carrying it as a concealed gap and forces a discontinuity on every tracked PID (the lost unit's PID is unknowable). This survives a loss that is an exact multiple of 16 packets (CC aliases to in-sequence — hole 3) and a loss at a PID's very start (no prior CC — hole 4); it also drops any open, potentially-truncated partial PES. 2. PUSI ATTRIBUTION. A gap landing on a PES boundary now flags the PES STARTING after it, not the one flushed at the boundary (hole 1) — stamping the pre-gap frame could arm-then-disarm the gate on a keyframe and admit the real post-gap inter frame. 3. PER-FRAME signal. codec::Frame gains `discontinuity`; each parser propagates it onto the first post-gap frame. MPEG-2 buffers whole GOPs asynchronously, so it associates the gap by ES OFFSET (like PTS/source), landing it on the exact post-gap picture mid-GOP (hole 2) — a per-PES flag stamped the previous picture. consume_ts (and the EOF flush drain) gate on frame.discontinuity. Tests: CC-independent marker with in-sequence CC + leading-loss; PUSI attribution flags the post-gap PES; MPEG-2 offset-mark stamps the post-gap picture through GOP reorder, not the previous one. Existing B1 gate + EOF tests still green (2270 lib tests).
This commit is contained in:
+22
-12
@@ -217,18 +217,23 @@ pub fn aacs_unit_still_ciphertext(unit: &[u8]) -> bool {
|
||||
/// Zero-filling such a unit is wrong at the TS layer: a run of `0x00` bytes
|
||||
/// carries no `0x47` sync, so the demuxer loses packet framing and can mis-parse
|
||||
/// the *next* unit if a stray `0x47` appears mid-zero. Instead we lay down 32
|
||||
/// well-formed BD source packets, each a TS null packet (PID `0x1FFF`):
|
||||
/// well-formed BD source packets, each a TS null packet (PID `0x1FFF`) carrying
|
||||
/// an adaptation-field **discontinuity_indicator**:
|
||||
///
|
||||
/// ```text
|
||||
/// [4-byte TP_extra_header = 0][47 1F FF 10][184 bytes 0xFF stuffing]
|
||||
/// [4-byte TP_extra_header = 0][47 1F FF 20 B7 80 + 182 bytes 0xFF stuffing]
|
||||
/// ^sync ^PID ^AF-only ^af_len=183 ^disc_indicator
|
||||
/// ```
|
||||
///
|
||||
/// The demuxer stays byte-synced on the 192-byte stride, and because PID
|
||||
/// `0x1FFF` matches no elementary stream every null packet is silently dropped —
|
||||
/// so the *video/audio* PID simply loses these packets. That shows up downstream
|
||||
/// as a continuity-counter gap on the real PID, which the TS assembler already
|
||||
/// turns into a dropped partial PES (see `mux::ts`), the foundation B1 builds on.
|
||||
/// This NEVER emits ciphertext and is lossless framing, not fabricated content.
|
||||
/// `0x1FFF` matches no elementary stream every null packet is silently dropped.
|
||||
/// The discontinuity_indicator is the B1 loss SIGNAL: `mux::ts` recognises a
|
||||
/// `0x1FFF` packet with that bit set as a concealed gap and forces a discontinuity
|
||||
/// on every tracked PID's next PES (the codec consumer then drops forward to the
|
||||
/// next keyframe). This is CC-INDEPENDENT — unlike the real PID's continuity
|
||||
/// counter it survives a loss that is an exact multiple of 16 packets, or a loss
|
||||
/// at a PID's very start. NEVER emits ciphertext; lossless framing, not fabricated
|
||||
/// content.
|
||||
pub fn fill_null_ts_unit(unit: &mut [u8]) {
|
||||
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
|
||||
let mut off = 0;
|
||||
@@ -236,14 +241,19 @@ pub fn fill_null_ts_unit(unit: &mut [u8]) {
|
||||
// TP_extra_header (arrival timestamp / copy-control) — zero is fine; the
|
||||
// demuxer never reads it for a PID it does not track.
|
||||
unit[off..off + 4].fill(0);
|
||||
// 188-byte TS null packet: sync, PID 0x1FFF (no PUSI/TEI), payload-only
|
||||
// with continuity counter 0.
|
||||
// 188-byte TS null packet: sync, PID 0x1FFF (no PUSI/TEI).
|
||||
unit[off + 4] = TS_SYNC; // 0x47
|
||||
unit[off + 5] = 0x1F; // PID high (top 5 bits of 0x1FFF, flags clear)
|
||||
unit[off + 6] = 0xFF; // PID low
|
||||
unit[off + 7] = 0x10; // adaptation=01 (payload only), CC=0
|
||||
// Stuffing: 0xFF is the conventional null-packet payload fill.
|
||||
unit[off + 8..off + PKT].fill(0xFF);
|
||||
// adaptation_field_control = 0b10 (AF only, no payload), CC = 0.
|
||||
unit[off + 7] = 0x20;
|
||||
// adaptation_field_length = 183: the AF (its flags byte + 182 stuffing)
|
||||
// fills the rest of the 188-byte packet.
|
||||
unit[off + 8] = 0xB7;
|
||||
// AF flags: discontinuity_indicator (0x80) — the concealed-gap signal.
|
||||
unit[off + 9] = 0x80;
|
||||
// Stuffing: 0xFF is the conventional adaptation-field fill.
|
||||
unit[off + 10..off + PKT].fill(0xFF);
|
||||
off += PKT;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user