Chapters, DVD subtitle palette, MKV track flags, progress total_bytes

Chapters:
- MPLS PlayList marks parsed (mark_type 1 = chapter)
- Chapter struct on DiscTitle (time_secs, name)
- MKV Chapters element with EditionEntry/ChapterAtom per mark
- 3 MPLS mark tests + 2 MKV chapter tests

DVD subtitle palette:
- IFO palette extraction (PGC offset 0xA4, 16 × YCbCr colors)
- YCbCr→RGB conversion for VobSub .idx format
- DvdSubParser codec_private returns formatted palette
- codec_data field on SubtitleStream flows through pipeline
- 5 palette tests (YCbCr conversion, formatting, overflow)

MKV track flags:
- FlagDefault: primary video/audio = 1, secondary = 0
- FlagForced: forced subtitles = 1
- Language: set from stream language code
- Already implemented, verified with 4 new tests

Progress total_bytes:
- IOStream trait: total_bytes() -> Option<u64>
- DiscStream, IsoStream: from disc_title.size_bytes
- M2tsStream, MkvStream: from file metadata on open
- NetworkStream, StdioStream, NullStream: None

316 tests total, all passing.
This commit is contained in:
MattJackson
2026-04-11 17:43:47 +00:00
parent cd575b7221
commit 96a65de3ff
21 changed files with 690 additions and 12 deletions
+19
View File
@@ -128,6 +128,7 @@ impl Disc {
codec,
language: s.language.clone(),
forced: false,
codec_data: None,
}))
} else {
Some(Stream::Audio(AudioStream {
@@ -146,6 +147,7 @@ impl Disc {
codec,
language: s.language.clone(),
forced: false,
codec_data: None,
})),
// Stream type 4 = IG, unknown types -- skip
_ => None,
@@ -153,6 +155,22 @@ impl Disc {
})
.collect();
// Convert marks to chapters (filter mark_type == 1 = chapter entry)
let first_in_time = parsed.play_items.first().map(|pi| pi.in_time).unwrap_or(0);
let chapters: Vec<Chapter> = parsed
.marks
.iter()
.filter(|m| m.mark_type == 1)
.enumerate()
.map(|(i, m)| {
let time_secs = (m.timestamp as f64 - first_in_time as f64) / 45000.0;
Chapter {
time_secs: if time_secs < 0.0 { 0.0 } else { time_secs },
name: format!("Chapter {}", i + 1),
}
})
.collect();
let playlist_num = filename.trim_end_matches(".mpls").trim_end_matches(".MPLS");
let playlist_id = playlist_num.parse::<u16>().unwrap_or(0);
@@ -163,6 +181,7 @@ impl Disc {
size_bytes: total_size,
clips,
streams,
chapters,
extents,
content_format: ContentFormat::BdTs,
})
+24
View File
@@ -97,8 +97,31 @@ impl Disc {
.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,
codec_data: codec_data.clone(),
})
})
.collect();
let mut streams = vec![video_stream.clone()];
streams.extend(audio_streams.iter().cloned());
streams.extend(subtitle_streams);
titles.push(DiscTitle {
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
@@ -107,6 +130,7 @@ impl Disc {
size_bytes,
clips: Vec::new(),
streams,
chapters: Vec::new(),
extents,
content_format: ContentFormat::MpegPs,
});
+15
View File
@@ -110,6 +110,8 @@ pub struct DiscTitle {
pub clips: Vec<Clip>,
/// All streams (video, audio, subtitle, etc.)
pub streams: Vec<Stream>,
/// Chapter points
pub chapters: Vec<Chapter>,
/// Sector extents for ripping (clip LBA ranges)
pub extents: Vec<Extent>,
/// Content format for this title
@@ -190,6 +192,8 @@ pub struct SubtitleStream {
pub language: String,
/// Whether this is a forced subtitle
pub forced: bool,
/// Pre-formatted codec private data (e.g. VobSub .idx palette header)
pub codec_data: Option<Vec<u8>>,
}
/// Video/audio codec.
@@ -231,6 +235,15 @@ pub enum ColorSpace {
Unknown,
}
/// A chapter point within a title.
#[derive(Debug, Clone)]
pub struct Chapter {
/// Chapter start time in seconds
pub time_secs: f64,
/// Chapter name (e.g. "Chapter 1", "Chapter 2")
pub name: String,
}
/// A contiguous range of sectors on disc.
#[derive(Debug, Clone, Copy)]
pub struct Extent {
@@ -310,6 +323,7 @@ impl DiscTitle {
size_bytes: 0,
clips: Vec::new(),
streams: Vec::new(),
chapters: Vec::new(),
extents: Vec::new(),
content_format: ContentFormat::BdTs,
}
@@ -1073,6 +1087,7 @@ mod tests {
secondary: false,
label: String::new(),
})],
chapters: Vec::new(),
extents: Vec::new(),
content_format: ContentFormat::BdTs,
}