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).
This commit is contained in:
+83
-87
@@ -91,39 +91,63 @@ pub struct Clip {
|
|||||||
|
|
||||||
/// A stream within a title.
|
/// A stream within a title.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Stream {
|
pub enum Stream {
|
||||||
/// Stream type
|
Video(VideoStream),
|
||||||
pub kind: StreamKind,
|
Audio(AudioStream),
|
||||||
|
Subtitle(SubtitleStream),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A video stream.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VideoStream {
|
||||||
/// MPEG-TS packet ID
|
/// MPEG-TS packet ID
|
||||||
pub pid: u16,
|
pub pid: u16,
|
||||||
/// Codec
|
/// Codec (HEVC, H.264, VC-1, MPEG-2)
|
||||||
pub codec: Codec,
|
pub codec: Codec,
|
||||||
/// ISO 639-2 language code (e.g. "eng", "fra")
|
/// Resolution (e.g. "2160p", "1080p", "1080i")
|
||||||
pub language: String,
|
|
||||||
/// Video resolution (e.g. "2160p", "1080p")
|
|
||||||
pub resolution: String,
|
pub resolution: String,
|
||||||
/// Frame rate (e.g. "23.976")
|
/// Frame rate (e.g. "23.976", "25")
|
||||||
pub frame_rate: String,
|
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
|
/// HDR format
|
||||||
pub hdr: HdrFormat,
|
pub hdr: HdrFormat,
|
||||||
/// Color space
|
/// Color space
|
||||||
pub color_space: ColorSpace,
|
pub color_space: ColorSpace,
|
||||||
/// Whether this is a secondary/enhancement stream
|
/// Whether this is a secondary stream (PiP, Dolby Vision EL)
|
||||||
pub secondary: bool,
|
pub secondary: bool,
|
||||||
/// Extra label (e.g. "Dolby Vision EL")
|
/// Extra label (e.g. "Dolby Vision EL")
|
||||||
pub label: String,
|
pub label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream type.
|
/// An audio stream.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum StreamKind {
|
pub struct AudioStream {
|
||||||
Video,
|
/// MPEG-TS packet ID
|
||||||
Audio,
|
pub pid: u16,
|
||||||
Subtitle,
|
/// 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.
|
/// 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 ─────────────────────────────────────────────────────────────
|
// ─── AACS state ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -576,17 +560,16 @@ impl Disc {
|
|||||||
|
|
||||||
/// Detect disc format from the main title's video streams.
|
/// Detect disc format from the main title's video streams.
|
||||||
fn detect_format(titles: &[Title]) -> DiscFormat {
|
fn detect_format(titles: &[Title]) -> DiscFormat {
|
||||||
// Check the longest title (first after sort) for 2160p HEVC
|
|
||||||
for title in titles.iter().take(3) {
|
for title in titles.iter().take(3) {
|
||||||
for stream in &title.streams {
|
for stream in &title.streams {
|
||||||
if stream.kind == StreamKind::Video {
|
if let Stream::Video(v) = stream {
|
||||||
if stream.resolution.contains("2160") {
|
if v.resolution.contains("2160") {
|
||||||
return DiscFormat::Uhd;
|
return DiscFormat::Uhd;
|
||||||
}
|
}
|
||||||
if stream.resolution.contains("1080") || stream.resolution.contains("720") {
|
if v.resolution.contains("1080") || v.resolution.contains("720") {
|
||||||
return DiscFormat::BluRay;
|
return DiscFormat::BluRay;
|
||||||
}
|
}
|
||||||
if stream.resolution.contains("480") || stream.resolution.contains("576") {
|
if v.resolution.contains("480") || v.resolution.contains("576") {
|
||||||
return DiscFormat::Dvd;
|
return DiscFormat::Dvd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,41 +689,54 @@ impl Disc {
|
|||||||
|
|
||||||
// Build streams from STN table
|
// Build streams from STN table
|
||||||
let streams: Vec<Stream> = parsed.streams.iter().map(|s| {
|
let streams: Vec<Stream> = 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 codec = Codec::from_coding_type(s.coding_type);
|
||||||
let hdr = match s.dynamic_range {
|
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,
|
1 => HdrFormat::Hdr10,
|
||||||
2 => HdrFormat::DolbyVision,
|
2 => HdrFormat::DolbyVision,
|
||||||
_ => HdrFormat::Sdr,
|
_ => HdrFormat::Sdr,
|
||||||
};
|
},
|
||||||
let cs = match s.color_space {
|
color_space: match s.color_space {
|
||||||
1 => ColorSpace::Bt709,
|
1 => ColorSpace::Bt709,
|
||||||
2 => ColorSpace::Bt2020,
|
2 => ColorSpace::Bt2020,
|
||||||
_ => ColorSpace::Unknown,
|
_ => ColorSpace::Unknown,
|
||||||
};
|
},
|
||||||
let label = match s.stream_type {
|
secondary: s.secondary,
|
||||||
5 => "secondary".to_string(),
|
label: match s.stream_type {
|
||||||
7 => "Dolby Vision EL".to_string(),
|
7 => "Dolby Vision EL".to_string(),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
},
|
||||||
Stream {
|
}),
|
||||||
kind,
|
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,
|
pid: s.pid,
|
||||||
codec,
|
codec,
|
||||||
language: s.language.clone(),
|
language: s.language.clone(),
|
||||||
resolution: format_resolution(s.video_format, s.video_rate),
|
forced: false, // TODO: parse from MPLS stream attributes
|
||||||
frame_rate: format_framerate(s.video_rate),
|
}),
|
||||||
channels: format_channels(s.audio_format),
|
_ => Stream::Video(VideoStream {
|
||||||
sample_rate: format_samplerate(s.audio_rate),
|
pid: s.pid,
|
||||||
hdr,
|
codec,
|
||||||
color_space: cs,
|
resolution: String::new(),
|
||||||
secondary: s.secondary,
|
frame_rate: String::new(),
|
||||||
label,
|
hdr: HdrFormat::Sdr,
|
||||||
|
color_space: ColorSpace::Unknown,
|
||||||
|
secondary: false,
|
||||||
|
label: String::new(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -87,5 +87,6 @@ pub use profile::{DriveProfile, Chipset};
|
|||||||
pub use platform::{Platform, DriveStatus};
|
pub use platform::{Platform, DriveStatus};
|
||||||
pub use scsi::ScsiTransport;
|
pub use scsi::ScsiTransport;
|
||||||
pub use speed::DriveSpeed;
|
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};
|
Extent, ContentReader, AacsState, KeySource, ScanOptions};
|
||||||
|
|||||||
Reference in New Issue
Block a user