mux: fix DVD subtitle/audio track collision, PGS/VobSub flush, unify TS codec table

Subtitle/DVD output-corruption + stream-mapping coverage fixes.

1. DVD subtitle/audio track-mapping collision (CRITICAL). The PS path
   routed 0xBD private-stream packets to a track via (sub_id & 0x1F)+1,
   so VobSub subtitle sub-id 0x20+j aliased audio track j+1: subtitle
   PES was fed to the AC-3 parser and the real subtitle track got
   nothing. Route by the canonical DVD PID instead via a new
   PsPacket::dvd_pid() that mirrors scan_dvd_titles' PID assignment
   (video 0xE0, audio 0xBD00+i, subtitle 0x20+j), then look up the
   track in pid_to_track. Fixed identically at all three sites
   (pipelined_stream consume_ps, disc.rs live feed, disc.rs EOF flush).
   Unmappable/unmapped packets now WARN instead of silently dropping.

2. PGS flush() missing. PgsParser inherited the no-op default flush, so
   the last subtitle of every PGS track (emitted only when a following
   PCS arrives) was dropped at EOF. Implemented flush() to drain the
   pending display set (duration_ns: None for the trailing block).

3. DVD VobSub multi-PES SPU not reassembled. A subpicture unit larger
   than one PES spans multiple PES (only the head carries a PTS).
   DvdSubParser is now stateful: it buffers per sub-stream until the
   leading 2-byte SPU_size is satisfied, inherits the head PTS, and
   emits one Frame. flush() drains a truncated trailing SPU at EOF.

4. One-table hygiene. scan_streams had a duplicate stream_type->Codec
   table that had drifted from Codec::from_coding_type (missing 0x80
   LPCM, 0x85 mapped to DTS-HD MA vs HR, etc.). scan_streams now uses
   from_coding_type plus a new Codec::kind()/CodecKind category split,
   so the two mappings can never diverge. Silent drops in
   scan_streams and bluray STN parsing now WARN with PID + type.

Tests: dvd_pid mapping + subtitle/audio collision regression, PGS
final-subtitle flush, VobSub multi-PES reassembly + EOF flush,
scan_streams 0x80 LPCM via from_coding_type.
This commit is contained in:
MattJackson
2026-06-06 21:33:11 -07:00
parent 7d58ba7b08
commit cfc12774f2
8 changed files with 519 additions and 152 deletions
+43
View File
@@ -117,6 +117,25 @@ impl CodecParser for PgsParser {
out
}
fn flush(&mut self) -> Vec<Frame> {
// A display set is only emitted when the *next* PCS arrives
// (either an empty clear PCS or a replacing display PCS). At
// end-of-stream there is no follower, so without this the last
// subtitle of every PGS track would be silently dropped. Emit
// the pending set with no duration — the trailing block lingers
// until end of file, which is exactly the desired behavior for
// the final on-screen subtitle (see the module doc).
match self.pending.take() {
Some((start_pts, data)) => vec![Frame {
pts_ns: start_pts,
keyframe: true,
data,
duration_ns: None,
}],
None => Vec::new(),
}
}
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
@@ -218,6 +237,30 @@ mod tests {
assert_eq!(frames.len(), 1);
}
#[test]
fn flush_emits_final_pending_subtitle() {
let mut parser = PgsParser::new();
// Display PCS at PTS 90000 — buffered as pending, no follower.
let display = pcs_bytes(1);
let frames = parser.parse(&make_pes(display.clone(), Some(90000)));
assert!(frames.is_empty(), "display PCS should be pending");
// EOF: without flush() this last subtitle would be dropped.
let frames = parser.flush();
assert_eq!(frames.len(), 1, "final pending subtitle must flush");
assert_eq!(frames[0].pts_ns, 1_000_000_000);
assert_eq!(frames[0].data, display);
// Trailing block lingers to EOF — no duration per module doc.
assert_eq!(frames[0].duration_ns, None);
}
#[test]
fn flush_with_nothing_pending_is_empty() {
let mut parser = PgsParser::new();
assert!(parser.flush().is_empty());
}
#[test]
fn codec_private_none() {
let parser = PgsParser::new();