1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! 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. The PID is derived from the
// stream's REAL on-wire private_stream_1 sub-stream id (assigned
// by per-codec ordinal in the IFO scan) via the same
// `dvd_audio_pid` table the demuxer's `PsPacket::dvd_pid` uses,
// so a mixed-codec title (AC-3 + DTS + LPCM) routes correctly
// instead of colliding on 0xBD00. Streams carried as a regular
// MPEG-audio PES (MP1/MP2, no sub-id) fall back to a distinct
// 0xBD00+ordinal PID — disjoint from the 0xBD80+ canonical audio
// space — though they are not routed via `dvd_pid` today.
let audio_streams: Vec<Stream> = ts
.audio_streams
.iter()
.enumerate()
.map(|(i, a)| {
let codec = a.codec;
let pid = a
.sub_stream_id
.and_then(crate::mux::ps::dvd_audio_pid)
.unwrap_or(0xBD00 + i as u16);
Stream::Audio(AudioStream {
pid,
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)| {
// VobSub sub-stream ids run 0x20..=0x3F; PID = sub-id
// (identity), shared with the demuxer via
// `dvd_subtitle_pid`.
let sub_id = 0x20u8.saturating_add(i.min(0x1F) as u8);
let pid = crate::mux::ps::dvd_subtitle_pid(sub_id).unwrap_or(sub_id as u16);
Stream::Subtitle(SubtitleStream {
pid,
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: chapter_name(i),
})
.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
}
}