From 3ecbbd9acbe7c758724082f292717bedc6fda3e8 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 6 Sep 2019 12:55:00 +0200 Subject: [PATCH 1/4] move tags into their own modules --- src/lib.rs | 1 + src/tags/basic/m3u.rs | 42 + src/tags/basic/mod.rs | 5 + src/tags/{basic.rs => basic/version.rs} | 37 +- src/tags/master_playlist.rs | 918 ------------------ .../master_playlist/i_frame_stream_inf.rs | 165 ++++ src/tags/master_playlist/media.rs | 382 ++++++++ src/tags/master_playlist/mod.rs | 11 + src/tags/master_playlist/session_data.rs | 152 +++ src/tags/master_playlist/session_key.rs | 76 ++ src/tags/master_playlist/stream_inf.rs | 218 +++++ src/tags/media_playlist.rs | 279 ------ .../media_playlist/discontinuity_sequence.rs | 62 ++ src/tags/media_playlist/end_list.rs | 44 + src/tags/media_playlist/i_frames_only.rs | 47 + src/tags/media_playlist/media_sequence.rs | 61 ++ src/tags/media_playlist/mod.rs | 13 + src/tags/media_playlist/playlist_type.rs | 61 ++ src/tags/media_playlist/target_duration.rs | 67 ++ src/tags/media_segment.rs | 613 ------------ src/tags/media_segment/byte_range.rs | 71 ++ src/tags/media_segment/date_range.rs | 158 +++ src/tags/media_segment/discontinuity.rs | 43 + src/tags/media_segment/inf.rs | 114 +++ src/tags/media_segment/key.rs | 130 +++ src/tags/media_segment/map.rs | 112 +++ src/tags/media_segment/mod.rs | 15 + src/tags/media_segment/program_date_time.rs | 63 ++ src/tags/mod.rs | 36 +- src/tags/shared/independent_segments.rs | 46 + src/tags/shared/mod.rs | 5 + .../start.rs} | 40 +- src/utils.rs | 15 + 33 files changed, 2194 insertions(+), 1908 deletions(-) create mode 100644 src/tags/basic/m3u.rs create mode 100644 src/tags/basic/mod.rs rename src/tags/{basic.rs => basic/version.rs} (64%) delete mode 100644 src/tags/master_playlist.rs create mode 100644 src/tags/master_playlist/i_frame_stream_inf.rs create mode 100644 src/tags/master_playlist/media.rs create mode 100644 src/tags/master_playlist/mod.rs create mode 100644 src/tags/master_playlist/session_data.rs create mode 100644 src/tags/master_playlist/session_key.rs create mode 100644 src/tags/master_playlist/stream_inf.rs delete mode 100644 src/tags/media_playlist.rs create mode 100644 src/tags/media_playlist/discontinuity_sequence.rs create mode 100644 src/tags/media_playlist/end_list.rs create mode 100644 src/tags/media_playlist/i_frames_only.rs create mode 100644 src/tags/media_playlist/media_sequence.rs create mode 100644 src/tags/media_playlist/mod.rs create mode 100644 src/tags/media_playlist/playlist_type.rs create mode 100644 src/tags/media_playlist/target_duration.rs delete mode 100644 src/tags/media_segment.rs create mode 100644 src/tags/media_segment/byte_range.rs create mode 100644 src/tags/media_segment/date_range.rs create mode 100644 src/tags/media_segment/discontinuity.rs create mode 100644 src/tags/media_segment/inf.rs create mode 100644 src/tags/media_segment/key.rs create mode 100644 src/tags/media_segment/map.rs create mode 100644 src/tags/media_segment/mod.rs create mode 100644 src/tags/media_segment/program_date_time.rs create mode 100644 src/tags/shared/independent_segments.rs create mode 100644 src/tags/shared/mod.rs rename src/tags/{media_or_master_playlist.rs => shared/start.rs} (74%) create mode 100644 src/utils.rs diff --git a/src/lib.rs b/src/lib.rs index 3feedfd..889d757 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ mod line; mod master_playlist; mod media_playlist; mod media_segment; +mod utils; /// This crate specific `Result` type. pub type Result = std::result::Result; diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs new file mode 100644 index 0000000..5d44b90 --- /dev/null +++ b/src/tags/basic/m3u.rs @@ -0,0 +1,42 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.1.1. EXTM3U] +/// +/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtM3u; +impl ExtM3u { + pub(crate) const PREFIX: &'static str = "#EXTM3U"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} +impl fmt::Display for ExtM3u { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::PREFIX.fmt(f) + } +} +impl FromStr for ExtM3u { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtM3u) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn extm3u() { + assert_eq!("#EXTM3U".parse::().ok(), Some(ExtM3u)); + assert_eq!(ExtM3u.to_string(), "#EXTM3U"); + assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/basic/mod.rs b/src/tags/basic/mod.rs new file mode 100644 index 0000000..e23eb19 --- /dev/null +++ b/src/tags/basic/mod.rs @@ -0,0 +1,5 @@ +mod m3u; +mod version; + +pub use m3u::*; +pub use version::*; diff --git a/src/tags/basic.rs b/src/tags/basic/version.rs similarity index 64% rename from src/tags/basic.rs rename to src/tags/basic/version.rs index a837104..13cc6ec 100644 --- a/src/tags/basic.rs +++ b/src/tags/basic/version.rs @@ -3,32 +3,6 @@ use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -/// [4.3.1.1. EXTM3U] -/// -/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtM3u; -impl ExtM3u { - pub(crate) const PREFIX: &'static str = "#EXTM3U"; - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtM3u { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } -} -impl FromStr for ExtM3u { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); - Ok(ExtM3u) - } -} - /// [4.3.1.2. EXT-X-VERSION] /// /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 @@ -36,6 +10,7 @@ impl FromStr for ExtM3u { pub struct ExtXVersion { version: ProtocolVersion, } + impl ExtXVersion { pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:"; @@ -54,11 +29,13 @@ impl ExtXVersion { ProtocolVersion::V1 } } + impl fmt::Display for ExtXVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.version) } } + impl FromStr for ExtXVersion { type Err = Error; fn from_str(s: &str) -> Result { @@ -69,17 +46,11 @@ impl FromStr for ExtXVersion { } } + #[cfg(test)] mod test { use super::*; - #[test] - fn extm3u() { - assert_eq!("#EXTM3U".parse::().ok(), Some(ExtM3u)); - assert_eq!(ExtM3u.to_string(), "#EXTM3U"); - assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1); - } - #[test] fn ext_x_version() { let tag = ExtXVersion::new(ProtocolVersion::V6); diff --git a/src/tags/master_playlist.rs b/src/tags/master_playlist.rs deleted file mode 100644 index ebfd28e..0000000 --- a/src/tags/master_playlist.rs +++ /dev/null @@ -1,918 +0,0 @@ -use super::{parse_u64, parse_yes_or_no}; -use crate::attribute::AttributePairs; -use crate::types::{ - ClosedCaptions, DecimalFloatingPoint, DecimalResolution, DecryptionKey, HdcpLevel, InStreamId, - MediaType, ProtocolVersion, QuotedString, SessionData, SingleLineString, -}; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::FromStr; - -/// `ExtXMedia` builder. -#[derive(Debug, Clone)] -pub struct ExtXMediaBuilder { - media_type: Option, - uri: Option, - group_id: Option, - language: Option, - assoc_language: Option, - name: Option, - default: bool, - autoselect: Option, - forced: Option, - instream_id: Option, - characteristics: Option, - channels: Option, -} -impl ExtXMediaBuilder { - /// Makes a `ExtXMediaBuilder` instance. - pub fn new() -> Self { - ExtXMediaBuilder { - media_type: None, - uri: None, - group_id: None, - language: None, - assoc_language: None, - name: None, - default: false, - autoselect: None, - forced: None, - instream_id: None, - characteristics: None, - channels: None, - } - } - - /// Sets the media type of the rendition. - pub fn media_type(&mut self, media_type: MediaType) -> &mut Self { - self.media_type = Some(media_type); - self - } - - /// Sets the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self { - self.group_id = Some(group_id); - self - } - - /// Sets a human-readable description of the rendition. - pub fn name(&mut self, name: QuotedString) -> &mut Self { - self.name = Some(name); - self - } - - /// Sets the URI that identifies the media playlist. - pub fn uri(&mut self, uri: QuotedString) -> &mut Self { - self.uri = Some(uri); - self - } - - /// Sets the name of the primary language used in the rendition. - pub fn language(&mut self, language: QuotedString) -> &mut Self { - self.language = Some(language); - self - } - - /// Sets the name of a language associated with the rendition. - pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self { - self.assoc_language = Some(language); - self - } - - /// Sets the value of the `default` flag. - pub fn default(&mut self, b: bool) -> &mut Self { - self.default = b; - self - } - - /// Sets the value of the `autoselect` flag. - pub fn autoselect(&mut self, b: bool) -> &mut Self { - self.autoselect = Some(b); - self - } - - /// Sets the value of the `forced` flag. - pub fn forced(&mut self, b: bool) -> &mut Self { - self.forced = Some(b); - self - } - - /// Sets the identifier that specifies a rendition within the segments in the media playlist. - pub fn instream_id(&mut self, id: InStreamId) -> &mut Self { - self.instream_id = Some(id); - self - } - - /// Sets the string that represents uniform type identifiers (UTI). - pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self { - self.characteristics = Some(characteristics); - self - } - - /// Sets the string that represents the parameters of the rendition. - pub fn channels(&mut self, channels: QuotedString) -> &mut Self { - self.channels = Some(channels); - self - } - - /// Builds a `ExtXMedia` instance. - pub fn finish(self) -> Result { - let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput); - let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput); - let name = track_assert_some!(self.name, ErrorKind::InvalidInput); - if MediaType::ClosedCaptions == media_type { - track_assert_ne!(self.uri, None, ErrorKind::InvalidInput); - track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput); - } else { - track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput); - } - if self.default && self.autoselect.is_some() { - track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput); - } - if MediaType::Subtitles != media_type { - track_assert_eq!(self.forced, None, ErrorKind::InvalidInput); - } - Ok(ExtXMedia { - media_type, - uri: self.uri, - group_id, - language: self.language, - assoc_language: self.assoc_language, - name, - default: self.default, - autoselect: self.autoselect.unwrap_or(false), - forced: self.forced.unwrap_or(false), - instream_id: self.instream_id, - characteristics: self.characteristics, - channels: self.channels, - }) - } -} -impl Default for ExtXMediaBuilder { - fn default() -> Self { - Self::new() - } -} - -/// [4.3.4.1. EXT-X-MEDIA] -/// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXMedia { - media_type: MediaType, - uri: Option, - group_id: QuotedString, - language: Option, - assoc_language: Option, - name: QuotedString, - default: bool, - autoselect: bool, - forced: bool, - instream_id: Option, - characteristics: Option, - channels: Option, -} -impl ExtXMedia { - pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; - - /// Makes a new `ExtXMedia` tag. - pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self { - ExtXMedia { - media_type, - uri: None, - group_id, - language: None, - assoc_language: None, - name, - default: false, - autoselect: false, - forced: false, - instream_id: None, - characteristics: None, - channels: None, - } - } - - /// Returns the type of the media associated with this tag. - pub fn media_type(&self) -> MediaType { - self.media_type - } - - /// Returns the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&self) -> &QuotedString { - &self.group_id - } - - /// Returns a human-readable description of the rendition. - pub fn name(&self) -> &QuotedString { - &self.name - } - - /// Returns the URI that identifies the media playlist. - pub fn uri(&self) -> Option<&QuotedString> { - self.uri.as_ref() - } - - /// Returns the name of the primary language used in the rendition. - pub fn language(&self) -> Option<&QuotedString> { - self.language.as_ref() - } - - /// Returns the name of a language associated with the rendition. - pub fn assoc_language(&self) -> Option<&QuotedString> { - self.assoc_language.as_ref() - } - - /// Returns whether this is the default rendition. - pub fn default(&self) -> bool { - self.default - } - - /// Returns whether the client may choose to - /// play this rendition in the absence of explicit user preference. - pub fn autoselect(&self) -> bool { - self.autoselect - } - - /// Returns whether the rendition contains content that is considered essential to play. - pub fn forced(&self) -> bool { - self.forced - } - - /// Returns the identifier that specifies a rendition within the segments in the media playlist. - pub fn instream_id(&self) -> Option { - self.instream_id - } - - /// Returns a string that represents uniform type identifiers (UTI). - /// - /// Each UTI indicates an individual characteristic of the rendition. - pub fn characteristics(&self) -> Option<&QuotedString> { - self.characteristics.as_ref() - } - - /// Returns a string that represents the parameters of the rendition. - pub fn channels(&self) -> Option<&QuotedString> { - self.channels.as_ref() - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - match self.instream_id { - None - | Some(InStreamId::Cc1) - | Some(InStreamId::Cc2) - | Some(InStreamId::Cc3) - | Some(InStreamId::Cc4) => ProtocolVersion::V1, - _ => ProtocolVersion::V7, - } - } -} -impl fmt::Display for ExtXMedia { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "TYPE={}", self.media_type)?; - if let Some(ref x) = self.uri { - write!(f, ",URI={}", x)?; - } - write!(f, ",GROUP-ID={}", self.group_id)?; - if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", x)?; - } - if let Some(ref x) = self.assoc_language { - write!(f, ",ASSOC-LANGUAGE={}", x)?; - } - write!(f, ",NAME={}", self.name)?; - if self.default { - write!(f, ",DEFAULT=YES")?; - } - if self.autoselect { - write!(f, ",AUTOSELECT=YES")?; - } - if self.forced { - write!(f, ",FORCED=YES")?; - } - if let Some(ref x) = self.instream_id { - write!(f, ",INSTREAM-ID=\"{}\"", x)?; - } - if let Some(ref x) = self.characteristics { - write!(f, ",CHARACTERISTICS={}", x)?; - } - if let Some(ref x) = self.channels { - write!(f, ",CHANNELS={}", x)?; - } - Ok(()) - } -} -impl FromStr for ExtXMedia { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - let mut builder = ExtXMediaBuilder::new(); - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "TYPE" => { - builder.media_type(track!(value.parse())?); - } - "URI" => { - builder.uri(track!(value.parse())?); - } - "GROUP-ID" => { - builder.group_id(track!(value.parse())?); - } - "LANGUAGE" => { - builder.language(track!(value.parse())?); - } - "ASSOC-LANGUAGE" => { - builder.assoc_language(track!(value.parse())?); - } - "NAME" => { - builder.name(track!(value.parse())?); - } - "DEFAULT" => { - builder.default(track!(parse_yes_or_no(value))?); - } - "AUTOSELECT" => { - builder.autoselect(track!(parse_yes_or_no(value))?); - } - "FORCED" => { - builder.forced(track!(parse_yes_or_no(value))?); - } - "INSTREAM-ID" => { - let s: QuotedString = track!(value.parse())?; - builder.instream_id(track!(s.parse())?); - } - "CHARACTERISTICS" => { - builder.characteristics(track!(value.parse())?); - } - "CHANNELS" => { - builder.channels(track!(value.parse())?); - } - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - track!(builder.finish()) - } -} - -/// [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtXStreamInf { - uri: SingleLineString, - bandwidth: u64, - average_bandwidth: Option, - codecs: Option, - resolution: Option, - frame_rate: Option, - hdcp_level: Option, - audio: Option, - video: Option, - subtitles: Option, - closed_captions: Option, -} -impl ExtXStreamInf { - pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; - - /// Makes a new `ExtXStreamInf` tag. - pub fn new(uri: SingleLineString, bandwidth: u64) -> Self { - ExtXStreamInf { - uri, - bandwidth, - average_bandwidth: None, - codecs: None, - resolution: None, - frame_rate: None, - hdcp_level: None, - audio: None, - video: None, - subtitles: None, - closed_captions: None, - } - } - - /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &SingleLineString { - &self.uri - } - - /// Returns the peak segment bit rate of the variant stream. - pub fn bandwidth(&self) -> u64 { - self.bandwidth - } - - /// Returns the average segment bit rate of the variant stream. - pub fn average_bandwidth(&self) -> Option { - self.average_bandwidth - } - - /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&QuotedString> { - self.codecs.as_ref() - } - - /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub fn resolution(&self) -> Option { - self.resolution - } - - /// Returns the maximum frame rate for all the video in the variant stream. - pub fn frame_rate(&self) -> Option { - self.frame_rate - } - - /// Returns the HDCP level of the variant stream. - pub fn hdcp_level(&self) -> Option { - self.hdcp_level - } - - /// Returns the group identifier for the audio in the variant stream. - pub fn audio(&self) -> Option<&QuotedString> { - self.audio.as_ref() - } - - /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&QuotedString> { - self.video.as_ref() - } - - /// Returns the group identifier for the subtitles in the variant stream. - pub fn subtitles(&self) -> Option<&QuotedString> { - self.subtitles.as_ref() - } - - /// Returns the value of `CLOSED-CAPTIONS` attribute. - pub fn closed_captions(&self) -> Option<&ClosedCaptions> { - self.closed_captions.as_ref() - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXStreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "BANDWIDTH={}", self.bandwidth)?; - if let Some(ref x) = self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", x)?; - } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", x)?; - } - if let Some(ref x) = self.resolution { - write!(f, ",RESOLUTION={}", x)?; - } - if let Some(ref x) = self.frame_rate { - write!(f, ",FRAME-RATE={:.3}", x.as_f64())?; - } - if let Some(ref x) = self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", x)?; - } - if let Some(ref x) = self.audio { - write!(f, ",AUDIO={}", x)?; - } - if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", x)?; - } - if let Some(ref x) = self.subtitles { - write!(f, ",SUBTITLES={}", x)?; - } - if let Some(ref x) = self.closed_captions { - write!(f, ",CLOSED-CAPTIONS={}", x)?; - } - write!(f, "\n{}", self.uri)?; - Ok(()) - } -} -impl FromStr for ExtXStreamInf { - type Err = Error; - fn from_str(s: &str) -> Result { - let mut lines = s.splitn(2, '\n'); - let first_line = lines.next().expect("Never fails").trim_end_matches('\r'); - let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput); - - track_assert!( - first_line.starts_with(Self::PREFIX), - ErrorKind::InvalidInput - ); - let uri = track!(SingleLineString::new(second_line))?; - let mut bandwidth = None; - let mut average_bandwidth = None; - let mut codecs = None; - let mut resolution = None; - let mut frame_rate = None; - let mut hdcp_level = None; - let mut audio = None; - let mut video = None; - let mut subtitles = None; - let mut closed_captions = None; - let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), - "CODECS" => codecs = Some(track!(value.parse())?), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "FRAME-RATE" => frame_rate = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "AUDIO" => audio = Some(track!(value.parse())?), - "VIDEO" => video = Some(track!(value.parse())?), - "SUBTITLES" => subtitles = Some(track!(value.parse())?), - "CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); - Ok(ExtXStreamInf { - uri, - bandwidth, - average_bandwidth, - codecs, - resolution, - frame_rate, - hdcp_level, - audio, - video, - subtitles, - closed_captions, - }) - } -} - -/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] -/// -/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXIFrameStreamInf { - uri: QuotedString, - bandwidth: u64, - average_bandwidth: Option, - codecs: Option, - resolution: Option, - hdcp_level: Option, - video: Option, -} -impl ExtXIFrameStreamInf { - pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; - - /// Makes a new `ExtXIFrameStreamInf` tag. - pub fn new(uri: QuotedString, bandwidth: u64) -> Self { - ExtXIFrameStreamInf { - uri, - bandwidth, - average_bandwidth: None, - codecs: None, - resolution: None, - hdcp_level: None, - video: None, - } - } - - /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &QuotedString { - &self.uri - } - - /// Returns the peak segment bit rate of the variant stream. - pub fn bandwidth(&self) -> u64 { - self.bandwidth - } - - /// Returns the average segment bit rate of the variant stream. - pub fn average_bandwidth(&self) -> Option { - self.average_bandwidth - } - - /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&QuotedString> { - self.codecs.as_ref() - } - - /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub fn resolution(&self) -> Option { - self.resolution - } - - /// Returns the HDCP level of the variant stream. - pub fn hdcp_level(&self) -> Option { - self.hdcp_level - } - - /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&QuotedString> { - self.video.as_ref() - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXIFrameStreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={}", self.uri)?; - write!(f, ",BANDWIDTH={}", self.bandwidth)?; - if let Some(ref x) = self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", x)?; - } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", x)?; - } - if let Some(ref x) = self.resolution { - write!(f, ",RESOLUTION={}", x)?; - } - if let Some(ref x) = self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", x)?; - } - if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", x)?; - } - Ok(()) - } -} -impl FromStr for ExtXIFrameStreamInf { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - let mut uri = None; - let mut bandwidth = None; - let mut average_bandwidth = None; - let mut codecs = None; - let mut resolution = None; - let mut hdcp_level = None; - let mut video = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "URI" => uri = Some(track!(value.parse())?), - "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), - "CODECS" => codecs = Some(track!(value.parse())?), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "VIDEO" => video = Some(track!(value.parse())?), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); - Ok(ExtXIFrameStreamInf { - uri, - bandwidth, - average_bandwidth, - codecs, - resolution, - hdcp_level, - video, - }) - } -} - -/// [4.3.4.4. EXT-X-SESSION-DATA] -/// -/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXSessionData { - data_id: QuotedString, - data: SessionData, - language: Option, -} -impl ExtXSessionData { - pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; - - /// Makes a new `ExtXSessionData` tag. - pub fn new(data_id: QuotedString, data: SessionData) -> Self { - ExtXSessionData { - data_id, - data, - language: None, - } - } - - /// Makes a new `ExtXSessionData` with the given language. - pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self { - ExtXSessionData { - data_id, - data, - language: Some(language), - } - } - - /// Returns the identifier of the data. - pub fn data_id(&self) -> &QuotedString { - &self.data_id - } - - /// Returns the session data. - pub fn data(&self) -> &SessionData { - &self.data - } - - /// Returns the language of the data. - pub fn language(&self) -> Option<&QuotedString> { - self.language.as_ref() - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXSessionData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "DATA-ID={}", self.data_id)?; - match self.data { - SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?, - SessionData::Uri(ref x) => write!(f, ",URI={}", x)?, - } - if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", x)?; - } - Ok(()) - } -} -impl FromStr for ExtXSessionData { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - let mut data_id = None; - let mut session_value = None; - let mut uri = None; - let mut language = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "DATA-ID" => data_id = Some(track!(value.parse())?), - "VALUE" => session_value = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), - "LANGUAGE" => language = Some(track!(value.parse())?), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - - let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput); - let data = if let Some(value) = session_value { - track_assert_eq!(uri, None, ErrorKind::InvalidInput); - SessionData::Value(value) - } else if let Some(uri) = uri { - SessionData::Uri(uri) - } else { - track_panic!(ErrorKind::InvalidInput); - }; - Ok(ExtXSessionData { - data_id, - data, - language, - }) - } -} - -/// [4.3.4.5. EXT-X-SESSION-KEY] -/// -/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXSessionKey { - key: DecryptionKey, -} -impl ExtXSessionKey { - pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; - - /// Makes a new `ExtXSessionKey` tag. - pub fn new(key: DecryptionKey) -> Self { - ExtXSessionKey { key } - } - - /// Returns a decryption key for the playlist. - pub fn key(&self) -> &DecryptionKey { - &self.key - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - self.key.requires_version() - } -} -impl fmt::Display for ExtXSessionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.key) - } -} -impl FromStr for ExtXSessionKey { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - let key = track!(suffix.parse())?; - Ok(ExtXSessionKey { key }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::types::{EncryptionMethod, InitializationVector}; - - #[test] - fn ext_x_media() { - let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar")); - let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_stream_inf() { - let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000); - let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_i_frame_stream_inf() { - let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 1000); - let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_session_data() { - let tag = ExtXSessionData::new( - quoted_string("foo"), - SessionData::Value(quoted_string("bar")), - ); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = - ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar"))); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = ExtXSessionData::with_language( - quoted_string("foo"), - SessionData::Value(quoted_string("bar")), - quoted_string("baz"), - ); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_session_key() { - let tag = ExtXSessionKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: quoted_string("foo"), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: None, - key_format_versions: None, - }); - let text = - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V2); - } - - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() - } -} diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs new file mode 100644 index 0000000..209f4d2 --- /dev/null +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -0,0 +1,165 @@ +use crate::utils::parse_u64; +use crate::attribute::AttributePairs; +use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion, QuotedString}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + + +/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] +/// +/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXIFrameStreamInf { + uri: QuotedString, + bandwidth: u64, + average_bandwidth: Option, + codecs: Option, + resolution: Option, + hdcp_level: Option, + video: Option, +} + +impl ExtXIFrameStreamInf { + pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; + + /// Makes a new `ExtXIFrameStreamInf` tag. + pub fn new(uri: QuotedString, bandwidth: u64) -> Self { + ExtXIFrameStreamInf { + uri, + bandwidth, + average_bandwidth: None, + codecs: None, + resolution: None, + hdcp_level: None, + video: None, + } + } + + /// Returns the URI that identifies the associated media playlist. + pub fn uri(&self) -> &QuotedString { + &self.uri + } + + /// Returns the peak segment bit rate of the variant stream. + pub fn bandwidth(&self) -> u64 { + self.bandwidth + } + + /// Returns the average segment bit rate of the variant stream. + pub fn average_bandwidth(&self) -> Option { + self.average_bandwidth + } + + /// Returns a string that represents the list of codec types contained the variant stream. + pub fn codecs(&self) -> Option<&QuotedString> { + self.codecs.as_ref() + } + + /// Returns the optimal pixel resolution at which to display all the video in the variant stream. + pub fn resolution(&self) -> Option { + self.resolution + } + + /// Returns the HDCP level of the variant stream. + pub fn hdcp_level(&self) -> Option { + self.hdcp_level + } + + /// Returns the group identifier for the video in the variant stream. + pub fn video(&self) -> Option<&QuotedString> { + self.video.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXIFrameStreamInf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "URI={}", self.uri)?; + write!(f, ",BANDWIDTH={}", self.bandwidth)?; + if let Some(ref x) = self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", x)?; + } + if let Some(ref x) = self.codecs { + write!(f, ",CODECS={}", x)?; + } + if let Some(ref x) = self.resolution { + write!(f, ",RESOLUTION={}", x)?; + } + if let Some(ref x) = self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", x)?; + } + if let Some(ref x) = self.video { + write!(f, ",VIDEO={}", x)?; + } + Ok(()) + } +} + +impl FromStr for ExtXIFrameStreamInf { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut uri = None; + let mut bandwidth = None; + let mut average_bandwidth = None; + let mut codecs = None; + let mut resolution = None; + let mut hdcp_level = None; + let mut video = None; + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "URI" => uri = Some(track!(value.parse())?), + "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), + "CODECS" => codecs = Some(track!(value.parse())?), + "RESOLUTION" => resolution = Some(track!(value.parse())?), + "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), + "VIDEO" => video = Some(track!(value.parse())?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); + Ok(ExtXIFrameStreamInf { + uri, + bandwidth, + average_bandwidth, + codecs, + resolution, + hdcp_level, + video, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_i_frame_stream_inf() { + let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 1000); + let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } + + + fn quoted_string(s: &str) -> QuotedString { + QuotedString::new(s).unwrap() + } +} diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs new file mode 100644 index 0000000..151139a --- /dev/null +++ b/src/tags/master_playlist/media.rs @@ -0,0 +1,382 @@ +use crate::utils::parse_yes_or_no; +use crate::attribute::AttributePairs; +use crate::types::{ InStreamId, MediaType, ProtocolVersion, QuotedString}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// `ExtXMedia` builder. +#[derive(Debug, Clone)] +pub struct ExtXMediaBuilder { + media_type: Option, + uri: Option, + group_id: Option, + language: Option, + assoc_language: Option, + name: Option, + default: bool, + autoselect: Option, + forced: Option, + instream_id: Option, + characteristics: Option, + channels: Option, +} + +impl ExtXMediaBuilder { + /// Makes a `ExtXMediaBuilder` instance. + pub fn new() -> Self { + ExtXMediaBuilder { + media_type: None, + uri: None, + group_id: None, + language: None, + assoc_language: None, + name: None, + default: false, + autoselect: None, + forced: None, + instream_id: None, + characteristics: None, + channels: None, + } + } + + /// Sets the media type of the rendition. + pub fn media_type(&mut self, media_type: MediaType) -> &mut Self { + self.media_type = Some(media_type); + self + } + + /// Sets the identifier that specifies the group to which the rendition belongs. + pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self { + self.group_id = Some(group_id); + self + } + + /// Sets a human-readable description of the rendition. + pub fn name(&mut self, name: QuotedString) -> &mut Self { + self.name = Some(name); + self + } + + /// Sets the URI that identifies the media playlist. + pub fn uri(&mut self, uri: QuotedString) -> &mut Self { + self.uri = Some(uri); + self + } + + /// Sets the name of the primary language used in the rendition. + pub fn language(&mut self, language: QuotedString) -> &mut Self { + self.language = Some(language); + self + } + + /// Sets the name of a language associated with the rendition. + pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self { + self.assoc_language = Some(language); + self + } + + /// Sets the value of the `default` flag. + pub fn default(&mut self, b: bool) -> &mut Self { + self.default = b; + self + } + + /// Sets the value of the `autoselect` flag. + pub fn autoselect(&mut self, b: bool) -> &mut Self { + self.autoselect = Some(b); + self + } + + /// Sets the value of the `forced` flag. + pub fn forced(&mut self, b: bool) -> &mut Self { + self.forced = Some(b); + self + } + + /// Sets the identifier that specifies a rendition within the segments in the media playlist. + pub fn instream_id(&mut self, id: InStreamId) -> &mut Self { + self.instream_id = Some(id); + self + } + + /// Sets the string that represents uniform type identifiers (UTI). + pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self { + self.characteristics = Some(characteristics); + self + } + + /// Sets the string that represents the parameters of the rendition. + pub fn channels(&mut self, channels: QuotedString) -> &mut Self { + self.channels = Some(channels); + self + } + + /// Builds a `ExtXMedia` instance. + pub fn finish(self) -> Result { + let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput); + let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput); + let name = track_assert_some!(self.name, ErrorKind::InvalidInput); + if MediaType::ClosedCaptions == media_type { + track_assert_ne!(self.uri, None, ErrorKind::InvalidInput); + track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput); + } else { + track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput); + } + if self.default && self.autoselect.is_some() { + track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput); + } + if MediaType::Subtitles != media_type { + track_assert_eq!(self.forced, None, ErrorKind::InvalidInput); + } + Ok(ExtXMedia { + media_type, + uri: self.uri, + group_id, + language: self.language, + assoc_language: self.assoc_language, + name, + default: self.default, + autoselect: self.autoselect.unwrap_or(false), + forced: self.forced.unwrap_or(false), + instream_id: self.instream_id, + characteristics: self.characteristics, + channels: self.channels, + }) + } +} + +impl Default for ExtXMediaBuilder { + fn default() -> Self { + Self::new() + } +} + +/// [4.3.4.1. EXT-X-MEDIA] +/// +/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXMedia { + media_type: MediaType, + uri: Option, + group_id: QuotedString, + language: Option, + assoc_language: Option, + name: QuotedString, + default: bool, + autoselect: bool, + forced: bool, + instream_id: Option, + characteristics: Option, + channels: Option, +} + +impl ExtXMedia { + pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; + + /// Makes a new `ExtXMedia` tag. + pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self { + ExtXMedia { + media_type, + uri: None, + group_id, + language: None, + assoc_language: None, + name, + default: false, + autoselect: false, + forced: false, + instream_id: None, + characteristics: None, + channels: None, + } + } + + /// Returns the type of the media associated with this tag. + pub fn media_type(&self) -> MediaType { + self.media_type + } + + /// Returns the identifier that specifies the group to which the rendition belongs. + pub fn group_id(&self) -> &QuotedString { + &self.group_id + } + + /// Returns a human-readable description of the rendition. + pub fn name(&self) -> &QuotedString { + &self.name + } + + /// Returns the URI that identifies the media playlist. + pub fn uri(&self) -> Option<&QuotedString> { + self.uri.as_ref() + } + + /// Returns the name of the primary language used in the rendition. + pub fn language(&self) -> Option<&QuotedString> { + self.language.as_ref() + } + + /// Returns the name of a language associated with the rendition. + pub fn assoc_language(&self) -> Option<&QuotedString> { + self.assoc_language.as_ref() + } + + /// Returns whether this is the default rendition. + pub fn default(&self) -> bool { + self.default + } + + /// Returns whether the client may choose to + /// play this rendition in the absence of explicit user preference. + pub fn autoselect(&self) -> bool { + self.autoselect + } + + /// Returns whether the rendition contains content that is considered essential to play. + pub fn forced(&self) -> bool { + self.forced + } + + /// Returns the identifier that specifies a rendition within the segments in the media playlist. + pub fn instream_id(&self) -> Option { + self.instream_id + } + + /// Returns a string that represents uniform type identifiers (UTI). + /// + /// Each UTI indicates an individual characteristic of the rendition. + pub fn characteristics(&self) -> Option<&QuotedString> { + self.characteristics.as_ref() + } + + /// Returns a string that represents the parameters of the rendition. + pub fn channels(&self) -> Option<&QuotedString> { + self.channels.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + match self.instream_id { + None + | Some(InStreamId::Cc1) + | Some(InStreamId::Cc2) + | Some(InStreamId::Cc3) + | Some(InStreamId::Cc4) => ProtocolVersion::V1, + _ => ProtocolVersion::V7, + } + } +} + +impl fmt::Display for ExtXMedia { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "TYPE={}", self.media_type)?; + if let Some(ref x) = self.uri { + write!(f, ",URI={}", x)?; + } + write!(f, ",GROUP-ID={}", self.group_id)?; + if let Some(ref x) = self.language { + write!(f, ",LANGUAGE={}", x)?; + } + if let Some(ref x) = self.assoc_language { + write!(f, ",ASSOC-LANGUAGE={}", x)?; + } + write!(f, ",NAME={}", self.name)?; + if self.default { + write!(f, ",DEFAULT=YES")?; + } + if self.autoselect { + write!(f, ",AUTOSELECT=YES")?; + } + if self.forced { + write!(f, ",FORCED=YES")?; + } + if let Some(ref x) = self.instream_id { + write!(f, ",INSTREAM-ID=\"{}\"", x)?; + } + if let Some(ref x) = self.characteristics { + write!(f, ",CHARACTERISTICS={}", x)?; + } + if let Some(ref x) = self.channels { + write!(f, ",CHANNELS={}", x)?; + } + Ok(()) + } +} + +impl FromStr for ExtXMedia { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut builder = ExtXMediaBuilder::new(); + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "TYPE" => { + builder.media_type(track!(value.parse())?); + } + "URI" => { + builder.uri(track!(value.parse())?); + } + "GROUP-ID" => { + builder.group_id(track!(value.parse())?); + } + "LANGUAGE" => { + builder.language(track!(value.parse())?); + } + "ASSOC-LANGUAGE" => { + builder.assoc_language(track!(value.parse())?); + } + "NAME" => { + builder.name(track!(value.parse())?); + } + "DEFAULT" => { + builder.default(track!(parse_yes_or_no(value))?); + } + "AUTOSELECT" => { + builder.autoselect(track!(parse_yes_or_no(value))?); + } + "FORCED" => { + builder.forced(track!(parse_yes_or_no(value))?); + } + "INSTREAM-ID" => { + let s: QuotedString = track!(value.parse())?; + builder.instream_id(track!(s.parse())?); + } + "CHARACTERISTICS" => { + builder.characteristics(track!(value.parse())?); + } + "CHANNELS" => { + builder.channels(track!(value.parse())?); + } + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + track!(builder.finish()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_media() { + let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar")); + let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } + + fn quoted_string(s: &str) -> QuotedString { + QuotedString::new(s).unwrap() + } +} diff --git a/src/tags/master_playlist/mod.rs b/src/tags/master_playlist/mod.rs new file mode 100644 index 0000000..293509a --- /dev/null +++ b/src/tags/master_playlist/mod.rs @@ -0,0 +1,11 @@ +mod i_frame_stream_inf; +mod media; +mod session_data; +mod session_key; +mod stream_inf; + +pub use i_frame_stream_inf::*; +pub use media::*; +pub use session_data::*; +pub use session_key::*; +pub use stream_inf::*; diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs new file mode 100644 index 0000000..28a69b3 --- /dev/null +++ b/src/tags/master_playlist/session_data.rs @@ -0,0 +1,152 @@ +use crate::attribute::AttributePairs; +use crate::types::{ProtocolVersion, QuotedString, SessionData}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + + +/// [4.3.4.4. EXT-X-SESSION-DATA] +/// +/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXSessionData { + data_id: QuotedString, + data: SessionData, + language: Option, +} + +impl ExtXSessionData { + pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; + + /// Makes a new `ExtXSessionData` tag. + pub fn new(data_id: QuotedString, data: SessionData) -> Self { + ExtXSessionData { + data_id, + data, + language: None, + } + } + + /// Makes a new `ExtXSessionData` with the given language. + pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self { + ExtXSessionData { + data_id, + data, + language: Some(language), + } + } + + /// Returns the identifier of the data. + pub fn data_id(&self) -> &QuotedString { + &self.data_id + } + + /// Returns the session data. + pub fn data(&self) -> &SessionData { + &self.data + } + + /// Returns the language of the data. + pub fn language(&self) -> Option<&QuotedString> { + self.language.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXSessionData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "DATA-ID={}", self.data_id)?; + match self.data { + SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?, + SessionData::Uri(ref x) => write!(f, ",URI={}", x)?, + } + if let Some(ref x) = self.language { + write!(f, ",LANGUAGE={}", x)?; + } + Ok(()) + } +} + +impl FromStr for ExtXSessionData { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut data_id = None; + let mut session_value = None; + let mut uri = None; + let mut language = None; + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "DATA-ID" => data_id = Some(track!(value.parse())?), + "VALUE" => session_value = Some(track!(value.parse())?), + "URI" => uri = Some(track!(value.parse())?), + "LANGUAGE" => language = Some(track!(value.parse())?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput); + let data = if let Some(value) = session_value { + track_assert_eq!(uri, None, ErrorKind::InvalidInput); + SessionData::Value(value) + } else if let Some(uri) = uri { + SessionData::Uri(uri) + } else { + track_panic!(ErrorKind::InvalidInput); + }; + Ok(ExtXSessionData { + data_id, + data, + language, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_session_data() { + let tag = ExtXSessionData::new( + quoted_string("foo"), + SessionData::Value(quoted_string("bar")), + ); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = + ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar"))); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtXSessionData::with_language( + quoted_string("foo"), + SessionData::Value(quoted_string("bar")), + quoted_string("baz"), + ); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } + + fn quoted_string(s: &str) -> QuotedString { + QuotedString::new(s).unwrap() + } +} diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs new file mode 100644 index 0000000..36ace8f --- /dev/null +++ b/src/tags/master_playlist/session_key.rs @@ -0,0 +1,76 @@ +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + + +/// [4.3.4.5. EXT-X-SESSION-KEY] +/// +/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXSessionKey { + key: DecryptionKey, +} + +impl ExtXSessionKey { + pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; + + /// Makes a new `ExtXSessionKey` tag. + pub fn new(key: DecryptionKey) -> Self { + ExtXSessionKey { key } + } + + /// Returns a decryption key for the playlist. + pub fn key(&self) -> &DecryptionKey { + &self.key + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + self.key.requires_version() + } +} + +impl fmt::Display for ExtXSessionKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.key) + } +} + +impl FromStr for ExtXSessionKey { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let suffix = s.split_at(Self::PREFIX.len()).1; + let key = track!(suffix.parse())?; + Ok(ExtXSessionKey { key }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::{EncryptionMethod, InitializationVector, QuotedString}; + + #[test] + fn ext_x_session_key() { + let tag = ExtXSessionKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: quoted_string("foo"), + iv: Some(InitializationVector([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + ])), + key_format: None, + key_format_versions: None, + }); + let text = + r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V2); + } + + fn quoted_string(s: &str) -> QuotedString { + QuotedString::new(s).unwrap() + } +} diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs new file mode 100644 index 0000000..f24bdaf --- /dev/null +++ b/src/tags/master_playlist/stream_inf.rs @@ -0,0 +1,218 @@ +use crate::utils::parse_u64; +use crate::attribute::AttributePairs; +use crate::types::{ + ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, + ProtocolVersion, QuotedString, SingleLineString, +}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + + +/// [4.3.4.2. EXT-X-STREAM-INF] +/// +/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtXStreamInf { + uri: SingleLineString, + bandwidth: u64, + average_bandwidth: Option, + codecs: Option, + resolution: Option, + frame_rate: Option, + hdcp_level: Option, + audio: Option, + video: Option, + subtitles: Option, + closed_captions: Option, +} + +impl ExtXStreamInf { + pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; + + /// Makes a new `ExtXStreamInf` tag. + pub fn new(uri: SingleLineString, bandwidth: u64) -> Self { + ExtXStreamInf { + uri, + bandwidth, + average_bandwidth: None, + codecs: None, + resolution: None, + frame_rate: None, + hdcp_level: None, + audio: None, + video: None, + subtitles: None, + closed_captions: None, + } + } + + /// Returns the URI that identifies the associated media playlist. + pub fn uri(&self) -> &SingleLineString { + &self.uri + } + + /// Returns the peak segment bit rate of the variant stream. + pub fn bandwidth(&self) -> u64 { + self.bandwidth + } + + /// Returns the average segment bit rate of the variant stream. + pub fn average_bandwidth(&self) -> Option { + self.average_bandwidth + } + + /// Returns a string that represents the list of codec types contained the variant stream. + pub fn codecs(&self) -> Option<&QuotedString> { + self.codecs.as_ref() + } + + /// Returns the optimal pixel resolution at which to display all the video in the variant stream. + pub fn resolution(&self) -> Option { + self.resolution + } + + /// Returns the maximum frame rate for all the video in the variant stream. + pub fn frame_rate(&self) -> Option { + self.frame_rate + } + + /// Returns the HDCP level of the variant stream. + pub fn hdcp_level(&self) -> Option { + self.hdcp_level + } + + /// Returns the group identifier for the audio in the variant stream. + pub fn audio(&self) -> Option<&QuotedString> { + self.audio.as_ref() + } + + /// Returns the group identifier for the video in the variant stream. + pub fn video(&self) -> Option<&QuotedString> { + self.video.as_ref() + } + + /// Returns the group identifier for the subtitles in the variant stream. + pub fn subtitles(&self) -> Option<&QuotedString> { + self.subtitles.as_ref() + } + + /// Returns the value of `CLOSED-CAPTIONS` attribute. + pub fn closed_captions(&self) -> Option<&ClosedCaptions> { + self.closed_captions.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXStreamInf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "BANDWIDTH={}", self.bandwidth)?; + if let Some(ref x) = self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", x)?; + } + if let Some(ref x) = self.codecs { + write!(f, ",CODECS={}", x)?; + } + if let Some(ref x) = self.resolution { + write!(f, ",RESOLUTION={}", x)?; + } + if let Some(ref x) = self.frame_rate { + write!(f, ",FRAME-RATE={:.3}", x.as_f64())?; + } + if let Some(ref x) = self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", x)?; + } + if let Some(ref x) = self.audio { + write!(f, ",AUDIO={}", x)?; + } + if let Some(ref x) = self.video { + write!(f, ",VIDEO={}", x)?; + } + if let Some(ref x) = self.subtitles { + write!(f, ",SUBTITLES={}", x)?; + } + if let Some(ref x) = self.closed_captions { + write!(f, ",CLOSED-CAPTIONS={}", x)?; + } + write!(f, "\n{}", self.uri)?; + Ok(()) + } +} + +impl FromStr for ExtXStreamInf { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut lines = s.splitn(2, '\n'); + let first_line = lines.next().expect("Never fails").trim_end_matches('\r'); + let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput); + + track_assert!( + first_line.starts_with(Self::PREFIX), + ErrorKind::InvalidInput + ); + let uri = track!(SingleLineString::new(second_line))?; + let mut bandwidth = None; + let mut average_bandwidth = None; + let mut codecs = None; + let mut resolution = None; + let mut frame_rate = None; + let mut hdcp_level = None; + let mut audio = None; + let mut video = None; + let mut subtitles = None; + let mut closed_captions = None; + let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), + "CODECS" => codecs = Some(track!(value.parse())?), + "RESOLUTION" => resolution = Some(track!(value.parse())?), + "FRAME-RATE" => frame_rate = Some(track!(value.parse())?), + "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), + "AUDIO" => audio = Some(track!(value.parse())?), + "VIDEO" => video = Some(track!(value.parse())?), + "SUBTITLES" => subtitles = Some(track!(value.parse())?), + "CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); + Ok(ExtXStreamInf { + uri, + bandwidth, + average_bandwidth, + codecs, + resolution, + frame_rate, + hdcp_level, + audio, + video, + subtitles, + closed_captions, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_stream_inf() { + let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000); + let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_playlist.rs b/src/tags/media_playlist.rs deleted file mode 100644 index ab7c0eb..0000000 --- a/src/tags/media_playlist.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::types::{PlaylistType, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::FromStr; -use std::time::Duration; -use trackable::error::ErrorKindExt; - -/// [4.3.3.1. EXT-X-TARGETDURATION] -/// -/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXTargetDuration { - duration: Duration, -} -impl ExtXTargetDuration { - pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; - - /// Makes a new `ExtXTargetduration` tag. - /// - /// Note that the nanoseconds part of the `duration` will be discarded. - pub fn new(duration: Duration) -> Self { - let duration = Duration::from_secs(duration.as_secs()); - ExtXTargetDuration { duration } - } - - /// Returns the maximum media segment duration in the associated playlist. - pub fn duration(&self) -> Duration { - self.duration - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXTargetDuration { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.duration.as_secs()) - } -} -impl FromStr for ExtXTargetDuration { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXTargetDuration { - duration: Duration::from_secs(duration), - }) - } -} - -/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] -/// -/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXMediaSequence { - seq_num: u64, -} -impl ExtXMediaSequence { - pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; - - /// Makes a new `ExtXMediaSequence` tag. - pub fn new(seq_num: u64) -> Self { - ExtXMediaSequence { seq_num } - } - - /// Returns the sequence number of the first media segment that appears in the associated playlist. - pub fn seq_num(self) -> u64 { - self.seq_num - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXMediaSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.seq_num) - } -} -impl FromStr for ExtXMediaSequence { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXMediaSequence { seq_num }) - } -} - -/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] -/// -/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXDiscontinuitySequence { - seq_num: u64, -} -impl ExtXDiscontinuitySequence { - pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; - - /// Makes a new `ExtXDiscontinuitySequence` tag. - pub fn new(seq_num: u64) -> Self { - ExtXDiscontinuitySequence { seq_num } - } - - /// Returns the discontinuity sequence number of - /// the first media segment that appears in the associated playlist. - pub fn seq_num(self) -> u64 { - self.seq_num - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXDiscontinuitySequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.seq_num) - } -} -impl FromStr for ExtXDiscontinuitySequence { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXDiscontinuitySequence { seq_num }) - } -} - -/// [4.3.3.4. EXT-X-ENDLIST] -/// -/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXEndList; -impl ExtXEndList { - pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST"; - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXEndList { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } -} -impl FromStr for ExtXEndList { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); - Ok(ExtXEndList) - } -} - -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE] -/// -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXPlaylistType { - playlist_type: PlaylistType, -} -impl ExtXPlaylistType { - pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; - - /// Makes a new `ExtXPlaylistType` tag. - pub fn new(playlist_type: PlaylistType) -> Self { - ExtXPlaylistType { playlist_type } - } - - /// Returns the type of the associated media playlist. - pub fn playlist_type(self) -> PlaylistType { - self.playlist_type - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXPlaylistType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.playlist_type) - } -} -impl FromStr for ExtXPlaylistType { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXPlaylistType { playlist_type }) - } -} - -/// [4.3.3.6. EXT-X-I-FRAMES-ONLY] -/// -/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXIFramesOnly; -impl ExtXIFramesOnly { - pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY"; - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V4 - } -} -impl fmt::Display for ExtXIFramesOnly { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } -} -impl FromStr for ExtXIFramesOnly { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); - Ok(ExtXIFramesOnly) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn ext_x_targetduration() { - let tag = ExtXTargetDuration::new(Duration::from_secs(5)); - let text = "#EXT-X-TARGETDURATION:5"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_media_sequence() { - let tag = ExtXMediaSequence::new(123); - let text = "#EXT-X-MEDIA-SEQUENCE:123"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_discontinuity_sequence() { - let tag = ExtXDiscontinuitySequence::new(123); - let text = "#EXT-X-DISCONTINUITY-SEQUENCE:123"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_endlist() { - let tag = ExtXEndList; - let text = "#EXT-X-ENDLIST"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_playlist_type() { - let tag = ExtXPlaylistType::new(PlaylistType::Vod); - let text = "#EXT-X-PLAYLIST-TYPE:VOD"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_i_frames_only() { - let tag = ExtXIFramesOnly; - let text = "#EXT-X-I-FRAMES-ONLY"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V4); - } -} diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs new file mode 100644 index 0000000..9aa6539 --- /dev/null +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -0,0 +1,62 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; +use trackable::error::ErrorKindExt; + +/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] +/// +/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXDiscontinuitySequence { + seq_num: u64, +} + +impl ExtXDiscontinuitySequence { + pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; + + /// Makes a new `ExtXDiscontinuitySequence` tag. + pub fn new(seq_num: u64) -> Self { + ExtXDiscontinuitySequence { seq_num } + } + + /// Returns the discontinuity sequence number of + /// the first media segment that appears in the associated playlist. + pub fn seq_num(self) -> u64 { + self.seq_num + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXDiscontinuitySequence { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.seq_num) + } +} + +impl FromStr for ExtXDiscontinuitySequence { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + Ok(ExtXDiscontinuitySequence { seq_num }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_discontinuity_sequence() { + let tag = ExtXDiscontinuitySequence::new(123); + let text = "#EXT-X-DISCONTINUITY-SEQUENCE:123"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs new file mode 100644 index 0000000..f8e333f --- /dev/null +++ b/src/tags/media_playlist/end_list.rs @@ -0,0 +1,44 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.3.4. EXT-X-ENDLIST] +/// +/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXEndList; +impl ExtXEndList { + pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} +impl fmt::Display for ExtXEndList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::PREFIX.fmt(f) + } +} +impl FromStr for ExtXEndList { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXEndList) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_endlist() { + let tag = ExtXEndList; + let text = "#EXT-X-ENDLIST"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs new file mode 100644 index 0000000..4bea636 --- /dev/null +++ b/src/tags/media_playlist/i_frames_only.rs @@ -0,0 +1,47 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.3.6. EXT-X-I-FRAMES-ONLY] +/// +/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXIFramesOnly; + +impl ExtXIFramesOnly { + pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V4 + } +} + +impl fmt::Display for ExtXIFramesOnly { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::PREFIX.fmt(f) + } +} + +impl FromStr for ExtXIFramesOnly { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXIFramesOnly) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_i_frames_only() { + let tag = ExtXIFramesOnly; + let text = "#EXT-X-I-FRAMES-ONLY"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V4); + } +} diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs new file mode 100644 index 0000000..99be333 --- /dev/null +++ b/src/tags/media_playlist/media_sequence.rs @@ -0,0 +1,61 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; +use trackable::error::ErrorKindExt; + +/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] +/// +/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXMediaSequence { + seq_num: u64, +} + +impl ExtXMediaSequence { + pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; + + /// Makes a new `ExtXMediaSequence` tag. + pub fn new(seq_num: u64) -> Self { + ExtXMediaSequence { seq_num } + } + + /// Returns the sequence number of the first media segment that appears in the associated playlist. + pub fn seq_num(self) -> u64 { + self.seq_num + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXMediaSequence { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.seq_num) + } +} + +impl FromStr for ExtXMediaSequence { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + Ok(ExtXMediaSequence { seq_num }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_media_sequence() { + let tag = ExtXMediaSequence::new(123); + let text = "#EXT-X-MEDIA-SEQUENCE:123"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_playlist/mod.rs b/src/tags/media_playlist/mod.rs new file mode 100644 index 0000000..af04ff9 --- /dev/null +++ b/src/tags/media_playlist/mod.rs @@ -0,0 +1,13 @@ +mod discontinuity_sequence; +mod end_list; +mod i_frames_only; +mod media_sequence; +mod playlist_type; +mod target_duration; + +pub use discontinuity_sequence::*; +pub use end_list::*; +pub use i_frames_only::*; +pub use media_sequence::*; +pub use playlist_type::*; +pub use target_duration::*; diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs new file mode 100644 index 0000000..e2a5f99 --- /dev/null +++ b/src/tags/media_playlist/playlist_type.rs @@ -0,0 +1,61 @@ +use crate::types::{PlaylistType, ProtocolVersion}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; +use trackable::error::ErrorKindExt; + +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE] +/// +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXPlaylistType { + playlist_type: PlaylistType, +} + +impl ExtXPlaylistType { + pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; + + /// Makes a new `ExtXPlaylistType` tag. + pub fn new(playlist_type: PlaylistType) -> Self { + ExtXPlaylistType { playlist_type } + } + + /// Returns the type of the associated media playlist. + pub fn playlist_type(self) -> PlaylistType { + self.playlist_type + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXPlaylistType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.playlist_type) + } +} + +impl FromStr for ExtXPlaylistType { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + Ok(ExtXPlaylistType { playlist_type }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_playlist_type() { + let tag = ExtXPlaylistType::new(PlaylistType::Vod); + let text = "#EXT-X-PLAYLIST-TYPE:VOD"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs new file mode 100644 index 0000000..a2af667 --- /dev/null +++ b/src/tags/media_playlist/target_duration.rs @@ -0,0 +1,67 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; +use trackable::error::ErrorKindExt; + +/// [4.3.3.1. EXT-X-TARGETDURATION] +/// +/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXTargetDuration { + duration: Duration, +} + +impl ExtXTargetDuration { + pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; + + /// Makes a new `ExtXTargetduration` tag. + /// + /// Note that the nanoseconds part of the `duration` will be discarded. + pub fn new(duration: Duration) -> Self { + let duration = Duration::from_secs(duration.as_secs()); + ExtXTargetDuration { duration } + } + + /// Returns the maximum media segment duration in the associated playlist. + pub fn duration(&self) -> Duration { + self.duration + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXTargetDuration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.duration.as_secs()) + } +} + +impl FromStr for ExtXTargetDuration { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + Ok(ExtXTargetDuration { + duration: Duration::from_secs(duration), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_targetduration() { + let tag = ExtXTargetDuration::new(Duration::from_secs(5)); + let text = "#EXT-X-TARGETDURATION:5"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_segment.rs b/src/tags/media_segment.rs deleted file mode 100644 index 992aa5c..0000000 --- a/src/tags/media_segment.rs +++ /dev/null @@ -1,613 +0,0 @@ -use crate::attribute::AttributePairs; -use crate::types::{ - ByteRange, DecimalFloatingPoint, DecryptionKey, ProtocolVersion, QuotedString, SingleLineString, -}; -use crate::{Error, ErrorKind, Result}; -use std::collections::BTreeMap; -use std::fmt; -use std::str::FromStr; -use std::time::Duration; -use trackable::error::ErrorKindExt; - -/// [4.3.2.1. EXTINF] -/// -/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtInf { - duration: Duration, - title: Option, -} -impl ExtInf { - pub(crate) const PREFIX: &'static str = "#EXTINF:"; - - /// Makes a new `ExtInf` tag. - pub fn new(duration: Duration) -> Self { - ExtInf { - duration, - title: None, - } - } - - /// Makes a new `ExtInf` tag with the given title. - pub fn with_title(duration: Duration, title: SingleLineString) -> Self { - ExtInf { - duration, - title: Some(title), - } - } - - /// Returns the duration of the associated media segment. - pub fn duration(&self) -> Duration { - self.duration - } - - /// Returns the title of the associated media segment. - pub fn title(&self) -> Option<&SingleLineString> { - self.title.as_ref() - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - if self.duration.subsec_nanos() == 0 { - ProtocolVersion::V1 - } else { - ProtocolVersion::V3 - } - } -} -impl fmt::Display for ExtInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - - let duration = (self.duration.as_secs() as f64) - + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); - write!(f, "{}", duration)?; - - if let Some(ref title) = self.title { - write!(f, ",{}", title)?; - } - Ok(()) - } -} -impl FromStr for ExtInf { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ','); - - let seconds: DecimalFloatingPoint = - may_invalid!(tokens.next().expect("Never fails").parse())?; - let duration = seconds.to_duration(); - - let title = if let Some(title) = tokens.next() { - Some(track!(SingleLineString::new(title))?) - } else { - None - }; - Ok(ExtInf { duration, title }) - } -} - -/// [4.3.2.2. EXT-X-BYTERANGE] -/// -/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXByteRange { - range: ByteRange, -} -impl ExtXByteRange { - pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; - - /// Makes a new `ExtXByteRange` tag. - pub fn new(range: ByteRange) -> Self { - ExtXByteRange { range } - } - - /// Returns the range of the associated media segment. - pub fn range(&self) -> ByteRange { - self.range - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V4 - } -} -impl fmt::Display for ExtXByteRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.range) - } -} -impl FromStr for ExtXByteRange { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXByteRange { range }) - } -} - -/// [4.3.2.3. EXT-X-DISCONTINUITY] -/// -/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXDiscontinuity; -impl ExtXDiscontinuity { - pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXDiscontinuity { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } -} -impl FromStr for ExtXDiscontinuity { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); - Ok(ExtXDiscontinuity) - } -} - -/// [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXKey { - key: Option, -} -impl ExtXKey { - pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; - - /// Makes a new `ExtXKey` tag. - pub fn new(key: DecryptionKey) -> Self { - ExtXKey { key: Some(key) } - } - - /// Makes a new `ExtXKey` tag without a decryption key. - /// - /// This tag has the `METHDO=NONE` attribute. - pub fn new_without_key() -> Self { - ExtXKey { key: None } - } - - /// Returns the decryption key for the following media segments and media initialization sections. - pub fn key(&self) -> Option<&DecryptionKey> { - self.key.as_ref() - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - self.key - .as_ref() - .map_or(ProtocolVersion::V1, |k| k.requires_version()) - } -} -impl fmt::Display for ExtXKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - if let Some(ref key) = self.key { - write!(f, "{}", key)?; - } else { - write!(f, "METHOD=NONE")?; - } - Ok(()) - } -} -impl FromStr for ExtXKey { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - - if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) { - for attr in AttributePairs::parse(suffix) { - let (key, _) = track!(attr)?; - track_assert_ne!(key, "URI", ErrorKind::InvalidInput); - track_assert_ne!(key, "IV", ErrorKind::InvalidInput); - track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput); - track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput); - } - Ok(ExtXKey { key: None }) - } else { - let key = track!(suffix.parse())?; - Ok(ExtXKey { key: Some(key) }) - } - } -} - -/// [4.3.2.5. EXT-X-MAP] -/// -/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXMap { - uri: QuotedString, - range: Option, -} -impl ExtXMap { - pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; - - /// Makes a new `ExtXMap` tag. - pub fn new(uri: QuotedString) -> Self { - ExtXMap { uri, range: None } - } - - /// Makes a new `ExtXMap` tag with the given range. - pub fn with_range(uri: QuotedString, range: ByteRange) -> Self { - ExtXMap { - uri, - range: Some(range), - } - } - - /// Returns the URI that identifies a resource that contains the media initialization section. - pub fn uri(&self) -> &QuotedString { - &self.uri - } - - /// Returns the range of the media initialization section. - pub fn range(&self) -> Option { - self.range - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V6 - } -} -impl fmt::Display for ExtXMap { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={}", self.uri)?; - if let Some(ref x) = self.range { - write!(f, ",BYTERANGE=\"{}\"", x)?; - } - Ok(()) - } -} -impl FromStr for ExtXMap { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - let mut uri = None; - let mut range = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "URI" => uri = Some(track!(value.parse())?), - "BYTERANGE" => { - let s: QuotedString = track!(value.parse())?; - range = Some(track!(s.parse())?); - } - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); - Ok(ExtXMap { uri, range }) - } -} - -/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] -/// -/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXProgramDateTime { - date_time: SingleLineString, -} -impl ExtXProgramDateTime { - pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; - - /// Makes a new `ExtXProgramDateTime` tag. - pub fn new(date_time: SingleLineString) -> Self { - ExtXProgramDateTime { date_time } - } - - /// Returns the date-time of the first sample of the associated media segment. - pub fn date_time(&self) -> &SingleLineString { - &self.date_time - } - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXProgramDateTime { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.date_time) - } -} -impl FromStr for ExtXProgramDateTime { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - Ok(ExtXProgramDateTime { - date_time: track!(SingleLineString::new(suffix))?, - }) - } -} - -/// [4.3.2.7. EXT-X-DATERANGE] -/// -/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 -/// -/// TODO: Implement properly -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXDateRange { - pub id: QuotedString, - pub class: Option, - pub start_date: QuotedString, - pub end_date: Option, - pub duration: Option, - pub planned_duration: Option, - pub scte35_cmd: Option, - pub scte35_out: Option, - pub scte35_in: Option, - pub end_on_next: bool, - pub client_attributes: BTreeMap, -} -impl ExtXDateRange { - pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXDateRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "ID={}", self.id)?; - if let Some(ref x) = self.class { - write!(f, ",CLASS={}", x)?; - } - write!(f, ",START-DATE={}", self.start_date)?; - if let Some(ref x) = self.end_date { - write!(f, ",END-DATE={}", x)?; - } - if let Some(x) = self.duration { - write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; - } - if let Some(x) = self.planned_duration { - write!( - f, - ",PLANNED-DURATION={}", - DecimalFloatingPoint::from_duration(x) - )?; - } - if let Some(ref x) = self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", x)?; - } - if let Some(ref x) = self.scte35_out { - write!(f, ",SCTE35-OUT={}", x)?; - } - if let Some(ref x) = self.scte35_in { - write!(f, ",SCTE35-IN={}", x)?; - } - if self.end_on_next { - write!(f, ",END-ON-NEXT=YES",)?; - } - for (k, v) in &self.client_attributes { - write!(f, ",{}={}", k, v)?; - } - Ok(()) - } -} -impl FromStr for ExtXDateRange { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - let mut id = None; - let mut class = None; - let mut start_date = None; - let mut end_date = None; - let mut duration = None; - let mut planned_duration = None; - let mut scte35_cmd = None; - let mut scte35_out = None; - let mut scte35_in = None; - let mut end_on_next = false; - let mut client_attributes = BTreeMap::new(); - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "ID" => id = Some(track!(value.parse())?), - "CLASS" => class = Some(track!(value.parse())?), - "START-DATE" => start_date = Some(track!(value.parse())?), - "END-DATE" => end_date = Some(track!(value.parse())?), - "DURATION" => { - let seconds: DecimalFloatingPoint = track!(value.parse())?; - duration = Some(seconds.to_duration()); - } - "PLANNED-DURATION" => { - let seconds: DecimalFloatingPoint = track!(value.parse())?; - planned_duration = Some(seconds.to_duration()); - } - "SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?), - "SCTE35-OUT" => scte35_out = Some(track!(value.parse())?), - "SCTE35-IN" => scte35_in = Some(track!(value.parse())?), - "END-ON-NEXT" => { - track_assert_eq!(value, "YES", ErrorKind::InvalidInput); - end_on_next = true; - } - _ => { - if key.starts_with("X-") { - client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); - } else { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - } - - let id = track_assert_some!(id, ErrorKind::InvalidInput); - let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput); - if end_on_next { - track_assert!(class.is_some(), ErrorKind::InvalidInput); - } - Ok(ExtXDateRange { - id, - class, - start_date, - end_date, - duration, - planned_duration, - scte35_cmd, - scte35_out, - scte35_in, - end_on_next, - client_attributes, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::types::{EncryptionMethod, InitializationVector}; - use std::time::Duration; - - #[test] - fn extinf() { - let tag = ExtInf::new(Duration::from_secs(5)); - assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), "#EXTINF:5"); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = ExtInf::with_title( - Duration::from_secs(5), - SingleLineString::new("foo").unwrap(), - ); - assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), "#EXTINF:5,foo"); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = ExtInf::new(Duration::from_millis(1234)); - assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), "#EXTINF:1.234"); - assert_eq!(tag.requires_version(), ProtocolVersion::V3); - } - - #[test] - fn ext_x_byterange() { - let tag = ExtXByteRange::new(ByteRange { - length: 3, - start: None, - }); - assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3"); - assert_eq!(tag.requires_version(), ProtocolVersion::V4); - - let tag = ExtXByteRange::new(ByteRange { - length: 3, - start: Some(5), - }); - assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5"); - assert_eq!(tag.requires_version(), ProtocolVersion::V4); - } - - #[test] - fn ext_x_discontinuity() { - let tag = ExtXDiscontinuity; - assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY"); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - - #[test] - fn ext_x_key() { - let tag = ExtXKey::new_without_key(); - let text = "#EXT-X-KEY:METHOD=NONE"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), - iv: None, - key_format: None, - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: None, - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V2); - - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: Some(QuotedString::new("baz").unwrap()), - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V5); - } - - #[test] - fn ext_x_map() { - let tag = ExtXMap::new(QuotedString::new("foo").unwrap()); - let text = r#"#EXT-X-MAP:URI="foo""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V6); - - let tag = ExtXMap::with_range( - QuotedString::new("foo").unwrap(), - ByteRange { - length: 9, - start: Some(2), - }, - ); - let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#; - track_try_unwrap!(ExtXMap::from_str(text)); - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V6); - } - - #[test] - fn ext_x_program_date_time() { - let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"; - assert!(text.parse::().is_ok()); - - let tag = text.parse::().unwrap(); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } -} diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs new file mode 100644 index 0000000..52397ba --- /dev/null +++ b/src/tags/media_segment/byte_range.rs @@ -0,0 +1,71 @@ +use crate::types::{ByteRange, ProtocolVersion}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; +use trackable::error::ErrorKindExt; + +/// [4.3.2.2. EXT-X-BYTERANGE] +/// +/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXByteRange { + range: ByteRange, +} + +impl ExtXByteRange { + pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; + + /// Makes a new `ExtXByteRange` tag. + pub fn new(range: ByteRange) -> Self { + ExtXByteRange { range } + } + + /// Returns the range of the associated media segment. + pub fn range(&self) -> ByteRange { + self.range + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V4 + } +} + +impl fmt::Display for ExtXByteRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.range) + } +} + +impl FromStr for ExtXByteRange { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + Ok(ExtXByteRange { range }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_byterange() { + let tag = ExtXByteRange::new(ByteRange { + length: 3, + start: None, + }); + assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3"); + assert_eq!(tag.requires_version(), ProtocolVersion::V4); + + let tag = ExtXByteRange::new(ByteRange { + length: 3, + start: Some(5), + }); + assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5"); + assert_eq!(tag.requires_version(), ProtocolVersion::V4); + } +} diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs new file mode 100644 index 0000000..2db82b6 --- /dev/null +++ b/src/tags/media_segment/date_range.rs @@ -0,0 +1,158 @@ +use crate::attribute::AttributePairs; +use crate::types::{DecimalFloatingPoint, ProtocolVersion, QuotedString}; +use crate::{Error, ErrorKind, Result}; +use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; + +/// [4.3.2.7. EXT-X-DATERANGE] +/// +/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 +/// +/// TODO: Implement properly +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXDateRange { + pub id: QuotedString, + pub class: Option, + pub start_date: QuotedString, + pub end_date: Option, + pub duration: Option, + pub planned_duration: Option, + pub scte35_cmd: Option, + pub scte35_out: Option, + pub scte35_in: Option, + pub end_on_next: bool, + pub client_attributes: BTreeMap, +} + +impl ExtXDateRange { + pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXDateRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "ID={}", self.id)?; + if let Some(ref x) = self.class { + write!(f, ",CLASS={}", x)?; + } + write!(f, ",START-DATE={}", self.start_date)?; + if let Some(ref x) = self.end_date { + write!(f, ",END-DATE={}", x)?; + } + if let Some(x) = self.duration { + write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; + } + if let Some(x) = self.planned_duration { + write!( + f, + ",PLANNED-DURATION={}", + DecimalFloatingPoint::from_duration(x) + )?; + } + if let Some(ref x) = self.scte35_cmd { + write!(f, ",SCTE35-CMD={}", x)?; + } + if let Some(ref x) = self.scte35_out { + write!(f, ",SCTE35-OUT={}", x)?; + } + if let Some(ref x) = self.scte35_in { + write!(f, ",SCTE35-IN={}", x)?; + } + if self.end_on_next { + write!(f, ",END-ON-NEXT=YES",)?; + } + for (k, v) in &self.client_attributes { + write!(f, ",{}={}", k, v)?; + } + Ok(()) + } +} + +impl FromStr for ExtXDateRange { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut id = None; + let mut class = None; + let mut start_date = None; + let mut end_date = None; + let mut duration = None; + let mut planned_duration = None; + let mut scte35_cmd = None; + let mut scte35_out = None; + let mut scte35_in = None; + let mut end_on_next = false; + let mut client_attributes = BTreeMap::new(); + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "ID" => id = Some(track!(value.parse())?), + "CLASS" => class = Some(track!(value.parse())?), + "START-DATE" => start_date = Some(track!(value.parse())?), + "END-DATE" => end_date = Some(track!(value.parse())?), + "DURATION" => { + let seconds: DecimalFloatingPoint = track!(value.parse())?; + duration = Some(seconds.to_duration()); + } + "PLANNED-DURATION" => { + let seconds: DecimalFloatingPoint = track!(value.parse())?; + planned_duration = Some(seconds.to_duration()); + } + "SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?), + "SCTE35-OUT" => scte35_out = Some(track!(value.parse())?), + "SCTE35-IN" => scte35_in = Some(track!(value.parse())?), + "END-ON-NEXT" => { + track_assert_eq!(value, "YES", ErrorKind::InvalidInput); + end_on_next = true; + } + _ => { + if key.starts_with("X-") { + client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); + } else { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + } + + let id = track_assert_some!(id, ErrorKind::InvalidInput); + let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput); + if end_on_next { + track_assert!(class.is_some(), ErrorKind::InvalidInput); + } + Ok(ExtXDateRange { + id, + class, + start_date, + end_date, + duration, + planned_duration, + scte35_cmd, + scte35_out, + scte35_in, + end_on_next, + client_attributes, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] // TODO; write some tests + fn it_works() { + + } +} diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs new file mode 100644 index 0000000..8e23325 --- /dev/null +++ b/src/tags/media_segment/discontinuity.rs @@ -0,0 +1,43 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.2.3. EXT-X-DISCONTINUITY] +/// +/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXDiscontinuity; +impl ExtXDiscontinuity { + pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} +impl fmt::Display for ExtXDiscontinuity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::PREFIX.fmt(f) + } +} +impl FromStr for ExtXDiscontinuity { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXDiscontinuity) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_discontinuity() { + let tag = ExtXDiscontinuity; + assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY"); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs new file mode 100644 index 0000000..3543f69 --- /dev/null +++ b/src/tags/media_segment/inf.rs @@ -0,0 +1,114 @@ +use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; +use trackable::error::ErrorKindExt; + +/// [4.3.2.1. EXTINF] +/// +/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtInf { + duration: Duration, + title: Option, +} + +impl ExtInf { + pub(crate) const PREFIX: &'static str = "#EXTINF:"; + + /// Makes a new `ExtInf` tag. + pub fn new(duration: Duration) -> Self { + ExtInf { + duration, + title: None, + } + } + + /// Makes a new `ExtInf` tag with the given title. + pub fn with_title(duration: Duration, title: SingleLineString) -> Self { + ExtInf { + duration, + title: Some(title), + } + } + + /// Returns the duration of the associated media segment. + pub fn duration(&self) -> Duration { + self.duration + } + + /// Returns the title of the associated media segment. + pub fn title(&self) -> Option<&SingleLineString> { + self.title.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + if self.duration.subsec_nanos() == 0 { + ProtocolVersion::V1 + } else { + ProtocolVersion::V3 + } + } +} + +impl fmt::Display for ExtInf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + + let duration = (self.duration.as_secs() as f64) + + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); + write!(f, "{}", duration)?; + + if let Some(ref title) = self.title { + write!(f, ",{}", title)?; + } + Ok(()) + } +} + +impl FromStr for ExtInf { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ','); + + let seconds: DecimalFloatingPoint = + may_invalid!(tokens.next().expect("Never fails").parse())?; + let duration = seconds.to_duration(); + + let title = if let Some(title) = tokens.next() { + Some(track!(SingleLineString::new(title))?) + } else { + None + }; + Ok(ExtInf { duration, title }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn extinf() { + let tag = ExtInf::new(Duration::from_secs(5)); + assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), "#EXTINF:5"); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtInf::with_title( + Duration::from_secs(5), + SingleLineString::new("foo").unwrap(), + ); + assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), "#EXTINF:5,foo"); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtInf::new(Duration::from_millis(1234)); + assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), "#EXTINF:1.234"); + assert_eq!(tag.requires_version(), ProtocolVersion::V3); + } +} diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs new file mode 100644 index 0000000..e9e535c --- /dev/null +++ b/src/tags/media_segment/key.rs @@ -0,0 +1,130 @@ +use crate::attribute::AttributePairs; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.2.4. EXT-X-KEY] +/// +/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXKey { + key: Option, +} + +impl ExtXKey { + pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; + + /// Makes a new `ExtXKey` tag. + pub fn new(key: DecryptionKey) -> Self { + ExtXKey { key: Some(key) } + } + + /// Makes a new `ExtXKey` tag without a decryption key. + /// + /// This tag has the `METHDO=NONE` attribute. + pub fn new_without_key() -> Self { + ExtXKey { key: None } + } + + /// Returns the decryption key for the following media segments and media initialization sections. + pub fn key(&self) -> Option<&DecryptionKey> { + self.key.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + self.key + .as_ref() + .map_or(ProtocolVersion::V1, |k| k.requires_version()) + } +} + +impl fmt::Display for ExtXKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + if let Some(ref key) = self.key { + write!(f, "{}", key)?; + } else { + write!(f, "METHOD=NONE")?; + } + Ok(()) + } +} + +impl FromStr for ExtXKey { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let suffix = s.split_at(Self::PREFIX.len()).1; + + if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) { + for attr in AttributePairs::parse(suffix) { + let (key, _) = track!(attr)?; + track_assert_ne!(key, "URI", ErrorKind::InvalidInput); + track_assert_ne!(key, "IV", ErrorKind::InvalidInput); + track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput); + track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput); + } + Ok(ExtXKey { key: None }) + } else { + let key = track!(suffix.parse())?; + Ok(ExtXKey { key: Some(key) }) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::{EncryptionMethod, InitializationVector, QuotedString}; + + #[test] + fn ext_x_key() { + let tag = ExtXKey::new_without_key(); + let text = "#EXT-X-KEY:METHOD=NONE"; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtXKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: QuotedString::new("foo").unwrap(), + iv: None, + key_format: None, + key_format_versions: None, + }); + let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtXKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: QuotedString::new("foo").unwrap(), + iv: Some(InitializationVector([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + ])), + key_format: None, + key_format_versions: None, + }); + let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V2); + + let tag = ExtXKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: QuotedString::new("foo").unwrap(), + iv: Some(InitializationVector([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + ])), + key_format: Some(QuotedString::new("baz").unwrap()), + key_format_versions: None, + }); + let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V5); + } +} diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs new file mode 100644 index 0000000..53c408c --- /dev/null +++ b/src/tags/media_segment/map.rs @@ -0,0 +1,112 @@ +use crate::attribute::AttributePairs; +use crate::types::{ByteRange, ProtocolVersion, QuotedString}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.2.5. EXT-X-MAP] +/// +/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXMap { + uri: QuotedString, + range: Option, +} + +impl ExtXMap { + pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; + + /// Makes a new `ExtXMap` tag. + pub fn new(uri: QuotedString) -> Self { + ExtXMap { uri, range: None } + } + + /// Makes a new `ExtXMap` tag with the given range. + pub fn with_range(uri: QuotedString, range: ByteRange) -> Self { + ExtXMap { + uri, + range: Some(range), + } + } + + /// Returns the URI that identifies a resource that contains the media initialization section. + pub fn uri(&self) -> &QuotedString { + &self.uri + } + + /// Returns the range of the media initialization section. + pub fn range(&self) -> Option { + self.range + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V6 + } +} + +impl fmt::Display for ExtXMap { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "URI={}", self.uri)?; + if let Some(ref x) = self.range { + write!(f, ",BYTERANGE=\"{}\"", x)?; + } + Ok(()) + } +} + +impl FromStr for ExtXMap { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut uri = None; + let mut range = None; + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "URI" => uri = Some(track!(value.parse())?), + "BYTERANGE" => { + let s: QuotedString = track!(value.parse())?; + range = Some(track!(s.parse())?); + } + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + Ok(ExtXMap { uri, range }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_map() { + let tag = ExtXMap::new(QuotedString::new("foo").unwrap()); + let text = r#"#EXT-X-MAP:URI="foo""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V6); + + let tag = ExtXMap::with_range( + QuotedString::new("foo").unwrap(), + ByteRange { + length: 9, + start: Some(2), + }, + ); + let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#; + track_try_unwrap!(ExtXMap::from_str(text)); + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V6); + } +} diff --git a/src/tags/media_segment/mod.rs b/src/tags/media_segment/mod.rs new file mode 100644 index 0000000..c5a0a5c --- /dev/null +++ b/src/tags/media_segment/mod.rs @@ -0,0 +1,15 @@ +mod inf; +mod byte_range; +mod date_range; +mod key; +mod map; +mod program_date_time; +mod discontinuity; + +pub use inf::*; +pub use byte_range::*; +pub use date_range::*; +pub use key::*; +pub use map::*; +pub use program_date_time::*; +pub use discontinuity::*; diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs new file mode 100644 index 0000000..b694c93 --- /dev/null +++ b/src/tags/media_segment/program_date_time.rs @@ -0,0 +1,63 @@ +use crate::types::{ProtocolVersion, SingleLineString}; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// +/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXProgramDateTime { + date_time: SingleLineString, +} + +impl ExtXProgramDateTime { + pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; + + /// Makes a new `ExtXProgramDateTime` tag. + pub fn new(date_time: SingleLineString) -> Self { + ExtXProgramDateTime { date_time } + } + + /// Returns the date-time of the first sample of the associated media segment. + pub fn date_time(&self) -> &SingleLineString { + &self.date_time + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXProgramDateTime { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.date_time) + } +} + +impl FromStr for ExtXProgramDateTime { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let suffix = s.split_at(Self::PREFIX.len()).1; + Ok(ExtXProgramDateTime { + date_time: track!(SingleLineString::new(suffix))?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_program_date_time() { + let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"; + assert!(text.parse::().is_ok()); + + let tag = text.parse::().unwrap(); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/mod.rs b/src/tags/mod.rs index 11db0a6..b4a72fa 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -1,8 +1,6 @@ //! [4.3. Playlist Tags] //! //! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3 -use crate::{ErrorKind, Result}; -use trackable::error::ErrorKindExt; macro_rules! may_invalid { ($expr:expr) => { @@ -20,24 +18,17 @@ macro_rules! impl_from { }; } -pub use self::basic::{ExtM3u, ExtXVersion}; -pub use self::master_playlist::{ - ExtXIFrameStreamInf, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStreamInf, -}; -pub use self::media_or_master_playlist::{ExtXIndependentSegments, ExtXStart}; -pub use self::media_playlist::{ - ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXMediaSequence, ExtXPlaylistType, - ExtXTargetDuration, -}; -pub use self::media_segment::{ - ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, -}; - mod basic; mod master_playlist; -mod media_or_master_playlist; mod media_playlist; mod media_segment; +mod shared; + +pub use basic::*; +pub use master_playlist::*; +pub use media_playlist::*; +pub use media_segment::*; +pub use shared::*; /// [4.3.4. Master Playlist Tags] /// @@ -114,16 +105,3 @@ impl_from!(MediaSegmentTag, ExtXDiscontinuity); impl_from!(MediaSegmentTag, ExtXKey); impl_from!(MediaSegmentTag, ExtXMap); impl_from!(MediaSegmentTag, ExtXProgramDateTime); - -fn parse_yes_or_no(s: &str) -> Result { - match s { - "YES" => Ok(true), - "NO" => Ok(false), - _ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s), - } -} - -fn parse_u64(s: &str) -> Result { - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - Ok(n) -} diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs new file mode 100644 index 0000000..d4916f9 --- /dev/null +++ b/src/tags/shared/independent_segments.rs @@ -0,0 +1,46 @@ +use crate::types::ProtocolVersion; +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::FromStr; + +/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] +/// +/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXIndependentSegments; +impl ExtXIndependentSegments { + pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} + +impl fmt::Display for ExtXIndependentSegments { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::PREFIX.fmt(f) + } +} + +impl FromStr for ExtXIndependentSegments { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXIndependentSegments) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ext_x_independent_segments() { + let tag = ExtXIndependentSegments; + let text = "#EXT-X-INDEPENDENT-SEGMENTS"; + assert_eq!(text.parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tags/shared/mod.rs b/src/tags/shared/mod.rs new file mode 100644 index 0000000..5811164 --- /dev/null +++ b/src/tags/shared/mod.rs @@ -0,0 +1,5 @@ +mod independent_segments; +mod start; + +pub use start::*; +pub use independent_segments::*; diff --git a/src/tags/media_or_master_playlist.rs b/src/tags/shared/start.rs similarity index 74% rename from src/tags/media_or_master_playlist.rs rename to src/tags/shared/start.rs index 9b6ccb2..c9afac5 100644 --- a/src/tags/media_or_master_playlist.rs +++ b/src/tags/shared/start.rs @@ -1,36 +1,10 @@ -use super::parse_yes_or_no; +use crate::utils::parse_yes_or_no; use crate::attribute::AttributePairs; use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] -/// -/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXIndependentSegments; -impl ExtXIndependentSegments { - pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS"; - - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { - ProtocolVersion::V1 - } -} -impl fmt::Display for ExtXIndependentSegments { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } -} -impl FromStr for ExtXIndependentSegments { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); - Ok(ExtXIndependentSegments) - } -} - /// [4.3.5.2. EXT-X-START] /// /// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 @@ -39,6 +13,7 @@ pub struct ExtXStart { time_offset: SignedDecimalFloatingPoint, precise: bool, } + impl ExtXStart { pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; @@ -74,6 +49,7 @@ impl ExtXStart { ProtocolVersion::V1 } } + impl fmt::Display for ExtXStart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; @@ -84,6 +60,7 @@ impl fmt::Display for ExtXStart { Ok(()) } } + impl FromStr for ExtXStart { type Err = Error; fn from_str(s: &str) -> Result { @@ -116,15 +93,6 @@ impl FromStr for ExtXStart { mod test { use super::*; - #[test] - fn ext_x_independent_segments() { - let tag = ExtXIndependentSegments; - let text = "#EXT-X-INDEPENDENT-SEGMENTS"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - } - #[test] fn ext_x_start() { let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap()); diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b313c66 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,15 @@ +use crate::{ErrorKind, Result}; +use trackable::error::ErrorKindExt; + +pub fn parse_yes_or_no(s: &str) -> Result { + match s { + "YES" => Ok(true), + "NO" => Ok(false), + _ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s), + } +} + +pub fn parse_u64(s: &str) -> Result { + let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + Ok(n) +} From 5da2fa8104ece58cddb6033389f673571b872563 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 6 Sep 2019 13:20:40 +0200 Subject: [PATCH 2/4] move types into their own files --- src/types.rs | 840 --------------------- src/types/byte_range.rs | 45 ++ src/types/closed_captions.rs | 36 + src/types/decimal_floating_point.rs | 70 ++ src/types/decimal_resolution.rs | 37 + src/types/decryption_key.rs | 84 +++ src/types/encryption_method.rs | 39 + src/types/hdcp_level.rs | 35 + src/types/hexadecimal_sequence.rs | 68 ++ src/types/in_stream_id.rs | 162 ++++ src/types/initialization_vector.rs | 56 ++ src/types/media_type.rs | 41 + src/types/mod.rs | 36 + src/types/playlist_type.rs | 35 + src/types/protocol_version.rs | 47 ++ src/types/quoted_string.rs | 62 ++ src/types/session_data.rs | 13 + src/types/signed_decimal_floating_point.rs | 56 ++ src/types/single_line_string.rs | 55 ++ 19 files changed, 977 insertions(+), 840 deletions(-) delete mode 100644 src/types.rs create mode 100644 src/types/byte_range.rs create mode 100644 src/types/closed_captions.rs create mode 100644 src/types/decimal_floating_point.rs create mode 100644 src/types/decimal_resolution.rs create mode 100644 src/types/decryption_key.rs create mode 100644 src/types/encryption_method.rs create mode 100644 src/types/hdcp_level.rs create mode 100644 src/types/hexadecimal_sequence.rs create mode 100644 src/types/in_stream_id.rs create mode 100644 src/types/initialization_vector.rs create mode 100644 src/types/media_type.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/playlist_type.rs create mode 100644 src/types/protocol_version.rs create mode 100644 src/types/quoted_string.rs create mode 100644 src/types/session_data.rs create mode 100644 src/types/signed_decimal_floating_point.rs create mode 100644 src/types/single_line_string.rs diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 43aed53..0000000 --- a/src/types.rs +++ /dev/null @@ -1,840 +0,0 @@ -//! Miscellaneous types. -use crate::attribute::AttributePairs; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::ops::Deref; -use std::str::{self, FromStr}; -use std::time::Duration; -use trackable::error::ErrorKindExt; - -/// String that represents a single line in a playlist file. -/// -/// See: [4.1. Definition of a Playlist] -/// -/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SingleLineString(String); -impl SingleLineString { - /// Makes a new `SingleLineString` instance. - /// - /// # Errors - /// - /// If the given string contains any control characters, - /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new>(s: T) -> Result { - let s = s.into(); - track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput); - Ok(SingleLineString(s)) - } -} -impl Deref for SingleLineString { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl AsRef for SingleLineString { - fn as_ref(&self) -> &str { - &self.0 - } -} -impl fmt::Display for SingleLineString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Quoted string. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct QuotedString(String); -impl QuotedString { - /// Makes a new `QuotedString` instance. - /// - /// # Errors - /// - /// If the given string contains any control characters or double-quote character, - /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new>(s: T) -> Result { - let s = s.into(); - track_assert!( - !s.chars().any(|c| c.is_control() || c == '"'), - ErrorKind::InvalidInput - ); - Ok(QuotedString(s)) - } -} -impl Deref for QuotedString { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl AsRef for QuotedString { - fn as_ref(&self) -> &str { - &self.0 - } -} -impl fmt::Display for QuotedString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} -impl FromStr for QuotedString { - type Err = Error; - fn from_str(s: &str) -> Result { - let len = s.len(); - let bytes = s.as_bytes(); - track_assert!(len >= 2, ErrorKind::InvalidInput); - track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput); - track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput); - - let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) }; - track!(QuotedString::new(s)) - } -} - -/// Decimal resolution. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DecimalResolution { - /// Horizontal pixel dimension. - pub width: usize, - - /// Vertical pixel dimension. - pub height: usize, -} -impl fmt::Display for DecimalResolution { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}x{}", self.width, self.height) - } -} -impl FromStr for DecimalResolution { - type Err = Error; - fn from_str(s: &str) -> Result { - let mut tokens = s.splitn(2, 'x'); - let width = tokens.next().expect("Never fails"); - let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput); - Ok(DecimalResolution { - width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - }) - } -} - -/// Non-negative decimal floating-point number. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct DecimalFloatingPoint(f64); -impl DecimalFloatingPoint { - /// Makes a new `DecimalFloatingPoint` instance. - /// - /// # Errors - /// - /// The given value must have a positive sign and be finite, - /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. - pub fn new(n: f64) -> Result { - track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput); - track_assert!(n.is_finite(), ErrorKind::InvalidInput); - Ok(DecimalFloatingPoint(n)) - } - - /// Converts `DecimalFloatingPoint` to `f64`. - pub fn as_f64(self) -> f64 { - self.0 - } - - pub(crate) fn to_duration(self) -> Duration { - let secs = self.0 as u64; - let nanos = (self.0.fract() * 1_000_000_000.0) as u32; - Duration::new(secs, nanos) - } - - pub(crate) fn from_duration(duration: Duration) -> Self { - let n = - (duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0); - DecimalFloatingPoint(n) - } -} -impl From for DecimalFloatingPoint { - fn from(f: u32) -> Self { - DecimalFloatingPoint(f64::from(f)) - } -} -impl Eq for DecimalFloatingPoint {} -impl fmt::Display for DecimalFloatingPoint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} -impl FromStr for DecimalFloatingPoint { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.chars().all(|c| c.is_digit(10) || c == '.'), - ErrorKind::InvalidInput - ); - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - Ok(DecimalFloatingPoint(n)) - } -} - -/// Signed decimal floating-point number. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct SignedDecimalFloatingPoint(f64); -impl SignedDecimalFloatingPoint { - /// Makes a new `SignedDecimalFloatingPoint` instance. - /// - /// # Errors - /// - /// The given value must be finite, - /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. - pub fn new(n: f64) -> Result { - track_assert!(n.is_finite(), ErrorKind::InvalidInput); - Ok(SignedDecimalFloatingPoint(n)) - } - - /// Converts `DecimalFloatingPoint` to `f64`. - pub fn as_f64(self) -> f64 { - self.0 - } -} -impl From for SignedDecimalFloatingPoint { - fn from(f: i32) -> Self { - SignedDecimalFloatingPoint(f64::from(f)) - } -} -impl Eq for SignedDecimalFloatingPoint {} -impl fmt::Display for SignedDecimalFloatingPoint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} -impl FromStr for SignedDecimalFloatingPoint { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'), - ErrorKind::InvalidInput - ); - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - Ok(SignedDecimalFloatingPoint(n)) - } -} - -/// Hexadecimal sequence. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct HexadecimalSequence(Vec); -impl HexadecimalSequence { - /// Makes a new `HexadecimalSequence` instance. - pub fn new>>(v: T) -> Self { - HexadecimalSequence(v.into()) - } - - /// Converts into the underlying byte sequence. - pub fn into_bytes(self) -> Vec { - self.0 - } -} -impl Deref for HexadecimalSequence { - type Target = [u8]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl AsRef<[u8]> for HexadecimalSequence { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} -impl fmt::Display for HexadecimalSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "0x")?; - for b in &self.0 { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} -impl FromStr for HexadecimalSequence { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.starts_with("0x") || s.starts_with("0X"), - ErrorKind::InvalidInput - ); - track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput); - - let mut v = Vec::with_capacity(s.len() / 2 - 1); - for c in s.as_bytes().chunks(2).skip(1) { - let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - let b = - track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - v.push(b); - } - Ok(HexadecimalSequence(v)) - } -} - -/// Initialization vector. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InitializationVector(pub [u8; 16]); -impl Deref for InitializationVector { - type Target = [u8]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl AsRef<[u8]> for InitializationVector { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} -impl fmt::Display for InitializationVector { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "0x")?; - for b in &self.0 { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} -impl FromStr for InitializationVector { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.starts_with("0x") || s.starts_with("0X"), - ErrorKind::InvalidInput - ); - track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput); - - let mut v = [0; 16]; - for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() { - let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - let b = - track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - v[i] = b; - } - Ok(InitializationVector(v)) - } -} - -/// [7. Protocol Version Compatibility] -/// -/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ProtocolVersion { - V1, - V2, - V3, - V4, - V5, - V6, - V7, -} -impl fmt::Display for ProtocolVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let n = match *self { - ProtocolVersion::V1 => 1, - ProtocolVersion::V2 => 2, - ProtocolVersion::V3 => 3, - ProtocolVersion::V4 => 4, - ProtocolVersion::V5 => 5, - ProtocolVersion::V6 => 6, - ProtocolVersion::V7 => 7, - }; - write!(f, "{}", n) - } -} -impl FromStr for ProtocolVersion { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(match s { - "1" => ProtocolVersion::V1, - "2" => ProtocolVersion::V2, - "3" => ProtocolVersion::V3, - "4" => ProtocolVersion::V4, - "5" => ProtocolVersion::V5, - "6" => ProtocolVersion::V6, - "7" => ProtocolVersion::V7, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s), - }) - } -} - -/// Byte range. -/// -/// See: [4.3.2.2. EXT-X-BYTERANGE] -/// -/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ByteRange { - pub length: usize, - pub start: Option, -} -impl fmt::Display for ByteRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.length)?; - if let Some(x) = self.start { - write!(f, "@{}", x)?; - } - Ok(()) - } -} -impl FromStr for ByteRange { - type Err = Error; - fn from_str(s: &str) -> Result { - let mut tokens = s.splitn(2, '@'); - let length = tokens.next().expect("Never fails"); - let start = if let Some(start) = tokens.next() { - Some(track!(start - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e)))?) - } else { - None - }; - Ok(ByteRange { - length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - start, - }) - } -} - -/// Decryption key. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct DecryptionKey { - pub method: EncryptionMethod, - pub uri: QuotedString, - pub iv: Option, - pub key_format: Option, - pub key_format_versions: Option, -} -impl DecryptionKey { - pub(crate) fn requires_version(&self) -> ProtocolVersion { - if self.key_format.is_some() | self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} -impl fmt::Display for DecryptionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "METHOD={}", self.method)?; - write!(f, ",URI={}", self.uri)?; - if let Some(ref x) = self.iv { - write!(f, ",IV={}", x)?; - } - if let Some(ref x) = self.key_format { - write!(f, ",KEYFORMAT={}", x)?; - } - if let Some(ref x) = self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", x)?; - } - Ok(()) - } -} -impl FromStr for DecryptionKey { - type Err = Error; - fn from_str(s: &str) -> Result { - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; - let attrs = AttributePairs::parse(s); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "METHOD" => method = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), - "IV" => iv = Some(track!(value.parse())?), - "KEYFORMAT" => key_format = Some(track!(value.parse())?), - "KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - let method = track_assert_some!(method, ErrorKind::InvalidInput); - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); - Ok(DecryptionKey { - method, - uri, - iv, - key_format, - key_format_versions, - }) - } -} - -/// Encryption method. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum EncryptionMethod { - Aes128, - SampleAes, -} -impl fmt::Display for EncryptionMethod { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - EncryptionMethod::Aes128 => "AES-128".fmt(f), - EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), - } - } -} -impl FromStr for EncryptionMethod { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "AES-128" => Ok(EncryptionMethod::Aes128), - "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), - _ => track_panic!( - ErrorKind::InvalidInput, - "Unknown encryption method: {:?}", - s - ), - } - } -} - -/// Playlist type. -/// -/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE] -/// -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PlaylistType { - Event, - Vod, -} -impl fmt::Display for PlaylistType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - PlaylistType::Event => write!(f, "EVENT"), - PlaylistType::Vod => write!(f, "VOD"), - } - } -} -impl FromStr for PlaylistType { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "EVENT" => Ok(PlaylistType::Event), - "VOD" => Ok(PlaylistType::Vod), - _ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s), - } - } -} - -/// Media type. -/// -/// See: [4.3.4.1. EXT-X-MEDIA] -/// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MediaType { - Audio, - Video, - Subtitles, - ClosedCaptions, -} -impl fmt::Display for MediaType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - MediaType::Audio => "AUDIO".fmt(f), - MediaType::Video => "VIDEO".fmt(f), - MediaType::Subtitles => "SUBTITLES".fmt(f), - MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f), - } - } -} -impl FromStr for MediaType { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(match s { - "AUDIO" => MediaType::Audio, - "VIDEO" => MediaType::Video, - "SUBTITLES" => MediaType::Subtitles, - "CLOSED-CAPTIONS" => MediaType::ClosedCaptions, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s), - }) - } -} - -/// Identifier of a rendition within the segments in a media playlist. -/// -/// See: [4.3.4.1. EXT-X-MEDIA] -/// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum InStreamId { - Cc1, - Cc2, - Cc3, - Cc4, - Service1, - Service2, - Service3, - Service4, - Service5, - Service6, - Service7, - Service8, - Service9, - Service10, - Service11, - Service12, - Service13, - Service14, - Service15, - Service16, - Service17, - Service18, - Service19, - Service20, - Service21, - Service22, - Service23, - Service24, - Service25, - Service26, - Service27, - Service28, - Service29, - Service30, - Service31, - Service32, - Service33, - Service34, - Service35, - Service36, - Service37, - Service38, - Service39, - Service40, - Service41, - Service42, - Service43, - Service44, - Service45, - Service46, - Service47, - Service48, - Service49, - Service50, - Service51, - Service52, - Service53, - Service54, - Service55, - Service56, - Service57, - Service58, - Service59, - Service60, - Service61, - Service62, - Service63, -} -impl fmt::Display for InStreamId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - format!("{:?}", self).to_uppercase().fmt(f) - } -} -impl FromStr for InStreamId { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(match s { - "CC1" => InStreamId::Cc1, - "CC2" => InStreamId::Cc2, - "CC3" => InStreamId::Cc3, - "CC4" => InStreamId::Cc4, - "SERVICE1" => InStreamId::Service1, - "SERVICE2" => InStreamId::Service2, - "SERVICE3" => InStreamId::Service3, - "SERVICE4" => InStreamId::Service4, - "SERVICE5" => InStreamId::Service5, - "SERVICE6" => InStreamId::Service6, - "SERVICE7" => InStreamId::Service7, - "SERVICE8" => InStreamId::Service8, - "SERVICE9" => InStreamId::Service9, - "SERVICE10" => InStreamId::Service10, - "SERVICE11" => InStreamId::Service11, - "SERVICE12" => InStreamId::Service12, - "SERVICE13" => InStreamId::Service13, - "SERVICE14" => InStreamId::Service14, - "SERVICE15" => InStreamId::Service15, - "SERVICE16" => InStreamId::Service16, - "SERVICE17" => InStreamId::Service17, - "SERVICE18" => InStreamId::Service18, - "SERVICE19" => InStreamId::Service19, - "SERVICE20" => InStreamId::Service20, - "SERVICE21" => InStreamId::Service21, - "SERVICE22" => InStreamId::Service22, - "SERVICE23" => InStreamId::Service23, - "SERVICE24" => InStreamId::Service24, - "SERVICE25" => InStreamId::Service25, - "SERVICE26" => InStreamId::Service26, - "SERVICE27" => InStreamId::Service27, - "SERVICE28" => InStreamId::Service28, - "SERVICE29" => InStreamId::Service29, - "SERVICE30" => InStreamId::Service30, - "SERVICE31" => InStreamId::Service31, - "SERVICE32" => InStreamId::Service32, - "SERVICE33" => InStreamId::Service33, - "SERVICE34" => InStreamId::Service34, - "SERVICE35" => InStreamId::Service35, - "SERVICE36" => InStreamId::Service36, - "SERVICE37" => InStreamId::Service37, - "SERVICE38" => InStreamId::Service38, - "SERVICE39" => InStreamId::Service39, - "SERVICE40" => InStreamId::Service40, - "SERVICE41" => InStreamId::Service41, - "SERVICE42" => InStreamId::Service42, - "SERVICE43" => InStreamId::Service43, - "SERVICE44" => InStreamId::Service44, - "SERVICE45" => InStreamId::Service45, - "SERVICE46" => InStreamId::Service46, - "SERVICE47" => InStreamId::Service47, - "SERVICE48" => InStreamId::Service48, - "SERVICE49" => InStreamId::Service49, - "SERVICE50" => InStreamId::Service50, - "SERVICE51" => InStreamId::Service51, - "SERVICE52" => InStreamId::Service52, - "SERVICE53" => InStreamId::Service53, - "SERVICE54" => InStreamId::Service54, - "SERVICE55" => InStreamId::Service55, - "SERVICE56" => InStreamId::Service56, - "SERVICE57" => InStreamId::Service57, - "SERVICE58" => InStreamId::Service58, - "SERVICE59" => InStreamId::Service59, - "SERVICE60" => InStreamId::Service60, - "SERVICE61" => InStreamId::Service61, - "SERVICE62" => InStreamId::Service62, - "SERVICE63" => InStreamId::Service63, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s), - }) - } -} - -/// HDCP level. -/// -/// See: [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum HdcpLevel { - Type0, - None, -} -impl fmt::Display for HdcpLevel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - HdcpLevel::Type0 => "TYPE-0".fmt(f), - HdcpLevel::None => "NONE".fmt(f), - } - } -} -impl FromStr for HdcpLevel { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "TYPE-0" => Ok(HdcpLevel::Type0), - "NONE" => Ok(HdcpLevel::None), - _ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s), - } - } -} - -/// The identifier of a closed captions group or its absence. -/// -/// See: [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ClosedCaptions { - GroupId(QuotedString), - None, -} -impl fmt::Display for ClosedCaptions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ClosedCaptions::GroupId(ref x) => x.fmt(f), - ClosedCaptions::None => "NONE".fmt(f), - } - } -} -impl FromStr for ClosedCaptions { - type Err = Error; - fn from_str(s: &str) -> Result { - if s == "NONE" { - Ok(ClosedCaptions::None) - } else { - Ok(ClosedCaptions::GroupId(track!(s.parse())?)) - } - } -} - -/// Session data. -/// -/// See: [4.3.4.4. EXT-X-SESSION-DATA] -/// -/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum SessionData { - Value(QuotedString), - Uri(QuotedString), -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn single_line_string() { - assert!(SingleLineString::new("foo").is_ok()); - assert!(SingleLineString::new("b\rar").is_err()); - } -} diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs new file mode 100644 index 0000000..b8db803 --- /dev/null +++ b/src/types/byte_range.rs @@ -0,0 +1,45 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; +use trackable::error::ErrorKindExt; + +/// Byte range. +/// +/// See: [4.3.2.2. EXT-X-BYTERANGE] +/// +/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ByteRange { + pub length: usize, + pub start: Option, +} + +impl fmt::Display for ByteRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.length)?; + if let Some(x) = self.start { + write!(f, "@{}", x)?; + } + Ok(()) + } +} + +impl FromStr for ByteRange { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut tokens = s.splitn(2, '@'); + let length = tokens.next().expect("Never fails"); + let start = if let Some(start) = tokens.next() { + Some(track!(start + .parse() + .map_err(|e| ErrorKind::InvalidInput.cause(e)))?) + } else { + None + }; + Ok(ByteRange { + length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, + start, + }) + } +} diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs new file mode 100644 index 0000000..6563ed4 --- /dev/null +++ b/src/types/closed_captions.rs @@ -0,0 +1,36 @@ +use crate::{Error, Result}; +use std::fmt; +use std::str::{self, FromStr}; +use crate::types::QuotedString; + +/// The identifier of a closed captions group or its absence. +/// +/// See: [4.3.4.2. EXT-X-STREAM-INF] +/// +/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ClosedCaptions { + GroupId(QuotedString), + None, +} + +impl fmt::Display for ClosedCaptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ClosedCaptions::GroupId(ref x) => x.fmt(f), + ClosedCaptions::None => "NONE".fmt(f), + } + } +} + +impl FromStr for ClosedCaptions { + type Err = Error; + fn from_str(s: &str) -> Result { + if s == "NONE" { + Ok(ClosedCaptions::None) + } else { + Ok(ClosedCaptions::GroupId(track!(s.parse())?)) + } + } +} diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs new file mode 100644 index 0000000..08c49fe --- /dev/null +++ b/src/types/decimal_floating_point.rs @@ -0,0 +1,70 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; +use std::time::Duration; +use trackable::error::ErrorKindExt; + +/// Non-negative decimal floating-point number. +/// +/// See: [4.2. Attribute Lists] +/// +/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct DecimalFloatingPoint(f64); + +impl DecimalFloatingPoint { + /// Makes a new `DecimalFloatingPoint` instance. + /// + /// # Errors + /// + /// The given value must have a positive sign and be finite, + /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. + pub fn new(n: f64) -> Result { + track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput); + track_assert!(n.is_finite(), ErrorKind::InvalidInput); + Ok(DecimalFloatingPoint(n)) + } + + /// Converts `DecimalFloatingPoint` to `f64`. + pub fn as_f64(self) -> f64 { + self.0 + } + + pub(crate) fn to_duration(self) -> Duration { + let secs = self.0 as u64; + let nanos = (self.0.fract() * 1_000_000_000.0) as u32; + Duration::new(secs, nanos) + } + + pub(crate) fn from_duration(duration: Duration) -> Self { + let n = + (duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0); + DecimalFloatingPoint(n) + } +} + +impl From for DecimalFloatingPoint { + fn from(f: u32) -> Self { + DecimalFloatingPoint(f64::from(f)) + } +} + +impl Eq for DecimalFloatingPoint {} + +impl fmt::Display for DecimalFloatingPoint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl FromStr for DecimalFloatingPoint { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!( + s.chars().all(|c| c.is_digit(10) || c == '.'), + ErrorKind::InvalidInput + ); + let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + Ok(DecimalFloatingPoint(n)) + } +} diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs new file mode 100644 index 0000000..e340451 --- /dev/null +++ b/src/types/decimal_resolution.rs @@ -0,0 +1,37 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; +use trackable::error::ErrorKindExt; + +/// Decimal resolution. +/// +/// See: [4.2. Attribute Lists] +/// +/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DecimalResolution { + /// Horizontal pixel dimension. + pub width: usize, + + /// Vertical pixel dimension. + pub height: usize, +} + +impl fmt::Display for DecimalResolution { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}x{}", self.width, self.height) + } +} + +impl FromStr for DecimalResolution { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut tokens = s.splitn(2, 'x'); + let width = tokens.next().expect("Never fails"); + let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput); + Ok(DecimalResolution { + width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, + height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, + }) + } +} diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs new file mode 100644 index 0000000..8b30328 --- /dev/null +++ b/src/types/decryption_key.rs @@ -0,0 +1,84 @@ +use crate::attribute::AttributePairs; +use crate::{Error, ErrorKind, Result}; +use crate::types::{QuotedString, InitializationVector, ProtocolVersion, EncryptionMethod}; +use std::fmt; +use std::str::{self, FromStr}; + +/// Decryption key. +/// +/// See: [4.3.2.4. EXT-X-KEY] +/// +/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DecryptionKey { + pub method: EncryptionMethod, + pub uri: QuotedString, + pub iv: Option, + pub key_format: Option, + pub key_format_versions: Option, +} + +impl DecryptionKey { + pub(crate) fn requires_version(&self) -> ProtocolVersion { + if self.key_format.is_some() | self.key_format_versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } + } +} + +impl fmt::Display for DecryptionKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "METHOD={}", self.method)?; + write!(f, ",URI={}", self.uri)?; + if let Some(ref x) = self.iv { + write!(f, ",IV={}", x)?; + } + if let Some(ref x) = self.key_format { + write!(f, ",KEYFORMAT={}", x)?; + } + if let Some(ref x) = self.key_format_versions { + write!(f, ",KEYFORMATVERSIONS={}", x)?; + } + Ok(()) + } +} + +impl FromStr for DecryptionKey { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut key_format = None; + let mut key_format_versions = None; + let attrs = AttributePairs::parse(s); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "METHOD" => method = Some(track!(value.parse())?), + "URI" => uri = Some(track!(value.parse())?), + "IV" => iv = Some(track!(value.parse())?), + "KEYFORMAT" => key_format = Some(track!(value.parse())?), + "KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + let method = track_assert_some!(method, ErrorKind::InvalidInput); + let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + Ok(DecryptionKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) + } +} diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs new file mode 100644 index 0000000..edd2911 --- /dev/null +++ b/src/types/encryption_method.rs @@ -0,0 +1,39 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; + +/// Encryption method. +/// +/// See: [4.3.2.4. EXT-X-KEY] +/// +/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EncryptionMethod { + Aes128, + SampleAes, +} + +impl fmt::Display for EncryptionMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + EncryptionMethod::Aes128 => "AES-128".fmt(f), + EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), + } + } +} + +impl FromStr for EncryptionMethod { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "AES-128" => Ok(EncryptionMethod::Aes128), + "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), + _ => track_panic!( + ErrorKind::InvalidInput, + "Unknown encryption method: {:?}", + s + ), + } + } +} diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs new file mode 100644 index 0000000..dacec45 --- /dev/null +++ b/src/types/hdcp_level.rs @@ -0,0 +1,35 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; + +/// HDCP level. +/// +/// See: [4.3.4.2. EXT-X-STREAM-INF] +/// +/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HdcpLevel { + Type0, + None, +} + +impl fmt::Display for HdcpLevel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + HdcpLevel::Type0 => "TYPE-0".fmt(f), + HdcpLevel::None => "NONE".fmt(f), + } + } +} + +impl FromStr for HdcpLevel { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "TYPE-0" => Ok(HdcpLevel::Type0), + "NONE" => Ok(HdcpLevel::None), + _ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s), + } + } +} diff --git a/src/types/hexadecimal_sequence.rs b/src/types/hexadecimal_sequence.rs new file mode 100644 index 0000000..930adca --- /dev/null +++ b/src/types/hexadecimal_sequence.rs @@ -0,0 +1,68 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::ops::Deref; +use std::str::{self, FromStr}; +use trackable::error::ErrorKindExt; + +/// Hexadecimal sequence. +/// +/// See: [4.2. Attribute Lists] +/// +/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HexadecimalSequence(Vec); + +impl HexadecimalSequence { + /// Makes a new `HexadecimalSequence` instance. + pub fn new>>(v: T) -> Self { + HexadecimalSequence(v.into()) + } + + /// Converts into the underlying byte sequence. + pub fn into_bytes(self) -> Vec { + self.0 + } +} + +impl Deref for HexadecimalSequence { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for HexadecimalSequence { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for HexadecimalSequence { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "0x")?; + for b in &self.0 { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +impl FromStr for HexadecimalSequence { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!( + s.starts_with("0x") || s.starts_with("0X"), + ErrorKind::InvalidInput + ); + track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput); + + let mut v = Vec::with_capacity(s.len() / 2 - 1); + for c in s.as_bytes().chunks(2).skip(1) { + let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + let b = + track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + v.push(b); + } + Ok(HexadecimalSequence(v)) + } +} diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs new file mode 100644 index 0000000..8b4aa56 --- /dev/null +++ b/src/types/in_stream_id.rs @@ -0,0 +1,162 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; + +/// Identifier of a rendition within the segments in a media playlist. +/// +/// See: [4.3.4.1. EXT-X-MEDIA] +/// +/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InStreamId { + Cc1, + Cc2, + Cc3, + Cc4, + Service1, + Service2, + Service3, + Service4, + Service5, + Service6, + Service7, + Service8, + Service9, + Service10, + Service11, + Service12, + Service13, + Service14, + Service15, + Service16, + Service17, + Service18, + Service19, + Service20, + Service21, + Service22, + Service23, + Service24, + Service25, + Service26, + Service27, + Service28, + Service29, + Service30, + Service31, + Service32, + Service33, + Service34, + Service35, + Service36, + Service37, + Service38, + Service39, + Service40, + Service41, + Service42, + Service43, + Service44, + Service45, + Service46, + Service47, + Service48, + Service49, + Service50, + Service51, + Service52, + Service53, + Service54, + Service55, + Service56, + Service57, + Service58, + Service59, + Service60, + Service61, + Service62, + Service63, +} + +impl fmt::Display for InStreamId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + format!("{:?}", self).to_uppercase().fmt(f) + } +} + +impl FromStr for InStreamId { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "CC1" => InStreamId::Cc1, + "CC2" => InStreamId::Cc2, + "CC3" => InStreamId::Cc3, + "CC4" => InStreamId::Cc4, + "SERVICE1" => InStreamId::Service1, + "SERVICE2" => InStreamId::Service2, + "SERVICE3" => InStreamId::Service3, + "SERVICE4" => InStreamId::Service4, + "SERVICE5" => InStreamId::Service5, + "SERVICE6" => InStreamId::Service6, + "SERVICE7" => InStreamId::Service7, + "SERVICE8" => InStreamId::Service8, + "SERVICE9" => InStreamId::Service9, + "SERVICE10" => InStreamId::Service10, + "SERVICE11" => InStreamId::Service11, + "SERVICE12" => InStreamId::Service12, + "SERVICE13" => InStreamId::Service13, + "SERVICE14" => InStreamId::Service14, + "SERVICE15" => InStreamId::Service15, + "SERVICE16" => InStreamId::Service16, + "SERVICE17" => InStreamId::Service17, + "SERVICE18" => InStreamId::Service18, + "SERVICE19" => InStreamId::Service19, + "SERVICE20" => InStreamId::Service20, + "SERVICE21" => InStreamId::Service21, + "SERVICE22" => InStreamId::Service22, + "SERVICE23" => InStreamId::Service23, + "SERVICE24" => InStreamId::Service24, + "SERVICE25" => InStreamId::Service25, + "SERVICE26" => InStreamId::Service26, + "SERVICE27" => InStreamId::Service27, + "SERVICE28" => InStreamId::Service28, + "SERVICE29" => InStreamId::Service29, + "SERVICE30" => InStreamId::Service30, + "SERVICE31" => InStreamId::Service31, + "SERVICE32" => InStreamId::Service32, + "SERVICE33" => InStreamId::Service33, + "SERVICE34" => InStreamId::Service34, + "SERVICE35" => InStreamId::Service35, + "SERVICE36" => InStreamId::Service36, + "SERVICE37" => InStreamId::Service37, + "SERVICE38" => InStreamId::Service38, + "SERVICE39" => InStreamId::Service39, + "SERVICE40" => InStreamId::Service40, + "SERVICE41" => InStreamId::Service41, + "SERVICE42" => InStreamId::Service42, + "SERVICE43" => InStreamId::Service43, + "SERVICE44" => InStreamId::Service44, + "SERVICE45" => InStreamId::Service45, + "SERVICE46" => InStreamId::Service46, + "SERVICE47" => InStreamId::Service47, + "SERVICE48" => InStreamId::Service48, + "SERVICE49" => InStreamId::Service49, + "SERVICE50" => InStreamId::Service50, + "SERVICE51" => InStreamId::Service51, + "SERVICE52" => InStreamId::Service52, + "SERVICE53" => InStreamId::Service53, + "SERVICE54" => InStreamId::Service54, + "SERVICE55" => InStreamId::Service55, + "SERVICE56" => InStreamId::Service56, + "SERVICE57" => InStreamId::Service57, + "SERVICE58" => InStreamId::Service58, + "SERVICE59" => InStreamId::Service59, + "SERVICE60" => InStreamId::Service60, + "SERVICE61" => InStreamId::Service61, + "SERVICE62" => InStreamId::Service62, + "SERVICE63" => InStreamId::Service63, + _ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s), + }) + } +} diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs new file mode 100644 index 0000000..9cb64fe --- /dev/null +++ b/src/types/initialization_vector.rs @@ -0,0 +1,56 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::ops::Deref; +use std::str::{self, FromStr}; +use trackable::error::ErrorKindExt; + +/// Initialization vector. +/// +/// See: [4.3.2.4. EXT-X-KEY] +/// +/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InitializationVector(pub [u8; 16]); + +impl Deref for InitializationVector { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for InitializationVector { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for InitializationVector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "0x")?; + for b in &self.0 { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +impl FromStr for InitializationVector { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!( + s.starts_with("0x") || s.starts_with("0X"), + ErrorKind::InvalidInput + ); + track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput); + + let mut v = [0; 16]; + for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() { + let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + let b = + track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + v[i] = b; + } + Ok(InitializationVector(v)) + } +} diff --git a/src/types/media_type.rs b/src/types/media_type.rs new file mode 100644 index 0000000..103abde --- /dev/null +++ b/src/types/media_type.rs @@ -0,0 +1,41 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; + +/// Media type. +/// +/// See: [4.3.4.1. EXT-X-MEDIA] +/// +/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MediaType { + Audio, + Video, + Subtitles, + ClosedCaptions, +} + +impl fmt::Display for MediaType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + MediaType::Audio => "AUDIO".fmt(f), + MediaType::Video => "VIDEO".fmt(f), + MediaType::Subtitles => "SUBTITLES".fmt(f), + MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f), + } + } +} + +impl FromStr for MediaType { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "AUDIO" => MediaType::Audio, + "VIDEO" => MediaType::Video, + "SUBTITLES" => MediaType::Subtitles, + "CLOSED-CAPTIONS" => MediaType::ClosedCaptions, + _ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s), + }) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..d3f30d2 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,36 @@ +//! Miscellaneous types. +mod byte_range; +mod closed_captions; +mod decimal_floating_point; +mod decimal_resolution; +mod decryption_key; +mod encryption_method; +mod hdcp_level; +mod hexadecimal_sequence; +mod in_stream_id; +mod initialization_vector; +mod media_type; +mod playlist_type; +mod protocol_version; +mod quoted_string; +mod session_data; +mod signed_decimal_floating_point; +mod single_line_string; + +pub use byte_range::*; +pub use closed_captions::*; +pub use decimal_floating_point::*; +pub use decimal_resolution::*; +pub use decryption_key::*; +pub use encryption_method::*; +pub use hdcp_level::*; +pub use hexadecimal_sequence::*; +pub use in_stream_id::*; +pub use initialization_vector::*; +pub use media_type::*; +pub use playlist_type::*; +pub use protocol_version::*; +pub use quoted_string::*; +pub use session_data::*; +pub use signed_decimal_floating_point::*; +pub use single_line_string::*; diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs new file mode 100644 index 0000000..e9dbb00 --- /dev/null +++ b/src/types/playlist_type.rs @@ -0,0 +1,35 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; + +/// Playlist type. +/// +/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE] +/// +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PlaylistType { + Event, + Vod, +} + +impl fmt::Display for PlaylistType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + PlaylistType::Event => write!(f, "EVENT"), + PlaylistType::Vod => write!(f, "VOD"), + } + } +} + +impl FromStr for PlaylistType { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "EVENT" => Ok(PlaylistType::Event), + "VOD" => Ok(PlaylistType::Vod), + _ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s), + } + } +} diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs new file mode 100644 index 0000000..312707b --- /dev/null +++ b/src/types/protocol_version.rs @@ -0,0 +1,47 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; + +/// [7. Protocol Version Compatibility] +/// +/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7 +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ProtocolVersion { + V1, + V2, + V3, + V4, + V5, + V6, + V7, +} +impl fmt::Display for ProtocolVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let n = match *self { + ProtocolVersion::V1 => 1, + ProtocolVersion::V2 => 2, + ProtocolVersion::V3 => 3, + ProtocolVersion::V4 => 4, + ProtocolVersion::V5 => 5, + ProtocolVersion::V6 => 6, + ProtocolVersion::V7 => 7, + }; + write!(f, "{}", n) + } +} +impl FromStr for ProtocolVersion { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "1" => ProtocolVersion::V1, + "2" => ProtocolVersion::V2, + "3" => ProtocolVersion::V3, + "4" => ProtocolVersion::V4, + "5" => ProtocolVersion::V5, + "6" => ProtocolVersion::V6, + "7" => ProtocolVersion::V7, + _ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s), + }) + } +} diff --git a/src/types/quoted_string.rs b/src/types/quoted_string.rs new file mode 100644 index 0000000..07c6118 --- /dev/null +++ b/src/types/quoted_string.rs @@ -0,0 +1,62 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::ops::Deref; +use std::str::{self, FromStr}; + +/// Quoted string. +/// +/// See: [4.2. Attribute Lists] +/// +/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct QuotedString(String); + +impl QuotedString { + /// Makes a new `QuotedString` instance. + /// + /// # Errors + /// + /// If the given string contains any control characters or double-quote character, + /// this function will return an error which has the kind `ErrorKind::InvalidInput`. + pub fn new>(s: T) -> Result { + let s = s.into(); + track_assert!( + !s.chars().any(|c| c.is_control() || c == '"'), + ErrorKind::InvalidInput + ); + Ok(QuotedString(s)) + } +} + +impl Deref for QuotedString { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for QuotedString { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for QuotedString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl FromStr for QuotedString { + type Err = Error; + fn from_str(s: &str) -> Result { + let len = s.len(); + let bytes = s.as_bytes(); + track_assert!(len >= 2, ErrorKind::InvalidInput); + track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput); + track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput); + + let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) }; + track!(QuotedString::new(s)) + } +} diff --git a/src/types/session_data.rs b/src/types/session_data.rs new file mode 100644 index 0000000..25c90e1 --- /dev/null +++ b/src/types/session_data.rs @@ -0,0 +1,13 @@ +use crate::types::QuotedString; + +/// Session data. +/// +/// See: [4.3.4.4. EXT-X-SESSION-DATA] +/// +/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SessionData { + Value(QuotedString), + Uri(QuotedString), +} diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs new file mode 100644 index 0000000..3c50998 --- /dev/null +++ b/src/types/signed_decimal_floating_point.rs @@ -0,0 +1,56 @@ +use crate::{Error, ErrorKind, Result}; +use std::fmt; +use std::str::{self, FromStr}; +use trackable::error::ErrorKindExt; + +/// Signed decimal floating-point number. +/// +/// See: [4.2. Attribute Lists] +/// +/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct SignedDecimalFloatingPoint(f64); + +impl SignedDecimalFloatingPoint { + /// Makes a new `SignedDecimalFloatingPoint` instance. + /// + /// # Errors + /// + /// The given value must be finite, + /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. + pub fn new(n: f64) -> Result { + track_assert!(n.is_finite(), ErrorKind::InvalidInput); + Ok(SignedDecimalFloatingPoint(n)) + } + + /// Converts `DecimalFloatingPoint` to `f64`. + pub fn as_f64(self) -> f64 { + self.0 + } +} + +impl From for SignedDecimalFloatingPoint { + fn from(f: i32) -> Self { + SignedDecimalFloatingPoint(f64::from(f)) + } +} + +impl Eq for SignedDecimalFloatingPoint {} + +impl fmt::Display for SignedDecimalFloatingPoint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl FromStr for SignedDecimalFloatingPoint { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!( + s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'), + ErrorKind::InvalidInput + ); + let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + Ok(SignedDecimalFloatingPoint(n)) + } +} diff --git a/src/types/single_line_string.rs b/src/types/single_line_string.rs new file mode 100644 index 0000000..3f99d35 --- /dev/null +++ b/src/types/single_line_string.rs @@ -0,0 +1,55 @@ +use crate::{ErrorKind, Result}; +use std::fmt; +use std::ops::Deref; + +/// String that represents a single line in a playlist file. +/// +/// See: [4.1. Definition of a Playlist] +/// +/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SingleLineString(String); + +impl SingleLineString { + /// Makes a new `SingleLineString` instance. + /// + /// # Errors + /// + /// If the given string contains any control characters, + /// this function will return an error which has the kind `ErrorKind::InvalidInput`. + pub fn new>(s: T) -> Result { + let s = s.into(); + track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput); + Ok(SingleLineString(s)) + } +} + +impl Deref for SingleLineString { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for SingleLineString { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for SingleLineString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn single_line_string() { + assert!(SingleLineString::new("foo").is_ok()); + assert!(SingleLineString::new("b\rar").is_err()); + } +} From cb2764086719296417978b6eadc79b4784a3ab49 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 6 Sep 2019 13:21:05 +0200 Subject: [PATCH 3/4] rustfmt code --- src/tags/basic/version.rs | 1 - src/tags/master_playlist/i_frame_stream_inf.rs | 4 +--- src/tags/master_playlist/media.rs | 4 ++-- src/tags/master_playlist/session_data.rs | 1 - src/tags/master_playlist/session_key.rs | 1 - src/tags/master_playlist/stream_inf.rs | 7 +++---- src/tags/media_segment/date_range.rs | 4 +--- src/tags/media_segment/mod.rs | 8 ++++---- src/tags/shared/mod.rs | 2 +- src/tags/shared/start.rs | 2 +- src/types/closed_captions.rs | 2 +- src/types/decryption_key.rs | 2 +- 12 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 13cc6ec..f0ab35f 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -46,7 +46,6 @@ impl FromStr for ExtXVersion { } } - #[cfg(test)] mod test { use super::*; diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 209f4d2..3850e47 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -1,11 +1,10 @@ -use crate::utils::parse_u64; use crate::attribute::AttributePairs; use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion, QuotedString}; +use crate::utils::parse_u64; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; - /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] /// /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 @@ -158,7 +157,6 @@ mod test { assert_eq!(tag.requires_version(), ProtocolVersion::V1); } - fn quoted_string(s: &str) -> QuotedString { QuotedString::new(s).unwrap() } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 151139a..82ee440 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,6 +1,6 @@ -use crate::utils::parse_yes_or_no; use crate::attribute::AttributePairs; -use crate::types::{ InStreamId, MediaType, ProtocolVersion, QuotedString}; +use crate::types::{InStreamId, MediaType, ProtocolVersion, QuotedString}; +use crate::utils::parse_yes_or_no; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 28a69b3..3ddc153 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -4,7 +4,6 @@ use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; - /// [4.3.4.4. EXT-X-SESSION-DATA] /// /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 36ace8f..dc0838d 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -3,7 +3,6 @@ use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; - /// [4.3.4.5. EXT-X-SESSION-KEY] /// /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index f24bdaf..d919e4d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -1,14 +1,13 @@ -use crate::utils::parse_u64; use crate::attribute::AttributePairs; use crate::types::{ - ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, - ProtocolVersion, QuotedString, SingleLineString, + ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion, + QuotedString, SingleLineString, }; +use crate::utils::parse_u64; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; - /// [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 2db82b6..b406289 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -152,7 +152,5 @@ mod test { use super::*; #[test] // TODO; write some tests - fn it_works() { - - } + fn it_works() {} } diff --git a/src/tags/media_segment/mod.rs b/src/tags/media_segment/mod.rs index c5a0a5c..9d10bd5 100644 --- a/src/tags/media_segment/mod.rs +++ b/src/tags/media_segment/mod.rs @@ -1,15 +1,15 @@ -mod inf; mod byte_range; mod date_range; +mod discontinuity; +mod inf; mod key; mod map; mod program_date_time; -mod discontinuity; -pub use inf::*; pub use byte_range::*; pub use date_range::*; +pub use discontinuity::*; +pub use inf::*; pub use key::*; pub use map::*; pub use program_date_time::*; -pub use discontinuity::*; diff --git a/src/tags/shared/mod.rs b/src/tags/shared/mod.rs index 5811164..956efe7 100644 --- a/src/tags/shared/mod.rs +++ b/src/tags/shared/mod.rs @@ -1,5 +1,5 @@ mod independent_segments; mod start; -pub use start::*; pub use independent_segments::*; +pub use start::*; diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index c9afac5..5c301c1 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -1,6 +1,6 @@ -use crate::utils::parse_yes_or_no; use crate::attribute::AttributePairs; use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; +use crate::utils::parse_yes_or_no; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 6563ed4..1e1963f 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,7 +1,7 @@ +use crate::types::QuotedString; use crate::{Error, Result}; use std::fmt; use std::str::{self, FromStr}; -use crate::types::QuotedString; /// The identifier of a closed captions group or its absence. /// diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 8b30328..ac57237 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,6 +1,6 @@ use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, QuotedString}; use crate::{Error, ErrorKind, Result}; -use crate::types::{QuotedString, InitializationVector, ProtocolVersion, EncryptionMethod}; use std::fmt; use std::str::{self, FromStr}; From fe032ee98421f125bb5c1684196b27f58e91264c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 6 Sep 2019 13:46:21 +0200 Subject: [PATCH 4/4] added tests --- src/types/byte_range.rs | 47 +++++++++++++++++++++++++++++ src/types/closed_captions.rs | 26 ++++++++++++++++ src/types/decimal_floating_point.rs | 29 ++++++++++++++++++ src/types/decimal_resolution.rs | 41 +++++++++++++++++++++++++ src/types/encryption_method.rs | 29 ++++++++++++++++++ src/types/hdcp_level.rs | 23 ++++++++++++++ 6 files changed, 195 insertions(+) diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index b8db803..af26870 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -43,3 +43,50 @@ impl FromStr for ByteRange { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let byte_range = ByteRange { + length: 0, + start: Some(5), + }; + assert_eq!(byte_range.to_string(), "0@5".to_string()); + + let byte_range = ByteRange { + length: 99999, + start: Some(2), + }; + assert_eq!(byte_range.to_string(), "99999@2".to_string()); + + let byte_range = ByteRange { + length: 99999, + start: None, + }; + assert_eq!(byte_range.to_string(), "99999".to_string()); + } + + #[test] + fn test_parse() { + let byte_range = ByteRange { + length: 99999, + start: Some(2), + }; + assert_eq!(byte_range, "99999@2".parse::().unwrap()); + + let byte_range = ByteRange { + length: 99999, + start: Some(2), + }; + assert_eq!(byte_range, "99999@2".parse::().unwrap()); + + let byte_range = ByteRange { + length: 99999, + start: None, + }; + assert_eq!(byte_range, "99999".parse::().unwrap()); + } +} diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 1e1963f..fb4ed85 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -34,3 +34,29 @@ impl FromStr for ClosedCaptions { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let closed_captions = ClosedCaptions::None; + assert_eq!(closed_captions.to_string(), "NONE".to_string()); + + let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap()); + assert_eq!(closed_captions.to_string(), "\"value\"".to_string()); + } + + #[test] + fn test_parse() { + let closed_captions = ClosedCaptions::None; + assert_eq!(closed_captions, "NONE".parse::().unwrap()); + + let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap()); + assert_eq!( + closed_captions, + "\"value\"".parse::().unwrap() + ); + } +} diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 08c49fe..5cd33cc 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -68,3 +68,32 @@ impl FromStr for DecimalFloatingPoint { Ok(DecimalFloatingPoint(n)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_display() { + let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap(); + assert_eq!(decimal_floating_point.to_string(), "22".to_string()); + + let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap(); + assert_eq!(decimal_floating_point.to_string(), "4.1".to_string()); + } + + #[test] + pub fn test_parse() { + let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap(); + assert_eq!( + decimal_floating_point, + "22".parse::().unwrap() + ); + + let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap(); + assert_eq!( + decimal_floating_point, + "4.1".parse::().unwrap() + ); + } +} diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index e340451..f323577 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -35,3 +35,44 @@ impl FromStr for DecimalResolution { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let decimal_resolution = DecimalResolution { + width: 1920, + height: 1080, + }; + assert_eq!(decimal_resolution.to_string(), "1920x1080".to_string()); + + let decimal_resolution = DecimalResolution { + width: 1280, + height: 720, + }; + assert_eq!(decimal_resolution.to_string(), "1280x720".to_string()); + } + + #[test] + fn test_parse() { + let decimal_resolution = DecimalResolution { + width: 1920, + height: 1080, + }; + assert_eq!( + decimal_resolution, + "1920x1080".parse::().unwrap() + ); + + let decimal_resolution = DecimalResolution { + width: 1280, + height: 720, + }; + assert_eq!( + decimal_resolution, + "1280x720".parse::().unwrap() + ); + } +} diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index edd2911..971e7bb 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -37,3 +37,32 @@ impl FromStr for EncryptionMethod { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let encryption_method = EncryptionMethod::Aes128; + assert_eq!(encryption_method.to_string(), "AES-128".to_string()); + + let encryption_method = EncryptionMethod::SampleAes; + assert_eq!(encryption_method.to_string(), "SAMPLE-AES".to_string()); + } + + #[test] + fn test_parse() { + let encryption_method = EncryptionMethod::Aes128; + assert_eq!( + encryption_method, + "AES-128".parse::().unwrap() + ); + + let encryption_method = EncryptionMethod::SampleAes; + assert_eq!( + encryption_method, + "SAMPLE-AES".parse::().unwrap() + ); + } +} diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index dacec45..436178f 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -33,3 +33,26 @@ impl FromStr for HdcpLevel { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let level = HdcpLevel::Type0; + assert_eq!(level.to_string(), "TYPE-0".to_string()); + + let level = HdcpLevel::None; + assert_eq!(level.to_string(), "NONE".to_string()); + } + + #[test] + fn test_parse() { + let level = HdcpLevel::Type0; + assert_eq!(level, "TYPE-0".parse::().unwrap()); + + let level = HdcpLevel::None; + assert_eq!(level, "NONE".parse::().unwrap()); + } +}