sheathe_package/
descriptor.rs1use anyhow::{Context, Result, bail};
6use sheathe_core::MediaKind;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
11pub enum StreamSelector {
12 #[default]
14 All,
15 Kind(MediaKind),
17 Index(usize),
19}
20
21#[derive(Debug, Clone, Default)]
23pub struct StreamDescriptor {
24 pub input: PathBuf,
26 pub selector: StreamSelector,
28 pub output: Option<String>,
30 pub segment_template: Option<String>,
32 pub bandwidth: Option<u32>,
34 pub language: Option<String>,
36 pub skip_encryption: bool,
38 pub drm_label: Option<String>,
40 pub trick_play_factor: Option<u32>,
42 pub hls_name: Option<String>,
44 pub hls_group_id: Option<String>,
46 pub playlist_name: Option<String>,
48 pub iframe_playlist_name: Option<String>,
50 pub hls_characteristics: Vec<String>,
52 pub dash_accessibilities: Vec<(String, String)>,
54 pub dash_roles: Vec<String>,
56 pub forced_subtitle: bool,
58}
59
60impl StreamDescriptor {
61 pub fn from_path(path: impl Into<PathBuf>) -> Self {
63 Self { input: path.into(), ..Self::default() }
64 }
65
66 pub fn matches(&self, index: usize, kind: MediaKind) -> bool {
68 match self.selector {
69 StreamSelector::All => true,
70 StreamSelector::Kind(k) => k == kind,
71 StreamSelector::Index(i) => i == index,
72 }
73 }
74}
75
76pub fn parse_input_arg(arg: &str) -> Result<StreamDescriptor> {
78 if Path::new(arg).exists() || !arg.contains('=') {
79 return Ok(StreamDescriptor::from_path(arg));
80 }
81 parse_descriptor(arg)
82}
83
84pub fn parse_descriptor(spec: &str) -> Result<StreamDescriptor> {
86 let mut d = StreamDescriptor::default();
87 for part in spec.split(',') {
88 let part = part.trim();
89 if part.is_empty() {
90 continue;
91 }
92 let (key, value) = part
93 .split_once('=')
94 .with_context(|| format!("stream descriptor field '{part}' is not key=value"))?;
95 let key = key.trim();
96 let value = value.trim();
97 match key {
98 "input" | "in" => d.input = PathBuf::from(value),
99 "stream" | "stream_selector" => d.selector = parse_selector(value)?,
100 "output" | "out" | "init_segment" => d.output = Some(value.to_string()),
101 "segment_template" | "segment" => d.segment_template = Some(value.to_string()),
102 "bandwidth" | "bw" => {
103 d.bandwidth = Some(value.parse().context("bandwidth must be an integer")?);
104 }
105 "language" | "lang" => d.language = Some(value.to_string()),
106 "skip_encryption" => d.skip_encryption = value != "0" && value != "false",
107 "drm_label" => d.drm_label = Some(value.to_string()),
108 "trick_play_factor" | "tpf" => {
109 d.trick_play_factor = Some(value.parse().context("trick_play_factor")?);
110 }
111 "hls_name" => d.hls_name = Some(value.to_string()),
112 "hls_group_id" => d.hls_group_id = Some(value.to_string()),
113 "playlist_name" => d.playlist_name = Some(value.to_string()),
114 "iframe_playlist_name" => d.iframe_playlist_name = Some(value.to_string()),
115 "hls_characteristics" | "charcs" => {
116 d.hls_characteristics =
117 value.split([':', ';']).filter(|s| !s.is_empty()).map(str::to_string).collect();
118 }
119 "dash_accessibilities" | "accessibilities" => {
120 for item in value.split(';').filter(|s| !s.is_empty()) {
121 let (scheme, v) = item
122 .split_once('=')
123 .context("dash_accessibilities entries must be scheme_id_uri=value")?;
124 d.dash_accessibilities.push((scheme.to_string(), v.to_string()));
125 }
126 }
127 "dash_roles" | "roles" => {
128 d.dash_roles =
129 value.split(';').filter(|s| !s.is_empty()).map(str::to_string).collect();
130 }
131 "forced_subtitle" => d.forced_subtitle = value != "0" && value != "false",
132 "output_format" | "input_format" | "format" => { }
133 other => bail!("unknown stream descriptor field '{other}'"),
134 }
135 }
136 anyhow::ensure!(!d.input.as_os_str().is_empty(), "stream descriptor missing input=/in=");
137 Ok(d)
138}
139
140fn parse_selector(value: &str) -> Result<StreamSelector> {
141 match value.to_ascii_lowercase().as_str() {
142 "audio" => Ok(StreamSelector::Kind(MediaKind::Audio)),
143 "video" => Ok(StreamSelector::Kind(MediaKind::Video)),
144 "text" => Ok(StreamSelector::Kind(MediaKind::Text)),
145 n => {
146 let i: usize = n.parse().with_context(|| {
147 format!("stream selector '{value}' (expected audio, video, text, or an index)")
148 })?;
149 Ok(StreamSelector::Index(i))
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn bare_path() {
160 let d = parse_input_arg("movie.mp4").unwrap();
161 assert_eq!(d.input, PathBuf::from("movie.mp4"));
162 assert_eq!(d.selector, StreamSelector::All);
163 }
164
165 #[test]
166 fn shaka_descriptor() {
167 let d = parse_descriptor(
168 "in=movie.mp4,stream=audio,language=eng,hls_name=English,drm_label=AUDIO,skip_encryption=1",
169 )
170 .unwrap();
171 assert_eq!(d.input, PathBuf::from("movie.mp4"));
172 assert_eq!(d.selector, StreamSelector::Kind(MediaKind::Audio));
173 assert_eq!(d.language.as_deref(), Some("eng"));
174 assert_eq!(d.hls_name.as_deref(), Some("English"));
175 assert_eq!(d.drm_label.as_deref(), Some("AUDIO"));
176 assert!(d.skip_encryption);
177 }
178
179 #[test]
180 fn roles_and_accessibilities() {
181 let d = parse_descriptor(
182 "in=a.mp4,stream=text,dash_roles=subtitle;forced-subtitle,dash_accessibilities=urn:foo=bar,forced_subtitle=1",
183 )
184 .unwrap();
185 assert_eq!(d.dash_roles, vec!["subtitle", "forced-subtitle"]);
186 assert_eq!(d.dash_accessibilities, vec![("urn:foo".into(), "bar".into())]);
187 assert!(d.forced_subtitle);
188 }
189}