Files
libfreemkv/src/mux/ts.rs
T
MattJackson cfc12774f2 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.
2026-06-06 21:33:11 -07:00

677 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! BD Transport Stream demuxer.
//!
//! Blu-ray uses 192-byte TS packets (not standard 188):
//! - 4-byte TP_extra_header (arrival timestamp + copy permission)
//! - 188-byte standard MPEG-TS packet
//!
//! This demuxer extracts PES packets from selected PIDs, with PTS/DTS timestamps.
/// BD transport stream packet size (4-byte extra header + 188-byte TS).
const BD_TS_PACKET_SIZE: usize = 192;
/// Standard TS packet size.
const TS_PACKET_SIZE: usize = 188;
/// TS sync byte.
const SYNC_BYTE: u8 = 0x47;
/// A reassembled PES packet with timestamp info.
#[derive(Debug)]
pub struct PesPacket {
/// MPEG-TS PID this packet belongs to.
pub pid: u16,
/// Presentation timestamp in 90kHz ticks (if present).
pub pts: Option<i64>,
/// Decode timestamp in 90kHz ticks (if present).
pub dts: Option<i64>,
/// Elementary stream data (video frame, audio frame, subtitle segment, etc.).
pub data: Vec<u8>,
}
/// Per-PID PES reassembly state.
struct PesAssembler {
pid: u16,
buffer: Vec<u8>,
pts: Option<i64>,
dts: Option<i64>,
active: bool,
/// PES-header bytes still to be skipped on the next continuation
/// packet(s). A PES header (9 + PES_header_data_length, up to 264
/// bytes) can exceed a single 184-byte TS payload, spilling into the
/// following continuation packet. Those spillover bytes are NOT
/// elementary-stream data and must be skipped, or the PES start code
/// (`00 00 01 …`) and timestamp bytes get injected into the ES — for
/// HEVC/H264 that reads as a spurious start code / corrupt slice
/// payload. Tracks how many header bytes remain across packets.
header_remaining: usize,
}
/// Initial capacity for a fresh PES buffer. Sized to cover the
/// common BD-TS audio / subtitle PES outright (a few KB to ~16 KB).
/// Video PES (typically 150300 KB on UHD) will grow this via the
/// standard Vec doubling, but the doublings hit the allocator's
/// slab caches instead of the 64-page first-touch faults that the
/// previous `Vec::with_capacity(256 * 1024)` triggered on every PES
/// boundary.
const PES_BUFFER_INIT_CAP: usize = 16 * 1024;
impl PesAssembler {
fn new(pid: u16) -> Self {
Self {
pid,
buffer: Vec::with_capacity(PES_BUFFER_INIT_CAP),
pts: None,
dts: None,
active: false,
header_remaining: 0,
}
}
/// Start a new PES packet. Returns the completed previous packet (if any).
fn start(&mut self, pts: Option<i64>, dts: Option<i64>) -> Option<PesPacket> {
let completed = if self.active && !self.buffer.is_empty() {
Some(PesPacket {
pid: self.pid,
pts: self.pts,
dts: self.dts,
data: std::mem::replace(&mut self.buffer, Vec::with_capacity(PES_BUFFER_INIT_CAP)),
})
} else {
self.buffer.clear();
None
};
self.pts = pts;
self.dts = dts;
self.active = true;
completed
}
/// Append payload data to the current PES packet.
fn push(&mut self, data: &[u8]) {
if self.active {
self.buffer.extend_from_slice(data);
}
}
/// Flush remaining data as a PES packet.
fn flush(&mut self) -> Option<PesPacket> {
if self.active && !self.buffer.is_empty() {
self.active = false;
Some(PesPacket {
pid: self.pid,
pts: self.pts,
dts: self.dts,
data: std::mem::take(&mut self.buffer),
})
} else {
None
}
}
}
/// BD Transport Stream demuxer.
pub struct TsDemuxer {
assemblers: Vec<PesAssembler>,
pid_index: Vec<i16>, // PID → index into assemblers, -1 = not tracked
remainder: Vec<u8>, // leftover bytes from previous feed() call
}
impl TsDemuxer {
/// Create a new demuxer tracking the given PIDs.
///
/// Allocates a flat lookup table of `i16` slots — one per possible PID
/// up to `max(8192, max_pid + 1)`. The 8192 floor matches the BD-TS
/// 13-bit PID space (0..0x1FFF); the variable upper bound exists for
/// DVD program streams which may use 16-bit stream IDs above 8191.
/// Worst-case allocation is `u16::MAX × 2 bytes ≈ 128 KB` — bounded by
/// the type, so adversarial input can't drive this beyond predictable
/// limits. Empty `pids` yields max_pid 0; the floor still produces a
/// valid (wholly-unused) table.
pub fn new(pids: &[u16]) -> Self {
let max_pid = pids.iter().copied().max().unwrap_or(0) as usize;
let table_size = (max_pid + 1).max(8192);
let mut pid_index = vec![-1i16; table_size];
let mut assemblers = Vec::with_capacity(pids.len());
for (i, &pid) in pids.iter().enumerate() {
pid_index[pid as usize] = i as i16;
assemblers.push(PesAssembler::new(pid));
}
Self {
assemblers,
pid_index,
remainder: Vec::new(),
}
}
/// Feed a chunk of BD transport stream data. Handles non-192-byte-
/// aligned input by buffering leftover bytes between calls. Returns
/// completed PES packets.
///
/// 16 MiB ISO batches never divide evenly into 192-byte BD-TS
/// packets, so every call after the first carries a ~64-byte
/// remainder. The pre-0.24 implementation handled this by building
/// a `combined` Vec containing remainder + the entire new input —
/// a 16 MiB+ memcpy on every call. Now we splice exactly one
/// boundary packet from a stack buffer, then process the rest of
/// `data` in place. Zero-copy on the bulk path; one 192-byte copy
/// on the boundary.
pub fn feed(&mut self, data: &[u8]) -> Vec<PesPacket> {
let mut completed = Vec::with_capacity(4);
let mut offset = 0;
// Boundary packet: if a partial packet was left from the last
// call, complete it from the head of `data` without touching
// the rest of `data`.
if !self.remainder.is_empty() {
let need = BD_TS_PACKET_SIZE - self.remainder.len();
if data.len() < need {
// Still not a full packet — accumulate and wait.
self.remainder.extend_from_slice(data);
return completed;
}
let mut boundary = [0u8; BD_TS_PACKET_SIZE];
boundary[..self.remainder.len()].copy_from_slice(&self.remainder);
boundary[self.remainder.len()..].copy_from_slice(&data[..need]);
self.remainder.clear();
self.process_packet(&boundary, &mut completed);
offset = need;
}
// Aligned-packets fast path — reads directly out of `data`.
while offset + BD_TS_PACKET_SIZE <= data.len() {
let packet = &data[offset..offset + BD_TS_PACKET_SIZE];
offset += BD_TS_PACKET_SIZE;
self.process_packet(packet, &mut completed);
}
// Save leftover bytes for next call (cap at one packet to
// prevent unbounded growth on a desynchronised stream).
if offset < data.len() {
let leftover = &data[offset..];
if leftover.len() < BD_TS_PACKET_SIZE {
self.remainder.extend_from_slice(leftover);
} else {
self.remainder.clear();
}
}
completed
}
/// Demux a single 192-byte BD-TS packet (4-byte TP_extra_header +
/// 188-byte TS). Routes payload bytes into the per-PID
/// `PesAssembler`; completed PES packets are pushed onto
/// `completed` so the caller's allocation amortises across the
/// batch.
fn process_packet(&mut self, packet: &[u8], completed: &mut Vec<PesPacket>) {
// Sync byte check skips malformed packets.
if packet[4] != SYNC_BYTE {
return;
}
let ts = &packet[4..]; // 188-byte standard TS packet
let pid = (((ts[1] & 0x1F) as u16) << 8) | ts[2] as u16;
let pusi = ts[1] & 0x40 != 0; // Payload Unit Start Indicator
let adaptation = (ts[3] >> 4) & 0x03;
let idx = if (pid as usize) < self.pid_index.len() {
self.pid_index[pid as usize]
} else {
-1
};
if idx < 0 {
return;
}
let asm = &mut self.assemblers[idx as usize];
let payload_start = if adaptation == 0x03 || adaptation == 0x02 {
let af_len = ts[4] as usize;
if af_len > 183 {
return; // Malformed: AF length exceeds TS payload
}
5 + af_len
} else {
4
};
if payload_start >= TS_PACKET_SIZE {
return;
}
// adaptation == 0x02 → AF only, no payload.
if adaptation == 0x02 {
return;
}
let payload = &ts[payload_start..];
if pusi {
// `header_len` is the FULL (uncapped) PES-header length:
// 0 = malformed (payload is not a PES start), else 6/9+N.
let (pts, dts, header_len) = parse_pes_header(payload);
if let Some(prev) = asm.start(pts, dts) {
completed.push(prev);
}
if header_len == 0 {
// PUSI packet whose payload is not a valid PES start. Do
// NOT push it — those bytes are not elementary-stream data
// and would inject a spurious start code / garbage.
asm.header_remaining = 0;
} else if header_len <= payload.len() {
// Header fits in this packet (the common case).
asm.header_remaining = 0;
if header_len < payload.len() {
asm.push(&payload[header_len..]);
}
} else {
// Header spills past this packet — skip the remainder on
// the following continuation packet(s).
asm.header_remaining = header_len - payload.len();
}
} else if asm.header_remaining > 0 {
// Continuation packet still inside a PES header that spanned
// the boundary — consume header bytes before any ES data.
let skip = asm.header_remaining.min(payload.len());
asm.header_remaining -= skip;
if skip < payload.len() {
asm.push(&payload[skip..]);
}
} else {
asm.push(payload);
}
}
/// Flush all assemblers, returning any remaining PES packets.
pub fn flush(&mut self) -> Vec<PesPacket> {
let mut completed = Vec::new();
for asm in &mut self.assemblers {
if let Some(pkt) = asm.flush() {
completed.push(pkt);
}
}
completed
}
}
/// Parse a PES packet header, extracting PTS and DTS.
///
/// Returns `(pts, dts, header_len)` where `header_len` is the FULL,
/// UNCAPPED PES-header length in bytes (`9 + PES_header_data_length`, or
/// 6 for stream IDs without the standard extension). `0` signals the
/// payload is not a valid PES start (malformed / too short). The caller
/// must treat `header_len` as bytes-to-skip and carry any remainder past
/// this packet's payload into the next continuation packet — the header
/// can exceed one TS payload, and the spillover is header, not ES data.
fn parse_pes_header(data: &[u8]) -> (Option<i64>, Option<i64>, usize) {
// PES packet: 00 00 01 [stream_id] [length:2] [flags...]
if data.len() < 9 || data[0] != 0x00 || data[1] != 0x00 || data[2] != 0x01 {
return (None, None, 0);
}
let stream_id = data[3];
// Some stream IDs don't have the standard PES header extension
// (program_stream_map, padding, private_stream_2, ECM, EMM, etc.)
if stream_id == 0xBC
|| stream_id == 0xBE
|| stream_id == 0xBF
|| stream_id == 0xF0
|| stream_id == 0xF1
|| stream_id == 0xFF
{
return (None, None, 6);
}
// Standard PES header: [6] = flags1, [7] = flags2, [8] = header_data_length
if data.len() < 9 {
return (None, None, 6);
}
let pts_dts_flags = (data[7] >> 6) & 0x03;
let header_data_len = data[8] as usize;
// Full, uncapped header length. PTS/DTS (if present) live in the
// first ~19 bytes, always within this packet's payload, so they parse
// here; only the *skip* length may extend into the next packet.
let header_len = 9 + header_data_len;
let mut pts = None;
let mut dts = None;
if pts_dts_flags >= 2 && header_data_len >= 5 && data.len() >= 14 {
pts = parse_timestamp(&data[9..14]);
}
if pts_dts_flags == 3 && header_data_len >= 10 && data.len() >= 19 {
dts = parse_timestamp(&data[14..19]);
}
(pts, dts, header_len)
}
/// Parse a 5-byte PTS/DTS timestamp (33 bits in 90kHz).
/// Validates marker bits per MPEG-2 spec. Returns None on invalid encoding.
fn parse_timestamp(data: &[u8]) -> Option<i64> {
if data.len() < 5 {
return None;
}
// Validate marker bits: byte 2 bit 0 and byte 4 bit 0 must be 1
if (data[2] & 0x01) == 0 || (data[4] & 0x01) == 0 {
return None;
}
let b0 = data[0] as i64;
let b1 = data[1] as i64;
let b2 = data[2] as i64;
let b3 = data[3] as i64;
let b4 = data[4] as i64;
Some(((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1)
}
// ============================================================
// Stream scanning (PAT/PMT → stream list)
// ============================================================
/// Scan BD-TS data for streams by parsing PAT and PMT tables.
/// Returns None if no valid program is found.
pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
use crate::disc::*;
// Pass 1: find PMT PID from PAT
let mut pat_pmt_pid: Option<u16> = None;
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == 0 && pusi {
let payload_start = offset + 4 + 4;
if payload_start + 12 < data.len() {
let pointer = data[payload_start] as usize;
let pat_start = payload_start + 1 + pointer;
if pat_start + 12 < data.len() && data[pat_start] == 0x00 {
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8)
| data[pat_start + 2] as usize;
let entries_start = pat_start + 8;
if section_len < 4 {
offset += BD_TS_PACKET_SIZE;
continue;
}
let entries_end = pat_start + 3 + section_len - 4;
let mut e = entries_start;
while e + 4 <= data.len() && e < entries_end {
let prog_num = ((data[e] as u16) << 8) | data[e + 1] as u16;
let p = (((data[e + 2] & 0x1F) as u16) << 8) | data[e + 3] as u16;
if prog_num != 0 {
pat_pmt_pid = Some(p);
break;
}
e += 4;
}
}
}
}
offset += BD_TS_PACKET_SIZE;
}
let pmt_pid = pat_pmt_pid?;
// Pass 2: parse PMT for stream entries
let mut streams = Vec::new();
offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == pmt_pid && pusi {
let payload_start = offset + 4 + 4;
if payload_start + 1 >= data.len() {
offset += BD_TS_PACKET_SIZE;
continue;
}
let pointer = data[payload_start] as usize;
let pmt_start = payload_start + 1 + pointer;
if pmt_start + 12 >= data.len() {
offset += BD_TS_PACKET_SIZE;
continue;
}
if data[pmt_start] != 0x02 {
offset += BD_TS_PACKET_SIZE;
continue;
}
let section_len =
(((data[pmt_start + 1] & 0x0F) as usize) << 8) | data[pmt_start + 2] as usize;
// section_length counts the bytes after this field, including the
// trailing 4-byte CRC; `< 4` would underflow `end` below. Guard it
// exactly like the PAT parser above.
if section_len < 4 {
offset += BD_TS_PACKET_SIZE;
continue;
}
let prog_info_len =
(((data[pmt_start + 10] & 0x0F) as usize) << 8) | data[pmt_start + 11] as usize;
let mut pos = pmt_start + 12 + prog_info_len;
// Clamp the section end to the buffer; a malformed section_len or
// prog_info_len must never drive reads past `data`.
let end = (pmt_start + 3 + section_len - 4).min(data.len());
while pos + 5 <= data.len() && pos < end {
let stream_type = data[pos];
let es_pid = (((data[pos + 1] & 0x1F) as u16) << 8) | data[pos + 2] as u16;
let es_info_len = (((data[pos + 3] & 0x0F) as usize) << 8) | data[pos + 4] as usize;
// Single source of truth for stream_type → Codec: reuse
// `Codec::from_coding_type` (the same table the BD STN /
// disc scanner uses) so the two mappings can never drift.
// We only retain the category (video/audio/subtitle) and
// per-kind default attribute logic here.
let codec = Codec::from_coding_type(stream_type);
let stream = match codec.kind() {
CodecKind::Video => {
// Default resolution by codec generation (HEVC →
// UHD, MPEG-2 → 1080i, else 1080p); refined later
// from the actual elementary stream.
let resolution = match codec {
Codec::Hevc => Resolution::R2160p,
Codec::Mpeg2 => Resolution::R1080i,
_ => Resolution::R1080p,
};
Some(Stream::Video(VideoStream {
pid: es_pid,
codec,
resolution,
frame_rate: FrameRate::Unknown,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
}))
}
CodecKind::Audio => Some(Stream::Audio(AudioStream {
pid: es_pid,
codec,
channels: AudioChannels::Surround51,
language: "und".into(),
sample_rate: SampleRate::S48,
secondary: false,
purpose: crate::disc::LabelPurpose::Normal,
label: String::new(),
})),
CodecKind::Subtitle => Some(Stream::Subtitle(SubtitleStream {
pid: es_pid,
codec,
language: "und".into(),
forced: false,
qualifier: crate::disc::LabelQualifier::None,
codec_data: None,
})),
CodecKind::Unknown => {
tracing::warn!(
target: "mux",
"dropping PMT stream entry with unknown stream_type {:#04x} (PID {:#06x})",
stream_type,
es_pid,
);
None
}
};
if let Some(s) = stream {
streams.push(s);
}
pos += 5 + es_info_len;
}
break;
}
offset += BD_TS_PACKET_SIZE;
}
if streams.is_empty() {
None
} else {
Some(streams)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_timestamp() {
// Example: PTS = 0 → encoded as 21 00 01 00 01
let data = [0x21, 0x00, 0x01, 0x00, 0x01];
assert_eq!(parse_timestamp(&data), Some(0));
// Example: PTS = 90000 (1 second at 90kHz)
// Manual encoding: 33 bits = 0x00015F90
// This is just a sanity check that the parser doesn't crash
let data2 = [0x21, 0x00, 0x07, 0xE9, 0x01]; // approximate
let pts = parse_timestamp(&data2);
assert!(pts.is_some() && pts.unwrap() >= 0);
// Invalid marker bits → returns None
let bad = [0x00, 0x00, 0x00, 0x00, 0x00]; // marker bits wrong
assert_eq!(parse_timestamp(&bad), None);
}
#[test]
fn test_demuxer_empty() {
let mut demux = TsDemuxer::new(&[0x1011]);
let result = demux.feed(&[]);
assert!(result.is_empty());
}
// ── scan_streams PMT parsing ──────────────────────────────────────────
/// Wrap a 188-byte TS packet body in a 192-byte BD-TS packet
/// (4-byte timecode prefix the scanner skips).
fn bdts_packet(body: [u8; 184], pid: u16, pusi: bool) -> Vec<u8> {
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
// 4-byte timecode prefix is ignored; leave zero.
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
if pusi {
pkt[5] |= 0x40;
}
pkt[6] = (pid & 0xFF) as u8;
pkt[7] = 0x10; // payload only, no adaptation field
pkt[8..8 + 184].copy_from_slice(&body);
pkt
}
/// Build a PAT TS packet pointing program 1 at `pmt_pid`.
fn pat_packet(pmt_pid: u16) -> Vec<u8> {
let mut body = [0xFFu8; 184];
let mut i = 0;
body[i] = 0x00; // pointer_field
i += 1;
body[i] = 0x00; // table_id = PAT
// section_length counts bytes after the length field: tsid(2) +
// version/current_next(1) + section_number(1) + last_section(1) +
// one 4-byte program entry + 4-byte CRC = 13.
body[i + 1] = 0xB0; // section_syntax + reserved + len high nibble
body[i + 2] = 0x0D; // section_length low byte = 13
body[i + 3] = 0x00; // tsid hi
body[i + 4] = 0x01; // tsid lo
body[i + 5] = 0xC1; // version/current_next
body[i + 6] = 0x00; // section_number
body[i + 7] = 0x00; // last_section_number
// program entry: program_number=1 → pmt_pid
body[i + 8] = 0x00;
body[i + 9] = 0x01;
body[i + 10] = 0xE0 | (((pmt_pid >> 8) as u8) & 0x1F);
body[i + 11] = (pmt_pid & 0xFF) as u8;
// (CRC bytes left as 0xFF — scanner doesn't validate CRC)
let _ = &mut i;
bdts_packet(body, 0, true)
}
/// Build a PMT TS packet listing the given `(stream_type, es_pid)` entries.
fn pmt_packet(pmt_pid: u16, entries: &[(u8, u16)]) -> Vec<u8> {
let mut body = [0xFFu8; 184];
body[0] = 0x00; // pointer_field
let s = 1; // table start
body[s] = 0x02; // table_id = PMT
// Fixed PMT fields after section_length: 2(prog) +1 +2 +2(pcr)
// +2(prog_info_len=0) = 9, then per-entry 5 bytes, then 4 CRC.
let entries_len = entries.len() * 5;
let section_length = 9 + entries_len + 4;
body[s + 1] = 0xB0 | (((section_length >> 8) as u8) & 0x0F);
body[s + 2] = (section_length & 0xFF) as u8;
body[s + 3] = 0x00; // program_number hi
body[s + 4] = 0x01; // program_number lo
body[s + 5] = 0xC1; // version/current_next
body[s + 6] = 0x00; // section_number
body[s + 7] = 0x00; // last_section_number
body[s + 8] = 0xE0; // PCR PID hi (reserved bits)
body[s + 9] = 0x00; // PCR PID lo
body[s + 10] = 0xF0; // program_info_length hi (=0)
body[s + 11] = 0x00; // program_info_length lo
let mut p = s + 12;
for &(stype, es_pid) in entries {
body[p] = stype;
body[p + 1] = 0xE0 | (((es_pid >> 8) as u8) & 0x1F);
body[p + 2] = (es_pid & 0xFF) as u8;
body[p + 3] = 0xF0; // ES_info_length hi (=0)
body[p + 4] = 0x00; // ES_info_length lo
p += 5;
}
bdts_packet(body, pmt_pid, true)
}
#[test]
fn scan_streams_maps_lpcm_via_from_coding_type() {
use crate::disc::{Codec, Stream};
let pmt_pid = 0x0100;
let mut data = pat_packet(pmt_pid);
// 0x80 = LPCM (present in from_coding_type, was MISSING from the
// old duplicate table in scan_streams). 0x1B = H.264 video.
data.extend(pmt_packet(pmt_pid, &[(0x1B, 0x1011), (0x80, 0x1100)]));
let streams = scan_streams(&data).expect("PMT should parse");
assert_eq!(streams.len(), 2, "video + LPCM audio");
let lpcm = streams
.iter()
.find(|s| matches!(s, Stream::Audio(a) if a.pid == 0x1100))
.expect("LPCM audio stream present");
if let Stream::Audio(a) = lpcm {
assert_eq!(a.codec, Codec::Lpcm, "0x80 must map to LPCM");
}
assert!(
streams
.iter()
.any(|s| matches!(s, Stream::Video(v) if v.codec == Codec::H264)),
"H.264 video present"
);
}
}