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)]
46#[non_exhaustive]
47pub struct Catalog<E: CatalogExt = ()> {
48 pub generated_at: Option<u64>,
50
51 pub is_complete: bool,
53
54 pub tracks: Vec<Track>,
56
57 pub ext: E,
59}
60
61pub trait CatalogExt: Serialize + serde::de::DeserializeOwned + Default + Clone + Send + Unpin + 'static {}
92
93impl<T: Serialize + serde::de::DeserializeOwned + Default + Clone + Send + Unpin + 'static> CatalogExt for T {}
95
96#[serde_with::serde_as]
104#[serde_with::skip_serializing_none]
105#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
106#[serde(rename_all = "camelCase")]
107#[non_exhaustive]
108pub struct Track {
109 pub name: String,
111
112 pub packaging: Packaging,
114
115 #[serde(default)]
121 pub is_live: bool,
122
123 pub role: Option<Role>,
125
126 pub codec: Option<String>,
128
129 pub width: Option<u32>,
131
132 pub height: Option<u32>,
134
135 pub framerate: Option<f64>,
137
138 pub samplerate: Option<u32>,
140
141 pub channel_config: Option<String>,
143
144 pub bitrate: Option<u64>,
146
147 pub stalled: Option<bool>,
151
152 pub init_data: Option<String>,
158
159 init_ref: Option<String>,
163
164 pub render_group: Option<u32>,
166
167 pub alt_group: Option<u32>,
169
170 #[serde(rename = "maxGrpSapStartingType")]
174 pub max_grp_sap_starting_type: Option<u8>,
175
176 #[serde(rename = "maxObjSapStartingType")]
180 pub max_obj_sap_starting_type: Option<u8>,
181
182 #[serde_as(as = "Option<DurationMilliSecondsWithFrac>")]
186 pub jitter: Option<Duration>,
187}
188
189impl Catalog<()> {
190 pub fn new(tracks: Vec<Track>) -> Self {
194 Self::with_ext(tracks, ())
195 }
196}
197
198impl<E: CatalogExt> Catalog<E> {
199 pub fn with_ext(tracks: Vec<Track>, ext: E) -> Self {
201 Self {
202 generated_at: None,
203 is_complete: false,
204 tracks,
205 ext,
206 }
207 }
208
209 pub fn to_json(&self) -> Result<String, serde_json::Error> {
211 serde_json::to_string(self)
212 }
213
214 #[allow(clippy::should_implement_trait)]
216 pub fn from_str(s: &str) -> Result<Self, serde_json::Error> {
217 serde_json::from_str(s)
218 }
219}
220
221const CURRENT_VERSION: &str = "draft-01";
223
224#[cfg(debug_assertions)]
229fn colliding_member<E: CatalogExt>(ext: &E) -> Option<String> {
230 match serde_json::to_value(ext) {
231 Ok(serde_json::Value::Object(members)) => members.into_iter().map(|(name, _)| name).find(|n| reserved_root(n)),
232 _ => None,
234 }
235}
236
237pub fn reserved_root(name: &str) -> bool {
244 matches!(
245 name,
246 "version" | "generatedAt" | "isComplete" | "tracks" | "initDataList"
247 )
248}
249
250impl<E: CatalogExt> Serialize for Catalog<E> {
251 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
252 use std::collections::HashMap;
253
254 #[cfg(debug_assertions)]
260 if let Some(name) = colliding_member(&self.ext) {
261 panic!("MSF catalog extension member {name:?} collides with a reserved root member");
262 }
263
264 let mut init_data_list: Vec<InitData> = Vec::new();
268 let mut ids: HashMap<String, String> = HashMap::new();
269 let mut tracks = Vec::with_capacity(self.tracks.len());
270
271 for track in &self.tracks {
272 let mut track = track.clone();
273 if let Some(payload) = track.init_data.take() {
274 let id = if let Some(id) = ids.get(&payload) {
275 id.clone()
276 } else {
277 let id = format!("init{}", init_data_list.len());
278 init_data_list.push(InitData {
279 id: id.clone(),
280 kind: "inline".to_string(),
281 data: payload.clone(),
282 });
283 ids.insert(payload, id.clone());
284 id
285 };
286 track.init_ref = Some(id);
287 }
288 tracks.push(track);
289 }
290
291 Wire {
292 version: WireVersion,
293 generated_at: self.generated_at,
294 is_complete: self.is_complete,
295 tracks,
296 init_data_list: (!init_data_list.is_empty()).then_some(init_data_list),
297 ext: self.ext.clone(),
298 }
299 .serialize(serializer)
300 }
301}
302
303impl<'de, E: CatalogExt> Deserialize<'de> for Catalog<E> {
304 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
305 use std::collections::HashMap;
306
307 let wire = Wire::<E>::deserialize(deserializer)?;
308 let init_data_list = wire.init_data_list.unwrap_or_default();
309
310 let by_id: HashMap<&str, &InitData> = init_data_list.iter().map(|e| (e.id.as_str(), e)).collect();
313
314 let tracks = wire
315 .tracks
316 .into_iter()
317 .map(|mut track| -> Result<Track, D::Error> {
318 if let Some(id) = track.init_ref.take() {
322 let entry = by_id.get(id.as_str()).ok_or_else(|| {
323 serde::de::Error::custom(format!(
324 "MSF track {:?} references missing initData {:?}",
325 track.name, id
326 ))
327 })?;
328
329 if entry.kind != "inline" {
330 return Err(serde::de::Error::custom(format!(
331 "MSF track {:?} references initData {:?} with unsupported type {:?}",
332 track.name, id, entry.kind
333 )));
334 }
335
336 match &track.init_data {
337 Some(inline) if inline != &entry.data => {
338 return Err(serde::de::Error::custom(format!(
339 "MSF track {:?} carries conflicting initData and initRef {:?}",
340 track.name, id
341 )));
342 }
343 Some(_) => {}
344 None => track.init_data = Some(entry.data.clone()),
345 }
346 }
347 track.init_ref = None;
348 Ok(track)
349 })
350 .collect::<Result<Vec<_>, _>>()?;
351
352 Ok(Catalog {
353 generated_at: wire.generated_at,
354 is_complete: wire.is_complete,
355 tracks,
356 ext: wire.ext,
357 })
358 }
359}
360
361fn is_false(value: &bool) -> bool {
362 !*value
363}
364
365#[serde_with::skip_serializing_none]
367#[derive(Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369#[serde(bound(serialize = "E: Serialize", deserialize = "E: serde::de::DeserializeOwned"))]
370struct Wire<E> {
371 version: WireVersion,
372 generated_at: Option<u64>,
373 #[serde(default, skip_serializing_if = "is_false")]
374 is_complete: bool,
375 #[serde(default)]
376 tracks: Vec<Track>,
377 init_data_list: Option<Vec<InitData>>,
378
379 #[serde(flatten)]
381 ext: E,
382}
383
384struct WireVersion;
388
389impl Serialize for WireVersion {
390 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
391 serializer.serialize_str(CURRENT_VERSION)
392 }
393}
394
395impl<'de> Deserialize<'de> for WireVersion {
396 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
397 struct VersionVisitor;
398
399 impl serde::de::Visitor<'_> for VersionVisitor {
400 type Value = WireVersion;
401
402 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
403 f.write_str("the JSON number 1 (draft-00) or a \"draft-XX\" version string")
404 }
405
406 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<WireVersion, E> {
410 match v {
411 1 => Ok(WireVersion),
412 other => Err(E::custom(format!("unsupported MSF catalog version: {other}"))),
413 }
414 }
415
416 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<WireVersion, E> {
417 if v == 1 {
418 Ok(WireVersion)
419 } else {
420 Err(E::custom(format!("unsupported MSF catalog version: {v}")))
421 }
422 }
423
424 fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<WireVersion, E> {
425 if v == 1.0 {
426 Ok(WireVersion)
427 } else {
428 Err(E::custom(format!("unsupported MSF catalog version: {v}")))
429 }
430 }
431
432 fn visit_str<E: serde::de::Error>(self, _v: &str) -> Result<WireVersion, E> {
433 Ok(WireVersion)
435 }
436 }
437
438 deserializer.deserialize_any(VersionVisitor)
439 }
440}
441
442#[derive(Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445struct InitData {
446 id: String,
448 #[serde(rename = "type")]
450 kind: String,
451 data: String,
453}
454
455impl Track {
456 pub fn new(name: impl Into<String>, packaging: Packaging) -> Self {
463 Self {
464 name: name.into(),
465 packaging,
466 is_live: false,
467 role: None,
468 codec: None,
469 width: None,
470 height: None,
471 framerate: None,
472 samplerate: None,
473 channel_config: None,
474 bitrate: None,
475 stalled: None,
476 init_data: None,
477 init_ref: None,
478 render_group: None,
479 alt_group: None,
480 max_grp_sap_starting_type: None,
481 max_obj_sap_starting_type: None,
482 jitter: None,
483 }
484 }
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
489pub enum Packaging {
490 Loc,
492 Cmaf,
494 Legacy,
496 MediaTimeline,
498 EventTimeline,
500 Unknown(String),
502}
503
504impl fmt::Display for Packaging {
505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 match self {
507 Packaging::Loc => write!(f, "loc"),
508 Packaging::Cmaf => write!(f, "cmaf"),
509 Packaging::Legacy => write!(f, "legacy"),
510 Packaging::MediaTimeline => write!(f, "mediatimeline"),
511 Packaging::EventTimeline => write!(f, "eventtimeline"),
512 Packaging::Unknown(s) => write!(f, "{s}"),
513 }
514 }
515}
516
517impl FromStr for Packaging {
518 type Err = std::convert::Infallible;
519
520 fn from_str(s: &str) -> Result<Self, Self::Err> {
521 Ok(match s {
522 "loc" => Packaging::Loc,
523 "cmaf" => Packaging::Cmaf,
524 "legacy" => Packaging::Legacy,
525 "mediatimeline" => Packaging::MediaTimeline,
526 "eventtimeline" => Packaging::EventTimeline,
527 other => Packaging::Unknown(other.to_string()),
528 })
529 }
530}
531
532impl Serialize for Packaging {
533 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
534 serializer.serialize_str(&self.to_string())
535 }
536}
537
538impl<'de> Deserialize<'de> for Packaging {
539 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
540 let s = String::deserialize(deserializer)?;
541 Ok(Packaging::from_str(&s).unwrap())
543 }
544}
545
546#[derive(Debug, Clone, PartialEq, Eq)]
548pub enum Role {
549 Video,
551 Audio,
553 AudioDescription,
555 Caption,
557 Subtitle,
559 SignLanguage,
561 Unknown(String),
563}
564
565impl fmt::Display for Role {
566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567 match self {
568 Role::Video => write!(f, "video"),
569 Role::Audio => write!(f, "audio"),
570 Role::AudioDescription => write!(f, "audiodescription"),
571 Role::Caption => write!(f, "caption"),
572 Role::Subtitle => write!(f, "subtitle"),
573 Role::SignLanguage => write!(f, "signlanguage"),
574 Role::Unknown(s) => write!(f, "{s}"),
575 }
576 }
577}
578
579impl FromStr for Role {
580 type Err = std::convert::Infallible;
581
582 fn from_str(s: &str) -> Result<Self, Self::Err> {
583 Ok(match s {
584 "video" => Role::Video,
585 "audio" => Role::Audio,
586 "audiodescription" => Role::AudioDescription,
587 "caption" => Role::Caption,
588 "subtitle" => Role::Subtitle,
589 "signlanguage" => Role::SignLanguage,
590 other => Role::Unknown(other.to_string()),
591 })
592 }
593}
594
595impl Serialize for Role {
596 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
597 serializer.serialize_str(&self.to_string())
598 }
599}
600
601impl<'de> Deserialize<'de> for Role {
602 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
603 let s = String::deserialize(deserializer)?;
604 Ok(Role::from_str(&s).unwrap())
606 }
607}
608
609#[cfg(test)]
610mod test {
611 use super::*;
612
613 fn video_track() -> Track {
614 Track {
615 name: "video0".to_string(),
616 packaging: Packaging::Legacy,
617 is_live: true,
618 role: Some(Role::Video),
619 codec: Some("avc3.64001f".to_string()),
620 width: Some(1280),
621 height: Some(720),
622 framerate: Some(30.0),
623 samplerate: None,
624 channel_config: None,
625 bitrate: Some(6_000_000),
626 stalled: Some(true),
627 init_data: None,
628 init_ref: None,
629 render_group: Some(1),
630 alt_group: None,
631 max_grp_sap_starting_type: None,
632 max_obj_sap_starting_type: None,
633 jitter: None,
634 }
635 }
636
637 fn audio_track() -> Track {
638 Track {
639 name: "audio0".to_string(),
640 packaging: Packaging::Legacy,
641 is_live: true,
642 role: Some(Role::Audio),
643 codec: Some("opus".to_string()),
644 width: None,
645 height: None,
646 framerate: None,
647 samplerate: Some(48_000),
648 channel_config: Some("2".to_string()),
649 bitrate: Some(128_000),
650 stalled: None,
651 init_data: None,
652 init_ref: None,
653 render_group: Some(1),
654 alt_group: None,
655 max_grp_sap_starting_type: None,
656 max_obj_sap_starting_type: None,
657 jitter: None,
658 }
659 }
660
661 fn track_with_sap_and_jitter() -> Track {
662 Track {
663 name: "video0".to_string(),
664 packaging: Packaging::Cmaf,
665 is_live: true,
666 role: Some(Role::Video),
667 codec: Some("avc1.640028".to_string()),
668 width: Some(1920),
669 height: Some(1080),
670 framerate: Some(30.0),
671 samplerate: None,
672 channel_config: None,
673 bitrate: Some(5_000_000),
674 stalled: None,
675 init_data: None,
676 init_ref: None,
677 render_group: Some(1),
678 alt_group: None,
679 max_grp_sap_starting_type: Some(1),
680 max_obj_sap_starting_type: Some(2),
681 jitter: Some(Duration::from_millis(15)),
682 }
683 }
684
685 #[test]
686 fn serialize_video_track() {
687 let catalog = Catalog::new(vec![video_track()]);
688
689 let json = catalog.to_json().unwrap();
690 let parsed = Catalog::<()>::from_str(&json).unwrap();
691 assert_eq!(catalog, parsed);
692
693 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
695 let track = &value["tracks"][0];
696 assert!(track.get("samplerate").is_none());
697 assert!(track.get("channelConfig").is_none());
698
699 assert!(track.get("maxGrpSapStartingType").is_none());
701 assert!(track.get("maxObjSapStartingType").is_none());
702 assert!(track.get("jitter").is_none());
703 assert_eq!(track["stalled"], true);
704 }
705
706 #[test]
707 fn serialize_audio_track() {
708 let catalog = Catalog::new(vec![audio_track()]);
709
710 let json = catalog.to_json().unwrap();
711 let parsed = Catalog::<()>::from_str(&json).unwrap();
712 assert_eq!(catalog, parsed);
713
714 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
716 let track = &value["tracks"][0];
717 assert!(track.get("width").is_none());
718 assert!(track.get("height").is_none());
719 assert!(track.get("framerate").is_none());
720 }
721
722 #[test]
723 fn packaging_roundtrip() {
724 for (s, expected) in [
725 ("loc", Packaging::Loc),
726 ("cmaf", Packaging::Cmaf),
727 ("legacy", Packaging::Legacy),
728 ("mediatimeline", Packaging::MediaTimeline),
729 ("eventtimeline", Packaging::EventTimeline),
730 ("custom", Packaging::Unknown("custom".to_string())),
731 ] {
732 let packaging: Packaging = s.parse().unwrap();
733 assert_eq!(packaging, expected);
734 assert_eq!(packaging.to_string(), s);
735 }
736 }
737
738 #[test]
739 fn role_roundtrip() {
740 for (s, expected) in [
741 ("video", Role::Video),
742 ("audio", Role::Audio),
743 ("audiodescription", Role::AudioDescription),
744 ("caption", Role::Caption),
745 ("subtitle", Role::Subtitle),
746 ("signlanguage", Role::SignLanguage),
747 ("custom", Role::Unknown("custom".to_string())),
748 ] {
749 let role: Role = s.parse().unwrap();
750 assert_eq!(role, expected);
751 assert_eq!(role.to_string(), s);
752 }
753 }
754
755 #[test]
756 fn roundtrip_empty() {
757 let catalog = Catalog::new(vec![]);
758 let json = catalog.to_json().unwrap();
759 let parsed = Catalog::<()>::from_str(&json).unwrap();
760 assert_eq!(catalog, parsed);
761 }
762
763 #[test]
764 fn cmaf_packaging() {
765 let mut track = track_with_sap_and_jitter();
766 track.name = "hd".to_string();
767 track.alt_group = Some(1);
768 track.max_grp_sap_starting_type = None;
769 track.max_obj_sap_starting_type = None;
770 track.jitter = None;
771 track.init_data = Some("AQID".to_string());
772
773 let catalog = Catalog::new(vec![track]);
774
775 let json = catalog.to_json().unwrap();
776 assert!(json.contains("\"packaging\":\"cmaf\""));
777 let parsed = Catalog::<()>::from_str(&json).unwrap();
778 assert_eq!(catalog, parsed);
779 assert_eq!(parsed.tracks[0].init_data.as_deref(), Some("AQID"));
780 }
781
782 #[test]
783 fn serialize_sap_fields() {
784 let catalog = Catalog::new(vec![track_with_sap_and_jitter()]);
785
786 let json = catalog.to_json().unwrap();
787
788 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
791 let track = &value["tracks"][0];
792 assert_eq!(track.get("maxGrpSapStartingType"), Some(&serde_json::json!(1)));
793 assert_eq!(track.get("maxObjSapStartingType"), Some(&serde_json::json!(2)));
794 assert_eq!(track.get("jitter").and_then(serde_json::Value::as_f64), Some(15.0));
795
796 assert!(track.get("max_grp_sap_starting_type").is_none());
798 assert!(track.get("max_obj_sap_starting_type").is_none());
799 }
800
801 #[test]
802 fn deserialize_without_sap_fields() {
803 let json = r#"{
806 "version": 1,
807 "tracks": [{
808 "name": "video0",
809 "packaging": "cmaf",
810 "isLive": true,
811 "role": "video",
812 "codec": "avc1.640028",
813 "width": 1920,
814 "height": 1080,
815 "framerate": 30.0,
816 "bitrate": 5000000,
817 "renderGroup": 1
818 }]
819 }"#;
820
821 let catalog = Catalog::<()>::from_str(json).unwrap();
822 let track = &catalog.tracks[0];
823 assert_eq!(track.max_grp_sap_starting_type, None);
824 assert_eq!(track.max_obj_sap_starting_type, None);
825 assert_eq!(track.jitter, None);
826 }
827
828 #[test]
829 fn sap_and_jitter_roundtrip() {
830 let original = Catalog::new(vec![track_with_sap_and_jitter()]);
831
832 let json = original.to_json().unwrap();
833 let parsed = Catalog::<()>::from_str(&json).unwrap();
834 assert_eq!(original, parsed);
835 assert_eq!(parsed.tracks[0].max_grp_sap_starting_type, Some(1));
836 assert_eq!(parsed.tracks[0].max_obj_sap_starting_type, Some(2));
837 assert_eq!(parsed.tracks[0].jitter, Some(Duration::from_millis(15)));
838 }
839
840 #[test]
841 fn fractional_jitter_roundtrips() {
842 let json = r#"{
843 "version": "draft-01",
844 "tracks": [{
845 "name": "video0",
846 "packaging": "cmaf",
847 "isLive": true,
848 "role": "video",
849 "codec": "avc1.640028",
850 "jitter": 15.0
851 }]
852 }"#;
853
854 let catalog = Catalog::<()>::from_str(json).expect("fractional jitter must decode");
855 assert_eq!(catalog.tracks[0].jitter, Some(Duration::from_millis(15)));
856
857 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
858 assert_eq!(value["tracks"][0]["jitter"].as_f64(), Some(15.0));
859 }
860
861 #[test]
862 fn serialize_emits_draft01_version() {
863 let json = Catalog::<()>::default().to_json().unwrap();
865 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
866 assert_eq!(value["version"], serde_json::json!("draft-01"));
867 }
868
869 #[test]
870 fn draft00_numeric_version_decodes_and_normalizes() {
871 let catalog = Catalog::<()>::from_str(r#"{"version":1,"tracks":[]}"#).unwrap();
874 assert!(catalog.tracks.is_empty());
875
876 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
877 assert_eq!(value["version"], serde_json::json!("draft-01"));
878 }
879
880 #[test]
881 fn draft01_string_version_decodes() {
882 let catalog = Catalog::<()>::from_str(r#"{"version":"draft-01","tracks":[]}"#).unwrap();
883 assert!(catalog.tracks.is_empty());
884 }
885
886 #[test]
887 fn unknown_version_string_is_accepted() {
888 assert!(Catalog::<()>::from_str(r#"{"version":"draft-99","tracks":[]}"#).is_ok());
891 }
892
893 #[test]
894 fn unsupported_numeric_version_errors() {
895 assert!(Catalog::<()>::from_str(r#"{"version":2,"tracks":[]}"#).is_err());
897 }
898
899 #[test]
900 fn float_numeric_version_is_accepted() {
901 assert!(Catalog::<()>::from_str(r#"{"version":1.0,"tracks":[]}"#).is_ok());
904 assert!(Catalog::<()>::from_str(r#"{"version":2.0,"tracks":[]}"#).is_err());
905 }
906
907 #[test]
908 fn dangling_init_ref_errors() {
909 let json = r#"{
912 "version": "draft-01",
913 "initDataList": [
914 { "id": "v0", "type": "inline", "data": "AQID" }
915 ],
916 "tracks": [
917 { "name": "a", "packaging": "cmaf", "isLive": true, "role": "video",
918 "codec": "avc1.640028", "initRef": "missing" }
919 ]
920 }"#;
921
922 let err = Catalog::<()>::from_str(json).expect_err("dangling initRef must fail");
923 assert!(err.to_string().contains("missing initData"), "unexpected error: {err}");
924 }
925
926 #[test]
927 fn unsupported_init_ref_type_errors() {
928 let json = r#"{
931 "version": "draft-01",
932 "initDataList": [
933 { "id": "v0", "type": "url", "data": "https://example.com/init" }
934 ],
935 "tracks": [
936 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
937 "codec": "avc1.640028", "initRef": "v0" }
938 ]
939 }"#;
940
941 let err = Catalog::<()>::from_str(json).expect_err("unsupported initRef type must fail");
942 assert!(err.to_string().contains("unsupported type"), "unexpected error: {err}");
943 }
944
945 #[test]
946 fn inline_init_data_and_init_ref_must_agree() {
947 let json = r#"{
948 "version": "draft-01",
949 "initDataList": [
950 { "id": "v0", "type": "inline", "data": "AQID" }
951 ],
952 "tracks": [
953 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
954 "codec": "avc1.640028", "initData": "BAUG", "initRef": "v0" }
955 ]
956 }"#;
957
958 let err = Catalog::<()>::from_str(json).expect_err("conflicting initData/initRef must fail");
959 assert!(err.to_string().contains("conflicting"), "unexpected error: {err}");
960 }
961
962 #[test]
963 fn matching_inline_init_data_and_init_ref_decodes() {
964 let json = r#"{
965 "version": "draft-01",
966 "initDataList": [
967 { "id": "v0", "type": "inline", "data": "AQID" }
968 ],
969 "tracks": [
970 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
971 "codec": "avc1.640028", "initData": "AQID", "initRef": "v0" }
972 ]
973 }"#;
974
975 let catalog = Catalog::<()>::from_str(json).unwrap();
976 assert_eq!(catalog.tracks[0].init_data.as_deref(), Some("AQID"));
977 }
978
979 #[test]
980 fn draft01_init_ref_resolves_to_inline() {
981 let json = r#"{
984 "version": "draft-01",
985 "initDataList": [
986 { "id": "v0", "type": "inline", "data": "AQID" }
987 ],
988 "tracks": [
989 { "name": "video0", "packaging": "cmaf", "isLive": true, "role": "video",
990 "codec": "avc1.640028", "initRef": "v0" }
991 ]
992 }"#;
993
994 let catalog = Catalog::<()>::from_str(json).unwrap();
995 assert_eq!(catalog.tracks[0].init_data.as_deref(), Some("AQID"));
996 }
997
998 #[test]
999 fn serialize_hoists_and_dedups_init_data() {
1000 let mut a = video_track();
1004 a.name = "a".to_string();
1005 a.init_data = Some("AQID".to_string());
1006 let mut b = video_track();
1007 b.name = "b".to_string();
1008 b.init_data = Some("AQID".to_string());
1009
1010 let catalog = Catalog::new(vec![a, b]);
1011 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
1012
1013 let list = value["initDataList"].as_array().expect("initDataList present");
1014 assert_eq!(list.len(), 1, "identical payloads should dedup to one entry");
1015 assert_eq!(list[0]["data"], serde_json::json!("AQID"));
1016 assert_eq!(list[0]["type"], serde_json::json!("inline"));
1017
1018 let id = list[0]["id"].as_str().unwrap();
1019 for t in value["tracks"].as_array().unwrap() {
1020 assert_eq!(t["initRef"], serde_json::json!(id));
1021 assert!(t.get("initData").is_none(), "no inline initData on the wire");
1022 }
1023
1024 let parsed = Catalog::<()>::from_str(&catalog.to_json().unwrap()).unwrap();
1026 assert_eq!(parsed.tracks[0].init_data.as_deref(), Some("AQID"));
1027 assert_eq!(parsed.tracks[1].init_data.as_deref(), Some("AQID"));
1028 }
1029
1030 #[test]
1031 fn draft00_example_av_decodes() {
1032 let json = r#"{
1036 "version": 1,
1037 "generatedAt": 1746104606044,
1038 "tracks": [
1039 {
1040 "name": "1080p-video",
1041 "namespace": "conference.example.com/conference123/alice",
1042 "packaging": "loc",
1043 "isLive": true,
1044 "targetLatency": 2000,
1045 "role": "video",
1046 "renderGroup": 1,
1047 "codec": "av01.0.08M.10.0.110.09",
1048 "width": 1920,
1049 "height": 1080,
1050 "framerate": 30,
1051 "bitrate": 1500000
1052 },
1053 {
1054 "name": "audio",
1055 "namespace": "conference.example.com/conference123/alice",
1056 "packaging": "loc",
1057 "isLive": true,
1058 "targetLatency": 2000,
1059 "role": "audio",
1060 "codec": "opus",
1061 "samplerate": 48000,
1062 "channelConfig": "2",
1063 "bitrate": 32000
1064 }
1065 ]
1066 }"#;
1067
1068 let catalog = Catalog::<()>::from_str(json).expect("draft-00 AV catalog must decode");
1069 assert_eq!(catalog.generated_at, Some(1746104606044));
1070 assert_eq!(catalog.tracks.len(), 2);
1071 assert_eq!(catalog.tracks[0].framerate, Some(30.0));
1072 assert_eq!(catalog.tracks[1].channel_config.as_deref(), Some("2"));
1073 }
1074
1075 #[test]
1076 fn draft00_example_timeline_tracks_decode() {
1077 let json = r#"{
1080 "version": 1,
1081 "generatedAt": 1746104606044,
1082 "tracks": [
1083 {
1084 "name": "history",
1085 "namespace": "conference.example.com/conference123/alice",
1086 "packaging": "mediatimeline",
1087 "mimetype": "application/json",
1088 "depends": ["1080p-video", "audio"]
1089 },
1090 {
1091 "name": "1080p-video",
1092 "namespace": "conference.example.com/conference123/alice",
1093 "packaging": "loc",
1094 "isLive": true,
1095 "role": "video",
1096 "codec": "av01.0.08M.10.0.110.09",
1097 "width": 1920,
1098 "height": 1080,
1099 "framerate": 30,
1100 "bitrate": 1500000
1101 }
1102 ]
1103 }"#;
1104
1105 let catalog = Catalog::<()>::from_str(json).expect("draft-00 timeline catalog must decode");
1106 assert_eq!(catalog.tracks.len(), 2);
1107 assert!(!catalog.tracks[0].is_live);
1109 assert_eq!(catalog.tracks[0].packaging, Packaging::MediaTimeline);
1110 }
1111
1112 #[test]
1113 fn draft00_example_complete_decodes() {
1114 let json = r#"{
1116 "version": 1,
1117 "generatedAt": 1746104606044,
1118 "isComplete": true,
1119 "tracks": []
1120 }"#;
1121 let catalog = Catalog::<()>::from_str(json).expect("draft-00 completion catalog must decode");
1122 assert_eq!(catalog.generated_at, Some(1746104606044));
1123 assert!(catalog.is_complete);
1124 assert!(catalog.tracks.is_empty());
1125
1126 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
1127 assert_eq!(value["generatedAt"], serde_json::json!(1746104606044u64));
1128 assert_eq!(value["isComplete"], serde_json::json!(true));
1129 }
1130
1131 #[test]
1132 fn default_catalog_omits_completion_fields() {
1133 let value: serde_json::Value = serde_json::from_str(&Catalog::<()>::default().to_json().unwrap()).unwrap();
1134 assert!(value.get("generatedAt").is_none());
1135 assert!(value.get("isComplete").is_none());
1136 }
1137
1138 #[test]
1140 fn typed_extension_roundtrip() {
1141 #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
1142 #[serde(rename_all = "camelCase")]
1143 struct Program {
1144 program_number: u16,
1145 pmt_pid: u16,
1146 }
1147
1148 #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
1149 struct MpegtsExt {
1150 #[serde(default, skip_serializing_if = "Option::is_none")]
1151 mpegts: Option<Program>,
1152 }
1153
1154 let ext = MpegtsExt {
1155 mpegts: Some(Program {
1156 program_number: 1,
1157 pmt_pid: 100,
1158 }),
1159 };
1160 let catalog = Catalog::with_ext(vec![video_track()], ext.clone());
1161
1162 let json = catalog.to_json().unwrap();
1163 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1164 assert_eq!(
1165 value["mpegts"]["pmtPid"],
1166 serde_json::json!(100),
1167 "written flat: {json}"
1168 );
1169
1170 let parsed = Catalog::<MpegtsExt>::from_str(&json).expect("typed extension must decode");
1171 assert_eq!(parsed.ext, ext);
1172 assert_eq!(parsed.tracks.len(), 1);
1173 }
1174
1175 #[test]
1178 #[should_panic(expected = "collides with a reserved root member")]
1179 fn colliding_typed_extension_trips_the_debug_assertion() {
1180 #[derive(Serialize, Deserialize, Clone, Default)]
1181 struct BadExt {
1182 tracks: Vec<String>,
1183 }
1184
1185 let _ = Catalog::with_ext(Vec::new(), BadExt::default()).to_json();
1186 }
1187
1188 #[test]
1191 fn unmodeled_root_members_are_dropped() {
1192 let json = r#"{ "version": "draft-01", "tracks": [], "somethingElse": [1, 2, 3] }"#;
1193 let catalog = Catalog::<()>::from_str(json).expect("an unknown member must not fail the parse");
1194 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
1195 assert!(value.get("somethingElse").is_none());
1196 }
1197
1198 #[test]
1201 fn untyped_extension_is_passthrough() {
1202 type Extra = serde_json::Map<String, serde_json::Value>;
1203
1204 let json = r#"{
1205 "version": "draft-01",
1206 "tracks": [],
1207 "mpegts": { "program": { "pmtPid": 100 } },
1208 "somethingElse": [1, 2, 3]
1209 }"#;
1210 let catalog = Catalog::<Extra>::from_str(json).expect("passthrough must decode");
1211 assert_eq!(catalog.ext.len(), 2);
1212
1213 let value: serde_json::Value = serde_json::from_str(&catalog.to_json().unwrap()).unwrap();
1214 assert_eq!(value["mpegts"]["program"]["pmtPid"], serde_json::json!(100));
1215 assert_eq!(value["somethingElse"], serde_json::json!([1, 2, 3]));
1216 }
1217}