mux: drop truncated partial audio frame on concealed gap
The AC-3, DTS and TrueHD parsers buffer access units across PES boundaries. At a concealed-loss gap the buffered unit is truncated: splicing post-gap bytes onto it manufactures a corrupt frame on top of the real loss (FFmpeg "Failed to decode block code(s)" / "Invalid data found" at the gap) and, for TrueHD, strands the PTS cadence into the non-monotonic audio-DTS band seen on multi-clip titles. The video parsers already handle this via the ResyncGate, but the discontinuity signal was only wired into video — audio parsers ignored pes.discontinuity and spliced across the gap. Now, when pes.discontinuity is set, each audio parser drops the partial (clears buf, and for DTS its PTS marks / pending base) so the post-gap PES re-bases a fresh unit. A lost gap degrades to a clean single-frame drop instead of a corrupt spliced frame. No effect on perfect rips: the branch only runs when concealment inserted a discontinuity marker. Adds a per-parser test feeding a partial frame then a discontinuity PES, asserting the truncated partial is dropped (not spliced) and the post-gap PTS is adopted.
This commit is contained in:
@@ -64,6 +64,17 @@ impl CodecParser for Ac3Parser {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// B1: a concealed/lost gap means the bytes held in `buf` are a TRUNCATED
|
||||
// frame. Appending the post-gap bytes would splice them into one corrupt
|
||||
// frame (wrong frame_size, bad CRC → "exponent out of range" / garbage).
|
||||
// Drop the partial and resync on the next syncword — a clean single-frame
|
||||
// gap instead of a frankenstein frame. (The video parsers carry this via
|
||||
// the ResyncGate; audio has no inter-frame refs, so dropping the spliced
|
||||
// partial is the whole fix.)
|
||||
if pes.discontinuity {
|
||||
self.buf.clear();
|
||||
}
|
||||
|
||||
// Base PTS for the FIRST frame emitted from this call. Each subsequent
|
||||
// frame in the same call advances by the previous frame's duration, so a
|
||||
// PES that carries several AC-3 frames stamps a monotonically increasing
|
||||
@@ -506,6 +517,48 @@ mod tests {
|
||||
assert_eq!(frames2[0].data.len(), 160);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discontinuity_drops_truncated_partial() {
|
||||
// B1: a partial AC-3 frame is buffered, then a concealed gap arrives
|
||||
// (PES marked discontinuity) carrying a fresh complete frame. The
|
||||
// truncated partial must be DROPPED, not spliced — otherwise the parser
|
||||
// emits one corrupt frame built from [stale partial | head of fresh] and
|
||||
// strands the tail (FFmpeg: "incomplete frame" / wrong sync).
|
||||
let mut parser = Ac3Parser::new();
|
||||
let frame_data = make_ac3_frame(0, 2); // 160 bytes, starts with 0x0B77
|
||||
|
||||
// First PES: only the first half of a frame (no boundary marker).
|
||||
let pes1 = PesPacket {
|
||||
source: None,
|
||||
pid: 0,
|
||||
pts: Some(90000),
|
||||
dts: None,
|
||||
data: frame_data[..80].to_vec(),
|
||||
discontinuity: false,
|
||||
};
|
||||
assert!(
|
||||
parser.parse(&pes1).is_empty(),
|
||||
"partial frame should not emit"
|
||||
);
|
||||
|
||||
// Concealed gap: a fresh whole frame, marked discontinuity.
|
||||
let fresh = make_ac3_frame(0, 2);
|
||||
let pes2 = PesPacket {
|
||||
source: None,
|
||||
pid: 0,
|
||||
pts: Some(99000),
|
||||
dts: None,
|
||||
data: fresh.clone(),
|
||||
discontinuity: true,
|
||||
};
|
||||
let frames = parser.parse(&pes2);
|
||||
assert_eq!(frames.len(), 1, "exactly one clean frame across the gap");
|
||||
assert_eq!(
|
||||
frames[0].data, fresh,
|
||||
"emitted frame is the fresh post-gap frame, not a spliced partial"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_garbage_before_sync() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
Reference in New Issue
Block a user