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:
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) ────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user