1use 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#[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 #[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 #[inline]
45 pub fn random_access(&self) -> Option<RandomAccess> {
46 self.random_access
47 }
48
49 #[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 #[inline]
58 pub fn content_modifications(&self) -> Option<ContentModifications> {
59 self.content_modifications
60 }
61
62 #[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 #[inline]
74 pub fn retention(&self) -> Option<Retention> {
75 self.retention
76 }
77
78 #[inline]
80 pub const fn with_retention(mut self, retention: Option<Retention>) -> Self {
81 self.retention = retention;
82 self
83 }
84
85 #[inline]
87 pub fn scales(&self) -> Option<&[Scale]> {
88 self.scales.as_ref().map(|s| &s.inner[..])
89 }
90
91 #[inline]
93 pub fn with_scales(mut self, scales: Option<Scales>) -> Self {
94 self.scales = scales;
95 self
96 }
97
98 #[inline]
100 pub fn other_properties(&self) -> &[String] {
101 &self.other
102 }
103
104 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#[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#[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#[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#[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#[derive(Clone)]
264pub struct Scales {
265 inner: Vec<Scale>,
266}
267
268impl Scales {
269 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#[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
378trait MediaPropertyParser<'a> {
380 fn parse_media_property(&mut self) -> Result<MediaProperty<'a>, Error>;
382
383 fn read_other_media_property_value(&mut self) -> Result<&'a str, Error>;
385
386 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}