Skip to main content

msf_rtsp/header/
props.rs

1//! Media-Properties and related types.
2
3use std::{
4    borrow::{Borrow, Cow},
5    fmt::{self, Display, Formatter},
6    ops::Deref,
7    str::FromStr,
8};
9
10use str_reader::StringReader;
11
12#[cfg(feature = "abs-time")]
13use chrono::{DateTime, Utc};
14
15use crate::{
16    Error,
17    header::{CharExt, HeaderFieldValue, StringReaderExt, ValueListDisplay},
18};
19
20/// Media properties header.
21#[derive(Default, Clone)]
22pub struct MediaPropertiesHeader {
23    random_access: Option<RandomAccess>,
24    content_modifications: Option<ContentModifications>,
25    retention: Option<Retention>,
26    scales: Option<Scales>,
27    other: Vec<String>,
28}
29
30impl MediaPropertiesHeader {
31    /// Create a new media properties header with no properties set.
32    #[inline]
33    pub const fn new() -> Self {
34        Self {
35            random_access: None,
36            content_modifications: None,
37            retention: None,
38            scales: None,
39            other: Vec::new(),
40        }
41    }
42
43    /// Get the random access property.
44    #[inline]
45    pub fn random_access(&self) -> Option<RandomAccess> {
46        self.random_access
47    }
48
49    /// Set the random access property.
50    #[inline]
51    pub const fn with_random_access(mut self, random_access: Option<RandomAccess>) -> Self {
52        self.random_access = random_access;
53        self
54    }
55
56    /// Get the content modifications property.
57    #[inline]
58    pub fn content_modifications(&self) -> Option<ContentModifications> {
59        self.content_modifications
60    }
61
62    /// Set the content modifications property.
63    #[inline]
64    pub const fn with_content_modifications(
65        mut self,
66        content_modifications: Option<ContentModifications>,
67    ) -> Self {
68        self.content_modifications = content_modifications;
69        self
70    }
71
72    /// Get the retention property.
73    #[inline]
74    pub fn retention(&self) -> Option<Retention> {
75        self.retention
76    }
77
78    /// Set the retention property.
79    #[inline]
80    pub const fn with_retention(mut self, retention: Option<Retention>) -> Self {
81        self.retention = retention;
82        self
83    }
84
85    /// Get the scales property.
86    #[inline]
87    pub fn scales(&self) -> Option<&[Scale]> {
88        self.scales.as_ref().map(|s| &s.inner[..])
89    }
90
91    /// Set the scales property.
92    #[inline]
93    pub fn with_scales(mut self, scales: Option<Scales>) -> Self {
94        self.scales = scales;
95        self
96    }
97
98    /// Get other properties.
99    #[inline]
100    pub fn other_properties(&self) -> &[String] {
101        &self.other
102    }
103
104    /// Set other properties.
105    pub fn with_other_properties<T>(mut self, other: T) -> Self
106    where
107        T: Into<Vec<String>>,
108    {
109        self.other = other.into();
110        self
111    }
112}
113
114impl Display for MediaPropertiesHeader {
115    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
116        let props = [
117            self.random_access.map(MediaProperty::from),
118            self.content_modifications.map(MediaProperty::from),
119            self.retention.map(MediaProperty::from),
120            self.scales.as_ref().map(MediaProperty::from),
121        ];
122
123        let other = self
124            .other
125            .iter()
126            .map(|p| MediaProperty::Other(Cow::Borrowed(p)));
127
128        let properties = props.into_iter().flatten().chain(other);
129
130        Display::fmt(&ValueListDisplay::new(", ", properties), f)
131    }
132}
133
134impl From<MediaPropertiesHeader> for HeaderFieldValue {
135    #[inline]
136    fn from(header: MediaPropertiesHeader) -> Self {
137        HeaderFieldValue::from(header.to_string())
138    }
139}
140
141impl FromStr for MediaPropertiesHeader {
142    type Err = Error;
143
144    fn from_str(s: &str) -> Result<Self, Self::Err> {
145        let mut reader = StringReader::new(s.trim());
146
147        let mut res = Self::new();
148
149        while !reader.is_empty() {
150            let property = reader
151                .parse_media_property()
152                .map_err(|err| Error::from_static_msg_and_cause("invalid media property", err))?;
153
154            match property {
155                MediaProperty::RandomAccess(p) => res.random_access = Some(p),
156                MediaProperty::ContentModifications(p) => res.content_modifications = Some(p),
157                MediaProperty::Retention(p) => res.retention = Some(p),
158                MediaProperty::Scales(p) => res.scales = Some(p.into_owned()),
159                MediaProperty::Other(p) => res.other.push(p.into_owned()),
160            }
161
162            if reader.match_rtsp_separator(',').is_err() {
163                break;
164            }
165        }
166
167        if reader.is_empty() {
168            Ok(res)
169        } else {
170            Err(Error::from_static_msg("unexpected character"))
171        }
172    }
173}
174
175impl TryFrom<&HeaderFieldValue> for MediaPropertiesHeader {
176    type Error = Error;
177
178    fn try_from(value: &HeaderFieldValue) -> Result<Self, Self::Error> {
179        value
180            .to_str()
181            .map_err(|_| Error::from_static_msg("header field is not UTF-8 encoded"))?
182            .parse()
183    }
184}
185
186/// Random access media property.
187#[derive(Debug, Copy, Clone, PartialEq)]
188pub enum RandomAccess {
189    RandomAccess(Option<f32>),
190    BeginningOnly,
191    NoSeeking,
192}
193
194impl Display for RandomAccess {
195    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
196        match self {
197            Self::RandomAccess(Some(max_delta)) => write!(f, "Random-Access={max_delta}"),
198            Self::RandomAccess(None) => f.write_str("Random-Access"),
199            Self::BeginningOnly => f.write_str("Beginning-Only"),
200            Self::NoSeeking => f.write_str("No-Seeking"),
201        }
202    }
203}
204
205/// Content modifications media property.
206#[derive(Debug, Copy, Clone, PartialEq, Eq)]
207pub enum ContentModifications {
208    Immutable,
209    Dynamic,
210    TimeProgressing,
211}
212
213impl Display for ContentModifications {
214    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
215        let s = match self {
216            Self::Immutable => "Immutable",
217            Self::Dynamic => "Dynamic",
218            Self::TimeProgressing => "Time-Progressing",
219        };
220
221        f.write_str(s)
222    }
223}
224
225/// Retention media property.
226#[derive(Debug, Copy, Clone, PartialEq)]
227pub enum Retention {
228    Unlimited,
229    #[cfg_attr(docsrs, doc(cfg(feature = "abs-time")))]
230    #[cfg(feature = "abs-time")]
231    TimeLimited(DateTime<Utc>),
232    TimeDuration(f32),
233}
234
235impl Display for Retention {
236    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
237        match self {
238            Self::Unlimited => f.write_str("Unlimited"),
239            #[cfg(feature = "abs-time")]
240            Self::TimeLimited(t) => write!(f, "Time-Limited={}", t.format("%Y%m%dT%H%M%S%.3fZ")),
241            Self::TimeDuration(d) => write!(f, "Time-Duration={d}"),
242        }
243    }
244}
245
246/// Media scale.
247#[derive(Debug, Copy, Clone, PartialEq)]
248pub enum Scale {
249    Value(f32),
250    Range(f32, f32),
251}
252
253impl Display for Scale {
254    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
255        match self {
256            Self::Value(v) => Display::fmt(v, f),
257            Self::Range(start, end) => write!(f, "{start}:{end}"),
258        }
259    }
260}
261
262/// Scales media property.
263#[derive(Clone)]
264pub struct Scales {
265    inner: Vec<Scale>,
266}
267
268impl Scales {
269    /// Create a new scales media property.
270    pub fn new<T>(scales: T) -> Self
271    where
272        T: Into<Vec<Scale>>,
273    {
274        Self {
275            inner: scales.into(),
276        }
277    }
278}
279
280impl Display for Scales {
281    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
282        write!(f, "Scales=\"")?;
283
284        let mut scales = self.inner.iter();
285
286        if let Some(scale) = scales.next() {
287            Display::fmt(scale, f)?;
288        }
289
290        for scale in scales {
291            write!(f, ", {scale}")?;
292        }
293
294        f.write_str("\"")
295    }
296}
297
298impl AsRef<[Scale]> for Scales {
299    #[inline]
300    fn as_ref(&self) -> &[Scale] {
301        &self.inner
302    }
303}
304
305impl Borrow<[Scale]> for Scales {
306    #[inline]
307    fn borrow(&self) -> &[Scale] {
308        &self.inner
309    }
310}
311
312impl Deref for Scales {
313    type Target = [Scale];
314
315    #[inline]
316    fn deref(&self) -> &Self::Target {
317        &self.inner
318    }
319}
320
321/// Media property reference.
322#[derive(Clone)]
323enum MediaProperty<'a> {
324    RandomAccess(RandomAccess),
325    ContentModifications(ContentModifications),
326    Retention(Retention),
327    Scales(Cow<'a, Scales>),
328    Other(Cow<'a, str>),
329}
330
331impl Display for MediaProperty<'_> {
332    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
333        match self {
334            Self::RandomAccess(p) => Display::fmt(p, f),
335            Self::ContentModifications(p) => Display::fmt(p, f),
336            Self::Retention(p) => Display::fmt(p, f),
337            Self::Scales(p) => Display::fmt(p, f),
338            Self::Other(p) => f.write_str(p),
339        }
340    }
341}
342
343impl From<RandomAccess> for MediaProperty<'_> {
344    #[inline]
345    fn from(property: RandomAccess) -> Self {
346        Self::RandomAccess(property)
347    }
348}
349
350impl From<ContentModifications> for MediaProperty<'_> {
351    #[inline]
352    fn from(property: ContentModifications) -> Self {
353        Self::ContentModifications(property)
354    }
355}
356
357impl From<Retention> for MediaProperty<'_> {
358    #[inline]
359    fn from(property: Retention) -> Self {
360        Self::Retention(property)
361    }
362}
363
364impl<'a> From<&'a Scales> for MediaProperty<'a> {
365    #[inline]
366    fn from(property: &'a Scales) -> Self {
367        Self::Scales(Cow::Borrowed(property))
368    }
369}
370
371impl From<Scales> for MediaProperty<'_> {
372    #[inline]
373    fn from(property: Scales) -> Self {
374        Self::Scales(Cow::Owned(property))
375    }
376}
377
378/// Helper trait.
379trait MediaPropertyParser<'a> {
380    /// Parse the next media property.
381    fn parse_media_property(&mut self) -> Result<MediaProperty<'a>, Error>;
382
383    /// Read other media property value.
384    fn read_other_media_property_value(&mut self) -> Result<&'a str, Error>;
385
386    /// Parse scales.
387    fn parse_scales(&mut self) -> Result<Scales, Error>;
388}
389
390impl<'a> MediaPropertyParser<'a> for StringReader<'a> {
391    fn parse_media_property(&mut self) -> Result<MediaProperty<'a>, Error> {
392        let s = self.as_str();
393
394        let mut reader = StringReader::new(s);
395
396        let name = reader.read_rtsp_token()?;
397
398        let property = if name.eq_ignore_ascii_case("Random-Access") {
399            if reader.match_rtsp_separator('=').is_ok() {
400                let max_delta = reader.parse_positive_f32()?;
401
402                MediaProperty::from(RandomAccess::RandomAccess(Some(max_delta)))
403            } else {
404                MediaProperty::from(RandomAccess::RandomAccess(None))
405            }
406        } else if name.eq_ignore_ascii_case("Beginning-Only") {
407            MediaProperty::from(RandomAccess::BeginningOnly)
408        } else if name.eq_ignore_ascii_case("No-Seeking") {
409            MediaProperty::from(RandomAccess::NoSeeking)
410        } else if name.eq_ignore_ascii_case("Immutable") {
411            MediaProperty::from(ContentModifications::Immutable)
412        } else if name.eq_ignore_ascii_case("Dynamic") {
413            MediaProperty::from(ContentModifications::Dynamic)
414        } else if name.eq_ignore_ascii_case("Time-Progressing") {
415            MediaProperty::from(ContentModifications::TimeProgressing)
416        } else if name.eq_ignore_ascii_case("Unlimited") {
417            MediaProperty::from(Retention::Unlimited)
418        } else if name.eq_ignore_ascii_case("Time-Limited") {
419            reader.match_rtsp_separator('=')?;
420
421            let timestamp =
422                reader.read_while(|c| c.is_ascii_digit() || matches!(c, 'T' | 'Z' | '.'));
423
424            #[cfg(feature = "abs-time")]
425            {
426                let ts = DateTime::parse_from_str(timestamp, "%Y%m%dT%H%M%S%.fZ")
427                    .map_err(|_| Error::from_static_msg("invalid timestamp"))?
428                    .to_utc();
429
430                MediaProperty::from(Retention::TimeLimited(ts))
431            }
432
433            #[cfg(not(feature = "abs-time"))]
434            {
435                let _ = timestamp;
436
437                let r = reader.as_str();
438
439                let original_len = s.len();
440                let remaining_len = r.len();
441
442                let len = original_len - remaining_len;
443
444                let property = &s[..len];
445
446                MediaProperty::Other(Cow::Borrowed(property))
447            }
448        } else if name.eq_ignore_ascii_case("Time-Duration") {
449            reader.match_rtsp_separator('=')?;
450
451            let duration = reader.parse_positive_f32()?;
452
453            MediaProperty::from(Retention::TimeDuration(duration))
454        } else if name.eq_ignore_ascii_case("Scales") {
455            reader.match_rtsp_separator('=')?;
456
457            let scales = reader.read_rtsp_quoted_string()?.trim_matches('"').trim();
458
459            let mut reader = StringReader::new(scales);
460
461            let scales = reader.parse_scales()?;
462
463            if !reader.is_empty() {
464                return Err(Error::from_static_msg("unexpected character"));
465            }
466
467            MediaProperty::from(scales)
468        } else {
469            if reader.match_rtsp_separator('=').is_ok() {
470                reader.read_other_media_property_value()?;
471            }
472
473            let r = reader.as_str();
474
475            let original_len = s.len();
476            let remaining_len = r.len();
477
478            let len = original_len - remaining_len;
479
480            let property = &s[..len];
481
482            MediaProperty::Other(Cow::Borrowed(property))
483        };
484
485        *self = reader;
486
487        Ok(property)
488    }
489
490    fn read_other_media_property_value(&mut self) -> Result<&'a str, Error> {
491        let s = self.as_str();
492
493        let mut reader = StringReader::new(s);
494
495        let c = reader
496            .current_char()
497            .ok_or_else(|| Error::from_static_msg("unexpected end of input"))?;
498
499        if c == '"' {
500            reader.read_rtsp_quoted_string()?;
501        } else if c.is_rtsp_unreserved() {
502            reader.read_while(|c| c.is_rtsp_unreserved());
503        } else {
504            return Err(Error::from_static_msg("unexpected character"));
505        }
506
507        let r = reader.as_str();
508
509        let original_len = s.len();
510        let remaining_len = r.len();
511
512        let len = original_len - remaining_len;
513
514        *self = reader;
515
516        Ok(&s[..len])
517    }
518
519    fn parse_scales(&mut self) -> Result<Scales, Error> {
520        let mut reader = StringReader::new(self.as_str());
521
522        let mut scales = Vec::new();
523
524        loop {
525            let a = reader.parse_f32()?;
526
527            if reader.match_rtsp_separator(':').is_ok() {
528                let b = reader.parse_f32()?;
529
530                scales.push(Scale::Range(a, b));
531            } else {
532                scales.push(Scale::Value(a));
533            }
534
535            if reader.match_rtsp_separator(',').is_err() {
536                break;
537            }
538        }
539
540        *self = reader;
541
542        Ok(Scales::new(scales))
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use std::str::FromStr;
549
550    use super::{ContentModifications, MediaPropertiesHeader, RandomAccess, Retention, Scale};
551
552    #[test]
553    fn test_parser() {
554        let header = MediaPropertiesHeader::from_str(" , ");
555
556        assert!(header.is_err());
557
558        let header = MediaPropertiesHeader::from_str("Random-Access , , Time-Progressing");
559
560        assert!(header.is_err());
561
562        let header = MediaPropertiesHeader::from_str("").unwrap();
563
564        assert!(header.random_access().is_none());
565        assert!(header.content_modifications().is_none());
566        assert!(header.retention().is_none());
567        assert!(header.scales().is_none());
568        assert!(header.other_properties().is_empty());
569
570        let header = MediaPropertiesHeader::from_str(
571            "Random-Access, Time-Progressing, Unlimited, Scales=\"1.5, 2:5\", Other",
572        )
573        .unwrap();
574
575        assert_eq!(
576            header.random_access().unwrap(),
577            RandomAccess::RandomAccess(None)
578        );
579        assert_eq!(
580            header.content_modifications().unwrap(),
581            ContentModifications::TimeProgressing
582        );
583        assert_eq!(header.retention().unwrap(), Retention::Unlimited);
584        assert_eq!(
585            header.scales().unwrap(),
586            &[Scale::Value(1.5f32), Scale::Range(2f32, 5f32)]
587        );
588        assert_eq!(header.other_properties(), &["Other"]);
589
590        let header = MediaPropertiesHeader::from_str(
591            "Random-Access, Time-Progressing, Time-Duration=30.000",
592        );
593
594        assert!(header.is_ok());
595    }
596}