Skip to main content

m3u8_rs/
playlist.rs

1//! Contains all the structs used for parsing.
2//!
3//! The main type here is the `Playlist` enum.
4//! Which is either a `MasterPlaylist` or a `MediaPlaylist`.
5
6use crate::QuotedOrUnquoted;
7use std::collections::HashMap;
8use std::convert::{TryFrom, TryInto};
9use std::fmt;
10use std::fmt::Display;
11use std::io::Write;
12use std::str::FromStr;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::usize::MAX;
15use std::{f32, usize};
16
17/// The output precision for floats, such as #EXTINF (default is unset)
18pub static WRITE_OPT_FLOAT_PRECISION: AtomicUsize = AtomicUsize::new(MAX);
19
20macro_rules! write_some_attribute_quoted {
21    ($w:expr, $tag:expr, $o:expr) => {
22        if let &Some(ref v) = $o {
23            write!($w, "{}=\"{}\"", $tag, v)
24        } else {
25            Ok(())
26        }
27    };
28}
29
30macro_rules! write_some_attribute {
31    ($w:expr, $tag:expr, $o:expr) => {
32        if let &Some(ref v) = $o {
33            write!($w, "{}={}", $tag, v)
34        } else {
35            Ok(())
36        }
37    };
38}
39
40macro_rules! write_some_other_attributes {
41    ($w:expr, $attr:expr) => {
42        if let &Some(ref attributes) = $attr {
43            let mut status = std::io::Result::Ok(());
44            for (name, val) in attributes {
45                let res = write!($w, ",{}={}", name, val);
46                if res.is_err() {
47                    status = res;
48                    break;
49                }
50            }
51            status
52        } else {
53            Ok(())
54        }
55    };
56}
57
58macro_rules! is_yes {
59    ($attrs:expr, $attr:expr) => {
60        match $attrs.remove($attr) {
61            Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "YES" => true,
62            Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "NO" => false,
63            Some(ref s) => {
64                return Err(format!(
65                    "Can't create bool from {} for {} attribute",
66                    s, $attr
67                ))
68            }
69            None => false,
70        }
71    };
72}
73
74macro_rules! quoted_string {
75    ($attrs:expr, $attr:expr) => {
76        match $attrs.remove($attr) {
77            Some(QuotedOrUnquoted::Quoted(s)) => Some(s),
78            Some(QuotedOrUnquoted::Unquoted(_)) => {
79                return Err(format!(
80                    "Can't create {} attribute from unquoted string",
81                    $attr
82                ))
83            }
84            None => None,
85        }
86    };
87}
88
89macro_rules! unquoted_string {
90    ($attrs:expr, $attr:expr) => {
91        match $attrs.remove($attr) {
92            Some(QuotedOrUnquoted::Unquoted(s)) => Some(s),
93            Some(QuotedOrUnquoted::Quoted(_)) => {
94                return Err(format!(
95                    "Can't create {} attribute from quoted string",
96                    $attr
97                ))
98            }
99            None => None,
100        }
101    };
102}
103
104macro_rules! unquoted_string_parse {
105    ($attrs:expr, $attr:expr, $parse:expr) => {
106        match $attrs.remove($attr) {
107            Some(QuotedOrUnquoted::Unquoted(s)) => Some(
108                ($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
109            ),
110            Some(QuotedOrUnquoted::Quoted(_)) => {
111                return Err(format!(
112                    "Can't create {} attribute from quoted string",
113                    $attr
114                ))
115            }
116            None => None,
117        }
118    };
119    ($attrs:expr, $attr:expr) => {
120        unquoted_string_parse!($attrs, $attr, |s: &str| s.parse())
121    };
122}
123
124macro_rules! quoted_string_parse {
125    ($attrs:expr, $attr:expr, $parse:expr) => {
126        match $attrs.remove($attr) {
127            Some(QuotedOrUnquoted::Quoted(s)) => Some(
128                ($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
129            ),
130            Some(QuotedOrUnquoted::Unquoted(_)) => {
131                return Err(format!(
132                    "Can't create {} attribute from unquoted string",
133                    $attr
134                ))
135            }
136            None => None,
137        }
138    };
139    ($attrs:expr, $attr:expr) => {
140        quoted_string_parse!($attrs, $attr, |s: &str| s.parse())
141    };
142}
143
144/// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1),
145/// can either be a `MasterPlaylist` or a `MediaPlaylist`.
146///
147/// A Playlist is a Media Playlist if all URI lines in the Playlist
148/// identify Media Segments.  A Playlist is a Master Playlist if all URI
149/// lines in the Playlist identify Media Playlists.  A Playlist MUST be
150/// either a Media Playlist or a Master Playlist; all other Playlists are invalid.
151#[derive(Debug, PartialEq, Clone)]
152pub enum Playlist {
153    MasterPlaylist(MasterPlaylist),
154    MediaPlaylist(MediaPlaylist),
155}
156
157impl Playlist {
158    pub fn write_to<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {
159        match *self {
160            Playlist::MasterPlaylist(ref pl) => pl.write_to(writer),
161            Playlist::MediaPlaylist(ref pl) => pl.write_to(writer),
162        }
163    }
164}
165
166// -----------------------------------------------------------------------------------------------
167// Master Playlist
168// -----------------------------------------------------------------------------------------------
169
170/// A [Master Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4)
171/// provides a set of Variant Streams, each of which
172/// describes a different version of the same content.
173#[derive(Debug, Default, PartialEq, Clone)]
174pub struct MasterPlaylist {
175    pub version: Option<usize>,
176    pub variants: Vec<VariantStream>,
177    pub session_data: Vec<SessionData>,
178    pub session_key: Vec<SessionKey>,
179    pub start: Option<Start>,
180    pub independent_segments: bool,
181    pub alternatives: Vec<AlternativeMedia>, // EXT-X-MEDIA tags
182    pub unknown_tags: Vec<ExtTag>,
183}
184
185impl MasterPlaylist {
186    pub fn get_newest_variant(&mut self) -> Option<&mut VariantStream> {
187        self.variants.iter_mut().rev().find(|v| !v.is_i_frame)
188    }
189
190    pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
191        writeln!(w, "#EXTM3U")?;
192
193        if let Some(ref v) = self.version {
194            writeln!(w, "#EXT-X-VERSION:{}", v)?;
195        }
196        if self.independent_segments {
197            writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
198        }
199
200        for alternative in &self.alternatives {
201            alternative.write_to(w)?;
202        }
203
204        for variant in &self.variants {
205            variant.write_to(w)?;
206        }
207        for session_data in &self.session_data {
208            session_data.write_to(w)?;
209        }
210        for session_key in &self.session_key {
211            session_key.write_to(w)?;
212        }
213        if let Some(ref start) = self.start {
214            start.write_to(w)?;
215        }
216        for unknown_tag in &self.unknown_tags {
217            writeln!(w, "{}", unknown_tag)?;
218        }
219
220        Ok(())
221    }
222}
223
224/// [`#EXT-X-STREAM-INF:<attribute-list> <URI>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.2)
225/// [`#EXT-X-I-FRAME-STREAM-INF:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.3)
226///
227/// A Variant Stream includes a Media Playlist that specifies media
228/// encoded at a particular bit rate, in a particular format, and at a
229/// particular resolution for media containing video.
230///
231/// A Variant Stream can also specify a set of Renditions.  Renditions
232/// are alternate versions of the content, such as audio produced in
233/// different languages or video recorded from different camera angles.
234///
235/// Clients should switch between different Variant Streams to adapt to
236/// network conditions.  Clients should choose Renditions based on user
237/// preferences.
238#[derive(Debug, Default, PartialEq, Clone)]
239pub struct VariantStream {
240    pub is_i_frame: bool,
241    pub uri: String,
242
243    // <attribute-list>
244    pub bandwidth: u64,
245    pub average_bandwidth: Option<u64>,
246    pub codecs: Option<String>,
247    pub resolution: Option<Resolution>,
248    pub frame_rate: Option<f64>,
249    pub hdcp_level: Option<HDCPLevel>,
250    pub audio: Option<String>,
251    pub video: Option<String>,
252    pub subtitles: Option<String>,
253    pub closed_captions: Option<ClosedCaptionGroupId>,
254    // PROGRAM-ID tag was removed in protocol version 6
255    pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
256}
257
258impl VariantStream {
259    pub(crate) fn from_hashmap(
260        mut attrs: HashMap<String, QuotedOrUnquoted>,
261        is_i_frame: bool,
262    ) -> Result<VariantStream, String> {
263        let uri = quoted_string!(attrs, "URI").unwrap_or_default();
264        // TODO: keep in attrs if parsing optional attributes fails
265        let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s
266            .parse::<u64>()
267            .map_err(|err| format!("Failed to parse BANDWIDTH attribute: {}", err)))
268        .ok_or_else(|| String::from("EXT-X-STREAM-INF without mandatory BANDWIDTH attribute"))?;
269        let average_bandwidth = unquoted_string_parse!(attrs, "AVERAGE-BANDWIDTH", |s: &str| s
270            .parse::<u64>()
271            .map_err(|err| format!("Failed to parse AVERAGE-BANDWIDTH: {}", err)));
272        let codecs = quoted_string!(attrs, "CODECS");
273        let resolution = unquoted_string_parse!(attrs, "RESOLUTION");
274        let frame_rate = unquoted_string_parse!(attrs, "FRAME-RATE", |s: &str| s
275            .parse::<f64>()
276            .map_err(|err| format!("Failed to parse FRAME-RATE attribute: {}", err)));
277        let hdcp_level = unquoted_string_parse!(attrs, "HDCP-LEVEL");
278        let audio = quoted_string!(attrs, "AUDIO");
279        let video = quoted_string!(attrs, "VIDEO");
280        let subtitles = quoted_string!(attrs, "SUBTITLES");
281        let closed_captions = attrs
282            .remove("CLOSED-CAPTIONS")
283            .map(|c| c.try_into())
284            .transpose()?;
285        let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
286
287        Ok(VariantStream {
288            is_i_frame,
289            uri,
290            bandwidth,
291            average_bandwidth,
292            codecs,
293            resolution,
294            frame_rate,
295            hdcp_level,
296            audio,
297            video,
298            subtitles,
299            closed_captions,
300            other_attributes,
301        })
302    }
303
304    pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
305        if self.is_i_frame {
306            write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?;
307            self.write_stream_inf_common_attributes(w)?;
308            writeln!(w, ",URI=\"{}\"", self.uri)
309        } else {
310            write!(w, "#EXT-X-STREAM-INF:")?;
311            self.write_stream_inf_common_attributes(w)?;
312            write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?;
313            write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?;
314            if let Some(ref closed_captions) = self.closed_captions {
315                match closed_captions {
316                    ClosedCaptionGroupId::None => write!(w, ",CLOSED-CAPTIONS=NONE")?,
317                    ClosedCaptionGroupId::GroupId(s) => write!(w, ",CLOSED-CAPTIONS=\"{}\"", s)?,
318                    ClosedCaptionGroupId::Other(s) => write!(w, ",CLOSED-CAPTIONS={}", s)?,
319                }
320            }
321            writeln!(w)?;
322            writeln!(w, "{}", self.uri)
323        }
324    }
325
326    fn write_stream_inf_common_attributes<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
327        write!(w, "BANDWIDTH={}", &self.bandwidth)?;
328        write_some_attribute!(w, ",AVERAGE-BANDWIDTH", &self.average_bandwidth)?;
329        write_some_attribute_quoted!(w, ",CODECS", &self.codecs)?;
330        write_some_attribute!(w, ",RESOLUTION", &self.resolution)?;
331        write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?;
332        write_some_attribute!(w, ",HDCP-LEVEL", &self.hdcp_level)?;
333        write_some_attribute_quoted!(w, ",VIDEO", &self.video)?;
334        write_some_other_attributes!(w, &self.other_attributes)?;
335        Ok(())
336    }
337}
338
339#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
340pub struct Resolution {
341    pub width: u64,
342    pub height: u64,
343}
344
345impl Display for Resolution {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        write!(f, "{}x{}", self.width, self.height)
348    }
349}
350
351impl FromStr for Resolution {
352    type Err = String;
353
354    fn from_str(s: &str) -> Result<Resolution, String> {
355        match s.split_once('x') {
356            Some((width, height)) => {
357                let width = width
358                    .parse::<u64>()
359                    .map_err(|err| format!("Can't parse RESOLUTION attribute width: {}", err))?;
360                let height = height
361                    .parse::<u64>()
362                    .map_err(|err| format!("Can't parse RESOLUTION attribute height: {}", err))?;
363                Ok(Resolution { width, height })
364            }
365            None => Err(String::from("Invalid RESOLUTION attribute")),
366        }
367    }
368}
369
370#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
371pub enum HDCPLevel {
372    Type0,
373    Type1,
374    None,
375    Other(String),
376}
377
378impl FromStr for HDCPLevel {
379    type Err = String;
380
381    fn from_str(s: &str) -> Result<HDCPLevel, String> {
382        match s {
383            "TYPE-0" => Ok(HDCPLevel::Type0),
384            "TYPE-1" => Ok(HDCPLevel::Type1),
385            "NONE" => Ok(HDCPLevel::None),
386            _ => Ok(HDCPLevel::Other(String::from(s))),
387        }
388    }
389}
390
391impl Display for HDCPLevel {
392    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
393        write!(
394            f,
395            "{}",
396            match self {
397                HDCPLevel::Type0 => "TYPE-0",
398                HDCPLevel::Type1 => "TYPE-1",
399                HDCPLevel::None => "NONE",
400                HDCPLevel::Other(s) => s,
401            }
402        )
403    }
404}
405
406/// TODO docs
407#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
408pub enum ClosedCaptionGroupId {
409    None,
410    GroupId(String),
411    Other(String),
412}
413
414impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId {
415    type Error = String;
416
417    fn try_from(s: QuotedOrUnquoted) -> Result<ClosedCaptionGroupId, String> {
418        match s {
419            QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(ClosedCaptionGroupId::None),
420            QuotedOrUnquoted::Unquoted(s) => Ok(ClosedCaptionGroupId::Other(s)),
421            QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)),
422        }
423    }
424}
425
426/// [`#EXT-X-MEDIA:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1)
427///
428/// The EXT-X-MEDIA tag is used to relate Media Playlists that contain
429/// alternative Renditions (Section 4.3.4.2.1) of the same content.  For
430/// example, three EXT-X-MEDIA tags can be used to identify audio-only
431/// Media Playlists that contain English, French and Spanish Renditions
432/// of the same presentation.  Or two EXT-X-MEDIA tags can be used to
433/// identify video-only Media Playlists that show two different camera angles.
434#[derive(Debug, Default, PartialEq, Eq, Clone)]
435pub struct AlternativeMedia {
436    // <attribute-list>
437    pub media_type: AlternativeMediaType,
438    pub uri: Option<String>,
439    pub group_id: String,
440    pub language: Option<String>,
441    pub assoc_language: Option<String>,
442    pub name: String, // All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes.
443    pub default: bool, // Its absence indicates an implicit value of NO
444    pub autoselect: bool, // Its absence indicates an implicit value of NO
445    pub forced: bool, // Its absence indicates an implicit value of NO
446    pub instream_id: Option<InstreamId>,
447    pub characteristics: Option<String>,
448    pub channels: Option<String>,
449    pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
450}
451
452impl AlternativeMedia {
453    pub(crate) fn from_hashmap(
454        mut attrs: HashMap<String, QuotedOrUnquoted>,
455    ) -> Result<AlternativeMedia, String> {
456        let media_type = unquoted_string_parse!(attrs, "TYPE")
457            .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory TYPE attribute"))?;
458        let uri = quoted_string!(attrs, "URI");
459
460        if media_type == AlternativeMediaType::ClosedCaptions && uri.is_some() {
461            return Err(String::from(
462                "URI attribute must not be included in CLOSED-CAPTIONS Alternative Medias",
463            ));
464        }
465
466        let group_id = quoted_string!(attrs, "GROUP-ID")
467            .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory GROUP-ID attribute"))?;
468        let language = quoted_string!(attrs, "LANGUAGE");
469        let assoc_language = quoted_string!(attrs, "ASSOC-LANGUAGE");
470        let name = quoted_string!(attrs, "NAME")
471            .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory NAME attribute"))?;
472        let default = is_yes!(attrs, "DEFAULT");
473        let autoselect = is_yes!(attrs, "AUTOSELECT");
474
475        if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") {
476            return Err(String::from(
477                "FORCED attribute must not be included in non-SUBTITLE Alternative Medias",
478            ));
479        }
480        let forced = is_yes!(attrs, "FORCED");
481
482        if media_type != AlternativeMediaType::ClosedCaptions && attrs.contains_key("INSTREAM-ID") {
483            return Err(String::from("INSTREAM-ID attribute must not be included in non-CLOSED-CAPTIONS Alternative Medias"));
484        } else if media_type == AlternativeMediaType::ClosedCaptions
485            && !attrs.contains_key("INSTREAM-ID")
486        {
487            return Err(String::from(
488                "INSTREAM-ID attribute must be included in CLOSED-CAPTIONS Alternative Medias",
489            ));
490        }
491        let instream_id = quoted_string_parse!(attrs, "INSTREAM-ID");
492        let characteristics = quoted_string!(attrs, "CHARACTERISTICS");
493        let channels = quoted_string!(attrs, "CHANNELS");
494        let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
495
496        Ok(AlternativeMedia {
497            media_type,
498            uri,
499            group_id,
500            language,
501            assoc_language,
502            name,
503            default,
504            autoselect,
505            forced,
506            instream_id,
507            characteristics,
508            channels,
509            other_attributes,
510        })
511    }
512
513    pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
514        write!(w, "#EXT-X-MEDIA:")?;
515        write!(w, "TYPE={}", self.media_type)?;
516        if self.media_type != AlternativeMediaType::ClosedCaptions {
517            write_some_attribute_quoted!(w, ",URI", &self.uri)?;
518        }
519        write!(w, ",GROUP-ID=\"{}\"", self.group_id)?;
520        write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
521        write_some_attribute_quoted!(w, ",ASSOC-LANGUAGE", &self.assoc_language)?;
522        write!(w, ",NAME=\"{}\"", self.name)?;
523        if self.default {
524            write!(w, ",DEFAULT=YES")?;
525        }
526        if self.autoselect {
527            write!(w, ",AUTOSELECT=YES")?;
528        }
529        if self.forced && self.media_type == AlternativeMediaType::Subtitles {
530            write!(w, ",FORCED=YES")?;
531        }
532        if self.media_type == AlternativeMediaType::ClosedCaptions {
533            // FIXME: Mandatory for closed captions
534            write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?;
535        }
536        write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?;
537        write_some_attribute_quoted!(w, ",CHANNELS", &self.channels)?;
538        write_some_other_attributes!(w, &self.other_attributes)?;
539        writeln!(w)
540    }
541}
542
543#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
544pub enum AlternativeMediaType {
545    Audio,
546    Video,
547    Subtitles,
548    ClosedCaptions,
549    Other(String),
550}
551
552impl FromStr for AlternativeMediaType {
553    type Err = String;
554
555    fn from_str(s: &str) -> Result<AlternativeMediaType, String> {
556        match s {
557            "AUDIO" => Ok(AlternativeMediaType::Audio),
558            "VIDEO" => Ok(AlternativeMediaType::Video),
559            "SUBTITLES" => Ok(AlternativeMediaType::Subtitles),
560            "CLOSED-CAPTIONS" => Ok(AlternativeMediaType::ClosedCaptions),
561            _ => Ok(AlternativeMediaType::Other(String::from(s))),
562        }
563    }
564}
565
566impl Default for AlternativeMediaType {
567    fn default() -> AlternativeMediaType {
568        AlternativeMediaType::Video
569    }
570}
571
572impl Display for AlternativeMediaType {
573    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
574        write!(
575            f,
576            "{}",
577            match self {
578                AlternativeMediaType::Audio => "AUDIO",
579                AlternativeMediaType::Video => "VIDEO",
580                AlternativeMediaType::Subtitles => "SUBTITLES",
581                AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS",
582                AlternativeMediaType::Other(s) => s.as_str(),
583            }
584        )
585    }
586}
587
588#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
589pub enum InstreamId {
590    CC(u8),
591    Service(u8),
592    Other(String),
593}
594
595impl FromStr for InstreamId {
596    type Err = String;
597
598    fn from_str(s: &str) -> Result<InstreamId, String> {
599        if let Some(cc) = s.strip_prefix("CC") {
600            let cc = cc
601                .parse::<u8>()
602                .map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
603            Ok(InstreamId::CC(cc))
604        } else if let Some(service) = s.strip_prefix("SERVICE") {
605            let service = service
606                .parse::<u8>()
607                .map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
608            Ok(InstreamId::Service(service))
609        } else {
610            Ok(InstreamId::Other(String::from(s)))
611        }
612    }
613}
614
615impl Display for InstreamId {
616    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
617        match self {
618            InstreamId::CC(cc) => write!(f, "CC{}", cc),
619            InstreamId::Service(service) => write!(f, "SERVICE{}", service),
620            InstreamId::Other(s) => write!(f, "{}", s),
621        }
622    }
623}
624
625/// [`#EXT-X-SESSION-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5)
626/// The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists
627/// to be specified in a Master Playlist.  This allows the client to
628/// preload these keys without having to read the Media Playlist(s) first.
629#[derive(Debug, Default, PartialEq, Eq, Clone)]
630pub struct SessionKey(pub Key);
631
632impl SessionKey {
633    pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
634        write!(w, "#EXT-X-SESSION-KEY:")?;
635        self.0.write_attributes_to(w)?;
636        writeln!(w)
637    }
638}
639
640#[derive(Debug, PartialEq, Eq, Clone)]
641pub enum SessionDataField {
642    Value(String),
643    Uri(String),
644}
645
646/// [`#EXT-X-SESSION-DATA:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4)
647/// The EXT-X-SESSION-DATA tag allows arbitrary session data to be carried
648/// in a Master Playlist.
649#[derive(Debug, PartialEq, Eq, Clone)]
650pub struct SessionData {
651    pub data_id: String,
652    pub field: SessionDataField,
653    pub language: Option<String>,
654    pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
655}
656
657impl SessionData {
658    pub(crate) fn from_hashmap(
659        mut attrs: HashMap<String, QuotedOrUnquoted>,
660    ) -> Result<SessionData, String> {
661        let data_id = quoted_string!(attrs, "DATA-ID")
662            .ok_or_else(|| String::from("EXT-X-SESSION-DATA field without DATA-ID attribute"))?;
663
664        let value = quoted_string!(attrs, "VALUE");
665        let uri = quoted_string!(attrs, "URI");
666
667        // SessionData must contain either a VALUE or a URI,
668        // but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4
669        let field = match (value, uri) {
670            (Some(value), None) => SessionDataField::Value(value),
671            (None, Some(uri)) => SessionDataField::Uri(uri),
672            (Some(_), Some(_)) => {
673                return Err(format![
674                    "EXT-X-SESSION-DATA tag {} contains both a value and an URI",
675                    data_id
676                ])
677            }
678            (None, None) => {
679                return Err(format![
680                    "EXT-X-SESSION-DATA tag {} must contain either a value or an URI",
681                    data_id
682                ])
683            }
684        };
685
686        let language = quoted_string!(attrs, "LANGUAGE");
687        let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
688
689        Ok(SessionData {
690            data_id,
691            field,
692            language,
693            other_attributes,
694        })
695    }
696
697    pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
698        write!(w, "#EXT-X-SESSION-DATA:")?;
699        write!(w, "DATA-ID=\"{}\"", self.data_id)?;
700        match &self.field {
701            SessionDataField::Value(value) => write!(w, ",VALUE=\"{}\"", value)?,
702            SessionDataField::Uri(uri) => write!(w, ",URI=\"{}\"", uri)?,
703        };
704        write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
705        write_some_other_attributes!(w, &self.other_attributes)?;
706        writeln!(w)
707    }
708}
709
710// -----------------------------------------------------------------------------------------------
711// Media Playlist
712// -----------------------------------------------------------------------------------------------
713
714/// A [Media Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3)
715/// contains a list of Media Segments, which when played
716/// sequentially will play the multimedia presentation.
717#[derive(Debug, Default, PartialEq, Clone)]
718pub struct MediaPlaylist {
719    pub version: Option<usize>,
720    /// `#EXT-X-TARGETDURATION:<s>`
721    pub target_duration: u64,
722    /// `#EXT-X-MEDIA-SEQUENCE:<number>`
723    pub media_sequence: u64,
724    pub segments: Vec<MediaSegment>,
725    /// `#EXT-X-DISCONTINUITY-SEQUENCE:<number>`
726    pub discontinuity_sequence: u64,
727    /// `#EXT-X-ENDLIST`
728    pub end_list: bool,
729    /// `#EXT-X-PLAYLIST-TYPE`
730    pub playlist_type: Option<MediaPlaylistType>,
731    /// `#EXT-X-I-FRAMES-ONLY`
732    pub i_frames_only: bool,
733    /// `#EXT-X-START`
734    pub start: Option<Start>,
735    /// `#EXT-X-INDEPENDENT-SEGMENTS`
736    pub independent_segments: bool,
737    /// Unknown tags before the first media segment
738    pub unknown_tags: Vec<ExtTag>,
739}
740
741impl MediaPlaylist {
742    pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
743        writeln!(w, "#EXTM3U")?;
744
745        if let Some(ref v) = self.version {
746            writeln!(w, "#EXT-X-VERSION:{}", v)?;
747        }
748        if self.independent_segments {
749            writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
750        }
751        writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?;
752
753        if self.media_sequence != 0 {
754            writeln!(w, "#EXT-X-MEDIA-SEQUENCE:{}", self.media_sequence)?;
755        }
756        if self.discontinuity_sequence != 0 {
757            writeln!(
758                w,
759                "#EXT-X-DISCONTINUITY-SEQUENCE:{}",
760                self.discontinuity_sequence
761            )?;
762        }
763        if let Some(ref v) = self.playlist_type {
764            writeln!(w, "#EXT-X-PLAYLIST-TYPE:{}", v)?;
765        }
766        if self.i_frames_only {
767            writeln!(w, "#EXT-X-I-FRAMES-ONLY")?;
768        }
769        if let Some(ref start) = self.start {
770            start.write_to(w)?;
771        }
772        for segment in &self.segments {
773            segment.write_to(w)?;
774        }
775        if self.end_list {
776            writeln!(w, "#EXT-X-ENDLIST")?;
777        }
778
779        Ok(())
780    }
781}
782
783/// [`#EXT-X-PLAYLIST-TYPE:<EVENT|VOD>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5)
784#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
785pub enum MediaPlaylistType {
786    Event,
787    Vod,
788    Other(String),
789}
790
791impl FromStr for MediaPlaylistType {
792    type Err = String;
793
794    fn from_str(s: &str) -> Result<MediaPlaylistType, String> {
795        match s {
796            "EVENT" => Ok(MediaPlaylistType::Event),
797            "VOD" => Ok(MediaPlaylistType::Vod),
798            _ => Ok(MediaPlaylistType::Other(String::from(s))),
799        }
800    }
801}
802
803impl Display for MediaPlaylistType {
804    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
805        write!(
806            f,
807            "{}",
808            match self {
809                MediaPlaylistType::Event => "EVENT",
810                MediaPlaylistType::Vod => "VOD",
811                MediaPlaylistType::Other(s) => s,
812            }
813        )
814    }
815}
816
817impl Default for MediaPlaylistType {
818    fn default() -> MediaPlaylistType {
819        MediaPlaylistType::Event
820    }
821}
822
823// -----------------------------------------------------------------------------------------------
824// Media Segment
825// -----------------------------------------------------------------------------------------------
826
827/// A [Media Segment](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3)
828/// is specified by a URI and optionally a byte range.
829#[derive(Debug, Default, PartialEq, Clone)]
830pub struct MediaSegment {
831    pub uri: String,
832    /// `#EXTINF:<duration>,[<title>]`
833    pub duration: f32,
834    /// `#EXTINF:<duration>,[<title>]`
835    pub title: Option<String>,
836    /// `#EXT-X-BYTERANGE:<n>[@<o>]`
837    pub byte_range: Option<ByteRange>,
838    /// `#EXT-X-DISCONTINUITY`
839    pub discontinuity: bool,
840    /// `#EXT-X-KEY:<attribute-list>`
841    pub key: Option<Key>,
842    /// `#EXT-X-MAP:<attribute-list>`
843    pub map: Option<Map>,
844    /// `#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>`
845    pub program_date_time: Option<chrono::DateTime<chrono::FixedOffset>>,
846    /// `#EXT-X-DATERANGE:<attribute-list>`
847    pub daterange: Option<DateRange>,
848    /// `#EXT-`
849    pub unknown_tags: Vec<ExtTag>,
850}
851
852impl MediaSegment {
853    pub fn empty() -> MediaSegment {
854        Default::default()
855    }
856
857    pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
858        if let Some(ref map) = self.map {
859            write!(w, "#EXT-X-MAP:")?;
860            map.write_attributes_to(w)?;
861            writeln!(w)?;
862        }
863        if let Some(ref byte_range) = self.byte_range {
864            write!(w, "#EXT-X-BYTERANGE:")?;
865            byte_range.write_value_to(w)?;
866            writeln!(w)?;
867        }
868        if self.discontinuity {
869            writeln!(w, "#EXT-X-DISCONTINUITY")?;
870        }
871        if let Some(ref key) = self.key {
872            write!(w, "#EXT-X-KEY:")?;
873            key.write_attributes_to(w)?;
874            writeln!(w)?;
875        }
876        if let Some(ref v) = self.program_date_time {
877            writeln!(
878                w,
879                "#EXT-X-PROGRAM-DATE-TIME:{}",
880                v.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
881            )?;
882        }
883        if let Some(ref v) = self.daterange {
884            write!(w, "#EXT-X-DATERANGE:")?;
885            v.write_attributes_to(w)?;
886            writeln!(w)?;
887        }
888        for unknown_tag in &self.unknown_tags {
889            writeln!(w, "{}", unknown_tag)?;
890        }
891
892        match WRITE_OPT_FLOAT_PRECISION.load(Ordering::Relaxed) {
893            MAX => {
894                write!(w, "#EXTINF:{},", self.duration)?;
895            }
896            n => {
897                write!(w, "#EXTINF:{:.*},", n, self.duration)?;
898            }
899        };
900
901        if let Some(ref v) = self.title {
902            writeln!(w, "{}", v)?;
903        } else {
904            writeln!(w)?;
905        }
906
907        writeln!(w, "{}", self.uri)
908    }
909}
910
911#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
912pub enum KeyMethod {
913    None,
914    AES128,
915    SampleAES,
916    Other(String),
917}
918
919impl Default for KeyMethod {
920    fn default() -> Self {
921        KeyMethod::None
922    }
923}
924
925impl FromStr for KeyMethod {
926    type Err = String;
927
928    fn from_str(s: &str) -> Result<KeyMethod, String> {
929        match s {
930            "NONE" => Ok(KeyMethod::None),
931            "AES-128" => Ok(KeyMethod::AES128),
932            "SAMPLE-AES" => Ok(KeyMethod::SampleAES),
933            _ => Ok(KeyMethod::Other(String::from(s))),
934        }
935    }
936}
937
938impl Display for KeyMethod {
939    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
940        write!(
941            f,
942            "{}",
943            match self {
944                KeyMethod::None => "NONE",
945                KeyMethod::AES128 => "AES-128",
946                KeyMethod::SampleAES => "SAMPLE-AES",
947                KeyMethod::Other(s) => s,
948            }
949        )
950    }
951}
952
953/// [`#EXT-X-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4)
954///
955/// Media Segments MAY be encrypted.  The EXT-X-KEY tag specifies how to
956/// decrypt them.  It applies to every Media Segment that appears between
957/// it and the next EXT-X-KEY tag in the Playlist file with the same
958/// KEYFORMAT attribute (or the end of the Playlist file).  Two or more
959/// EXT-X-KEY tags with different KEYFORMAT attributes MAY apply to the
960/// same Media Segment if they ultimately produce the same decryption key.
961#[derive(Debug, Default, PartialEq, Eq, Clone)]
962pub struct Key {
963    pub method: KeyMethod,
964    pub uri: Option<String>,
965    pub iv: Option<String>,
966    pub keyformat: Option<String>,
967    pub keyformatversions: Option<String>,
968}
969
970impl Key {
971    pub(crate) fn from_hashmap(
972        mut attrs: HashMap<String, QuotedOrUnquoted>,
973    ) -> Result<Key, String> {
974        let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD")
975            .ok_or_else(|| String::from("EXT-X-KEY without mandatory METHOD attribute"))?;
976
977        let uri = quoted_string!(attrs, "URI");
978        let iv = unquoted_string!(attrs, "IV");
979        if method == KeyMethod::None && iv.is_none() {
980            return Err("IV is required unless METHOD is NONE".parse().unwrap());
981        }
982        let keyformat = quoted_string!(attrs, "KEYFORMAT");
983        let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS");
984
985        Ok(Key {
986            method,
987            uri,
988            iv,
989            keyformat,
990            keyformatversions,
991        })
992    }
993
994    pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
995        write!(w, "METHOD={}", self.method)?;
996        write_some_attribute_quoted!(w, ",URI", &self.uri)?;
997        write_some_attribute!(w, ",IV", &self.iv)?;
998        write_some_attribute_quoted!(w, ",KEYFORMAT", &self.keyformat)?;
999        write_some_attribute_quoted!(w, ",KEYFORMATVERSIONS", &self.keyformatversions)
1000    }
1001}
1002
1003/// [`#EXT-X-MAP:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.5)
1004///
1005/// The EXT-X-MAP tag specifies how to obtain the Media Initialization Section
1006/// [(Section 3)](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3)
1007/// required to parse the applicable Media Segments.
1008/// It applies to every Media Segment that appears after it in the
1009/// Playlist until the next EXT-X-MAP tag or until the end of the
1010/// playlist.
1011#[derive(Debug, Default, PartialEq, Eq, Clone)]
1012pub struct Map {
1013    pub uri: String,
1014    pub byte_range: Option<ByteRange>,
1015    pub other_attributes: HashMap<String, QuotedOrUnquoted>,
1016}
1017
1018impl Map {
1019    pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1020        write!(w, "URI=\"{}\"", self.uri)?;
1021        if let Some(ref byte_range) = self.byte_range {
1022            write!(w, ",BYTERANGE=\"")?;
1023            byte_range.write_value_to(w)?;
1024            write!(w, "\"")?;
1025        }
1026        Ok(())
1027    }
1028}
1029
1030/// [`#EXT-X-BYTERANGE:<n>[@<o>]`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.2)
1031///
1032/// The EXT-X-BYTERANGE tag indicates that a Media Segment is a sub-range
1033/// of the resource identified by its URI.  It applies only to the next
1034/// URI line that follows it in the Playlist.
1035#[derive(Debug, Default, PartialEq, Eq, Clone)]
1036pub struct ByteRange {
1037    pub length: u64,
1038    pub offset: Option<u64>,
1039}
1040
1041impl ByteRange {
1042    pub fn write_value_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1043        write!(w, "{}", self.length)?;
1044        if let Some(offset) = self.offset {
1045            write!(w, "@{}", offset)?;
1046        }
1047        Ok(())
1048    }
1049}
1050
1051/// [`#EXT-X-DATERANGE:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7)
1052///
1053/// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time
1054/// defined by a starting and ending date) with a set of attribute /
1055/// value pairs.
1056#[derive(Debug, PartialEq, Clone)]
1057pub struct DateRange {
1058    pub id: String,
1059    pub class: Option<String>,
1060    pub start_date: chrono::DateTime<chrono::FixedOffset>,
1061    pub end_date: Option<chrono::DateTime<chrono::FixedOffset>>,
1062    pub duration: Option<f64>,
1063    pub planned_duration: Option<f64>,
1064    pub x_prefixed: Option<HashMap<String, QuotedOrUnquoted>>, //  X-<client-attribute>
1065    pub end_on_next: bool,
1066    pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
1067}
1068
1069impl DateRange {
1070    pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Result<DateRange, String> {
1071        let id = quoted_string!(attrs, "ID")
1072            .ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?;
1073        let class = quoted_string!(attrs, "CLASS");
1074        let start_date =
1075            quoted_string_parse!(attrs, "START-DATE", chrono::DateTime::parse_from_rfc3339)
1076                .ok_or_else(|| {
1077                    String::from("EXT-X-DATERANGE without mandatory START-DATE attribute")
1078                })?;
1079        let end_date =
1080            quoted_string_parse!(attrs, "END-DATE", chrono::DateTime::parse_from_rfc3339);
1081        let duration = unquoted_string_parse!(attrs, "DURATION", |s: &str| s
1082            .parse::<f64>()
1083            .map_err(|err| format!("Failed to parse DURATION attribute: {}", err)));
1084        let planned_duration = unquoted_string_parse!(attrs, "PLANNED-DURATION", |s: &str| s
1085            .parse::<f64>()
1086            .map_err(|err| format!("Failed to parse PLANNED-DURATION attribute: {}", err)));
1087        let end_on_next = is_yes!(attrs, "END-ON-NEXT");
1088        let mut x_prefixed = HashMap::new();
1089        let mut other_attributes = HashMap::new();
1090        for (k, v) in attrs.into_iter() {
1091            if k.starts_with("X-") {
1092                x_prefixed.insert(k, v);
1093            } else {
1094                other_attributes.insert(k, v);
1095            }
1096        }
1097
1098        Ok(DateRange {
1099            id,
1100            class,
1101            start_date,
1102            end_date,
1103            duration,
1104            planned_duration,
1105            x_prefixed: if x_prefixed.is_empty() {
1106                None
1107            } else {
1108                Some(x_prefixed)
1109            },
1110            end_on_next,
1111            other_attributes: if other_attributes.is_empty() {
1112                None
1113            } else {
1114                Some(other_attributes)
1115            },
1116        })
1117    }
1118
1119    pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1120        write_some_attribute_quoted!(w, "ID", &Some(&self.id))?;
1121        write_some_attribute_quoted!(w, ",CLASS", &self.class)?;
1122        write_some_attribute_quoted!(w, ",START-DATE", &Some(&self.start_date.to_rfc3339()))?;
1123        write_some_attribute_quoted!(
1124            w,
1125            ",END-DATE",
1126            &self.end_date.as_ref().map(|dt| dt.to_rfc3339())
1127        )?;
1128        write_some_attribute!(w, ",DURATION", &self.duration)?;
1129        write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
1130        if let Some(x_prefixed) = &self.x_prefixed {
1131            for (name, attr) in x_prefixed {
1132                write!(w, ",{}={}", name, attr)?;
1133            }
1134        }
1135        if self.end_on_next {
1136            write!(w, ",END-ON-NEXT=YES")?;
1137        }
1138        if let Some(other_attributes) = &self.other_attributes {
1139            for (name, attr) in other_attributes {
1140                write!(w, ",{}={}", name, attr)?;
1141            }
1142        }
1143        Ok(())
1144    }
1145}
1146
1147// -----------------------------------------------------------------------------------------------
1148// Rest
1149// -----------------------------------------------------------------------------------------------
1150
1151/// [`#EXT-X-START:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.5.2)
1152///
1153/// The EXT-X-START tag indicates a preferred point at which to start
1154/// playing a Playlist. By default, clients SHOULD start playback at
1155/// this point when beginning a playback session.
1156#[derive(Debug, Default, PartialEq, Clone)]
1157pub struct Start {
1158    pub time_offset: f64,
1159    pub precise: Option<bool>,
1160    pub other_attributes: HashMap<String, QuotedOrUnquoted>,
1161}
1162
1163impl Start {
1164    pub(crate) fn from_hashmap(
1165        mut attrs: HashMap<String, QuotedOrUnquoted>,
1166    ) -> Result<Start, String> {
1167        let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s
1168            .parse::<f64>()
1169            .map_err(|err| format!("Failed to parse TIME-OFFSET attribute: {}", err)))
1170        .ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
1171        Ok(Start {
1172            time_offset,
1173            precise: is_yes!(attrs, "PRECISE").into(),
1174            other_attributes: attrs,
1175        })
1176    }
1177
1178    pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1179        write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?;
1180        if let Some(precise) = self.precise {
1181            if precise {
1182                write!(w, ",PRECISE=YES")?;
1183            }
1184        }
1185        writeln!(w)?;
1186
1187        Ok(())
1188    }
1189}
1190
1191/// A simple `#EXT-` tag
1192#[derive(Debug, Default, PartialEq, Eq, Clone)]
1193pub struct ExtTag {
1194    pub tag: String,
1195    pub rest: Option<String>,
1196}
1197
1198impl Display for ExtTag {
1199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1200        write!(f, "#EXT-{}", self.tag)?;
1201        if let Some(v) = &self.rest {
1202            write!(f, ":{}", v)?;
1203        }
1204        Ok(())
1205    }
1206}
1207
1208#[cfg(test)]
1209mod test {
1210    use super::*;
1211
1212    #[test]
1213    fn ext_tag_with_value_is_printable() {
1214        let cue_out_tag = ExtTag {
1215            tag: "X-CUE-OUT".into(),
1216            rest: Some("DURATION=30".into()),
1217        };
1218
1219        let mut output = Vec::new();
1220        write!(output, "{}", cue_out_tag).unwrap();
1221
1222        assert_eq!(
1223            std::str::from_utf8(output.as_slice()).unwrap(),
1224            "#EXT-X-CUE-OUT:DURATION=30"
1225        )
1226    }
1227
1228    #[test]
1229    fn ext_tag_without_value_is_printable() {
1230        let cue_in_tag = ExtTag {
1231            tag: "X-CUE-IN".into(),
1232            rest: None,
1233        };
1234
1235        let mut output = Vec::new();
1236        write!(output, "{}", cue_in_tag).unwrap();
1237
1238        assert_eq!(
1239            std::str::from_utf8(output.as_slice()).unwrap(),
1240            "#EXT-X-CUE-IN"
1241        )
1242    }
1243}