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
+95
View File
@@ -39,6 +39,39 @@ pub struct PsPacket {
pub data: Vec<u8>,
}
impl PsPacket {
/// Map this packet to the canonical DVD PID assigned by
/// `Disc::scan_dvd_titles` (`src/disc/dvd.rs`), so demux output can
/// be looked up in the title's `pid_to_track` map.
///
/// The PID space mirrors `dvd.rs` exactly:
/// - video stream id `0xE0..=0xEF` → `0xE0`
/// - private-stream-1 audio sub-id `0x80..=0x87` (AC-3),
/// `0x88..=0x8F` (DTS), `0xA0..=0xA7` (LPCM) → `0xBD00 + index`
/// - private-stream-1 subtitle sub-id `0x20..=0x3F` → `0x20 + index`
///
/// Returns `None` for stream/sub-stream combinations the DVD title
/// scanner does not assign a PID to (e.g. MPEG audio 0xC0-0xDF,
/// private stream 2, unrecognized sub-stream ranges). The caller is
/// expected to WARN-and-drop in that case rather than silently
/// mis-routing the packet.
pub fn dvd_pid(&self) -> Option<u16> {
match self.stream_id {
0xE0..=0xEF => Some(0xE0),
0xBD => match self.sub_stream_id? {
// AC-3 / DTS / LPCM audio → 0xBD00 + audio index.
s @ 0x80..=0x87 => Some(0xBD00 + (s - 0x80) as u16),
s @ 0x88..=0x8F => Some(0xBD00 + (s - 0x88) as u16),
s @ 0xA0..=0xA7 => Some(0xBD00 + (s - 0xA0) as u16),
// VobSub subtitle sub-id 0x20+j → PID 0x20+j (identity).
s @ 0x20..=0x3F => Some(s as u16),
_ => None,
},
_ => None,
}
}
}
/// MPEG-2 Program Stream demuxer.
///
/// Accepts raw PS bytes via `feed()` and produces demuxed PES packets.
@@ -549,6 +582,68 @@ mod tests {
assert_eq!(decoded, val);
}
// --- DVD PID mapping (track-routing collision regression) ---
fn mk(stream_id: u8, sub: Option<u8>) -> PsPacket {
PsPacket {
stream_id,
sub_stream_id: sub,
pts: None,
dts: None,
data: vec![0xAA],
}
}
#[test]
fn dvd_pid_matches_scanner_assignment() {
// Video → 0xE0 (matches dvd.rs VideoStream pid).
assert_eq!(mk(0xE0, None).dvd_pid(), Some(0xE0));
// AC-3 audio stream 0/1 → 0xBD00 / 0xBD01 (matches 0xBD00 + i).
assert_eq!(mk(0xBD, Some(0x80)).dvd_pid(), Some(0xBD00));
assert_eq!(mk(0xBD, Some(0x81)).dvd_pid(), Some(0xBD01));
// DTS / LPCM audio indices.
assert_eq!(mk(0xBD, Some(0x88)).dvd_pid(), Some(0xBD00));
assert_eq!(mk(0xBD, Some(0xA0)).dvd_pid(), Some(0xBD00));
// VobSub subtitle 0x20/0x21 → 0x20 / 0x21 (matches 0x20 + j).
assert_eq!(mk(0xBD, Some(0x20)).dvd_pid(), Some(0x20));
assert_eq!(mk(0xBD, Some(0x21)).dvd_pid(), Some(0x21));
// Unmappable: MPEG audio, private stream 2, bogus sub-id.
assert_eq!(mk(0xC0, None).dvd_pid(), None);
assert_eq!(mk(0xBF, None).dvd_pid(), None);
assert_eq!(mk(0xBD, Some(0x10)).dvd_pid(), None);
}
#[test]
fn subtitle_does_not_collide_with_audio_track() {
// Regression for the (sub_id & 0x1F)+1 bug: subtitle sub-id 0x20
// used to alias audio track 1. With the real PID it routes to its
// own subtitle PID (0x20), distinct from audio (0xBD00+).
let audio0 = mk(0xBD, Some(0x80)).dvd_pid().unwrap(); // 0xBD00
let sub0 = mk(0xBD, Some(0x20)).dvd_pid().unwrap(); // 0x20
assert_ne!(
audio0, sub0,
"subtitle sub-id 0x20 must NOT map to the audio PID"
);
// Mirror dvd.rs PID assignment for a title with [video, audio0,
// audio1, sub0, sub1] and confirm each PS packet lands on its
// own track via pid_to_track.
let pid_to_track: Vec<(u16, usize)> =
vec![(0xE0, 0), (0xBD00, 1), (0xBD01, 2), (0x20, 3), (0x21, 4)];
let route = |p: PsPacket| -> Option<usize> {
let pid = p.dvd_pid()?;
pid_to_track
.iter()
.find(|(x, _)| *x == pid)
.map(|(_, t)| *t)
};
assert_eq!(route(mk(0xE0, None)), Some(0));
assert_eq!(route(mk(0xBD, Some(0x80))), Some(1));
assert_eq!(route(mk(0xBD, Some(0x81))), Some(2));
assert_eq!(route(mk(0xBD, Some(0x20))), Some(3)); // sub0 → track 3, NOT 1
assert_eq!(route(mk(0xBD, Some(0x21))), Some(4)); // sub1 → track 4, NOT 2
}
// --- Helper: encode PTS for tests ---
fn encode_pts(pts: u64, marker_prefix: u8) -> [u8; 5] {