1use std::fmt;
21use std::str::FromStr;
22use std::time::Duration;
23
24use serde::{Deserialize, Serialize};
25use serde_with::DurationMilliSecondsWithFrac;
26
27pub const DEFAULT_NAME: &str = "catalog";
29
30#[derive(Debug, Clone, PartialEq, Default)]
42#[non_exhaustive]
43pub struct Catalog {
44 pub generated_at: Option<u64>,
46
47 pub is_complete: bool,
49
50 pub tracks: Vec<Track>,
52}
53
54#[serde_with::serde_as]
62#[serde_with::skip_serializing_none]
63#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
64#[serde(rename_all = "camelCase")]
65#[non_exhaustive]
66pub struct Track {
67 pub name: String,
69
70 pub packaging: Packaging,
72
73 #[serde(default)]
79 pub is_live: bool,
80
81 pub role: Option<Role>,
83
84 pub codec: Option<String>,
86
87 pub width: Option<u32>,
89
90 pub height: Option<u32>,
92
93 pub framerate: Option<f64>,
95
96 pub samplerate: Option<u32>,
98
99 pub channel_config: Option<String>,
101
102 pub bitrate: Option<u64>,
104
105 pub init_data: Option<String>,
111
112 init_ref: Option<String>,
116
117 pub render_group: Option<u32>,
119
120 pub alt_group: Option<u32>,
122
123 #[serde(rename = "maxGrpSapStartingType")]
127 pub max_grp_sap_starting_type: Option<u8>,
128
129 #[serde(rename = "maxObjSapStartingType")]
133 pub max_obj_sap_starting_type: Option<u8>,
134
135 #[serde_as(as = "Option<DurationMilliSecondsWithFrac>")]
139 pub jitter: Option<Duration>,
140}
141
142impl Catalog {
143 pub fn new(tracks: Vec<Track>) -> Self {
145 Self {
146 generated_at: None,
147 is_complete: false,
148 tracks,
149 }
150 }
151
152 pub fn to_json(&self) -> Result<String, serde_json::Error> {
154 serde_json::to_string(self)
155 }
156
157 #[allow(clippy::should_implement_trait)]
159 pub fn from_str(s: &str) -> Result<Self, serde_json::Error> {
160 serde_json::from_str(s)
161 }
162}
163
164const CURRENT_VERSION: &str = "draft-01";
166
167impl Serialize for Catalog {
168 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
169 use std::collections::HashMap;
170
171 let mut init_data_list: Vec<InitData> = Vec::new();
175 let mut ids: HashMap<String, String> = HashMap::new();
176 let mut tracks = Vec::with_capacity(self.tracks.len());
177
178 for track in &self.tracks {
179 let mut track = track.clone();
180 if let Some(payload) = track.init_data.take() {
181 let id = if let Some(id) = ids.get(&payload) {
182 id.clone()
183 } else {
184 let id = format!("init{}", init_data_list.len());
185 init_data_list.push(InitData {
186 id: id.clone(),
187 kind: "inline".to_string(),
188 data: payload.clone(),
189 });
190 ids.insert(payload, id.clone());
191 id
192 };
193 track.init_ref = Some(id);
194 }
195 tracks.push(track);
196 }
197
198 Wire {
199 version: WireVersion,
200 generated_at: self.generated_at,
201 is_complete: self.is_complete,
202 tracks,
203 init_data_list: (!init_data_list.is_empty()).then_some(init_data_list),
204 }
205 .serialize(serializer)
206 }
207}
208
209impl<'de> Deserialize<'de> for Catalog {
210 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
211 use std::collections::HashMap;
212
213 let wire = Wire::deserialize(deserializer)?;
214 let init_data_list = wire.init_data_list.unwrap_or_default();
215
216 let by_id: HashMap<&str, &InitData> = init_data_list.iter().map(|e| (e.id.as_str(), e)).collect();
219
220 let tracks = wire
221 .tracks
222 .into_iter()
223 .map(|mut track| -> Result<Track, D::Error> {
224 if let Some(id) = track.init_ref.take() {
228 let entry = by_id.get(id.as_str()).ok_or_else(|| {
229 serde::de::Error::custom(format!(
230 "MSF track {:?} references missing initData {:?}",
231 track.name, id
232 ))
233 })?;
234
235 if entry.kind != "inline" {
236 return Err(serde::de::Error::custom(format!(
237 "MSF track {:?} references initData {:?} with unsupported type {:?}",
238 track.name, id, entry.kind
239 )));
240 }
241
242 match &track.init_data {
243 Some(inline) if inline != &entry.data => {
244 return Err(serde::de::Error::custom(format!(
245 "MSF track {:?} carries conflicting initData and initRef {:?}",
246 track.name, id
247 )));
248 }
249 Some(_) => {}
250 None => track.init_data = Some(entry.data.clone()),
251 }
252 }
253 track.init_ref = None;
254 Ok(track)
255 })
256 .collect::<Result<Vec<_>, _>>()?;
257
258 Ok(Catalog {
259 generated_at: wire.generated_at,
260 is_complete: wire.is_complete,
261 tracks,
262 })
263 }
264}
265
266fn is_false(value: &bool) -> bool {
267 !*value
268}
269
270#[serde_with::skip_serializing_none]
272#[derive(Serialize, Deserialize)]
273#[serde(rename_all = "camelCase")]
274struct Wire {
275 version: WireVersion,
276 generated_at: Option<u64>,
277 #[serde(default, skip_serializing_if = "is_false")]
278 is_complete: bool,
279 #[serde(default)]
280 tracks: Vec<Track>,
281 init_data_list: Option<Vec<InitData>>,
282}
283
284struct WireVersion;
288
289impl Serialize for WireVersion {
290 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
291 serializer.serialize_str(CURRENT_VERSION)
292 }
293}
294
295impl<'de> Deserialize<'de> for WireVersion {
296 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
297 struct VersionVisitor;
298
299 impl serde::de::Visitor<'_> for VersionVisitor {
300 type Value = WireVersion;
301
302 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
303 f.write_str("the JSON number 1 (draft-00) or a \"draft-XX\" version string")
304 }
305
306 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<WireVersion, E> {
310 match v {
311 1 => Ok(WireVersion),
312 other => Err(E::custom(format!("unsupported MSF catalog version: {other}"))),
313 }
314 }
315
316 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<WireVersion, E> {
317 if v == 1 {
318 Ok(WireVersion)
319 } else {
320 Err(E::custom(format!("unsupported MSF catalog version: {v}")))
321 }
322 }
323
324 fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<WireVersion, E> {
325 if v == 1.0 {
326 Ok(WireVersion)
327 } else {
328 Err(E::custom(format!("unsupported MSF catalog version: {v}")))
329 }
330 }
331
332 fn visit_str<E: serde::de::Error>(self, _v: &str) -> Result<WireVersion, E> {
333 Ok(WireVersion)
335 }
336 }
337
338 deserializer.deserialize_any(VersionVisitor)
339 }
340}
341
342#[derive(Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345struct InitData {
346 id: String,
348 #[serde(rename = "type")]
350 kind: String,
351 data: String,
353}
354
355impl Track {
356 pub fn new(name: impl Into<String>, packaging: Packaging) -> Self {
363 Self {
364 name: name.into(),
365 packaging,
366 is_live: false,
367 role: None,
368 codec: None,
369 width: None,
370 height: None,
371 framerate: None,
372 samplerate: None,
373 channel_config: None,
374 bitrate: None,
375 init_data: None,
376 init_ref: None,
377 render_group: None,
378 alt_group: None,
379 max_grp_sap_starting_type: None,
380 max_obj_sap_starting_type: None,
381 jitter: None,
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
388pub enum Packaging {
389 Loc,
391 Cmaf,
393 Legacy,
395 MediaTimeline,
397 EventTimeline,
399 Unknown(String),
401}
402
403impl fmt::Display for Packaging {
404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405 match self {
406 Packaging::Loc => write!(f, "loc"),
407 Packaging::Cmaf => write!(f, "cmaf"),
408 Packaging::Legacy => write!(f, "legacy"),
409 Packaging::MediaTimeline => write!(f, "mediatimeline"),
410 Packaging::EventTimeline => write!(f, "eventtimeline"),
411 Packaging::Unknown(s) => write!(f, "{s}"),
412 }
413 }
414}
415
416impl FromStr for Packaging {
417 type Err = std::convert::Infallible;
418
419 fn from_str(s: &str) -> Result<Self, Self::Err> {
420 Ok(match s {
421 "loc" => Packaging::Loc,
422 "cmaf" => Packaging::Cmaf,
423 "legacy" => Packaging::Legacy,
424 "mediatimeline" => Packaging::MediaTimeline,
425 "eventtimeline" => Packaging::EventTimeline,
426 other => Packaging::Unknown(other.to_string()),
427 })
428 }
429}
430
431impl Serialize for Packaging {
432 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
433 serializer.serialize_str(&self.to_string())
434 }
435}
436
437impl<'de> Deserialize<'de> for Packaging {
438 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
439 let s = String::deserialize(deserializer)?;
440 Ok(Packaging::from_str(&s).unwrap())
442 }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
447pub enum Role {
448 Video,
450 Audio,
452 AudioDescription,
454 Caption,
456 Subtitle,
458 SignLanguage,
460 Unknown(String),
462}
463
464impl fmt::Display for Role {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 match self {
467 Role::Video => write!(f, "video"),
468 Role::Audio => write!(f, "audio"),
469 Role::AudioDescription => write!(f, "audiodescription"),
470 Role::Caption => write!(f, "caption"),
471 Role::Subtitle => write!(f, "subtitle"),
472 Role::SignLanguage => write!(f, "signlanguage"),
473 Role::Unknown(s) => write!(f, "{s}"),
474 }
475 }
476}
477
478impl FromStr for Role {
479 type Err = std::convert::Infallible;
480
481 fn from_str(s: &str) -> Result<Self, Self::Err> {
482 Ok(match s {
483 "video" => Role::Video,
484 "audio" => Role::Audio,
485 "audiodescription" => Role::AudioDescription,
486 "caption" => Role::Caption,
487 "subtitle" => Role::Subtitle,
488 "signlanguage" => Role::SignLanguage,
489 other => Role::Unknown(other.to_string()),
490 })
491 }
492}
493
494impl Serialize for Role {
495 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
496 serializer.serialize_str(&self.to_string())
497 }
498}
499
500impl<'de> Deserialize<'de> for Role {
501 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
502 let s = String::deserialize(deserializer)?;
503 Ok(Role::from_str(&s).unwrap())
505 }
506}
507
508#[cfg(test)]
509mod test {
510 use super::*;
511
512 fn video_track() -> Track {
513 Track {
514 name: "video0".to_string(),
515 packaging: Packaging::Legacy,
516 is_live: true,
517 role: Some(Role::Video),
518 codec: Some("avc3.64001f".to_string()),
519 width: Some(1280),
520 height: Some(720),
521 framerate: Some(30.0),
522 samplerate: None,
523 channel_config: None,
524 bitrate: Some(6_000_000),
525 init_data: None,
526 init_ref: None,
527 render_group: Some(1),
528 alt_group: None,
529 max_grp_sap_starting_type: None,
530 max_obj_sap_starting_type: None,
531 jitter: None,
532 }
533 }
534
535 fn audio_track() -> Track {
536 Track {
537 name: "audio0".to_string(),
538 packaging: Packaging::Legacy,
539 is_live: true,
540 role: Some(Role::Audio),
541 codec: Some("opus".to_string()),
542 width: None,
543 height: None,
544 framerate: None,
545 samplerate: Some(48_000),
546 channel_config: Some("2".to_string()),
547 bitrate: Some(128_000),
548 init_data: None,
549 init_ref: None,
550 render_group: Some(1),
551 alt_group: None,
552 max_grp_sap_starting_type: None,
553 max_obj_sap_starting_type: None,
554 jitter: None,
555 }
556 }
557
558 fn track_with_sap_and_jitter() -> Track {
559 Track {
560 name: "video0".to_string(),
561 packaging: Packaging::Cmaf,
562 is_live: true,
563 role: Some(Role::Video),
564 codec: Some("avc1.640028".to_string()),
565 width: Some(1920),
566 height: Some(1080),
567 framerate: Some(30.0),
568 samplerate: None,
569 channel_config: None,
570 bitrate: Some(5_000_000),
571 init_data: None,
572 init_ref: None,
573 render_group: Some(1),
574 alt_group: None,
575 max_grp_sap_starting_type: Some(1),
576 max_obj_sap_starting_type: Some(2),
577 jitter: Some(Duration::from_millis(15)),
578 }
579 }
580
581 #[test]
582 fn serialize_video_track() {
583 let catalog = Catalog::new(vec![video_track()]);
584
585 let json = catalog.to_json().unwrap();
586 let parsed = Catalog::from_str(&json).unwrap();
587 assert_eq!(catalog, parsed);
588
589 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
591 let track = &value["tracks"][0];
592 assert!(track.get("samplerate").is_none());
593 assert!(track.get("channelConfig").is_none());
594
595 assert!(track.get("maxGrpSapStartingType").is_none());
597 assert!(track.get("maxObjSapStartingType").is_none());
598 assert!(track.get("jitter").is_none());
599 }
600
601 #[test]
602 fn serialize_audio_track() {
603 let catalog = Catalog::new(vec![audio_track()]);
604
605 let json = catalog.to_json().unwrap();
606 let parsed = Catalog::from_str(&json).unwrap();
607 assert_eq!(catalog, parsed);
608
609 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
611 let track = &value["tracks"][0];
612 assert!(track.get("width").is_none());
613 assert!(track.get("height").is_none());
614 assert!(track.get("framerate").is_none());
615 }
616
617 #[test]
618 fn packaging_roundtrip() {
619 for (s, expected) in [
620 ("loc", Packaging::Loc),
621 ("cmaf", Packaging::Cmaf),
622 ("legacy", Packaging::Legacy),
623 ("mediatimeline", Packaging::MediaTimeline),
624 ("eventtimeline", Packaging::EventTimeline),
625 ("custom", Packaging::Unknown("custom".to_string())),
626 ] {
627 let packaging: Packaging = s.parse().unwrap();
628 assert_eq!(packaging, expected);
629 assert_eq!(packaging.to_string(), s);
630 }
631 }
632
633 #[test]
634 fn role_roundtrip() {
635 for (s, expected) in [
636 ("video", Role::Video),
637 ("audio", Role::Audio),
638 ("audiodescription", Role::AudioDescription),
639 ("caption", Role::Caption),
640 ("subtitle", Role::Subtitle),
641 ("signlanguage", Role::SignLanguage),
642 ("custom", Role::Unknown("custom".to_string())),
643 ] {
644 let role: Role = s.parse().unwrap();
645 assert_eq!(role, expected);
646 assert_eq!(role.to_string(), s);
647 }
648 }
649
650 #[test]
651 fn roundtrip_empty() {
652 let catalog = Catalog::new(vec![]);
653 let json = catalog.to_json().unwrap();
654 let parsed = Catalog::from_str(&json).unwrap();
655 assert_eq!(catalog, parsed);
656 }
657
658 #[test]
659 fn cmaf_packaging() {
660 let mut track = track_with_sap_and_jitter();
661 track.name = "hd".to_string();
662 track.alt_group = Some(1);
663 track.max_grp_sap_starting_type = None;
664 track.max_obj_sap_starting_type = None;
665 track.jitter = None;
666 track.init_data = Some("AQID".to_string());
667
668 let catalog = Catalog::new(vec![track]);
669
670 let json = catalog.to_json().unwrap();
671 assert!(json.contains("\"packaging\":\"cmaf\""));
672 let parsed = Catalog::from_str(&json).unwrap();
673 assert_eq!(catalog, parsed);
674 assert_eq!(parsed.tracks[0].init_data.as_deref(), Some("AQID"));
675 }
676
677 #[test]
678 fn serialize_sap_fields() {
679 let catalog = Catalog::new(vec![track_with_sap_and_jitter()]);
680
681 let json = catalog.to_json().unwrap();
682
683 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
686 let track = &value["tracks"][0];
687 assert_eq!(track.get("maxGrpSapStartingType"), Some(&serde_json::json!(1)));
688 assert_eq!(track.get("maxObjSapStartingType"), Some(&serde_json::json!(2)));
689 assert_eq!(track.get("jitter").and_then(serde_json::Value::as_f64), Some(15.0));
690
691 assert!(track.get("max_grp_sap_starting_type").is_none());
693 assert!(track.get("max_obj_sap_starting_type").is_none());
694 }
695
696 #[test]
697 fn deserialize_without_sap_fields() {
698 let json = r#"{
701 "version": 1,
702 "tracks": [{
703 "name": "video0",
704 "packaging": "cmaf",
705 "isLive": true,
706 "role": "video",
707 "codec": "avc1.640028",
708 "width": 1920,
709 "height": 1080,
710 "framerate": 30.0,
711 "bitrate": 5000000,
712 "renderGroup": 1
713 }]
714 }"#;
715
716 let catalog = Catalog::from_str(json).unwrap();
717 let track = &catalog.tracks[0];
718 assert_eq!(track.max_grp_sap_starting_type, None);
719 assert_eq!(track.max_obj_sap_starting_type, None);
720 assert_eq!(track.jitter, None);
721 }
722
723 #[test]
724 fn sap_and_jitter_roundtrip() {
725 let original = Catalog::new(vec![track_with_sap_and_jitter()]);
726
727 let json = original.to_json().unwrap();
728 let parsed = Catalog::from_str(&json).unwrap();
729 assert_eq!(original, parsed);
730 assert_eq!(parsed.tracks[0].max_grp_sap_starting_type, Some(1));
731 assert_eq!(parsed.tracks[0].max_obj_sap_starting_type, Some(2));
732 assert_eq!(parsed.tracks[0].jitter, Some(Duration::from_millis(15)));
733 }
734
735 #[test]
736 fn fractional_jitter_roundtrips() {
737 let json = r#"{
738 "version": "draft-01",
739 "tracks": [{
740 "name": "video0",
741 "packaging": "cmaf",
742 "isLive": true,
743 "role": "video",
744 "codec": "avc1.640028",
745 "jitter": 15.0
746 }]
747 }"#;
748
749 let catalog = Catalog::from_str(json).expect("fractional jitter must decode");
750 assert_eq!(catalog.tracks[0].jitter, Some(Duration::from_millis(15)));
751
752 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
753 assert_eq!(value["tracks"][0]["jitter"].as_f64(), Some(15.0));
754 }
755
756 #[test]
757 fn serialize_emits_draft01_version() {
758 let json = Catalog::default().to_json().unwrap();
760 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
761 assert_eq!(value["version"], serde_json::json!("draft-01"));
762 }
763
764 #[test]
765 fn draft00_numeric_version_decodes_and_normalizes() {
766 let catalog = Catalog::from_str(r#"{"version":1,"tracks":[]}"#).unwrap();
769 assert!(catalog.tracks.is_empty());
770
771 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
772 assert_eq!(value["version"], serde_json::json!("draft-01"));
773 }
774
775 #[test]
776 fn draft01_string_version_decodes() {
777 let catalog = Catalog::from_str(r#"{"version":"draft-01","tracks":[]}"#).unwrap();
778 assert!(catalog.tracks.is_empty());
779 }
780
781 #[test]
782 fn unknown_version_string_is_accepted() {
783 assert!(Catalog::from_str(r#"{"version":"draft-99","tracks":[]}"#).is_ok());
786 }
787
788 #[test]
789 fn unsupported_numeric_version_errors() {
790 assert!(Catalog::from_str(r#"{"version":2,"tracks":[]}"#).is_err());
792 }
793
794 #[test]
795 fn float_numeric_version_is_accepted() {
796 assert!(Catalog::from_str(r#"{"version":1.0,"tracks":[]}"#).is_ok());
799 assert!(Catalog::from_str(r#"{"version":2.0,"tracks":[]}"#).is_err());
800 }
801
802 #[test]
803 fn dangling_init_ref_errors() {
804 let json = r#"{
807 "version": "draft-01",
808 "initDataList": [
809 { "id": "v0", "type": "inline", "data": "AQID" }
810 ],
811 "tracks": [
812 { "name": "a", "packaging": "cmaf", "isLive": true, "role": "video",
813 "codec": "avc1.640028", "initRef": "missing" }
814 ]
815 }"#;
816
817 let err = Catalog::from_str(json).expect_err("dangling initRef must fail");
818 assert!(err.to_string().contains("missing initData"), "unexpected error: {err}");
819 }
820
821 #[test]
822 fn unsupported_init_ref_type_errors() {
823 let json = r#"{
826 "version": "draft-01",
827 "initDataList": [
828 { "id": "v0", "type": "url", "data": "https://example.com/init" }
829 ],
830 "tracks": [
831 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
832 "codec": "avc1.640028", "initRef": "v0" }
833 ]
834 }"#;
835
836 let err = Catalog::from_str(json).expect_err("unsupported initRef type must fail");
837 assert!(err.to_string().contains("unsupported type"), "unexpected error: {err}");
838 }
839
840 #[test]
841 fn inline_init_data_and_init_ref_must_agree() {
842 let json = r#"{
843 "version": "draft-01",
844 "initDataList": [
845 { "id": "v0", "type": "inline", "data": "AQID" }
846 ],
847 "tracks": [
848 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
849 "codec": "avc1.640028", "initData": "BAUG", "initRef": "v0" }
850 ]
851 }"#;
852
853 let err = Catalog::from_str(json).expect_err("conflicting initData/initRef must fail");
854 assert!(err.to_string().contains("conflicting"), "unexpected error: {err}");
855 }
856
857 #[test]
858 fn matching_inline_init_data_and_init_ref_decodes() {
859 let json = r#"{
860 "version": "draft-01",
861 "initDataList": [
862 { "id": "v0", "type": "inline", "data": "AQID" }
863 ],
864 "tracks": [
865 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
866 "codec": "avc1.640028", "initData": "AQID", "initRef": "v0" }
867 ]
868 }"#;
869
870 let catalog = Catalog::from_str(json).unwrap();
871 assert_eq!(catalog.tracks[0].init_data.as_deref(), Some("AQID"));
872 }
873
874 #[test]
875 fn draft01_init_ref_resolves_to_inline() {
876 let json = r#"{
879 "version": "draft-01",
880 "initDataList": [
881 { "id": "v0", "type": "inline", "data": "AQID" }
882 ],
883 "tracks": [
884 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
885 "codec": "avc1.640028", "initRef": "v0" }
886 ]
887 }"#;
888
889 let catalog = Catalog::from_str(json).unwrap();
890 assert_eq!(catalog.tracks[0].init_data.as_deref(), Some("AQID"));
891 }
892
893 #[test]
894 fn serialize_hoists_and_dedups_init_data() {
895 let mut a = video_track();
899 a.name = "a".to_string();
900 a.init_data = Some("AQID".to_string());
901 let mut b = video_track();
902 b.name = "b".to_string();
903 b.init_data = Some("AQID".to_string());
904
905 let catalog = Catalog::new(vec![a, b]);
906 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
907
908 let list = value["initDataList"].as_array().expect("initDataList present");
909 assert_eq!(list.len(), 1, "identical payloads should dedup to one entry");
910 assert_eq!(list[0]["data"], serde_json::json!("AQID"));
911 assert_eq!(list[0]["type"], serde_json::json!("inline"));
912
913 let id = list[0]["id"].as_str().unwrap();
914 for t in value["tracks"].as_array().unwrap() {
915 assert_eq!(t["initRef"], serde_json::json!(id));
916 assert!(t.get("initData").is_none(), "no inline initData on the wire");
917 }
918
919 let parsed = Catalog::from_str(&catalog.to_json().unwrap()).unwrap();
921 assert_eq!(parsed.tracks[0].init_data.as_deref(), Some("AQID"));
922 assert_eq!(parsed.tracks[1].init_data.as_deref(), Some("AQID"));
923 }
924
925 #[test]
926 fn draft00_example_av_decodes() {
927 let json = r#"{
931 "version": 1,
932 "generatedAt": 1746104606044,
933 "tracks": [
934 {
935 "name": "1080p-video",
936 "namespace": "conference.example.com/conference123/alice",
937 "packaging": "loc",
938 "isLive": true,
939 "targetLatency": 2000,
940 "role": "video",
941 "renderGroup": 1,
942 "codec": "av01.0.08M.10.0.110.09",
943 "width": 1920,
944 "height": 1080,
945 "framerate": 30,
946 "bitrate": 1500000
947 },
948 {
949 "name": "audio",
950 "namespace": "conference.example.com/conference123/alice",
951 "packaging": "loc",
952 "isLive": true,
953 "targetLatency": 2000,
954 "role": "audio",
955 "codec": "opus",
956 "samplerate": 48000,
957 "channelConfig": "2",
958 "bitrate": 32000
959 }
960 ]
961 }"#;
962
963 let catalog = Catalog::from_str(json).expect("draft-00 AV catalog must decode");
964 assert_eq!(catalog.generated_at, Some(1746104606044));
965 assert_eq!(catalog.tracks.len(), 2);
966 assert_eq!(catalog.tracks[0].framerate, Some(30.0));
967 assert_eq!(catalog.tracks[1].channel_config.as_deref(), Some("2"));
968 }
969
970 #[test]
971 fn draft00_example_timeline_tracks_decode() {
972 let json = r#"{
975 "version": 1,
976 "generatedAt": 1746104606044,
977 "tracks": [
978 {
979 "name": "history",
980 "namespace": "conference.example.com/conference123/alice",
981 "packaging": "mediatimeline",
982 "mimetype": "application/json",
983 "depends": ["1080p-video", "audio"]
984 },
985 {
986 "name": "1080p-video",
987 "namespace": "conference.example.com/conference123/alice",
988 "packaging": "loc",
989 "isLive": true,
990 "role": "video",
991 "codec": "av01.0.08M.10.0.110.09",
992 "width": 1920,
993 "height": 1080,
994 "framerate": 30,
995 "bitrate": 1500000
996 }
997 ]
998 }"#;
999
1000 let catalog = Catalog::from_str(json).expect("draft-00 timeline catalog must decode");
1001 assert_eq!(catalog.tracks.len(), 2);
1002 assert!(!catalog.tracks[0].is_live);
1004 assert_eq!(catalog.tracks[0].packaging, Packaging::MediaTimeline);
1005 }
1006
1007 #[test]
1008 fn draft00_example_complete_decodes() {
1009 let json = r#"{
1011 "version": 1,
1012 "generatedAt": 1746104606044,
1013 "isComplete": true,
1014 "tracks": []
1015 }"#;
1016 let catalog = Catalog::from_str(json).expect("draft-00 completion catalog must decode");
1017 assert_eq!(catalog.generated_at, Some(1746104606044));
1018 assert!(catalog.is_complete);
1019 assert!(catalog.tracks.is_empty());
1020
1021 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
1022 assert_eq!(value["generatedAt"], serde_json::json!(1746104606044u64));
1023 assert_eq!(value["isComplete"], serde_json::json!(true));
1024 }
1025
1026 #[test]
1027 fn default_catalog_omits_completion_fields() {
1028 let value: serde_json::Value = serde_json::from_str(&Catalog::default().to_json().unwrap()).unwrap();
1029 assert!(value.get("generatedAt").is_none());
1030 assert!(value.get("isComplete").is_none());
1031 }
1032}