diff --git a/Cargo.toml b/Cargo.toml index 2d8fae9..f790b1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,12 @@ codecov = { repository = "sile/hls_m3u8" } [dependencies] failure = "0.1.5" -derive_builder = "0.7.2" +derive_builder = "0.8.0" chrono = "0.4.9" strum = { version = "0.16.0", features = ["derive"] } derive_more = "0.15.0" +hex = "0.4.0" [dev-dependencies] -clap = "2" +clap = "2.33.0" +pretty_assertions = "0.6.1" diff --git a/src/attribute.rs b/src/attribute.rs index 91d42e6..95f99a6 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -39,7 +39,7 @@ impl FromStr for AttributePairs { type Err = Error; fn from_str(input: &str) -> Result { - let mut result = AttributePairs::new(); + let mut result = Self::new(); for line in split(input, ',') { let pair = split(line.trim(), '='); @@ -67,16 +67,12 @@ fn split(value: &str, terminator: char) -> Vec { let mut result = vec![]; let mut inside_quotes = false; - let mut temp_string = String::new(); + let mut temp_string = String::with_capacity(1024); for c in value.chars() { match c { '"' => { - if inside_quotes { - inside_quotes = false; - } else { - inside_quotes = true; - } + inside_quotes = !inside_quotes; temp_string.push(c); } k if (k == terminator) => { @@ -84,7 +80,7 @@ fn split(value: &str, terminator: char) -> Vec { temp_string.push(c); } else { result.push(temp_string); - temp_string = String::new(); + temp_string = String::with_capacity(1024); } } _ => { @@ -100,6 +96,7 @@ fn split(value: &str, terminator: char) -> Vec { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/error.rs b/src/error.rs index 0d85eca..c64e031 100644 --- a/src/error.rs +++ b/src/error.rs @@ -76,6 +76,10 @@ pub enum ErrorKind { /// An unexpected value. UnexpectedAttribute(String), + #[fail(display = "Unexpected Tag: {:?}", _0)] + /// An unexpected tag. + UnexpectedTag(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -103,11 +107,11 @@ impl fmt::Display for Error { } impl From for Error { - fn from(kind: ErrorKind) -> Error { Error::from(Context::new(kind)) } + fn from(kind: ErrorKind) -> Self { Self::from(Context::new(kind)) } } impl From> for Error { - fn from(inner: Context) -> Error { Error { inner } } + fn from(inner: Context) -> Self { Self { inner } } } impl Error { @@ -119,6 +123,10 @@ impl Error { Self::from(ErrorKind::UnexpectedAttribute(value.to_string())) } + pub(crate) fn unexpected_tag(value: T) -> Self { + Self::from(ErrorKind::UnexpectedTag(value.to_string())) + } + pub(crate) fn invalid_input() -> Self { Self::from(ErrorKind::InvalidInput) } pub(crate) fn parse_int_error(value: T) -> Self { @@ -157,17 +165,6 @@ impl Error { pub(crate) fn io(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) } - pub(crate) fn required_version(required_version: T, specified_version: U) -> Self - where - T: ToString, - U: ToString, - { - Self::from(ErrorKind::VersionError( - required_version.to_string(), - specified_version.to_string(), - )) - } - pub(crate) fn builder_error(value: T) -> Self { Self::from(ErrorKind::BuilderError(value.to_string())) } @@ -182,23 +179,39 @@ impl Error { } impl From<::std::num::ParseIntError> for Error { - fn from(value: ::std::num::ParseIntError) -> Self { Error::parse_int_error(value) } + fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int_error(value) } } impl From<::std::num::ParseFloatError> for Error { - fn from(value: ::std::num::ParseFloatError) -> Self { Error::parse_float_error(value) } + fn from(value: ::std::num::ParseFloatError) -> Self { Self::parse_float_error(value) } } impl From<::std::io::Error> for Error { - fn from(value: ::std::io::Error) -> Self { Error::io(value) } + fn from(value: ::std::io::Error) -> Self { Self::io(value) } } impl From<::chrono::ParseError> for Error { - fn from(value: ::chrono::ParseError) -> Self { Error::chrono(value) } + fn from(value: ::chrono::ParseError) -> Self { Self::chrono(value) } } impl From<::strum::ParseError> for Error { fn from(value: ::strum::ParseError) -> Self { - Error::custom(value) // TODO! + Self::custom(value) // TODO! + } +} + +impl From for Error { + fn from(value: String) -> Self { Self::custom(value) } +} + +impl From<::core::convert::Infallible> for Error { + fn from(_: ::core::convert::Infallible) -> Self { + Self::custom("An Infallible error has been returned! (this should never happen!)") + } +} + +impl From<::hex::FromHexError> for Error { + fn from(value: ::hex::FromHexError) -> Self { + Self::custom(value) // TODO! } } diff --git a/src/lib.rs b/src/lib.rs index 54a0964..30fe3cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![feature(option_flattening)] #![warn( //clippy::pedantic, clippy::nursery, @@ -35,20 +36,23 @@ //! assert!(m3u8.parse::().is_ok()); //! ``` -pub use error::{Error, ErrorKind}; -pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; -pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder}; -pub use media_segment::{MediaSegment, MediaSegmentBuilder}; +pub use error::Error; +pub use master_playlist::MasterPlaylist; +pub use media_playlist::MediaPlaylist; +pub use media_segment::MediaSegment; pub mod tags; pub mod types; +#[macro_use] +mod utils; mod attribute; mod error; mod line; mod master_playlist; mod media_playlist; mod media_segment; -mod utils; +mod traits; pub use error::Result; +pub use traits::*; diff --git a/src/line.rs b/src/line.rs index 8923f44..f36782a 100644 --- a/src/line.rs +++ b/src/line.rs @@ -16,46 +16,46 @@ impl FromStr for Lines { type Err = Error; fn from_str(input: &str) -> Result { - let mut result = Lines::new(); + let mut result = Self::new(); let mut stream_inf = false; let mut stream_inf_line = None; for l in input.lines() { - let line = l.trim(); + let raw_line = l.trim(); - if line.is_empty() { + if raw_line.is_empty() { continue; } - let pline = { - if line.starts_with(tags::ExtXStreamInf::PREFIX) { + let line = { + if raw_line.starts_with(tags::ExtXStreamInf::PREFIX) { stream_inf = true; - stream_inf_line = Some(line); + stream_inf_line = Some(raw_line); continue; - } else if line.starts_with("#EXT") { - Line::Tag(line.parse()?) - } else if line.starts_with('#') { + } else if raw_line.starts_with("#EXT") { + Line::Tag(raw_line.parse()?) + } else if raw_line.starts_with('#') { continue; // ignore comments } else { // stream inf line needs special treatment if stream_inf { stream_inf = false; if let Some(first_line) = stream_inf_line { - let res = Line::Tag(format!("{}\n{}", first_line, line).parse()?); + let res = Line::Tag(format!("{}\n{}", first_line, raw_line).parse()?); stream_inf_line = None; res } else { continue; } } else { - Line::Uri(line.trim().to_string()) + Line::Uri(raw_line.to_string()) } } }; - result.push(pline); + result.push(line); } Ok(result) @@ -79,14 +79,14 @@ impl DerefMut for Lines { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum Line { Tag(Tag), Uri(String), } #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum Tag { ExtM3u(tags::ExtM3u), ExtXVersion(tags::ExtXVersion), @@ -116,29 +116,29 @@ pub enum Tag { impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { - Tag::ExtM3u(value) => value.fmt(f), - Tag::ExtXVersion(value) => value.fmt(f), - Tag::ExtInf(value) => value.fmt(f), - Tag::ExtXByteRange(value) => value.fmt(f), - Tag::ExtXDiscontinuity(value) => value.fmt(f), - Tag::ExtXKey(value) => value.fmt(f), - Tag::ExtXMap(value) => value.fmt(f), - Tag::ExtXProgramDateTime(value) => value.fmt(f), - Tag::ExtXDateRange(value) => value.fmt(f), - Tag::ExtXTargetDuration(value) => value.fmt(f), - Tag::ExtXMediaSequence(value) => value.fmt(f), - Tag::ExtXDiscontinuitySequence(value) => value.fmt(f), - Tag::ExtXEndList(value) => value.fmt(f), - Tag::ExtXPlaylistType(value) => value.fmt(f), - Tag::ExtXIFramesOnly(value) => value.fmt(f), - Tag::ExtXMedia(value) => value.fmt(f), - Tag::ExtXStreamInf(value) => value.fmt(f), - Tag::ExtXIFrameStreamInf(value) => value.fmt(f), - Tag::ExtXSessionData(value) => value.fmt(f), - Tag::ExtXSessionKey(value) => value.fmt(f), - Tag::ExtXIndependentSegments(value) => value.fmt(f), - Tag::ExtXStart(value) => value.fmt(f), - Tag::Unknown(value) => value.fmt(f), + Self::ExtM3u(value) => value.fmt(f), + Self::ExtXVersion(value) => value.fmt(f), + Self::ExtInf(value) => value.fmt(f), + Self::ExtXByteRange(value) => value.fmt(f), + Self::ExtXDiscontinuity(value) => value.fmt(f), + Self::ExtXKey(value) => value.fmt(f), + Self::ExtXMap(value) => value.fmt(f), + Self::ExtXProgramDateTime(value) => value.fmt(f), + Self::ExtXDateRange(value) => value.fmt(f), + Self::ExtXTargetDuration(value) => value.fmt(f), + Self::ExtXMediaSequence(value) => value.fmt(f), + Self::ExtXDiscontinuitySequence(value) => value.fmt(f), + Self::ExtXEndList(value) => value.fmt(f), + Self::ExtXPlaylistType(value) => value.fmt(f), + Self::ExtXIFramesOnly(value) => value.fmt(f), + Self::ExtXMedia(value) => value.fmt(f), + Self::ExtXStreamInf(value) => value.fmt(f), + Self::ExtXIFrameStreamInf(value) => value.fmt(f), + Self::ExtXSessionData(value) => value.fmt(f), + Self::ExtXSessionKey(value) => value.fmt(f), + Self::ExtXIndependentSegments(value) => value.fmt(f), + Self::ExtXStart(value) => value.fmt(f), + Self::Unknown(value) => value.fmt(f), } } } @@ -148,51 +148,51 @@ impl FromStr for Tag { fn from_str(input: &str) -> Result { if input.starts_with(tags::ExtM3u::PREFIX) { - input.parse().map(Tag::ExtM3u) + input.parse().map(Self::ExtM3u) } else if input.starts_with(tags::ExtXVersion::PREFIX) { - input.parse().map(Tag::ExtXVersion) + input.parse().map(Self::ExtXVersion) } else if input.starts_with(tags::ExtInf::PREFIX) { - input.parse().map(Tag::ExtInf) + input.parse().map(Self::ExtInf) } else if input.starts_with(tags::ExtXByteRange::PREFIX) { - input.parse().map(Tag::ExtXByteRange) + input.parse().map(Self::ExtXByteRange) } else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) { - input.parse().map(Tag::ExtXDiscontinuity) + input.parse().map(Self::ExtXDiscontinuity) } else if input.starts_with(tags::ExtXKey::PREFIX) { - input.parse().map(Tag::ExtXKey) + input.parse().map(Self::ExtXKey) } else if input.starts_with(tags::ExtXMap::PREFIX) { - input.parse().map(Tag::ExtXMap) + input.parse().map(Self::ExtXMap) } else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) { - input.parse().map(Tag::ExtXProgramDateTime) + input.parse().map(Self::ExtXProgramDateTime) } else if input.starts_with(tags::ExtXTargetDuration::PREFIX) { - input.parse().map(Tag::ExtXTargetDuration) + input.parse().map(Self::ExtXTargetDuration) } else if input.starts_with(tags::ExtXDateRange::PREFIX) { - input.parse().map(Tag::ExtXDateRange) + input.parse().map(Self::ExtXDateRange) } else if input.starts_with(tags::ExtXMediaSequence::PREFIX) { - input.parse().map(Tag::ExtXMediaSequence) + input.parse().map(Self::ExtXMediaSequence) } else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - input.parse().map(Tag::ExtXDiscontinuitySequence) + input.parse().map(Self::ExtXDiscontinuitySequence) } else if input.starts_with(tags::ExtXEndList::PREFIX) { - input.parse().map(Tag::ExtXEndList) + input.parse().map(Self::ExtXEndList) } else if input.starts_with(tags::ExtXPlaylistType::PREFIX) { - input.parse().map(Tag::ExtXPlaylistType) + input.parse().map(Self::ExtXPlaylistType) } else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) { - input.parse().map(Tag::ExtXIFramesOnly) + input.parse().map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { - input.parse().map(Tag::ExtXMedia).map_err(Error::custom) + input.parse().map(Self::ExtXMedia).map_err(Error::custom) } else if input.starts_with(tags::ExtXStreamInf::PREFIX) { - input.parse().map(Tag::ExtXStreamInf) + input.parse().map(Self::ExtXStreamInf) } else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - input.parse().map(Tag::ExtXIFrameStreamInf) + input.parse().map(Self::ExtXIFrameStreamInf) } else if input.starts_with(tags::ExtXSessionData::PREFIX) { - input.parse().map(Tag::ExtXSessionData) + input.parse().map(Self::ExtXSessionData) } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { - input.parse().map(Tag::ExtXSessionKey) + input.parse().map(Self::ExtXSessionKey) } else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) { - input.parse().map(Tag::ExtXIndependentSegments) + input.parse().map(Self::ExtXIndependentSegments) } else if input.starts_with(tags::ExtXStart::PREFIX) { - input.parse().map(Tag::ExtXStart) + input.parse().map(Self::ExtXStart) } else { - Ok(Tag::Unknown(input.to_string())) + Ok(Self::Unknown(input.to_string())) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 2af4ff7..04b2d99 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,6 +1,5 @@ use std::collections::HashSet; use std::fmt; -use std::iter; use std::str::FromStr; use derive_builder::Builder; @@ -10,90 +9,210 @@ use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; -use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, RequiredVersion}; -use crate::Error; +use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; +use crate::{Error, RequiredVersion}; -/// Master playlist. -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, PartialEq)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] +/// Master playlist. pub struct MasterPlaylist { - #[builder(default, setter(name = "version"))] - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility - /// version greater than `version`, - /// `build()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - version_tag: ExtXVersion, #[builder(default)] - /// Sets the [ExtXIndependentSegments] tag. + /// Sets the [`ExtXIndependentSegments`] tag. + /// + /// # Note + /// This tag is optional. independent_segments_tag: Option, #[builder(default)] - /// Sets the [ExtXStart] tag. + /// Sets the [`ExtXStart`] tag. + /// + /// # Note + /// This tag is optional. start_tag: Option, - /// Sets the [ExtXMedia] tag. + #[builder(default)] + /// Sets the [`ExtXMedia`] tag. + /// + /// # Note + /// This tag is optional. media_tags: Vec, - /// Sets all [ExtXStreamInf]s. + #[builder(default)] + /// Sets all [`ExtXStreamInf`] tags. + /// + /// # Note + /// This tag is optional. stream_inf_tags: Vec, - /// Sets all [ExtXIFrameStreamInf]s. + #[builder(default)] + /// Sets all [`ExtXIFrameStreamInf`] tags. + /// + /// # Note + /// This tag is optional. i_frame_stream_inf_tags: Vec, - /// Sets all [ExtXSessionData]s. + #[builder(default)] + /// Sets all [`ExtXSessionData`] tags. + /// + /// # Note + /// This tag is optional. session_data_tags: Vec, - /// Sets all [ExtXSessionKey]s. + #[builder(default)] + /// Sets all [`ExtXSessionKey`] tags. + /// + /// # Note + /// This tag is optional. session_key_tags: Vec, } impl MasterPlaylist { - /// Returns a Builder for a MasterPlaylist. + /// Returns a Builder for a [`MasterPlaylist`]. + /// + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXStart; + /// use hls_m3u8::MasterPlaylist; + /// + /// # fn main() -> Result<(), hls_m3u8::Error> { + /// MasterPlaylist::builder() + /// .start_tag(ExtXStart::new(20.123456)) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } - /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } - - /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. - pub const fn independent_segments_tag(&self) -> Option { + /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. + pub const fn independent_segments(&self) -> Option { self.independent_segments_tag } - /// Returns the `EXT-X-START` tag contained in the playlist. - pub const fn start_tag(&self) -> Option { self.start_tag } + /// Sets the [`ExtXIndependentSegments`] tag contained in the playlist. + pub fn set_independent_segments(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.independent_segments_tag = value.map(Into::into); + self + } - /// Returns the `EXT-X-MEDIA` tags contained in the playlist. - pub fn media_tags(&self) -> &[ExtXMedia] { &self.media_tags } + /// Returns the [`ExtXStart`] tag contained in the playlist. + pub const fn start(&self) -> Option { self.start_tag } - /// Returns the `EXT-X-STREAM-INF` tags contained in the playlist. - pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] { &self.stream_inf_tags } + /// Sets the [`ExtXStart`] tag contained in the playlist. + pub fn set_start(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.start_tag = value.map(Into::into); + self + } - /// Returns the `EXT-X-I-FRAME-STREAM-INF` tags contained in the playlist. - pub fn i_frame_stream_inf_tags(&self) -> &[ExtXIFrameStreamInf] { + /// Returns the [`ExtXMedia`] tags contained in the playlist. + pub const fn media_tags(&self) -> &Vec { &self.media_tags } + + /// Appends an [`ExtXMedia`]. + pub fn push_media_tag(&mut self, value: ExtXMedia) -> &mut Self { + self.media_tags.push(value); + self + } + + /// Sets the [`ExtXMedia`] tags contained in the playlist. + pub fn set_media_tags(&mut self, value: Vec) -> &mut Self + where + T: Into, + { + self.media_tags = value.into_iter().map(Into::into).collect(); + self + } + + /// Returns the [`ExtXStreamInf`] tags contained in the playlist. + pub const fn stream_inf_tags(&self) -> &Vec { &self.stream_inf_tags } + + /// Appends an [`ExtXStreamInf`]. + pub fn push_stream_inf(&mut self, value: ExtXStreamInf) -> &mut Self { + self.stream_inf_tags.push(value); + self + } + + /// Sets the [`ExtXStreamInf`] tags contained in the playlist. + pub fn set_stream_inf_tags(&mut self, value: Vec) -> &mut Self + where + T: Into, + { + self.stream_inf_tags = value.into_iter().map(Into::into).collect(); + self + } + + /// Returns the [`ExtXIFrameStreamInf`] tags contained in the playlist. + pub const fn i_frame_stream_inf_tags(&self) -> &Vec { &self.i_frame_stream_inf_tags } - /// Returns the `EXT-X-SESSION-DATA` tags contained in the playlist. - pub fn session_data_tags(&self) -> &[ExtXSessionData] { &self.session_data_tags } + /// Appends an [`ExtXIFrameStreamInf`]. + pub fn push_i_frame_stream_inf(&mut self, value: ExtXIFrameStreamInf) -> &mut Self { + self.i_frame_stream_inf_tags.push(value); + self + } - /// Returns the `EXT-X-SESSION-KEY` tags contained in the playlist. - pub fn session_key_tags(&self) -> &[ExtXSessionKey] { &self.session_key_tags } + /// Sets the [`ExtXIFrameStreamInf`] tags contained in the playlist. + pub fn set_i_frame_stream_inf_tags(&mut self, value: Vec) -> &mut Self + where + T: Into, + { + self.i_frame_stream_inf_tags = value.into_iter().map(Into::into).collect(); + self + } + + /// Returns the [`ExtXSessionData`] tags contained in the playlist. + pub const fn session_data_tags(&self) -> &Vec { &self.session_data_tags } + + /// Appends an [`ExtXSessionData`]. + pub fn push_session_data(&mut self, value: ExtXSessionData) -> &mut Self { + self.session_data_tags.push(value); + self + } + + /// Sets the [`ExtXSessionData`] tags contained in the playlist. + pub fn set_session_data_tags(&mut self, value: Vec) -> &mut Self + where + T: Into, + { + self.session_data_tags = value.into_iter().map(Into::into).collect(); + self + } + + /// Returns the [`ExtXSessionKey`] tags contained in the playlist. + pub const fn session_key_tags(&self) -> &Vec { &self.session_key_tags } + + /// Appends an [`ExtXSessionKey`]. + pub fn push_session_key(&mut self, value: ExtXSessionKey) -> &mut Self { + self.session_key_tags.push(value); + self + } + + /// Sets the [`ExtXSessionKey`] tags contained in the playlist. + pub fn set_session_key_tags(&mut self, value: Vec) -> &mut Self + where + T: Into, + { + self.session_key_tags = value.into_iter().map(Into::into).collect(); + self + } } impl RequiredVersion for MasterPlaylist { - fn required_version(&self) -> ProtocolVersion { self.version_tag.version() } + fn required_version(&self) -> ProtocolVersion { + required_version![ + self.independent_segments_tag, + self.start_tag, + self.media_tags, + self.stream_inf_tags, + self.i_frame_stream_inf_tags, + self.session_data_tags, + self.session_key_tags + ] + } } impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { - let required_version = self.required_version(); - let specified_version = self - .version_tag - .unwrap_or_else(|| required_version.into()) - .version(); - - if required_version > specified_version { - return Err(Error::required_version(required_version, specified_version).to_string()); - } - self.validate_stream_inf_tags().map_err(|e| e.to_string())?; self.validate_i_frame_stream_inf_tags() .map_err(|e| e.to_string())?; @@ -103,54 +222,6 @@ impl MasterPlaylistBuilder { Ok(()) } - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.start_tag - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.media_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.stream_inf_tags - .iter() - .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.required_version())) - .flatten(), - ) - .chain( - self.session_data_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.session_key_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .max() - .unwrap_or_else(ProtocolVersion::latest) - } - fn validate_stream_inf_tags(&self) -> crate::Result<()> { if let Some(value) = &self.stream_inf_tags { let mut has_none_closed_captions = false; @@ -230,11 +301,29 @@ impl MasterPlaylistBuilder { } } +impl RequiredVersion for MasterPlaylistBuilder { + fn required_version(&self) -> ProtocolVersion { + // TODO: the .flatten() can be removed as soon as `recursive traits` are + // supported. (RequiredVersion is implemented for Option, but + // not for Option>) + // https://github.com/rust-lang/chalk/issues/12 + required_version![ + self.independent_segments_tag.flatten(), + self.start_tag.flatten(), + self.media_tags, + self.stream_inf_tags, + self.i_frame_stream_inf_tags, + self.session_data_tags, + self.session_key_tags + ] + } +} + impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; - if self.version_tag.version() != ProtocolVersion::V1 { - writeln!(f, "{}", self.version_tag)?; + if self.required_version() != ProtocolVersion::V1 { + writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } for t in &self.media_tags { writeln!(f, "{}", t)?; @@ -265,7 +354,7 @@ impl FromStr for MasterPlaylist { type Err = Error; fn from_str(input: &str) -> Result { - let mut builder = MasterPlaylist::builder(); + let mut builder = Self::builder(); let mut media_tags = vec![]; let mut stream_inf_tags = vec![]; @@ -286,8 +375,10 @@ impl FromStr for MasterPlaylist { Tag::ExtM3u(_) => { return Err(Error::invalid_input()); } - Tag::ExtXVersion(t) => { - builder.version(t.version()); + Tag::ExtXVersion(_) => { + // This tag can be ignored, because the + // MasterPlaylist will automatically set the + // ExtXVersion tag to correct version! } Tag::ExtInf(_) | Tag::ExtXByteRange(_) @@ -354,39 +445,39 @@ impl FromStr for MasterPlaylist { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { - r#"#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/low/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/lo_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/hi_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360 -http://example.com/high/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" -http://example.com/audio/index.m3u8 -"# - .parse::() - .unwrap(); + "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/low/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/lo_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/hi_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n\ + http://example.com/high/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n\ + http://example.com/audio/index.m3u8\n" + .parse::() + .unwrap(); } #[test] fn test_display() { - let input = r#"#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/low/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/lo_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/hi_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360 -http://example.com/high/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" -http://example.com/audio/index.m3u8 -"#; + let input = "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/low/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/lo_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/hi_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n\ + http://example.com/high/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n\ + http://example.com/audio/index.m3u8\n"; + let playlist = input.parse::().unwrap(); assert_eq!(playlist.to_string(), input); } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 31dde47..a334650 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::iter; use std::str::FromStr; use std::time::Duration; @@ -11,47 +10,38 @@ use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::{ProtocolVersion, RequiredVersion}; -use crate::Error; +use crate::types::ProtocolVersion; +use crate::{Encrypted, Error, RequiredVersion}; /// Media playlist. -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] pub struct MediaPlaylist { - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility - /// version greater than `version`, - /// `build()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - #[builder(setter(name = "version"))] - version_tag: ExtXVersion, - /// Sets the [ExtXTargetDuration] tag. + /// Sets the [`ExtXTargetDuration`] tag. target_duration_tag: ExtXTargetDuration, #[builder(default)] - /// Sets the [ExtXMediaSequence] tag. + /// Sets the [`ExtXMediaSequence`] tag. media_sequence_tag: Option, #[builder(default)] - /// Sets the [ExtXDiscontinuitySequence] tag. + /// Sets the [`ExtXDiscontinuitySequence`] tag. discontinuity_sequence_tag: Option, #[builder(default)] - /// Sets the [ExtXPlaylistType] tag. + /// Sets the [`ExtXPlaylistType`] tag. playlist_type_tag: Option, #[builder(default)] - /// Sets the [ExtXIFramesOnly] tag. + /// Sets the [`ExtXIFramesOnly`] tag. i_frames_only_tag: Option, #[builder(default)] - /// Sets the [ExtXIndependentSegments] tag. + /// Sets the [`ExtXIndependentSegments`] tag. independent_segments_tag: Option, #[builder(default)] - /// Sets the [ExtXStart] tag. + /// Sets the [`ExtXStart`] tag. start_tag: Option, #[builder(default)] - /// Sets the [ExtXEndList] tag. + /// Sets the [`ExtXEndList`] tag. end_list_tag: Option, - /// Sets all [MediaSegment]s. + /// Sets all [`MediaSegment`]s. segments: Vec, /// Sets the allowable excess duration of each media segment in the /// associated playlist. @@ -68,20 +58,6 @@ pub struct MediaPlaylist { impl MediaPlaylistBuilder { fn validate(&self) -> Result<(), String> { - let required_version = self.required_version(); - let specified_version = self - .version_tag - .unwrap_or_else(|| required_version.into()) - .version(); - - if required_version > specified_version { - return Err(Error::custom(format!( - "required_version: {}, specified_version: {}", - required_version, specified_version - )) - .to_string()); - } - if let Some(target_duration) = &self.target_duration_tag { self.validate_media_segments(target_duration.duration()) .map_err(|e| e.to_string())?; @@ -96,10 +72,12 @@ impl MediaPlaylistBuilder { for s in segments { // CHECK: `#EXT-X-TARGETDURATION` let segment_duration = s.inf_tag().duration(); - let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { - Duration::from_secs(segment_duration.as_secs()) - } else { - Duration::from_secs(segment_duration.as_secs() + 1) + let rounded_segment_duration = { + if segment_duration.subsec_nanos() < 500_000_000 { + Duration::from_secs(segment_duration.as_secs()) + } else { + Duration::from_secs(segment_duration.as_secs() + 1) + } }; let max_segment_duration = { @@ -138,72 +116,6 @@ impl MediaPlaylistBuilder { Ok(()) } - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.target_duration_tag - .iter() - .map(|t| t.required_version()), - ) - .chain(self.media_sequence_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.discontinuity_sequence_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.playlist_type_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.i_frames_only_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.independent_segments_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.start_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.end_list_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.segments.iter().map(|t| { - t.iter() - .map(|s| s.required_version()) - .max() - .unwrap_or(ProtocolVersion::V1) - })) - .max() - .unwrap_or_else(ProtocolVersion::latest) - } - /// Adds a media segment to the resulting playlist. pub fn push_segment>(&mut self, value: VALUE) -> &mut Self { if let Some(segments) = &mut self.segments { @@ -214,57 +126,86 @@ impl MediaPlaylistBuilder { self } - /// Parse the rest of the [MediaPlaylist] from an m3u8 file. + /// Parse the rest of the [`MediaPlaylist`] from an m3u8 file. pub fn parse(&mut self, input: &str) -> crate::Result { parse_media_playlist(input, self) } } +impl RequiredVersion for MediaPlaylistBuilder { + fn required_version(&self) -> ProtocolVersion { + required_version![ + self.target_duration_tag, + self.media_sequence_tag, + self.discontinuity_sequence_tag, + self.playlist_type_tag, + self.i_frames_only_tag, + self.independent_segments_tag, + self.start_tag, + self.end_list_tag, + self.segments + ] + } +} + impl MediaPlaylist { - /// Creates a [MediaPlaylistBuilder]. + /// Returns a builder for [`MediaPlaylist`]. pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } - /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } - - /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. + /// Returns the [`ExtXTargetDuration`] tag contained in the playlist. pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. pub const fn media_sequence_tag(&self) -> Option { self.media_sequence_tag } - /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the + /// Returns the [`ExtXDiscontinuitySequence`] tag contained in the /// playlist. pub const fn discontinuity_sequence_tag(&self) -> Option { self.discontinuity_sequence_tag } - /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. + /// Returns the [`ExtXPlaylistType`] tag contained in the playlist. pub const fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } - /// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist. + /// Returns the [`ExtXIFramesOnly`] tag contained in the playlist. pub const fn i_frames_only_tag(&self) -> Option { self.i_frames_only_tag } - /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. + /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } - /// Returns the `EXT-X-START` tag contained in the playlist. + /// Returns the [`ExtXStart`] tag contained in the playlist. pub const fn start_tag(&self) -> Option { self.start_tag } - /// Returns the `EXT-X-ENDLIST` tag contained in the playlist. + /// Returns the [`ExtXEndList`] tag contained in the playlist. pub const fn end_list_tag(&self) -> Option { self.end_list_tag } - /// Returns the media segments contained in the playlist. - pub fn segments(&self) -> &[MediaSegment] { &self.segments } + /// Returns the [`MediaSegment`]s contained in the playlist. + pub const fn segments(&self) -> &Vec { &self.segments } +} + +impl RequiredVersion for MediaPlaylist { + fn required_version(&self) -> ProtocolVersion { + required_version![ + self.target_duration_tag, + self.media_sequence_tag, + self.discontinuity_sequence_tag, + self.playlist_type_tag, + self.i_frames_only_tag, + self.independent_segments_tag, + self.start_tag, + self.end_list_tag, + self.segments + ] + } } impl fmt::Display for MediaPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; - if self.version_tag.version() != ProtocolVersion::V1 { - writeln!(f, "{}", self.version_tag)?; + if self.required_version() != ProtocolVersion::V1 { + writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } writeln!(f, "{}", self.target_duration_tag)?; if let Some(value) = &self.media_sequence_tag { @@ -304,7 +245,8 @@ fn parse_media_playlist( let mut has_partial_segment = false; let mut has_discontinuity_tag = false; - let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1 + + let mut available_key_tags: Vec = vec![]; for (i, line) in input.parse::()?.into_iter().enumerate() { match line { @@ -317,10 +259,6 @@ fn parse_media_playlist( } match tag { Tag::ExtM3u(_) => return Err(Error::invalid_input()), - Tag::ExtXVersion(t) => { - builder.version(t.version()); - has_version = true; - } Tag::ExtInf(t) => { has_partial_segment = true; segment.inf_tag(t); @@ -336,10 +274,29 @@ fn parse_media_playlist( } Tag::ExtXKey(t) => { has_partial_segment = true; - segment.push_key_tag(t); + if available_key_tags.is_empty() { + // An ExtXKey applies to every MediaSegment and to every Media + // Initialization Section declared by an EXT-X-MAP tag, that appears + // between it and the next EXT-X-KEY tag in the Playlist file with the + // same KEYFORMAT attribute (or the end of the Playlist file). + available_key_tags = available_key_tags + .into_iter() + .map(|k| { + if t.key_format() == k.key_format() { + t.clone() + } else { + k + } + }) + .collect(); + } else { + available_key_tags.push(t); + } } - Tag::ExtXMap(t) => { + Tag::ExtXMap(mut t) => { has_partial_segment = true; + + t.set_keys(available_key_tags.clone()); segment.map_tag(t); } Tag::ExtXProgramDateTime(t) => { @@ -379,7 +336,7 @@ fn parse_media_playlist( | Tag::ExtXIFrameStreamInf(_) | Tag::ExtXSessionData(_) | Tag::ExtXSessionKey(_) => { - return Err(Error::custom(tag)); + return Err(Error::unexpected_tag(tag)); } Tag::ExtXIndependentSegments(t) => { builder.independent_segments_tag(t); @@ -387,7 +344,7 @@ fn parse_media_playlist( Tag::ExtXStart(t) => { builder.start_tag(t); } - Tag::Unknown(_) => { + Tag::Unknown(_) | Tag::ExtXVersion(_) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. } @@ -395,18 +352,17 @@ fn parse_media_playlist( } Line::Uri(uri) => { segment.uri(uri); + segment.keys(available_key_tags.clone()); segments.push(segment.build().map_err(Error::builder_error)?); segment = MediaSegment::builder(); has_partial_segment = false; } } } + if has_partial_segment { return Err(Error::invalid_input()); } - if !has_version { - builder.version(ProtocolVersion::V1); - } builder.segments(segments); builder.build().map_err(Error::builder_error) @@ -423,6 +379,7 @@ impl FromStr for MediaPlaylist { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn too_large_segment_duration_test() { diff --git a/src/media_segment.rs b/src/media_segment.rs index 8f85a3c..ea4f160 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,48 +1,143 @@ use std::fmt; -use std::iter; use derive_builder::Builder; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, }; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; +use crate::{Encrypted, RequiredVersion}; -/// Media segment. -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] #[builder(setter(into, strip_option))] +/// Media segment. pub struct MediaSegment { #[builder(default)] - /// Sets all [ExtXKey] tags. - key_tags: Vec, + /// Sets all [`ExtXKey`] tags. + keys: Vec, #[builder(default)] - /// Sets an [ExtXMap] tag. + /// Sets an [`ExtXMap`] tag. map_tag: Option, #[builder(default)] - /// Sets an [ExtXByteRange] tag. + /// Sets an [`ExtXByteRange`] tag. byte_range_tag: Option, #[builder(default)] - /// Sets an [ExtXDateRange] tag. + /// Sets an [`ExtXDateRange`] tag. date_range_tag: Option, #[builder(default)] - /// Sets an [ExtXDiscontinuity] tag. + /// Sets an [`ExtXDiscontinuity`] tag. discontinuity_tag: Option, #[builder(default)] - /// Sets an [ExtXProgramDateTime] tag. + /// Sets an [`ExtXProgramDateTime`] tag. program_date_time_tag: Option, - /// Sets an [ExtInf] tag. + /// Sets an [`ExtInf`] tag. inf_tag: ExtInf, - /// Sets an Uri. + /// Sets an `URI`. uri: String, } +impl MediaSegment { + /// Returns a Builder for a [`MasterPlaylist`]. + /// + /// [`MasterPlaylist`]: crate::MasterPlaylist + pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } + + /// Returns the `URI` of the media segment. + pub const fn uri(&self) -> &String { &self.uri } + + /// Sets the `URI` of the media segment. + pub fn set_uri(&mut self, value: T) -> &mut Self + where + T: Into, + { + self.uri = value.into(); + self + } + + /// Returns the [`ExtInf`] tag associated with the media segment. + pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } + + /// Sets the [`ExtInf`] tag associated with the media segment. + pub fn set_inf_tag(&mut self, value: T) -> &mut Self + where + T: Into, + { + self.inf_tag = value.into(); + self + } + + /// Returns the [`ExtXByteRange`] tag associated with the media segment. + pub const fn byte_range_tag(&self) -> Option { self.byte_range_tag } + + /// Sets the [`ExtXByteRange`] tag associated with the media segment. + pub fn set_byte_range_tag(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.byte_range_tag = value.map(Into::into); + self + } + + /// Returns the [`ExtXDateRange`] tag associated with the media segment. + pub const fn date_range_tag(&self) -> &Option { &self.date_range_tag } + + /// Sets the [`ExtXDateRange`] tag associated with the media segment. + pub fn set_date_range_tag(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.date_range_tag = value.map(Into::into); + self + } + + /// Returns the [`ExtXDiscontinuity`] tag associated with the media segment. + pub const fn discontinuity_tag(&self) -> Option { self.discontinuity_tag } + + /// Sets the [`ExtXDiscontinuity`] tag associated with the media segment. + pub fn set_discontinuity_tag(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.discontinuity_tag = value.map(Into::into); + self + } + + /// Returns the [`ExtXProgramDateTime`] tag associated with the media + /// segment. + pub const fn program_date_time_tag(&self) -> Option { + self.program_date_time_tag + } + + /// Sets the [`ExtXProgramDateTime`] tag associated with the media + /// segment. + pub fn set_program_date_time_tag(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.program_date_time_tag = value.map(Into::into); + self + } + + /// Returns the [`ExtXMap`] tag associated with the media segment. + pub const fn map_tag(&self) -> &Option { &self.map_tag } + + /// Sets the [`ExtXMap`] tag associated with the media segment. + pub fn set_map_tag(&mut self, value: Option) -> &mut Self + where + T: Into, + { + self.map_tag = value.map(Into::into); + self + } +} + impl MediaSegmentBuilder { - /// Pushes an [ExtXKey] tag. + /// Pushes an [`ExtXKey`] tag. pub fn push_key_tag>(&mut self, value: VALUE) -> &mut Self { - if let Some(key_tags) = &mut self.key_tags { + if let Some(key_tags) = &mut self.keys { key_tags.push(value.into()); } else { - self.key_tags = Some(vec![value.into()]); + self.keys = Some(vec![value.into()]); } self } @@ -50,7 +145,7 @@ impl MediaSegmentBuilder { impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for value in &self.key_tags { + for value in &self.keys { writeln!(f, "{}", value)?; } if let Some(value) = &self.map_tag { @@ -68,59 +163,59 @@ impl fmt::Display for MediaSegment { if let Some(value) = &self.program_date_time_tag { writeln!(f, "{}", value)?; } - writeln!(f, "{},", self.inf_tag)?; + writeln!(f, "{}", self.inf_tag)?; // TODO: there might be a `,` missing writeln!(f, "{}", self.uri)?; Ok(()) } } -impl MediaSegment { - /// Creates a [MediaSegmentBuilder]. - pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } - - /// Returns the URI of the media segment. - pub const fn uri(&self) -> &String { &self.uri } - - /// Returns the `EXT-X-INF` tag associated with the media segment. - pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } - - /// Returns the `EXT-X-BYTERANGE` tag associated with the media segment. - pub const fn byte_range_tag(&self) -> Option { self.byte_range_tag } - - /// Returns the `EXT-X-DATERANGE` tag associated with the media segment. - pub fn date_range_tag(&self) -> Option<&ExtXDateRange> { self.date_range_tag.as_ref() } - - /// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment. - pub const fn discontinuity_tag(&self) -> Option { self.discontinuity_tag } - - /// Returns the `EXT-X-PROGRAM-DATE-TIME` tag associated with the media - /// segment. - pub fn program_date_time_tag(&self) -> Option<&ExtXProgramDateTime> { - self.program_date_time_tag.as_ref() - } - - /// Returns the `EXT-X-MAP` tag associated with the media segment. - pub fn map_tag(&self) -> Option<&ExtXMap> { self.map_tag.as_ref() } - - /// Returns the `EXT-X-KEY` tags associated with the media segment. - pub fn key_tags(&self) -> &[ExtXKey] { &self.key_tags } -} - impl RequiredVersion for MediaSegment { fn required_version(&self) -> ProtocolVersion { - iter::empty() - .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.required_version()), - ) - .chain(iter::once(self.inf_tag.required_version())) - .max() - .unwrap_or(ProtocolVersion::V7) + required_version![ + self.keys, + self.map_tag, + self.byte_range_tag, + self.date_range_tag, + self.discontinuity_tag, + self.program_date_time_tag, + self.inf_tag + ] + } +} + +impl Encrypted for MediaSegment { + fn keys(&self) -> &Vec { &self.keys } + + fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use std::time::Duration; + + #[test] + fn test_display() { + assert_eq!( + MediaSegment::builder() + .keys(vec![ExtXKey::empty()]) + .map_tag(ExtXMap::new("https://www.example.com/")) + .byte_range_tag(ExtXByteRange::new(20, Some(5))) + //.date_range_tag() // TODO! + .discontinuity_tag(ExtXDiscontinuity) + .inf_tag(ExtInf::new(Duration::from_secs(4))) + .uri("http://www.uri.com/") + .build() + .unwrap() + .to_string(), + "#EXT-X-KEY:METHOD=NONE\n\ + #EXT-X-MAP:URI=\"https://www.example.com/\"\n\ + #EXT-X-BYTERANGE:20@5\n\ + #EXT-X-DISCONTINUITY\n\ + #EXTINF:4,\n\ + http://www.uri.com/\n" + .to_string() + ); } } diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 2fc1523..b1f8592 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -1,11 +1,12 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.1.1. EXTM3U] +/// # [4.3.1.1. EXTM3U] +/// /// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]** /// Playlist file. /// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. @@ -32,8 +33,7 @@ use crate::Error; /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist /// [`M3U`]: https://en.wikipedia.org/wiki/M3U -/// [4.4.1.1. EXTM3U]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.1.1 +/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ExtM3u; @@ -62,6 +62,7 @@ impl FromStr for ExtM3u { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { @@ -70,7 +71,7 @@ mod test { #[test] fn test_parser() { - assert_eq!("#EXTM3U".parse::().ok(), Some(ExtM3u)); + assert_eq!("#EXTM3U".parse::().unwrap(), ExtM3u); } #[test] diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index d473810..e39f079 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -1,11 +1,12 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.1.2. EXT-X-VERSION] +/// # [4.3.1.2. EXT-X-VERSION] +/// /// The [`ExtXVersion`] tag indicates the compatibility version of the /// [`Master Playlist`] or [`Media Playlist`] file. /// It applies to the entire Playlist. @@ -41,8 +42,7 @@ use crate::Error; /// /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist -/// [4.4.1.2. EXT-X-VERSION]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.1.2 +/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct ExtXVersion(ProtocolVersion); @@ -104,6 +104,7 @@ impl FromStr for ExtXVersion { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index f886430..2e9c9f5 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -3,11 +3,12 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion, StreamInf}; +use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder}; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.5.3. EXT-X-I-FRAME-STREAM-INF] +/// # [4.3.5.3. EXT-X-I-FRAME-STREAM-INF] +/// /// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file, /// containing the I-frames of a multimedia presentation. /// @@ -16,14 +17,79 @@ use crate::Error; /// /// [`Master Playlist`]: crate::MasterPlaylist /// [`Media Playlist`]: crate::MediaPlaylist -/// [4.4.5.3. EXT-X-I-FRAME-STREAM-INF]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.3 +/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { uri: String, stream_inf: StreamInf, } +#[derive(Default, Debug, Clone, PartialEq)] +/// Builder for [`ExtXIFrameStreamInf`]. +pub struct ExtXIFrameStreamInfBuilder { + uri: Option, + stream_inf: StreamInfBuilder, +} + +impl ExtXIFrameStreamInfBuilder { + /// An `URI` to the [`MediaPlaylist`] file. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + pub fn uri>(&mut self, value: T) -> &mut Self { + self.uri = Some(value.into()); + self + } + + /// The maximum bandwidth of the stream. + pub fn bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.bandwidth(value); + self + } + + /// The average bandwidth of the stream. + pub fn average_bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.average_bandwidth(value); + self + } + + /// Every media format in any of the renditions specified by the Variant + /// Stream. + pub fn codecs>(&mut self, value: T) -> &mut Self { + self.stream_inf.codecs(value); + self + } + + /// The resolution of the stream. + pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self { + self.stream_inf.resolution(value); + self + } + + /// High-bandwidth Digital Content Protection + pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self { + self.stream_inf.hdcp_level(value); + self + } + + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. + pub fn video>(&mut self, value: T) -> &mut Self { + self.stream_inf.video(value); + self + } + + /// Build an [`ExtXIFrameStreamInf`]. + pub fn build(&self) -> crate::Result { + Ok(ExtXIFrameStreamInf { + uri: self + .uri + .clone() + .ok_or_else(|| Error::missing_value("frame rate"))?, + stream_inf: self.stream_inf.build().map_err(Error::builder_error)?, + }) + } +} + impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; @@ -35,12 +101,15 @@ impl ExtXIFrameStreamInf { /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); /// ``` pub fn new(uri: T, bandwidth: u64) -> Self { - ExtXIFrameStreamInf { + Self { uri: uri.to_string(), stream_inf: StreamInf::new(bandwidth), } } + /// Returns a builder for [`ExtXIFrameStreamInf`]. + pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() } + /// Returns the `URI`, that identifies the associated [`media playlist`]. /// /// # Example @@ -121,6 +190,34 @@ impl DerefMut for ExtXIFrameStreamInf { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_builder() { + let mut i_frame_stream_inf = + ExtXIFrameStreamInf::new("http://example.com/audio-only.m3u8", 200_000); + + i_frame_stream_inf + .set_average_bandwidth(Some(100_000)) + .set_codecs(Some("mp4a.40.5")) + .set_resolution(1920, 1080) + .set_hdcp_level(Some(HdcpLevel::None)) + .set_video(Some("video")); + + assert_eq!( + ExtXIFrameStreamInf::builder() + .uri("http://example.com/audio-only.m3u8") + .bandwidth(200_000) + .average_bandwidth(100_000) + .codecs("mp4a.40.5") + .resolution((1920, 1080)) + .hdcp_level(HdcpLevel::None) + .video("video") + .build() + .unwrap(), + i_frame_stream_inf + ); + } #[test] fn test_display() { diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 8358609..f43e73f 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -4,11 +4,12 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion, RequiredVersion}; +use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.5.1. EXT-X-MEDIA] +/// /// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s, /// that contain alternative Renditions of the same content. /// @@ -31,7 +32,7 @@ pub struct ExtXMedia { /// # Note /// This attribute is **required**. media_type: MediaType, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the `URI` that identifies the [`Media Playlist`]. /// /// # Note @@ -48,7 +49,7 @@ pub struct ExtXMedia { /// # Note /// This attribute is **required**. group_id: String, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the name of the primary language used in the rendition. /// The value has to conform to [`RFC5646`]. /// @@ -57,7 +58,7 @@ pub struct ExtXMedia { /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 language: Option, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the name of a language associated with the rendition. /// /// # Note @@ -92,14 +93,14 @@ pub struct ExtXMedia { #[builder(default)] /// Sets the value of the `forced` flag. is_forced: bool, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the identifier that specifies a rendition within the segments in /// the media playlist. instream_id: Option, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the string that represents uniform type identifiers (UTI). characteristics: Option, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the parameters of the rendition. channels: Option, } @@ -127,9 +128,11 @@ impl ExtXMediaBuilder { } 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(), - ); + return Err(Error::custom(format!( + "If `DEFAULT` is true, `AUTOSELECT` has to be true too, Default: {:?}, Autoselect: {:?}!", + self.is_default, self.is_autoselect + )) + .to_string()); } if media_type != MediaType::Subtitles && self.is_forced.is_some() { @@ -308,7 +311,7 @@ impl ExtXMedia { /// /// [`Media Playlist`]: crate::MediaPlaylist pub fn set_uri>(&mut self, value: Option) -> &mut Self { - self.uri = value.map(|v| v.into()); + self.uri = value.map(Into::into); self } @@ -346,7 +349,7 @@ impl ExtXMedia { /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 pub fn set_language>(&mut self, value: Option) -> &mut Self { - self.language = value.map(|v| v.into()); + self.language = value.map(Into::into); self } @@ -386,7 +389,7 @@ impl ExtXMedia { /// /// [`language`]: #method.language pub fn set_assoc_language>(&mut self, value: Option) -> &mut Self { - self.assoc_language = value.map(|v| v.into()); + self.assoc_language = value.map(Into::into); self } @@ -588,7 +591,7 @@ impl ExtXMedia { /// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI /// [`subtitles`]: crate::types::MediaType::Subtitles pub fn set_characteristics>(&mut self, value: Option) -> &mut Self { - self.characteristics = value.map(|v| v.into()); + self.characteristics = value.map(Into::into); self } @@ -623,7 +626,7 @@ impl ExtXMedia { /// assert_eq!(media.channels(), &Some(Channels::new(6))); /// ``` pub fn set_channels>(&mut self, value: Option) -> &mut Self { - self.channels = value.map(|v| v.into()); + self.channels = value.map(Into::into); self } } @@ -739,6 +742,7 @@ impl FromStr for ExtXMedia { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index a6fd617..a264158 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -4,18 +4,21 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// The data of an [ExtXSessionData] tag. +/// The data of an [`ExtXSessionData`] tag. #[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] pub enum SessionData { /// A String, that contains the data identified by - /// [`data_id`](ExtXSessionData::data_id). - /// If a [`language`](ExtXSessionData::language) is specified, the value + /// [`data_id`]. + /// If a [`language`] is specified, the value /// should contain a human-readable string written in the specified /// language. + /// + /// [`data_id`]: ExtXSessionData::data_id + /// [`language`]: ExtXSessionData::language Value(String), /// An [`uri`], which points to a [`json`]. /// @@ -35,17 +38,20 @@ pub enum SessionData { #[builder(setter(into))] pub struct ExtXSessionData { /// The identifier of the data. - /// For more information look [`here`](ExtXSessionData::set_data_id). + /// For more information look [`here`]. /// /// # Note /// This field is required. + /// + /// [`here`]: ExtXSessionData::set_data_id data_id: String, - /// The data associated with the - /// [`data_id`](ExtXSessionDataBuilder::data_id). + /// The data associated with the [`data_id`]. /// For more information look [`here`](SessionData). /// /// # Note /// This field is required. + /// + /// [`data_id`]: ExtXSessionDataBuilder::data_id data: SessionData, /// The language of the [`data`](ExtXSessionDataBuilder::data). #[builder(setter(into, strip_option), default)] @@ -318,6 +324,7 @@ impl FromStr for ExtXSessionData { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index c9776fc..6bd3731 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -2,9 +2,9 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVersion}; +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.3.4.5. EXT-X-SESSION-KEY] /// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s @@ -86,6 +86,7 @@ impl DerefMut for ExtXSessionKey { mod test { use super::*; use crate::types::{EncryptionMethod, KeyFormat}; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 4772ae6..d8cc87d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -4,15 +4,24 @@ use std::str::FromStr; use crate::attribute::AttributePairs; use crate::types::{ - ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, RequiredVersion, StreamInf, + ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder, }; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// [4.3.4.2. EXT-X-STREAM-INF] +/// # [4.3.4.2. EXT-X-STREAM-INF] +/// +/// The [`ExtXStreamInf`] tag specifies a Variant Stream, which is a set +/// of Renditions that can be combined to play the presentation. The +/// attributes of the tag provide information about the Variant Stream. +/// +/// The URI line that follows the [`ExtXStreamInf`] tag specifies a Media +/// Playlist that carries a rendition of the Variant Stream. The URI +/// line is REQUIRED. Clients that do not support multiple video +/// Renditions SHOULD play this Rendition. /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(PartialOrd, Debug, Clone, PartialEq, Eq)] +#[derive(PartialOrd, Debug, Clone, PartialEq)] pub struct ExtXStreamInf { uri: String, frame_rate: Option, @@ -22,6 +31,104 @@ pub struct ExtXStreamInf { stream_inf: StreamInf, } +#[derive(Default, Debug, Clone)] +/// Builder for [`ExtXStreamInf`]. +pub struct ExtXStreamInfBuilder { + uri: Option, + frame_rate: Option, + audio: Option, + subtitles: Option, + closed_captions: Option, + stream_inf: StreamInfBuilder, +} + +impl ExtXStreamInfBuilder { + /// An `URI` to the [`MediaPlaylist`] file. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + pub fn uri>(&mut self, value: T) -> &mut Self { + self.uri = Some(value.into()); + self + } + + /// Maximum frame rate for all the video in the variant stream. + pub fn frame_rate(&mut self, value: f64) -> &mut Self { + self.frame_rate = Some(value.into()); + self + } + + /// The group identifier for the audio in the variant stream. + pub fn audio>(&mut self, value: T) -> &mut Self { + self.audio = Some(value.into()); + self + } + + /// The group identifier for the subtitles in the variant stream. + pub fn subtitles>(&mut self, value: T) -> &mut Self { + self.subtitles = Some(value.into()); + self + } + + /// The value of [`ClosedCaptions`] attribute. + pub fn closed_captions>(&mut self, value: T) -> &mut Self { + self.closed_captions = Some(value.into()); + self + } + + /// The maximum bandwidth of the stream. + pub fn bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.bandwidth(value); + self + } + + /// The average bandwidth of the stream. + pub fn average_bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.average_bandwidth(value); + self + } + + /// Every media format in any of the renditions specified by the Variant + /// Stream. + pub fn codecs>(&mut self, value: T) -> &mut Self { + self.stream_inf.codecs(value); + self + } + + /// The resolution of the stream. + pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self { + self.stream_inf.resolution(value); + self + } + + /// High-bandwidth Digital Content Protection + pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self { + self.stream_inf.hdcp_level(value); + self + } + + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. + pub fn video>(&mut self, value: T) -> &mut Self { + self.stream_inf.video(value); + self + } + + /// Build an [`ExtXStreamInf`]. + pub fn build(&self) -> crate::Result { + Ok(ExtXStreamInf { + uri: self + .uri + .clone() + .ok_or_else(|| Error::missing_value("frame rate"))?, + frame_rate: self.frame_rate, + audio: self.audio.clone(), + subtitles: self.subtitles.clone(), + closed_captions: self.closed_captions.clone(), + stream_inf: self.stream_inf.build().map_err(Error::builder_error)?, + }) + } +} + impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; @@ -43,6 +150,9 @@ impl ExtXStreamInf { } } + /// Returns a builder for [`ExtXStreamInf`]. + pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() } + /// Returns the `URI` that identifies the associated media playlist. /// /// # Example @@ -81,7 +191,7 @@ impl ExtXStreamInf { /// assert_eq!(stream.frame_rate(), Some(59.9)); /// ``` pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { - self.frame_rate = value.map(|v| v.into()); + self.frame_rate = value.map(Into::into); self } @@ -123,7 +233,7 @@ impl ExtXStreamInf { /// assert_eq!(stream.audio(), &Some("audio".to_string())); /// ``` pub fn set_audio>(&mut self, value: Option) -> &mut Self { - self.audio = value.map(|v| v.into()); + self.audio = value.map(Into::into); self } @@ -152,7 +262,7 @@ impl ExtXStreamInf { /// assert_eq!(stream.subtitles(), &Some("subs".to_string())); /// ``` pub fn set_subtitles>(&mut self, value: Option) -> &mut Self { - self.subtitles = value.map(|v| v.into()); + self.subtitles = value.map(Into::into); self } @@ -171,7 +281,7 @@ impl ExtXStreamInf { /// ``` pub const fn closed_captions(&self) -> &Option { &self.closed_captions } - /// Returns the value of [`ClosedCaptions`] attribute. + /// Sets the value of [`ClosedCaptions`] attribute. /// /// # Example /// ``` @@ -190,6 +300,7 @@ impl ExtXStreamInf { } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXStreamInf { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -265,6 +376,7 @@ impl DerefMut for ExtXStreamInf { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 1ca9d8e..c49b9d7 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,8 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; +use crate::RequiredVersion; /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// @@ -83,6 +84,7 @@ impl FromStr for ExtXDiscontinuitySequence { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 119de43..5463f5e 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.4. EXT-X-ENDLIST] /// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be @@ -18,7 +18,7 @@ use crate::Error; /// [`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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXEndList; impl ExtXEndList { @@ -38,13 +38,14 @@ impl FromStr for ExtXEndList { fn from_str(input: &str) -> Result { tag(input, Self::PREFIX)?; - Ok(ExtXEndList) + Ok(Self) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 7c52ba4..c9c4dc9 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] /// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the @@ -20,7 +20,7 @@ use crate::Error; /// [`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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXIFramesOnly; impl ExtXIFramesOnly { @@ -40,13 +40,14 @@ impl FromStr for ExtXIFramesOnly { fn from_str(input: &str) -> Result { tag(input, Self::PREFIX)?; - Ok(ExtXIFramesOnly) + Ok(Self) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index e80a570..5e26eb7 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -1,20 +1,14 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [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. /// -/// 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 @@ -61,6 +55,7 @@ impl ExtXMediaSequence { } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXMediaSequence { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -74,13 +69,14 @@ impl FromStr for ExtXMediaSequence { fn from_str(input: &str) -> Result { let seq_num = tag(input, Self::PREFIX)?.parse()?; - Ok(ExtXMediaSequence::new(seq_num)) + Ok(Self::new(seq_num)) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 80bcee8..69c2e52 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -1,30 +1,29 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE] +/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE] /// /// 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: -/// ``` -/// -/// [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)] +/// [`Media Playlist`]: crate::MediaPlaylist +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum ExtXPlaylistType { - /// If the [`ExtXPlaylistType`] is Event, Media Segments - /// can only be added to the end of the Media Playlist. + /// If the [`ExtXPlaylistType`] is Event, [`Media Segment`]s + /// can only be added to the end of the [`Media Playlist`]. + /// + /// [`Media Segment`]: crate::MediaSegment + /// [`Media Playlist`]: crate::MediaPlaylist Event, /// If the [`ExtXPlaylistType`] is Video On Demand (Vod), - /// the Media Playlist cannot change. + /// the [`Media Playlist`] cannot change. + /// + /// [`Media Playlist`]: crate::MediaPlaylist Vod, } @@ -32,6 +31,7 @@ impl ExtXPlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXPlaylistType { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -61,6 +61,7 @@ impl FromStr for ExtXPlaylistType { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { @@ -81,6 +82,8 @@ mod test { assert!("#EXT-X-PLAYLIST-TYPE:H" .parse::() .is_err()); + + assert!("garbage".parse::().is_err()); } #[test] diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index c70ab24..652afb4 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -1,19 +1,19 @@ use std::fmt; +use std::ops::Deref; use std::str::FromStr; use std::time::Duration; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.3.1. EXT-X-TARGETDURATION] -/// The [`ExtXTargetDuration`] tag specifies the maximum [`Media Segment`] +/// # [4.3.3.1. EXT-X-TARGETDURATION] +/// The [`ExtXTargetDuration`] tag specifies the maximum [`MediaSegment`] /// duration. /// -/// [`Media Segment`]: crate::MediaSegment -/// [4.4.3.1. EXT-X-TARGETDURATION]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.3.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +/// [`MediaSegment`]: crate::MediaSegment +/// [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, PartialOrd, Ord)] pub struct ExtXTargetDuration(Duration); impl ExtXTargetDuration { @@ -31,10 +31,7 @@ impl ExtXTargetDuration { /// /// # 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())) - } + pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) } /// Returns the maximum media segment duration. /// @@ -55,6 +52,12 @@ impl RequiredVersion for ExtXTargetDuration { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } +impl Deref for ExtXTargetDuration { + type Target = Duration; + + fn deref(&self) -> &Self::Target { &self.0 } +} + impl fmt::Display for ExtXTargetDuration { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0.as_secs()) @@ -73,6 +76,7 @@ impl FromStr for ExtXTargetDuration { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { @@ -97,4 +101,9 @@ mod test { "#EXT-X-TARGETDURATION:5".parse().unwrap() ); } + + #[test] + fn test_deref() { + assert_eq!(ExtXTargetDuration::new(Duration::from_secs(5)).as_secs(), 5); + } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index a79769f..f2aafb4 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -2,9 +2,9 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; +use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.2.2. EXT-X-BYTERANGE] /// @@ -23,7 +23,7 @@ use crate::Error; /// [`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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { @@ -97,13 +97,14 @@ impl FromStr for ExtXByteRange { } }; - Ok(ExtXByteRange::new(length, start)) + Ok(Self::new(length, start)) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 67c1089..6586f8a 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -3,63 +3,122 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, SecondsFormat}; +use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::{ProtocolVersion, Value}; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.3.2.7. EXT-X-DATERANGE] +/// The [`ExtXDateRange`] tag associates a date range (i.e., a range of +/// time defined by a starting and ending date) with a set of attribute/ +/// value pairs. /// /// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 -/// -/// TODO: Implement properly -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, Debug, Clone, PartialEq, PartialOrd)] +#[builder(setter(into))] pub struct ExtXDateRange { - /// A string that uniquely identifies a [`ExtXDateRange`] in the Playlist. + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Note /// This attribute is required. id: String, + #[builder(setter(strip_option), default)] /// 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 semantics. This attribute is - /// OPTIONAL. + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Note + /// This attribute is optional. class: Option, - /// The date at which the Date Range begins. This attribute is REQUIRED. + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Note + /// This attribute is required. start_date: DateTime, - /// The date at which the Date Range ends. It MUST be equal to or later than - /// the value of the START-DATE attribute. This attribute is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Note + /// This attribute is optional. + /// + /// [`start-date`]: #method.start_date end_date: Option>, - /// The duration of the Date Range. It MUST NOT be negative. A single - /// instant in time (e.g., crossing a finish line) SHOULD be - /// represented with a duration of 0. This attribute is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Note + /// This attribute is optional. duration: Option, - /// The expected duration of the Date Range. It MUST NOT be negative. This - /// attribute SHOULD be used to indicate the expected duration of a - /// Date Range whose actual duration is not yet known. - /// It is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// + /// # Note + /// This attribute is optional. planned_duration: Option, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_cmd: Option, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_out: Option, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_in: Option, + #[builder(default)] /// This attribute indicates that the end of the range containing it is - /// equal to the START-DATE of its Following Range. The Following Range - /// is the Date Range of the same CLASS, that has the earliest - /// START-DATE after the START-DATE of the range in question. This - /// attribute is OPTIONAL. + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Note + /// This attribute is optional. end_on_next: bool, - /// The "X-" prefix defines a namespace reserved for client-defined - /// attributes. The client-attribute MUST be a legal AttributeName. - /// Clients SHOULD use a reverse-DNS syntax when defining their own - /// attribute names to avoid collisions. The attribute value MUST be - /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- - /// point. An example of a client-defined attribute is X-COM-EXAMPLE- - /// AD-ID="XYZ123". These attributes are OPTIONAL. - client_attributes: BTreeMap, + #[builder(default)] + /// The `"X-"` prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Note + /// This attribute is optional. + client_attributes: BTreeMap, +} + +impl ExtXDateRangeBuilder { + /// Inserts a key value pair. + pub fn insert_client_attribute>( + &mut self, + key: K, + value: V, + ) -> &mut Self { + if self.client_attributes.is_none() { + self.client_attributes = Some(BTreeMap::new()); + } + + if let Some(client_attributes) = &mut self.client_attributes { + client_attributes.insert(key.to_string(), value.into()); + } else { + unreachable!(); + } + self + } } impl ExtXDateRange { @@ -97,67 +156,545 @@ impl ExtXDateRange { client_attributes: BTreeMap::new(), } } + + /// Returns a builder for [`ExtXDateRange`]. + pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() } + + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// assert_eq!(date_range.id(), &"id".to_string()); + /// ``` + pub const fn id(&self) -> &String { &self.id } + + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// date_range.set_id("new_id"); + /// assert_eq!(date_range.id(), &"new_id".to_string()); + /// ``` + pub fn set_id(&mut self, value: T) -> &mut Self { + self.id = value.to_string(); + self + } + + /// A client-defined string that specifies some set of attributes and their + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.class(), &None); + /// + /// date_range.set_class(Some("example_class")); + /// assert_eq!(date_range.class(), &Some("example_class".to_string())); + /// ``` + pub const fn class(&self) -> &Option { &self.class } + + /// A client-defined string that specifies some set of attributes and their + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.class(), &None); + /// + /// date_range.set_class(Some("example_class")); + /// assert_eq!(date_range.class(), &Some("example_class".to_string())); + /// ``` + pub fn set_class(&mut self, value: Option) -> &mut Self { + self.class = value.map(|v| v.to_string()); + self + } + + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// assert_eq!( + /// date_range.start_date(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31) + /// ); + /// ``` + pub const fn start_date(&self) -> DateTime { self.start_date } + + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// date_range.set_start_date( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// ); + /// assert_eq!( + /// date_range.start_date(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ); + /// ``` + pub fn set_start_date(&mut self, value: T) -> &mut Self + where + T: Into>, + { + self.start_date = value.into(); + self + } + + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_date(), None); + /// + /// date_range.set_end_date(Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// )); + /// assert_eq!( + /// date_range.end_date(), + /// Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ) + /// ); + /// ``` + pub const fn end_date(&self) -> Option> { self.end_date } + + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_date(), None); + /// + /// date_range.set_end_date(Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// )); + /// assert_eq!( + /// date_range.end_date(), + /// Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ) + /// ); + /// ``` + pub fn set_end_date(&mut self, value: Option) -> &mut Self + where + T: Into>, + { + self.end_date = value.map(Into::into); + self + } + + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.duration(), None); + /// + /// date_range.set_duration(Some(Duration::from_secs_f64(1.234))); + /// assert_eq!(date_range.duration(), Some(Duration::from_secs_f64(1.234))); + /// ``` + pub const fn duration(&self) -> Option { self.duration } + + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.duration(), None); + /// + /// date_range.set_duration(Some(Duration::from_secs_f64(1.234))); + /// assert_eq!(date_range.duration(), Some(Duration::from_secs_f64(1.234))); + /// ``` + pub fn set_duration(&mut self, value: Option) -> &mut Self { + self.duration = value; + self + } + + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.planned_duration(), None); + /// + /// date_range.set_planned_duration(Some(Duration::from_secs_f64(1.2345))); + /// assert_eq!( + /// date_range.planned_duration(), + /// Some(Duration::from_secs_f64(1.2345)) + /// ); + /// ``` + pub const fn planned_duration(&self) -> Option { self.planned_duration } + + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.planned_duration(), None); + /// + /// date_range.set_planned_duration(Some(Duration::from_secs_f64(1.2345))); + /// assert_eq!( + /// date_range.planned_duration(), + /// Some(Duration::from_secs_f64(1.2345)) + /// ); + /// ``` + pub fn set_planned_duration(&mut self, value: Option) -> &mut Self { + self.planned_duration = value; + self + } + + /// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf + pub const fn scte35_cmd(&self) -> &Option { &self.scte35_cmd } + + /// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf + pub const fn scte35_in(&self) -> &Option { &self.scte35_in } + + /// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf + pub const fn scte35_out(&self) -> &Option { &self.scte35_out } + + /// This attribute indicates that the end of the range containing it is + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_on_next(), false); + /// + /// date_range.set_end_on_next(true); + /// assert_eq!(date_range.end_on_next(), true); + /// ``` + pub const fn end_on_next(&self) -> bool { self.end_on_next } + + /// This attribute indicates that the end of the range containing it is + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_on_next(), false); + /// + /// date_range.set_end_on_next(true); + /// assert_eq!(date_range.end_on_next(), true); + /// ``` + pub fn set_end_on_next(&mut self, value: bool) -> &mut Self { + self.end_on_next = value; + self + } + + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use std::collections::BTreeMap; + /// + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use hls_m3u8::types::Value; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.client_attributes(), &BTreeMap::new()); + /// + /// let mut attributes = BTreeMap::new(); + /// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// date_range.set_client_attributes(attributes.clone()); + /// assert_eq!(date_range.client_attributes(), &attributes); + /// ``` + pub const fn client_attributes(&self) -> &BTreeMap { &self.client_attributes } + + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use std::collections::BTreeMap; + /// + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use hls_m3u8::types::Value; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.client_attributes(), &BTreeMap::new()); + /// + /// let mut attributes = BTreeMap::new(); + /// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// date_range + /// .client_attributes_mut() + /// .insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// assert_eq!(date_range.client_attributes(), &attributes); + /// ``` + pub fn client_attributes_mut(&mut self) -> &mut BTreeMap { + &mut self.client_attributes + } + + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use std::collections::BTreeMap; + /// + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use hls_m3u8::types::Value; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.client_attributes(), &BTreeMap::new()); + /// + /// let mut attributes = BTreeMap::new(); + /// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// date_range.set_client_attributes(attributes.clone()); + /// assert_eq!(date_range.client_attributes(), &attributes); + /// ``` + pub fn set_client_attributes(&mut self, value: BTreeMap) -> &mut Self { + self.client_attributes = value; + self + } } /// This tag requires [`ProtocolVersion::V1`]. -/// -/// # Example -/// ``` -/// # use hls_m3u8::tags::ExtXDateRange; -/// use chrono::offset::TimeZone; -/// use chrono::{DateTime, FixedOffset}; -/// use hls_m3u8::types::{ProtocolVersion, RequiredVersion}; -/// -/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds -/// -/// let date_range = ExtXDateRange::new( -/// "id", -/// FixedOffset::east(8 * HOURS_IN_SECS) -/// .ymd(2010, 2, 19) -/// .and_hms_milli(14, 54, 23, 31), -/// ); -/// assert_eq!(date_range.required_version(), ProtocolVersion::V1); -/// ``` impl RequiredVersion for ExtXDateRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl fmt::Display for ExtXDateRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "ID={}", quote(&self.id))?; - if let Some(value) = &self.class { - write!(f, ",CLASS={}", quote(value))?; - } - write!(f, ",START-DATE={}", quote(&self.start_date))?; - if let Some(value) = &self.end_date { - write!(f, ",END-DATE={}", quote(value))?; - } - if let Some(value) = &self.duration { - write!(f, ",DURATION={}", value.as_secs_f64())?; - } - if let Some(value) = &self.planned_duration { - write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; - } - if let Some(value) = &self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", quote(value))?; - } - if let Some(value) = &self.scte35_out { - write!(f, ",SCTE35-OUT={}", quote(value))?; - } - if let Some(value) = &self.scte35_in { - write!(f, ",SCTE35-IN={}", quote(value))?; - } - if self.end_on_next { - write!(f, ",END-ON-NEXT=YES",)?; - } - for (k, v) in &self.client_attributes { - write!(f, ",{}={}", k, v)?; - } - Ok(()) - } -} - impl FromStr for ExtXDateRange { type Err = Error; @@ -194,13 +731,13 @@ impl FromStr for ExtXDateRange { "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { if value != "YES" { - return Err(Error::invalid_input()); + return Err(Error::custom("The value of `END-ON-NEXT` has to be `YES`!")); } end_on_next = true; } _ => { if key.starts_with("X-") { - client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); + client_attributes.insert(key.to_ascii_uppercase(), value.parse()?); } else { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an @@ -234,13 +771,174 @@ impl FromStr for ExtXDateRange { } } +impl fmt::Display for ExtXDateRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "ID={}", quote(&self.id))?; + if let Some(value) = &self.class { + write!(f, ",CLASS={}", quote(value))?; + } + + write!( + f, + ",START-DATE={}", + quote(&self.start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + + if let Some(value) = &self.end_date { + write!( + f, + ",END-DATE={}", + quote(value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + } + + if let Some(value) = &self.duration { + write!(f, ",DURATION={}", value.as_secs_f64())?; + } + + if let Some(value) = &self.planned_duration { + write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; + } + + if let Some(value) = &self.scte35_cmd { + write!(f, ",SCTE35-CMD={}", value)?; + } + + if let Some(value) = &self.scte35_out { + write!(f, ",SCTE35-OUT={}", value)?; + } + + if let Some(value) = &self.scte35_in { + write!(f, ",SCTE35-IN={}", value)?; + } + + for (k, v) in &self.client_attributes { + write!(f, ",{}={}", k, v)?; + } + + if self.end_on_next { + write!(f, ",END-ON-NEXT=YES",)?; + } + + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; use chrono::offset::TimeZone; + use pretty_assertions::assert_eq; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + #[test] + fn test_parser() { + assert_eq!( + "#EXT-X-DATERANGE:\ + ID=\"splice-6FFFFFF0\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + PLANNED-DURATION=59.993,\ + SCTE35-OUT=0xFC002F0000000000FF000014056F\ + FFFFF000E011622DCAFF000052636200000000000\ + A0008029896F50000008700000000" + .parse::() + .unwrap(), + ExtXDateRange::builder() + .id("splice-6FFFFFF0") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .planned_duration(Duration::from_secs_f64(59.993)) + .scte35_out( + "0xFC002F0000000000FF00001\ + 4056FFFFFF000E011622DCAFF0\ + 00052636200000000000A00080\ + 29896F50000008700000000" + ) + .build() + .unwrap() + ); + + assert_eq!( + "#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + CLASS=\"test_class\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-DATE=\"2014-03-05T11:16:00Z\",\ + DURATION=60.1,\ + PLANNED-DURATION=59.993,\ + X-CUSTOM=45.3,\ + SCTE35-CMD=0xFC002F0000000000FF2,\ + SCTE35-OUT=0xFC002F0000000000FF0,\ + SCTE35-IN=0xFC002F0000000000FF1,\ + END-ON-NEXT=YES,\ + UNKNOWN=PHANTOM" + .parse::() + .unwrap(), + ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0)) + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", 45.3) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build() + .unwrap() + ); + + assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO" + .parse::() + .is_err()); + + assert!("garbage".parse::().is_err()); + assert!("".parse::().is_err()); + + assert!("#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-ON-NEXT=YES" + .parse::() + .is_err()); + } + + #[test] + fn test_display() { + assert_eq!( + ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0)) + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", 45.3) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build() + .unwrap() + .to_string(), + "#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + CLASS=\"test_class\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-DATE=\"2014-03-05T11:16:00Z\",\ + DURATION=60.1,\ + PLANNED-DURATION=59.993,\ + SCTE35-CMD=0xFC002F0000000000FF2,\ + SCTE35-OUT=0xFC002F0000000000FF0,\ + SCTE35-IN=0xFC002F0000000000FF1,\ + X-CUSTOM=45.3,\ + END-ON-NEXT=YES" + ) + } + #[test] fn test_required_version() { assert_eq!( diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index ee0d607..5a40451 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, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.2.3. EXT-X-DISCONTINUITY] /// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the @@ -17,7 +17,7 @@ use crate::Error; /// [`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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXDiscontinuity; impl ExtXDiscontinuity { @@ -37,13 +37,14 @@ impl FromStr for ExtXDiscontinuity { fn from_str(input: &str) -> Result { tag(input, Self::PREFIX)?; - Ok(ExtXDiscontinuity) + Ok(Self) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index e13338f..fc1cb51 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -2,24 +2,17 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.2.1. EXTINF] +/// # [4.3.2.1. EXTINF] /// /// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies /// only to the next [`Media Segment`]. /// -/// Its format is: -/// ```text -/// #EXTINF:,[] -/// ``` -/// The title is an optional informative title about the [Media Segment]. -/// /// [`Media Segment`]: crate::media_segment::MediaSegment -/// [4.4.2.1. EXTINF]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1 +/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { duration: Duration, @@ -137,10 +130,7 @@ impl RequiredVersion for ExtInf { impl fmt::Display for ExtInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - - let duration = (self.duration.as_secs() as f64) - + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); - write!(f, "{},", duration)?; + write!(f, "{},", self.duration.as_secs_f64())?; if let Some(value) = &self.title { write!(f, "{}", value)?; @@ -188,6 +178,7 @@ impl From<Duration> for ExtInf { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { @@ -236,6 +227,9 @@ mod test { "#EXTINF:5,title".parse::<ExtInf>().unwrap(), ExtInf::with_title(Duration::from_secs(5), "title") ); + + assert!("#EXTINF:".parse::<ExtInf>().is_err()); + assert!("#EXTINF:garbage".parse::<ExtInf>().is_err()); } #[test] @@ -258,4 +252,12 @@ mod test { ProtocolVersion::V3 ); } + + #[test] + fn test_from() { + assert_eq!( + ExtInf::from(Duration::from_secs(1)), + ExtInf::new(Duration::from_secs(1)) + ); + } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index df15ef2..4ebe50b 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -2,32 +2,27 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod}; +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.2.4. EXT-X-KEY] +/// # [4.3.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:<attribute-list> -/// ``` -/// /// # Note -/// In case of an empty key (`EncryptionMethod::None`), +/// 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)] +/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXKey(DecryptionKey); impl ExtXKey { @@ -37,7 +32,7 @@ impl ExtXKey { /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXKey; + /// # use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; /// /// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); @@ -55,8 +50,7 @@ impl ExtXKey { /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// + /// # use hls_m3u8::tags::ExtXKey; /// let key = ExtXKey::empty(); /// /// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE"); @@ -72,20 +66,26 @@ impl ExtXKey { } /// Returns whether the [`EncryptionMethod`] is - /// [`None`](EncryptionMethod::None). + /// [`None`]. /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXKey; + /// # use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; /// /// let key = ExtXKey::empty(); /// /// assert_eq!(key.method() == EncryptionMethod::None, key.is_empty()); /// ``` + /// + /// [`None`]: EncryptionMethod::None pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } } +impl RequiredVersion for ExtXKey { + fn required_version(&self) -> ProtocolVersion { self.0.required_version() } +} + impl FromStr for ExtXKey { type Err = Error; @@ -113,6 +113,7 @@ impl DerefMut for ExtXKey { mod test { use super::*; use crate::types::{EncryptionMethod, KeyFormat}; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 05de349..f63f8d5 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -2,66 +2,159 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; +use crate::tags::ExtXKey; +use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Encrypted, Error, RequiredVersion}; -/// # [4.4.2.5. EXT-X-MAP] +/// # [4.3.2.5. EXT-X-MAP] +/// /// The [`ExtXMap`] tag specifies how to obtain the Media Initialization -/// Section, required to parse the applicable [Media Segment]s. +/// Section, required to parse the applicable [`MediaSegment`]s. /// -/// Its format is: -/// ```text -/// #EXT-X-MAP:<attribute-list> -/// ``` -/// -/// [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)] +/// [`MediaSegment`]: crate::MediaSegment +/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXMap { uri: String, range: Option<ByteRange>, + keys: Vec<ExtXKey>, } impl ExtXMap { pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; /// Makes a new [`ExtXMap`] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); + /// ``` pub fn new<T: ToString>(uri: T) -> Self { - ExtXMap { + Self { uri: uri.to_string(), range: None, + keys: vec![], } } /// Makes a new [`ExtXMap`] tag with the given range. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::ByteRange; + /// + /// let map = ExtXMap::with_range( + /// "https://prod.mediaspace.com/init.bin", + /// ByteRange::new(9, Some(2)), + /// ); + /// ``` pub fn with_range<T: ToString>(uri: T, range: ByteRange) -> Self { - ExtXMap { + Self { uri: uri.to_string(), range: Some(range), + keys: vec![], } } - /// Returns the `URI` that identifies a resource, - /// that contains the media initialization section. + /// Returns the `URI` that identifies a resource, that contains the media + /// initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); + /// + /// assert_eq!( + /// map.uri(), + /// &"https://prod.mediaspace.com/init.bin".to_string() + /// ); + /// ``` pub const fn uri(&self) -> &String { &self.uri } + /// Sets the `URI` that identifies a resource, that contains the media + /// initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// let mut map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); + /// + /// map.set_uri("https://dev.mediaspace.com/init.bin"); + /// assert_eq!( + /// map.uri(), + /// &"https://dev.mediaspace.com/init.bin".to_string() + /// ); + /// ``` + pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self { + self.uri = value.to_string(); + self + } + /// Returns the range of the media initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::ByteRange; + /// + /// let map = ExtXMap::with_range( + /// "https://prod.mediaspace.com/init.bin", + /// ByteRange::new(9, Some(2)), + /// ); + /// + /// assert_eq!(map.range(), Some(ByteRange::new(9, Some(2)))); + /// ``` pub const fn range(&self) -> Option<ByteRange> { self.range } + + /// Sets the range of the media initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::ByteRange; + /// + /// let mut map = ExtXMap::with_range( + /// "https://prod.mediaspace.com/init.bin", + /// ByteRange::new(9, Some(2)), + /// ); + /// + /// map.set_range(Some(ByteRange::new(1, None))); + /// assert_eq!(map.range(), Some(ByteRange::new(1, None))); + /// ``` + pub fn set_range(&mut self, value: Option<ByteRange>) -> &mut Self { + self.range = value; + self + } } +impl Encrypted for ExtXMap { + fn keys(&self) -> &Vec<ExtXKey> { &self.keys } + + fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys } +} + +/// This tag requires [`ProtocolVersion::V6`]. impl RequiredVersion for ExtXMap { + // this should return ProtocolVersion::V5, if it does not contain an + // EXT-X-I-FRAMES-ONLY! + // http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/ fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } + + fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } impl fmt::Display for ExtXMap { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "URI={}", quote(&self.uri))?; + if let Some(value) = &self.range { write!(f, ",BYTERANGE={}", quote(value))?; } + Ok(()) } } @@ -79,7 +172,7 @@ impl FromStr for ExtXMap { match key.as_str() { "URI" => uri = Some(unquote(value)), "BYTERANGE" => { - range = Some((unquote(value).parse())?); + range = Some(unquote(value).parse()?); } _ => { // [6.3.1. General Client Responsibilities] @@ -90,13 +183,18 @@ impl FromStr for ExtXMap { } let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?; - Ok(ExtXMap { uri, range }) + Ok(Self { + uri, + range, + keys: vec![], + }) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { @@ -122,6 +220,12 @@ mod test { ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap() ); + assert_eq!( + ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED" + .parse() + .unwrap() + ); } #[test] @@ -132,4 +236,10 @@ mod test { ProtocolVersion::V6 ); } + + #[test] + fn test_encrypted() { + assert_eq!(ExtXMap::new("foo").keys(), &vec![]); + assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]); + } } diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 4ce0aa2..5ea34ca 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -2,11 +2,11 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, SecondsFormat}; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] /// The [`ExtXProgramDateTime`] tag associates the first sample of a @@ -24,8 +24,8 @@ impl ExtXProgramDateTime { /// /// # Example /// ``` + /// # use hls_m3u8::tags::ExtXProgramDateTime; /// use chrono::{FixedOffset, TimeZone}; - /// use hls_m3u8::tags::ExtXProgramDateTime; /// /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds /// @@ -39,22 +39,71 @@ impl ExtXProgramDateTime { /// Returns the date-time of the first sample of the associated media /// segment. + /// + /// # 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), + /// ); + /// + /// assert_eq!( + /// program_date_time.date_time(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31) + /// ); + /// ``` pub const fn date_time(&self) -> DateTime<FixedOffset> { self.0 } /// Sets the date-time of the first sample of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXProgramDateTime; + /// use chrono::{FixedOffset, TimeZone}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut program_date_time = ExtXProgramDateTime::new( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// program_date_time.set_date_time( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// ); + /// + /// assert_eq!( + /// program_date_time.date_time(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ); + /// ``` pub fn set_date_time(&mut self, value: DateTime<FixedOffset>) -> &mut Self { self.0 = value; self } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXProgramDateTime { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXProgramDateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let date_time = self.0.to_rfc3339(); + let date_time = self.0.to_rfc3339_opts(SecondsFormat::Millis, true); write!(f, "{}{}", Self::PREFIX, date_time) } } @@ -83,7 +132,8 @@ impl DerefMut for ExtXProgramDateTime { #[cfg(test)] mod test { use super::*; - use chrono::TimeZone; + use chrono::{Datelike, TimeZone}; + use pretty_assertions::assert_eq; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds @@ -126,4 +176,32 @@ mod test { ProtocolVersion::V1 ); } + + #[test] + fn test_deref() { + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ) + .year(), + 2010 + ); + } + + #[test] + fn test_deref_mut() { + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ) + .deref_mut(), + &mut FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ); + } } diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 9655e0f..47b0f66 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// @@ -28,13 +28,14 @@ impl FromStr for ExtXIndependentSegments { fn from_str(input: &str) -> Result<Self, Self::Err> { tag(input, Self::PREFIX)?; - Ok(ExtXIndependentSegments) + Ok(Self) } } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 045bfa4..22a3acb 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -2,14 +2,14 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion, SignedDecimalFloatingPoint}; +use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; use crate::utils::{parse_yes_or_no, tag}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// [4.3.5.2. EXT-X-START] /// /// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 -#[derive(PartialOrd, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(PartialOrd, Debug, Clone, Copy, PartialEq)] pub struct ExtXStart { time_offset: SignedDecimalFloatingPoint, precise: bool, @@ -26,7 +26,7 @@ impl ExtXStart { /// # Example /// ``` /// # use hls_m3u8::tags::ExtXStart; - /// ExtXStart::new(20.123456); + /// let start = ExtXStart::new(20.123456); /// ``` pub fn new(time_offset: f64) -> Self { Self { @@ -157,6 +157,7 @@ impl FromStr for ExtXStart { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..af8c5ca --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,150 @@ +use crate::tags::ExtXKey; +use crate::types::{EncryptionMethod, ProtocolVersion}; + +/// A trait, that is implemented on all tags, that could be encrypted. +/// +/// # Example +/// ``` +/// use hls_m3u8::tags::ExtXKey; +/// use hls_m3u8::types::EncryptionMethod; +/// use hls_m3u8::Encrypted; +/// +/// struct ExampleTag { +/// keys: Vec<ExtXKey>, +/// } +/// +/// // Implementing the trait is very simple: +/// // Simply expose the internal buffer, that contains all the keys. +/// impl Encrypted for ExampleTag { +/// fn keys(&self) -> &Vec<ExtXKey> { &self.keys } +/// +/// fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys } +/// } +/// +/// let mut example_tag = ExampleTag { keys: vec![] }; +/// +/// // adding new keys: +/// example_tag.set_keys(vec![ExtXKey::empty()]); +/// example_tag.push_key(ExtXKey::new( +/// EncryptionMethod::Aes128, +/// "http://www.example.com/data.bin", +/// )); +/// +/// // getting the keys: +/// assert_eq!( +/// example_tag.keys(), +/// &vec![ +/// ExtXKey::empty(), +/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",) +/// ] +/// ); +/// +/// assert_eq!( +/// example_tag.keys_mut(), +/// &mut vec![ +/// ExtXKey::empty(), +/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",) +/// ] +/// ); +/// +/// assert!(example_tag.is_encrypted()); +/// assert!(!example_tag.is_not_encrypted()); +/// ``` +pub trait Encrypted { + /// Returns a shared reference to all keys, that can be used to decrypt this + /// tag. + fn keys(&self) -> &Vec<ExtXKey>; + + /// Returns an exclusive reference to all keys, that can be used to decrypt + /// this tag. + fn keys_mut(&mut self) -> &mut Vec<ExtXKey>; + + /// Sets all keys, that can be used to decrypt this tag. + fn set_keys(&mut self, value: Vec<ExtXKey>) -> &mut Self { + let keys = self.keys_mut(); + *keys = value; + self + } + + /// Add a single key to the list of keys, that can be used to decrypt this + /// tag. + fn push_key(&mut self, value: ExtXKey) -> &mut Self { + self.keys_mut().push(value); + self + } + + /// Returns `true`, if the tag is encrypted. + /// + /// # Note + /// This will return `true`, if any of the keys satisfies + /// ```text + /// key.method() != EncryptionMethod::None + /// ``` + fn is_encrypted(&self) -> bool { + if self.keys().is_empty() { + return false; + } + self.keys() + .iter() + .any(|k| k.method() != EncryptionMethod::None) + } + + /// Returns `false`, if the tag is not encrypted. + /// + /// # Note + /// This is the inverse of [`is_encrypted`]. + /// + /// [`is_encrypted`]: #method.is_encrypted + fn is_not_encrypted(&self) -> bool { !self.is_encrypted() } +} + +/// # Example +/// Implementing it: +/// ``` +/// # use hls_m3u8::RequiredVersion; +/// use hls_m3u8::types::ProtocolVersion; +/// +/// struct ExampleTag(u64); +/// +/// impl RequiredVersion for ExampleTag { +/// fn required_version(&self) -> ProtocolVersion { +/// if self.0 == 5 { +/// ProtocolVersion::V4 +/// } else { +/// ProtocolVersion::V1 +/// } +/// } +/// } +/// assert_eq!(ExampleTag(5).required_version(), ProtocolVersion::V4); +/// assert_eq!(ExampleTag(2).required_version(), ProtocolVersion::V1); +/// ``` +pub trait RequiredVersion { + /// Returns the protocol compatibility version that this tag requires. + /// + /// # Note + /// This is for the latest working [`ProtocolVersion`] and a client, that + /// only supports an older version would break. + fn required_version(&self) -> ProtocolVersion; + + /// The protocol version, in which the tag has been introduced. + fn introduced_version(&self) -> ProtocolVersion { self.required_version() } +} + +impl<T: RequiredVersion> RequiredVersion for Vec<T> { + fn required_version(&self) -> ProtocolVersion { + self.iter() + .map(|v| v.required_version()) + .max() + // return ProtocolVersion::V1, if the iterator is empty: + .unwrap_or_default() + } +} + +impl<T: RequiredVersion> RequiredVersion for Option<T> { + fn required_version(&self) -> ProtocolVersion { + self.iter() + .map(|v| v.required_version()) + .max() + .unwrap_or_default() + } +} diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index a1807d1..795514d 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -114,6 +114,7 @@ impl FromStr for ByteRange { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/channels.rs b/src/types/channels.rs index b620edb..effec06 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -11,14 +11,6 @@ use crate::Error; /// present in any [`MediaSegment`] in the rendition. For example, an /// `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute. /// -/// The second parameter identifies the encoding of object-based audio used by -/// the rendition. This parameter is a comma-separated list of Audio -/// Object Coding Identifiers. It is optional. An Audio Object -/// Coding Identifier is a string containing characters from the set -/// `[A..Z]`, `[0..9]`, and `'-'`. They are codec-specific. A parameter -/// value of consisting solely of the dash character (`'-'`) indicates -/// that the audio is not object-based. -/// /// # Example /// Creating a `CHANNELS="6"` attribute /// ``` @@ -31,16 +23,11 @@ use crate::Error; /// ); /// ``` /// -/// # Note -/// Currently there are no example playlists in the documentation, -/// or in popular m3u8 libraries, showing a usage for the second parameter -/// of [`Channels`], so if you have one please open an issue on github! -/// /// [`MediaSegment`]: crate::MediaSegment #[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Channels { - first_parameter: u64, - second_parameter: Option<Vec<String>>, + channel_number: u64, + unknown: Vec<String>, } impl Channels { @@ -51,77 +38,36 @@ impl Channels { /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); /// ``` - pub const fn new(value: u64) -> Self { + pub fn new(value: u64) -> Self { Self { - first_parameter: value, - second_parameter: None, + channel_number: value, + unknown: vec![], } } - /// Returns the first parameter. + /// Returns the channel number. /// /// # Example /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); /// - /// assert_eq!(channels.first_parameter(), 6); + /// assert_eq!(channels.channel_number(), 6); /// ``` - pub const fn first_parameter(&self) -> u64 { self.first_parameter } + pub const fn channel_number(&self) -> u64 { self.channel_number } - /// Sets the first parameter. + /// Sets the channel number. /// /// # Example /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(3); /// - /// channels.set_first_parameter(6); - /// assert_eq!(channels.first_parameter(), 6) + /// channels.set_channel_number(6); + /// assert_eq!(channels.channel_number(), 6) /// ``` - pub fn set_first_parameter(&mut self, value: u64) -> &mut Self { - self.first_parameter = value; - self - } - - /// Returns the second parameter, if there is any! - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(3); - /// # assert_eq!(channels.second_parameter(), &None); - /// - /// channels.set_second_parameter(Some(vec!["AAC", "MP3"])); - /// assert_eq!( - /// channels.second_parameter(), - /// &Some(vec!["AAC".to_string(), "MP3".to_string()]) - /// ) - /// ``` - /// - /// # Note - /// Currently there is no use for this parameter. - pub const fn second_parameter(&self) -> &Option<Vec<String>> { &self.second_parameter } - - /// Sets the second parameter. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(3); - /// # assert_eq!(channels.second_parameter(), &None); - /// - /// channels.set_second_parameter(Some(vec!["AAC", "MP3"])); - /// assert_eq!( - /// channels.second_parameter(), - /// &Some(vec!["AAC".to_string(), "MP3".to_string()]) - /// ) - /// ``` - /// - /// # Note - /// Currently there is no use for this parameter. - pub fn set_second_parameter<T: ToString>(&mut self, value: Option<Vec<T>>) -> &mut Self { - self.second_parameter = value.map(|v| v.into_iter().map(|s| s.to_string()).collect()); + pub fn set_channel_number(&mut self, value: u64) -> &mut Self { + self.channel_number = value; self } } @@ -131,28 +77,23 @@ impl FromStr for Channels { fn from_str(input: &str) -> Result<Self, Self::Err> { let parameters = input.split('/').collect::<Vec<_>>(); - let first_parameter = parameters + let channel_number = parameters .first() .ok_or_else(|| Error::missing_attribute("First parameter of channels!"))? .parse()?; - let second_parameter = parameters - .get(1) - .map(|v| v.split(',').map(|v| v.to_string()).collect()); - Ok(Self { - first_parameter, - second_parameter, + channel_number, + unknown: parameters[1..].iter().map(|v| v.to_string()).collect(), }) } } impl fmt::Display for Channels { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.first_parameter)?; - - if let Some(second) = &self.second_parameter { - write!(f, "/{}", second.join(","))?; + write!(f, "{}", self.channel_number)?; + if !self.unknown.is_empty() { + write!(f, "{}", self.unknown.join(","))?; } Ok(()) @@ -162,31 +103,20 @@ impl fmt::Display for Channels { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { let mut channels = Channels::new(6); assert_eq!(channels.to_string(), "6".to_string()); - channels.set_first_parameter(7); + channels.set_channel_number(7); assert_eq!(channels.to_string(), "7".to_string()); - - assert_eq!( - "6/P,K,J".to_string(), - Channels::new(6) - .set_second_parameter(Some(vec!["P", "K", "J"])) - .to_string() - ); } #[test] fn test_parser() { assert_eq!("6".parse::<Channels>().unwrap(), Channels::new(6)); - let mut result = Channels::new(6); - result.set_second_parameter(Some(vec!["P", "K", "J"])); - - assert_eq!("6/P,K,J".parse::<Channels>().unwrap(), result); - assert!("garbage".parse::<Channels>().is_err()); assert!("".parse::<Channels>().is_err()); } diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index b3425a4..93bf543 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,8 +1,8 @@ +use core::convert::Infallible; use std::fmt; use std::str::FromStr; use crate::utils::{quote, unquote}; -use crate::Error; /// The identifier of a closed captions group or its absence. /// @@ -26,7 +26,7 @@ impl fmt::Display for ClosedCaptions { } impl FromStr for ClosedCaptions { - type Err = Error; + type Err = Infallible; fn from_str(input: &str) -> Result<Self, Self::Err> { if input.trim() == "NONE" { @@ -40,6 +40,7 @@ impl FromStr for ClosedCaptions { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 8ab8b20..5e8c2de 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -23,7 +23,7 @@ impl DecimalFloatingPoint { /// otherwise this function will return an error that has the kind /// `ErrorKind::InvalidInput`. pub fn new(value: f64) -> crate::Result<Self> { - if value.is_sign_negative() || value.is_infinite() { + if value.is_sign_negative() || value.is_infinite() || value.is_nan() { return Err(Error::invalid_input()); } Ok(Self(value)) @@ -35,8 +35,6 @@ impl DecimalFloatingPoint { pub const fn as_f64(self) -> f64 { self.0 } } -impl Eq for DecimalFloatingPoint {} - // this trait is implemented manually, so it doesn't construct a // [`DecimalFloatingPoint`], with a negative value. impl FromStr for DecimalFloatingPoint { @@ -56,7 +54,7 @@ impl From<f64> for DecimalFloatingPoint { let mut result = value; // guard against the unlikely case of an infinite value... - if result.is_infinite() { + if result.is_infinite() || result.is_nan() { result = 0.0; } @@ -65,12 +63,13 @@ impl From<f64> for DecimalFloatingPoint { } impl From<f32> for DecimalFloatingPoint { - fn from(value: f32) -> Self { (value as f64).into() } + fn from(value: f32) -> Self { f64::from(value).into() } } #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; macro_rules! test_from { ( $($input:expr),* ) => { @@ -88,7 +87,7 @@ mod tests { } } - test_from![1u8, 1u16, 1u32, 1.0f32, -1.0f32, 1.0f64, -1.0f64]; + test_from![1_u8, 1_u16, 1_u32, 1.0_f32, -1.0_f32, 1.0_f64, -1.0_f64]; #[test] pub fn test_display() { diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index 0deea04..2a4b4ae 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -4,14 +4,15 @@ use derive_more::Display; use crate::Error; -/// Decimal resolution. +/// This is a simple wrapper type for the display resolution. (1920x1080, +/// 1280x720, ...). /// /// See: [4.2. Attribute Lists] /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)] #[display(fmt = "{}x{}", width, height)] -pub(crate) struct DecimalResolution { +pub struct DecimalResolution { width: usize, height: usize, } @@ -41,7 +42,7 @@ impl DecimalResolution { /// [`DecimalResolution`] can be constructed from a tuple; `(width, height)`. impl From<(usize, usize)> for DecimalResolution { - fn from(value: (usize, usize)) -> Self { DecimalResolution::new(value.0, value.1) } + fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) } } impl FromStr for DecimalResolution { @@ -67,6 +68,7 @@ impl FromStr for DecimalResolution { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index a6995cd..77d85b6 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -6,12 +6,11 @@ use derive_builder::Builder; use crate::attribute::AttributePairs; use crate::types::{ EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion, - RequiredVersion, }; use crate::utils::{quote, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; -#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[builder(setter(into), build_fn(validate = "Self::validate"))] /// [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`] /// and [`ExtXKey`]. @@ -46,7 +45,7 @@ impl DecryptionKeyBuilder { } impl DecryptionKey { - /// Makes a new [DecryptionKey]. + /// Makes a new [`DecryptionKey`]. /// /// # Example /// ``` @@ -65,7 +64,7 @@ impl DecryptionKey { } } - /// Returns the [EncryptionMethod]. + /// Returns the [`EncryptionMethod`]. /// /// # Example /// ``` @@ -81,7 +80,7 @@ impl DecryptionKey { /// Returns a Builder to build a [DecryptionKey]. pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } - /// Sets the [EncryptionMethod]. + /// Sets the [`EncryptionMethod`]. /// /// # Example /// ``` @@ -121,7 +120,7 @@ impl DecryptionKey { /// Sets the `URI` attribute. /// /// # Note - /// This attribute is required, if the [EncryptionMethod] is not `None`. + /// This attribute is required, if the [`EncryptionMethod`] is not `None`. /// /// # Example /// ``` @@ -208,7 +207,7 @@ impl DecryptionKey { /// ``` pub const fn key_format(&self) -> Option<KeyFormat> { self.key_format } - /// Sets the [KeyFormat] attribute. + /// Sets the [`KeyFormat`] attribute. /// /// # Example /// ``` @@ -222,11 +221,11 @@ impl DecryptionKey { /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); /// ``` pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) -> &mut Self { - self.key_format = value.map(|v| v.into()); + self.key_format = value.map(Into::into); self } - /// Returns the [KeyFormatVersions] attribute. + /// Returns the [`KeyFormatVersions`] attribute. /// /// # Example /// ``` @@ -246,7 +245,7 @@ impl DecryptionKey { &self.key_format_versions } - /// Sets the [KeyFormatVersions] attribute. + /// Sets the [`KeyFormatVersions`] attribute. /// /// # Example /// ``` @@ -267,7 +266,7 @@ impl DecryptionKey { &mut self, value: Option<T>, ) -> &mut Self { - self.key_format_versions = value.map(|v| v.into()); + self.key_format_versions = value.map(Into::into); self } } @@ -354,6 +353,7 @@ impl fmt::Display for DecryptionKey { mod test { use super::*; use crate::types::EncryptionMethod; + use pretty_assertions::assert_eq; #[test] fn test_builder() { diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index fb3673f..a555009 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -50,6 +50,7 @@ pub enum EncryptionMethod { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index a268181..1d03e2a 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -17,6 +17,7 @@ pub enum HdcpLevel { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 1bd7cab..b39ce5c 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -81,6 +81,7 @@ pub enum InStreamId { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; macro_rules! gen_tests { ( $($string:expr => $enum:expr),* ) => { diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index c19bf50..971bcc9 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -66,6 +66,7 @@ impl FromStr for InitializationVector { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 01510db..3ffac49 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] /// [`KeyFormat`] specifies, how the key is represented in the @@ -31,6 +31,7 @@ impl fmt::Display for KeyFormat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote("identity")) } } +/// This tag requires [`ProtocolVersion::V5`]. impl RequiredVersion for KeyFormat { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } @@ -38,6 +39,7 @@ impl RequiredVersion for KeyFormat { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 9a871cb..2b4fc95 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -1,12 +1,13 @@ +use std::convert::Infallible; use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, unquote}; -use crate::Error; +use crate::RequiredVersion; -/// A list of [usize], that can be used to indicate which version(s) +/// 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. /// @@ -14,10 +15,6 @@ use crate::Error; #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct KeyFormatVersions(Vec<usize>); -impl Default for KeyFormatVersions { - fn default() -> Self { Self(vec![1]) } -} - impl KeyFormatVersions { /// Makes a new [`KeyFormatVersions`]. pub fn new() -> Self { Self::default() } @@ -36,6 +33,10 @@ impl KeyFormatVersions { pub fn is_default(&self) -> bool { self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() } } +impl Default for KeyFormatVersions { + fn default() -> Self { Self(vec![1]) } +} + impl Deref for KeyFormatVersions { type Target = Vec<usize>; @@ -46,12 +47,13 @@ impl DerefMut for KeyFormatVersions { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } +/// This tag requires [`ProtocolVersion::V5`]. impl RequiredVersion for KeyFormatVersions { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } impl FromStr for KeyFormatVersions { - type Err = Error; + type Err = Infallible; fn from_str(input: &str) -> Result<Self, Self::Err> { let mut result = unquote(input) @@ -95,6 +97,7 @@ impl<T: Into<Vec<usize>>> From<T> for KeyFormatVersions { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 52a7e57..19a999f 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -14,6 +14,7 @@ pub enum MediaType { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/types/mod.rs b/src/types/mod.rs index 64aa441..a1df2bd 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,6 +15,7 @@ mod media_type; mod protocol_version; mod signed_decimal_floating_point; mod stream_inf; +mod value; pub use byte_range::*; pub use channels::*; @@ -32,3 +33,4 @@ pub use media_type::*; pub use protocol_version::*; pub(crate) use signed_decimal_floating_point::*; pub use stream_inf::*; +pub use value::*; diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index c90c5a5..1ea2a0e 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -3,30 +3,6 @@ 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] /// The [`ProtocolVersion`] specifies, which m3u8 revision is required, to parse /// a certain tag correctly. @@ -97,6 +73,7 @@ impl Default for ProtocolVersion { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 541503b..54a58e5 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -10,20 +10,20 @@ use derive_more::{Display, FromStr}; pub(crate) struct SignedDecimalFloatingPoint(f64); impl SignedDecimalFloatingPoint { - /// Makes a new [SignedDecimalFloatingPoint] instance. + /// Makes a new [`SignedDecimalFloatingPoint`] instance. /// /// # Panics /// The given value must be finite, otherwise this function will panic! pub fn new(value: f64) -> Self { - if value.is_infinite() { - panic!("Floating point value must be finite!"); + if value.is_infinite() || value.is_nan() { + panic!("Floating point value must be finite and not NaN!"); } Self(value) } pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) } - /// Converts [DecimalFloatingPoint] to [f64]. + /// Converts [`DecimalFloatingPoint`] to [`f64`]. pub const fn as_f64(self) -> f64 { self.0 } } @@ -33,11 +33,10 @@ impl Deref for SignedDecimalFloatingPoint { fn deref(&self) -> &Self::Target { &self.0 } } -impl Eq for SignedDecimalFloatingPoint {} - #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; macro_rules! test_from { ( $( $input:expr => $output:expr ),* ) => { @@ -56,21 +55,21 @@ mod tests { } test_from![ - SignedDecimalFloatingPoint::from(1u8) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1i8) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1u16) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1i16) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1u32) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1i32) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1.0f32) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1.0f64) => SignedDecimalFloatingPoint::new(1.0) + SignedDecimalFloatingPoint::from(1_u8) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_i8) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_u16) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_i16) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_u32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_i32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1.0_f32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1.0_f64) => SignedDecimalFloatingPoint::new(1.0) ]; #[test] fn test_display() { assert_eq!( SignedDecimalFloatingPoint::new(1.0).to_string(), - 1.0f64.to_string() + 1.0_f64.to_string() ); } diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index e40d037..3f5b4b1 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -1,26 +1,44 @@ use std::fmt; use std::str::FromStr; +use derive_builder::Builder; + use crate::attribute::AttributePairs; use crate::types::{DecimalResolution, HdcpLevel}; use crate::utils::{quote, unquote}; use crate::Error; -/// [4.3.4.2. EXT-X-STREAM-INF] +/// # [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] +#[builder(setter(into, strip_option))] +#[builder(derive(Debug, PartialEq))] pub struct StreamInf { + /// The maximum bandwidth of the stream. bandwidth: u64, + #[builder(default)] + /// The average bandwidth of the stream. average_bandwidth: Option<u64>, + #[builder(default)] + /// Every media format in any of the renditions specified by the Variant + /// Stream. codecs: Option<String>, + #[builder(default)] + /// The resolution of the stream. resolution: Option<DecimalResolution>, + #[builder(default)] + /// High-bandwidth Digital Content Protection hdcp_level: Option<HdcpLevel>, + #[builder(default)] + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. video: Option<String>, } impl StreamInf { - /// Creates a new [StreamInf]. + /// Creates a new [`StreamInf`]. + /// /// # Examples /// ``` /// # use hls_m3u8::types::StreamInf; @@ -211,7 +229,7 @@ impl StreamInf { /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); /// ``` pub fn set_hdcp_level<T: Into<HdcpLevel>>(&mut self, value: Option<T>) -> &mut Self { - self.hdcp_level = value.map(|v| v.into()); + self.hdcp_level = value.map(Into::into); self } } @@ -282,6 +300,7 @@ impl FromStr for StreamInf { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/value.rs b/src/types/value.rs new file mode 100644 index 0000000..8c0d961 --- /dev/null +++ b/src/types/value.rs @@ -0,0 +1,107 @@ +use std::fmt; +use std::str::FromStr; + +use hex; + +use crate::utils::{quote, unquote}; +use crate::Error; + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// A [`Value`]. +pub enum Value { + /// A [`String`]. + String(String), + /// A sequence of bytes. + Hex(Vec<u8>), + /// A floating point number, that's neither NaN nor infinite! + Float(f64), +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Self::String(value) => write!(f, "{}", quote(value)), + Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)), + Self::Float(value) => write!(f, "{}", value), + } + } +} + +impl FromStr for Value { + type Err = Error; + + fn from_str(input: &str) -> Result<Self, Self::Err> { + if input.starts_with("0x") || input.starts_with("0X") { + Ok(Self::Hex(hex::decode( + input.trim_start_matches("0x").trim_start_matches("0X"), + )?)) + } else { + match input.parse() { + Ok(value) => Ok(Self::Float(value)), + Err(_) => Ok(Self::String(unquote(input))), + } + } + } +} + +impl From<f64> for Value { + fn from(value: f64) -> Self { Self::Float(value) } +} + +impl From<Vec<u8>> for Value { + fn from(value: Vec<u8>) -> Self { Self::Hex(value) } +} + +impl From<String> for Value { + fn from(value: String) -> Self { Self::String(unquote(value)) } +} + +impl From<&str> for Value { + fn from(value: &str) -> Self { Self::String(unquote(value)) } +} + +// impl<T: AsRef<[u8]>> From<T> for Value { +// fn from(value: T) -> Self { Self::Hex(value.as_ref().into()) } +// } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_display() { + assert_eq!(Value::Float(1.1).to_string(), "1.1".to_string()); + assert_eq!( + Value::String("&str".to_string()).to_string(), + "\"&str\"".to_string() + ); + assert_eq!( + Value::Hex(vec![1, 2, 3]).to_string(), + "0x010203".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!(Value::Float(1.1), "1.1".parse().unwrap()); + assert_eq!( + Value::String("&str".to_string()), + "\"&str\"".parse().unwrap() + ); + assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap()); + assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap()); + assert!("0x010203Z".parse::<Value>().is_err()); + } + + #[test] + fn test_from() { + assert_eq!(Value::from(1.0_f64), Value::Float(1.0)); + assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string())); + assert_eq!( + Value::from("&str".to_string()), + Value::String("&str".to_string()) + ); + assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3])); + } +} diff --git a/src/utils.rs b/src/utils.rs index a48a2eb..d21d9e6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,16 @@ use crate::Error; +macro_rules! required_version { + ( $( $tag:expr ),* ) => { + ::core::iter::empty() + $( + .chain(::core::iter::once($tag.required_version())) + )* + .max() + .unwrap_or_default() + } +} + macro_rules! impl_from { ( $($( $type:tt ),* => $target:path ),* ) => { use ::core::convert::From; @@ -72,6 +83,7 @@ where #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parse_yes_or_no() { diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs new file mode 100644 index 0000000..fb2ecfc --- /dev/null +++ b/tests/master_playlist.rs @@ -0,0 +1,371 @@ +use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf}; +use hls_m3u8::types::MediaType; +use hls_m3u8::MasterPlaylist; + +use pretty_assertions::assert_eq; + +#[test] +fn test_master_playlist() { + // https://tools.ietf.org/html/rfc8216#section-8.4 + let master_playlist = "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\ + http://example.com/low.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n\ + http://example.com/mid.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n\ + http://example.com/hi.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\ + http://example.com/audio-only.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .average_bandwidth(1000000) + .uri("http://example.com/low.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .average_bandwidth(2000000) + .uri("http://example.com/mid.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .average_bandwidth(6000000) + .uri("http://example.com/hi.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .uri("http://example.com/audio-only.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} + +#[test] +fn test_master_playlist_with_i_frames() { + // https://tools.ietf.org/html/rfc8216#section-8.5 + let master_playlist = "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=1280000\n\ + low/audio-video.m3u8\n\ + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n\ + #EXT-X-STREAM-INF:BANDWIDTH=2560000\n\ + mid/audio-video.m3u8\n\ + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n\ + #EXT-X-STREAM-INF:BANDWIDTH=7680000\n\ + hi/audio-video.m3u8\n\ + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n\ + #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\ + audio-only.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .uri("low/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .uri("mid/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .uri("hi/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .uri("audio-only.m3u8") + .build() + .unwrap(), + ]) + .i_frame_stream_inf_tags(vec![ + ExtXIFrameStreamInf::builder() + .bandwidth(86000) + .uri("low/iframe.m3u8") + .build() + .unwrap(), + ExtXIFrameStreamInf::builder() + .bandwidth(150000) + .uri("mid/iframe.m3u8") + .build() + .unwrap(), + ExtXIFrameStreamInf::builder() + .bandwidth(550000) + .uri("hi/iframe.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} + +#[test] +fn test_master_playlist_with_alternative_audio() { + // https://tools.ietf.org/html/rfc8216#section-8.6 + // TODO: I think the CODECS=\"..." have to be replaced. + let master_playlist = "#EXTM3U\n\ + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", \ + DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", \ + URI=\"main/english-audio.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", \ + DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", \ + URI=\"main/german-audio.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", \ + DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", \ + URI=\"commentary/audio-only.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n\ + low/video-only.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n\ + mid/video-only.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n\ + hi/video-only.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n\ + main/english-audio.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .media_tags(vec![ + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("aac") + .name("English") + .is_default(true) + .is_autoselect(true) + .language("en") + .uri("main/english-audio.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("aac") + .name("Deutsch") + .is_default(false) + .is_autoselect(true) + .language("de") + .uri("main/german-audio.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("aac") + .name("Commentary") + .is_default(false) + .is_autoselect(false) + .language("en") + .uri("commentary/audio-only.m3u8") + .build() + .unwrap(), + ]) + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .codecs("...") + .audio("aac") + .uri("low/video-only.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .codecs("...") + .audio("aac") + .uri("mid/video-only.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .codecs("...") + .audio("aac") + .uri("hi/video-only.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .audio("aac") + .uri("main/english-audio.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} + +#[test] +fn test_master_playlist_with_alternative_video() { + // https://tools.ietf.org/html/rfc8216#section-8.7 + let master_playlist = "#EXTM3U\n\ + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", \ + AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", \ + DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", \ + DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n\ + low/main/audio-video.m3u8\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", \ + AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", \ + DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", \ + DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n\ + mid/main/audio-video.m3u8\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\", \ + AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", \ + DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", \ + DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\" + hi/main/audio-video.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .media_tags(vec![ + // low + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("low") + .name("Main") + .is_default(true) + .is_autoselect(true) + .uri("low/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("low") + .name("Centerfield") + .is_default(false) + .uri("low/centerfield/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("low") + .name("Dugout") + .is_default(false) + .uri("low/dugout/audio-video.m3u8") + .build() + .unwrap(), + // mid + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("mid") + .name("Main") + .is_default(true) + .is_autoselect(true) + .uri("mid/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("mid") + .name("Centerfield") + .is_default(false) + .uri("mid/centerfield/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("mid") + .name("Dugout") + .is_default(false) + .uri("mid/dugout/audio-video.m3u8") + .build() + .unwrap(), + // hi + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("hi") + .name("Main") + .is_default(true) + .is_autoselect(true) + .uri("hi/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("hi") + .name("Centerfield") + .is_default(false) + .uri("hi/centerfield/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("hi") + .name("Dugout") + .is_default(false) + .uri("hi/dugout/audio-video.m3u8") + .build() + .unwrap(), + ]) + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .codecs("...") + .video("low") + .uri("low/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .codecs("...") + .video("mid") + .uri("mid/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .codecs("...") + .video("hi") + .uri("hi/main/audio-video.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs new file mode 100644 index 0000000..7f8a860 --- /dev/null +++ b/tests/media_playlist.rs @@ -0,0 +1,53 @@ +use std::time::Duration; + +use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration}; +use hls_m3u8::{MediaPlaylist, MediaSegment}; +use pretty_assertions::assert_eq; + +#[test] +fn test_media_playlist_with_byterange() { + let media_playlist = "#EXTM3U\n\ + #EXT-X-TARGETDURATION:10\n\ + #EXT-X-VERSION:4\n\ + #EXT-X-MEDIA-SEQUENCE:0\n\ + #EXTINF:10.0,\n\ + #EXT-X-BYTERANGE:75232@0\n\ + video.ts\n\ + #EXT-X-BYTERANGE:82112@752321\n\ + #EXTINF:10.0,\n\ + video.ts\n\ + #EXTINF:10.0,\n\ + #EXT-X-BYTERANGE:69864\n\ + video.ts" + .parse::<MediaPlaylist>() + .unwrap(); + + assert_eq!( + MediaPlaylist::builder() + .target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10))) + .media_sequence_tag(ExtXMediaSequence::new(0)) + .segments(vec![ + MediaSegment::builder() + .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range_tag(ExtXByteRange::new(75232, Some(0))) + .uri("video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range_tag(ExtXByteRange::new(82112, Some(752321))) + .uri("video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range_tag(ExtXByteRange::new(69864, None)) + .uri("video.ts") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + media_playlist + ) +}