1use std::fmt;
12use std::str::FromStr;
13
14use serde::{Deserialize, Serialize};
15
16pub const DEFAULT_NAME: &str = "catalog";
18
19#[serde_with::skip_serializing_none]
21#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
22#[serde(rename_all = "camelCase")]
23pub struct Catalog {
24 pub version: u32,
26
27 pub tracks: Vec<Track>,
29}
30
31#[serde_with::skip_serializing_none]
33#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
34#[serde(rename_all = "camelCase")]
35pub struct Track {
36 pub name: String,
38
39 pub packaging: Packaging,
41
42 pub is_live: bool,
44
45 pub role: Option<Role>,
47
48 pub codec: Option<String>,
50
51 pub width: Option<u32>,
53
54 pub height: Option<u32>,
56
57 pub framerate: Option<f64>,
59
60 pub samplerate: Option<u32>,
62
63 pub channel_config: Option<String>,
65
66 pub bitrate: Option<u64>,
68
69 pub init_data: Option<String>,
71
72 pub render_group: Option<u32>,
74
75 pub alt_group: Option<u32>,
77}
78
79impl Catalog {
80 pub fn to_string(&self) -> Result<String, serde_json::Error> {
82 serde_json::to_string(self)
83 }
84
85 #[allow(clippy::should_implement_trait)]
87 pub fn from_str(s: &str) -> Result<Self, serde_json::Error> {
88 serde_json::from_str(s)
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum Packaging {
95 Loc,
97 Cmaf,
99 Legacy,
101 MediaTimeline,
103 EventTimeline,
105 Unknown(String),
107}
108
109impl fmt::Display for Packaging {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Packaging::Loc => write!(f, "loc"),
113 Packaging::Cmaf => write!(f, "cmaf"),
114 Packaging::Legacy => write!(f, "legacy"),
115 Packaging::MediaTimeline => write!(f, "mediatimeline"),
116 Packaging::EventTimeline => write!(f, "eventtimeline"),
117 Packaging::Unknown(s) => write!(f, "{s}"),
118 }
119 }
120}
121
122impl FromStr for Packaging {
123 type Err = std::convert::Infallible;
124
125 fn from_str(s: &str) -> Result<Self, Self::Err> {
126 Ok(match s {
127 "loc" => Packaging::Loc,
128 "cmaf" => Packaging::Cmaf,
129 "legacy" => Packaging::Legacy,
130 "mediatimeline" => Packaging::MediaTimeline,
131 "eventtimeline" => Packaging::EventTimeline,
132 other => Packaging::Unknown(other.to_string()),
133 })
134 }
135}
136
137impl Serialize for Packaging {
138 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
139 serializer.serialize_str(&self.to_string())
140 }
141}
142
143impl<'de> Deserialize<'de> for Packaging {
144 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
145 let s = String::deserialize(deserializer)?;
146 Ok(Packaging::from_str(&s).unwrap())
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum Role {
154 Video,
156 Audio,
158 AudioDescription,
160 Caption,
162 Subtitle,
164 SignLanguage,
166 Unknown(String),
168}
169
170impl fmt::Display for Role {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match self {
173 Role::Video => write!(f, "video"),
174 Role::Audio => write!(f, "audio"),
175 Role::AudioDescription => write!(f, "audiodescription"),
176 Role::Caption => write!(f, "caption"),
177 Role::Subtitle => write!(f, "subtitle"),
178 Role::SignLanguage => write!(f, "signlanguage"),
179 Role::Unknown(s) => write!(f, "{s}"),
180 }
181 }
182}
183
184impl FromStr for Role {
185 type Err = std::convert::Infallible;
186
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 Ok(match s {
189 "video" => Role::Video,
190 "audio" => Role::Audio,
191 "audiodescription" => Role::AudioDescription,
192 "caption" => Role::Caption,
193 "subtitle" => Role::Subtitle,
194 "signlanguage" => Role::SignLanguage,
195 other => Role::Unknown(other.to_string()),
196 })
197 }
198}
199
200impl Serialize for Role {
201 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
202 serializer.serialize_str(&self.to_string())
203 }
204}
205
206impl<'de> Deserialize<'de> for Role {
207 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
208 let s = String::deserialize(deserializer)?;
209 Ok(Role::from_str(&s).unwrap())
211 }
212}
213
214#[cfg(test)]
215mod test {
216 use super::*;
217
218 #[test]
219 fn serialize_video_track() {
220 let catalog = Catalog {
221 version: 1,
222 tracks: vec![Track {
223 name: "video0".to_string(),
224 packaging: Packaging::Legacy,
225 is_live: true,
226 role: Some(Role::Video),
227 codec: Some("avc3.64001f".to_string()),
228 width: Some(1280),
229 height: Some(720),
230 framerate: Some(30.0),
231 samplerate: None,
232 channel_config: None,
233 bitrate: Some(6_000_000),
234 init_data: None,
235 render_group: Some(1),
236 alt_group: None,
237 }],
238 };
239
240 let json = catalog.to_string().unwrap();
241 let parsed = Catalog::from_str(&json).unwrap();
242 assert_eq!(catalog, parsed);
243
244 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
246 let track = &value["tracks"][0];
247 assert!(track.get("samplerate").is_none());
248 assert!(track.get("channelConfig").is_none());
249 }
250
251 #[test]
252 fn serialize_audio_track() {
253 let catalog = Catalog {
254 version: 1,
255 tracks: vec![Track {
256 name: "audio0".to_string(),
257 packaging: Packaging::Legacy,
258 is_live: true,
259 role: Some(Role::Audio),
260 codec: Some("opus".to_string()),
261 width: None,
262 height: None,
263 framerate: None,
264 samplerate: Some(48_000),
265 channel_config: Some("2".to_string()),
266 bitrate: Some(128_000),
267 init_data: None,
268 render_group: Some(1),
269 alt_group: None,
270 }],
271 };
272
273 let json = catalog.to_string().unwrap();
274 let parsed = Catalog::from_str(&json).unwrap();
275 assert_eq!(catalog, parsed);
276
277 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
279 let track = &value["tracks"][0];
280 assert!(track.get("width").is_none());
281 assert!(track.get("height").is_none());
282 assert!(track.get("framerate").is_none());
283 }
284
285 #[test]
286 fn packaging_roundtrip() {
287 for (s, expected) in [
288 ("loc", Packaging::Loc),
289 ("cmaf", Packaging::Cmaf),
290 ("legacy", Packaging::Legacy),
291 ("mediatimeline", Packaging::MediaTimeline),
292 ("eventtimeline", Packaging::EventTimeline),
293 ("custom", Packaging::Unknown("custom".to_string())),
294 ] {
295 let packaging: Packaging = s.parse().unwrap();
296 assert_eq!(packaging, expected);
297 assert_eq!(packaging.to_string(), s);
298 }
299 }
300
301 #[test]
302 fn role_roundtrip() {
303 for (s, expected) in [
304 ("video", Role::Video),
305 ("audio", Role::Audio),
306 ("audiodescription", Role::AudioDescription),
307 ("caption", Role::Caption),
308 ("subtitle", Role::Subtitle),
309 ("signlanguage", Role::SignLanguage),
310 ("custom", Role::Unknown("custom".to_string())),
311 ] {
312 let role: Role = s.parse().unwrap();
313 assert_eq!(role, expected);
314 assert_eq!(role.to_string(), s);
315 }
316 }
317
318 #[test]
319 fn roundtrip_empty() {
320 let catalog = Catalog {
321 version: 1,
322 tracks: vec![],
323 };
324 let json = catalog.to_string().unwrap();
325 let parsed = Catalog::from_str(&json).unwrap();
326 assert_eq!(catalog, parsed);
327 }
328
329 #[test]
330 fn cmaf_packaging() {
331 let catalog = Catalog {
332 version: 1,
333 tracks: vec![Track {
334 name: "hd".to_string(),
335 packaging: Packaging::Cmaf,
336 is_live: true,
337 role: Some(Role::Video),
338 codec: Some("avc1.640028".to_string()),
339 width: Some(1920),
340 height: Some(1080),
341 framerate: Some(30.0),
342 samplerate: None,
343 channel_config: None,
344 bitrate: Some(5_000_000),
345 init_data: Some("AQID".to_string()),
346 render_group: Some(1),
347 alt_group: Some(1),
348 }],
349 };
350
351 let json = catalog.to_string().unwrap();
352 assert!(json.contains("\"packaging\":\"cmaf\""));
353 let parsed = Catalog::from_str(&json).unwrap();
354 assert_eq!(catalog, parsed);
355 }
356}