WO-2 (delete SectorReader trait): - The 0.18 trait split into SectorSource (read-only) and SectorSink (write-only) is final; the legacy SectorReader alias was a bridge. - Renames every internal &mut dyn SectorReader (~25 sites) to &mut dyn SectorSource. The trait method capacity() becomes capacity_sectors() with a default of 0 (preserves SectorReader's default-0 behavior). - Deletes the SectorReader trait, its blanket-to-Source bridge, and the FileSectorReader type alias. Adds explicit forwarding impls for Box<dyn SectorSource> and &mut dyn SectorSource so generic decorators like DecryptingSectorSource<S: SectorSource> compose. WO-3a (extract Disc::patch): - Moves Disc::patch (1230 lines) and bytes_bad_in_title from disc/mod.rs into disc/patch.rs as a split inherent impl. Zero behavior change — pure mechanical relocation. disc/mod.rs drops from 3,945 to 2,714 LOC. WO-6 (partial): - Deletes src/labels/png_filenames.rs — was a 72-LOC stub with detect() returning false, never wired into the PARSERS registry. project docs doc drift fixes (audited 2026-05-13): - JUMP_BASE_SECTORS: 256→1024 (64 MB base for UHD, not 8 MB) - PASSN_DAMAGE_THRESHOLD_PCT: 12→6 - PASSN_SKIP_SECTORS_BASE: 64→32 - MAX_RANGE_SECS=180: replaced by proportional range_sectors × 25, capped at RANGE_BUDGET_CAP_SECS=1800.
134 lines
4.9 KiB
Rust
134 lines
4.9 KiB
Rust
//! DVD title scanning — IFO parsing, stream mapping, VOB extent building.
|
|
|
|
use super::*;
|
|
use crate::ifo;
|
|
use crate::sector::SectorSource;
|
|
use crate::udf;
|
|
|
|
impl Disc {
|
|
/// Scan DVD titles from IFO files (VIDEO_TS.IFO + VTS_XX_0.IFO).
|
|
pub(super) fn scan_dvd_titles(
|
|
reader: &mut dyn SectorSource,
|
|
udf_fs: &udf::UdfFs,
|
|
) -> Vec<DiscTitle> {
|
|
let dvd_info = match ifo::parse_vmg(reader, udf_fs) {
|
|
Ok(info) => info,
|
|
Err(_) => return Vec::new(),
|
|
};
|
|
|
|
let mut titles = Vec::new();
|
|
let mut title_number: u16 = 0;
|
|
|
|
for ts in &dvd_info.title_sets {
|
|
let video_stream = Stream::Video(VideoStream {
|
|
pid: 0xE0, // DVD video PID (standard MPEG PS video stream)
|
|
codec: ts.video.codec,
|
|
resolution: ts.video.resolution,
|
|
frame_rate: match ts.video.standard.as_str() {
|
|
"PAL" => FrameRate::F25,
|
|
_ => FrameRate::F29_97,
|
|
},
|
|
hdr: HdrFormat::Sdr,
|
|
color_space: ColorSpace::Bt709,
|
|
secondary: false,
|
|
label: String::new(),
|
|
});
|
|
|
|
// Map DvdAudioAttr to Stream::Audio
|
|
let audio_streams: Vec<Stream> = ts
|
|
.audio_streams
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, a)| {
|
|
let codec = a.codec;
|
|
Stream::Audio(AudioStream {
|
|
pid: 0xBD00 + i as u16, // DVD private stream 1 sub-IDs
|
|
codec,
|
|
channels: AudioChannels::from_count(a.channels),
|
|
language: a.language.clone(),
|
|
sample_rate: SampleRate::from_hz(a.sample_rate),
|
|
secondary: false,
|
|
purpose: crate::disc::LabelPurpose::Normal,
|
|
label: String::new(),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for dvd_title in &ts.titles {
|
|
title_number += 1;
|
|
|
|
// Build extents from cell sector ranges (absolute = vob_start + cell offset)
|
|
let extents: Vec<Extent> = dvd_title
|
|
.cells
|
|
.iter()
|
|
.map(|cell| {
|
|
let start = ts.vob_start_sector.saturating_add(cell.first_sector);
|
|
let count = cell
|
|
.last_sector
|
|
.saturating_sub(cell.first_sector)
|
|
.saturating_add(1);
|
|
Extent {
|
|
start_lba: start,
|
|
sector_count: count,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let size_bytes: u64 = extents.iter().map(|e| e.sector_count as u64 * 2048).sum();
|
|
|
|
// Build pre-formatted palette codec_data for VobSub subtitle streams
|
|
let codec_data = dvd_title
|
|
.palette
|
|
.as_ref()
|
|
.map(|pal| crate::mux::codec::dvdsub::format_palette(pal));
|
|
|
|
// Map DvdSubtitleAttr to Stream::Subtitle
|
|
let subtitle_streams: Vec<Stream> = ts
|
|
.subtitle_streams
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, s)| {
|
|
Stream::Subtitle(SubtitleStream {
|
|
pid: 0x20 + i as u16, // DVD sub-stream IDs 0x20-0x3F
|
|
codec: Codec::DvdSub,
|
|
language: s.language.clone(),
|
|
forced: false,
|
|
qualifier: crate::disc::LabelQualifier::None,
|
|
codec_data: codec_data.clone(),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let mut streams = vec![video_stream.clone()];
|
|
streams.extend(audio_streams.iter().cloned());
|
|
streams.extend(subtitle_streams);
|
|
|
|
let chapters: Vec<Chapter> = dvd_title
|
|
.chapter_times
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &t)| Chapter {
|
|
time_secs: t,
|
|
name: format!("Chapter {}", i + 1),
|
|
})
|
|
.collect();
|
|
|
|
titles.push(DiscTitle {
|
|
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
|
|
playlist_id: title_number,
|
|
duration_secs: dvd_title.duration_secs,
|
|
size_bytes,
|
|
clips: Vec::new(),
|
|
streams,
|
|
chapters,
|
|
extents,
|
|
content_format: ContentFormat::MpegPs,
|
|
codec_privates: Vec::new(),
|
|
});
|
|
}
|
|
}
|
|
|
|
titles
|
|
}
|
|
}
|