feat(3d): enumerate MVC dependent view (stream_type 0x20 -> H.264 video)

from_coding_type maps 0x20 (MVC dependent view) to Codec::H264, so the existing
PAT/PMT scan surfaces the SSIF right-eye substream as a second video stream on
its own PID instead of dropping it as Unknown. No new parser: the dependent eye
rides the same demux path as any other TS video stream. Removes the throwaway
3D-structure probes (findings captured in prior commit messages).
This commit is contained in:
Matthew Jackson
2026-07-12 22:16:13 -07:00
parent 9973849408
commit ef39674194
3 changed files with 5 additions and 183 deletions
-100
View File
@@ -1,100 +0,0 @@
// Prove the SSIF de-interleave: the base-view .m2ts extents are the base-view
// interleaved units INSIDE the SSIF region, so the dependent (right-eye MVC)
// view = SSIF sectors MINUS base sectors. Then confirm those dependent units
// decrypt under the same Unit Key.
// probe3d_deint <iso> <uk-hex-32> [clip=00042]
use libfreemkv::aacs::content::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, aacs_unit_encrypted, decrypt_unit};
use libfreemkv::sector::SectorSource;
use libfreemkv::{FileSectorSource, read_filesystem};
use std::path::Path;
fn hex16(s: &str) -> [u8; 16] {
let s = s.trim().trim_start_matches("0x");
let mut o = [0u8; 16];
for i in 0..16 {
o[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("hex");
}
o
}
fn main() {
let a: Vec<String> = std::env::args().collect();
let iso = &a[1];
let uk = hex16(&a[2]);
let clip = a.get(3).map(|s| s.as_str()).unwrap_or("00042");
let mut r = FileSectorSource::open(Path::new(iso)).expect("open");
let fs = read_filesystem(&mut r).expect("udf");
let base = fs
.file_extents(&mut r, &format!("/BDMV/STREAM/{clip}.m2ts"))
.expect("base extents");
let ssif = fs
.file_extents(&mut r, &format!("/BDMV/STREAM/SSIF/{clip}.ssif"))
.expect("ssif extents");
let gb = |sec: u64| sec as f64 * 2048.0 / 1e9;
let bsum: u64 = base.iter().map(|(_, c)| *c as u64).sum();
let ssum: u64 = ssif.iter().map(|(_, c)| *c as u64).sum();
println!("base: {} extents {:.2} GB", base.len(), gb(bsum));
println!("ssif: {} extents {:.2} GB", ssif.len(), gb(ssum));
// Is the base range a subset of the SSIF range? (LBA span check)
let brange = (
base.iter().map(|(l, _)| *l).min().unwrap_or(0),
base.iter().map(|(l, c)| l + c).max().unwrap_or(0),
);
let srange = (
ssif.iter().map(|(l, _)| *l).min().unwrap_or(0),
ssif.iter().map(|(l, c)| l + c).max().unwrap_or(0),
);
println!(
"base LBA span [{},{}) ssif LBA span [{},{}) base⊆ssif={}",
brange.0,
brange.1,
srange.0,
srange.1,
brange.0 >= srange.0 && brange.1 <= srange.1
);
// dependent = ssif base (per SSIF extent, subtract overlapping base ranges).
let mut b: Vec<(u32, u32)> = base.iter().map(|&(l, c)| (l, l + c)).collect();
b.sort();
let mut dep: Vec<(u32, u32)> = Vec::new();
for &(sl, sc) in &ssif {
let (s, e) = (sl, sl + sc);
let mut cur = s;
for &(bs, be) in b.iter().filter(|&&(bs, be)| be > s && bs < e) {
if bs > cur {
dep.push((cur, bs));
}
cur = cur.max(be);
}
if cur < e {
dep.push((cur, e));
}
}
let dsum: u64 = dep.iter().map(|(s, e)| (*e - *s) as u64).sum();
println!(
"\ndependent (ssif base): {} ranges {:.2} GB (expected ≈ {:.2} GB)",
dep.len(),
gb(dsum),
gb(ssum - bsum)
);
// Decrypt-test dependent-view units at several points across the ranges.
let (mut tested, mut enc, mut dec) = (0u32, 0u32, 0u32);
for &(s, e) in dep.iter().filter(|(s, e)| e - s >= ALIGNED_UNIT_SECTORS).take(200) {
let lba = s;
let mut buf = vec![0u8; ALIGNED_UNIT_LEN];
if r.read_sectors(lba, ALIGNED_UNIT_SECTORS as u16, &mut buf, false).is_ok() {
tested += 1;
if aacs_unit_encrypted(&buf) {
enc += 1;
}
if decrypt_unit(&mut buf, &uk) {
dec += 1;
}
}
}
println!("dependent-view unit decrypt: tested={tested} cpi-encrypted={enc} decrypted={dec}");
}
-82
View File
@@ -1,82 +0,0 @@
// Read-only 3D structure probe: does the disc carry a dependent (MVC) view,
// where, and how is it described? Uses freemkv's own UDF reader (7z can't parse
// these ISOs). probe3d_struct <iso> [clip=00098]
use libfreemkv::sector::SectorSource;
use libfreemkv::{FileSectorSource, read_filesystem};
use std::path::Path;
fn be16(b: &[u8], o: usize) -> u16 {
u16::from_be_bytes([b[o], b[o + 1]])
}
fn be32(b: &[u8], o: usize) -> u32 {
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let iso = &args[1];
let clip = args.get(2).map(|s| s.as_str()).unwrap_or("00098");
let mut r = FileSectorSource::open(Path::new(iso)).expect("open iso");
let fs = read_filesystem(&mut r).expect("read udf");
// Base view vs dependent-interleave file.
for (label, path) in [
("base .m2ts", format!("/BDMV/STREAM/{clip}.m2ts")),
("SSIF", format!("/BDMV/STREAM/SSIF/{clip}.ssif")),
] {
match fs.file_extents(&mut r, &path) {
Ok(exts) => {
let sectors: u64 = exts.iter().map(|(_, c)| *c as u64).sum();
println!(
"{label:<11}: EXISTS {:>5} extents {:>7.2} GB {path}",
exts.len(),
sectors as f64 * 2048.0 / 1e9
);
}
Err(_) => println!("{label:<11}: MISSING {path}"),
}
}
// MPLS ExtensionData → the 3D STN_table_SS lives here.
let mpls = match fs.read_file(&mut r, &format!("/BDMV/PLAYLIST/{clip}.mpls")) {
Ok(m) => m,
Err(e) => {
println!("\nmpls read failed: {e}");
return;
}
};
println!("\nmpls: {} bytes", mpls.len());
let ext_addr = be32(&mpls, 16) as usize; // ExtensionData_start_address
if ext_addr == 0 || ext_addr + 12 > mpls.len() {
println!("ExtensionData_start_address = {ext_addr} → NO extension data (not a 3D/SS playlist)");
return;
}
let ed = &mpls[ext_addr..];
let ed_len = be32(ed, 0) as usize;
let n_entries = ed[11] as usize; // len(4)+data_block_start(4)+reserved(3)+count(1)
println!("ExtensionData @ {ext_addr}: length={ed_len} entries={n_entries}");
for i in 0..n_entries {
let o = 12 + i * 12;
if o + 12 > ed.len() {
break;
}
let id1 = be16(ed, o);
let id2 = be16(ed, o + 2);
let addr = be32(ed, o + 4) as usize; // relative to MPLS file start
let len = be32(ed, o + 8) as usize;
let tag = match (id1, id2) {
(1, 1) => "PiP metadata",
(1, 2) => "SubPath entries (SS)",
(2, 1) => "STN_table_SS?",
(2, 2) => "STN_table_SS (MVC dependent view)",
_ => "?",
};
print!(" entry {i}: ID1={id1:#06x} ID2={id2:#06x} addr={addr} len={len} [{tag}] hex:");
let blk = &mpls[addr.min(mpls.len())..(addr + 40).min(mpls.len())];
for b in blk {
print!(" {b:02x}");
}
println!();
}
}
+5 -1
View File
@@ -811,7 +811,11 @@ impl Codec {
use crate::consts::coding_type as c;
match ct {
c::HEVC => Codec::Hevc,
c::H264 => Codec::H264,
// 0x1B base-view AVC and 0x20 MVC dependent-view (Blu-ray 3D right
// eye) are both H.264. Mapping 0x20 to video is what makes the PMT
// scan enumerate the dependent view as a second H.264 stream (its
// own PID in the SSIF) instead of dropping it — the basis of 3D.
c::H264 | c::H264_MVC => Codec::H264,
c::VC1 => Codec::Vc1,
c::MPEG2_VIDEO => Codec::Mpeg2,
c::TRUEHD => Codec::TrueHd,