From 34e84c4082d142721595ccde6a9c4c518cab96c7 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:08:45 -0700 Subject: [PATCH] Refactor Stream to enum with typed variants (Video, Audio, Subtitle) Each stream type has only its relevant fields. No more HDR on audio or channels on video. Added SubtitleStream.forced field (TODO: parse). Removed display helpers from lib (belongs in CLI). --- src/disc.rs | 192 +++++++++++++++++++++++++--------------------------- src/lib.rs | 3 +- 2 files changed, 96 insertions(+), 99 deletions(-) diff --git a/src/disc.rs b/src/disc.rs index 34b0c04..0660497 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -91,39 +91,63 @@ pub struct Clip { /// A stream within a title. #[derive(Debug, Clone)] -pub struct Stream { - /// Stream type - pub kind: StreamKind, +pub enum Stream { + Video(VideoStream), + Audio(AudioStream), + Subtitle(SubtitleStream), +} + +/// A video stream. +#[derive(Debug, Clone)] +pub struct VideoStream { /// MPEG-TS packet ID pub pid: u16, - /// Codec + /// Codec (HEVC, H.264, VC-1, MPEG-2) pub codec: Codec, - /// ISO 639-2 language code (e.g. "eng", "fra") - pub language: String, - /// Video resolution (e.g. "2160p", "1080p") + /// Resolution (e.g. "2160p", "1080p", "1080i") pub resolution: String, - /// Frame rate (e.g. "23.976") + /// Frame rate (e.g. "23.976", "25") pub frame_rate: String, - /// Channel layout (e.g. "5.1", "7.1", "stereo") - pub channels: String, - /// Sample rate (e.g. "48kHz") - pub sample_rate: String, /// HDR format pub hdr: HdrFormat, /// Color space pub color_space: ColorSpace, - /// Whether this is a secondary/enhancement stream + /// Whether this is a secondary stream (PiP, Dolby Vision EL) pub secondary: bool, /// Extra label (e.g. "Dolby Vision EL") pub label: String, } -/// Stream type. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum StreamKind { - Video, - Audio, - Subtitle, +/// An audio stream. +#[derive(Debug, Clone)] +pub struct AudioStream { + /// MPEG-TS packet ID + pub pid: u16, + /// Codec (TrueHD, DTS-HD MA, DD, LPCM, etc.) + pub codec: Codec, + /// Channel layout (e.g. "5.1", "7.1", "stereo", "mono") + pub channels: String, + /// ISO 639-2 language code (e.g. "eng", "fra") + pub language: String, + /// Sample rate (e.g. "48kHz", "96kHz") + pub sample_rate: String, + /// Whether this is a secondary stream (commentary) + pub secondary: bool, + /// Extra label + pub label: String, +} + +/// A subtitle stream. +#[derive(Debug, Clone)] +pub struct SubtitleStream { + /// MPEG-TS packet ID + pub pid: u16, + /// Codec (PGS) + pub codec: Codec, + /// ISO 639-2 language code (e.g. "eng", "fra") + pub language: String, + /// Whether this is a forced subtitle + pub forced: bool, } /// Video/audio codec. @@ -251,46 +275,6 @@ impl Title { } } -impl Stream { - /// Human-readable one-line description. - pub fn display(&self) -> String { - match self.kind { - StreamKind::Video => { - let mut parts = vec![self.codec.name().to_string()]; - if !self.resolution.is_empty() { parts.push(self.resolution.clone()); } - if !self.frame_rate.is_empty() { parts.push(format!("{}fps", self.frame_rate)); } - if self.hdr != HdrFormat::Sdr { parts.push(self.hdr.name().to_string()); } - if self.color_space != ColorSpace::Unknown && self.color_space != ColorSpace::Bt709 { - parts.push(self.color_space.name().to_string()); - } - if self.secondary { parts.push(format!("[{}]", self.label)); } - parts.join(" ") - } - StreamKind::Audio => { - let mut parts = vec![self.codec.name().to_string()]; - if !self.channels.is_empty() { parts.push(self.channels.clone()); } - if !self.sample_rate.is_empty() { parts.push(self.sample_rate.clone()); } - if !self.language.is_empty() { parts.push(format!("({})", self.language)); } - if self.secondary { parts.push("[secondary]".to_string()); } - parts.join(" ") - } - StreamKind::Subtitle => { - let mut parts = vec![self.codec.name().to_string()]; - if !self.language.is_empty() { parts.push(format!("({})", self.language)); } - parts.join(" ") - } - } - } - - /// Kind as a display string - pub fn kind_name(&self) -> &'static str { - match self.kind { - StreamKind::Video => "Video", - StreamKind::Audio => "Audio", - StreamKind::Subtitle => "Subtitle", - } - } -} // ─── AACS state ───────────────────────────────────────────────────────────── @@ -576,17 +560,16 @@ impl Disc { /// Detect disc format from the main title's video streams. fn detect_format(titles: &[Title]) -> DiscFormat { - // Check the longest title (first after sort) for 2160p HEVC for title in titles.iter().take(3) { for stream in &title.streams { - if stream.kind == StreamKind::Video { - if stream.resolution.contains("2160") { + if let Stream::Video(v) = stream { + if v.resolution.contains("2160") { return DiscFormat::Uhd; } - if stream.resolution.contains("1080") || stream.resolution.contains("720") { + if v.resolution.contains("1080") || v.resolution.contains("720") { return DiscFormat::BluRay; } - if stream.resolution.contains("480") || stream.resolution.contains("576") { + if v.resolution.contains("480") || v.resolution.contains("576") { return DiscFormat::Dvd; } } @@ -706,41 +689,54 @@ impl Disc { // Build streams from STN table let streams: Vec = parsed.streams.iter().map(|s| { - let kind = match s.stream_type { - 1 | 6 | 7 => StreamKind::Video, - 2 | 5 => StreamKind::Audio, - 3 => StreamKind::Subtitle, - _ => StreamKind::Video, - }; let codec = Codec::from_coding_type(s.coding_type); - let hdr = match s.dynamic_range { - 1 => HdrFormat::Hdr10, - 2 => HdrFormat::DolbyVision, - _ => HdrFormat::Sdr, - }; - let cs = match s.color_space { - 1 => ColorSpace::Bt709, - 2 => ColorSpace::Bt2020, - _ => ColorSpace::Unknown, - }; - let label = match s.stream_type { - 5 => "secondary".to_string(), - 7 => "Dolby Vision EL".to_string(), - _ => String::new(), - }; - Stream { - kind, - pid: s.pid, - codec, - language: s.language.clone(), - resolution: format_resolution(s.video_format, s.video_rate), - frame_rate: format_framerate(s.video_rate), - channels: format_channels(s.audio_format), - sample_rate: format_samplerate(s.audio_rate), - hdr, - color_space: cs, - secondary: s.secondary, - label, + match s.stream_type { + 1 | 6 | 7 => Stream::Video(VideoStream { + pid: s.pid, + codec, + resolution: format_resolution(s.video_format, s.video_rate), + frame_rate: format_framerate(s.video_rate), + hdr: match s.dynamic_range { + 1 => HdrFormat::Hdr10, + 2 => HdrFormat::DolbyVision, + _ => HdrFormat::Sdr, + }, + color_space: match s.color_space { + 1 => ColorSpace::Bt709, + 2 => ColorSpace::Bt2020, + _ => ColorSpace::Unknown, + }, + secondary: s.secondary, + label: match s.stream_type { + 7 => "Dolby Vision EL".to_string(), + _ => String::new(), + }, + }), + 2 | 5 => Stream::Audio(AudioStream { + pid: s.pid, + codec, + channels: format_channels(s.audio_format), + language: s.language.clone(), + sample_rate: format_samplerate(s.audio_rate), + secondary: s.stream_type == 5, + label: String::new(), + }), + 3 => Stream::Subtitle(SubtitleStream { + pid: s.pid, + codec, + language: s.language.clone(), + forced: false, // TODO: parse from MPLS stream attributes + }), + _ => Stream::Video(VideoStream { + pid: s.pid, + codec, + resolution: String::new(), + frame_rate: String::new(), + hdr: HdrFormat::Sdr, + color_space: ColorSpace::Unknown, + secondary: false, + label: String::new(), + }), } }).collect(); diff --git a/src/lib.rs b/src/lib.rs index 637cb63..83478cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,5 +87,6 @@ pub use profile::{DriveProfile, Chipset}; pub use platform::{Platform, DriveStatus}; pub use scsi::ScsiTransport; pub use speed::DriveSpeed; -pub use disc::{Disc, DiscFormat, Title, Clip, Stream, StreamKind, Codec, HdrFormat, ColorSpace, +pub use disc::{Disc, DiscFormat, Title, Clip, Stream, VideoStream, AudioStream, SubtitleStream, + Codec, HdrFormat, ColorSpace, Extent, ContentReader, AacsState, KeySource, ScanOptions};