From d8c323bf9f3109c8f09db74479604b36e832d0d9 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:20:21 -0700 Subject: [PATCH] Magic-number/taxonomy pass: central wire-format + sector + unit consts - libfreemkv::consts: coding_type::* (ES coding-type bytes), pes_stream_id::* + PAYLOAD_RANGE, SECTOR_BYTES (usize) + SECTOR_BYTES_U64 (offset math) - replace bare wire-code/sector literals across disc, mpls, clpi, labels, m2ts_mux, ps, tsmux, file_sector_source, extract - remove two unreachable secondary-stream match arms in mpls parse_stream_entry --- src/clpi.rs | 28 +++++----- src/consts.rs | 87 ++++++++++++++++++++++++++++++++ src/disc/extract.rs | 6 +-- src/disc/mod.rs | 47 ++++++++--------- src/io/file_sector_source/mod.rs | 8 +-- src/labels/mod.rs | 9 ++-- src/labels/mpls_universal.rs | 29 ++++++----- src/mpls.rs | 63 +++++++++++------------ src/mux/m2ts_mux/mod.rs | 23 +++++++-- src/mux/ps.rs | 16 +++--- src/mux/tsmux.rs | 7 +-- 11 files changed, 212 insertions(+), 111 deletions(-) diff --git a/src/clpi.rs b/src/clpi.rs index 768bf58..9c41e7e 100644 --- a/src/clpi.rs +++ b/src/clpi.rs @@ -6,7 +6,7 @@ //! //! Reference: https://github.com/lw/BluRay/wiki/CLPI -use crate::consts::{BD_SOURCE_PACKET_BYTES, SECTOR_BYTES}; +use crate::consts::{BD_SOURCE_PACKET_BYTES, SECTOR_BYTES_U64}; use crate::disc::Extent; use crate::error::{Error, Result}; @@ -175,8 +175,8 @@ impl ClipInfo { // a sub-sector-aligned range still spans every sector it touches. let start_byte = start_spn as u64 * BD_SOURCE_PACKET_BYTES as u64; let end_byte = end_spn as u64 * BD_SOURCE_PACKET_BYTES as u64; - let start_sector = (start_byte / SECTOR_BYTES as u64) as u32; - let end_sector = end_byte.div_ceil(SECTOR_BYTES as u64) as u32; + let start_sector = (start_byte / SECTOR_BYTES_U64) as u32; + let end_sector = end_byte.div_ceil(SECTOR_BYTES_U64) as u32; vec![Extent { start_lba: start_sector, // relative to m2ts file start @@ -261,6 +261,7 @@ pub fn parse(data: &[u8]) -> Result { /// errors because the EP map is the primary CLPI output, and a corrupt /// program_info shouldn't break sector-range lookups. fn parse_program_info(data: &[u8]) -> Vec { + use crate::consts::coding_type as c; let mut out = Vec::new(); if data.len() < 6 { return out; @@ -299,16 +300,15 @@ fn parse_program_info(data: &[u8]) -> Vec { let mut language = String::new(); match coding_type { - // Video — MPEG-2 (0x02), H.264 (0x1B), HEVC (0x24) - 0x02 | 0x1B | 0x24 => { + // Video — MPEG-2, H.264, HEVC + c::MPEG2_VIDEO | c::H264 | c::HEVC => { if sci.len() >= 2 { video_format = (sci[1] >> 4) & 0x0F; video_rate = sci[1] & 0x0F; } } - // Primary audio — LPCM(0x80), AC-3(0x81), DTS(0x82), - // TrueHD(0x83), AC-3+(0x84), DTS-HD(0x85), DTS-HD MA(0x86) - 0x80..=0x86 => { + // Primary audio — LPCM, AC-3, DTS, TrueHD, AC-3+, DTS-HD HR, DTS-HD MA + c::LPCM..=c::DTS_HD_MA => { if sci.len() >= 2 { audio_format = (sci[1] >> 4) & 0x0F; audio_rate = sci[1] & 0x0F; @@ -317,8 +317,8 @@ fn parse_program_info(data: &[u8]) -> Vec { language = String::from_utf8_lossy(&sci[2..5]).to_string(); } } - // Secondary audio (0xA1 AC-3+, 0xA2 DTS-HD) - 0xA1 | 0xA2 => { + // Secondary audio (AC-3+ secondary, DTS-HD secondary) + c::AC3_PLUS_SECONDARY | c::DTS_HD_SECONDARY => { if sci.len() >= 2 { audio_format = (sci[1] >> 4) & 0x0F; audio_rate = sci[1] & 0x0F; @@ -327,8 +327,8 @@ fn parse_program_info(data: &[u8]) -> Vec { language = String::from_utf8_lossy(&sci[2..5]).to_string(); } } - // PG (0x90), IG (0x91): coding_type + 3-byte language [+ char_code for PG] - 0x90 | 0x91 => { + // PG, IG: coding_type + 3-byte language [+ char_code for PG] + c::PG | c::IG => { if sci.len() >= 4 { language = String::from_utf8_lossy(&sci[1..4]).to_string(); } @@ -1125,8 +1125,8 @@ mod tests { let end_spn = big_spn as u64; let start_byte = start_spn * BD_SOURCE_PACKET_BYTES as u64; let end_byte = end_spn * BD_SOURCE_PACKET_BYTES as u64; - let start_sector = (start_byte / SECTOR_BYTES as u64) as u32; - let end_sector = end_byte.div_ceil(SECTOR_BYTES as u64) as u32; + let start_sector = (start_byte / SECTOR_BYTES_U64) as u32; + let end_sector = end_byte.div_ceil(SECTOR_BYTES_U64) as u32; assert_eq!(extents[0].start_lba, start_sector); assert_eq!(extents[0].sector_count, end_sector - start_sector); // Concretely: 0x20000 × 192 / 2048 = 12288 sectors. diff --git a/src/consts.rs b/src/consts.rs index 50c2a82..29e06d9 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -8,8 +8,18 @@ /// Bytes per logical sector on every optical medium freemkv reads /// (Blu-ray, DVD-Video, CD-ROM Mode 1). Universal — hence unprefixed. +/// +/// `usize` because its dominant use is buffer sizing and slice indexing, where +/// Rust *requires* `usize` (`vec![0u8; SECTOR_BYTES]`, `buf.len() < SECTOR_BYTES`). +/// For byte-offset / capacity arithmetic — which is `u64` because a disc can +/// exceed 4 GiB — use [`SECTOR_BYTES_U64`] instead of casting at each site. pub const SECTOR_BYTES: usize = 2048; +/// [`SECTOR_BYTES`] as `u64`, for byte-offset and capacity arithmetic. The +/// single `usize → u64` boundary cast lives here, once, so offset math across +/// the workspace reads as `sectors * SECTOR_BYTES_U64` with no per-site cast. +pub const SECTOR_BYTES_U64: u64 = SECTOR_BYTES as u64; + /// Bytes per MPEG-2 transport-stream packet. Common to all MPEG-TS, not just /// Blu-ray — prefixed by the format, not a disc type. pub const TS_PACKET_BYTES: usize = 188; @@ -31,3 +41,80 @@ pub const TS_PAYLOAD_BYTES: usize = TS_PACKET_BYTES - TS_HEADER_BYTES; /// prefixed with the [`BD_TIMESTAMP_PREFIX_BYTES`] arrival-timestamp header. /// A BDAV/M2TS construct only — DVD VOBs have no source packets — hence `BD_`. pub const BD_SOURCE_PACKET_BYTES: usize = TS_PACKET_BYTES + BD_TIMESTAMP_PREFIX_BYTES; + +/// Elementary-stream coding-type codes — the single source of truth for the +/// byte that identifies a stream's codec. +/// +/// This is one registry used in two places that share the same value space: +/// the MPEG-TS PMT `stream_type` (ISO/IEC 13818-1 Table 2-34) and the Blu-ray +/// STN/CLPI `stream_coding_type` (BD-ROM Part 3). The standardized video codes +/// (`0x02`, `0x1B`, `0x24`, `0xEA`) are ISO assignments; the `0x80..=0xA2` +/// audio/graphics codes sit in the ISO user-private range and follow the +/// Blu-ray Disc Association / ATSC A/52 convention. Because every consumer +/// reads or writes this single byte, the family is unprefixed — the scope is +/// "any elementary stream freemkv parses or muxes". +/// +/// Each constant is `u8`: the spec defines an 8-bit field and the code compares +/// it directly against a byte read from the buffer, so no casts are needed. +pub mod coding_type { + /// MPEG-2 video (ISO/IEC 13818-1 Table 2-34). + pub const MPEG2_VIDEO: u8 = 0x02; + /// H.264 / AVC video (ISO/IEC 13818-1 Table 2-34). + pub const H264: u8 = 0x1B; + /// HEVC / H.265 video (ISO/IEC 13818-1 Table 2-34, 2015 amendment). + pub const HEVC: u8 = 0x24; + /// SMPTE VC-1 video (BD-ROM convention, ISO user-private range). + pub const VC1: u8 = 0xEA; + + /// LPCM audio (BD-ROM convention). + pub const LPCM: u8 = 0x80; + /// Dolby Digital (AC-3) audio (BD-ROM / ATSC A/52 convention). + pub const AC3: u8 = 0x81; + /// DTS audio (BD-ROM convention). + pub const DTS: u8 = 0x82; + /// Dolby TrueHD audio (BD-ROM convention). + pub const TRUEHD: u8 = 0x83; + /// Dolby Digital Plus (E-AC-3 / AC-3+) audio (BD-ROM convention). + pub const AC3_PLUS: u8 = 0x84; + /// DTS-HD High Resolution audio (BD-ROM Part 3-1). + pub const DTS_HD_HR: u8 = 0x85; + /// DTS-HD Master Audio (lossless) (BD-ROM Part 3-1). + pub const DTS_HD_MA: u8 = 0x86; + + /// Presentation Graphics — PG subtitle stream (BD-ROM HDMV). + pub const PG: u8 = 0x90; + /// Interactive Graphics — IG / BD-J menu overlay, NOT a subtitle (BD-ROM HDMV). + pub const IG: u8 = 0x91; + /// Text subtitle stream (BD-ROM HDMV). + pub const TEXT_SUBTITLE: u8 = 0x92; + + /// Secondary Dolby Digital Plus audio (BD-ROM convention). + pub const AC3_PLUS_SECONDARY: u8 = 0xA1; + /// Secondary DTS-HD audio (lossless MA, not lossy HR) (BD-ROM convention). + pub const DTS_HD_SECONDARY: u8 = 0xA2; +} + +/// MPEG PES `stream_id` codes — the byte after the `00 00 01` start-code prefix +/// that identifies an elementary stream's role in a PES packet (ISO/IEC +/// 13818-1 Table 2-22). Shared by the program-stream demuxer and the TS/M2TS +/// muxers, so defined here once. Each is `u8` (matches the byte on the wire). +pub mod pes_stream_id { + /// Video stream (`110x xxxx`; freemkv emits the base id `0xE0`). + pub const VIDEO: u8 = 0xE0; + /// private_stream_1 — AC-3 / DTS / LPCM / PGS subtitle payloads. + pub const PRIVATE_STREAM_1: u8 = 0xBD; + /// padding_stream — stuffing bytes only, no payload to demux. + pub const PADDING_STREAM: u8 = 0xBE; + /// private_stream_2 — DVD navigation (PCI/DSI); carries no muxable ES. + pub const PRIVATE_STREAM_2: u8 = 0xBF; + + /// Highest video stream_id — the `110x xxxx` video range tops out at 0xEF. + pub const VIDEO_MAX: u8 = 0xEF; + + /// Inclusive range of every PES `stream_id` that carries demuxable payload: + /// [`PRIVATE_STREAM_1`] (0xBD) through [`VIDEO_MAX`] (0xEF) — i.e. private + /// stream 1/2, padding, MPEG audio (0xC0-0xDF) and video (0xE0-0xEF). The + /// pack (0xBA), system-header (0xBB) and program-end (0xB9) codes sit below + /// this range and are deliberately excluded: they're structural, not ES. + pub const PAYLOAD_RANGE: core::ops::RangeInclusive = PRIVATE_STREAM_1..=VIDEO_MAX; +} diff --git a/src/disc/extract.rs b/src/disc/extract.rs index b4b68d2..99a8516 100644 --- a/src/disc/extract.rs +++ b/src/disc/extract.rs @@ -24,7 +24,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; -use crate::consts::SECTOR_BYTES; +use crate::consts::{SECTOR_BYTES, SECTOR_BYTES_U64}; /// AACS aligned unit = 3 sectors / 6144 bytes. Content reads are issued in /// multiples of this so the decrypt step always sees whole units. const AACS_UNIT_SECTORS: u32 = 3; @@ -282,7 +282,7 @@ impl Disc { for &(abs_lba, byte_len) in &pf.extents { extents.push(crate::disc::Extent { start_lba: abs_lba, - sector_count: (byte_len as u64).div_ceil(SECTOR_BYTES as u64) as u32, + sector_count: (byte_len as u64).div_ceil(SECTOR_BYTES_U64) as u32, }); } } @@ -467,7 +467,7 @@ fn extract_one_file( // the per-extent re-anchoring in the mux read paths // (`mux/disc.rs`, `sector/prefetched.rs`). No-op for CSS / None. dec.set_unit_base(abs_lba); - let sectors = (byte_len as u64).div_ceil(SECTOR_BYTES as u64) as u32; + let sectors = (byte_len as u64).div_ceil(SECTOR_BYTES_U64) as u32; let mut sector_off: u32 = 0; while sector_off < sectors { let mut batch = (sectors - sector_off).min(READ_BATCH_SECTORS); diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 10d9482..6e2ad83 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -627,28 +627,29 @@ impl Codec { ]; pub(crate) fn from_coding_type(ct: u8) -> Self { + use crate::consts::coding_type as c; match ct { - 0x24 => Codec::Hevc, - 0x1B => Codec::H264, - 0xEA => Codec::Vc1, - 0x02 => Codec::Mpeg2, - 0x83 => Codec::TrueHd, - 0x86 => Codec::DtsHdMa, - 0x85 => Codec::DtsHdHr, - 0x82 => Codec::Dts, - 0x81 => Codec::Ac3, - 0x84 | 0xA1 => Codec::Ac3Plus, - 0x80 => Codec::Lpcm, - // 0x86 (primary) / 0xA2 (secondary) are the DTS-HD MA - // lossless pair, parallel to 0x81/0xA1 for AC-3. 0xA2 is - // lossless MA, not lossy HR. - 0xA2 => Codec::DtsHdMa, - // 0x90 = Presentation Graphics (PG / subtitles). 0x91 = Interactive - // Graphics (IG / menus) and 0x92 = Text subtitles are distinct HDMV - // coding types and are NOT PG subtitle streams; only 0x90 maps to - // Pgs. IG (0x91) falls through to Unknown so the PMT/STN walker drops - // it rather than surfacing a bogus PGS subtitle track for a menu ES. - 0x90 => Codec::Pgs, + c::HEVC => Codec::Hevc, + c::H264 => Codec::H264, + c::VC1 => Codec::Vc1, + c::MPEG2_VIDEO => Codec::Mpeg2, + c::TRUEHD => Codec::TrueHd, + c::DTS_HD_MA => Codec::DtsHdMa, + c::DTS_HD_HR => Codec::DtsHdHr, + c::DTS => Codec::Dts, + c::AC3 => Codec::Ac3, + c::AC3_PLUS | c::AC3_PLUS_SECONDARY => Codec::Ac3Plus, + c::LPCM => Codec::Lpcm, + // DTS_HD_MA (primary 0x86) / DTS_HD_SECONDARY (0xA2) are the + // DTS-HD MA lossless pair, parallel to AC3/AC3_PLUS_SECONDARY for + // AC-3. The secondary code is lossless MA, not lossy HR. + c::DTS_HD_SECONDARY => Codec::DtsHdMa, + // PG (0x90) = Presentation Graphics (subtitles). IG (0x91, menus) + // and TEXT_SUBTITLE (0x92) are distinct HDMV coding types and are + // NOT PG subtitle streams; only PG maps to Pgs. IG falls through to + // Unknown so the PMT/STN walker drops it rather than surfacing a + // bogus PGS subtitle track for a menu ES. + c::PG => Codec::Pgs, ct => Codec::Unknown(ct), } } @@ -5068,8 +5069,8 @@ mod tests { let mf = Mapfile::load(&disc.mapfile_for(&iso_path)).expect("load mapfile"); let good = mf.ranges_with(&[SectorStatus::Finished]); let bad_ranges = mf.ranges_with(&[SectorStatus::NonTrimmed]); - let disc_bytes = sectors as u64 * 2048; - const SEC: u64 = crate::consts::SECTOR_BYTES as u64; + const SEC: u64 = crate::consts::SECTOR_BYTES_U64; + let disc_bytes = sectors as u64 * SEC; // The first failing batch starts at LBA 320; everything before it read // cleanly and must be Finished. diff --git a/src/io/file_sector_source/mod.rs b/src/io/file_sector_source/mod.rs index b5f47c4..2fcea81 100644 --- a/src/io/file_sector_source/mod.rs +++ b/src/io/file_sector_source/mod.rs @@ -72,7 +72,7 @@ use std::path::Path; use crate::error::{Error, Result}; use crate::sector::SectorSource; -use crate::consts::SECTOR_BYTES; +use crate::consts::{SECTOR_BYTES, SECTOR_BYTES_U64}; /// Bytes-read threshold per `posix_fadvise(DONTNEED)` drop on the /// read side. Mirrors `WRITEBACK_CHUNK_BYTES` so the read-side page @@ -134,7 +134,7 @@ impl FileSectorSource { .metadata() .map_err(|e| Error::IoError { source: e })? .len(); - let sectors = len / SECTOR_BYTES as u64; + let sectors = len / SECTOR_BYTES_U64; if sectors > u32::MAX as u64 { return Err(Error::IsoTooLarge { path: path.to_string_lossy().into_owned(), @@ -180,7 +180,7 @@ impl SectorSource for FileSectorSource { if count == 0 { return Ok(0); } - let offset = lba as u64 * SECTOR_BYTES as u64; + let offset = lba as u64 * SECTOR_BYTES_U64; self.file .seek(SeekFrom::Start(offset)) .map_err(|e| Error::IoError { source: e })?; @@ -494,7 +494,7 @@ mod tests { #[test] fn dontneed_eviction_does_not_affect_data() { // 32 MiB default chunk = 16384 sectors; read a bit past it. - let total = (READ_DROP_CHUNK_BYTES_DEFAULT / SECTOR_BYTES as u64) as u32 + 64; + let total = (READ_DROP_CHUNK_BYTES_DEFAULT / SECTOR_BYTES_U64) as u32 + 64; let dir = tempdir().unwrap(); let path = dir.path().join("drop.iso"); make_iso(&path, total); diff --git a/src/labels/mod.rs b/src/labels/mod.rs index 4f588f5..f3ac45e 100644 --- a/src/labels/mod.rs +++ b/src/labels/mod.rs @@ -634,6 +634,7 @@ fn append_clpi_orphans( reader: &mut dyn SectorSource, udf: &UdfFs, ) -> usize { + use crate::consts::coding_type as c; // Index existing labels by PID — but StreamLabel doesn't carry // PID. Index by (type, language, codec_hint) tuple instead; this // is fuzzier than PID matching but the only signal available @@ -679,9 +680,11 @@ fn append_clpi_orphans( // Interactive Graphics (BD-J menu overlay), NOT a user-facing // subtitle — skip it, matching the MPLS path which drops IG. let stype = match s.coding_type { - 0x80..=0x86 | 0xA1 | 0xA2 => StreamLabelType::Audio, - 0x90 => StreamLabelType::Subtitle, - _ => continue, // 0x91 IG / video / unknown — skip + c::LPCM..=c::DTS_HD_MA | c::AC3_PLUS_SECONDARY | c::DTS_HD_SECONDARY => { + StreamLabelType::Audio + } + c::PG => StreamLabelType::Subtitle, + _ => continue, // IG / video / unknown — skip }; // Same dedup logic as MPLS: normalize language, build codec // hint, check against existing label set. diff --git a/src/labels/mpls_universal.rs b/src/labels/mpls_universal.rs index 0d13cfd..fd52e04 100644 --- a/src/labels/mpls_universal.rs +++ b/src/labels/mpls_universal.rs @@ -227,21 +227,22 @@ pub(crate) fn language_display_name(iso: &str) -> String { /// bytes (the table covers everything the spec defines, but unknown /// values are still possible on malformed discs). pub(crate) fn codec_name(coding_type: u8) -> &'static str { + use crate::consts::coding_type as c; match coding_type { - 0x02 => "MPEG-2", - 0x1B => "H.264", - 0x24 => "HEVC", - 0x80 => "LPCM", - 0x81 => "AC-3", - 0x82 => "DTS", - 0x83 => "TrueHD", - 0x84 => "AC-3+", - 0x85 => "DTS-HD HR", // BD-ROM Part 3-1: 0x85 = DTS-HD High Resolution - 0x86 => "DTS-HD MA", - 0x90 => "PG", - 0x91 => "IG", - 0xA1 => "AC-3+ Secondary", - 0xA2 => "DTS-HD Secondary", + c::MPEG2_VIDEO => "MPEG-2", + c::H264 => "H.264", + c::HEVC => "HEVC", + c::LPCM => "LPCM", + c::AC3 => "AC-3", + c::DTS => "DTS", + c::TRUEHD => "TrueHD", + c::AC3_PLUS => "AC-3+", + c::DTS_HD_HR => "DTS-HD HR", // BD-ROM Part 3-1: 0x85 = DTS-HD High Resolution + c::DTS_HD_MA => "DTS-HD MA", + c::PG => "PG", + c::IG => "IG", + c::AC3_PLUS_SECONDARY => "AC-3+ Secondary", + c::DTS_HD_SECONDARY => "DTS-HD Secondary", _ => "", } } diff --git a/src/mpls.rs b/src/mpls.rs index ab75fc2..4d37542 100644 --- a/src/mpls.rs +++ b/src/mpls.rs @@ -179,7 +179,9 @@ pub fn parse(data: &[u8]) -> Result { } // PG subtitles for _ in 0..n_pg { - if let Some((entry, next)) = parse_stream_entry(item, spos, STREAM_CATEGORY_PG_SUBTITLE) { + if let Some((entry, next)) = + parse_stream_entry(item, spos, STREAM_CATEGORY_PG_SUBTITLE) + { streams.push(entry); spos = next; } else { @@ -196,7 +198,9 @@ pub fn parse(data: &[u8]) -> Result { } // Secondary audio for _ in 0..n_sec_audio { - if let Some((mut entry, next)) = parse_stream_entry(item, spos, STREAM_CATEGORY_AUDIO) { + if let Some((mut entry, next)) = + parse_stream_entry(item, spos, STREAM_CATEGORY_AUDIO) + { entry.stream_type = 5; entry.secondary = true; streams.push(entry); @@ -213,7 +217,9 @@ pub fn parse(data: &[u8]) -> Result { } // Secondary video (PiP) for _ in 0..n_sec_video { - if let Some((mut entry, next)) = parse_stream_entry(item, spos, STREAM_CATEGORY_VIDEO) { + if let Some((mut entry, next)) = + parse_stream_entry(item, spos, STREAM_CATEGORY_VIDEO) + { entry.stream_type = 6; entry.secondary = true; streams.push(entry); @@ -240,7 +246,9 @@ pub fn parse(data: &[u8]) -> Result { } // Secondary PG (PiP subtitles) — must consume to keep spos aligned for _ in 0..n_pip_pg { - if let Some((mut entry, next)) = parse_stream_entry(item, spos, STREAM_CATEGORY_PG_SUBTITLE) { + if let Some((mut entry, next)) = + parse_stream_entry(item, spos, STREAM_CATEGORY_PG_SUBTITLE) + { entry.secondary = true; streams.push(entry); // Skip reference data: num_refs(1) + reserved(1) + refs + padding @@ -256,7 +264,9 @@ pub fn parse(data: &[u8]) -> Result { } // Dolby Vision enhancement layer for _ in 0..n_dv { - if let Some((mut entry, next)) = parse_stream_entry(item, spos, STREAM_CATEGORY_VIDEO) { + if let Some((mut entry, next)) = + parse_stream_entry(item, spos, STREAM_CATEGORY_VIDEO) + { entry.stream_type = 7; entry.secondary = true; streams.push(entry); @@ -333,6 +343,7 @@ const STREAM_CATEGORY_PG_SUBTITLE: u8 = 3; const STREAM_CATEGORY_IG: u8 = 4; fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(StreamEntry, usize)> { + use crate::consts::coding_type as c; if pos + 2 > item.len() { return None; } @@ -387,22 +398,27 @@ fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(Strea let mut color_space_val = 0u8; let mut language = String::new(); + // `stream_type` here is the STN category passed by the caller, which is + // only ever a primary category (VIDEO/AUDIO/PG_SUBTITLE/IG). Secondary + // audio/video and the DV enhancement layer are parsed through their + // matching primary category (identical attribute layout) and re-tagged by + // the caller after this returns, so there are no secondary arms here. match stream_type { - 1 => { + STREAM_CATEGORY_VIDEO => { // Video: coding_type(1) + format_rate(1) + [hdr_info(1) if HEVC] if sa.len() >= 2 { video_format = (sa[1] >> 4) & 0x0F; video_rate = sa[1] & 0x0F; } - if coding_type == 0x24 && sa.len() > 2 { + if coding_type == c::HEVC && sa.len() > 2 { dynamic_range = (sa[2] >> 4) & 0x0F; color_space_val = sa[2] & 0x0F; } } - 2 => { + STREAM_CATEGORY_AUDIO => { // Audio: coding_type(1) + format_rate(1) + language(3) - // Exception: PGS (0x90/0x91) in audio slot uses PG layout: coding_type(1) + language(3) - if coding_type == 0x90 || coding_type == 0x91 { + // Exception: PG/IG in an audio slot uses PG layout: coding_type(1) + language(3) + if coding_type == c::PG || coding_type == c::IG { if sa.len() >= 4 { language = String::from_utf8_lossy(&sa[1..4]).to_string(); } @@ -416,35 +432,14 @@ fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(Strea } } } - 3 => { + STREAM_CATEGORY_PG_SUBTITLE => { // PG: coding_type(1) + language(3). - // IG (type 4) is parsed only to advance spos and is then - // discarded by the caller, so it deliberately has no arm here. + // IG is parsed only to advance spos and is then discarded by the + // caller, so it deliberately has no arm here. if sa.len() >= 4 { language = String::from_utf8_lossy(&sa[1..4]).to_string(); } } - 5 => { - // Secondary audio: same as primary audio - if sa.len() >= 2 { - audio_format = (sa[1] >> 4) & 0x0F; - audio_rate = sa[1] & 0x0F; - } - if sa.len() >= 5 { - language = String::from_utf8_lossy(&sa[2..5]).to_string(); - } - } - 6 | 7 => { - // Secondary video: same as primary video - if sa.len() >= 2 { - video_format = (sa[1] >> 4) & 0x0F; - video_rate = sa[1] & 0x0F; - } - if coding_type == 0x24 && sa.len() > 2 { - dynamic_range = (sa[2] >> 4) & 0x0F; - color_space_val = sa[2] & 0x0F; - } - } _ => {} } diff --git a/src/mux/m2ts_mux/mod.rs b/src/mux/m2ts_mux/mod.rs index 447c745..d13a41d 100644 --- a/src/mux/m2ts_mux/mod.rs +++ b/src/mux/m2ts_mux/mod.rs @@ -76,15 +76,18 @@ const PCR_INTERVAL_PACKETS: u64 = 40; /// the picture it timestamps. 200 ms in 90 kHz ticks. const PCR_LEAD_90KHZ: u64 = 90_000 / 5; +// PMT stream-type codes. These are the same elementary-stream coding-type +// registry as the parse side; the single source of truth is `consts::coding_type`. +use crate::consts::coding_type; /// HEVC stream-type code, ISO/IEC 13818-1 Table 2-34 (2015 amendment). -const STREAM_TYPE_HEVC: u8 = 0x24; +const STREAM_TYPE_HEVC: u8 = coding_type::HEVC; /// AC-3 / E-AC-3. Not an ISO assignment — sits in the user-private /// 0x80-0xFF range and is the Blu-ray Disc Association / ATSC A/52 /// convention. -const STREAM_TYPE_AC3: u8 = 0x81; +const STREAM_TYPE_AC3: u8 = coding_type::AC3; /// Dolby TrueHD. Also a private/BD-conventional value in the /// user-private 0x80-0xFF range, not an ISO assignment. -const STREAM_TYPE_TRUEHD: u8 = 0x83; +const STREAM_TYPE_TRUEHD: u8 = coding_type::TRUEHD; /// Audio codec hint for [`M2tsMux::new`] / [`M2tsMux::set_audio`]. The /// muxer needs to know the codec to pick the right PMT `stream_type` @@ -420,7 +423,12 @@ impl M2tsMux { /// Build a PES packet for a video access unit. fn build_video_pes(pts_90k: u64, es: &[u8]) -> Vec { - build_pes_packet(0xE0, pts_90k, es, /* length_in_header */ false) + build_pes_packet( + crate::consts::pes_stream_id::VIDEO, + pts_90k, + es, + /* length_in_header */ false, + ) } /// Build a PES packet for an audio access unit. @@ -430,7 +438,12 @@ fn build_audio_pes(pts_90k: u64, es: &[u8]) -> Vec { // start code. For an access unit larger than ~64 KiB (rare — e.g. a // large TrueHD frame) the length field falls back to the unbounded // (0x0000) form, which most demuxers tolerate for private_stream_1. - build_pes_packet(0xBD, pts_90k, es, /* length_in_header */ true) + build_pes_packet( + crate::consts::pes_stream_id::PRIVATE_STREAM_1, + pts_90k, + es, + /* length_in_header */ true, + ) } fn build_pes_packet(stream_id: u8, pts_90k: u64, es: &[u8], length_in_header: bool) -> Vec { diff --git a/src/mux/ps.rs b/src/mux/ps.rs index d1a3ea8..c40fe9e 100644 --- a/src/mux/ps.rs +++ b/src/mux/ps.rs @@ -23,10 +23,10 @@ const SYSTEM_HEADER_ID: u8 = 0xBB; const PROGRAM_END_ID: u8 = 0xB9; /// Private stream 1 (AC3, DTS, LPCM, subtitles). -const PRIVATE_STREAM_1: u8 = 0xBD; +const PRIVATE_STREAM_1: u8 = crate::consts::pes_stream_id::PRIVATE_STREAM_1; /// Private stream 2 (0xBF) — DVD navigation (PCI/DSI). Carries no muxable /// elementary stream; expected to be dropped on every disc. -const PRIVATE_STREAM_2: u8 = 0xBF; +const PRIVATE_STREAM_2: u8 = crate::consts::pes_stream_id::PRIVATE_STREAM_2; /// Hard cap on the demuxer's reassembly buffer. A length-0 (unbounded) video /// PES is delimited by the next PS-layer boundary; if a corrupt stream declares @@ -107,8 +107,8 @@ impl PsPacket { /// mis-routing the packet. pub fn dvd_pid(&self) -> Option { match self.stream_id { - 0xE0..=0xEF => Some(DVD_VIDEO_PID), - 0xBD => { + crate::consts::pes_stream_id::VIDEO..=0xEF => Some(DVD_VIDEO_PID), + PRIVATE_STREAM_1 => { let sub = self.sub_stream_id?; dvd_audio_pid(sub).or_else(|| dvd_subtitle_pid(sub)) } @@ -348,8 +348,8 @@ fn find_ps_boundary(data: &[u8], from: usize) -> Option { fn is_pes_stream_id(id: u8) -> bool { // Video: 0xE0-0xEF, MPEG audio: 0xC0-0xDF, private stream 1: 0xBD, // private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc. - // We parse anything in the PES range. - matches!(id, 0xBD..=0xEF) + // We parse anything in the payload-bearing PES range. + crate::consts::pes_stream_id::PAYLOAD_RANGE.contains(&id) } /// Parse a single PES packet from a byte slice that starts at the start code. @@ -365,12 +365,12 @@ fn parse_pes_packet(data: &[u8]) -> Option { let stream_id = data[3]; // Padding stream — skip entirely. - if stream_id == 0xBE { + if stream_id == crate::consts::pes_stream_id::PADDING_STREAM { return None; } // Streams without standard PES header extension. - if stream_id == 0xBF { + if stream_id == PRIVATE_STREAM_2 { let payload = if data.len() > 6 { &data[6..] } else { &[] }; return Some(PsPacket { stream_id, diff --git a/src/mux/tsmux.rs b/src/mux/tsmux.rs index 285c2a3..e44e2f6 100644 --- a/src/mux/tsmux.rs +++ b/src/mux/tsmux.rs @@ -312,11 +312,12 @@ impl TsMuxer { /// Build a PES packet header for a BD stream. fn build_pes_header(pid: u16, pts_90k: u64, data_len: usize) -> Vec { + use crate::consts::pes_stream_id; // Determine stream_id from PID range let stream_id: u8 = if is_video_pid(pid) { - 0xE0 // video + pes_stream_id::VIDEO } else { - 0xBD // audio, PGS subtitle, or default (private stream 1) + pes_stream_id::PRIVATE_STREAM_1 // audio, PGS subtitle, or default }; let pes_data_len = data_len + 8; // 3 header bytes + 5 PTS bytes + data @@ -332,7 +333,7 @@ fn build_pes_header(pid: u16, pts_90k: u64, data_len: usize) -> Vec { // video; `write_frame` splits oversized 0xBD access units so a private // stream always fits a bounded u16 length here. The `> 65535` arm // remains a defensive fallback for video only. - if stream_id == 0xE0 || pes_data_len > 65535 { + if stream_id == pes_stream_id::VIDEO || pes_data_len > u16::MAX as usize { header.push(0x00); header.push(0x00); } else {