From 93f6db890486dbc84b1b4519412f7df8cafda256 Mon Sep 17 00:00:00 2001 From: Takeru Ohta Date: Wed, 14 Feb 2018 18:57:15 +0900 Subject: [PATCH] Add `tag::master_playlist` module --- src/attribute.rs | 6 +- src/master_playlist.rs | 2 + src/tag/master_playlist.rs | 788 +++++++++++++++++++++++++++++++++++++ src/tag/media_segment.rs | 34 +- src/tag/mod.rs | 610 +--------------------------- src/types.rs | 264 +++++++++++++ 6 files changed, 1072 insertions(+), 632 deletions(-) create mode 100644 src/tag/master_playlist.rs diff --git a/src/attribute.rs b/src/attribute.rs index f97f5d1..2debe9a 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -141,8 +141,9 @@ impl FromStr for HexadecimalSequence { } } +// TODO: delete #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DecimalInteger(u64); +pub struct DecimalInteger(pub u64); impl fmt::Display for DecimalInteger { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) @@ -168,6 +169,9 @@ impl DecimalFloatingPoint { let n = (duration.as_secs() as f64) + (duration.subsec_nanos() as f64 / 1_000_000_000.0); DecimalFloatingPoint(n) } + pub fn as_f64(&self) -> f64 { + self.0 + } } impl Eq for DecimalFloatingPoint {} impl fmt::Display for DecimalFloatingPoint { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index cd765e7..276f839 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -110,10 +110,12 @@ impl FromStr for MasterPlaylist { media_tags.push(t); } Tag::ExtXStreamInf(t) => { + // TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag track_assert_eq!(last_stream_inf, None, ErrorKind::InvalidInput); last_stream_inf = Some((i, t)); } Tag::ExtXIFrameStreamInf(t) => { + // TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag i_frame_stream_infs.push(t); } Tag::ExtXSessionData(t) => { diff --git a/src/tag/master_playlist.rs b/src/tag/master_playlist.rs new file mode 100644 index 0000000..f22cd76 --- /dev/null +++ b/src/tag/master_playlist.rs @@ -0,0 +1,788 @@ +use std::fmt; +use std::str::FromStr; + +use {Error, ErrorKind, Result}; +use attribute::{AttributePairs, DecimalFloatingPoint, DecimalInteger, DecimalResolution, + QuotedString}; +use types::{ClosedCaptions, DecryptionKey, HdcpLevel, InStreamId, M3u8String, MediaType, + ProtocolVersion, SessionData, YesOrNo}; + +/// [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 +/// +/// TODO: Add `ExtXMediaBuilder` +#[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: YesOrNo, + autoselect: YesOrNo, + forced: YesOrNo, + 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: YesOrNo::No, + autoselect: YesOrNo::No, + forced: YesOrNo::No, + 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) -> YesOrNo { + self.default + } + + /// Returns whether the client may choose to + /// play this rendition in the absence of explicit user preference. + pub fn autoselect(&self) -> YesOrNo { + self.autoselect + } + + /// Returns whether the rendition contains content that is considered essential to play. + pub fn forced(&self) -> YesOrNo { + 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 YesOrNo::Yes == self.default { + write!(f, ",DEFAULT=YES")?; + } + if YesOrNo::Yes == self.autoselect { + write!(f, ",AUTOSELECT=YES")?; + } + if YesOrNo::Yes == 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 media_type = None; + let mut uri = None; + let mut group_id = None; + let mut language = None; + let mut assoc_language = None; + let mut name = None; + let mut default = None; + let mut autoselect = None; + let mut forced = None; + let mut instream_id = None; + let mut characteristics = None; + let mut channels = None; + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "TYPE" => media_type = Some(track!(value.parse())?), + "URI" => uri = Some(track!(value.parse())?), + "GROUP-ID" => group_id = Some(track!(value.parse())?), + "LANGUAGE" => language = Some(track!(value.parse())?), + "ASSOC-LANGUAGE" => assoc_language = Some(track!(value.parse())?), + "NAME" => name = Some(track!(value.parse())?), + "DEFAULT" => default = Some(track!(value.parse())?), + "AUTOSELECT" => autoselect = Some(track!(value.parse())?), + "FORCED" => forced = Some(track!(value.parse())?), + "INSTREAM-ID" => { + let s: QuotedString = track!(value.parse())?; + instream_id = Some(track!(s.as_str().parse())?); + } + "CHARACTERISTICS" => characteristics = Some(track!(value.parse())?), + "CHANNELS" => channels = Some(track!(value.parse())?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + let media_type = track_assert_some!(media_type, ErrorKind::InvalidInput); + let group_id = track_assert_some!(group_id, ErrorKind::InvalidInput); + let name = track_assert_some!(name, ErrorKind::InvalidInput); + if MediaType::ClosedCaptions == media_type { + track_assert_ne!(uri, None, ErrorKind::InvalidInput); + track_assert!(instream_id.is_some(), ErrorKind::InvalidInput); + } else { + track_assert!(instream_id.is_none(), ErrorKind::InvalidInput); + } + if default == Some(YesOrNo::Yes) && autoselect.is_some() { + track_assert_eq!(autoselect, Some(YesOrNo::Yes), ErrorKind::InvalidInput); + } + if MediaType::Subtitles != media_type { + track_assert_eq!(forced, None, ErrorKind::InvalidInput); + } + Ok(ExtXMedia { + media_type, + uri, + group_id, + language, + assoc_language, + name, + default: default.unwrap_or(YesOrNo::No), + autoselect: autoselect.unwrap_or(YesOrNo::No), + forced: forced.unwrap_or(YesOrNo::No), + instream_id, + characteristics, + channels, + }) + } +} + +/// [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: M3u8String, + bandwidth: DecimalInteger, + 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: M3u8String, bandwidth: DecimalInteger) -> 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) -> &M3u8String { + &self.uri + } + + /// Returns the peak segment bit rate of the variant stream. + pub fn bandwidth(&self) -> DecimalInteger { + 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'); // TODO: + let first_line = lines.next().expect("Never fails"); + let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput); + + track_assert!( + first_line.starts_with(Self::PREFIX), + ErrorKind::InvalidInput + ); + let uri = track!(M3u8String::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!(value.parse())?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?), + "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: DecimalInteger, + 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: DecimalInteger) -> 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) -> DecimalInteger { + 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!(value.parse())?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?), + "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 attribute::HexadecimalSequence; + use types::EncryptionMethod; + 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); + } + + #[test] + fn ext_x_stream_inf() { + let tag = ExtXStreamInf::new(M3u8String::new("foo").unwrap(), DecimalInteger(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"), DecimalInteger(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(HexadecimalSequence::new(vec![0, 1, 2])), + key_format: None, + key_format_versions: None, + }); + let text = r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102"#; + 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/tag/media_segment.rs b/src/tag/media_segment.rs index 317bf69..44070cb 100644 --- a/src/tag/media_segment.rs +++ b/src/tag/media_segment.rs @@ -283,14 +283,8 @@ impl FromStr for ExtXMap { for attr in attrs { let (key, value) = track!(attr)?; match key { - "URI" => { - track_assert_eq!(uri, None, ErrorKind::InvalidInput); - uri = Some(track!(value.parse())?); - } - "BYTERANGE" => { - track_assert_eq!(range, None, ErrorKind::InvalidInput); - range = Some(track!(value.parse())?); - } + "URI" => uri = Some(track!(value.parse())?), + "BYTERANGE" => range = Some(track!(value.parse())?), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -435,12 +429,8 @@ impl FromStr for ExtXDateRange { for attr in attrs { let (key, value) = track!(attr)?; match key { - "ID" => { - id = Some(track!(value.parse())?); - } - "CLASS" => { - class = Some(track!(value.parse())?); - } + "ID" => id = Some(track!(value.parse())?), + "CLASS" => class = Some(track!(value.parse())?), "START-DATE" => { let s: QuotedString = track!(value.parse())?; start_date = Some(track!( @@ -463,18 +453,10 @@ impl FromStr for ExtXDateRange { 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" => { - end_on_next = Some(track!(value.parse())?); - } + "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" => end_on_next = Some(track!(value.parse())?), _ => { if key.starts_with("X-") { client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); diff --git a/src/tag/mod.rs b/src/tag/mod.rs index a80643d..415945c 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -5,8 +5,8 @@ use std::fmt; use std::str::FromStr; use {Error, ErrorKind, Result}; -use attribute::{AttributePairs, DecimalFloatingPoint, DecimalInteger, DecimalResolution, - HexadecimalSequence, QuotedString, SignedDecimalFloatingPoint}; +use attribute::{AttributePairs, SignedDecimalFloatingPoint}; +use types::YesOrNo; macro_rules! may_invalid { ($expr:expr) => { @@ -25,12 +25,15 @@ macro_rules! impl_from { } pub use self::basic::{ExtM3u, ExtXVersion}; +pub use self::master_playlist::{ExtXIFrameStreamInf, ExtXMedia, ExtXSessionData, ExtXSessionKey, + ExtXStreamInf}; 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_playlist; mod media_segment; @@ -227,580 +230,6 @@ impl FromStr for Tag { } } -#[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), - }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum YesOrNo { - Yes, - No, -} -impl fmt::Display for YesOrNo { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - YesOrNo::Yes => "YES".fmt(f), - YesOrNo::No => "NO".fmt(f), - } - } -} -impl FromStr for YesOrNo { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "YES" => Ok(YesOrNo::Yes), - "NO" => Ok(YesOrNo::No), - _ => track_panic!( - ErrorKind::InvalidInput, - "Unexpected enumerated-string: {:?}", - s - ), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtXMedia { - media_type: MediaType, - uri: Option, - group_id: QuotedString, - language: Option, - assoc_language: Option, - name: QuotedString, - default: YesOrNo, - autoselect: YesOrNo, - forced: Option, - instream_id: Option, // TODO: `InStreamId` type - characteristics: Option, - channels: Option, -} -impl ExtXMedia { - const PREFIX: &'static str = "#EXT-X-MEDIA:"; -} -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 YesOrNo::Yes == self.default { - write!(f, ",DEFAULT={}", self.default)?; - } - if YesOrNo::Yes == self.autoselect { - write!(f, ",AUTOSELECT={}", self.autoselect)?; - } - if let Some(ref x) = self.forced { - write!(f, ",FORCED={}", x)?; - } - 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 media_type = None; - let mut uri = None; - let mut group_id = None; - let mut language = None; - let mut assoc_language = None; - let mut name = None; - let mut default = None; - let mut autoselect = None; - let mut forced = None; - let mut instream_id = None; - let mut characteristics = None; - let mut channels = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "TYPE" => media_type = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), - "GROUP-ID" => group_id = Some(track!(value.parse())?), - "LANGUAGE" => language = Some(track!(value.parse())?), - "ASSOC-LANGUAGE" => assoc_language = Some(track!(value.parse())?), - "NAME" => name = Some(track!(value.parse())?), - "DEFAULT" => default = Some(track!(value.parse())?), - "AUTOSELECT" => autoselect = Some(track!(value.parse())?), - "FORCED" => forced = Some(track!(value.parse())?), - "INSTREAM-ID" => instream_id = Some(track!(value.parse())?), - "CHARACTERISTICS" => characteristics = Some(track!(value.parse())?), - "CHANNELS" => channels = Some(track!(value.parse())?), - _ => { - // [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - let media_type = track_assert_some!(media_type, ErrorKind::InvalidInput); - let group_id = track_assert_some!(group_id, ErrorKind::InvalidInput); - let name = track_assert_some!(name, ErrorKind::InvalidInput); - if MediaType::ClosedCaptions == media_type { - track_assert_ne!(uri, None, ErrorKind::InvalidInput); - track_assert!(instream_id.is_some(), ErrorKind::InvalidInput); - } else { - track_assert!(instream_id.is_none(), ErrorKind::InvalidInput); - } - if MediaType::Subtitles != media_type { - track_assert_eq!(forced, None, ErrorKind::InvalidInput); - } - Ok(ExtXMedia { - media_type, - uri, - group_id, - language, - assoc_language, - name, - default: default.unwrap_or(YesOrNo::No), - autoselect: autoselect.unwrap_or(YesOrNo::No), - forced, - instream_id, - characteristics, - channels, - }) - } -} - -#[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), - } - } -} - -#[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())?)) - } - } -} - -// TODO: The URI line is REQUIRED. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtXStreamInf { - bandwidth: DecimalInteger, - average_bandwidth: Option, - codecs: Option, - resolution: Option, - - // TODO: rounded to three decimal places - frame_rate: Option, - hdcp_level: Option, - audio: Option, - video: Option, - subtitles: Option, - closed_captions: Option, -} -impl ExtXStreamInf { - const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; -} -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={}", x)?; - } - 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)?; - } - Ok(()) - } -} -impl FromStr for ExtXStreamInf { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - 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(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "BANDWIDTH" => bandwidth = Some(track!(value.parse())?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?), - "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" => { - // TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag - audio = Some(track!(value.parse())?); - } - "VIDEO" => { - // TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag - video = Some(track!(value.parse())?); - } - "SUBTITLES" => { - // TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag - subtitles = Some(track!(value.parse())?); - } - "CLOSED-CAPTIONS" => { - // TODO: validate - closed_captions = Some(track!(value.parse())?); - } - _ => { - // [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); - Ok(ExtXStreamInf { - bandwidth, - average_bandwidth, - codecs, - resolution, - frame_rate, - hdcp_level, - audio, - video, - subtitles, - closed_captions, - }) - } -} - -// TODO: That Playlist file MUST contain an EXT-X-I-FRAMES-ONLY tag. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtXIFrameStreamInf { - uri: QuotedString, - bandwidth: DecimalInteger, - average_bandwidth: Option, - codecs: Option, - resolution: Option, - hdcp_level: Option, - video: Option, -} -impl ExtXIFrameStreamInf { - const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; -} -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!(value.parse())?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?), - "CODECS" => codecs = Some(track!(value.parse())?), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "VIDEO" => { - // TODO: - video = Some(track!(value.parse())?); - } - _ => { - // [6.3.1] 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, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SessionData { - Value(QuotedString), - Uri(QuotedString), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtXSessionData { - pub data_id: QuotedString, - pub data: SessionData, - pub language: Option, -} -impl ExtXSessionData { - const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; -} -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] 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, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtXSessionKey { - pub method: SessionEncryptionMethod, - pub uri: QuotedString, - pub iv: Option, - pub key_format: Option, - pub key_format_versions: Option, -} -impl ExtXSessionKey { - const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; -} -impl fmt::Display for ExtXSessionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - 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 ExtXSessionKey { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - - 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.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "METHOD" => { - track_assert_eq!(method, None, ErrorKind::InvalidInput); - method = Some(track!(value.parse())?); - } - "URI" => { - track_assert_eq!(uri, None, ErrorKind::InvalidInput); - uri = Some(track!(value.parse())?); - } - "IV" => { - // TODO: validate length(128-bit) - track_assert_eq!(iv, None, ErrorKind::InvalidInput); - iv = Some(track!(value.parse())?); - } - "KEYFORMAT" => { - track_assert_eq!(key_format, None, ErrorKind::InvalidInput); - key_format = Some(track!(value.parse())?); - } - "KEYFORMATVERSIONS" => { - track_assert_eq!(key_format_versions, None, ErrorKind::InvalidInput); - key_format_versions = Some(track!(value.parse())?); - } - _ => { - // [6.3.1] 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(ExtXSessionKey { - method, - uri, - iv, - key_format, - key_format_versions, - }) - } -} - // 4.3.5. Media or Master Playlist Tags // TODO: A tag that appears in both MUST have the same value; otherwise, clients SHOULD ignore the value in the Media Playlist(s). // TODO: These tags MUST NOT appear more than once in a Playlist. @@ -867,32 +296,3 @@ impl FromStr for ExtXStart { }) } } - -// TODO: move -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SessionEncryptionMethod { - Aes128, - SampleAes, -} -impl fmt::Display for SessionEncryptionMethod { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - SessionEncryptionMethod::Aes128 => "AES-128".fmt(f), - SessionEncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), - } - } -} -impl FromStr for SessionEncryptionMethod { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "AES-128" => Ok(SessionEncryptionMethod::Aes128), - "SAMPLE-AES" => Ok(SessionEncryptionMethod::SampleAes), - _ => track_panic!( - ErrorKind::InvalidInput, - "Unknown encryption method: {:?}", - s - ), - } - } -} diff --git a/src/types.rs b/src/types.rs index 70f05bd..69ef857 100644 --- a/src/types.rs +++ b/src/types.rs @@ -261,3 +261,267 @@ impl FromStr for PlaylistType { } } } + +#[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), + }) + } +} + +// TODO: remove +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum YesOrNo { + Yes, + No, +} +impl fmt::Display for YesOrNo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + YesOrNo::Yes => "YES".fmt(f), + YesOrNo::No => "NO".fmt(f), + } + } +} +impl FromStr for YesOrNo { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "YES" => Ok(YesOrNo::Yes), + "NO" => Ok(YesOrNo::No), + _ => track_panic!( + ErrorKind::InvalidInput, + "Unexpected enumerated-string: {:?}", + s + ), + } + } +} + +#[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), + }) + } +} + +#[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), + } + } +} + +#[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())?)) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SessionData { + Value(QuotedString), + Uri(QuotedString), +}