Unified Stream trait: read() and write() on one type
Stream trait: read() returns PesFrame, write() accepts PesFrame. A stream is a stream — you read from it or write to it. No separate Input/Output traits. API: libfreemkv::input(url) and libfreemkv::output(url, title, codecs) Returns Box<dyn Stream>.
This commit is contained in:
+57
-20
@@ -363,8 +363,7 @@ fn ref_aes_cbc_encrypt(key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) {
|
||||
/// The standard AACS IV, copied here independently so we are NOT importing
|
||||
/// the library's constant — this IS the cross-validation reference value.
|
||||
const CROSS_AACS_IV: [u8; 16] = [
|
||||
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3,
|
||||
0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
|
||||
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
|
||||
];
|
||||
|
||||
/// Build a plaintext aligned unit with TS sync markers and recognisable
|
||||
@@ -373,8 +372,8 @@ const CROSS_AACS_IV: [u8; 16] = [
|
||||
#[test]
|
||||
fn aacs_cross_validation_encrypt_then_decrypt() {
|
||||
let unit_key: [u8; 16] = [
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
|
||||
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32,
|
||||
0x10,
|
||||
];
|
||||
|
||||
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||
@@ -403,7 +402,11 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
||||
for i in 0..16 {
|
||||
dk[i] = derived[i] ^ header[i];
|
||||
}
|
||||
ref_aes_cbc_encrypt(&dk, &CROSS_AACS_IV, &mut plaintext[16..aacs::ALIGNED_UNIT_LEN]);
|
||||
ref_aes_cbc_encrypt(
|
||||
&dk,
|
||||
&CROSS_AACS_IV,
|
||||
&mut plaintext[16..aacs::ALIGNED_UNIT_LEN],
|
||||
);
|
||||
|
||||
// Sanity: ciphertext should differ
|
||||
assert_ne!(
|
||||
@@ -414,7 +417,10 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
||||
|
||||
// -- Decrypt with the library --
|
||||
let ok = aacs::decrypt_unit(&mut plaintext, &unit_key);
|
||||
assert!(ok, "decrypt_unit returned false (TS sync verification failed)");
|
||||
assert!(
|
||||
ok,
|
||||
"decrypt_unit returned false (TS sync verification failed)"
|
||||
);
|
||||
assert_eq!(plaintext[0] & 0xC0, 0x00, "encryption flag not cleared");
|
||||
|
||||
// Compare (byte 0 flag was cleared)
|
||||
@@ -432,8 +438,8 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
||||
#[test]
|
||||
fn aacs_cross_validation_alternate_key() {
|
||||
let unit_key: [u8; 16] = [
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08,
|
||||
];
|
||||
|
||||
let mut plaintext = vec![0xFFu8; aacs::ALIGNED_UNIT_LEN];
|
||||
@@ -452,7 +458,11 @@ fn aacs_cross_validation_alternate_key() {
|
||||
for i in 0..16 {
|
||||
dk[i] = derived[i] ^ header[i];
|
||||
}
|
||||
ref_aes_cbc_encrypt(&dk, &CROSS_AACS_IV, &mut plaintext[16..aacs::ALIGNED_UNIT_LEN]);
|
||||
ref_aes_cbc_encrypt(
|
||||
&dk,
|
||||
&CROSS_AACS_IV,
|
||||
&mut plaintext[16..aacs::ALIGNED_UNIT_LEN],
|
||||
);
|
||||
|
||||
assert!(aacs::decrypt_unit(&mut plaintext, &unit_key));
|
||||
|
||||
@@ -469,8 +479,8 @@ fn aacs_cross_validation_alternate_key() {
|
||||
#[test]
|
||||
fn aacs_bus_decrypt_cross_validation() {
|
||||
let read_data_key: [u8; 16] = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
||||
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00,
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
|
||||
0x00,
|
||||
];
|
||||
|
||||
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||
@@ -552,10 +562,22 @@ fn css_roundtrip_with_snapshot() {
|
||||
#[test]
|
||||
fn css_roundtrip_multiple_keys() {
|
||||
let cases: &[([u8; 5], [u8; 5])] = &[
|
||||
([0x00, 0x00, 0x00, 0x00, 0x00], [0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
([0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
|
||||
([0x01, 0x02, 0x03, 0x04, 0x05], [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]),
|
||||
([0xAB, 0xCD, 0xEF, 0x01, 0x23], [0x12, 0x34, 0x56, 0x78, 0x9A]),
|
||||
(
|
||||
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
),
|
||||
(
|
||||
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
|
||||
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
|
||||
),
|
||||
(
|
||||
[0x01, 0x02, 0x03, 0x04, 0x05],
|
||||
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
|
||||
),
|
||||
(
|
||||
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
|
||||
[0x12, 0x34, 0x56, 0x78, 0x9A],
|
||||
),
|
||||
];
|
||||
|
||||
for (idx, (key, seed)) in cases.iter().enumerate() {
|
||||
@@ -594,11 +616,26 @@ fn css_roundtrip_multiple_keys() {
|
||||
#[test]
|
||||
fn css_stevenson_attack_validates_cracked_key() {
|
||||
let candidates: &[([u8; 5], [u8; 5])] = &[
|
||||
([0x42, 0x13, 0x37, 0xBE, 0xEF], [0x11, 0x22, 0x33, 0x44, 0x55]),
|
||||
([0x01, 0x02, 0x03, 0x04, 0x05], [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]),
|
||||
([0x10, 0x20, 0x30, 0x40, 0x50], [0x05, 0x06, 0x07, 0x08, 0x09]),
|
||||
([0xAB, 0xCD, 0xEF, 0x01, 0x23], [0x12, 0x34, 0x56, 0x78, 0x9A]),
|
||||
([0x55, 0xAA, 0x55, 0xAA, 0x55], [0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
(
|
||||
[0x42, 0x13, 0x37, 0xBE, 0xEF],
|
||||
[0x11, 0x22, 0x33, 0x44, 0x55],
|
||||
),
|
||||
(
|
||||
[0x01, 0x02, 0x03, 0x04, 0x05],
|
||||
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
|
||||
),
|
||||
(
|
||||
[0x10, 0x20, 0x30, 0x40, 0x50],
|
||||
[0x05, 0x06, 0x07, 0x08, 0x09],
|
||||
),
|
||||
(
|
||||
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
|
||||
[0x12, 0x34, 0x56, 0x78, 0x9A],
|
||||
),
|
||||
(
|
||||
[0x55, 0xAA, 0x55, 0xAA, 0x55],
|
||||
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
),
|
||||
];
|
||||
|
||||
let mut any_cracked = false;
|
||||
|
||||
+131
-6
@@ -143,9 +143,15 @@ fn scan_options_with_keydb_pathbuf() {
|
||||
|
||||
// ── detect_format integration tests ───────────────────────────────────────
|
||||
|
||||
use libfreemkv::{Codec, ColorSpace, ContentFormat, HdrFormat, Stream, VideoStream};
|
||||
use libfreemkv::{
|
||||
Codec, ColorSpace, ContentFormat, FrameRate, HdrFormat, Resolution, Stream, VideoStream,
|
||||
};
|
||||
|
||||
fn title_with_video(codec: Codec, resolution: &str, content_format: ContentFormat) -> DiscTitle {
|
||||
fn title_with_video(
|
||||
codec: Codec,
|
||||
resolution: Resolution,
|
||||
content_format: ContentFormat,
|
||||
) -> DiscTitle {
|
||||
DiscTitle {
|
||||
playlist: "00800.mpls".into(),
|
||||
playlist_id: 800,
|
||||
@@ -155,8 +161,8 @@ fn title_with_video(codec: Codec, resolution: &str, content_format: ContentForma
|
||||
streams: vec![Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec,
|
||||
resolution: resolution.into(),
|
||||
frame_rate: "23.976".into(),
|
||||
resolution,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Sdr,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
@@ -191,13 +197,13 @@ fn disc_title_duration_display_edge_cases() {
|
||||
|
||||
#[test]
|
||||
fn content_format_default_bdts() {
|
||||
let t = title_with_video(Codec::H264, "1080p", ContentFormat::BdTs);
|
||||
let t = title_with_video(Codec::H264, Resolution::R1080p, ContentFormat::BdTs);
|
||||
assert_eq!(t.content_format, ContentFormat::BdTs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_format_dvd_mpegps() {
|
||||
let t = title_with_video(Codec::Mpeg2, "480i", ContentFormat::MpegPs);
|
||||
let t = title_with_video(Codec::Mpeg2, Resolution::R480i, ContentFormat::MpegPs);
|
||||
assert_eq!(t.content_format, ContentFormat::MpegPs);
|
||||
}
|
||||
|
||||
@@ -368,6 +374,125 @@ fn resolve_encryption_no_aacs_dir() {
|
||||
assert!(disc.aacs.is_none(), "aacs should be None without /AACS dir");
|
||||
}
|
||||
|
||||
// ── Batch count arithmetic tests ──────────────────────────────────────────
|
||||
// Regression tests for the u16 truncation bug: when (remaining as u16) was
|
||||
// used instead of remaining.min(batch as u32) as u16, any remaining count
|
||||
// that was a multiple of 65536 would truncate to 0, causing an infinite loop.
|
||||
|
||||
/// Simulates the fixed batch count calculation from pipe.rs / drive.rs
|
||||
fn safe_batch_count(remaining: u32, batch_sectors: u16) -> u16 {
|
||||
remaining.min(batch_sectors as u32) as u16
|
||||
}
|
||||
|
||||
/// Simulates the BUGGY calculation that caused the infinite loop
|
||||
fn buggy_batch_count(remaining: u32, batch_sectors: u16) -> u16 {
|
||||
(remaining as u16).min(batch_sectors)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_normal() {
|
||||
// Normal case: remaining > batch_sectors
|
||||
assert_eq!(safe_batch_count(1000, 60), 60);
|
||||
assert_eq!(safe_batch_count(47533152, 60), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_last_batch() {
|
||||
// Last batch: remaining < batch_sectors
|
||||
assert_eq!(safe_batch_count(30, 60), 30);
|
||||
assert_eq!(safe_batch_count(1, 60), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_exact_boundary() {
|
||||
// Exact boundary: remaining == batch_sectors
|
||||
assert_eq!(safe_batch_count(60, 60), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_u16_overflow_regression() {
|
||||
// THE BUG: remaining is a multiple of 65536 → truncates to 0
|
||||
// 47513600 = 725 * 65536, lower 16 bits = 0
|
||||
let remaining: u32 = 47533152 - 19552; // = 47513600
|
||||
assert_eq!(remaining, 47513600);
|
||||
assert_eq!(remaining % 65536, 0, "remaining should be multiple of 65536");
|
||||
|
||||
// Buggy version produces 0 → infinite loop
|
||||
assert_eq!(buggy_batch_count(remaining, 60), 0);
|
||||
|
||||
// Fixed version produces 60
|
||||
assert_eq!(safe_batch_count(remaining, 60), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_other_u16_overflow_values() {
|
||||
// Other multiples of 65536
|
||||
assert_eq!(safe_batch_count(65536, 60), 60);
|
||||
assert_eq!(safe_batch_count(131072, 60), 60);
|
||||
assert_eq!(safe_batch_count(65536 * 100, 60), 60);
|
||||
|
||||
// Verify buggy version fails on all of these
|
||||
assert_eq!(buggy_batch_count(65536, 60), 0);
|
||||
assert_eq!(buggy_batch_count(131072, 60), 0);
|
||||
assert_eq!(buggy_batch_count(65536 * 100, 60), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_near_u16_boundary() {
|
||||
// Values just below and above 65536
|
||||
assert_eq!(safe_batch_count(65535, 60), 60);
|
||||
assert_eq!(safe_batch_count(65536, 60), 60);
|
||||
assert_eq!(safe_batch_count(65537, 60), 60);
|
||||
|
||||
// Buggy: 65535 as u16 = 65535, min(60) = 60 (OK by accident)
|
||||
assert_eq!(buggy_batch_count(65535, 60), 60);
|
||||
// Buggy: 65536 as u16 = 0, min(60) = 0 (BUG)
|
||||
assert_eq!(buggy_batch_count(65536, 60), 0);
|
||||
// Buggy: 65537 as u16 = 1, min(60) = 1 (wrong but doesn't loop)
|
||||
assert_eq!(buggy_batch_count(65537, 60), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_real_disc_sizes() {
|
||||
let batch: u16 = 60;
|
||||
|
||||
// DVD-5: ~2,295,104 sectors
|
||||
assert_eq!(safe_batch_count(2295104, batch), 60);
|
||||
|
||||
// BD-25: ~12,219,392 sectors
|
||||
assert_eq!(safe_batch_count(12219392, batch), 60);
|
||||
|
||||
// BD-50: ~24,438,784 sectors
|
||||
assert_eq!(safe_batch_count(24438784, batch), 60);
|
||||
|
||||
// UHD BD-66: ~33,554,432 sectors
|
||||
assert_eq!(safe_batch_count(33554432, batch), 60);
|
||||
|
||||
// UHD BD-100: ~47,533,152 sectors
|
||||
assert_eq!(safe_batch_count(47533152, batch), 60);
|
||||
|
||||
// Last few sectors of each
|
||||
assert_eq!(safe_batch_count(52, batch), 52);
|
||||
assert_eq!(safe_batch_count(3, batch), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_zero_remaining() {
|
||||
// Zero remaining should produce 0 (loop exits before this)
|
||||
assert_eq!(safe_batch_count(0, 60), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_max_batch_sizes() {
|
||||
// Test with different batch sizes used by detect_max_batch_sectors
|
||||
for &batch in &[3u16, 6, 9, 30, 60, 120, 240, 510] {
|
||||
// Large remaining should always return batch
|
||||
assert_eq!(safe_batch_count(47533152, batch), batch);
|
||||
// Small remaining should return remaining
|
||||
assert_eq!(safe_batch_count(1, batch), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_encryption_no_keydb() {
|
||||
// A UDF image with /AACS directory but no keydb path -> aacs is None
|
||||
|
||||
+38
-38
@@ -15,8 +15,8 @@ fn sample_disc_title() -> DiscTitle {
|
||||
Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: Codec::Hevc,
|
||||
resolution: "2160p".into(),
|
||||
frame_rate: "23.976".into(),
|
||||
resolution: Resolution::R2160p,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Hdr10,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
@@ -25,18 +25,18 @@ fn sample_disc_title() -> DiscTitle {
|
||||
Stream::Audio(AudioStream {
|
||||
pid: 0x1100,
|
||||
codec: Codec::TrueHd,
|
||||
channels: "7.1".into(),
|
||||
channels: AudioChannels::Surround71,
|
||||
language: "eng".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: "English Atmos".into(),
|
||||
}),
|
||||
Stream::Audio(AudioStream {
|
||||
pid: 0x1101,
|
||||
codec: Codec::Ac3,
|
||||
channels: "5.1".into(),
|
||||
channels: AudioChannels::Surround51,
|
||||
language: "fra".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: "French".into(),
|
||||
}),
|
||||
@@ -201,7 +201,7 @@ fn m2ts_meta_roundtrip() {
|
||||
// Check video
|
||||
if let Stream::Video(v) = &restored.streams[0] {
|
||||
assert_eq!(v.codec, Codec::Hevc);
|
||||
assert_eq!(v.resolution, "2160p");
|
||||
assert_eq!(v.resolution, Resolution::R2160p);
|
||||
assert_eq!(v.label, "Main");
|
||||
} else {
|
||||
panic!("expected video");
|
||||
@@ -413,8 +413,8 @@ fn meta_codec_roundtrip() {
|
||||
streams.push(Stream::Video(VideoStream {
|
||||
pid: (0x1011 + i) as u16,
|
||||
codec,
|
||||
resolution: "1080p".into(),
|
||||
frame_rate: "23.976".into(),
|
||||
resolution: Resolution::R1080p,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Sdr,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
@@ -425,9 +425,9 @@ fn meta_codec_roundtrip() {
|
||||
streams.push(Stream::Audio(AudioStream {
|
||||
pid: (0x1100 + i) as u16,
|
||||
codec,
|
||||
channels: "5.1".into(),
|
||||
channels: AudioChannels::Surround51,
|
||||
language: "eng".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: String::new(),
|
||||
}));
|
||||
@@ -509,8 +509,8 @@ fn meta_all_stream_types() {
|
||||
Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: Codec::Hevc,
|
||||
resolution: "2160p".into(),
|
||||
frame_rate: "23.976".into(),
|
||||
resolution: Resolution::R2160p,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Hdr10,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
@@ -519,9 +519,9 @@ fn meta_all_stream_types() {
|
||||
Stream::Audio(AudioStream {
|
||||
pid: 0x1100,
|
||||
codec: Codec::TrueHd,
|
||||
channels: "7.1".into(),
|
||||
channels: AudioChannels::Surround71,
|
||||
language: "eng".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: "Primary Audio".into(),
|
||||
}),
|
||||
@@ -535,9 +535,9 @@ fn meta_all_stream_types() {
|
||||
Stream::Audio(AudioStream {
|
||||
pid: 0x1110,
|
||||
codec: Codec::Ac3,
|
||||
channels: "stereo".into(),
|
||||
channels: AudioChannels::Stereo,
|
||||
language: "eng".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: true,
|
||||
label: "Commentary".into(),
|
||||
}),
|
||||
@@ -553,7 +553,7 @@ fn meta_all_stream_types() {
|
||||
// Video preserved
|
||||
if let Stream::Video(v) = &restored.streams[0] {
|
||||
assert_eq!(v.codec, Codec::Hevc);
|
||||
assert_eq!(v.resolution, "2160p");
|
||||
assert_eq!(v.resolution, Resolution::R2160p);
|
||||
assert_eq!(v.label, "Primary");
|
||||
assert!(!v.secondary);
|
||||
} else {
|
||||
@@ -563,7 +563,7 @@ fn meta_all_stream_types() {
|
||||
// Primary audio preserved
|
||||
if let Stream::Audio(a) = &restored.streams[1] {
|
||||
assert_eq!(a.codec, Codec::TrueHd);
|
||||
assert_eq!(a.channels, "7.1");
|
||||
assert_eq!(a.channels, AudioChannels::Surround71);
|
||||
assert!(!a.secondary);
|
||||
} else {
|
||||
panic!("expected audio");
|
||||
@@ -642,9 +642,9 @@ fn mkvstream_roundtrip_bdts() {
|
||||
streams: vec![Stream::Audio(AudioStream {
|
||||
pid: 0x1100,
|
||||
codec: Codec::Ac3,
|
||||
channels: "5.1".into(),
|
||||
channels: AudioChannels::Surround51,
|
||||
language: "eng".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: "English".into(),
|
||||
})],
|
||||
@@ -688,8 +688,8 @@ fn mkvstream_meta_preserves_all_streams() {
|
||||
Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: Codec::H264,
|
||||
resolution: "1080p".into(),
|
||||
frame_rate: "23.976".into(),
|
||||
resolution: Resolution::R1080p,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Sdr,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
@@ -698,18 +698,18 @@ fn mkvstream_meta_preserves_all_streams() {
|
||||
Stream::Audio(AudioStream {
|
||||
pid: 0x1100,
|
||||
codec: Codec::Ac3,
|
||||
channels: "5.1".into(),
|
||||
channels: AudioChannels::Surround51,
|
||||
language: "eng".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: "English".into(),
|
||||
}),
|
||||
Stream::Audio(AudioStream {
|
||||
pid: 0x1101,
|
||||
codec: Codec::DtsHdMa,
|
||||
channels: "7.1".into(),
|
||||
channels: AudioChannels::Surround71,
|
||||
language: "fra".into(),
|
||||
sample_rate: "48kHz".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
label: "French".into(),
|
||||
}),
|
||||
@@ -781,8 +781,8 @@ fn mkvstream_e2e_h264_produces_valid_mkv() {
|
||||
streams: vec![Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: Codec::H264,
|
||||
resolution: "1080p".into(),
|
||||
frame_rate: "23.976".into(),
|
||||
resolution: Resolution::R1080p,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Sdr,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
@@ -803,7 +803,7 @@ fn mkvstream_e2e_h264_produces_valid_mkv() {
|
||||
// SPS NAL (type 7): minimal valid SPS
|
||||
es_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); // start code
|
||||
es_data.push(0x67); // NAL type 7 (SPS), nal_ref_idc=3
|
||||
// Minimal SPS payload: profile_idc=66 (Baseline), constraint flags, level_idc=30
|
||||
// Minimal SPS payload: profile_idc=66 (Baseline), constraint flags, level_idc=30
|
||||
es_data.extend_from_slice(&[
|
||||
0x42, 0xC0, 0x1E, // profile=66, constraint_set0=1, level=30
|
||||
0xD9, 0x00, 0xA0, 0x47, 0xFE, 0x88, // minimal SPS rbsp
|
||||
@@ -817,7 +817,7 @@ fn mkvstream_e2e_h264_produces_valid_mkv() {
|
||||
// IDR NAL (type 5): keyframe
|
||||
es_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); // start code
|
||||
es_data.push(0x65); // NAL type 5 (IDR), nal_ref_idc=3
|
||||
// Some IDR slice data
|
||||
// Some IDR slice data
|
||||
es_data.extend_from_slice(&[0x88, 0x84, 0x00, 0x21, 0xFF, 0xFE, 0xF6, 0xE2]);
|
||||
// Pad to reasonable size
|
||||
es_data.extend_from_slice(&[0x00; 64]);
|
||||
@@ -961,7 +961,11 @@ fn mkvstream_e2e_h264_produces_valid_mkv() {
|
||||
let data = output2.lock().unwrap().clone().into_inner();
|
||||
|
||||
// Verify output starts with EBML magic (0x1A45DFA3)
|
||||
assert!(data.len() >= 4, "MKV output too small: {} bytes", data.len());
|
||||
assert!(
|
||||
data.len() >= 4,
|
||||
"MKV output too small: {} bytes",
|
||||
data.len()
|
||||
);
|
||||
assert_eq!(
|
||||
&data[0..4],
|
||||
&[0x1A, 0x45, 0xDF, 0xA3],
|
||||
@@ -970,17 +974,13 @@ fn mkvstream_e2e_h264_produces_valid_mkv() {
|
||||
|
||||
// Verify output contains a Tracks element (0x1654AE6B)
|
||||
let tracks_needle = [0x16, 0x54, 0xAE, 0x6B];
|
||||
let has_tracks = data
|
||||
.windows(4)
|
||||
.any(|w| w == tracks_needle);
|
||||
let has_tracks = data.windows(4).any(|w| w == tracks_needle);
|
||||
assert!(has_tracks, "output should contain Tracks element");
|
||||
|
||||
// Verify codecPrivate is non-empty (not all zeros)
|
||||
// CodecPrivate element ID is 0x63A2
|
||||
let cp_needle = [0x63, 0xA2];
|
||||
let cp_pos = data
|
||||
.windows(2)
|
||||
.position(|w| w == cp_needle);
|
||||
let cp_pos = data.windows(2).position(|w| w == cp_needle);
|
||||
if let Some(pos) = cp_pos {
|
||||
// After the ID, there's a size VINT, then the data
|
||||
let after_id = pos + 2;
|
||||
|
||||
Reference in New Issue
Block a user