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]
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
40#[serde(rename_all = "camelCase")]
41#[non_exhaustive]
42pub struct Track {
43 pub name: String,
45
46 pub packaging: Packaging,
48
49 pub is_live: bool,
51
52 pub role: Option<Role>,
54
55 pub codec: Option<String>,
57
58 pub width: Option<u32>,
60
61 pub height: Option<u32>,
63
64 pub framerate: Option<f64>,
66
67 pub samplerate: Option<u32>,
69
70 pub channel_config: Option<String>,
72
73 pub bitrate: Option<u64>,
75
76 pub init_data: Option<String>,
78
79 pub render_group: Option<u32>,
81
82 pub alt_group: Option<u32>,
84
85 #[serde(rename = "maxGrpSapStartingType")]
89 pub max_grp_sap_starting_type: Option<u8>,
90
91 #[serde(rename = "maxObjSapStartingType")]
95 pub max_obj_sap_starting_type: Option<u8>,
96
97 pub jitter: Option<f64>,
99}
100
101impl Catalog {
102 pub fn to_string(&self) -> Result<String, serde_json::Error> {
104 serde_json::to_string(self)
105 }
106
107 #[allow(clippy::should_implement_trait)]
109 pub fn from_str(s: &str) -> Result<Self, serde_json::Error> {
110 serde_json::from_str(s)
111 }
112}
113
114impl Track {
115 pub fn new(name: impl Into<String>, packaging: Packaging) -> Self {
122 Self {
123 name: name.into(),
124 packaging,
125 is_live: false,
126 role: None,
127 codec: None,
128 width: None,
129 height: None,
130 framerate: None,
131 samplerate: None,
132 channel_config: None,
133 bitrate: None,
134 init_data: None,
135 render_group: None,
136 alt_group: None,
137 max_grp_sap_starting_type: None,
138 max_obj_sap_starting_type: None,
139 jitter: None,
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Packaging {
147 Loc,
149 Cmaf,
151 Legacy,
153 MediaTimeline,
155 EventTimeline,
157 Unknown(String),
159}
160
161impl fmt::Display for Packaging {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 Packaging::Loc => write!(f, "loc"),
165 Packaging::Cmaf => write!(f, "cmaf"),
166 Packaging::Legacy => write!(f, "legacy"),
167 Packaging::MediaTimeline => write!(f, "mediatimeline"),
168 Packaging::EventTimeline => write!(f, "eventtimeline"),
169 Packaging::Unknown(s) => write!(f, "{s}"),
170 }
171 }
172}
173
174impl FromStr for Packaging {
175 type Err = std::convert::Infallible;
176
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
178 Ok(match s {
179 "loc" => Packaging::Loc,
180 "cmaf" => Packaging::Cmaf,
181 "legacy" => Packaging::Legacy,
182 "mediatimeline" => Packaging::MediaTimeline,
183 "eventtimeline" => Packaging::EventTimeline,
184 other => Packaging::Unknown(other.to_string()),
185 })
186 }
187}
188
189impl Serialize for Packaging {
190 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
191 serializer.serialize_str(&self.to_string())
192 }
193}
194
195impl<'de> Deserialize<'de> for Packaging {
196 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
197 let s = String::deserialize(deserializer)?;
198 Ok(Packaging::from_str(&s).unwrap())
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum Role {
206 Video,
208 Audio,
210 AudioDescription,
212 Caption,
214 Subtitle,
216 SignLanguage,
218 Unknown(String),
220}
221
222impl fmt::Display for Role {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 match self {
225 Role::Video => write!(f, "video"),
226 Role::Audio => write!(f, "audio"),
227 Role::AudioDescription => write!(f, "audiodescription"),
228 Role::Caption => write!(f, "caption"),
229 Role::Subtitle => write!(f, "subtitle"),
230 Role::SignLanguage => write!(f, "signlanguage"),
231 Role::Unknown(s) => write!(f, "{s}"),
232 }
233 }
234}
235
236impl FromStr for Role {
237 type Err = std::convert::Infallible;
238
239 fn from_str(s: &str) -> Result<Self, Self::Err> {
240 Ok(match s {
241 "video" => Role::Video,
242 "audio" => Role::Audio,
243 "audiodescription" => Role::AudioDescription,
244 "caption" => Role::Caption,
245 "subtitle" => Role::Subtitle,
246 "signlanguage" => Role::SignLanguage,
247 other => Role::Unknown(other.to_string()),
248 })
249 }
250}
251
252impl Serialize for Role {
253 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254 serializer.serialize_str(&self.to_string())
255 }
256}
257
258impl<'de> Deserialize<'de> for Role {
259 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
260 let s = String::deserialize(deserializer)?;
261 Ok(Role::from_str(&s).unwrap())
263 }
264}
265
266#[cfg(test)]
267mod test {
268 use super::*;
269
270 #[test]
271 fn serialize_video_track() {
272 let catalog = Catalog {
273 version: 1,
274 tracks: vec![Track {
275 name: "video0".to_string(),
276 packaging: Packaging::Legacy,
277 is_live: true,
278 role: Some(Role::Video),
279 codec: Some("avc3.64001f".to_string()),
280 width: Some(1280),
281 height: Some(720),
282 framerate: Some(30.0),
283 samplerate: None,
284 channel_config: None,
285 bitrate: Some(6_000_000),
286 init_data: None,
287 render_group: Some(1),
288 alt_group: None,
289 max_grp_sap_starting_type: None,
290 max_obj_sap_starting_type: None,
291 jitter: None,
292 }],
293 };
294
295 let json = catalog.to_string().unwrap();
296 let parsed = Catalog::from_str(&json).unwrap();
297 assert_eq!(catalog, parsed);
298
299 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
301 let track = &value["tracks"][0];
302 assert!(track.get("samplerate").is_none());
303 assert!(track.get("channelConfig").is_none());
304
305 assert!(track.get("maxGrpSapStartingType").is_none());
307 assert!(track.get("maxObjSapStartingType").is_none());
308 assert!(track.get("jitter").is_none());
309 }
310
311 #[test]
312 fn serialize_audio_track() {
313 let catalog = Catalog {
314 version: 1,
315 tracks: vec![Track {
316 name: "audio0".to_string(),
317 packaging: Packaging::Legacy,
318 is_live: true,
319 role: Some(Role::Audio),
320 codec: Some("opus".to_string()),
321 width: None,
322 height: None,
323 framerate: None,
324 samplerate: Some(48_000),
325 channel_config: Some("2".to_string()),
326 bitrate: Some(128_000),
327 init_data: None,
328 render_group: Some(1),
329 alt_group: None,
330 max_grp_sap_starting_type: None,
331 max_obj_sap_starting_type: None,
332 jitter: None,
333 }],
334 };
335
336 let json = catalog.to_string().unwrap();
337 let parsed = Catalog::from_str(&json).unwrap();
338 assert_eq!(catalog, parsed);
339
340 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
342 let track = &value["tracks"][0];
343 assert!(track.get("width").is_none());
344 assert!(track.get("height").is_none());
345 assert!(track.get("framerate").is_none());
346 }
347
348 #[test]
349 fn packaging_roundtrip() {
350 for (s, expected) in [
351 ("loc", Packaging::Loc),
352 ("cmaf", Packaging::Cmaf),
353 ("legacy", Packaging::Legacy),
354 ("mediatimeline", Packaging::MediaTimeline),
355 ("eventtimeline", Packaging::EventTimeline),
356 ("custom", Packaging::Unknown("custom".to_string())),
357 ] {
358 let packaging: Packaging = s.parse().unwrap();
359 assert_eq!(packaging, expected);
360 assert_eq!(packaging.to_string(), s);
361 }
362 }
363
364 #[test]
365 fn role_roundtrip() {
366 for (s, expected) in [
367 ("video", Role::Video),
368 ("audio", Role::Audio),
369 ("audiodescription", Role::AudioDescription),
370 ("caption", Role::Caption),
371 ("subtitle", Role::Subtitle),
372 ("signlanguage", Role::SignLanguage),
373 ("custom", Role::Unknown("custom".to_string())),
374 ] {
375 let role: Role = s.parse().unwrap();
376 assert_eq!(role, expected);
377 assert_eq!(role.to_string(), s);
378 }
379 }
380
381 #[test]
382 fn roundtrip_empty() {
383 let catalog = Catalog {
384 version: 1,
385 tracks: vec![],
386 };
387 let json = catalog.to_string().unwrap();
388 let parsed = Catalog::from_str(&json).unwrap();
389 assert_eq!(catalog, parsed);
390 }
391
392 #[test]
393 fn cmaf_packaging() {
394 let catalog = Catalog {
395 version: 1,
396 tracks: vec![Track {
397 name: "hd".to_string(),
398 packaging: Packaging::Cmaf,
399 is_live: true,
400 role: Some(Role::Video),
401 codec: Some("avc1.640028".to_string()),
402 width: Some(1920),
403 height: Some(1080),
404 framerate: Some(30.0),
405 samplerate: None,
406 channel_config: None,
407 bitrate: Some(5_000_000),
408 init_data: Some("AQID".to_string()),
409 render_group: Some(1),
410 alt_group: Some(1),
411 max_grp_sap_starting_type: None,
412 max_obj_sap_starting_type: None,
413 jitter: None,
414 }],
415 };
416
417 let json = catalog.to_string().unwrap();
418 assert!(json.contains("\"packaging\":\"cmaf\""));
419 let parsed = Catalog::from_str(&json).unwrap();
420 assert_eq!(catalog, parsed);
421 }
422
423 fn track_with_sap_and_jitter() -> Track {
424 Track {
425 name: "video0".to_string(),
426 packaging: Packaging::Cmaf,
427 is_live: true,
428 role: Some(Role::Video),
429 codec: Some("avc1.640028".to_string()),
430 width: Some(1920),
431 height: Some(1080),
432 framerate: Some(30.0),
433 samplerate: None,
434 channel_config: None,
435 bitrate: Some(5_000_000),
436 init_data: None,
437 render_group: Some(1),
438 alt_group: None,
439 max_grp_sap_starting_type: Some(1),
440 max_obj_sap_starting_type: Some(2),
441 jitter: Some(15.5),
442 }
443 }
444
445 #[test]
446 fn serialize_sap_fields() {
447 let catalog = Catalog {
448 version: 1,
449 tracks: vec![track_with_sap_and_jitter()],
450 };
451
452 let json = catalog.to_string().unwrap();
453
454 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
457 let track = &value["tracks"][0];
458 assert_eq!(track.get("maxGrpSapStartingType"), Some(&serde_json::json!(1)));
459 assert_eq!(track.get("maxObjSapStartingType"), Some(&serde_json::json!(2)));
460 assert_eq!(track.get("jitter"), Some(&serde_json::json!(15.5)));
461
462 assert!(track.get("max_grp_sap_starting_type").is_none());
464 assert!(track.get("max_obj_sap_starting_type").is_none());
465 }
466
467 #[test]
468 fn deserialize_without_sap_fields() {
469 let json = r#"{
472 "version": 1,
473 "tracks": [{
474 "name": "video0",
475 "packaging": "cmaf",
476 "isLive": true,
477 "role": "video",
478 "codec": "avc1.640028",
479 "width": 1920,
480 "height": 1080,
481 "framerate": 30.0,
482 "bitrate": 5000000,
483 "renderGroup": 1
484 }]
485 }"#;
486
487 let catalog = Catalog::from_str(json).unwrap();
488 let track = &catalog.tracks[0];
489 assert_eq!(track.max_grp_sap_starting_type, None);
490 assert_eq!(track.max_obj_sap_starting_type, None);
491 assert_eq!(track.jitter, None);
492 }
493
494 #[test]
495 fn sap_and_jitter_roundtrip() {
496 let original = Catalog {
497 version: 1,
498 tracks: vec![track_with_sap_and_jitter()],
499 };
500
501 let json = original.to_string().unwrap();
502 let parsed = Catalog::from_str(&json).unwrap();
503 assert_eq!(original, parsed);
504 assert_eq!(parsed.tracks[0].max_grp_sap_starting_type, Some(1));
505 assert_eq!(parsed.tracks[0].max_obj_sap_starting_type, Some(2));
506 assert_eq!(parsed.tracks[0].jitter, Some(15.5));
507 }
508}