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
This commit is contained in:
Matthew Jackson
2026-06-26 13:20:21 -07:00
parent decb87a250
commit d8c323bf9f
11 changed files with 212 additions and 111 deletions
+14 -14
View File
@@ -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<ClipInfo> {
/// 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<ClpiStream> {
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<ClpiStream> {
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<ClpiStream> {
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<ClpiStream> {
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.
+87
View File
@@ -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<u8> = PRIVATE_STREAM_1..=VIDEO_MAX;
}
+3 -3
View File
@@ -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<S: SectorSource>(
// 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);
+24 -23
View File
@@ -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.
+4 -4
View File
@@ -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);
+6 -3
View File
@@ -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.
+15 -14
View File
@@ -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",
_ => "",
}
}
+29 -34
View File
@@ -179,7 +179,9 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
}
// 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<Playlist> {
}
// 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<Playlist> {
}
// 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<Playlist> {
}
// 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<Playlist> {
}
// 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;
}
}
_ => {}
}
+18 -5
View File
@@ -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<W: Write> M2tsMux<W> {
/// Build a PES packet for a video access unit.
fn build_video_pes(pts_90k: u64, es: &[u8]) -> Vec<u8> {
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<u8> {
// 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<u8> {
+8 -8
View File
@@ -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<u16> {
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<usize> {
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<PsPacket> {
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,
+4 -3
View File
@@ -312,11 +312,12 @@ impl<W: Write> TsMuxer<W> {
/// Build a PES packet header for a BD stream.
fn build_pes_header(pid: u16, pts_90k: u64, data_len: usize) -> Vec<u8> {
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<u8> {
// 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 {