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