From 81f9a421fe9d0dd2e6f56c2d676d18b8aec53022 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 22 Sep 2019 10:57:28 +0200 Subject: [PATCH 1/3] added RequiredVersion trait --- src/attribute.rs | 4 +- src/master_playlist.rs | 22 +++-- src/media_playlist.rs | 20 ++--- src/media_segment.rs | 21 ++--- src/tags/basic/m3u.rs | 11 +-- src/tags/basic/version.rs | 30 ++++--- .../master_playlist/i_frame_stream_inf.rs | 26 ++++-- src/tags/master_playlist/media.rs | 36 ++++++-- src/tags/master_playlist/session_data.rs | 16 +--- src/tags/master_playlist/session_key.rs | 31 +++---- src/tags/master_playlist/stream_inf.rs | 27 ++++-- .../media_playlist/discontinuity_sequence.rs | 50 +++++++---- src/tags/media_playlist/end_list.rs | 26 ++++-- src/tags/media_playlist/i_frames_only.rs | 28 ++++-- src/tags/media_playlist/media_sequence.rs | 50 +++++++---- src/tags/media_playlist/playlist_type.rs | 13 +-- src/tags/media_playlist/target_duration.rs | 49 +++++++---- src/tags/media_segment/byte_range.rs | 26 +++--- src/tags/media_segment/date_range.rs | 7 +- src/tags/media_segment/discontinuity.rs | 31 ++++--- src/tags/media_segment/inf.rs | 13 +-- src/tags/media_segment/map.rs | 49 +++++++---- src/tags/media_segment/program_date_time.rs | 38 +++++--- src/tags/shared/independent_segments.rs | 35 ++++++-- src/tags/shared/start.rs | 55 +++++++++--- src/types/decimal_floating_point.rs | 12 +++ src/types/decryption_key.rs | 30 +++---- src/types/protocol_version.rs | 87 +++++++++++++++---- 28 files changed, 550 insertions(+), 293 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 3c9807f..d0cf36e 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -61,10 +61,10 @@ impl FromStr for AttributePairs { let key = pair[0].to_uppercase(); let value = pair[1].to_string(); - result.insert(key.to_string(), value.to_string()); + result.insert(key.trim().to_string(), value.trim().to_string()); } - #[cfg(test)] + #[cfg(test)] // this is very useful, when a test fails! dbg!(&result); Ok(result) } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index e228e13..6f52f0f 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -10,7 +10,7 @@ use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; -use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; +use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, RequiredVersion}; use crate::Error; /// Master playlist. @@ -92,6 +92,12 @@ impl MasterPlaylist { } } +impl RequiredVersion for MasterPlaylist { + fn required_version(&self) -> ProtocolVersion { + self.version_tag.version() + } +} + impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { let required_version = self.required_version(); @@ -118,43 +124,43 @@ impl MasterPlaylistBuilder { .chain( self.independent_segments_tag .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .chain( self.start_tag .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .chain( self.media_tags .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .chain( self.stream_inf_tags .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .chain( self.i_frame_stream_inf_tags .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .chain( self.session_data_tags .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .chain( self.session_key_tags .iter() - .map(|t| t.iter().map(|t| t.requires_version())) + .map(|t| t.iter().map(|t| t.required_version())) .flatten(), ) .max() diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 3f673c0..da20213 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -11,7 +11,7 @@ use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::Error; /// Media playlist. @@ -142,60 +142,60 @@ impl MediaPlaylistBuilder { .chain( self.target_duration_tag .iter() - .map(|t| t.requires_version()), + .map(|t| t.required_version()), ) .chain(self.media_sequence_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.discontinuity_sequence_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.playlist_type_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.i_frames_only_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.independent_segments_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.start_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.end_list_tag.iter().map(|t| { if let Some(p) = t { - p.requires_version() + p.required_version() } else { ProtocolVersion::V1 } })) .chain(self.segments.iter().map(|t| { t.iter() - .map(|s| s.requires_version()) + .map(|s| s.required_version()) .max() .unwrap_or(ProtocolVersion::V1) })) diff --git a/src/media_segment.rs b/src/media_segment.rs index 8460547..e1d51bf 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -6,7 +6,7 @@ use derive_builder::Builder; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, }; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; /// Media segment. #[derive(Debug, Clone, Builder)] @@ -118,21 +118,22 @@ impl MediaSegment { pub fn key_tags(&self) -> &[ExtXKey] { &self.key_tags } +} - /// Returns the protocol compatibility version that this segment requires. - pub fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for MediaSegment { + fn required_version(&self) -> ProtocolVersion { iter::empty() - .chain(self.key_tags.iter().map(|t| t.requires_version())) - .chain(self.map_tag.iter().map(|t| t.requires_version())) - .chain(self.byte_range_tag.iter().map(|t| t.requires_version())) - .chain(self.date_range_tag.iter().map(|t| t.requires_version())) - .chain(self.discontinuity_tag.iter().map(|t| t.requires_version())) + .chain(self.key_tags.iter().map(|t| t.required_version())) + .chain(self.map_tag.iter().map(|t| t.required_version())) + .chain(self.byte_range_tag.iter().map(|t| t.required_version())) + .chain(self.date_range_tag.iter().map(|t| t.required_version())) + .chain(self.discontinuity_tag.iter().map(|t| t.required_version())) .chain( self.program_date_time_tag .iter() - .map(|t| t.requires_version()), + .map(|t| t.required_version()), ) - .chain(iter::once(self.inf_tag.requires_version())) + .chain(iter::once(self.inf_tag.required_version())) .max() .unwrap_or(ProtocolVersion::V7) } diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 07f95ec..d64939c 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -13,9 +13,10 @@ pub struct ExtM3u; impl ExtM3u { pub(crate) const PREFIX: &'static str = "#EXTM3U"; +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtM3u { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -50,7 +51,7 @@ mod test { } #[test] - fn test_requires_version() { - assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1); + fn test_required_version() { + assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1); } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 5f196db..b5afef2 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -20,12 +20,24 @@ impl ExtXVersion { } /// Returns the protocol compatibility version of the playlist containing this tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXVersion; + /// use hls_m3u8::types::ProtocolVersion; + /// + /// assert_eq!( + /// ExtXVersion::new(ProtocolVersion::V6).version(), + /// ProtocolVersion::V6 + /// ); + /// ``` pub const fn version(&self) -> ProtocolVersion { self.0 } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXVersion { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -78,18 +90,10 @@ mod test { } #[test] - fn test_requires_version() { + fn test_required_version() { assert_eq!( - ExtXVersion::new(ProtocolVersion::V6).requires_version(), + ExtXVersion::new(ProtocolVersion::V6).required_version(), ProtocolVersion::V1 ); } - - #[test] - fn test_version() { - assert_eq!( - ExtXVersion::new(ProtocolVersion::V6).version(), - ProtocolVersion::V6 - ); - } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 989ac36..fc38814 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -3,12 +3,23 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, StreamInf}; +use crate::types::{ProtocolVersion, RequiredVersion, StreamInf}; use crate::utils::{quote, tag, unquote}; use crate::Error; -/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] +/// # [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] +/// The [ExtXIFrameStreamInf] tag identifies a [Media Playlist] file +/// containing the I-frames of a multimedia presentation. It stands +/// alone, in that it does not apply to a particular URI in the [Master Playlist]. /// +/// Its format is: +/// +/// ```text +/// #EXT-X-I-FRAME-STREAM-INF: +/// ``` +/// +/// [Master Playlist]: crate::MasterPlaylist +/// [Media Playlist]: crate::MediaPlaylist /// [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 { @@ -19,7 +30,7 @@ pub struct ExtXIFrameStreamInf { impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; - /// Makes a new `ExtXIFrameStreamInf` tag. + /// Makes a new [ExtXIFrameStreamInf] tag. pub fn new(uri: T, bandwidth: u64) -> Self { ExtXIFrameStreamInf { uri: uri.to_string(), @@ -55,9 +66,10 @@ impl ExtXIFrameStreamInf { self.uri = value.to_string(); self } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXIFrameStreamInf { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -133,9 +145,9 @@ mod test { } #[test] - fn test_requires_version() { + fn test_required_version() { assert_eq!( - ExtXIFrameStreamInf::new("foo", 1000).requires_version(), + ExtXIFrameStreamInf::new("foo", 1000).required_version(), ProtocolVersion::V1 ); } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 819f62b..ed71581 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{InStreamId, MediaType, ProtocolVersion}; +use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::Error; @@ -273,9 +273,10 @@ impl ExtXMedia { pub fn channels(&self) -> Option<&String> { self.channels.as_ref() } +} - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXMedia { + fn required_version(&self) -> ProtocolVersion { match self.instream_id { None | Some(InStreamId::Cc1) @@ -385,11 +386,28 @@ mod test { use super::*; #[test] - fn ext_x_media() { - let tag = ExtXMedia::new(MediaType::Audio, "foo", "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 test_display() { + assert_eq!( + ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() + ) + } + + #[test] + fn test_parser() { + assert_eq!( + ExtXMedia::new(MediaType::Audio, "foo", "bar"), + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" + .parse() + .unwrap() + ) + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXMedia::new(MediaType::Audio, "foo", "bar").required_version(), + ProtocolVersion::V1 + ) } } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index ddd7ee6..09d74ae 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; @@ -243,18 +243,10 @@ impl ExtXSessionData { self.data = value; self } +} - /// Returns the protocol compatibility version, that this tag requires. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::ProtocolVersion; - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); - /// assert_eq!(tag.requires_version(), ProtocolVersion::V1); - /// ``` - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXSessionData { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index e76fb64..ba1d9fc 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -2,7 +2,7 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -25,24 +25,10 @@ impl ExtXSessionKey { Self(DecryptionKey::new(method, uri)) } +} - /// Returns the protocol compatibility version that this tag requires. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion}; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); - /// - /// assert_eq!( - /// key.requires_version(), - /// ProtocolVersion::V1 - /// ); - /// ``` - pub fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXSessionKey { + fn required_version(&self) -> ProtocolVersion { if self.0.key_format.is_some() | self.0.key_format_versions.is_some() { ProtocolVersion::V5 } else if self.0.iv.is_some() { @@ -143,4 +129,13 @@ mod test { key ) } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") + .required_version(), + ProtocolVersion::V1 + ); + } } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index dd237a7..e50a297 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, StreamInf}; +use crate::types::{ + ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, RequiredVersion, StreamInf, +}; use crate::utils::{quote, tag, unquote}; use crate::Error; @@ -23,7 +25,9 @@ pub struct ExtXStreamInf { impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; - /// Makes a new [ExtXStreamInf] tag. + /// Creates a new [ExtXStreamInf] tag. + /// + /// # Examples /// ``` /// # use hls_m3u8::tags::ExtXStreamInf; /// # @@ -40,11 +44,21 @@ impl ExtXStreamInf { } } + pub fn set_uri(&mut self, value: T) -> &mut Self { + self.uri = value.to_string(); + self + } + /// Returns the URI that identifies the associated media playlist. pub const fn uri(&self) -> &String { &self.uri } + pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { + self.frame_rate = value.map(|v| v.into()); + self + } + /// Returns the maximum frame rate for all the video in the variant stream. pub fn frame_rate(&self) -> Option { self.frame_rate.map_or(None, |v| Some(v.as_f64())) @@ -64,9 +78,10 @@ impl ExtXStreamInf { pub fn closed_captions(&self) -> Option<&ClosedCaptions> { self.closed_captions.as_ref() } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXStreamInf { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -158,10 +173,10 @@ mod test { } #[test] - fn test_requires_version() { + fn test_required_version() { assert_eq!( ProtocolVersion::V1, - ExtXStreamInf::new("http://www.example.com", 1000).requires_version() + ExtXStreamInf::new("http://www.example.com", 1000).required_version() ); } diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 6619db1..28dda75 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,40 +1,39 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; /// [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, -} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ExtXDiscontinuitySequence(u64); impl ExtXDiscontinuitySequence { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; /// Makes a new `ExtXDiscontinuitySequence` tag. pub const fn new(seq_num: u64) -> Self { - ExtXDiscontinuitySequence { seq_num } + Self(seq_num) } /// Returns the discontinuity sequence number of /// the first media segment that appears in the associated playlist. - pub const fn seq_num(self) -> u64 { - self.seq_num + pub const fn seq_num(&self) -> u64 { + self.0 } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(self) -> ProtocolVersion { +impl RequiredVersion for ExtXDiscontinuitySequence { + fn required_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) + write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -42,7 +41,7 @@ impl FromStr for ExtXDiscontinuitySequence { type Err = crate::Error; fn from_str(input: &str) -> Result { - let seq_num = tag(input, Self::PREFIX)?.parse().unwrap(); // TODO! + let seq_num = tag(input, Self::PREFIX)?.parse()?; Ok(Self::new(seq_num)) } } @@ -52,11 +51,26 @@ 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); + fn test_display() { + assert_eq!( + ExtXDiscontinuitySequence::new(123).to_string(), + "#EXT-X-DISCONTINUITY-SEQUENCE:123".to_string() + ); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXDiscontinuitySequence::new(123).required_version(), + ProtocolVersion::V1 + ) + } + + #[test] + fn test_parser() { + assert_eq!( + ExtXDiscontinuitySequence::new(123), + "#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap() + ); } } diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index b34b357..c4fcc5b 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -10,11 +10,13 @@ use crate::Error; /// [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 const fn requires_version(self) -> ProtocolVersion { +impl RequiredVersion for ExtXEndList { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,11 +41,17 @@ 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); + fn test_display() { + assert_eq!(ExtXEndList.to_string(), "#EXT-X-ENDLIST".to_string()); + } + + #[test] + fn test_parser() { + assert_eq!(ExtXEndList, "#EXT-X-ENDLIST".parse().unwrap()); + } + + #[test] + fn test_required_version() { + assert_eq!(ExtXEndList.required_version(), ProtocolVersion::V1); } } diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index dfe3abc..796c68f 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -13,9 +13,10 @@ 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 const fn requires_version(self) -> ProtocolVersion { +impl RequiredVersion for ExtXIFramesOnly { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } @@ -40,11 +41,20 @@ 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); + fn test_display() { + assert_eq!( + ExtXIFramesOnly.to_string(), + "#EXT-X-I-FRAMES-ONLY".to_string(), + ) + } + + #[test] + fn test_parser() { + assert_eq!(ExtXIFramesOnly, "#EXT-X-I-FRAMES-ONLY".parse().unwrap(),) + } + + #[test] + fn test_required_version() { + assert_eq!(ExtXIFramesOnly.required_version(), ProtocolVersion::V4) } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 1f328f0..db4301e 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -9,32 +9,32 @@ use crate::Error; /// /// [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, -} +pub struct ExtXMediaSequence(u64); impl ExtXMediaSequence { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; /// Makes a new `ExtXMediaSequence` tag. pub const fn new(seq_num: u64) -> Self { - ExtXMediaSequence { seq_num } + Self(seq_num) } - /// Returns the sequence number of the first media segment that appears in the associated playlist. - pub const fn seq_num(self) -> u64 { - self.seq_num + /// Returns the sequence number of the first media segment, + /// that appears in the associated playlist. + pub const fn seq_num(&self) -> u64 { + self.0 } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(self) -> ProtocolVersion { +impl RequiredVersion for ExtXMediaSequence { + fn required_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) + write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -43,7 +43,6 @@ impl FromStr for ExtXMediaSequence { fn from_str(input: &str) -> Result { let seq_num = tag(input, Self::PREFIX)?.parse()?; - Ok(ExtXMediaSequence::new(seq_num)) } } @@ -53,11 +52,26 @@ 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); + fn test_display() { + assert_eq!( + ExtXMediaSequence::new(123).to_string(), + "#EXT-X-MEDIA-SEQUENCE:123".to_string() + ); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXMediaSequence::new(123).required_version(), + ProtocolVersion::V1 + ); + } + + #[test] + fn test_parser() { + assert_eq!( + ExtXMediaSequence::new(123), + "#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap() + ); } } diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 221914f..e0b7ce8 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -31,9 +31,10 @@ pub enum ExtXPlaylistType { impl ExtXPlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXPlaylistType { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -99,13 +100,13 @@ mod test { } #[test] - fn test_requires_version() { + fn test_required_version() { assert_eq!( - ExtXPlaylistType::Vod.requires_version(), + ExtXPlaylistType::Vod.required_version(), ProtocolVersion::V1 ); assert_eq!( - ExtXPlaylistType::Event.requires_version(), + ExtXPlaylistType::Event.required_version(), ProtocolVersion::V1 ); } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 71b0c66..522616d 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -10,9 +10,7 @@ use crate::Error; /// /// [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, Default)] -pub struct ExtXTargetDuration { - duration: Duration, -} +pub struct ExtXTargetDuration(Duration); impl ExtXTargetDuration { pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; @@ -21,24 +19,24 @@ impl ExtXTargetDuration { /// /// Note that the nanoseconds part of the `duration` will be discarded. pub const fn new(duration: Duration) -> Self { - let duration = Duration::from_secs(duration.as_secs()); - ExtXTargetDuration { duration } + Self(Duration::from_secs(duration.as_secs())) } /// Returns the maximum media segment duration in the associated playlist. pub const fn duration(&self) -> Duration { - self.duration + self.0 } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXTargetDuration { + fn required_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()) + write!(f, "{}{}", Self::PREFIX, self.0.as_secs()) } } @@ -47,9 +45,7 @@ impl FromStr for ExtXTargetDuration { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?.parse()?; - Ok(ExtXTargetDuration { - duration: Duration::from_secs(input), - }) + Ok(Self::new(Duration::from_secs(input))) } } @@ -58,11 +54,26 @@ 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); + fn test_display() { + assert_eq!( + ExtXTargetDuration::new(Duration::from_secs(5)).to_string(), + "#EXT-X-TARGETDURATION:5".to_string() + ); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXTargetDuration::new(Duration::from_secs(5)).required_version(), + ProtocolVersion::V1 + ); + } + + #[test] + fn test_parser() { + assert_eq!( + ExtXTargetDuration::new(Duration::from_secs(5)), + "#EXT-X-TARGETDURATION:5".parse().unwrap() + ); } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index c1bf2ce..e0d02b7 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -2,7 +2,7 @@ use std::fmt; use std::ops::Deref; use std::str::FromStr; -use crate::types::{ByteRange, ProtocolVersion}; +use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -16,6 +16,7 @@ impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; /// Makes a new `ExtXByteRange` tag. + /// /// # Example /// ``` /// use hls_m3u8::tags::ExtXByteRange; @@ -27,6 +28,7 @@ impl ExtXByteRange { } /// Converts the [ExtXByteRange] to a [ByteRange]. + /// /// # Example /// ``` /// use hls_m3u8::tags::ExtXByteRange; @@ -38,17 +40,10 @@ impl ExtXByteRange { pub const fn to_range(&self) -> ByteRange { self.0 } +} - /// Returns the protocol compatibility version that this tag requires. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXByteRange; - /// use hls_m3u8::types::ProtocolVersion; - /// - /// let byte_range = ExtXByteRange::new(20, Some(5)); - /// assert_eq!(byte_range.requires_version(), ProtocolVersion::V4); - /// ``` - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXByteRange { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } @@ -76,6 +71,7 @@ impl FromStr for ExtXByteRange { let input = tag(input, Self::PREFIX)?; let tokens = input.splitn(2, '@').collect::>(); + if tokens.is_empty() { return Err(Error::invalid_input()); } @@ -135,4 +131,12 @@ mod test { assert_eq!(byte_range.length(), 0); assert_eq!(byte_range.start(), Some(22)); } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXByteRange::new(20, Some(5)).required_version(), + ProtocolVersion::V4 + ); + } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 4f17db3..2b6619a 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -6,7 +6,7 @@ use std::time::Duration; use chrono::{DateTime, FixedOffset}; use crate::attribute::AttributePairs; -use crate::types::{DecimalFloatingPoint, ProtocolVersion}; +use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; @@ -63,9 +63,10 @@ pub struct ExtXDateRange { impl ExtXDateRange { pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXDateRange { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 9571f2c..2289a24 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; -use crate::{Error, Result}; +use crate::Error; /// [4.3.2.3. EXT-X-DISCONTINUITY] /// @@ -13,9 +13,10 @@ pub struct ExtXDiscontinuity; impl ExtXDiscontinuity { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(self) -> ProtocolVersion { +impl RequiredVersion for ExtXDiscontinuity { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -29,7 +30,7 @@ impl fmt::Display for ExtXDiscontinuity { impl FromStr for ExtXDiscontinuity { type Err = Error; - fn from_str(input: &str) -> Result { + fn from_str(input: &str) -> Result { tag(input, Self::PREFIX)?; Ok(ExtXDiscontinuity) } @@ -40,10 +41,20 @@ 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); + fn test_display() { + assert_eq!( + ExtXDiscontinuity.to_string(), + "#EXT-X-DISCONTINUITY".to_string(), + ) + } + + #[test] + fn test_parser() { + assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap(),) + } + + #[test] + fn test_required_version() { + assert_eq!(ExtXDiscontinuity.required_version(), ProtocolVersion::V1) } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 287864b..af0178a 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use crate::types::{DecimalFloatingPoint, ProtocolVersion}; +use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -78,9 +78,10 @@ impl ExtInf { pub fn title(&self) -> Option<&String> { self.title.as_ref() } +} - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtInf { + fn required_version(&self) -> ProtocolVersion { if self.duration.subsec_nanos() == 0 { ProtocolVersion::V1 } else { @@ -206,13 +207,13 @@ mod test { } #[test] - fn test_requires_version() { + fn test_required_version() { assert_eq!( - ExtInf::new(Duration::from_secs(4)).requires_version(), + ExtInf::new(Duration::from_secs(4)).required_version(), ProtocolVersion::V1 ); assert_eq!( - ExtInf::new(Duration::from_millis(4400)).requires_version(), + ExtInf::new(Duration::from_millis(4400)).required_version(), ProtocolVersion::V3 ); } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index df6659c..d680f56 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ByteRange, ProtocolVersion}; +use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; @@ -43,9 +43,10 @@ impl ExtXMap { pub const fn range(&self) -> Option { self.range } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXMap { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } } @@ -93,19 +94,37 @@ mod test { use super::*; #[test] - fn ext_x_map() { - let tag = ExtXMap::new("foo"); - 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); + fn test_display() { + assert_eq!( + ExtXMap::new("foo").to_string(), + "#EXT-X-MAP:URI=\"foo\"".to_string(), + ); - let tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2))); - let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#; - ExtXMap::from_str(text).unwrap(); + assert_eq!( + ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).to_string(), + "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(), + ); + } - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V6); + #[test] + fn test_parser() { + assert_eq!( + ExtXMap::new("foo"), + "#EXT-X-MAP:URI=\"foo\"".parse().unwrap() + ); + + assert_eq!( + ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap() + ); + } + + #[test] + fn test_required_version() { + assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6); + assert_eq!( + ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).required_version(), + ProtocolVersion::V6 + ); } } diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 643ec4b..2f852f6 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use chrono::{DateTime, FixedOffset}; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -25,9 +25,10 @@ impl ExtXProgramDateTime { pub const fn date_time(&self) -> &DateTime { &self.0 } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXProgramDateTime { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -45,7 +46,6 @@ impl FromStr for ExtXProgramDateTime { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - // TODO: parse with chrono let date_time = DateTime::parse_from_rfc3339(input)?; Ok(Self::new(date_time)) } @@ -54,6 +54,9 @@ impl FromStr for ExtXProgramDateTime { #[cfg(test)] mod test { use super::*; + use chrono::TimeZone; + + const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds #[test] fn test_display() { @@ -71,19 +74,26 @@ mod test { #[test] fn test_parser() { - "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00" - .parse::() - .unwrap(); + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + ), + "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00" + .parse::() + .unwrap() + ); } #[test] - fn test_requires_version() { - let date_time = "2010-02-19T14:54:23.031+08:00" - .parse::>() - .unwrap(); + fn test_required_version() { + let program_date_time = ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ); - let program_date_time = ExtXProgramDateTime::new(date_time); - - assert_eq!(program_date_time.requires_version(), ProtocolVersion::V1); + assert_eq!(program_date_time.required_version(), ProtocolVersion::V1); } } diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 5268dcd..7967a8d 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -10,11 +10,13 @@ use crate::Error; /// [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 const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXIndependentSegments { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,11 +41,26 @@ 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); + fn test_display() { + assert_eq!( + ExtXIndependentSegments.to_string(), + "#EXT-X-INDEPENDENT-SEGMENTS".to_string(), + ) + } + + #[test] + fn test_parser() { + assert_eq!( + ExtXIndependentSegments, + "#EXT-X-INDEPENDENT-SEGMENTS".parse().unwrap(), + ) + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXIndependentSegments.required_version(), + ProtocolVersion::V1 + ) } } diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index a4c594a..e30fa9c 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; +use crate::types::{ProtocolVersion, RequiredVersion, SignedDecimalFloatingPoint}; use crate::utils::{parse_yes_or_no, tag}; use crate::Error; @@ -19,6 +19,7 @@ impl ExtXStart { pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; /// Makes a new `ExtXStart` tag. + /// /// # Panic /// Panics if the time_offset value is infinite. pub fn new(time_offset: f64) -> Self { @@ -33,6 +34,7 @@ impl ExtXStart { } /// Makes a new `ExtXStart` tag with the given `precise` flag. + /// /// # Panic /// Panics if the time_offset value is infinite. pub fn with_precise(time_offset: f64, precise: bool) -> Self { @@ -56,9 +58,10 @@ impl ExtXStart { pub const fn precise(&self) -> bool { self.precise } +} - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for ExtXStart { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -108,17 +111,41 @@ mod test { use super::*; #[test] - fn ext_x_start() { - let tag = ExtXStart::new(-1.23); - let text = "#EXT-X-START:TIME-OFFSET=-1.23"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_display() { + assert_eq!( + ExtXStart::new(-1.23).to_string(), + "#EXT-X-START:TIME-OFFSET=-1.23".to_string(), + ); - let tag = ExtXStart::with_precise(1.23, true); - let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + assert_eq!( + ExtXStart::with_precise(1.23, true).to_string(), + "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".to_string(), + ); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXStart::new(-1.23).required_version(), + ProtocolVersion::V1, + ); + + assert_eq!( + ExtXStart::with_precise(1.23, true).required_version(), + ProtocolVersion::V1, + ); + } + + #[test] + fn test_parser() { + assert_eq!( + ExtXStart::new(-1.23), + "#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(), + ); + + assert_eq!( + ExtXStart::with_precise(1.23, true), + "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(), + ); } } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index af08b24..8cff64a 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -70,6 +70,18 @@ impl FromStr for DecimalFloatingPoint { } } +impl From for DecimalFloatingPoint { + fn from(value: f64) -> Self { + Self(value) + } +} + +impl From for DecimalFloatingPoint { + fn from(value: f32) -> Self { + Self(value.into()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 8e2ec86..438eace 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, RequiredVersion}; use crate::utils::{quote, unquote}; use crate::Error; @@ -292,23 +292,10 @@ impl DecryptionKey { pub fn set_key_format_versions(&mut self, value: T) { self.key_format_versions = Some(value.to_string()); } +} - /// Returns the protocol compatibility version that this tag requires. - /// # Example - /// ``` - /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion, DecryptionKey}; - /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); - /// - /// assert_eq!( - /// key.requires_version(), - /// ProtocolVersion::V1 - /// ); - /// ``` - pub fn requires_version(&self) -> ProtocolVersion { +impl RequiredVersion for DecryptionKey { + fn required_version(&self) -> ProtocolVersion { if self.key_format.is_some() || self.key_format_versions.is_some() { ProtocolVersion::V5 } else if self.iv.is_some() { @@ -464,4 +451,13 @@ mod test { key ) } + + #[test] + fn test_required_version() { + assert_eq!( + DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") + .required_version(), + ProtocolVersion::V1 + ) + } } diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 1d54938..a39747d 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -3,6 +3,30 @@ use std::str::FromStr; use crate::Error; +/// # Example +/// Implementing it: +/// ``` +/// # use hls_m3u8::types::{ProtocolVersion, RequiredVersion}; +/// # +/// struct NewTag(u64); +/// +/// impl RequiredVersion for NewTag { +/// fn required_version(&self) -> ProtocolVersion { +/// if self.0 == 5 { +/// ProtocolVersion::V4 +/// } else { +/// ProtocolVersion::V1 +/// } +/// } +/// } +/// assert_eq!(NewTag(5).required_version(), ProtocolVersion::V4); +/// assert_eq!(NewTag(2).required_version(), ProtocolVersion::V1); +/// ``` +pub trait RequiredVersion { + /// Returns the protocol compatibility version that this tag requires. + fn required_version(&self) -> ProtocolVersion; +} + /// [7. Protocol Version Compatibility] /// /// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7 @@ -29,13 +53,13 @@ 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, + Self::V1 => 1, + Self::V2 => 2, + Self::V3 => 3, + Self::V4 => 4, + Self::V5 => 5, + Self::V6 => 6, + Self::V7 => 7, } }; write!(f, "{}", n) @@ -47,16 +71,49 @@ impl FromStr for ProtocolVersion { fn from_str(input: &str) -> Result { Ok({ - match input { - "1" => ProtocolVersion::V1, - "2" => ProtocolVersion::V2, - "3" => ProtocolVersion::V3, - "4" => ProtocolVersion::V4, - "5" => ProtocolVersion::V5, - "6" => ProtocolVersion::V6, - "7" => ProtocolVersion::V7, + match input.trim() { + "1" => Self::V1, + "2" => Self::V2, + "3" => Self::V3, + "4" => Self::V4, + "5" => Self::V5, + "6" => Self::V6, + "7" => Self::V7, _ => return Err(Error::unknown_protocol_version(input)), } }) } } + +impl Default for ProtocolVersion { + fn default() -> Self { + Self::V1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!(ProtocolVersion::V1.to_string(), "1".to_string()); + assert_eq!(ProtocolVersion::V2.to_string(), "2".to_string()); + assert_eq!(ProtocolVersion::V3.to_string(), "3".to_string()); + assert_eq!(ProtocolVersion::V4.to_string(), "4".to_string()); + assert_eq!(ProtocolVersion::V5.to_string(), "5".to_string()); + assert_eq!(ProtocolVersion::V6.to_string(), "6".to_string()); + assert_eq!(ProtocolVersion::V7.to_string(), "7".to_string()); + } + + #[test] + fn test_parser() { + assert_eq!(ProtocolVersion::V1, "1".parse().unwrap()); + assert_eq!(ProtocolVersion::V2, "2".parse().unwrap()); + assert_eq!(ProtocolVersion::V3, "3".parse().unwrap()); + assert_eq!(ProtocolVersion::V4, "4".parse().unwrap()); + assert_eq!(ProtocolVersion::V5, "5".parse().unwrap()); + assert_eq!(ProtocolVersion::V6, "6".parse().unwrap()); + assert_eq!(ProtocolVersion::V7, "7".parse().unwrap()); + } +} From 324041730454ffeb92eb9d74a96e507aa20e5906 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 22 Sep 2019 12:56:28 +0200 Subject: [PATCH 2/3] replaced builder with `derive_builder` #14 --- src/tags/master_playlist/media.rs | 241 +++++++++--------------------- 1 file changed, 69 insertions(+), 172 deletions(-) diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index ed71581..4cf05ce 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,195 +1,87 @@ use std::fmt; use std::str::FromStr; +use derive_builder::Builder; + use crate::attribute::AttributePairs; use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::Error; -/// `ExtXMedia` builder. -#[derive(Debug, Clone)] -pub struct ExtXMediaBuilder { - media_type: Option, +/// [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(Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[builder(setter(into))] +#[builder(build_fn(validate = "Self::validate"))] +pub struct ExtXMedia { + /// Sets the media type of the rendition. + media_type: MediaType, + #[builder(setter(strip_option, into), default)] + /// Sets the URI that identifies the media playlist. uri: Option, - group_id: Option, + /// Sets the identifier that specifies the group to which the rendition belongs. + group_id: String, + /// Sets the name of the primary language used in the rendition. + #[builder(setter(strip_option, into), default)] language: Option, + /// Sets the name of a language associated with the rendition. + #[builder(setter(strip_option, into), default)] assoc_language: Option, - name: Option, - default: bool, - autoselect: Option, - forced: Option, + /// Sets a human-readable description of the rendition. + name: String, + /// Sets the value of the `default` flag. + #[builder(default)] + is_default: bool, + /// Sets the value of the `autoselect` flag. + #[builder(default)] + is_autoselect: bool, + /// Sets the value of the `forced` flag. + #[builder(default)] + is_forced: bool, + /// Sets the identifier that specifies a rendition within the segments in the media playlist. + #[builder(setter(strip_option, into), default)] instream_id: Option, + /// Sets the string that represents uniform type identifiers (UTI). + #[builder(setter(strip_option, into), default)] characteristics: Option, + /// Sets the string that represents the parameters of the rendition. + #[builder(setter(strip_option, into), default)] channels: Option, } impl ExtXMediaBuilder { - /// Makes a `ExtXMediaBuilder` instance. - pub const 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: T) -> &mut Self { - self.group_id = Some(group_id.to_string()); - self - } - - /// Sets a human-readable description of the rendition. - pub fn name(&mut self, name: T) -> &mut Self { - self.name = Some(name.to_string()); - self - } - - /// Sets the URI that identifies the media playlist. - pub fn uri(&mut self, uri: T) -> &mut Self { - self.uri = Some(uri.to_string()); - self - } - - /// Sets the name of the primary language used in the rendition. - pub fn language(&mut self, language: T) -> &mut Self { - self.language = Some(language.to_string()); - self - } - - /// Sets the name of a language associated with the rendition. - pub fn assoc_language(&mut self, language: T) -> &mut Self { - self.assoc_language = Some(language.to_string()); - 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: T) -> &mut Self { - self.characteristics = Some(characteristics.to_string()); - self - } - - /// Sets the string that represents the parameters of the rendition. - pub fn channels(&mut self, channels: T) -> &mut Self { - self.channels = Some(channels.to_string()); - self - } - - /// Builds a `ExtXMedia` instance. - pub fn finish(self) -> crate::Result { + fn validate(&self) -> Result<(), String> { let media_type = self .media_type - .ok_or(Error::missing_value("self.media_type"))?; - let group_id = self.group_id.ok_or(Error::missing_value("self.group_id"))?; - let name = self.name.ok_or(Error::missing_value("self.name"))?; + .ok_or(Error::missing_value("self.media_type").to_string())?; if MediaType::ClosedCaptions == media_type { if let None = self.uri { - return Err(Error::missing_value("self.uri")); + return Err(Error::missing_value("self.uri").to_string()); } self.instream_id - .ok_or(Error::missing_value("self.instream_id"))?; + .ok_or(Error::missing_value("self.instream_id").to_string())?; } else { if let Some(_) = &self.instream_id { - Err(Error::invalid_input())?; + return Err(Error::invalid_input().to_string()); } } - if self.default && self.autoselect.is_some() { - if let Some(value) = &self.autoselect { - if *value { - Err(Error::invalid_input())?; - } - } + if self.is_default.unwrap_or(false) && self.is_autoselect.unwrap_or(false) { + return Err(Error::invalid_input().to_string()); } if MediaType::Subtitles != media_type { - if self.forced.is_some() { - Err(Error::invalid_input())?; + if self.is_forced.is_some() { + return Err(Error::invalid_input().to_string()); } } - 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, - }) + Ok(()) } } -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: String, - language: Option, - assoc_language: Option, - name: String, - default: bool, - autoselect: bool, - forced: bool, - instream_id: Option, - characteristics: Option, - channels: Option, -} - impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; @@ -202,15 +94,20 @@ impl ExtXMedia { language: None, assoc_language: None, name: name.to_string(), - default: false, - autoselect: false, - forced: false, + is_default: false, + is_autoselect: false, + is_forced: false, instream_id: None, characteristics: None, channels: None, } } + /// Makes a [ExtXMediaBuilder] for [ExtXMedia]. + pub fn builder() -> ExtXMediaBuilder { + ExtXMediaBuilder::default() + } + /// Returns the type of the media associated with this tag. pub const fn media_type(&self) -> MediaType { self.media_type @@ -242,19 +139,19 @@ impl ExtXMedia { } /// Returns whether this is the default rendition. - pub const fn default(&self) -> bool { - self.default + pub const fn is_default(&self) -> bool { + self.is_default } /// Returns whether the client may choose to /// play this rendition in the absence of explicit user preference. pub const fn autoselect(&self) -> bool { - self.autoselect + self.is_autoselect } /// Returns whether the rendition contains content that is considered essential to play. - pub const fn forced(&self) -> bool { - self.forced + pub const fn is_forced(&self) -> bool { + self.is_forced } /// Returns the identifier that specifies a rendition within the segments in the media playlist. @@ -303,13 +200,13 @@ impl fmt::Display for ExtXMedia { write!(f, ",ASSOC-LANGUAGE={}", quote(value))?; } write!(f, ",NAME={}", quote(&self.name))?; - if self.default { + if self.is_default { write!(f, ",DEFAULT=YES")?; } - if self.autoselect { + if self.is_autoselect { write!(f, ",AUTOSELECT=YES")?; } - if self.forced { + if self.is_forced { write!(f, ",FORCED=YES")?; } if let Some(value) = &self.instream_id { @@ -331,12 +228,12 @@ impl FromStr for ExtXMedia { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - let mut builder = ExtXMediaBuilder::new(); + let mut builder = ExtXMedia::builder(); for (key, value) in input.parse::()? { match key.as_str() { "TYPE" => { - builder.media_type(value.parse()?); + builder.media_type(value.parse::()?); } "URI" => { builder.uri(unquote(value)); @@ -354,16 +251,16 @@ impl FromStr for ExtXMedia { builder.name(unquote(value)); } "DEFAULT" => { - builder.default((parse_yes_or_no(value))?); + builder.is_default(parse_yes_or_no(value)?); } "AUTOSELECT" => { - builder.autoselect((parse_yes_or_no(value))?); + builder.is_autoselect(parse_yes_or_no(value)?); } "FORCED" => { - builder.forced((parse_yes_or_no(value))?); + builder.is_forced(parse_yes_or_no(value)?); } "INSTREAM-ID" => { - builder.instream_id(unquote(value).parse()?); + builder.instream_id(unquote(value).parse::()?); } "CHARACTERISTICS" => { builder.characteristics(unquote(value)); @@ -377,7 +274,7 @@ impl FromStr for ExtXMedia { } } } - (builder.finish()) + builder.build().map_err(Error::builder_error) } } From 0c4fa008e6fc37cb9e2bc6f43ae9c4c3cd9b35d1 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 22 Sep 2019 18:00:38 +0200 Subject: [PATCH 3/3] more documentation #31 + tests #25 --- src/attribute.rs | 2 +- src/error.rs | 8 + src/tags/basic/m3u.rs | 12 +- src/tags/basic/version.rs | 31 +++- .../master_playlist/i_frame_stream_inf.rs | 24 ++- src/tags/master_playlist/media.rs | 87 +++++++-- src/tags/master_playlist/session_data.rs | 127 +++++++++---- src/tags/master_playlist/session_key.rs | 52 ++++-- src/tags/master_playlist/stream_inf.rs | 4 +- .../media_playlist/discontinuity_sequence.rs | 50 +++++- src/tags/media_playlist/end_list.rs | 14 +- src/tags/media_playlist/i_frames_only.rs | 16 +- src/tags/media_playlist/media_sequence.rs | 47 ++++- src/tags/media_playlist/playlist_type.rs | 19 +- src/tags/media_playlist/target_duration.rs | 20 ++- src/tags/media_segment/byte_range.rs | 44 ++++- src/tags/media_segment/date_range.rs | 4 +- src/tags/media_segment/discontinuity.rs | 15 +- src/tags/media_segment/inf.rs | 130 ++++++++++---- src/tags/media_segment/key.rs | 35 +++- src/tags/media_segment/map.rs | 13 +- src/tags/media_segment/program_date_time.rs | 75 ++++++-- src/tags/mod.rs | 86 --------- src/types/decryption_key.rs | 160 ++++++++++------- src/types/key_format.rs | 68 +++++++ src/types/key_format_versions.rs | 170 ++++++++++++++++++ src/types/mod.rs | 4 + src/utils.rs | 2 +- 28 files changed, 984 insertions(+), 335 deletions(-) create mode 100644 src/types/key_format.rs create mode 100644 src/types/key_format_versions.rs diff --git a/src/attribute.rs b/src/attribute.rs index d0cf36e..2da9be6 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -110,7 +110,7 @@ mod test { #[test] fn test_parser() { - let pairs = ("FOO=BAR,BAR=\"baz,qux\",ABC=12.3") + let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3" .parse::() .unwrap(); diff --git a/src/error.rs b/src/error.rs index c2db7c2..14c43f4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -69,6 +69,10 @@ pub enum ErrorKind { /// An Error from a Builder. BuilderError(String), + #[fail(display = "Missing Attribute: {}", _0)] + /// An attribute is missing. + MissingAttribute(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -185,6 +189,10 @@ impl Error { pub(crate) fn chrono(value: T) -> Self { Self::from(ErrorKind::ChronoParseError(value.to_string())) } + + pub(crate) fn missing_attribute(value: T) -> Self { + Self::from(ErrorKind::MissingAttribute(value.to_string())) + } } impl From<::std::num::ParseIntError> for Error { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index d64939c..ac870b1 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -5,10 +5,18 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.1.1. EXTM3U] +/// # [4.3.1.1. EXTM3U] +/// The [ExtM3u] tag indicates that the file is an Extended [M3U] +/// Playlist file. /// +/// Its format is: +/// ```text +/// #EXTM3U +/// ``` +/// +/// [M3U]: https://en.wikipedia.org/wiki/M3U /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ExtM3u; impl ExtM3u { diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index b5afef2..bff523f 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -5,21 +5,40 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.1.2. EXT-X-VERSION] +/// # [4.3.1.2. EXT-X-VERSION] +/// The [ExtXVersion] tag indicates the compatibility version of the +/// Playlist file, its associated media, and its server. +/// +/// The [ExtXVersion] tag applies to the entire Playlist file. Its +/// format is: +/// +/// ```text +/// #EXT-X-VERSION: +/// ``` +/// where `n` is an integer indicating the protocol compatibility version +/// number. /// /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct ExtXVersion(ProtocolVersion); impl ExtXVersion { pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:"; - /// Makes a new `ExtXVersion` tag. + /// Makes a new [ExtXVersion] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXVersion; + /// use hls_m3u8::types::ProtocolVersion; + /// + /// let version_tag = ExtXVersion::new(ProtocolVersion::V2); + /// ``` pub const fn new(version: ProtocolVersion) -> Self { Self(version) } - /// Returns the protocol compatibility version of the playlist containing this tag. + /// Returns the protocol compatibility version of the playlist, containing this tag. /// /// # Example /// ``` @@ -84,8 +103,8 @@ mod test { #[test] fn test_parser() { assert_eq!( - "#EXT-X-VERSION:6".parse().ok(), - Some(ExtXVersion::new(ProtocolVersion::V6)) + "#EXT-X-VERSION:6".parse::().unwrap(), + ExtXVersion::new(ProtocolVersion::V6) ); } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index fc38814..167b358 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -10,7 +10,7 @@ use crate::Error; /// # [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] /// The [ExtXIFrameStreamInf] tag identifies a [Media Playlist] file /// containing the I-frames of a multimedia presentation. It stands -/// alone, in that it does not apply to a particular URI in the [Master Playlist]. +/// alone, in that it does not apply to a particular `URI` in the [Master Playlist]. /// /// Its format is: /// @@ -38,7 +38,7 @@ impl ExtXIFrameStreamInf { } } - /// Returns the URI, that identifies the associated media playlist. + /// Returns the `URI`, that identifies the associated media playlist. /// /// # Example /// ``` @@ -51,7 +51,7 @@ impl ExtXIFrameStreamInf { &self.uri } - /// Sets the URI, that identifies the associated media playlist. + /// Sets the `URI`, that identifies the associated media playlist. /// /// # Example /// ``` @@ -126,22 +126,20 @@ mod test { #[test] fn test_display() { - let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text); + assert_eq!( + ExtXIFrameStreamInf::new("foo", 1000).to_string(), + "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string() + ); } #[test] fn test_parser() { - let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - let i_frame_stream_inf = ExtXIFrameStreamInf::new("foo", 1000); assert_eq!( - text.parse::().unwrap(), - i_frame_stream_inf.clone() + "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000" + .parse::() + .unwrap(), + ExtXIFrameStreamInf::new("foo", 1000) ); - - assert_eq!(i_frame_stream_inf.uri(), "foo"); - assert_eq!(i_frame_stream_inf.bandwidth(), 1000); - // TODO: test all the optional fields } #[test] diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 4cf05ce..9232b25 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -8,9 +8,23 @@ use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::Error; -/// [4.3.4.1. EXT-X-MEDIA] +/// # [4.4.4.1. EXT-X-MEDIA] +/// The [ExtXMedia] tag is used to relate [Media Playlist]s that contain +/// alternative Renditions of the same content. For +/// example, three [ExtXMedia] tags can be used to identify audio-only +/// [Media Playlist]s, that contain English, French, and Spanish Renditions +/// of the same presentation. Or, two [ExtXMedia] tags can be used to +/// identify video-only [Media Playlist]s that show two different camera +/// angles. /// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +/// Its format is: +/// ```text +/// #EXT-X-MEDIA: +/// ``` +/// +/// [Media Playlist]: crate::MediaPlaylist +/// [4.4.4.1. EXT-X-MEDIA]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.4.1 #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] @@ -54,22 +68,27 @@ impl ExtXMediaBuilder { fn validate(&self) -> Result<(), String> { let media_type = self .media_type - .ok_or(Error::missing_value("self.media_type").to_string())?; + .ok_or(Error::missing_attribute("MEDIA-TYPE").to_string())?; if MediaType::ClosedCaptions == media_type { - if let None = self.uri { - return Err(Error::missing_value("self.uri").to_string()); + if self.uri.is_some() { + return Err(Error::custom( + "Unexpected attribute: \"URL\" for MediaType::ClosedCaptions!", + ) + .to_string()); } self.instream_id - .ok_or(Error::missing_value("self.instream_id").to_string())?; + .ok_or(Error::missing_attribute("INSTREAM-ID").to_string())?; } else { - if let Some(_) = &self.instream_id { - return Err(Error::invalid_input().to_string()); + if self.instream_id.is_some() { + return Err(Error::custom("Unexpected attribute: \"INSTREAM-ID\"!").to_string()); } } - if self.is_default.unwrap_or(false) && self.is_autoselect.unwrap_or(false) { - return Err(Error::invalid_input().to_string()); + if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { + return Err( + Error::custom("If `DEFAULT` is true, `AUTOSELECT` has to be true too!").to_string(), + ); } if MediaType::Subtitles != media_type { @@ -85,7 +104,7 @@ impl ExtXMediaBuilder { impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; - /// Makes a new `ExtXMedia` tag. + /// Makes a new [ExtXMedia] tag. pub fn new(media_type: MediaType, group_id: T, name: T) -> Self { ExtXMedia { media_type, @@ -284,6 +303,52 @@ mod test { #[test] fn test_display() { + // TODO: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .to_string() + ); + assert_eq!( ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 09d74ae..b97b9e4 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -22,11 +22,12 @@ pub enum SessionData { Uri(String), } -/// [4.3.4.4. EXT-X-SESSION-DATA] +/// # [4.3.4.4. EXT-X-SESSION-DATA] /// /// The [ExtXSessionData] tag allows arbitrary session data to be -/// carried in a [Master Playlist](crate::MasterPlaylist). +/// carried in a [Master Playlist]. /// +/// [Master Playlist]: crate::MasterPlaylist /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 #[derive(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[builder(setter(into))] @@ -319,40 +320,73 @@ mod test { #[test] fn test_display() { - let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(tag.to_string(), text); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"".to_string(), + ExtXSessionData::new( + "com.example.lyrics", + SessionData::Uri("lyrics.json".to_string()) + ) + .to_string() + ); - let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(tag.to_string(), text); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + VALUE=\"This is an example\",LANGUAGE=\"en\"" + .to_string(), + ExtXSessionData::with_language( + "com.example.title", + SessionData::Value("This is an example".to_string()), + "en" + ) + .to_string() + ); - let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(tag.to_string(), text); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + VALUE=\"Este es un ejemplo\",LANGUAGE=\"es\"" + .to_string(), + ExtXSessionData::with_language( + "com.example.title", + SessionData::Value("Este es un ejemplo".to_string()), + "es" + ) + .to_string() + ); + + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"".to_string(), + ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string() + ); + + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"".to_string(), + ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string() + ); + + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"".to_string(), + ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") + .to_string() + ); } #[test] fn test_parser() { - let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"" - .parse::() - .unwrap(); - assert_eq!( - tag, + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"" + .parse::() + .unwrap(), ExtXSessionData::new( "com.example.lyrics", SessionData::Uri("lyrics.json".to_string()) ) ); - let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - LANGUAGE=\"en\", VALUE=\"This is an example\"" - .parse::() - .unwrap(); - assert_eq!( - tag, + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + LANGUAGE=\"en\", VALUE=\"This is an example\"" + .parse::() + .unwrap(), ExtXSessionData::with_language( "com.example.title", SessionData::Value("This is an example".to_string()), @@ -360,13 +394,11 @@ mod test { ) ); - let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\"" - .parse::() - .unwrap(); - assert_eq!( - tag, + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\"" + .parse::() + .unwrap(), ExtXSessionData::with_language( "com.example.title", SessionData::Value("Este es un ejemplo".to_string()), @@ -374,16 +406,37 @@ mod test { ) ); - let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(text.parse::().unwrap(), tag); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"" + .parse::() + .unwrap(), + ExtXSessionData::new("foo", SessionData::Value("bar".into())) + ); - let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(text.parse::().unwrap(), tag); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"" + .parse::() + .unwrap(), + ExtXSessionData::new("foo", SessionData::Uri("bar".into())) + ); - let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(text.parse::().unwrap(), tag); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"" + .parse::() + .unwrap(), + ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") + ); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXSessionData::new( + "com.example.lyrics", + SessionData::Uri("lyrics.json".to_string()) + ) + .required_version(), + ProtocolVersion::V1 + ); } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index ba1d9fc..0162213 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -6,8 +6,19 @@ use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVer use crate::utils::tag; use crate::Error; -/// [4.3.4.5. EXT-X-SESSION-KEY] +/// # [4.3.4.5. EXT-X-SESSION-KEY] +/// The [ExtXSessionKey] tag allows encryption keys from [Media Playlist]s +/// to be specified in a [Master Playlist]. This allows the client to +/// preload these keys without having to read the [Media Playlist]s +/// first. /// +/// Its format is: +/// ```text +/// #EXT-X-SESSION-KEY: +/// ``` +/// +/// [Media Playlist]: crate::MediaPlaylist +/// [Master Playlist]: crate::MasterPlaylist /// [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(DecryptionKey); @@ -16,8 +27,21 @@ impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; /// Makes a new [ExtXSessionKey] tag. + /// /// # Panic - /// This method will panic, if the [EncryptionMethod] is None. + /// An [ExtXSessionKey] should only be used, if the segments of the stream are encrypted. + /// Therefore this function will panic, if the `method` is [EncryptionMethod::None]. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let session_key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com/" + /// ); + /// ``` pub fn new(method: EncryptionMethod, uri: T) -> Self { if method == EncryptionMethod::None { panic!("The EncryptionMethod is not allowed to be None"); @@ -29,18 +53,15 @@ impl ExtXSessionKey { impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { - if self.0.key_format.is_some() | self.0.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.0.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } + self.0.required_version() } } impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0.method == EncryptionMethod::None { + return Err(fmt::Error); + } write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -71,7 +92,7 @@ impl DerefMut for ExtXSessionKey { #[cfg(test)] mod test { use super::*; - use crate::types::EncryptionMethod; + use crate::types::{EncryptionMethod, KeyFormat}; #[test] fn test_display() { @@ -121,11 +142,16 @@ mod test { key ); - key.set_key_format("baz"); + key.set_key_format(Some(KeyFormat::Identity)); assert_eq!( - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://www.example.com/hls-key/key.bin",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# - .parse::().unwrap(), + "#EXT-X-SESSION-KEY:\ + METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\"" + .parse::() + .unwrap(), key ) } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index e50a297..89ac00d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -44,16 +44,18 @@ impl ExtXStreamInf { } } + /// Sets the `URI` that identifies the associated media playlist. pub fn set_uri(&mut self, value: T) -> &mut Self { self.uri = value.to_string(); self } - /// Returns the URI that identifies the associated media playlist. + /// Returns the `URI` that identifies the associated media playlist. pub const fn uri(&self) -> &String { &self.uri } + /// Sets the maximum frame rate for all the video in the variant stream. pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { self.frame_rate = value.map(|v| v.into()); self diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 28dda75..8ca5504 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -4,25 +4,67 @@ use std::str::FromStr; use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; -/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] +/// # [4.4.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, Ord, PartialOrd)] +/// The [ExtXDiscontinuitySequence] tag allows synchronization between +/// different Renditions of the same Variant Stream or different Variant +/// Streams that have [ExtXDiscontinuity] tags in their [Media Playlist]s. +/// +/// Its format is: +/// ```text +/// #EXT-X-DISCONTINUITY-SEQUENCE: +/// ``` +/// where `number` is a [u64]. +/// +/// [ExtXDiscontinuity]: crate::tags::ExtXDiscontinuity +/// [Media Playlist]: crate::MediaPlaylist +/// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3 +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct ExtXDiscontinuitySequence(u64); impl ExtXDiscontinuitySequence { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; - /// Makes a new `ExtXDiscontinuitySequence` tag. + /// Makes a new [ExtXDiscontinuitySequence] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// ``` pub const fn new(seq_num: u64) -> Self { Self(seq_num) } /// Returns the discontinuity sequence number of /// the first media segment that appears in the associated playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// + /// assert_eq!(discontinuity_sequence.seq_num(), 5); + /// ``` pub const fn seq_num(&self) -> u64 { self.0 } + + /// Sets the sequence number. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// + /// discontinuity_sequence.set_seq_num(10); + /// assert_eq!(discontinuity_sequence.seq_num(), 10); + /// ``` + pub fn set_seq_num(&mut self, value: u64) -> &mut Self { + self.0 = value; + self + } } impl RequiredVersion for ExtXDiscontinuitySequence { diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index c4fcc5b..2865b4c 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -5,9 +5,19 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.4. EXT-X-ENDLIST] +/// # [4.4.3.4. EXT-X-ENDLIST] +/// The [ExtXEndList] tag indicates, that no more [Media Segment]s will be +/// added to the [Media Playlist] file. /// -/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 +/// Its format is: +/// ```text +/// #EXT-X-ENDLIST +/// ``` +/// +/// [Media Segment]: crate::MediaSegment +/// [Media Playlist]: crate::MediaPlaylist +/// [4.4.3.4. EXT-X-ENDLIST]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtXEndList; diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 796c68f..c0291ca 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -5,9 +5,21 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.6. EXT-X-I-FRAMES-ONLY] +/// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] +/// The [ExtXIFramesOnly] tag indicates that each [Media Segment] in the +/// Playlist describes a single I-frame. I-frames are encoded video +/// frames, whose decoding does not depend on any other frame. I-frame +/// Playlists can be used for trick play, such as fast forward, rapid +/// reverse, and scrubbing. /// -/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 +/// Its format is: +/// ```text +/// #EXT-X-I-FRAMES-ONLY +/// ``` +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.3.6. EXT-X-I-FRAMES-ONLY]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtXIFramesOnly; diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index db4301e..c841b19 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -5,25 +5,64 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] +/// # [4.4.3.2. EXT-X-MEDIA-SEQUENCE] +/// The [ExtXMediaSequence] tag indicates the Media Sequence Number of +/// the first [Media Segment] that appears in a Playlist file. /// -/// [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)] +/// Its format is: +/// ```text +/// #EXT-X-MEDIA-SEQUENCE: +/// ``` +/// where `number` is a [u64]. +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2 +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct ExtXMediaSequence(u64); impl ExtXMediaSequence { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; - /// Makes a new `ExtXMediaSequence` tag. + /// Makes a new [ExtXMediaSequence] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMediaSequence; + /// let media_sequence = ExtXMediaSequence::new(5); + /// ``` pub const fn new(seq_num: u64) -> Self { Self(seq_num) } /// Returns the sequence number of the first media segment, /// that appears in the associated playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMediaSequence; + /// let media_sequence = ExtXMediaSequence::new(5); + /// + /// assert_eq!(media_sequence.seq_num(), 5); + /// ``` pub const fn seq_num(&self) -> u64 { self.0 } + + /// Sets the sequence number. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMediaSequence; + /// let mut media_sequence = ExtXMediaSequence::new(5); + /// + /// media_sequence.set_seq_num(10); + /// assert_eq!(media_sequence.seq_num(), 10); + /// ``` + pub fn set_seq_num(&mut self, value: u64) -> &mut Self { + self.0 = value; + self + } } impl RequiredVersion for ExtXMediaSequence { diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index e0b7ce8..b6ded13 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -5,26 +5,25 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5) +/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE] /// -/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the -/// Media Playlist. It applies to the entire Media Playlist. -/// It is OPTIONAL. Its format is: +/// The [ExtXPlaylistType] tag provides mutability information about the +/// [Media Playlist]. It applies to the entire [Media Playlist]. /// +/// Its format is: /// ```text /// #EXT-X-PLAYLIST-TYPE: /// ``` /// -/// # Note -/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the -/// Playlist can be updated according to the rules in Section 6.2.1 with -/// no additional restrictions. +/// [Media Playlist]: crate::MediaPlaylist +/// [4.4.3.5. EXT-X-PLAYLIST-TYPE]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExtXPlaylistType { - /// If the ExtXPlaylistType is Event, Media Segments can only be added to + /// If the [ExtXPlaylistType] is Event, Media Segments can only be added to /// the end of the Media Playlist. Event, - /// If the ExtXPlaylistType is Video On Demand (Vod), + /// If the [ExtXPlaylistType] is Video On Demand (Vod), /// the Media Playlist cannot change. Vod, } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 522616d..d7fe890 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -6,19 +6,31 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.1. EXT-X-TARGETDURATION] +/// # [4.4.3.1. EXT-X-TARGETDURATION] +/// The [ExtXTargetDuration] tag specifies the maximum [Media Segment] +/// duration. /// -/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 +/// Its format is: +/// ```text +/// #EXT-X-TARGETDURATION: +/// ``` +/// where `s` is the target [Duration] in seconds. +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.3.1. EXT-X-TARGETDURATION]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.1 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct ExtXTargetDuration(Duration); impl ExtXTargetDuration { pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; - /// Makes a new `ExtXTargetduration` tag. + /// Makes a new [ExtXTargetduration] tag. /// - /// Note that the nanoseconds part of the `duration` will be discarded. + /// # Note + /// The nanoseconds part of the [Duration] will be discarded. pub const fn new(duration: Duration) -> Self { + // TOOD: round instead of discarding? Self(Duration::from_secs(duration.as_secs())) } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index e0d02b7..37c3330 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,26 +1,39 @@ use std::fmt; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.2. EXT-X-BYTERANGE] +/// # [4.4.2.2. EXT-X-BYTERANGE] /// -/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +/// The [ExtXByteRange] tag indicates that a [Media Segment] is a sub-range +/// of the resource identified by its `URI`. +/// +/// Its format is: +/// ```text +/// #EXT-X-BYTERANGE:[@] +/// ``` +/// +/// where `n` is a [usize] indicating the length of the sub-range in bytes. +/// If present, `o` is a [usize] indicating the start of the sub-range, +/// as a byte offset from the beginning of the resource. +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.2.2. EXT-X-BYTERANGE]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; - /// Makes a new `ExtXByteRange` tag. + /// Makes a new [ExtXByteRange] tag. /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXByteRange; - /// + /// # use hls_m3u8::tags::ExtXByteRange; /// let byte_range = ExtXByteRange::new(20, Some(5)); /// ``` pub const fn new(length: usize, start: Option) -> Self { @@ -31,7 +44,7 @@ impl ExtXByteRange { /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXByteRange; + /// # use hls_m3u8::tags::ExtXByteRange; /// use hls_m3u8::types::ByteRange; /// /// let byte_range = ExtXByteRange::new(20, Some(5)); @@ -56,6 +69,12 @@ impl Deref for ExtXByteRange { } } +impl DerefMut for ExtXByteRange { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; @@ -132,6 +151,17 @@ mod test { assert_eq!(byte_range.start(), Some(22)); } + #[test] + fn test_deref_mut() { + let mut byte_range = ExtXByteRange::new(0, Some(22)); + + byte_range.set_length(100); + byte_range.set_start(Some(50)); + + assert_eq!(byte_range.length(), 100); + assert_eq!(byte_range.start(), Some(50)); + } + #[test] fn test_required_version() { assert_eq!( diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 2b6619a..0734b6f 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -18,8 +18,8 @@ use crate::Error; #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXDateRange { - /// A string that uniquely identifies a Date Range in the Playlist. - /// This attribute is REQUIRED. + /// A string that uniquely identifies a [ExtXDateRange] in the Playlist. + /// This attribute is required. id: String, /// A client-defined string that specifies some set of attributes and their associated value /// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 2289a24..a4846d9 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -5,9 +5,18 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.3. EXT-X-DISCONTINUITY] +/// # [4.4.2.3. EXT-X-DISCONTINUITY] +/// The [ExtXDiscontinuity] tag indicates a discontinuity between the +/// [Media Segment] that follows it and the one that preceded it. /// -/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 +/// Its format is: +/// ```text +/// #EXT-X-DISCONTINUITY +/// ``` +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.2.3. EXT-X-DISCONTINUITY]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtXDiscontinuity; @@ -50,7 +59,7 @@ mod test { #[test] fn test_parser() { - assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap(),) + assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap()) } #[test] diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index af0178a..8524d81 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -6,10 +6,10 @@ use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) +/// # [4.4.2.1. EXTINF] /// -/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies -/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment]. +/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies +/// only to the next [Media Segment]. /// /// Its format is: /// ```text @@ -18,32 +18,8 @@ use crate::Error; /// The title is an optional informative title about the [Media Segment]. /// /// [Media Segment]: crate::media_segment::MediaSegment -/// -/// # Examples -/// Parsing from a String: -/// ``` -/// use std::time::Duration; -/// use hls_m3u8::tags::ExtInf; -/// -/// let ext_inf = "#EXTINF:8,".parse::().expect("Failed to parse tag!"); -/// -/// assert_eq!(ext_inf.duration(), Duration::from_secs(8)); -/// assert_eq!(ext_inf.title(), None); -/// ``` -/// -/// Converting to a String: -/// ``` -/// use std::time::Duration; -/// use hls_m3u8::tags::ExtInf; -/// -/// let ext_inf = ExtInf::with_title( -/// Duration::from_millis(88), -/// "title" -/// ); -/// -/// assert_eq!(ext_inf.duration(), Duration::from_millis(88)); -/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string()); -/// ``` +/// [4.4.2.1. EXTINF]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { duration: Duration, @@ -53,7 +29,15 @@ pub struct ExtInf { impl ExtInf { pub(crate) const PREFIX: &'static str = "#EXTINF:"; - /// Makes a new `ExtInf` tag. + /// Makes a new [ExtInf] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::new(Duration::from_secs(5)); + /// ``` pub const fn new(duration: Duration) -> Self { ExtInf { duration, @@ -61,7 +45,15 @@ impl ExtInf { } } - /// Makes a new `ExtInf` tag with the given title. + /// Makes a new [ExtInf] tag with the given title. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); + /// ``` pub fn with_title(duration: Duration, title: T) -> Self { ExtInf { duration, @@ -70,13 +62,81 @@ impl ExtInf { } /// Returns the duration of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::new(Duration::from_secs(5)); + /// + /// assert_eq!( + /// ext_inf.duration(), + /// Duration::from_secs(5) + /// ); + /// ``` pub const fn duration(&self) -> Duration { self.duration } + /// Sets the duration of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let mut ext_inf = ExtInf::new(Duration::from_secs(5)); + /// + /// ext_inf.set_duration(Duration::from_secs(10)); + /// + /// assert_eq!( + /// ext_inf.duration(), + /// Duration::from_secs(10) + /// ); + /// ``` + pub fn set_duration(&mut self, value: Duration) -> &mut Self { + self.duration = value; + self + } + /// Returns the title of the associated media segment. - pub fn title(&self) -> Option<&String> { - self.title.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); + /// + /// assert_eq!( + /// ext_inf.title(), + /// &Some("title".to_string()) + /// ); + /// ``` + pub const fn title(&self) -> &Option { + &self.title + } + + /// Sets the title of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let mut ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); + /// + /// ext_inf.set_title(Some("better title")); + /// + /// assert_eq!( + /// ext_inf.title(), + /// &Some("better title".to_string()) + /// ); + /// ``` + pub fn set_title(&mut self, value: Option) -> &mut Self { + self.title = value.map(|v| v.to_string()); + self } } @@ -199,10 +259,10 @@ mod test { #[test] fn test_title() { - assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None); + assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None); assert_eq!( ExtInf::with_title(Duration::from_secs(5), "title").title(), - Some(&"title".to_string()) + &Some("title".to_string()) ); } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index ecdc3d6..e81479d 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -2,15 +2,30 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod}; +use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions}; use crate::utils::tag; use crate::Error; -/// [4.3.2.4. EXT-X-KEY] +/// # [4.4.2.4. EXT-X-KEY] +/// [Media Segment]s may be encrypted. The [ExtXKey] tag specifies how to +/// decrypt them. It applies to every [Media Segment] and to every Media +/// Initialization Section declared by an [ExtXMap] tag, that appears +/// between it and the next [ExtXKey] tag in the Playlist file with the +/// same [KeyFormat] attribute (or the end of the Playlist file). +/// +/// The format is: +/// ```text +/// #EXT-X-KEY: +/// ``` /// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 /// # Note /// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored. +/// +/// [KeyFormat]: crate::types::KeyFormat +/// [ExtXMap]: crate::tags::ExtXMap +/// [Media Segment]: crate::MediaSegment +/// [4.4.2.4. EXT-X-KEY]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.4 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXKey(DecryptionKey); @@ -49,13 +64,13 @@ impl ExtXKey { /// "#EXT-X-KEY:METHOD=NONE" /// ); /// ``` - pub const fn empty() -> Self { + pub fn empty() -> Self { Self(DecryptionKey { method: EncryptionMethod::None, uri: None, iv: None, key_format: None, - key_format_versions: None, + key_format_versions: KeyFormatVersions::new(), }) } @@ -109,7 +124,7 @@ impl DerefMut for ExtXKey { #[cfg(test)] mod test { use super::*; - use crate::types::EncryptionMethod; + use crate::types::{EncryptionMethod, KeyFormat}; #[test] fn test_display() { @@ -120,12 +135,12 @@ mod test { let mut key = ExtXKey::empty(); // it is expected, that all attributes will be ignored in an empty key! - key.set_key_format("hi"); + key.set_key_format(Some(KeyFormat::Identity)); key.set_iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]); key.set_uri(Some("https://www.example.com")); - key.set_key_format_versions("1/2/3"); + key.set_key_format_versions(vec![1, 2, 3]); assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); } @@ -133,7 +148,9 @@ mod test { #[test] fn test_parser() { assert_eq!( - r#"#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + "#EXT-X-KEY:\ + METHOD=AES-128,\ + URI=\"https://priv.example.com/key.php?r=52\"" .parse::() .unwrap(), ExtXKey::new( diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index d680f56..9b1ce94 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -6,9 +6,18 @@ use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; -/// [4.3.2.5. EXT-X-MAP] +/// # [4.4.2.5. EXT-X-MAP] +/// The [ExtXMap] tag specifies how to obtain the Media Initialization +/// Section, required to parse the applicable [Media Segment]s. /// -/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 +/// Its format is: +/// ```text +/// #EXT-X-MAP: +/// ``` +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.2.5. EXT-X-MAP]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.5 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXMap { uri: String, diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 2f852f6..d6a68ce 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use chrono::{DateTime, FixedOffset}; @@ -7,8 +8,11 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// The [ExtXProgramDateTime] tag associates the first sample of a +/// [Media Segment] with an absolute date and/or time. /// +/// [Media Segment]: crate::MediaSegment /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXProgramDateTime(DateTime); @@ -17,13 +21,33 @@ impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new `ExtXProgramDateTime` tag. - pub fn new>>(date_time: T) -> Self { - Self(date_time.into()) + /// + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXProgramDateTime; + /// use chrono::{FixedOffset, TimeZone}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let program_date_time = ExtXProgramDateTime::new( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31) + /// ); + /// ``` + pub const fn new(date_time: DateTime) -> Self { + Self(date_time) } /// Returns the date-time of the first sample of the associated media segment. - pub const fn date_time(&self) -> &DateTime { - &self.0 + pub const fn date_time(&self) -> DateTime { + self.0 + } + + /// Sets the date-time of the first sample of the associated media segment. + pub fn set_date_time(&mut self, value: DateTime) -> &mut Self { + self.0 = value; + self } } @@ -51,6 +75,20 @@ impl FromStr for ExtXProgramDateTime { } } +impl Deref for ExtXProgramDateTime { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ExtXProgramDateTime { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[cfg(test)] mod test { use super::*; @@ -60,14 +98,13 @@ mod test { #[test] fn test_display() { - let date_time = "2010-02-19T14:54:23.031+08:00" - .parse::>() - .unwrap(); - - let program_date_time = ExtXProgramDateTime::new(date_time); - assert_eq!( - program_date_time.to_string(), + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + ) + .to_string(), "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string() ); } @@ -88,12 +125,14 @@ mod test { #[test] fn test_required_version() { - let program_date_time = ExtXProgramDateTime::new( - FixedOffset::east(8 * HOURS_IN_SECS) - .ymd(2010, 2, 19) - .and_hms_milli(14, 54, 23, 31), + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ) + .required_version(), + ProtocolVersion::V1 ); - - assert_eq!(program_date_time.required_version(), ProtocolVersion::V1); } } diff --git a/src/tags/mod.rs b/src/tags/mod.rs index ebad0b7..35ae4c4 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -2,16 +2,6 @@ //! //! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3 -macro_rules! impl_from { - ($to:ident, $from:ident) => { - impl From<$from> for $to { - fn from(f: $from) -> Self { - $to::$from(f) - } - } - }; -} - mod basic; mod master_playlist; mod media_playlist; @@ -23,79 +13,3 @@ pub use master_playlist::*; pub use media_playlist::*; pub use media_segment::*; pub use shared::*; - -/// [4.3.4. Master Playlist Tags] -/// -/// See also [4.3.5. Media or Master Playlist Tags] -/// -/// [4.3.4. Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.4 -/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5 -#[allow(missing_docs)] -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MasterPlaylistTag { - ExtXMedia(ExtXMedia), - ExtXStreamInf(ExtXStreamInf), - ExtXIFrameStreamInf(ExtXIFrameStreamInf), - ExtXSessionData(ExtXSessionData), - ExtXSessionKey(ExtXSessionKey), - ExtXIndependentSegments(ExtXIndependentSegments), - ExtXStart(ExtXStart), -} -impl_from!(MasterPlaylistTag, ExtXMedia); -impl_from!(MasterPlaylistTag, ExtXStreamInf); -impl_from!(MasterPlaylistTag, ExtXIFrameStreamInf); -impl_from!(MasterPlaylistTag, ExtXSessionData); -impl_from!(MasterPlaylistTag, ExtXSessionKey); -impl_from!(MasterPlaylistTag, ExtXIndependentSegments); -impl_from!(MasterPlaylistTag, ExtXStart); - -/// [4.3.3. Media Playlist Tags] -/// -/// See also [4.3.5. Media or Master Playlist Tags] -/// -/// [4.3.3. Media Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.3 -/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MediaPlaylistTag { - ExtXTargetDuration(ExtXTargetDuration), - ExtXMediaSequence(ExtXMediaSequence), - ExtXDiscontinuitySequence(ExtXDiscontinuitySequence), - ExtXEndList(ExtXEndList), - ExtXPlaylistType(ExtXPlaylistType), - ExtXIFramesOnly(ExtXIFramesOnly), - ExtXIndependentSegments(ExtXIndependentSegments), - ExtXStart(ExtXStart), -} -impl_from!(MediaPlaylistTag, ExtXTargetDuration); -impl_from!(MediaPlaylistTag, ExtXMediaSequence); -impl_from!(MediaPlaylistTag, ExtXDiscontinuitySequence); -impl_from!(MediaPlaylistTag, ExtXEndList); -impl_from!(MediaPlaylistTag, ExtXPlaylistType); -impl_from!(MediaPlaylistTag, ExtXIFramesOnly); -impl_from!(MediaPlaylistTag, ExtXIndependentSegments); -impl_from!(MediaPlaylistTag, ExtXStart); - -/// [4.3.2. Media Segment Tags] -/// -/// [4.3.2. Media Segment Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.2 -#[allow(missing_docs)] -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MediaSegmentTag { - ExtInf(ExtInf), - ExtXByteRange(ExtXByteRange), - ExtXDateRange(ExtXDateRange), - ExtXDiscontinuity(ExtXDiscontinuity), - ExtXKey(ExtXKey), - ExtXMap(ExtXMap), - ExtXProgramDateTime(ExtXProgramDateTime), -} -impl_from!(MediaSegmentTag, ExtInf); -impl_from!(MediaSegmentTag, ExtXByteRange); -impl_from!(MediaSegmentTag, ExtXDateRange); -impl_from!(MediaSegmentTag, ExtXDiscontinuity); -impl_from!(MediaSegmentTag, ExtXKey); -impl_from!(MediaSegmentTag, ExtXMap); -impl_from!(MediaSegmentTag, ExtXProgramDateTime); diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 438eace..2e1ea20 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -4,39 +4,49 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, RequiredVersion}; +use crate::types::{ + EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion, + RequiredVersion, +}; use crate::utils::{quote, unquote}; use crate::Error; #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[builder(setter(into))] +/// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey]. +/// +/// [ExtXSessionKey]: crate::tags::ExtXSessionKey +/// [ExtXKey]: crate::tags::ExtXKey pub struct DecryptionKey { + /// The [EncryptionMethod]. pub(crate) method: EncryptionMethod, #[builder(setter(into, strip_option), default)] + /// An `URI`, that specifies how to obtain the key. pub(crate) uri: Option, #[builder(setter(into, strip_option), default)] + /// The IV (Initialization Vector) attribute. pub(crate) iv: Option, #[builder(setter(into, strip_option), default)] - pub(crate) key_format: Option, - #[builder(setter(into, strip_option), default)] - pub(crate) key_format_versions: Option, + /// A string that specifies how the key is + /// represented in the resource identified by the `URI`. + pub(crate) key_format: Option, + #[builder(setter(into), default)] + /// The `KEYFORMATVERSIONS` attribute. + pub(crate) key_format_versions: KeyFormatVersions, } impl DecryptionKey { - /// Makes a new `DecryptionKey`. + /// Makes a new [DecryptionKey]. + /// /// # Example /// ``` - /// use hls_m3u8::types::{EncryptionMethod, DecryptionKey}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"https://www.example.com/\"" - /// ); /// ``` pub fn new(method: EncryptionMethod, uri: T) -> Self { Self { @@ -44,14 +54,16 @@ impl DecryptionKey { uri: Some(uri.to_string()), iv: None, key_format: None, - key_format_versions: None, + key_format_versions: KeyFormatVersions::new(), } } /// Returns the [EncryptionMethod]. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -67,15 +79,17 @@ impl DecryptionKey { self.method } - /// Returns a Builder to build a `DecryptionKey`. + /// Returns a Builder to build a [DecryptionKey]. pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } /// Sets the [EncryptionMethod]. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -93,12 +107,14 @@ impl DecryptionKey { self.method = value; } - /// Returns an `URI` that specifies how to obtain the key. + /// Returns an `URI`, that specifies how to obtain the key. /// /// This attribute is required, if the [EncryptionMethod] is not None. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -121,7 +137,8 @@ impl DecryptionKey { /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -142,9 +159,11 @@ impl DecryptionKey { /// Returns the IV (Initialization Vector) attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -171,9 +190,11 @@ impl DecryptionKey { /// Sets the `IV` attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -197,106 +218,110 @@ impl DecryptionKey { } /// Returns a string that specifies how the key is - /// represented in the resource identified by the URI. + /// represented in the resource identified by the `URI`. + /// + /// This attribute is optional. /// - //// This attribute is optional. /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{KeyFormat, EncryptionMethod}; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format("key_format_attribute"); + /// key.set_key_format(Some(KeyFormat::Identity)); /// /// assert_eq!( /// key.key_format(), - /// &Some("key_format_attribute".to_string()) + /// Some(KeyFormat::Identity) /// ); /// ``` - pub const fn key_format(&self) -> &Option { - &self.key_format + pub const fn key_format(&self) -> Option { + self.key_format } /// Sets the `KEYFORMAT` attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{KeyFormat, EncryptionMethod}; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format("key_format_attribute"); + /// key.set_key_format(Some(KeyFormat::Identity)); /// /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// key.key_format(), + /// Some(KeyFormat::Identity) /// ); /// ``` - pub fn set_key_format(&mut self, value: T) { - self.key_format = Some(value.to_string()); + pub fn set_key_format>(&mut self, value: Option) { + self.key_format = value.map(|v| v.into()); } - /// Returns a string containing one or more positive - /// integers separated by the "/" character (for example, "1", "1/2", - /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` - /// is defined, this attribute can be used to indicate which - /// version(s) this instance complies with. + /// Returns the [KeyFormatVersions] attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{KeyFormatVersions, EncryptionMethod}; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format_versions("1/2/3/4/5"); + /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); /// /// assert_eq!( /// key.key_format_versions(), - /// &Some("1/2/3/4/5".to_string()) + /// &KeyFormatVersions::from(vec![1, 2, 3, 4, 5]) /// ); /// ``` - pub const fn key_format_versions(&self) -> &Option { + pub const fn key_format_versions(&self) -> &KeyFormatVersions { &self.key_format_versions } - /// Sets the `KEYFORMATVERSIONS` attribute. + /// Sets the [KeyFormatVersions] attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format_versions("1/2/3/4/5"); + /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); /// /// assert_eq!( /// key.to_string(), /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() /// ); /// ``` - pub fn set_key_format_versions(&mut self, value: T) { - self.key_format_versions = Some(value.to_string()); + pub fn set_key_format_versions>(&mut self, value: T) { + self.key_format_versions = value.into(); } } impl RequiredVersion for DecryptionKey { fn required_version(&self) -> ProtocolVersion { - if self.key_format.is_some() || self.key_format_versions.is_some() { + if self.key_format.is_some() || !self.key_format_versions.is_default() { ProtocolVersion::V5 } else if self.iv.is_some() { ProtocolVersion::V2 @@ -318,11 +343,11 @@ impl FromStr for DecryptionKey { for (key, value) in input.parse::()? { match key.as_str() { - "METHOD" => method = Some((value.parse())?), + "METHOD" => method = Some(value.parse()?), "URI" => uri = Some(unquote(value)), - "IV" => iv = Some((value.parse())?), - "KEYFORMAT" => key_format = Some(unquote(value)), - "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + "IV" => iv = Some(value.parse()?), + "KEYFORMAT" => key_format = Some(value.parse()?), + "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse()?), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -340,7 +365,7 @@ impl FromStr for DecryptionKey { uri, iv, key_format, - key_format_versions, + key_format_versions: key_format_versions.unwrap_or(KeyFormatVersions::new()), }) } } @@ -361,8 +386,8 @@ impl fmt::Display for DecryptionKey { if let Some(value) = &self.key_format { write!(f, ",KEYFORMAT={}", quote(value))?; } - if let Some(value) = &self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; + if !self.key_format_versions.is_default() { + write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?; } Ok(()) } @@ -381,13 +406,19 @@ mod test { .iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]) - .key_format("ABC123") - .key_format_versions("1,2,3,4,5/12345") + .key_format(KeyFormat::Identity) + .key_format_versions(vec![1, 2, 3, 4, 5]) .build() .unwrap(); assert_eq!( key.to_string(), - "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT=\"ABC123\",KEYFORMATVERSIONS=\"1,2,3,4,5/12345\"".to_string() + "METHOD=AES-128,\ + URI=\"https://www.example.com/\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\",\ + KEYFORMATVERSIONS=\"1/2/3/4/5\"\ + " + .to_string() ) } @@ -413,7 +444,8 @@ mod test { #[test] fn test_parser() { assert_eq!( - r#"METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + "METHOD=AES-128,\ + URI=\"https://priv.example.com/key.php?r=52\"" .parse::() .unwrap(), DecryptionKey::new( @@ -443,11 +475,15 @@ mod test { key.set_iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]); - key.set_key_format("baz"); + key.set_key_format(Some(KeyFormat::Identity)); assert_eq!( - r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# - .parse::().unwrap(), + "METHOD=AES-128,\ + URI=\"http://www.example.com\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\"" + .parse::() + .unwrap(), key ) } diff --git a/src/types/key_format.rs b/src/types/key_format.rs new file mode 100644 index 0000000..43ef6a7 --- /dev/null +++ b/src/types/key_format.rs @@ -0,0 +1,68 @@ +use std::fmt; +use std::str::FromStr; + +use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +/// KeyFormat specifies, how the key is represented in the resource identified by the URI +pub enum KeyFormat { + /// The key is a single packed array of 16 octets in binary format. + Identity, +} + +impl Default for KeyFormat { + fn default() -> Self { + Self::Identity + } +} + +impl FromStr for KeyFormat { + type Err = Error; + + fn from_str(input: &str) -> Result { + tag(&unquote(input), "identity")?; // currently only KeyFormat::Identity exists! + + Ok(Self::Identity) + } +} + +impl fmt::Display for KeyFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", quote("identity")) + } +} + +impl RequiredVersion for KeyFormat { + fn required_version(&self) -> ProtocolVersion { + ProtocolVersion::V5 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!(KeyFormat::Identity.to_string(), quote("identity")); + } + + #[test] + fn test_parser() { + assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap()); + + assert_eq!(KeyFormat::Identity, "identity".parse().unwrap()); + } + + #[test] + fn test_required_version() { + assert_eq!(KeyFormat::Identity.required_version(), ProtocolVersion::V5) + } + + #[test] + fn test_default() { + assert_eq!(KeyFormat::Identity, KeyFormat::default()); + } +} diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs new file mode 100644 index 0000000..8448905 --- /dev/null +++ b/src/types/key_format_versions.rs @@ -0,0 +1,170 @@ +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::utils::{quote, unquote}; +use crate::Error; + +/// A list of [usize], that can be used to indicate which version(s) +/// this instance complies with, if more than one version of a particular +/// [KeyFormat] is defined. +/// +/// [KeyFormat]: crate::types::KeyFormat +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct KeyFormatVersions(Vec); + +impl Default for KeyFormatVersions { + fn default() -> Self { + Self(vec![1]) + } +} + +impl KeyFormatVersions { + /// Makes a new [KeyFormatVersions]. + pub fn new() -> Self { + Self::default() + } + + /// Add a value to the [KeyFormatVersions]. + pub fn push(&mut self, value: usize) { + if self.is_default() { + self.0 = vec![value]; + } else { + self.0.push(value); + } + } + + /// Returns `true`, if [KeyFormatVersions] has the default value of `vec![1]`. + pub fn is_default(&self) -> bool { + self.0 == vec![1] || self.0.is_empty() + } +} + +impl Deref for KeyFormatVersions { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for KeyFormatVersions { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl RequiredVersion for KeyFormatVersions { + fn required_version(&self) -> ProtocolVersion { + ProtocolVersion::V5 + } +} + +impl FromStr for KeyFormatVersions { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut result = unquote(input) + .split("/") + .filter_map(|v| v.parse().ok()) + .collect::>(); + + if result.is_empty() { + result.push(1); + } + + Ok(Self(result)) + } +} + +impl fmt::Display for KeyFormatVersions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_default() { + return write!(f, "{}", quote("1")); + } + + write!( + f, + "{}", + quote( + // vec![1, 2, 3] -> "1/2/3" + self.0 + .iter() + .map(|v| v.to_string()) + .collect::>() + .join("/") + ) + ) + } +} + +impl>> From for KeyFormatVersions { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!( + KeyFormatVersions::from(vec![1, 2, 3, 4, 5]).to_string(), + quote("1/2/3/4/5") + ); + + assert_eq!(KeyFormatVersions::from(vec![]).to_string(), quote("1")); + + assert_eq!(KeyFormatVersions::new().to_string(), quote("1")); + } + + #[test] + fn test_parser() { + assert_eq!( + KeyFormatVersions::from(vec![1, 2, 3, 4, 5]), + quote("1/2/3/4/5").parse().unwrap() + ); + + assert_eq!(KeyFormatVersions::from(vec![1]), "1".parse().unwrap()); + + assert_eq!(KeyFormatVersions::from(vec![1, 2]), "1/2".parse().unwrap()); + } + + #[test] + fn test_required_version() { + assert_eq!( + KeyFormatVersions::new().required_version(), + ProtocolVersion::V5 + ) + } + + #[test] + fn test_is_default() { + assert!(KeyFormatVersions::new().is_default()); + assert!(KeyFormatVersions::from(vec![]).is_default()); + assert!(!KeyFormatVersions::from(vec![1, 2, 3]).is_default()); + } + + #[test] + fn test_push() { + let mut key_format_versions = KeyFormatVersions::from(vec![]); + + key_format_versions.push(2); + assert_eq!(KeyFormatVersions::from(vec![2]), key_format_versions); + } + + #[test] + fn test_deref() { + assert!(!KeyFormatVersions::new().is_empty()); + } + + #[test] + fn test_deref_mut() { + let mut key_format_versions = KeyFormatVersions::from(vec![1, 2, 3]); + key_format_versions.pop(); + assert_eq!(key_format_versions, KeyFormatVersions::from(vec![1, 2])); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index f3c751f..70c891f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -8,6 +8,8 @@ mod encryption_method; mod hdcp_level; mod in_stream_id; mod initialization_vector; +mod key_format; +mod key_format_versions; mod media_type; mod protocol_version; mod signed_decimal_floating_point; @@ -22,6 +24,8 @@ pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; pub use initialization_vector::*; +pub use key_format::*; +pub use key_format_versions::*; pub use media_type::*; pub use protocol_version::*; pub(crate) use signed_decimal_floating_point::*; diff --git a/src/utils.rs b/src/utils.rs index eeda232..bb3662c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -46,7 +46,7 @@ pub(crate) fn tag(input: &str, tag: T) -> crate::Result<&str> where T: AsRef, { - if !input.starts_with(tag.as_ref()) { + if !input.trim().starts_with(tag.as_ref()) { return Err(Error::missing_tag(tag.as_ref(), input)); } let result = input.split_at(tag.as_ref().len()).1;