AACS pipeline reshape + TrueHD metadata + central consts + clippy/fmt clean

- AACS: delete in-lib keydb parser (Step 3); boil-down primitives
  (mk_from_dk/vuk_from_mk/uk_from_vuk) + newtypes; KeySource->get_uk(ctx)+
  ResolveCtx; Unlocker->unlock()->Result<Vid,UnlockError> + AacsCertUnlocker;
  OEM bus-key gate (AacsBusKeyUnavailable); structured ResolutionTrace (Step 4).
- TrueHD: sample-rate from major-sync, Atmos label, 44.1k AU duration.
- consts: central media/format constants module; 17 duplicate const-defs
  centralized (sector/TS-packet/source-packet); mpls stream-entry + category
  codes named.
- clippy --all-targets -D warnings clean (1.86); fmt clean; 2199 lib tests.
This commit is contained in:
Matthew Jackson
2026-06-26 12:19:24 -07:00
parent 05729f5dfe
commit decb87a250
48 changed files with 2208 additions and 2031 deletions
+5 -5
View File
@@ -977,9 +977,9 @@ mod tests {
let mut parser = Mpeg2Parser::new();
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
pic1.extend_from_slice(&vec![0x11; 100]);
pic1.extend_from_slice(&[0x11; 100]);
let mut pic2 = make_picture_header(2); // P
pic2.extend_from_slice(&vec![0x22; 100]);
pic2.extend_from_slice(&[0x22; 100]);
let mut stream = pic1.clone();
stream.extend_from_slice(&pic2);
@@ -1005,7 +1005,7 @@ mod tests {
let mut au = make_picture_header(PICTURE_TYPE_I);
au.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE, 0x88, 0x00]); // pic coding ext
au.extend_from_slice(&[0x00, 0x00, 0x01, 0x01]); // slice
au.extend_from_slice(&vec![0x77; 50]);
au.extend_from_slice(&[0x77; 50]);
let frames = parse_then_flush(&mut parser, &make_pes(au.clone(), Some(0)));
assert_eq!(frames.len(), 1);
@@ -1025,12 +1025,12 @@ mod tests {
let mut parser = Mpeg2Parser::new();
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
pic1.extend_from_slice(&vec![0x11; 50]);
pic1.extend_from_slice(&[0x11; 50]);
let frames1 = parser.parse(&make_pes(pic1, Some(90000)));
assert!(frames1.is_empty(), "buffered until flush");
let mut pic2 = make_picture_header(2);
pic2.extend_from_slice(&vec![0x22; 50]);
pic2.extend_from_slice(&[0x22; 50]);
let frames2 = parser.parse(&make_pes(pic2, Some(180000)));
assert!(frames2.is_empty(), "same GOP — still buffered");
+307 -2
View File
@@ -14,9 +14,19 @@
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
/// Duration of one TrueHD access unit in nanoseconds (1/1200 second).
/// Duration of one TrueHD access unit in nanoseconds for the 48 kHz family
/// (48 / 96 / 192 kHz). `access_unit_size = 40 << (ratebits & 7)` and
/// `sample_rate = 48000 << (ratebits & 7)`; the shared shift cancels in
/// `samples_per_AU / sample_rate = 40/48000 = 1/1200 s`, so this constant is
/// exact for the whole 48 kHz family — 48, 96 and 192 kHz alike. Used as the
/// default until a major sync reveals the actual rate family.
const AU_DURATION_NS: i64 = 833_333;
/// Duration of one TrueHD access unit in nanoseconds for the 44.1 kHz family
/// (44.1 / 88.2 / 176.4 kHz): `40/44100 = 1/1102.5 s = 907_029.478… ns`. The
/// 48 kHz constant would run ~8.95 % fast on these (rare) streams.
const AU_DURATION_NS_441: i64 = 907_029;
/// Hard cap on the reassembly buffer. A valid TrueHD/MAT access unit is
/// well under 32 KiB; if the buffer grows far past that without yielding a
/// frame the stream is malformed, so we drop it and resync rather than grow
@@ -26,6 +36,12 @@ const MAX_TRUEHD_BUF: usize = 256 * 1024;
pub struct TrueHdParser {
buf: Vec<u8>,
next_pts_ns: i64,
/// Per-AU PTS increment. Defaults to the 48 kHz-family value (833_333) and
/// is refined to the 44.1 kHz-family value once the first major sync reveals
/// the actual rate. Stays at the default for streams whose major sync is not
/// yet seen (head of stream) — preserving byte-identical timing for the
/// common 48 kHz case.
au_duration_ns: i64,
}
impl Default for TrueHdParser {
@@ -39,6 +55,7 @@ impl TrueHdParser {
Self {
buf: Vec::with_capacity(32768),
next_pts_ns: 0,
au_duration_ns: AU_DURATION_NS,
}
}
@@ -185,6 +202,17 @@ impl CodecParser for TrueHdParser {
& 0xFFFF_FFFE)
== 0xF872_6FBA;
// On a major sync the 32-bit `format_info` word (immediately after
// the 4-byte sync, i.e. AU bytes 8..12) carries the rate nibble.
// Refine the per-AU PTS increment to the actual rate family. The
// 48 kHz family resolves to the unchanged 833_333 default, so the
// common case stays byte-identical; only the 44.1 kHz family shifts.
if is_major_sync && unit_bytes >= 12 {
let format_info =
u32::from_be_bytes([self.buf[8], self.buf[9], self.buf[10], self.buf[11]]);
self.au_duration_ns = truehd_au_duration_ns(format_info);
}
frames.push(Frame {
coding: None,
source: None,
@@ -194,7 +222,7 @@ impl CodecParser for TrueHdParser {
duration_ns: None,
});
self.buf.drain(..unit_bytes);
self.next_pts_ns += AU_DURATION_NS;
self.next_pts_ns += self.au_duration_ns;
}
// Bound memory on malformed input: a stream that never yields a
@@ -257,6 +285,93 @@ pub fn truehd_channels_from_stream(data: &[u8]) -> Option<u8> {
None
}
/// Real sample rate (Hz) from a TrueHD major-sync `format_info` word.
///
/// The 4-bit `ratebits` nibble sits in `format_info` bits 31..28 (the top
/// nibble), the same word `truehd_channels` reads for the channel masks. The
/// MLP rate formula is `(ratebits & 8 ? 44100 : 48000) << (ratebits & 7)`;
/// rather than evaluate it blindly this is a **strict whitelist** of the only
/// six rates that occur on real BD/UHD TrueHD. Every other code — the invalid
/// `0xF`, the formula-only `0x3`/`0xB`, and all reserved values — returns
/// `None`, so a malformed or unexpected field can never produce a wrong
/// `SamplingFrequency`; the caller falls back to its container-derived rate.
pub fn truehd_sample_rate_hz(format_info: u32) -> Option<u32> {
match (format_info >> 28) & 0xF {
0x0 => Some(48000),
0x1 => Some(96000),
0x2 => Some(192000),
0x8 => Some(44100),
0x9 => Some(88200),
0xA => Some(176400),
_ => None,
}
}
/// Per-AU PTS increment (ns) for the rate family encoded in `format_info`.
///
/// Derived from the same whitelisted rate as [`truehd_sample_rate_hz`]: the
/// 44.1 kHz family (44.1 / 88.2 / 176.4 kHz) is `907_029` ns; everything else —
/// the entire 48 kHz family AND any unrecognised rate — keeps the exact current
/// `833_333` default, so the common case and all unknown/garbage inputs are
/// byte-identical to prior behaviour.
pub fn truehd_au_duration_ns(format_info: u32) -> i64 {
match truehd_sample_rate_hz(format_info) {
Some(44100) | Some(88200) | Some(176400) => AU_DURATION_NS_441,
_ => AU_DURATION_NS,
}
}
/// First TrueHD major sync found in a demuxed elementary-stream chunk: the
/// `format_info` word plus the Atmos signal. A single scan that the per-field
/// helpers below share, so the host probes the bitstream once for channels,
/// sample rate and Atmos.
pub struct TrueHdSyncInfo {
/// The 32-bit word immediately after the 0xF8726FBA sync (channel masks +
/// rate nibble). Feed to `truehd_channels` / `truehd_sample_rate_hz`.
pub format_info: u32,
/// `num_substreams >= 4` ⟺ a 4th (Atmos object/OAMD) substream is present.
/// `num_substreams = msync[16] >> 4`, where `msync[0]` is the sync's 0xF8.
/// `None` when the AU is too short to reach that byte — never guess Atmos.
pub is_atmos: Option<bool>,
}
/// Scan a demuxed TrueHD chunk for the first major sync and return its
/// `format_info` and Atmos signal. The stream may interleave AC-3; the scan
/// advances one byte at a time and matches the sync word anywhere.
pub fn truehd_sync_info_from_stream(data: &[u8]) -> Option<TrueHdSyncInfo> {
let mut p = 0;
while p + 8 <= data.len() {
let w = u32::from_be_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]);
if (w & 0xFFFF_FFFE) == 0xF872_6FBA {
let format_info =
u32::from_be_bytes([data[p + 4], data[p + 5], data[p + 6], data[p + 7]]);
// num_substreams is the top nibble of the 17th sync byte (p + 16).
// .get() yields None — not a panic and not a false Atmos — when the
// AU is truncated before that byte.
let is_atmos = data.get(p + 16).map(|&b| (b >> 4) >= 4);
return Some(TrueHdSyncInfo {
format_info,
is_atmos,
});
}
p += 1;
}
None
}
/// Real sample rate (Hz) from the first major sync in a demuxed chunk, or
/// `None` if no major sync is found or its rate code is not whitelisted.
pub fn truehd_sample_rate_from_stream(data: &[u8]) -> Option<u32> {
truehd_sync_info_from_stream(data).and_then(|s| truehd_sample_rate_hz(s.format_info))
}
/// Whether the first major sync in a demuxed chunk carries an Atmos substream.
/// `None` when no major sync is found or the AU is too short to read the
/// substream count — callers must treat `None` as "not Atmos" (never label).
pub fn truehd_is_atmos_from_stream(data: &[u8]) -> Option<bool> {
truehd_sync_info_from_stream(data).and_then(|s| s.is_atmos)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -720,4 +835,194 @@ mod tests {
let f = parser.parse(&make_pes(vec![0x0B, 0x77, 0x00], Some(0)));
assert!(f.is_empty());
}
// --- #2 sample rate from the major-sync rate nibble ---
/// Build a `format_info` word with the given `ratebits` (top nibble) and a
/// 7.1 8-channel mask (ch8 = 0x1F) in the low 13 bits — exactly the layout
/// §1.A pins, so the rate nibble and the channel masks are co-located in one
/// real word.
fn format_info_with(ratebits: u32) -> u32 {
((ratebits & 0xF) << 28) | 0x1F
}
#[test]
fn sample_rate_whitelist_real_rates() {
assert_eq!(truehd_sample_rate_hz(format_info_with(0x0)), Some(48000));
assert_eq!(truehd_sample_rate_hz(format_info_with(0x1)), Some(96000));
assert_eq!(truehd_sample_rate_hz(format_info_with(0x2)), Some(192000));
assert_eq!(truehd_sample_rate_hz(format_info_with(0x8)), Some(44100));
assert_eq!(truehd_sample_rate_hz(format_info_with(0x9)), Some(88200));
assert_eq!(truehd_sample_rate_hz(format_info_with(0xA)), Some(176400));
}
#[test]
fn sample_rate_unknown_rate_falls_back_to_none() {
// 0xF is the explicit invalid code; 0x3/0xB are formula-only and not
// whitelisted; 0x7/0xE are reserved. None of them may produce a rate —
// the host must fall back to its container value, never write a wrong
// SamplingFrequency.
for bad in [0x3u32, 0x7, 0xB, 0xC, 0xD, 0xE, 0xF] {
assert_eq!(
truehd_sample_rate_hz(format_info_with(bad)),
None,
"ratebits {bad:#x} must not yield a rate"
);
}
}
#[test]
fn sample_rate_nibble_does_not_disturb_channel_decode() {
// Internal-consistency guard: with the 96 kHz nibble AND a 7.1 mask in
// the same word, the rate reads 96000 and the channels still read 8 —
// proving the rate nibble (bits 31..28) and the channel masks
// (bits 19..0) do not collide.
let fi = format_info_with(0x1);
assert_eq!(truehd_sample_rate_hz(fi), Some(96000));
assert_eq!(truehd_channels(fi), Some(8));
}
#[test]
fn sample_rate_from_stream_scans_major_sync() {
// [junk][0xF8726FBA][format_info: ratebits=0x1 (96k), ch8=0x1F]
let mut data = vec![0xAA, 0xBB];
data.extend_from_slice(&0xF872_6FBAu32.to_be_bytes());
data.extend_from_slice(&format_info_with(0x1).to_be_bytes());
assert_eq!(truehd_sample_rate_from_stream(&data), Some(96000));
}
#[test]
fn sample_rate_from_stream_none_without_sync() {
let data = vec![0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
assert_eq!(truehd_sample_rate_from_stream(&data), None);
}
// --- #3 per-AU duration: family-aware, 48 kHz family byte-identical ---
#[test]
fn au_duration_48k_family_unchanged() {
// 48 / 96 / 192 kHz (ratebits 0x0/0x1/0x2) all keep the exact current
// 833_333 constant — the common case must never shift.
for rb in [0x0u32, 0x1, 0x2] {
assert_eq!(truehd_au_duration_ns(format_info_with(rb)), 833_333);
}
}
#[test]
fn au_duration_441k_family_is_907029() {
// 44.1 / 88.2 / 176.4 kHz (ratebits 0x8/0x9/0xA) → 907_029 ns.
for rb in [0x8u32, 0x9, 0xA] {
assert_eq!(truehd_au_duration_ns(format_info_with(rb)), 907_029);
}
}
#[test]
fn au_duration_unknown_rate_keeps_default() {
// An unrecognised/garbage rate nibble must not pick the 44.1 k value
// (note 0xF & 8 != 0): it falls back to the 833_333 default.
for rb in [0x3u32, 0x7, 0xB, 0xF] {
assert_eq!(truehd_au_duration_ns(format_info_with(rb)), 833_333);
}
}
#[test]
fn parser_44k_major_sync_sets_907029_increment() {
// Two AUs: the first carries a major sync with ratebits=0x8 (44.1 k).
// After the parser reads it, the per-AU PTS increment must be 907_029.
let mut parser = TrueHdParser::new();
let mut a1 = make_truehd_unit(200);
a1[4..8].copy_from_slice(&0xF872_6FBAu32.to_be_bytes()); // major sync
a1[8..12].copy_from_slice(&format_info_with(0x8).to_be_bytes()); // 44.1 k
let mut data = a1;
data.extend_from_slice(&make_truehd_unit(200));
let frames = parser.parse(&make_pes(data, Some(90000)));
assert_eq!(frames.len(), 2);
assert_eq!(
frames[1].pts_ns - frames[0].pts_ns,
907_029,
"44.1 k-family AU increments by 907_029 once the major sync is read"
);
}
#[test]
fn parser_48k_major_sync_keeps_833333_increment() {
// Regression: a 48 k-family (ratebits=0x0) major sync keeps the exact
// current 833_333 increment.
let mut parser = TrueHdParser::new();
let mut a1 = make_truehd_unit(200);
a1[4..8].copy_from_slice(&0xF872_6FBAu32.to_be_bytes());
a1[8..12].copy_from_slice(&format_info_with(0x0).to_be_bytes()); // 48 k
let mut data = a1;
data.extend_from_slice(&make_truehd_unit(200));
let frames = parser.parse(&make_pes(data, Some(90000)));
assert_eq!(frames.len(), 2);
assert_eq!(frames[1].pts_ns - frames[0].pts_ns, 833_333);
}
// --- #1 Atmos detection from num_substreams (msync[16] >> 4) ---
/// Build a demuxed chunk with one major sync whose 17th sync byte (offset
/// 16 from the 0xF8) has top nibble `num_substreams`. The AU is padded past
/// byte 16 so the substream count is reachable.
fn major_sync_with_substreams(num_substreams: u8) -> Vec<u8> {
let mut data = vec![0x00, 0x00]; // leading junk; scan is byte-aligned
let sync_off = data.len();
data.extend_from_slice(&0xF872_6FBAu32.to_be_bytes()); // bytes [off..off+4]
data.extend_from_slice(&format_info_with(0x0).to_be_bytes()); // format_info
// Pad up to and including byte `sync_off + 16`.
while data.len() <= sync_off + 16 {
data.push(0x00);
}
data[sync_off + 16] = (num_substreams & 0xF) << 4;
data
}
#[test]
fn atmos_true_when_four_substreams() {
// num_substreams = 4 → byte 16 = 0x40 → Atmos object substream present.
let data = major_sync_with_substreams(4);
assert_eq!(truehd_is_atmos_from_stream(&data), Some(true));
}
#[test]
fn atmos_false_when_three_substreams() {
// num_substreams = 3 (plain 7.1 TrueHD) → byte 16 = 0x30 → not Atmos.
let data = major_sync_with_substreams(3);
assert_eq!(truehd_is_atmos_from_stream(&data), Some(false));
}
#[test]
fn atmos_none_when_au_too_short_for_substream_byte() {
// Major sync present but the chunk ends before byte sync_off+16 → None,
// never a false Atmos. Sync at offset 0; only format_info follows.
let mut data = 0xF872_6FBAu32.to_be_bytes().to_vec();
data.extend_from_slice(&format_info_with(0x0).to_be_bytes()); // 8 bytes total
assert_eq!(truehd_is_atmos_from_stream(&data), None);
}
#[test]
fn atmos_none_without_major_sync() {
let data = vec![0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
assert_eq!(truehd_is_atmos_from_stream(&data), None);
}
#[test]
fn sync_info_combines_channels_rate_and_atmos() {
// One scan yields all three facts: 7.1 channels, 96 kHz, 4 substreams.
let data = {
let mut d = vec![0x00, 0x00];
let off = d.len();
d.extend_from_slice(&0xF872_6FBAu32.to_be_bytes());
d.extend_from_slice(&format_info_with(0x1).to_be_bytes()); // 96k + 7.1
while d.len() <= off + 16 {
d.push(0x00);
}
d[off + 16] = 0x40; // 4 substreams
d
};
let info = truehd_sync_info_from_stream(&data).expect("major sync found");
assert_eq!(truehd_channels(info.format_info), Some(8));
assert_eq!(truehd_sample_rate_hz(info.format_info), Some(96000));
assert_eq!(info.is_atmos, Some(true));
}
}
+6 -6
View File
@@ -1050,10 +1050,10 @@ mod tests {
/// codecPrivate, or the A-segment decodes against the wrong entry point.
#[test]
fn vc1_emits_entry_point_revert_to_first_value() {
let sh = vec![0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xAA, 0xBB];
let sh = [0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xAA, 0xBB];
let ep_a = vec![0x00, 0x00, 0x01, SC_ENTRY_POINT, 0x11, 0x22];
let ep_b = vec![0x00, 0x00, 0x01, SC_ENTRY_POINT, 0x33, 0x44, 0x55];
let frame = vec![0x00, 0x00, 0x01, SC_FRAME, 0x77];
let frame = [0x00, 0x00, 0x01, SC_FRAME, 0x77];
let mut parser = Vc1Parser::new();
@@ -1106,10 +1106,10 @@ mod tests {
/// seek points carry valid decoder state (SMPTE 421M).
#[test]
fn vc1_reasserts_active_headers_at_bare_keyframe() {
let sh_a = vec![0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xAA, 0xBB];
let sh_a = [0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xAA, 0xBB];
let ep_a = vec![0x00, 0x00, 0x01, SC_ENTRY_POINT, 0x11, 0x22];
let ep_b = vec![0x00, 0x00, 0x01, SC_ENTRY_POINT, 0x33, 0x44, 0x55];
let frame = vec![0x00, 0x00, 0x01, SC_FRAME, 0x77];
let frame = [0x00, 0x00, 0x01, SC_FRAME, 0x77];
let mut parser = Vc1Parser::new();
@@ -1154,10 +1154,10 @@ mod tests {
/// violating SMPTE 421M. After the fix, assembly is always seq-then-entry.
#[test]
fn vc1_keyframe_prefix_order_seq_unchanged_entry_redefined() {
let sh = vec![0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xAA, 0xBB, 0xCC];
let sh = [0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xAA, 0xBB, 0xCC];
let ep_a = vec![0x00, 0x00, 0x01, SC_ENTRY_POINT, 0x11, 0x22];
let ep_b = vec![0x00, 0x00, 0x01, SC_ENTRY_POINT, 0x33, 0x44, 0x55];
let frame = vec![0x00, 0x00, 0x01, SC_FRAME, 0x77];
let frame = [0x00, 0x00, 0x01, SC_FRAME, 0x77];
let mut parser = Vc1Parser::new();
+3 -3
View File
@@ -268,15 +268,15 @@ mod tests {
/// absorb). ISO 13818-1 packet layout: sync 0x47 at TS offset 0 (BD off 4).
fn bdts_pes_packet(pid: u16, payload: &[u8]) -> Vec<u8> {
const SYNC: u8 = 0x47;
const TS_PAYLOAD: usize = 184;
use crate::consts::TS_PAYLOAD_BYTES;
let mut pes = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
pes.extend_from_slice(payload);
assert!(pes.len() <= TS_PAYLOAD);
assert!(pes.len() <= TS_PAYLOAD_BYTES);
let mut pkt = vec![0u8; 192];
pkt[4] = SYNC;
pkt[5] = (((pid >> 8) as u8) & 0x1F) | 0x40; // PUSI
pkt[6] = (pid & 0xFF) as u8;
let pad = TS_PAYLOAD - pes.len();
let pad = TS_PAYLOAD_BYTES - pes.len();
if pad == 0 {
pkt[7] = 0x10; // payload only
pkt[8..8 + pes.len()].copy_from_slice(&pes);
+9 -9
View File
@@ -7,11 +7,11 @@
use crate::error::Error;
use std::io::{self, Write};
const TS_PACKET_SIZE: usize = 188;
use crate::consts::TS_PACKET_BYTES;
/// Header is 4 bytes, leaving 184 bytes for the adaptation field area
/// plus payload. With a 1-byte `adaptation_field_length` prefix the
/// field body + stuffing can be at most 183 bytes.
const MAX_AF_LEN: usize = TS_PACKET_SIZE - 4 - 1;
const MAX_AF_LEN: usize = TS_PACKET_BYTES - 4 - 1;
const SYNC_BYTE: u8 = 0x47;
const STUFF_BYTE: u8 = 0xFF;
@@ -20,14 +20,14 @@ const STUFF_BYTE: u8 = 0xFF;
/// bytes when [`pad_to_188`](Self::pad_to_188) is called; if it's not
/// called the caller is responsible for filling the packet exactly.
pub(super) struct Packet {
buf: [u8; TS_PACKET_SIZE],
buf: [u8; TS_PACKET_BYTES],
len: usize,
}
impl Packet {
pub(super) fn new() -> Self {
Self {
buf: [0u8; TS_PACKET_SIZE],
buf: [0u8; TS_PACKET_BYTES],
len: 0,
}
}
@@ -36,14 +36,14 @@ impl Packet {
/// never reached by the sole caller (mod.rs sizes every field to sum
/// to 188); the bound prevents a future caller from corrupting memory.
fn push(&mut self, b: u8) {
if self.len < TS_PACKET_SIZE {
if self.len < TS_PACKET_BYTES {
self.buf[self.len] = b;
self.len += 1;
}
}
fn extend(&mut self, bytes: &[u8]) {
let n = bytes.len().min(TS_PACKET_SIZE - self.len);
let n = bytes.len().min(TS_PACKET_BYTES - self.len);
self.buf[self.len..self.len + n].copy_from_slice(&bytes[..n]);
self.len += n;
}
@@ -111,7 +111,7 @@ impl Packet {
/// packet past 188 bytes — overflow is a muxer invariant break, not
/// something to silently emit.
pub(super) fn append_payload(&mut self, payload: &[u8]) -> io::Result<()> {
if self.len + payload.len() > TS_PACKET_SIZE {
if self.len + payload.len() > TS_PACKET_BYTES {
return Err(Error::M2tsPacketMalformed.into());
}
self.extend(payload);
@@ -123,7 +123,7 @@ impl Packet {
/// For PSI packets only — payload-carrying packets reserve room for
/// stuffing via `append_adaptation`.
pub(super) fn pad_to_188(&mut self) {
while self.len < TS_PACKET_SIZE {
while self.len < TS_PACKET_BYTES {
self.push(STUFF_BYTE);
}
}
@@ -155,7 +155,7 @@ impl<W: Write> PacketWriter<W> {
// Hard check, not a debug_assert: a non-188-byte packet would
// corrupt the transport stream, so refuse to write it in any
// build rather than emitting a short/long packet silently.
if bytes.len() != TS_PACKET_SIZE {
if bytes.len() != TS_PACKET_BYTES {
return Err(Error::M2tsPacketMalformed.into());
}
self.inner.write_all(bytes)
+11 -12
View File
@@ -32,8 +32,7 @@ const SUPPORTED_VERSION: u8 = 1;
/// Index of the version byte within [`MAGIC`].
const VERSION_BYTE: usize = 5;
/// BD-TS packet size (header must be padded to this boundary).
const PACKET_SIZE: usize = 192;
use crate::consts::BD_SOURCE_PACKET_BYTES;
/// Metadata embedded in an m2ts file.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -310,15 +309,15 @@ pub fn write_header(w: &mut impl Write, meta: &M2tsMeta) -> io::Result<()> {
// real stream metadata, but a v1.0 primitive shouldn't truncate.
let json_len = u32::try_from(json.len()).map_err(|_| crate::error::Error::NoMetadata)?;
let raw_len = 8 + 4 + json.len(); // magic + len + json
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(BD_SOURCE_PACKET_BYTES) * BD_SOURCE_PACKET_BYTES;
let padding = padded_len - raw_len;
w.write_all(&MAGIC)?;
w.write_all(&json_len.to_be_bytes())?;
w.write_all(&json)?;
if padding > 0 {
// Padding is at most PACKET_SIZE-1 bytes — stack buffer, no heap alloc.
let pad = [0u8; PACKET_SIZE];
// Padding is at most BD_SOURCE_PACKET_BYTES-1 bytes — stack buffer, no heap alloc.
let pad = [0u8; BD_SOURCE_PACKET_BYTES];
w.write_all(&pad[..padding])?;
}
Ok(())
@@ -375,13 +374,13 @@ pub fn read_header(r: &mut impl Read) -> io::Result<Option<M2tsMeta>> {
let meta: M2tsMeta =
serde_json::from_slice(&json_buf).map_err(|_| crate::error::Error::NoMetadata)?;
// Skip padding to next 192-byte boundary (at most PACKET_SIZE-1 bytes →
// Skip padding to next 192-byte boundary (at most BD_SOURCE_PACKET_BYTES-1 bytes →
// a stack buffer, no heap allocation).
let raw_len = 8 + 4 + json_len;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(BD_SOURCE_PACKET_BYTES) * BD_SOURCE_PACKET_BYTES;
let padding = padded_len - raw_len;
if padding > 0 {
let mut skip = [0u8; PACKET_SIZE];
let mut skip = [0u8; BD_SOURCE_PACKET_BYTES];
r.read_exact(&mut skip[..padding])?;
}
@@ -512,7 +511,7 @@ mod tests {
assert_eq!(back.streams.len(), 1);
// Header is padded to a 192-byte boundary; the cursor must land
// exactly there so the following BD-TS data stays aligned.
assert_eq!(cursor.position() as usize % PACKET_SIZE, 0);
assert_eq!(cursor.position() as usize % BD_SOURCE_PACKET_BYTES, 0);
}
#[test]
@@ -643,7 +642,7 @@ mod tests {
#[test]
fn write_header_pads_to_192_byte_boundary() {
// The total written length must always be a multiple of PACKET_SIZE
// The total written length must always be a multiple of BD_SOURCE_PACKET_BYTES
// (192). Test a range of JSON sizes by varying stream count.
for n_streams in 0..6 {
let mut t = DiscTitle::empty();
@@ -665,7 +664,7 @@ mod tests {
let mut buf = Vec::new();
write_header(&mut buf, &meta).unwrap();
assert_eq!(
buf.len() % PACKET_SIZE,
buf.len() % BD_SOURCE_PACKET_BYTES,
0,
"header for {n_streams} streams (len {}) not 192-aligned",
buf.len()
@@ -766,7 +765,7 @@ mod tests {
let mut cur = io::Cursor::new(buf);
read_header(&mut cur).unwrap().expect("header present");
assert_eq!(cur.position() as usize, header_len);
assert_eq!(header_len % PACKET_SIZE, 0);
assert_eq!(header_len % BD_SOURCE_PACKET_BYTES, 0);
let mut next = [0u8; 1];
use std::io::Read as _;
cur.read_exact(&mut next).unwrap();
+1 -1
View File
@@ -4113,7 +4113,7 @@ mod tests {
}
/// Read the body bytes of a direct TrackEntry child element by ID.
fn track_entry_child_body<'a>(data: &'a [u8], id: u32) -> Option<&'a [u8]> {
fn track_entry_child_body(data: &[u8], id: u32) -> Option<&[u8]> {
let (te_start, te_size) = first_track_entry(data);
let (_, body_start, body_size) = master_children(data, te_start, te_size)
.into_iter()
+9 -1
View File
@@ -729,8 +729,16 @@ fn parse_track(
};
let res = Resolution::from_height(ph);
let chs = AudioChannels::from_count(ch);
let srs = if sr >= 96000.0 {
let srs = if sr >= 192000.0 {
SampleRate::S192
} else if sr >= 176400.0 {
SampleRate::S176_4
} else if sr >= 96000.0 {
SampleRate::S96
} else if sr >= 88200.0 {
SampleRate::S88_2
} else if (44100.0..48000.0).contains(&sr) {
SampleRate::S44_1
} else {
SampleRate::S48
};
+44 -42
View File
@@ -6,11 +6,9 @@
//!
//! 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;
use crate::consts::BD_SOURCE_PACKET_BYTES;
/// Standard TS packet size.
const TS_PACKET_SIZE: usize = 188;
use crate::consts::TS_PACKET_BYTES;
/// TS sync byte.
const SYNC_BYTE: u8 = 0x47;
@@ -259,13 +257,13 @@ impl TsDemuxer {
// 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();
let need = BD_SOURCE_PACKET_BYTES - 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];
let mut boundary = [0u8; BD_SOURCE_PACKET_BYTES];
boundary[..self.remainder.len()].copy_from_slice(&self.remainder);
boundary[self.remainder.len()..].copy_from_slice(&data[..need]);
self.remainder.clear();
@@ -279,10 +277,10 @@ impl TsDemuxer {
}
// 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];
while offset + BD_SOURCE_PACKET_BYTES <= data.len() {
let packet = &data[offset..offset + BD_SOURCE_PACKET_BYTES];
let src = self.pkt_source(offset);
offset += BD_TS_PACKET_SIZE;
offset += BD_SOURCE_PACKET_BYTES;
self.process_packet(packet, src, &mut completed);
}
// Advance the running base past every byte consumed this feed so the
@@ -295,7 +293,7 @@ impl TsDemuxer {
// prevent unbounded growth on a desynchronised stream).
if offset < data.len() {
let leftover = &data[offset..];
if leftover.len() < BD_TS_PACKET_SIZE {
if leftover.len() < BD_SOURCE_PACKET_BYTES {
self.remainder.extend_from_slice(leftover);
} else {
self.remainder.clear();
@@ -353,7 +351,7 @@ impl TsDemuxer {
4
};
if payload_start >= TS_PACKET_SIZE {
if payload_start >= TS_PACKET_BYTES {
return;
}
// adaptation == 0x02 → AF only, no payload.
@@ -532,7 +530,7 @@ fn is_resync_point(data: &[u8], offset: usize) -> bool {
if data.get(offset + 4) != Some(&SYNC_BYTE) {
return false;
}
match data.get(offset + BD_TS_PACKET_SIZE + 4) {
match data.get(offset + BD_SOURCE_PACKET_BYTES + 4) {
Some(&b) => b == SYNC_BYTE,
None => true, // last packet in the buffer — no follower to corroborate
}
@@ -544,7 +542,7 @@ fn is_resync_point(data: &[u8], offset: usize) -> bool {
/// Accounts for the adaptation_field_control (bits 5:4 of the 4th TS header
/// byte). Returns `None` when the packet carries no payload (AFC 0b10 = AF
/// only, or the reserved 0b00) or when the adaptation field length runs past
/// the packet. `pkt` must be at least [`BD_TS_PACKET_SIZE`] bytes.
/// the packet. `pkt` must be at least [`BD_SOURCE_PACKET_BYTES`] bytes.
fn psi_payload_base(pkt: &[u8]) -> Option<usize> {
// TS header is pkt[4..]; byte pkt[7] holds AFC in bits 5:4.
let afc = (pkt[7] >> 4) & 0x03;
@@ -555,7 +553,7 @@ fn psi_payload_base(pkt: &[u8]) -> Option<usize> {
// payload starts after it.
let af_len = pkt[8] as usize;
let base = 9 + af_len; // 4 + 4 + 1(length byte) + af_len
if base < BD_TS_PACKET_SIZE {
if base < BD_SOURCE_PACKET_BYTES {
Some(base)
} else {
None // AF overruns the packet
@@ -587,7 +585,7 @@ fn psi_payload_base(pkt: &[u8]) -> Option<usize> {
/// matching section is found.
fn collect_psi_section(data: &[u8], target_pid: u16, table_id: u8) -> Option<Vec<u8>> {
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
while offset + BD_SOURCE_PACKET_BYTES <= data.len() {
if !is_resync_point(data, offset) {
offset += 1;
continue;
@@ -599,12 +597,13 @@ fn collect_psi_section(data: &[u8], target_pid: u16, table_id: u8) -> Option<Vec
// Locate the payload (pointer_field) accounting for any
// adaptation field. A packet with no payload (AF only) or an
// AF that overruns the packet is skipped.
let Some(payload_off) = psi_payload_base(&data[offset..offset + BD_TS_PACKET_SIZE])
let Some(payload_off) =
psi_payload_base(&data[offset..offset + BD_SOURCE_PACKET_BYTES])
else {
offset += BD_TS_PACKET_SIZE;
offset += BD_SOURCE_PACKET_BYTES;
continue;
};
let payload = &data[offset + payload_off..offset + BD_TS_PACKET_SIZE];
let payload = &data[offset + payload_off..offset + BD_SOURCE_PACKET_BYTES];
// pointer_field is the FIRST payload byte; the section starts
// pointer_field bytes after it. Bound the start to within
// THIS packet's payload — a pointer that runs into the next
@@ -612,7 +611,7 @@ fn collect_psi_section(data: &[u8], target_pid: u16, table_id: u8) -> Option<Vec
let pointer = payload[0] as usize;
let sec_start = 1 + pointer;
if sec_start + 3 > payload.len() || payload[sec_start] != table_id {
offset += BD_TS_PACKET_SIZE;
offset += BD_SOURCE_PACKET_BYTES;
continue;
}
let section_len =
@@ -631,9 +630,9 @@ fn collect_psi_section(data: &[u8], target_pid: u16, table_id: u8) -> Option<Vec
// dropped/duplicated packet → the assembled section is corrupt, so
// abandon it rather than splicing in misordered payload.
let mut expected_cc = ((data[offset + 7] & 0x0F) + 1) & 0x0F;
let mut scan = offset + BD_TS_PACKET_SIZE;
let mut scan = offset + BD_SOURCE_PACKET_BYTES;
let mut desync = false;
while scan + BD_TS_PACKET_SIZE <= data.len() && section.len() < total {
while scan + BD_SOURCE_PACKET_BYTES <= data.len() && section.len() < total {
// Require a corroborated resync point (this sync byte plus the
// follower one packet ahead) before trusting the header. A
// stray 0x47 in corrupt payload would otherwise misread the CC
@@ -653,16 +652,19 @@ fn collect_psi_section(data: &[u8], target_pid: u16, table_id: u8) -> Option<Vec
expected_cc = (cc + 1) & 0x0F;
// Continuation packets may also carry an adaptation
// field; compute their payload base the same way.
if let Some(cbase) = psi_payload_base(&data[scan..scan + BD_TS_PACKET_SIZE]) {
section.extend_from_slice(&data[scan + cbase..scan + BD_TS_PACKET_SIZE]);
if let Some(cbase) =
psi_payload_base(&data[scan..scan + BD_SOURCE_PACKET_BYTES])
{
section
.extend_from_slice(&data[scan + cbase..scan + BD_SOURCE_PACKET_BYTES]);
}
}
scan += BD_TS_PACKET_SIZE;
scan += BD_SOURCE_PACKET_BYTES;
}
if desync {
// Restart PSI assembly from the next packet after this PUSI;
// a later clean copy of the section may still appear.
offset += BD_TS_PACKET_SIZE;
offset += BD_SOURCE_PACKET_BYTES;
continue;
}
if section.len() >= total {
@@ -672,7 +674,7 @@ fn collect_psi_section(data: &[u8], target_pid: u16, table_id: u8) -> Option<Vec
// Incomplete section (truncated input) — stop looking.
return None;
}
offset += BD_TS_PACKET_SIZE;
offset += BD_SOURCE_PACKET_BYTES;
}
None
}
@@ -833,7 +835,7 @@ mod tests {
/// continuity_counter, carrying `payload` (truncated/padded to 184 bytes,
/// payload-only adaptation).
fn ts_payload_packet(pid: u16, pusi: bool, cc: u8, payload: &[u8]) -> Vec<u8> {
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
if pusi {
@@ -934,7 +936,7 @@ mod tests {
/// 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];
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
// 4-byte timecode prefix is ignored; leave zero.
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
@@ -1010,7 +1012,7 @@ mod tests {
/// Build a 192-byte BD-TS data packet on `pid` carrying `payload`
/// (payload-only adaptation, truncated/padded to fit one packet).
fn data_packet(pid: u16, pusi: bool, payload: &[u8]) -> Vec<u8> {
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
if pusi {
@@ -1018,7 +1020,7 @@ mod tests {
}
pkt[6] = (pid & 0xFF) as u8;
pkt[7] = 0x10; // payload only, no adaptation field
let room = TS_PACKET_SIZE - 4; // 184 ES bytes after the 4-byte TS header
let room = TS_PACKET_BYTES - 4; // 184 ES bytes after the 4-byte TS header
let n = payload.len().min(room);
pkt[8..8 + n].copy_from_slice(&payload[..n]);
pkt
@@ -1029,7 +1031,7 @@ mod tests {
/// payload base computation in scan_streams.
fn pmt_packet_with_af(pmt_pid: u16, entries: &[(u8, u16)]) -> Vec<u8> {
let af_len: u8 = 2; // 1 flags byte + 1 stuffing byte
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = (((pmt_pid >> 8) as u8) & 0x1F) | 0x40; // PUSI set
pkt[6] = (pmt_pid & 0xFF) as u8;
@@ -1039,7 +1041,7 @@ mod tests {
pkt[10] = 0xFF; // stuffing
// Payload (PSI) begins at 4 + 4 + 1 + af_len = 11.
let payload_off = 4 + 4 + 1 + af_len as usize;
let mut body = vec![0xFFu8; BD_TS_PACKET_SIZE - payload_off];
let mut body = vec![0xFFu8; BD_SOURCE_PACKET_BYTES - payload_off];
body[0] = 0x00; // pointer_field
let s = 1;
body[s] = 0x02; // table_id = PMT
@@ -1270,9 +1272,9 @@ mod tests {
}
let mut pmt = pmt_two_packets(pmt_pid, &entries);
// Corrupt the continuation packet's CC. pmt is exactly two BD-TS
// packets; the second starts at BD_TS_PACKET_SIZE. Its CC (low nibble
// packets; the second starts at BD_SOURCE_PACKET_BYTES. Its CC (low nibble
// of offset+7) was set to 1 by pmt_two_packets; flip it to a gap (5).
let cc_off = BD_TS_PACKET_SIZE + 7;
let cc_off = BD_SOURCE_PACKET_BYTES + 7;
pmt[cc_off] = (pmt[cc_off] & 0xF0) | 0x05;
let mut data = pat_packet(pmt_pid);
@@ -1296,16 +1298,16 @@ mod tests {
/// bytes the demuxer must produce, unlike `data_packet` which leaves
/// zero padding that a length-0 (unbounded) PES would absorb as ES.
fn es_packet_exact(pid: u16, pusi: bool, payload: &[u8]) -> Vec<u8> {
const TS_PAYLOAD: usize = 184;
assert!(payload.len() <= TS_PAYLOAD);
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
use crate::consts::TS_PAYLOAD_BYTES;
assert!(payload.len() <= TS_PAYLOAD_BYTES);
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
if pusi {
pkt[5] |= 0x40;
}
pkt[6] = (pid & 0xFF) as u8;
let pad = TS_PAYLOAD - payload.len();
let pad = TS_PAYLOAD_BYTES - payload.len();
if pad == 0 {
pkt[7] = 0x10; // payload only
pkt[8..8 + payload.len()].copy_from_slice(payload);
@@ -1550,7 +1552,7 @@ mod tests {
demux.feed(&es_packet_exact(pid, true, &start));
// …then an AF-only continuation packet whose "payload" bytes must
// be discarded.
let mut afonly = vec![0u8; BD_TS_PACKET_SIZE];
let mut afonly = vec![0u8; BD_SOURCE_PACKET_BYTES];
afonly[4] = SYNC_BYTE;
afonly[5] = ((pid >> 8) as u8) & 0x1F; // no PUSI
afonly[6] = (pid & 0xFF) as u8;
@@ -1576,7 +1578,7 @@ mod tests {
// the TS packet. The AF bytes must NOT appear in the ES.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = (((pid >> 8) as u8) & 0x1F) | 0x40; // PUSI
pkt[6] = (pid & 0xFF) as u8;
@@ -1616,7 +1618,7 @@ mod tests {
// A larger value runs past the packet and must be discarded.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = (((pid >> 8) as u8) & 0x1F) | 0x40;
pkt[6] = (pid & 0xFF) as u8;
@@ -1808,7 +1810,7 @@ mod tests {
#[test]
fn scan_streams_no_pat_returns_none() {
// Without a PAT (table_id 0x00 on PID 0) there is no program to find.
let data = vec![0u8; BD_TS_PACKET_SIZE * 2]; // all zero, no sync bytes
let data = vec![0u8; BD_SOURCE_PACKET_BYTES * 2]; // all zero, no sync bytes
assert!(scan_streams(&data).is_none());
}
+17 -13
View File
@@ -8,7 +8,7 @@ use super::hevc::{hvcc_to_annex_b, length_prefixed_to_annex_b};
use std::io::{self, Write};
const SYNC_BYTE: u8 = 0x47;
const TS_PAYLOAD: usize = 184;
use crate::consts::TS_PAYLOAD_BYTES;
/// PID range treated as video (HEVC, triggers Annex-B conversion + RAI
/// on keyframes). Both `write_frame` and `build_pes_header` consult this
@@ -204,21 +204,21 @@ impl<W: Write> TsMuxer<W> {
let remaining = pes_len - offset;
// Invariant: TP_extra(4) + TS_header(4) + AF(af_bytes) + payload(payload_len) = 192,
// i.e. af_bytes + payload_len = TS_PAYLOAD (184).
// i.e. af_bytes + payload_len = TS_PAYLOAD_BYTES (184).
// RAI on first packet of a keyframe video PES requires AF with flags=0x40.
let want_rai = first && keyframe && is_video;
// Pick payload_len and af_bytes per case.
let (af_bytes, payload_len): (usize, usize) = if want_rai {
// Minimum AF = 2 bytes (length=1, flags=0x40). Payload caps at 182.
let max_payload = TS_PAYLOAD - 2;
let max_payload = TS_PAYLOAD_BYTES - 2;
let p = remaining.min(max_payload);
(TS_PAYLOAD - p, p)
} else if remaining >= TS_PAYLOAD {
(0, TS_PAYLOAD) // no AF, full payload
(TS_PAYLOAD_BYTES - p, p)
} else if remaining >= TS_PAYLOAD_BYTES {
(0, TS_PAYLOAD_BYTES) // no AF, full payload
} else {
// Stuffing-only AF, payload = remaining.
(TS_PAYLOAD - remaining, remaining)
(TS_PAYLOAD_BYTES - remaining, remaining)
};
// TP_extra_header (4 bytes — arrival time, set to 0)
@@ -363,7 +363,7 @@ fn build_pes_header(pid: u16, pts_90k: u64, data_len: usize) -> Vec<u8> {
mod tests {
use super::*;
const BD_PACKET_SIZE: usize = 192;
use crate::consts::BD_SOURCE_PACKET_BYTES;
const VIDEO_PID: u16 = 0x1011;
/// Parsed BD-TS packet (192 bytes total: 4 TP_extra + 4 TS header + 184 body).
@@ -381,8 +381,8 @@ mod tests {
/// Walk 192-byte BD-TS packets.
fn parse_bd_ts(buf: &[u8]) -> Vec<TsPacket> {
let mut out = Vec::new();
for chunk in buf.chunks(BD_PACKET_SIZE) {
if chunk.len() != BD_PACKET_SIZE {
for chunk in buf.chunks(BD_SOURCE_PACKET_BYTES) {
if chunk.len() != BD_SOURCE_PACKET_BYTES {
break;
}
// Skip TP_extra_header (4 bytes), parse TS header.
@@ -762,9 +762,13 @@ mod tests {
mux.finish().unwrap();
}
assert!(!sink.is_empty());
assert_eq!(sink.len() % BD_PACKET_SIZE, 0, "output must be 192-aligned");
for chunk in sink.chunks(BD_PACKET_SIZE) {
assert_eq!(chunk.len(), BD_PACKET_SIZE);
assert_eq!(
sink.len() % BD_SOURCE_PACKET_BYTES,
0,
"output must be 192-aligned"
);
for chunk in sink.chunks(BD_SOURCE_PACKET_BYTES) {
assert_eq!(chunk.len(), BD_SOURCE_PACKET_BYTES);
assert_eq!(chunk[4], SYNC_BYTE, "TS sync byte at offset 4");
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ pub const FVI_TIMESCALE: u64 = 1_000_000_000;
/// Bytes per `src.sector` unit (`docs/FVI_FORMAT.md` §6.2, §9). The highway's
/// [`SourcePos`] counts 2048-byte logical sectors.
pub const FVI_SECTOR_SIZE: u32 = 2048;
pub const FVI_SECTOR_SIZE: u32 = crate::consts::SECTOR_BYTES as u32;
// ── Logical model (serialization-independent) ────────────────────────────────