mux: B1 drop-to-keyframe resync after a concealed gap

Pairs with A2 (read-path NULL-TS concealment). When the demux assembler
sees a TS continuity gap it now stamps `discontinuity` on the next
completed PES; the codec-parse stage carries that onto a per-track
ResyncGate. After a gap on an inter-coded video track the gate drops
forward to the next IRAP/IDR keyframe so no frame with a dangling
reference reaches the muxer (an ffmpeg deep scan would otherwise report
a missing-reference / non-existing-PPS error). Audio and subtitle tracks
have no cross-frame references, so the gate is a no-op there.

- ts.rs: PesPacket gains `discontinuity`; PesAssembler tracks a sticky
  pending_discontinuity flag set on CC gap / discontinuity_indicator and
  carried to the next completed/flushed PES.
- resync.rs (new): ResyncGate — per-track arm-on-gap, drop non-keyframes
  until the next keyframe disarms and resumes. Logs the resync + drop
  count once at the keyframe.
- pipelined_stream.rs: precompute per-track is_video, apply the gate in
  consume_ts. Out-of-range track index emits as-is (defensive).

Tests: ResyncGate unit tests; ts.rs gap-stamps-discontinuity; end-to-end
B1 video-drops-to-keyframe and audio-never-drops through PipelinedPesStream.
This commit is contained in:
Matthew Jackson
2026-06-28 23:01:47 -07:00
parent 9a7be7a1a5
commit d715a0943a
17 changed files with 405 additions and 2 deletions
+13
View File
@@ -450,6 +450,7 @@ mod tests {
pts: None,
dts: None,
data: vec![],
discontinuity: false,
};
assert!(parser.parse(&pes).is_empty());
}
@@ -464,6 +465,7 @@ mod tests {
pts: Some(90000),
dts: None,
data: frame_data.clone(),
discontinuity: false,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
@@ -483,6 +485,7 @@ mod tests {
pts: Some(90000),
dts: None,
data: frame_data[..mid].to_vec(),
discontinuity: false,
};
let frames1 = parser.parse(&pes1);
assert!(frames1.is_empty(), "partial frame should not emit");
@@ -494,6 +497,7 @@ mod tests {
pts: Some(93000),
dts: None,
data: frame_data[mid..].to_vec(),
discontinuity: false,
};
let frames2 = parser.parse(&pes2);
assert_eq!(frames2.len(), 1);
@@ -512,6 +516,7 @@ mod tests {
pts: None,
dts: None,
data,
discontinuity: false,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
@@ -535,6 +540,7 @@ mod tests {
pts: Some(90000),
dts: None,
data: pes1_data,
discontinuity: false,
};
let frames1 = parser.parse(&pes1);
assert_eq!(frames1.len(), 1, "first complete frame emitted");
@@ -548,6 +554,7 @@ mod tests {
pts: Some(93000),
dts: None,
data: pes2_data,
discontinuity: false,
};
let frames2 = parser.parse(&pes2);
assert_eq!(frames2.len(), 1, "split-sync frame must be recovered");
@@ -574,6 +581,7 @@ mod tests {
pts: None,
dts: None,
data,
discontinuity: false,
};
let frames = parser.parse(&pes);
assert!(frames.is_empty());
@@ -602,6 +610,7 @@ mod tests {
pts: None,
dts: None,
data,
discontinuity: false,
};
assert!(parser.parse(&pes).is_empty());
assert_eq!(parser.buf, vec![0x0B], "lone trailing 0x0B retained");
@@ -639,6 +648,7 @@ mod tests {
pts: Some(90000),
dts: None,
data,
discontinuity: false,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 1, "frame 1 emitted in parse");
@@ -671,6 +681,7 @@ mod tests {
pts: Some(90000),
dts: None,
data,
discontinuity: false,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 2);
@@ -714,6 +725,7 @@ mod tests {
pts: Some(90000),
dts: None,
data,
discontinuity: false,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1, "only the real AC-3 frame is emitted");
@@ -1179,6 +1191,7 @@ mod tests {
pts: Some(90000),
dts: None,
data,
discontinuity: false,
}
}
}
+1
View File
@@ -482,6 +482,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
+1
View File
@@ -233,6 +233,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
+4
View File
@@ -537,6 +537,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
@@ -923,6 +924,7 @@ mod tests {
pts: Some(180000), // 2 seconds (presentation)
dts: Some(90000), // 1 second (decode)
data,
discontinuity: false,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
@@ -1232,6 +1234,7 @@ mod tests {
pts: None,
dts: Some(90000),
data: vec![0x00, 0x00, 0x01, 0x41, 0x10],
discontinuity: false,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 1);
@@ -1247,6 +1250,7 @@ mod tests {
pts: None,
dts: None,
data: vec![0x00, 0x00, 0x01, 0x41, 0x10],
discontinuity: false,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 1);
+3
View File
@@ -973,6 +973,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
@@ -2243,6 +2244,7 @@ mod tests {
pts: Some(180000), // 2 s (presentation)
dts: Some(90000), // 1 s (decode)
data,
discontinuity: false,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
@@ -2850,6 +2852,7 @@ mod tests {
d.extend_from_slice(&[0x10, 0x20]);
d
},
discontinuity: false,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 1);
+1
View File
@@ -98,6 +98,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
+1
View File
@@ -198,6 +198,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
+4
View File
@@ -707,6 +707,7 @@ mod tests {
pts: None,
dts: None,
data,
discontinuity: false,
};
let mut p = Mpeg2Parser::new();
let mut frames = Vec::new();
@@ -822,6 +823,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
@@ -1415,6 +1417,7 @@ mod tests {
pts: None,
dts: Some(90000),
data,
discontinuity: false,
};
let f = parse_then_flush(&mut parser, &pes);
assert_eq!(f[0].pts_ns, 1_000_000_000, "DTS fallback");
@@ -1428,6 +1431,7 @@ mod tests {
pts: None,
dts: None,
data: data2,
discontinuity: false,
};
let f2 = parse_then_flush(&mut parser2, &pes2);
assert_eq!(f2[0].pts_ns, 0, "no PTS/DTS → 0");
+1
View File
@@ -223,6 +223,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
+1
View File
@@ -425,6 +425,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
+4
View File
@@ -451,6 +451,7 @@ mod tests {
pts,
dts: None,
data,
discontinuity: false,
}
}
@@ -757,6 +758,7 @@ mod tests {
pts: Some(180000), // presentation
dts: Some(90000), // decode
data,
discontinuity: false,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
@@ -1007,6 +1009,7 @@ mod tests {
pts: None,
dts: Some(90000),
data: vec![0x00, 0x00, 0x01, SC_FRAME, 0x55],
discontinuity: false,
};
let f = parser.parse(&pes);
assert_eq!(f[0].pts_ns, 1_000_000_000, "DTS fallback");
@@ -1018,6 +1021,7 @@ mod tests {
pts: None,
dts: None,
data: vec![0x00, 0x00, 0x01, SC_FRAME, 0x55],
discontinuity: false,
};
let f2 = parser2.parse(&pes2);
assert_eq!(f2[0].pts_ns, 0, "no PTS/DTS → 0");