moq_catalog/
lib.rs

1//! This module contains the structs and functions for the MoQ catalog format
2/// The catalog format is a JSON file that describes the tracks available in a broadcast.
3///
4/// The current version of the catalog format is draft-01.
5/// https://www.ietf.org/archive/id/draft-ietf-moq-catalogformat-01.html
6use serde::{Deserialize, Serialize};
7
8#[derive(Serialize, Deserialize, Debug)]
9pub struct Root {
10    pub version: u16,
11
12    #[serde(rename = "streamingFormat")]
13    pub streaming_format: u16,
14
15    #[serde(rename = "streamingFormatVersion")]
16    pub streaming_format_version: String,
17
18    #[serde(rename = "supportsDeltaUpdates")]
19    pub streaming_delta_updates: bool,
20
21    #[serde(rename = "commonTrackFields")]
22    pub common_track_fields: CommonTrackFields,
23
24    pub tracks: Vec<Track>,
25}
26
27#[derive(Serialize, Deserialize, Debug, Default)]
28pub struct Track {
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub namespace: Option<String>,
31
32    pub name: String,
33
34    #[serde(rename = "initTrack", skip_serializing_if = "Option::is_none")]
35    pub init_track: Option<String>,
36
37    #[serde(rename = "initData", skip_serializing_if = "Option::is_none")]
38    pub init_data: Option<String>,
39
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub packaging: Option<TrackPackaging>,
42
43    #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")]
44    pub render_group: Option<u16>,
45
46    #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")]
47    pub alt_group: Option<u16>,
48
49    #[serde(rename = "selectionParams")]
50    pub selection_params: SelectionParam,
51
52    #[serde(rename = "temporalId", skip_serializing_if = "Option::is_none")]
53    pub temporal_id: Option<u32>,
54
55    #[serde(rename = "spatialId", skip_serializing_if = "Option::is_none")]
56    pub spatial_id: Option<u32>,
57
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub depends: Option<Vec<String>>,
60}
61
62impl Track {
63    #[allow(dead_code)] // TODO use
64    fn with_common(&mut self, common: &CommonTrackFields) {
65        if self.namespace.is_none() {
66            self.namespace.clone_from(&common.namespace);
67        }
68        if self.packaging.is_none() {
69            self.packaging.clone_from(&common.packaging);
70        }
71        if self.render_group.is_none() {
72            self.render_group = common.render_group;
73        }
74        if self.alt_group.is_none() {
75            self.alt_group = common.alt_group;
76        }
77    }
78}
79
80#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)]
81pub enum TrackPackaging {
82    #[serde(rename = "cmaf")]
83    #[default]
84    Cmaf,
85
86    #[serde(rename = "loc")]
87    Loc,
88}
89
90#[derive(Serialize, Deserialize, Debug, Default)]
91pub struct SelectionParam {
92    pub codec: Option<String>,
93
94    #[serde(rename = "mimeType")]
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub mime_type: Option<String>,
97
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub framerate: Option<u64>,
100
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub bitrate: Option<u32>,
103
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub width: Option<u32>,
106
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub height: Option<u32>,
109
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub samplerate: Option<u32>,
112
113    #[serde(rename = "channelConfig", skip_serializing_if = "Option::is_none")]
114    pub channel_config: Option<String>,
115
116    #[serde(rename = "displayWidth", skip_serializing_if = "Option::is_none")]
117    pub display_width: Option<u16>,
118
119    #[serde(rename = "displayHeight", skip_serializing_if = "Option::is_none")]
120    pub display_height: Option<u16>,
121
122    #[serde(rename = "lang", skip_serializing_if = "Option::is_none")]
123    pub language: Option<String>,
124}
125
126#[derive(Serialize, Deserialize, Debug, Default)]
127pub struct CommonTrackFields {
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub namespace: Option<String>,
130
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub packaging: Option<TrackPackaging>,
133
134    #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")]
135    pub render_group: Option<u16>,
136
137    #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")]
138    pub alt_group: Option<u16>,
139}
140
141impl CommonTrackFields {
142    /// Serialize function to conditionally include fields based on their commonality amoung tracks
143    pub fn from_tracks(tracks: &mut [Track]) -> Self {
144        if tracks.is_empty() {
145            return Default::default();
146        }
147
148        // Use the first track as the basis
149        let mut common = Self {
150            namespace: tracks[0].namespace.clone(),
151            packaging: tracks[0].packaging.clone(),
152            render_group: tracks[0].render_group,
153            alt_group: tracks[0].alt_group,
154        };
155
156        // Loop over the other tracks to check if they have the same values
157        for track in &mut tracks[1..] {
158            if track.namespace != common.namespace {
159                common.namespace = None;
160            }
161            if track.packaging != common.packaging {
162                common.packaging = None;
163            }
164            if track.render_group != common.render_group {
165                common.render_group = None
166            }
167            if track.alt_group != common.alt_group {
168                common.alt_group = None;
169            }
170        }
171
172        // Loop again to remove the common fields from the tracks
173        for track in tracks {
174            if common.namespace.is_some() {
175                track.namespace = None;
176            }
177            if track.packaging.is_some() {
178                track.packaging = None;
179            }
180            if track.render_group.is_some() {
181                track.render_group = None;
182            }
183            if track.alt_group.is_some() {
184                track.alt_group = None;
185            }
186        }
187
188        common
189    }
190}