From 448c331447fbf118f46fb2336b03b978ab7e9fe9 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 23 Jan 2020 17:57:57 +0100 Subject: [PATCH 001/112] fix compilation --- src/lib.rs | 3 +-- src/tags/master_playlist/session_data.rs | 21 ++++++--------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 30fe3cc..3d4b2be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #![forbid(unsafe_code)] -#![feature(option_flattening)] #![warn( - //clippy::pedantic, + clippy::pedantic, // clippy::nursery, clippy::cargo )] diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index a264158..b03b0e5 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -132,13 +132,10 @@ impl ExtXSessionData { /// # /// let data = ExtXSessionData::new( /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()) + /// SessionData::Value("some data".to_string()), /// ); /// - /// assert_eq!( - /// data.data_id(), - /// &"com.example.movie.title".to_string() - /// ) + /// assert_eq!(data.data_id(), &"com.example.movie.title".to_string()) /// ``` pub const fn data_id(&self) -> &String { &self.data_id } @@ -150,13 +147,10 @@ impl ExtXSessionData { /// # /// let data = ExtXSessionData::new( /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()) + /// SessionData::Value("some data".to_string()), /// ); /// - /// assert_eq!( - /// data.data(), - /// &SessionData::Value("some data".to_string()) - /// ) + /// assert_eq!(data.data(), &SessionData::Value("some data".to_string())) /// ``` pub const fn data(&self) -> &SessionData { &self.data } @@ -170,13 +164,10 @@ impl ExtXSessionData { /// let data = ExtXSessionData::with_language( /// "com.example.movie.title", /// SessionData::Value("some data".to_string()), - /// "english" + /// "english", /// ); /// - /// assert_eq!( - /// data.language(), - /// &Some("english".to_string()) - /// ) + /// assert_eq!(data.language(), &Some("english".to_string())) /// ``` pub const fn language(&self) -> &Option { &self.language } From ac80ac5c9d98ea37456c9ad3449702a6b804c865 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 23 Jan 2020 19:13:26 +0100 Subject: [PATCH 002/112] switch error implementation #23 closes #23 --- Cargo.toml | 2 +- src/error.rs | 179 ++++++++---------- src/master_playlist.rs | 7 +- src/media_playlist.rs | 4 +- src/tags/basic/m3u.rs | 10 +- src/tags/basic/version.rs | 7 +- .../master_playlist/i_frame_stream_inf.rs | 2 +- src/tags/master_playlist/media.rs | 2 +- src/tags/master_playlist/stream_inf.rs | 4 +- src/tags/media_segment/date_range.rs | 5 +- src/tags/media_segment/program_date_time.rs | 2 +- src/types/channels.rs | 3 +- src/types/decimal_floating_point.rs | 4 +- src/types/decimal_resolution.rs | 4 +- src/types/decryption_key.rs | 4 +- src/types/stream_inf.rs | 10 +- src/types/value.rs | 7 +- 17 files changed, 115 insertions(+), 141 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f790b1d..e7703b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ travis-ci = { repository = "sile/hls_m3u8" } codecov = { repository = "sile/hls_m3u8" } [dependencies] -failure = "0.1.5" +thiserror = "1.0" derive_builder = "0.8.0" chrono = "0.4.9" strum = { version = "0.16.0", features = ["derive"] } diff --git a/src/error.rs b/src/error.rs index c64e031..1333ce5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,39 +1,33 @@ use std::fmt; -use failure::{Backtrace, Context, Fail}; +use thiserror::Error; + +use crate::types::ProtocolVersion; /// This crate specific `Result` type. pub type Result = std::result::Result; /// The [`ErrorKind`]. -#[derive(Debug, Fail, Clone, PartialEq, Eq)] -pub enum ErrorKind { - #[fail(display = "ChronoParseError: {}", _0)] - /// An error from the [Chrono](chrono) crate. - ChronoParseError(String), - - #[fail(display = "UnknownError: {}", _0)] - /// An unknown error occured. - UnknownError(String), - - #[fail(display = "A value is missing for the attribute {}", _0)] +#[derive(Debug, Error, Clone, PartialEq)] +enum ErrorKind { /// A required value is missing. + #[error("A value is missing for the attribute {}", _0)] MissingValue(String), - #[fail(display = "Invalid Input")] /// Error for anything. + #[error("Invalid Input")] InvalidInput, - #[fail(display = "ParseIntError: {}", _0)] + #[error("{}", _0)] /// Failed to parse a String to int. - ParseIntError(String), + ParseIntError(::std::num::ParseIntError), - #[fail(display = "ParseFloatError: {}", _0)] + #[error("{}", _0)] /// Failed to parse a String to float. - ParseFloatError(String), + ParseFloatError(::std::num::ParseFloatError), - #[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)] /// A tag is missing, that is required at the start of the input. + #[error("Expected `{}` at the start of {:?}", tag, input)] MissingTag { /// The required tag. tag: String, @@ -41,100 +35,98 @@ pub enum ErrorKind { input: String, }, - #[fail(display = "CustomError: {}", _0)] + #[error("{}", _0)] /// A custom error. Custom(String), - #[fail(display = "Unmatched Group: {:?}", _0)] /// Unmatched Group + #[error("Unmatched Group: {:?}", _0)] UnmatchedGroup(String), - #[fail(display = "Unknown Protocol version: {:?}", _0)] /// Unknown m3u8 version. This library supports up to ProtocolVersion 7. + #[error("Unknown protocol version {:?}", _0)] UnknownProtocolVersion(String), - #[fail(display = "IoError: {}", _0)] /// Some io error + #[error("{}", _0)] Io(String), - #[fail( - display = "VersionError: required_version: {:?}, specified_version: {:?}", - _0, _1 - )] /// This error occurs, if there is a ProtocolVersion mismatch. - VersionError(String, String), + #[error("required_version: {:?}, specified_version: {:?}", _0, _1)] + VersionError(ProtocolVersion, ProtocolVersion), - #[fail(display = "BuilderError: {}", _0)] - /// An Error from a Builder. - BuilderError(String), - - #[fail(display = "Missing Attribute: {}", _0)] /// An attribute is missing. + #[error("Missing Attribute: {}", _0)] MissingAttribute(String), - #[fail(display = "Unexpected Attribute: {:?}", _0)] /// An unexpected value. + #[error("Unexpected Attribute: {:?}", _0)] UnexpectedAttribute(String), - #[fail(display = "Unexpected Tag: {:?}", _0)] /// An unexpected tag. + #[error("Unexpected Tag: {:?}", _0)] UnexpectedTag(String), + /// An error from the [`chrono`] crate. + #[error("{}", _0)] + ChronoParseError(chrono::ParseError), + + /// An error from a Builder. + #[error("BuilderError: {}", _0)] + Builder(String), + + #[error("{}", _0)] + Hex(hex::FromHexError), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients /// don't count on exhaustive matching. (Otherwise, adding a new variant /// could break existing code.) #[doc(hidden)] - #[fail(display = "Invalid error")] + #[error("Invalid error")] __Nonexhaustive, } -#[derive(Debug)] /// The Error type of this library. +#[derive(Debug)] pub struct Error { - inner: Context, + inner: ErrorKind, } -impl Fail for Error { - fn cause(&self) -> Option<&dyn Fail> { self.inner.cause() } - - fn backtrace(&self) -> Option<&Backtrace> { self.inner.backtrace() } -} +impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt(f) } } -impl From for Error { - fn from(kind: ErrorKind) -> Self { Self::from(Context::new(kind)) } -} - -impl From> for Error { - fn from(inner: Context) -> Self { Self { inner } } -} - impl Error { + fn new(inner: ErrorKind) -> Self { Self { inner } } + + pub(crate) fn custom(value: T) -> Self { + Self::new(ErrorKind::Custom(value.to_string())) + } + pub(crate) fn missing_value(value: T) -> Self { - Self::from(ErrorKind::MissingValue(value.to_string())) + Self::new(ErrorKind::MissingValue(value.to_string())) } pub(crate) fn unexpected_attribute(value: T) -> Self { - Self::from(ErrorKind::UnexpectedAttribute(value.to_string())) + Self::new(ErrorKind::UnexpectedAttribute(value.to_string())) } pub(crate) fn unexpected_tag(value: T) -> Self { - Self::from(ErrorKind::UnexpectedTag(value.to_string())) + Self::new(ErrorKind::UnexpectedTag(value.to_string())) } - pub(crate) fn invalid_input() -> Self { Self::from(ErrorKind::InvalidInput) } + pub(crate) fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) } - pub(crate) fn parse_int_error(value: T) -> Self { - Self::from(ErrorKind::ParseIntError(value.to_string())) + pub(crate) fn parse_int(value: ::std::num::ParseIntError) -> Self { + Self::new(ErrorKind::ParseIntError(value)) } - pub(crate) fn parse_float_error(value: T) -> Self { - Self::from(ErrorKind::ParseFloatError(value.to_string())) + pub(crate) fn parse_float(value: ::std::num::ParseFloatError) -> Self { + Self::new(ErrorKind::ParseFloatError(value)) } pub(crate) fn missing_tag(tag: T, input: U) -> Self @@ -142,76 +134,53 @@ impl Error { T: ToString, U: ToString, { - Self::from(ErrorKind::MissingTag { + Self::new(ErrorKind::MissingTag { tag: tag.to_string(), input: input.to_string(), }) } pub(crate) fn unmatched_group(value: T) -> Self { - Self::from(ErrorKind::UnmatchedGroup(value.to_string())) - } - - pub(crate) fn custom(value: T) -> Self - where - T: fmt::Display, - { - Self::from(ErrorKind::Custom(value.to_string())) + Self::new(ErrorKind::UnmatchedGroup(value.to_string())) } pub(crate) fn unknown_protocol_version(value: T) -> Self { - Self::from(ErrorKind::UnknownProtocolVersion(value.to_string())) + Self::new(ErrorKind::UnknownProtocolVersion(value.to_string())) } - pub(crate) fn io(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) } + pub(crate) fn io(value: T) -> Self { Self::new(ErrorKind::Io(value.to_string())) } - pub(crate) fn builder_error(value: T) -> Self { - Self::from(ErrorKind::BuilderError(value.to_string())) - } - - pub(crate) fn chrono(value: T) -> Self { - Self::from(ErrorKind::ChronoParseError(value.to_string())) + pub(crate) fn builder(value: T) -> Self { + Self::new(ErrorKind::Builder(value.to_string())) } pub(crate) fn missing_attribute(value: T) -> Self { - Self::from(ErrorKind::MissingAttribute(value.to_string())) + Self::new(ErrorKind::MissingAttribute(value.to_string())) + } + + // third party crates: + pub(crate) fn chrono(value: chrono::format::ParseError) -> Self { + Self::new(ErrorKind::ChronoParseError(value)) + } + + pub(crate) fn hex(value: hex::FromHexError) -> Self { Self::new(ErrorKind::Hex(value)) } + + pub(crate) fn strum(value: strum::ParseError) -> Self { + Self::new(ErrorKind::Custom(value.to_string())) } } +#[doc(hidden)] impl From<::std::num::ParseIntError> for Error { - fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int_error(value) } + fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int(value) } } +#[doc(hidden)] impl From<::std::num::ParseFloatError> for Error { - 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 { Self::io(value) } -} - -impl From<::chrono::ParseError> for Error { - fn from(value: ::chrono::ParseError) -> Self { Self::chrono(value) } + fn from(value: ::std::num::ParseFloatError) -> Self { Self::parse_float(value) } } +#[doc(hidden)] impl From<::strum::ParseError> for Error { - fn from(value: ::strum::ParseError) -> Self { - 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! - } + fn from(value: ::strum::ParseError) -> Self { Self::strum(value) } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 04b2d99..513658d 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -65,16 +65,15 @@ impl 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(()) - /// # } + /// # Ok::<(), Box>(()) /// ``` pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } @@ -438,7 +437,7 @@ impl FromStr for MasterPlaylist { builder.session_data_tags(session_data_tags); builder.session_key_tags(session_key_tags); - builder.build().map_err(Error::builder_error) + builder.build().map_err(Error::builder) } } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index a334650..4fb97f4 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -353,7 +353,7 @@ 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)?); + segments.push(segment.build().map_err(Error::builder)?); segment = MediaSegment::builder(); has_partial_segment = false; } @@ -365,7 +365,7 @@ fn parse_media_playlist( } builder.segments(segments); - builder.build().map_err(Error::builder_error) + builder.build().map_err(Error::builder) } impl FromStr for MediaPlaylist { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index b1f8592..f930ddc 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -12,18 +12,18 @@ use crate::{Error, RequiredVersion}; /// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. /// /// # Examples +/// /// Parsing from a [`str`]: +/// /// ``` -/// # use failure::Error; /// # use hls_m3u8::tags::ExtM3u; /// # -/// # fn main() -> Result<(), Error> { /// assert_eq!("#EXTM3U".parse::()?, ExtM3u); -/// # -/// # Ok(()) -/// # } +/// # Ok::<(), Box>(()) /// ``` +/// /// Converting to a [`str`]: +/// /// ``` /// # use hls_m3u8::tags::ExtM3u; /// # diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index e39f079..be37b45 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -12,21 +12,18 @@ use crate::{Error, RequiredVersion}; /// It applies to the entire Playlist. /// /// # Examples +/// /// Parsing from a [`str`]: /// ``` -/// # use failure::Error; /// # use hls_m3u8::tags::ExtXVersion; /// # -/// # fn main() -> Result<(), Error> { /// use hls_m3u8::types::ProtocolVersion; /// /// assert_eq!( /// "#EXT-X-VERSION:5".parse::()?, /// ExtXVersion::new(ProtocolVersion::V5) /// ); -/// # -/// # Ok(()) -/// # } +/// # Ok::<(), Box>(()) /// ``` /// Converting to a [`str`]: /// ``` diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 2e9c9f5..1e2ddec 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -85,7 +85,7 @@ impl ExtXIFrameStreamInfBuilder { .uri .clone() .ok_or_else(|| Error::missing_value("frame rate"))?, - stream_inf: self.stream_inf.build().map_err(Error::builder_error)?, + stream_inf: self.stream_inf.build().map_err(Error::builder)?, }) } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index f43e73f..f8f65ac 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -735,7 +735,7 @@ impl FromStr for ExtXMedia { } } - builder.build().map_err(Error::builder_error) + builder.build().map_err(Error::builder) } } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index d8cc87d..bf6aced 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -124,7 +124,7 @@ impl ExtXStreamInfBuilder { 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)?, + stream_inf: self.stream_inf.build().map_err(Error::builder)?, }) } } @@ -347,7 +347,7 @@ impl FromStr for ExtXStreamInf { "FRAME-RATE" => frame_rate = Some((value.parse())?), "AUDIO" => audio = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)), - "CLOSED-CAPTIONS" => closed_captions = Some(value.parse()?), + "CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()), _ => {} } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 6586f8a..87bca24 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -719,7 +719,7 @@ impl FromStr for ExtXDateRange { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), - "END-DATE" => end_date = Some(unquote(value).parse()?), + "END-DATE" => end_date = Some(unquote(value).parse().map_err(Error::chrono)?), "DURATION" => { duration = Some(Duration::from_secs_f64(value.parse()?)); } @@ -750,7 +750,8 @@ impl FromStr for ExtXDateRange { let id = id.ok_or_else(|| Error::missing_value("ID"))?; let start_date = start_date .ok_or_else(|| Error::missing_value("START-DATE"))? - .parse()?; + .parse() + .map_err(Error::chrono)?; if end_on_next && class.is_none() { return Err(Error::invalid_input()); diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 5ea34ca..23ccc0b 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -114,7 +114,7 @@ impl FromStr for ExtXProgramDateTime { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - let date_time = DateTime::parse_from_rfc3339(input)?; + let date_time = DateTime::parse_from_rfc3339(input).map_err(Error::chrono)?; Ok(Self::new(date_time)) } } diff --git a/src/types/channels.rs b/src/types/channels.rs index effec06..d56fd01 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -80,7 +80,8 @@ impl FromStr for Channels { let channel_number = parameters .first() .ok_or_else(|| Error::missing_attribute("First parameter of channels!"))? - .parse()?; + .parse() + .map_err(Error::parse_int)?; Ok(Self { channel_number, diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 5e8c2de..af0ac36 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -40,7 +40,9 @@ impl DecimalFloatingPoint { impl FromStr for DecimalFloatingPoint { type Err = Error; - fn from_str(input: &str) -> Result { Self::new(input.parse()?) } + fn from_str(input: &str) -> Result { + Self::new(input.parse().map_err(Error::parse_float)?) + } } impl Deref for DecimalFloatingPoint { diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index 2a4b4ae..50e44c1 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -59,8 +59,8 @@ impl FromStr for DecimalResolution { } Ok(Self { - width: tokens[0].parse()?, - height: tokens[1].parse()?, + width: tokens[0].parse().map_err(Error::parse_int)?, + height: tokens[1].parse().map_err(Error::parse_int)?, }) } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 77d85b6..9a57fa4 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -295,11 +295,11 @@ impl FromStr for DecryptionKey { for (key, value) in input.parse::()? { match key.as_str() { - "METHOD" => method = Some(value.parse()?), + "METHOD" => method = Some(value.parse().map_err(Error::strum)?), "URI" => uri = Some(unquote(value)), "IV" => iv = Some(value.parse()?), "KEYFORMAT" => key_format = Some(value.parse()?), - "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse()?), + "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index 3f5b4b1..4886249 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -270,11 +270,15 @@ impl FromStr for StreamInf { for (key, value) in input.parse::()? { match key.as_str() { - "BANDWIDTH" => bandwidth = Some(value.parse::()?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(value.parse::()?), + "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), + "AVERAGE-BANDWIDTH" => { + average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) + } "CODECS" => codecs = Some(unquote(value)), "RESOLUTION" => resolution = Some(value.parse()?), - "HDCP-LEVEL" => hdcp_level = Some(value.parse()?), + "HDCP-LEVEL" => { + hdcp_level = Some(value.parse::().map_err(Error::strum)?) + } "VIDEO" => video = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] diff --git a/src/types/value.rs b/src/types/value.rs index 8c0d961..98eebe0 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -32,9 +32,10 @@ impl FromStr for Value { fn from_str(input: &str) -> Result { if input.starts_with("0x") || input.starts_with("0X") { - Ok(Self::Hex(hex::decode( - input.trim_start_matches("0x").trim_start_matches("0X"), - )?)) + Ok(Self::Hex( + hex::decode(input.trim_start_matches("0x").trim_start_matches("0X")) + .map_err(Error::hex)?, + )) } else { match input.parse() { Ok(value) => Ok(Self::Float(value)), From e156f6e3fd940c9268088c2912b186573c779e51 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 25 Jan 2020 12:26:20 +0100 Subject: [PATCH 003/112] improve `ExtXMedia` --- src/tags/master_playlist/media.rs | 880 +++++++++++------------------- 1 file changed, 314 insertions(+), 566 deletions(-) diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index f8f65ac..6bfb3c0 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -13,8 +13,7 @@ use crate::{Error, RequiredVersion}; /// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s, /// that contain alternative Renditions of the same content. /// -/// For -/// example, three [`ExtXMedia`] tags can be used to identify audio-only +/// For example, three [`ExtXMedia`] tags can be used to identify audio-only /// [`Media Playlist`]s, that contain English, French, and Spanish Renditions /// of the same presentation. Or, two [`ExtXMedia`] tags can be used to /// identify video-only [`Media Playlist`]s that show two different camera @@ -30,45 +29,51 @@ pub struct ExtXMedia { /// Sets the [`MediaType`] of the rendition. /// /// # Note + /// /// This attribute is **required**. media_type: MediaType, - #[builder(setter(strip_option), default)] /// Sets the `URI` that identifies the [`Media Playlist`]. /// /// # Note + /// /// - This attribute is **required**, if the [`MediaType`] is /// [`MediaType::Subtitles`]. /// - This attribute is **not allowed**, if the [`MediaType`] is /// [`MediaType::ClosedCaptions`]. /// /// [`Media Playlist`]: crate::MediaPlaylist + #[builder(setter(strip_option), default)] uri: Option, /// Sets the identifier, that specifies the group to which the rendition /// belongs. /// /// # Note + /// /// This attribute is **required**. group_id: String, - #[builder(setter(strip_option), default)] /// Sets the name of the primary language used in the rendition. /// The value has to conform to [`RFC5646`]. /// /// # Note + /// /// This attribute is **optional**. /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 - language: Option, #[builder(setter(strip_option), default)] + language: Option, /// Sets the name of a language associated with the rendition. /// /// # Note + /// /// This attribute is **optional**. /// /// [`language`]: #method.language + #[builder(setter(strip_option), default)] assoc_language: Option, /// Sets a human-readable description of the rendition. /// /// # Note + /// /// This attribute is **required**. /// /// If the [`language`] attribute is present, this attribute should be in @@ -76,32 +81,34 @@ pub struct ExtXMedia { /// /// [`language`]: #method.language name: String, - #[builder(default)] /// Sets the value of the `default` flag. /// /// # Note + /// /// This attribute is **optional**, its absence indicates an implicit value /// of `false`. - is_default: bool, #[builder(default)] + is_default: bool, /// Sets the value of the `autoselect` flag. /// /// # Note + /// /// This attribute is **optional**, its absence indicates an implicit value /// of `false`. - is_autoselect: bool, #[builder(default)] + is_autoselect: bool, /// Sets the value of the `forced` flag. + #[builder(default)] is_forced: bool, - #[builder(setter(strip_option), default)] /// Sets the identifier that specifies a rendition within the segments in /// the media playlist. + #[builder(setter(strip_option), default)] instream_id: Option, - #[builder(setter(strip_option), default)] /// Sets the string that represents uniform type identifiers (UTI). - characteristics: Option, #[builder(setter(strip_option), default)] + characteristics: Option, /// Sets the parameters of the rendition. + #[builder(setter(strip_option), default)] channels: Option, } @@ -170,6 +177,7 @@ impl ExtXMedia { /// Returns the type of the media, associated with this tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -184,6 +192,7 @@ impl ExtXMedia { /// Sets the type of the media, associated with this tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -203,6 +212,7 @@ impl ExtXMedia { /// belongs. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -218,6 +228,7 @@ impl ExtXMedia { /// belongs. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -236,6 +247,7 @@ impl ExtXMedia { /// Returns a human-readable description of the rendition. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -250,10 +262,12 @@ impl ExtXMedia { /// Sets a human-readable description of the rendition. /// /// # Note + /// /// If the [`language`] attribute is present, this attribute should be in /// that language. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -274,6 +288,7 @@ impl ExtXMedia { /// Returns the `URI`, that identifies the [`Media Playlist`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -292,11 +307,13 @@ impl ExtXMedia { /// Sets the `URI`, that identifies the [`Media Playlist`]. /// /// # Note + /// /// This attribute is **required**, if the [`MediaType`] is /// [`MediaType::Subtitles`]. This attribute is **not allowed**, if the /// [`MediaType`] is [`MediaType::ClosedCaptions`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -318,6 +335,7 @@ impl ExtXMedia { /// Returns the name of the primary language used in the rendition. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -335,6 +353,7 @@ impl ExtXMedia { /// The value has to conform to [`RFC5646`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -356,6 +375,7 @@ impl ExtXMedia { /// Returns the name of a language associated with the rendition. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -375,6 +395,7 @@ impl ExtXMedia { /// spoken, or a fallback dialect). /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -396,6 +417,7 @@ impl ExtXMedia { /// Returns whether this is the `default` rendition. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -415,6 +437,7 @@ impl ExtXMedia { /// from the user indicating a different choice. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -435,6 +458,7 @@ impl ExtXMedia { /// play this rendition in the absence of explicit user preference. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -451,6 +475,7 @@ impl ExtXMedia { /// Sets the `autoselect` flag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -471,6 +496,7 @@ impl ExtXMedia { /// essential to play. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -487,6 +513,7 @@ impl ExtXMedia { /// Sets the `forced` flag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -507,6 +534,7 @@ impl ExtXMedia { /// the [`Media Playlist`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::{InStreamId, MediaType}; @@ -526,6 +554,7 @@ impl ExtXMedia { /// segments in the [`Media Playlist`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::{InStreamId, MediaType}; @@ -547,6 +576,7 @@ impl ExtXMedia { /// Each UTI indicates an individual characteristic of the rendition. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -576,6 +606,7 @@ impl ExtXMedia { /// The characteristics attribute may include private UTIs. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::MediaType; @@ -598,6 +629,7 @@ impl ExtXMedia { /// Returns the channels. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::{Channels, MediaType}; @@ -614,6 +646,7 @@ impl ExtXMedia { /// Sets the channels. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMedia; /// use hls_m3u8::types::{Channels, MediaType}; @@ -648,32 +681,43 @@ impl fmt::Display for ExtXMedia { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TYPE={}", self.media_type)?; + if let Some(value) = &self.uri { write!(f, ",URI={}", quote(value))?; } + write!(f, ",GROUP-ID={}", quote(&self.group_id))?; + if let Some(value) = &self.language { write!(f, ",LANGUAGE={}", quote(value))?; } + if let Some(value) = &self.assoc_language { write!(f, ",ASSOC-LANGUAGE={}", quote(value))?; } + write!(f, ",NAME={}", quote(&self.name))?; + if self.is_default { write!(f, ",DEFAULT=YES")?; } + if self.is_autoselect { write!(f, ",AUTOSELECT=YES")?; } + if self.is_forced { write!(f, ",FORCED=YES")?; } + if let Some(value) = &self.instream_id { write!(f, ",INSTREAM-ID={}", quote(value))?; } + if let Some(value) = &self.characteristics { write!(f, ",CHARACTERISTICS={}", quote(value))?; } + if let Some(value) = &self.channels { write!(f, ",CHANNELS={}", quote(value))?; } @@ -745,565 +789,269 @@ mod test { use pretty_assertions::assert_eq; #[test] - fn test_display() { + fn test_display_and_parse() { // TODO: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("eng/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"eng/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" - .to_string() - ); - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("fre/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"fre/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" - .to_string() - ); + macro_rules! generate_tests { + ( $( { $media:expr, $string:tt } ),* $(,)* ) => { + $( + assert_eq!( + $media.to_string(), + $string.to_string() + ); - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("sp") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("sp/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"sp/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"sp\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" - .to_string() - ); - // ---- - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("englo/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"englo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" - .to_string() - ); + assert_eq!( + $media, + $string.parse::().unwrap(), + ); + )* + } + } - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("frelo/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"frelo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" - .to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("es") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("splo/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"splo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"es\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" - .to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("eng/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"eng/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" - .to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("fre/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"fre/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" - .to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("es") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("sp/prog_index.m3u8") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"sp/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"es\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" - .to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-aacl-312") - .language("en") - .name("English") - .is_autoselect(true) - .is_default(true) - .channels(Channels::new(2)) - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - GROUP-ID=\"audio-aacl-312\",\ - LANGUAGE=\"en\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES,\ - CHANNELS=\"2\"" - .to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Subtitles) - .uri("french/ed.ttml") - .group_id("subs") - .language("fra") - .assoc_language("fra") - .name("French") - .is_autoselect(true) - .is_forced(true) - .characteristics("public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound") - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=SUBTITLES,\ - URI=\"french/ed.ttml\",\ - GROUP-ID=\"subs\",\ - LANGUAGE=\"fra\",\ - ASSOC-LANGUAGE=\"fra\",\ - NAME=\"French\",\ - AUTOSELECT=YES,\ - FORCED=YES,\ - CHARACTERISTICS=\"public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound\"".to_string() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::ClosedCaptions) - .group_id("cc") - .language("sp") - .name("CC2") - .instream_id(InStreamId::Cc2) - .is_autoselect(true) - .build() - .unwrap() - .to_string(), - "#EXT-X-MEDIA:\ - TYPE=CLOSED-CAPTIONS,\ - GROUP-ID=\"cc\",\ - LANGUAGE=\"sp\",\ - NAME=\"CC2\",\ - AUTOSELECT=YES,\ - INSTREAM-ID=\"CC2\"" - .to_string() - ); - - // ---- - assert_eq!( - ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), - "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() - ) - } - - #[test] - fn test_parser() { - // TODO: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("eng/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"eng/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("fre/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"fre/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("sp") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("sp/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"sp/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"sp\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - // ---- - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("englo/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"englo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("frelo/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"frelo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("es") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("splo/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"splo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"es\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("eng/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"eng/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("fre/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"fre/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("es") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("sp/prog_index.m3u8") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"sp/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"es\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-aacl-312") - .language("en") - .name("English") - .is_autoselect(true) - .is_default(true) - .channels(Channels::new(2)) - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - GROUP-ID=\"audio-aacl-312\",\ - LANGUAGE=\"en\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES,\ - CHANNELS=\"2\"" - .parse() - .unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::Subtitles) - .uri("french/ed.ttml") - .group_id("subs") - .language("fra") - .assoc_language("fra") - .name("French") - .is_autoselect(true) - .characteristics("public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound") - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - URI=\"french/ed.ttml\",\ - TYPE=SUBTITLES,\ - GROUP-ID=\"subs\",\ - LANGUAGE=\"fra\",\ - ASSOC-LANGUAGE=\"fra\",\ - NAME=\"French\",\ - AUTOSELECT=YES,\ - FORCED=NO,\ - CHARACTERISTICS=\"public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound\"".parse().unwrap() - ); - - assert_eq!( - ExtXMedia::builder() - .media_type(MediaType::ClosedCaptions) - .group_id("cc") - .language("sp") - .name("CC2") - .instream_id(InStreamId::Cc2) - .is_autoselect(true) - .build() - .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=CLOSED-CAPTIONS,\ - GROUP-ID=\"cc\",\ - LANGUAGE=\"sp\",\ - NAME=\"CC2\",\ - AUTOSELECT=YES,\ - INSTREAM-ID=\"CC2\",\ - UNKNOWN=TAG" - .parse() - .unwrap() - ); - // ---- - assert_eq!( - ExtXMedia::new(MediaType::Audio, "foo", "bar"), - "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" - .parse() - .unwrap() - ); + generate_tests! { + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("sp") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"sp/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"sp\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("englo/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"englo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("frelo/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"frelo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("splo/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"splo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"es\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"sp/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"es\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-aacl-312") + .language("en") + .name("English") + .is_autoselect(true) + .is_default(true) + .channels(Channels::new(2)) + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + GROUP-ID=\"audio-aacl-312\",\ + LANGUAGE=\"en\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES,\ + CHANNELS=\"2\"" + }, + { + ExtXMedia::builder() + .media_type(MediaType::Subtitles) + .uri("french/ed.ttml") + .group_id("subs") + .language("fra") + .assoc_language("fra") + .name("French") + .is_autoselect(true) + .is_forced(true) + .characteristics("public.accessibility.transcribes-spoken\ + -dialog,public.accessibility.describes-music-and-sound") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=SUBTITLES,\ + URI=\"french/ed.ttml\",\ + GROUP-ID=\"subs\",\ + LANGUAGE=\"fra\",\ + ASSOC-LANGUAGE=\"fra\",\ + NAME=\"French\",\ + AUTOSELECT=YES,\ + FORCED=YES,\ + CHARACTERISTICS=\"public.accessibility.\ + transcribes-spoken-dialog,public.accessibility.describes-music-and-sound\"" + }, + { + ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("cc") + .language("sp") + .name("CC2") + .instream_id(InStreamId::Cc2) + .is_autoselect(true) + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=CLOSED-CAPTIONS,\ + GROUP-ID=\"cc\",\ + LANGUAGE=\"sp\",\ + NAME=\"CC2\",\ + AUTOSELECT=YES,\ + INSTREAM-ID=\"CC2\"" + }, + { + ExtXMedia::new(MediaType::Audio, "foo", "bar"), + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" + }, + }; } #[test] From a777f74cfa1c942dce9ea06b24550e59aade03e9 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 26 Jan 2020 13:11:57 +0100 Subject: [PATCH 004/112] refactor attribute parsing to comply with #26 --- src/attribute.rs | 226 +++++++++--------- .../master_playlist/i_frame_stream_inf.rs | 4 +- src/tags/master_playlist/media.rs | 8 +- src/tags/master_playlist/session_data.rs | 4 +- src/tags/master_playlist/stream_inf.rs | 4 +- src/tags/media_segment/date_range.rs | 4 +- src/tags/media_segment/map.rs | 4 +- src/tags/shared/start.rs | 4 +- src/types/decryption_key.rs | 14 +- src/types/stream_inf.rs | 4 +- 10 files changed, 140 insertions(+), 136 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 95f99a6..228ca82 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,148 +1,144 @@ -use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::str::FromStr; - -use crate::Error; +use core::iter::FusedIterator; #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct AttributePairs(HashMap); - -impl AttributePairs { - pub fn new() -> Self { Self::default() } +pub(crate) struct AttributePairs<'a> { + string: &'a str, + index: usize, } -impl Deref for AttributePairs { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } +impl<'a> AttributePairs<'a> { + pub const fn new(string: &'a str) -> Self { Self { string, index: 0 } } } -impl DerefMut for AttributePairs { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} +impl<'a> Iterator for AttributePairs<'a> { + type Item = (&'a str, &'a str); -impl IntoIterator for AttributePairs { - type IntoIter = ::std::collections::hash_map::IntoIter; - type Item = (String, String); + fn next(&mut self) -> Option { + // return `None`, if there are no more chars + self.string.as_bytes().get(self.index + 1)?; - fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } -} + let key = { + // the position in the string: + let start = self.index; + // the key ends at an `=`: + let end = self + .string + .bytes() + .skip(self.index) + .position(|i| i == b'=')? + + start; -impl<'a> IntoIterator for &'a AttributePairs { - type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>; - type Item = (&'a String, &'a String); + // advance the index to the 2nd char after the end of the key + // (this will skip the `=`) + self.index = end + 1; - fn into_iter(self) -> Self::IntoIter { self.0.iter() } -} + core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + }; -impl FromStr for AttributePairs { - type Err = Error; + let value = { + let start = self.index; + let mut end = 0; - fn from_str(input: &str) -> Result { - let mut result = Self::new(); + // find the end of the value by searching for `,`. + // it should ignore `,` that are inside double quotes. + let mut inside_quotes = false; + while let Some(item) = self.string.as_bytes().get(start + end) { + end += 1; - for line in split(input, ',') { - let pair = split(line.trim(), '='); - - if pair.len() < 2 { - continue; - } - - let key = pair[0].trim().to_uppercase(); - let value = pair[1].trim().to_string(); - if value.is_empty() { - continue; - } - - result.insert(key.trim().to_string(), value.trim().to_string()); - } - - #[cfg(test)] // this is very useful, when a test fails! - dbg!(&result); - Ok(result) - } -} - -fn split(value: &str, terminator: char) -> Vec { - let mut result = vec![]; - - let mut inside_quotes = false; - let mut temp_string = String::with_capacity(1024); - - for c in value.chars() { - match c { - '"' => { - inside_quotes = !inside_quotes; - temp_string.push(c); - } - k if (k == terminator) => { - if inside_quotes { - temp_string.push(c); - } else { - result.push(temp_string); - temp_string = String::with_capacity(1024); + if *item == b'"' { + inside_quotes = !inside_quotes; + } else if *item == b',' && !inside_quotes { + self.index += 1; + end -= 1; + break; } } - _ => { - temp_string.push(c); + + self.index += end; + end += start; + + core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + }; + + Some((key.trim(), value.trim())) + } + + fn size_hint(&self) -> (usize, Option) { + let mut remaining = 0; + + // each `=` in the remaining str is an iteration + // this also ignores `=` inside quotes! + let mut inside_quotes = false; + for c in self.string.as_bytes().iter().skip(self.index) { + if *c == b'=' && !inside_quotes { + remaining += 1; + } else if *c == b'"' { + inside_quotes = !inside_quotes; } } - } - result.push(temp_string); - result + (remaining, Some(remaining)) + } } +impl<'a> ExactSizeIterator for AttributePairs<'a> {} +impl<'a> FusedIterator for AttributePairs<'a> {} + #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] - fn test_parser() { - let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3" - .parse::() - .unwrap(); + fn test_attributes() { + let mut attributes = AttributePairs::new("KEY=VALUE,PAIR=YES"); + assert_eq!((2, Some(2)), attributes.size_hint()); + assert_eq!(Some(("KEY", "VALUE")), attributes.next()); + assert_eq!((1, Some(1)), attributes.size_hint()); + assert_eq!(Some(("PAIR", "YES")), attributes.next()); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut iterator = pairs.iter(); - assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v)); + let mut attributes = AttributePairs::new("garbage"); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut iterator = pairs.iter(); - assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\"")); + let mut attributes = AttributePairs::new("KEY=,=VALUE,=,"); + assert_eq!((3, Some(3)), attributes.size_hint()); + assert_eq!(Some(("KEY", "")), attributes.next()); + assert_eq!((2, Some(2)), attributes.size_hint()); + assert_eq!(Some(("", "VALUE")), attributes.next()); + assert_eq!((1, Some(1)), attributes.size_hint()); + assert_eq!(Some(("", "")), attributes.next()); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut iterator = pairs.iter(); - assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); + // test quotes: + let mut attributes = AttributePairs::new("KEY=\"VALUE,\","); + assert_eq!((1, Some(1)), attributes.size_hint()); + assert_eq!(Some(("KEY", "\"VALUE,\"")), attributes.next()); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut pairs = AttributePairs::new(); - pairs.insert("FOO".to_string(), "BAR".to_string()); - - assert_eq!("FOO=BAR,VAL".parse::().unwrap(), pairs); - } - - #[test] - fn test_iterator() { - let mut attrs = AttributePairs::new(); - attrs.insert("key_01".to_string(), "value_01".to_string()); - attrs.insert("key_02".to_string(), "value_02".to_string()); - - let mut iterator = attrs.iter(); - assert!(iterator.any(|(k, v)| k == "key_01" && v == "value_01")); - - let mut iterator = attrs.iter(); - assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02")); - } - - #[test] - fn test_into_iter() { - let mut map = HashMap::new(); - map.insert("k".to_string(), "v".to_string()); - - let mut attrs = AttributePairs::new(); - attrs.insert("k".to_string(), "v".to_string()); - - assert_eq!( - attrs.into_iter().collect::>(), - map.into_iter().collect::>() + // test with chars, that are larger, than 1 byte + let mut attributes = AttributePairs::new( + "LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES", ); + + assert_eq!(Some(("LANGUAGE", "\"fre\"")), attributes.next()); + assert_eq!(Some(("NAME", "\"Français\"")), attributes.next()); + assert_eq!(Some(("AUTOSELECT", "YES")), attributes.next()); + } + + #[test] + fn test_parser() { + let mut pairs = AttributePairs::new("FOO=BAR,BAR=\"baz,qux\",ABC=12.3"); + + assert_eq!(pairs.next(), Some(("FOO", "BAR"))); + assert_eq!(pairs.next(), Some(("BAR", "\"baz,qux\""))); + assert_eq!(pairs.next(), Some(("ABC", "12.3"))); + assert_eq!(pairs.next(), None); } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 1e2ddec..7f748ac 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -162,8 +162,8 @@ impl FromStr for ExtXIFrameStreamInf { let mut uri = None; - for (key, value) in input.parse::()? { - if let "URI" = key.as_str() { + for (key, value) in AttributePairs::new(input) { + if key == "URI" { uri = Some(unquote(value)); } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 6bfb3c0..027ddfb 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -733,8 +733,8 @@ impl FromStr for ExtXMedia { let mut builder = Self::builder(); - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "TYPE" => { builder.media_type(value.parse::()?); } @@ -832,12 +832,12 @@ mod test { { ExtXMedia::builder() .media_type(MediaType::Audio) + .uri("fre/prog_index.m3u8") .group_id("audio") .language("fre") .name("Français") - .is_autoselect(true) .is_default(false) - .uri("fre/prog_index.m3u8") + .is_autoselect(true) .build() .unwrap(), "#EXT-X-MEDIA:\ diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index b03b0e5..d73e9ab 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -275,8 +275,8 @@ impl FromStr for ExtXSessionData { let mut uri = None; let mut language = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "DATA-ID" => data_id = Some(unquote(value)), "VALUE" => session_value = Some(unquote(value)), "URI" => uri = Some(unquote(value)), diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index bf6aced..7fcb885 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -342,8 +342,8 @@ impl FromStr for ExtXStreamInf { let mut subtitles = None; let mut closed_captions = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "FRAME-RATE" => frame_rate = Some((value.parse())?), "AUDIO" => audio = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)), diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 87bca24..4374ef7 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -714,8 +714,8 @@ impl FromStr for ExtXDateRange { let mut client_attributes = BTreeMap::new(); - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index f63f8d5..e8237a6 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -168,8 +168,8 @@ impl FromStr for ExtXMap { let mut uri = None; let mut range = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "URI" => uri = Some(unquote(value)), "BYTERANGE" => { range = Some(unquote(value).parse()?); diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 22a3acb..42d0f07 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -133,8 +133,8 @@ impl FromStr for ExtXStart { let mut time_offset = None; let mut precise = false; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "TIME-OFFSET" => time_offset = Some((value.parse())?), "PRECISE" => precise = (parse_yes_or_no(value))?, _ => { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 9a57fa4..f5933d3 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -293,10 +293,18 @@ impl FromStr for DecryptionKey { let mut key_format = None; let mut key_format_versions = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "METHOD" => method = Some(value.parse().map_err(Error::strum)?), - "URI" => uri = Some(unquote(value)), + "URI" => { + let unquoted_uri = unquote(value); + + if unquoted_uri.trim().is_empty() { + uri = None; + } else { + uri = Some(unquoted_uri); + } + } "IV" => iv = Some(value.parse()?), "KEYFORMAT" => key_format = Some(value.parse()?), "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index 4886249..afe462e 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -268,8 +268,8 @@ impl FromStr for StreamInf { let mut hdcp_level = None; let mut video = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), "AVERAGE-BANDWIDTH" => { average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) From 048f09bd14d1c19a276f07e83ec1cc647df2c74a Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 26 Jan 2020 13:12:19 +0100 Subject: [PATCH 005/112] minor improvements --- examples/parse.rs | 3 --- src/error.rs | 2 +- src/lib.rs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/parse.rs b/examples/parse.rs index 226a22d..16c960b 100644 --- a/examples/parse.rs +++ b/examples/parse.rs @@ -1,6 +1,3 @@ -extern crate clap; -extern crate hls_m3u8; - use clap::{App, Arg}; use hls_m3u8::{MasterPlaylist, MediaPlaylist}; use std::io::{self, Read}; diff --git a/src/error.rs b/src/error.rs index 1333ce5..2cf9a75 100644 --- a/src/error.rs +++ b/src/error.rs @@ -101,7 +101,7 @@ impl fmt::Display for Error { } impl Error { - fn new(inner: ErrorKind) -> Self { Self { inner } } + const fn new(inner: ErrorKind) -> Self { Self { inner } } pub(crate) fn custom(value: T) -> Self { Self::new(ErrorKind::Custom(value.to_string())) diff --git a/src/lib.rs b/src/lib.rs index 3d4b2be..45b1bc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ clippy::nursery, clippy::cargo )] -#![allow(clippy::multiple_crate_versions)] +#![allow(clippy::multiple_crate_versions, clippy::must_use_candidate)] #![warn( missing_docs, missing_copy_implementations, From 27d94faec4b5f5d75ce5e7f05faa23e6ef107b42 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 2 Feb 2020 13:38:11 +0100 Subject: [PATCH 006/112] use shorthand #24 --- Cargo.toml | 1 + src/attribute.rs | 2 +- src/master_playlist.rs | 179 ++---- src/media_playlist.rs | 113 ++-- src/media_segment.rs | 120 +--- .../master_playlist/i_frame_stream_inf.rs | 5 +- src/tags/master_playlist/media.rs | 547 ++--------------- src/tags/master_playlist/session_data.rs | 139 +---- src/tags/master_playlist/session_key.rs | 20 +- src/tags/master_playlist/stream_inf.rs | 168 +---- .../media_playlist/discontinuity_sequence.rs | 10 +- src/tags/media_playlist/end_list.rs | 7 +- src/tags/media_playlist/i_frames_only.rs | 7 +- src/tags/media_playlist/media_sequence.rs | 4 + src/tags/media_playlist/target_duration.rs | 7 +- src/tags/media_segment/byte_range.rs | 12 +- src/tags/media_segment/date_range.rs | 575 +----------------- src/tags/media_segment/discontinuity.rs | 7 +- src/tags/media_segment/inf.rs | 8 + src/tags/media_segment/key.rs | 6 + src/tags/media_segment/map.rs | 111 ++-- src/tags/media_segment/program_date_time.rs | 7 +- src/tags/shared/independent_segments.rs | 6 +- src/tags/shared/start.rs | 11 +- src/traits.rs | 7 + src/types/byte_range.rs | 137 ++--- src/types/channels.rs | 15 +- src/types/decimal_floating_point.rs | 8 +- src/types/decimal_resolution.rs | 123 ---- src/types/decryption_key.rs | 349 ++++------- src/types/encryption_method.rs | 9 +- src/types/initialization_vector.rs | 150 ----- src/types/key_format.rs | 2 +- src/types/mod.rs | 6 +- src/types/protocol_version.rs | 3 + src/types/resolution.rs | 104 ++++ src/types/signed_decimal_floating_point.rs | 1 + src/types/stream_inf.rs | 292 ++++----- src/utils.rs | 23 + 39 files changed, 820 insertions(+), 2481 deletions(-) delete mode 100644 src/types/decimal_resolution.rs delete mode 100644 src/types/initialization_vector.rs create mode 100644 src/types/resolution.rs diff --git a/Cargo.toml b/Cargo.toml index e7703b6..2868525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ chrono = "0.4.9" strum = { version = "0.16.0", features = ["derive"] } derive_more = "0.15.0" hex = "0.4.0" +shorthand = "0.1" [dev-dependencies] clap = "2.33.0" diff --git a/src/attribute.rs b/src/attribute.rs index 228ca82..50a6d17 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,6 +1,6 @@ use core::iter::FusedIterator; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash)] pub(crate) struct AttributePairs<'a> { string: &'a str, index: usize, diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 513658d..b081256 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -3,6 +3,7 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; +use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::tags::{ @@ -12,57 +13,65 @@ use crate::tags::{ use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::{Error, RequiredVersion}; -#[derive(Debug, Clone, Builder, PartialEq)] +/// Master playlist. +#[derive(ShortHand, Debug, Clone, Builder, PartialEq)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] -/// Master playlist. +#[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MasterPlaylist { - #[builder(default)] - /// Sets the [`ExtXIndependentSegments`] tag. + /// The [`ExtXIndependentSegments`] tag of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] independent_segments_tag: Option, - #[builder(default)] - /// Sets the [`ExtXStart`] tag. + /// The [`ExtXStart`] tag of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] start_tag: Option, - #[builder(default)] - /// Sets the [`ExtXMedia`] tag. + /// The [`ExtXMedia`] tags of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] media_tags: Vec, - #[builder(default)] - /// Sets all [`ExtXStreamInf`] tags. + /// The [`ExtXStreamInf`] tags of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] stream_inf_tags: Vec, - #[builder(default)] - /// Sets all [`ExtXIFrameStreamInf`] tags. + /// The [`ExtXIFrameStreamInf`] tags of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] i_frame_stream_inf_tags: Vec, - #[builder(default)] - /// Sets all [`ExtXSessionData`] tags. + /// The [`ExtXSessionData`] tags of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] session_data_tags: Vec, - #[builder(default)] - /// Sets all [`ExtXSessionKey`] tags. + /// The [`ExtXSessionKey`] tags of the playlist. /// /// # Note + /// /// This tag is optional. + #[builder(default)] session_key_tags: Vec, } impl MasterPlaylist { - /// Returns a Builder for a [`MasterPlaylist`]. + /// Returns a builder for a [`MasterPlaylist`]. /// /// # Example /// @@ -76,124 +85,6 @@ impl MasterPlaylist { /// # Ok::<(), Box>(()) /// ``` pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } - - /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. - pub const fn independent_segments(&self) -> Option { - self.independent_segments_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 [`ExtXStart`] tag contained in the playlist. - pub const fn start(&self) -> Option { self.start_tag } - - /// 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 [`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 - } - - /// 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 - } - - /// 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 { @@ -241,22 +132,22 @@ impl MasterPlaylistBuilder { return Err(Error::unmatched_group(group_id)); } } - match t.closed_captions() { - &Some(ClosedCaptions::GroupId(ref group_id)) => { + match &t.closed_captions() { + Some(ClosedCaptions::GroupId(group_id)) => { if !self.check_media_group(MediaType::ClosedCaptions, group_id) { return Err(Error::unmatched_group(group_id)); } } - &Some(ClosedCaptions::None) => { + Some(ClosedCaptions::None) => { has_none_closed_captions = true; } - None => {} + _ => {} } } if has_none_closed_captions && !value .iter() - .all(|t| t.closed_captions() == &Some(ClosedCaptions::None)) + .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) { return Err(Error::invalid_input()); } @@ -324,27 +215,35 @@ impl fmt::Display for MasterPlaylist { if self.required_version() != ProtocolVersion::V1 { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } + for t in &self.media_tags { writeln!(f, "{}", t)?; } + for t in &self.stream_inf_tags { writeln!(f, "{}", t)?; } + for t in &self.i_frame_stream_inf_tags { writeln!(f, "{}", t)?; } + for t in &self.session_data_tags { writeln!(f, "{}", t)?; } + for t in &self.session_key_tags { writeln!(f, "{}", t)?; } + if let Some(value) = &self.independent_segments_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.start_tag { writeln!(f, "{}", value)?; } + Ok(()) } } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 4fb97f4..f0c5bab 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use std::time::Duration; use derive_builder::Builder; +use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::media_segment::MediaSegment; @@ -14,44 +15,87 @@ use crate::types::ProtocolVersion; use crate::{Encrypted, Error, RequiredVersion}; /// Media playlist. -#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] +#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] +#[shorthand(enable(must_use, collection_magic, get_mut))] pub struct MediaPlaylist { - /// Sets the [`ExtXTargetDuration`] tag. + /// The [`ExtXTargetDuration`] tag of the playlist. + /// + /// # Note + /// + /// This field is required. + #[shorthand(enable(copy))] target_duration_tag: ExtXTargetDuration, - #[builder(default)] /// Sets the [`ExtXMediaSequence`] tag. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] media_sequence_tag: Option, - #[builder(default)] /// Sets the [`ExtXDiscontinuitySequence`] tag. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] discontinuity_sequence_tag: Option, - #[builder(default)] /// Sets the [`ExtXPlaylistType`] tag. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] playlist_type_tag: Option, - #[builder(default)] /// Sets the [`ExtXIFramesOnly`] tag. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] i_frames_only_tag: Option, - #[builder(default)] /// Sets the [`ExtXIndependentSegments`] tag. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] independent_segments_tag: Option, - #[builder(default)] /// Sets the [`ExtXStart`] tag. - start_tag: Option, + /// + /// # Note + /// + /// This field is optional. #[builder(default)] + start_tag: Option, /// Sets the [`ExtXEndList`] tag. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] end_list_tag: Option, - /// Sets all [`MediaSegment`]s. + /// A list of all [`MediaSegment`]s. + /// + /// # Note + /// + /// This field is required. segments: Vec, - /// Sets the allowable excess duration of each media segment in the + /// The allowable excess duration of each media segment in the /// associated playlist. /// /// # Error + /// /// If there is a media segment of which duration exceeds /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, /// the invocation of `MediaPlaylistBuilder::build()` method will fail. /// - /// The default value is `Duration::from_secs(0)`. + /// + /// # Note + /// + /// This field is optional and the default value is + /// `Duration::from_secs(0)`. #[builder(default = "Duration::from_secs(0)")] allowable_excess_duration: Duration, } @@ -68,6 +112,7 @@ impl MediaPlaylistBuilder { fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> { let mut last_range_uri = None; + if let Some(segments) = &self.segments { for s in segments { // CHECK: `#EXT-X-TARGETDURATION` @@ -113,6 +158,7 @@ impl MediaPlaylistBuilder { } } } + Ok(()) } @@ -151,38 +197,6 @@ impl RequiredVersion for MediaPlaylistBuilder { impl MediaPlaylist { /// Returns a builder for [`MediaPlaylist`]. pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } - - /// 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 [`ExtXDiscontinuitySequence`] tag contained in the - /// playlist. - pub const fn discontinuity_sequence_tag(&self) -> Option { - self.discontinuity_sequence_tag - } - - /// Returns the [`ExtXPlaylistType`] tag contained in the playlist. - pub const fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } - - /// Returns the [`ExtXIFramesOnly`] tag contained in the playlist. - pub const fn i_frames_only_tag(&self) -> Option { self.i_frames_only_tag } - - /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. - pub const fn independent_segments_tag(&self) -> Option { - self.independent_segments_tag - } - - /// Returns the [`ExtXStart`] tag contained in the playlist. - pub const fn start_tag(&self) -> Option { self.start_tag } - - /// Returns the [`ExtXEndList`] tag contained in the playlist. - pub const fn end_list_tag(&self) -> Option { self.end_list_tag } - - /// Returns the [`MediaSegment`]s contained in the playlist. - pub const fn segments(&self) -> &Vec { &self.segments } } impl RequiredVersion for MediaPlaylist { @@ -204,34 +218,45 @@ impl RequiredVersion for MediaPlaylist { impl fmt::Display for MediaPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; + 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 { writeln!(f, "{}", value)?; } + if let Some(value) = &self.discontinuity_sequence_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.playlist_type_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.i_frames_only_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.independent_segments_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.start_tag { writeln!(f, "{}", value)?; } + for segment in &self.segments { write!(f, "{}", segment)?; } + if let Some(value) = &self.end_list_tag { writeln!(f, "{}", value)?; } + Ok(()) } } diff --git a/src/media_segment.rs b/src/media_segment.rs index ea4f160..803086d 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,6 +1,7 @@ use std::fmt; use derive_builder::Builder; +use shorthand::ShortHand; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, @@ -8,127 +9,42 @@ use crate::tags::{ use crate::types::ProtocolVersion; use crate::{Encrypted, RequiredVersion}; -#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] -#[builder(setter(into, strip_option))] /// Media segment. +#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] +#[builder(setter(into, strip_option))] +#[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MediaSegment { - #[builder(default)] /// Sets all [`ExtXKey`] tags. + #[builder(default)] keys: Vec, + /// The [`ExtXMap`] tag associated with the media segment. #[builder(default)] - /// Sets an [`ExtXMap`] tag. map_tag: Option, + /// The [`ExtXByteRange`] tag associated with the [`MediaSegment`]. #[builder(default)] - /// Sets an [`ExtXByteRange`] tag. byte_range_tag: Option, + /// The [`ExtXDateRange`] tag associated with the media segment. #[builder(default)] - /// Sets an [`ExtXDateRange`] tag. date_range_tag: Option, + /// The [`ExtXDiscontinuity`] tag associated with the media segment. #[builder(default)] - /// Sets an [`ExtXDiscontinuity`] tag. discontinuity_tag: Option, + /// The [`ExtXProgramDateTime`] tag associated with the media + /// segment. #[builder(default)] - /// Sets an [`ExtXProgramDateTime`] tag. program_date_time_tag: Option, - /// Sets an [`ExtInf`] tag. + /// The [`ExtInf`] tag associated with the [`MediaSegment`]. inf_tag: ExtInf, - /// Sets an `URI`. + /// The `URI` of the [`MediaSegment`]. + #[shorthand(enable(into))] uri: String, } impl MediaSegment { - /// Returns a Builder for a [`MasterPlaylist`]. + /// 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 { @@ -148,21 +64,27 @@ impl fmt::Display for MediaSegment { for value in &self.keys { writeln!(f, "{}", value)?; } + if let Some(value) = &self.map_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.byte_range_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.date_range_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.discontinuity_tag { writeln!(f, "{}", value)?; } + if let Some(value) = &self.program_date_time_tag { writeln!(f, "{}", value)?; } + writeln!(f, "{}", self.inf_tag)?; // TODO: there might be a `,` missing writeln!(f, "{}", self.uri)?; Ok(()) diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 7f748ac..6fee28c 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -96,6 +96,7 @@ impl ExtXIFrameStreamInf { /// Makes a new [`ExtXIFrameStreamInf`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXIFrameStreamInf; /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); @@ -113,6 +114,7 @@ impl ExtXIFrameStreamInf { /// Returns the `URI`, that identifies the associated [`media playlist`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXIFrameStreamInf; /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); @@ -125,6 +127,7 @@ impl ExtXIFrameStreamInf { /// Sets the `URI`, that identifies the associated [`media playlist`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXIFrameStreamInf; /// # @@ -200,7 +203,7 @@ mod test { i_frame_stream_inf .set_average_bandwidth(Some(100_000)) .set_codecs(Some("mp4a.40.5")) - .set_resolution(1920, 1080) + .set_resolution(Some((1920, 1080))) .set_hdcp_level(Some(HdcpLevel::None)) .set_video(Some("video")); diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 027ddfb..7a5e2cc 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -2,6 +2,7 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; +use shorthand::ShortHand; use crate::attribute::AttributePairs; use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion}; @@ -22,17 +23,19 @@ use crate::{Error, RequiredVersion}; /// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.5.1. EXT-X-MEDIA]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.1 -#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[shorthand(enable(must_use, into))] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] pub struct ExtXMedia { - /// Sets the [`MediaType`] of the rendition. + /// The [`MediaType`] that is associated with this tag. /// /// # Note /// /// This attribute is **required**. + #[shorthand(enable(copy))] media_type: MediaType, - /// Sets the `URI` that identifies the [`Media Playlist`]. + /// The `URI` that identifies the [`Media Playlist`]. /// /// # Note /// @@ -44,14 +47,14 @@ pub struct ExtXMedia { /// [`Media Playlist`]: crate::MediaPlaylist #[builder(setter(strip_option), default)] uri: Option, - /// Sets the identifier, that specifies the group to which the rendition + /// The identifier that specifies the group to which the rendition /// belongs. /// /// # Note /// /// This attribute is **required**. group_id: String, - /// Sets the name of the primary language used in the rendition. + /// The name of the primary language used in the rendition. /// The value has to conform to [`RFC5646`]. /// /// # Note @@ -61,7 +64,10 @@ pub struct ExtXMedia { /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 #[builder(setter(strip_option), default)] language: Option, - /// Sets the name of a language associated with the rendition. + /// The name of a language associated with the rendition. + /// An associated language is often used in a different role, than the + /// language specified by the [`language`] attribute (e.g., written versus + /// spoken, or a fallback dialect). /// /// # Note /// @@ -70,7 +76,7 @@ pub struct ExtXMedia { /// [`language`]: #method.language #[builder(setter(strip_option), default)] assoc_language: Option, - /// Sets a human-readable description of the rendition. + /// A human-readable description of the rendition. /// /// # Note /// @@ -81,7 +87,10 @@ pub struct ExtXMedia { /// /// [`language`]: #method.language name: String, - /// Sets the value of the `default` flag. + /// The value of the `default` flag. + /// A value of `true` indicates, that the client should play + /// this rendition of the content in the absence of information + /// from the user indicating a different choice. /// /// # Note /// @@ -89,7 +98,8 @@ pub struct ExtXMedia { /// of `false`. #[builder(default)] is_default: bool, - /// Sets the value of the `autoselect` flag. + /// Whether the client may choose to play this rendition in the absence of + /// explicit user preference. /// /// # Note /// @@ -97,17 +107,37 @@ pub struct ExtXMedia { /// of `false`. #[builder(default)] is_autoselect: bool, - /// Sets the value of the `forced` flag. + /// Whether the rendition contains content that is considered + /// essential to play. #[builder(default)] is_forced: bool, - /// Sets the identifier that specifies a rendition within the segments in - /// the media playlist. + /// An [`InStreamId`] specifies a rendition within the + /// segments in the [`Media Playlist`]. + /// + /// [`Media Playlist`]: crate::MediaPlaylist #[builder(setter(strip_option), default)] + #[shorthand(enable(copy))] instream_id: Option, - /// Sets the string that represents uniform type identifiers (UTI). + /// The characteristics attribute, containing one or more Uniform Type + /// Identifiers (UTI) separated by comma. + /// Each [`UTI`] indicates an individual characteristic of the Rendition. + /// + /// A [`subtitles`] rendition may include the following characteristics: + /// "public.accessibility.transcribes-spoken-dialog", + /// "public.accessibility.describes-music-and-sound", and + /// "public.easy-to-read" (which indicates that the subtitles have + /// been edited for ease of reading). + /// + /// An AUDIO Rendition MAY include the following characteristic: + /// "public.accessibility.describes-video". + /// + /// The characteristics attribute may include private UTIs. + /// + /// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI + /// [`subtitles`]: crate::types::MediaType::Subtitles #[builder(setter(strip_option), default)] characteristics: Option, - /// Sets the parameters of the rendition. + /// The [`Channels`]. #[builder(setter(strip_option), default)] channels: Option, } @@ -173,495 +203,6 @@ impl ExtXMedia { /// Returns a builder for [`ExtXMedia`]. pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() } - - /// Returns the type of the media, associated with this tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// assert_eq!( - /// ExtXMedia::new(MediaType::Audio, "audio", "name").media_type(), - /// MediaType::Audio - /// ); - /// ``` - pub const fn media_type(&self) -> MediaType { self.media_type } - - /// Sets the type of the media, associated with this tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// - /// media.set_media_type(MediaType::Video); - /// - /// assert_eq!(media.media_type(), MediaType::Video); - /// ``` - pub fn set_media_type(&mut self, value: MediaType) -> &mut Self { - self.media_type = value; - self - } - - /// Returns the identifier that specifies the group to which the rendition - /// belongs. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// assert_eq!( - /// ExtXMedia::new(MediaType::Audio, "audio", "name").group_id(), - /// &"audio".to_string() - /// ); - /// ``` - pub const fn group_id(&self) -> &String { &self.group_id } - - /// Sets the identifier that specifies the group, to which the rendition - /// belongs. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// - /// media.set_group_id("video"); - /// - /// assert_eq!(media.group_id(), &"video".to_string()); - /// ``` - pub fn set_group_id>(&mut self, value: T) -> &mut Self { - self.group_id = value.into(); - self - } - - /// Returns a human-readable description of the rendition. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// assert_eq!( - /// ExtXMedia::new(MediaType::Audio, "audio", "name").name(), - /// &"name".to_string() - /// ); - /// ``` - pub const fn name(&self) -> &String { &self.name } - - /// Sets a human-readable description of the rendition. - /// - /// # Note - /// - /// If the [`language`] attribute is present, this attribute should be in - /// that language. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// - /// media.set_name("new_name"); - /// - /// assert_eq!(media.name(), &"new_name".to_string()); - /// ``` - /// - /// [`language`]: #method.language - pub fn set_name>(&mut self, value: T) -> &mut Self { - self.name = value.into(); - self - } - - /// Returns the `URI`, that identifies the [`Media Playlist`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.uri(), &None); - /// - /// media.set_uri(Some("https://www.example.com/")); - /// - /// assert_eq!(media.uri(), &Some("https://www.example.com/".into())); - /// ``` - /// - /// [`Media Playlist`]: crate::MediaPlaylist - pub const fn uri(&self) -> &Option { &self.uri } - - /// Sets the `URI`, that identifies the [`Media Playlist`]. - /// - /// # Note - /// - /// This attribute is **required**, if the [`MediaType`] is - /// [`MediaType::Subtitles`]. This attribute is **not allowed**, if the - /// [`MediaType`] is [`MediaType::ClosedCaptions`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.uri(), &None); - /// - /// media.set_uri(Some("https://www.example.com/")); - /// - /// assert_eq!(media.uri(), &Some("https://www.example.com/".into())); - /// ``` - /// - /// [`Media Playlist`]: crate::MediaPlaylist - pub fn set_uri>(&mut self, value: Option) -> &mut Self { - self.uri = value.map(Into::into); - self - } - - /// Returns the name of the primary language used in the rendition. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.language(), &None); - /// - /// media.set_language(Some("english")); - /// - /// assert_eq!(media.language(), &Some("english".into())); - /// ``` - pub const fn language(&self) -> &Option { &self.language } - - /// Sets the name of the primary language used in the rendition. - /// The value has to conform to [`RFC5646`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.language(), &None); - /// - /// media.set_language(Some("english")); - /// - /// assert_eq!(media.language(), &Some("english".into())); - /// ``` - /// - /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 - pub fn set_language>(&mut self, value: Option) -> &mut Self { - self.language = value.map(Into::into); - self - } - - /// Returns the name of a language associated with the rendition. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.assoc_language(), &None); - /// - /// media.set_assoc_language(Some("spanish")); - /// - /// assert_eq!(media.assoc_language(), &Some("spanish".into())); - /// ``` - pub const fn assoc_language(&self) -> &Option { &self.assoc_language } - - /// Sets the name of a language associated with the rendition. - /// An associated language is often used in a different role, than the - /// language specified by the [`language`] attribute (e.g., written versus - /// spoken, or a fallback dialect). - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.assoc_language(), &None); - /// - /// media.set_assoc_language(Some("spanish")); - /// - /// assert_eq!(media.assoc_language(), &Some("spanish".into())); - /// ``` - /// - /// [`language`]: #method.language - pub fn set_assoc_language>(&mut self, value: Option) -> &mut Self { - self.assoc_language = value.map(Into::into); - self - } - - /// Returns whether this is the `default` rendition. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.is_default(), false); - /// - /// media.set_default(true); - /// - /// assert_eq!(media.is_default(), true); - /// ``` - pub const fn is_default(&self) -> bool { self.is_default } - - /// Sets the `default` flag. - /// A value of `true` indicates, that the client should play - /// this rendition of the content in the absence of information - /// from the user indicating a different choice. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.is_default(), false); - /// - /// media.set_default(true); - /// - /// assert_eq!(media.is_default(), true); - /// ``` - pub fn set_default(&mut self, value: bool) -> &mut Self { - self.is_default = value; - self - } - - /// Returns whether the client may choose to - /// play this rendition in the absence of explicit user preference. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.is_autoselect(), false); - /// - /// media.set_autoselect(true); - /// - /// assert_eq!(media.is_autoselect(), true); - /// ``` - pub const fn is_autoselect(&self) -> bool { self.is_autoselect } - - /// Sets the `autoselect` flag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.is_autoselect(), false); - /// - /// media.set_autoselect(true); - /// - /// assert_eq!(media.is_autoselect(), true); - /// ``` - pub fn set_autoselect(&mut self, value: bool) -> &mut Self { - self.is_autoselect = value; - self - } - - /// Returns whether the rendition contains content that is considered - /// essential to play. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.is_forced(), false); - /// - /// media.set_forced(true); - /// - /// assert_eq!(media.is_forced(), true); - /// ``` - pub const fn is_forced(&self) -> bool { self.is_forced } - - /// Sets the `forced` flag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.is_forced(), false); - /// - /// media.set_forced(true); - /// - /// assert_eq!(media.is_forced(), true); - /// ``` - pub fn set_forced(&mut self, value: bool) -> &mut Self { - self.is_forced = value; - self - } - - /// Returns the identifier that specifies a rendition within the segments in - /// the [`Media Playlist`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::{InStreamId, MediaType}; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.instream_id(), None); - /// - /// media.set_instream_id(Some(InStreamId::Cc1)); - /// - /// assert_eq!(media.instream_id(), Some(InStreamId::Cc1)); - /// ``` - /// - /// [`Media Playlist`]: crate::MediaPlaylist - pub const fn instream_id(&self) -> Option { self.instream_id } - - /// Sets the [`InStreamId`], that specifies a rendition within the - /// segments in the [`Media Playlist`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::{InStreamId, MediaType}; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.instream_id(), None); - /// - /// media.set_instream_id(Some(InStreamId::Cc1)); - /// - /// assert_eq!(media.instream_id(), Some(InStreamId::Cc1)); - /// ``` - pub fn set_instream_id(&mut self, value: Option) -> &mut Self { - self.instream_id = value; - self - } - - /// Returns a string that represents uniform type identifiers (UTI). - /// - /// Each UTI indicates an individual characteristic of the rendition. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.characteristics(), &None); - /// - /// media.set_characteristics(Some("characteristic")); - /// - /// assert_eq!(media.characteristics(), &Some("characteristic".into())); - /// ``` - pub const fn characteristics(&self) -> &Option { &self.characteristics } - - /// Sets the characteristics attribute, containing one or more Uniform Type - /// Identifiers separated by comma. - /// Each [`UTI`] indicates an individual characteristic of the Rendition. - /// - /// A [`subtitles`] Rendition may include the following characteristics: - /// "public.accessibility.transcribes-spoken-dialog", - /// "public.accessibility.describes-music-and-sound", and - /// "public.easy-to-read" (which indicates that the subtitles have - /// been edited for ease of reading). - /// - /// An AUDIO Rendition MAY include the following characteristic: - /// "public.accessibility.describes-video". - /// - /// The characteristics attribute may include private UTIs. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.characteristics(), &None); - /// - /// media.set_characteristics(Some("characteristic")); - /// - /// assert_eq!(media.characteristics(), &Some("characteristic".into())); - /// ``` - /// - /// [`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(Into::into); - self - } - - /// Returns the channels. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::{Channels, MediaType}; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.channels(), &None); - /// - /// media.set_channels(Some(Channels::new(6))); - /// - /// assert_eq!(media.channels(), &Some(Channels::new(6))); - /// ``` - pub const fn channels(&self) -> &Option { &self.channels } - - /// Sets the channels. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::{Channels, MediaType}; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); - /// # assert_eq!(media.channels(), &None); - /// - /// media.set_channels(Some(Channels::new(6))); - /// - /// assert_eq!(media.channels(), &Some(Channels::new(6))); - /// ``` - pub fn set_channels>(&mut self, value: Option) -> &mut Self { - self.channels = value.map(Into::into); - self - } } impl RequiredVersion for ExtXMedia { diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index d73e9ab..206b4da 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -2,6 +2,7 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; +use shorthand::ShortHand; use crate::attribute::AttributePairs; use crate::types::ProtocolVersion; @@ -34,26 +35,31 @@ pub enum SessionData { /// /// [`Master Playlist`]: crate::MasterPlaylist /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 -#[derive(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] +#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[builder(setter(into))] +#[shorthand(enable(must_use, into))] pub struct ExtXSessionData { - /// The identifier of the data. - /// For more information look [`here`]. + /// Sets the `data_id` attribute, that should conform to a [reverse DNS] + /// naming convention, such as `com.example.movie.title`. /// /// # Note + /// + /// There is no central registration authority, so a value + /// should be choosen, that is unlikely to collide with others. + /// /// This field is required. /// - /// [`here`]: ExtXSessionData::set_data_id + /// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation data_id: String, /// The data associated with the [`data_id`]. /// For more information look [`here`](SessionData). /// /// # Note - /// This field is required. /// - /// [`data_id`]: ExtXSessionDataBuilder::data_id + /// This field is required. data: SessionData, - /// The language of the [`data`](ExtXSessionDataBuilder::data). + /// The `language` attribute identifies the language of [`SessionData`]. + /// See [rfc5646](https://tools.ietf.org/html/rfc5646). #[builder(setter(into, strip_option), default)] language: Option, } @@ -107,6 +113,7 @@ impl ExtXSessionData { /// Makes a new [`ExtXSessionData`] tag, with the given language. /// /// # Example + /// /// ``` /// use hls_m3u8::tags::{ExtXSessionData, SessionData}; /// @@ -123,125 +130,9 @@ impl ExtXSessionData { language: Some(language.to_string()), } } - - /// Returns the `data_id`, that identifies a `data_value`. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// ); - /// - /// assert_eq!(data.data_id(), &"com.example.movie.title".to_string()) - /// ``` - pub const fn data_id(&self) -> &String { &self.data_id } - - /// Returns the `data`. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// ); - /// - /// assert_eq!(data.data(), &SessionData::Value("some data".to_string())) - /// ``` - pub const fn data(&self) -> &SessionData { &self.data } - - /// Returns the `language` tag, that identifies the language of - /// [`SessionData`]. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let data = ExtXSessionData::with_language( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// "english", - /// ); - /// - /// assert_eq!(data.language(), &Some("english".to_string())) - /// ``` - pub const fn language(&self) -> &Option { &self.language } - - /// Sets the `language` attribute, that identifies the language of - /// [`SessionData`]. See [rfc5646](https://tools.ietf.org/html/rfc5646). - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let mut data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// ); - /// - /// assert_eq!(data.language(), &None); - /// - /// data.set_language(Some("english")); - /// assert_eq!(data.language(), &Some("english".to_string())); - /// ``` - pub fn set_language(&mut self, value: Option) -> &mut Self { - self.language = value.map(|v| v.to_string()); - self - } - - /// Sets the `data_id` attribute, that should conform to a [reverse DNS] - /// naming convention, such as `com.example.movie.title`. - /// - /// # Note: - /// There is no central registration authority, so a value - /// should be choosen, that is unlikely to collide with others. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let mut data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// ); - /// - /// assert_eq!(data.data_id(), &"com.example.movie.title".to_string()); - /// - /// data.set_data_id("com.other.movie.title"); - /// assert_eq!(data.data_id(), &"com.other.movie.title".to_string()); - /// ``` - /// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation - pub fn set_data_id(&mut self, value: T) -> &mut Self { - self.data_id = value.to_string(); - self - } - - /// Sets the [`data`](ExtXSessionData::data) of this tag. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::{ExtXSessionData, SessionData}; - /// # - /// let mut data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// ); - /// - /// assert_eq!(data.data(), &SessionData::Value("some data".to_string())); - /// - /// data.set_data(SessionData::Value("new data".to_string())); - /// assert_eq!(data.data(), &SessionData::Value("new data".to_string())); - /// ``` - pub fn set_data(&mut self, value: SessionData) -> &mut Self { - self.data = value; - self - } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXSessionData { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 6bd3731..0f23184 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -7,16 +7,12 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// # [4.3.4.5. EXT-X-SESSION-KEY] +/// /// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s /// to be specified in a [`Master Playlist`]. This allows the client to /// preload these keys without having to read the [`Media Playlist`]s /// first. /// -/// Its format is: -/// ```text -/// #EXT-X-SESSION-KEY: -/// ``` -/// /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 @@ -29,12 +25,14 @@ impl ExtXSessionKey { /// Makes a new [`ExtXSessionKey`] tag. /// /// # Panic + /// /// An [`ExtXSessionKey`] should only be used, /// if the segments of the stream are encrypted. /// Therefore this function will panic, /// if the `method` is [`EncryptionMethod::None`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXSessionKey; /// use hls_m3u8::types::EncryptionMethod; @@ -50,6 +48,8 @@ impl ExtXSessionKey { } } +/// This tag requires the version returned by +/// [`DecryptionKey::required_version`]. impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } @@ -57,8 +57,10 @@ impl RequiredVersion for ExtXSessionKey { impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.0.method == EncryptionMethod::None { + // TODO: this is bad practice, this function should never fail! return Err(fmt::Error); } + write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -159,10 +161,10 @@ mod test { ); } - #[test] - #[should_panic] // ExtXSessionKey::new should panic, if the provided // EncryptionMethod is None! + #[test] + #[should_panic] fn test_new_panic() { ExtXSessionKey::new(EncryptionMethod::None, ""); } #[test] @@ -176,7 +178,7 @@ mod test { let key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); assert_eq!(key.method(), EncryptionMethod::Aes128); - assert_eq!(key.uri(), &Some("https://www.example.com/".into())); + assert_eq!(key.uri(), Some(&"https://www.example.com/".into())); } #[test] @@ -186,6 +188,6 @@ mod test { key.set_method(EncryptionMethod::None); assert_eq!(key.method(), EncryptionMethod::None); key.set_uri(Some("https://www.github.com/")); - assert_eq!(key.uri(), &Some("https://www.github.com/".into())); + assert_eq!(key.uri(), Some(&"https://www.github.com/".into())); } } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 7fcb885..df7b350 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -2,6 +2,8 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use shorthand::ShortHand; + use crate::attribute::AttributePairs; use crate::types::{ ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder, @@ -21,13 +23,20 @@ use crate::{Error, RequiredVersion}; /// 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)] +#[derive(ShortHand, PartialOrd, Debug, Clone, PartialEq)] +#[shorthand(enable(must_use, into))] pub struct ExtXStreamInf { + /// The `URI` that identifies the associated media playlist. uri: String, + #[shorthand(enable(skip))] frame_rate: Option, + /// The group identifier for the audio in the variant stream. audio: Option, + /// The group identifier for the subtitles in the variant stream. subtitles: Option, + /// The value of the [`ClosedCaptions`] attribute. closed_captions: Option, + #[shorthand(enable(skip))] stream_inf: StreamInf, } @@ -135,6 +144,7 @@ impl ExtXStreamInf { /// Creates a new [`ExtXStreamInf`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStreamInf; /// let stream = ExtXStreamInf::new("https://www.example.com/", 20); @@ -153,149 +163,20 @@ impl ExtXStreamInf { /// Returns a builder for [`ExtXStreamInf`]. pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() } - /// Returns the `URI` that identifies the associated media playlist. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// - /// assert_eq!(stream.uri(), &"https://www.example.com/".to_string()); - /// ``` - pub const fn uri(&self) -> &String { &self.uri } + /// The maximum frame rate for all the video in the variant stream. + #[must_use] + #[inline] + pub fn frame_rate(&self) -> Option { self.frame_rate.map(DecimalFloatingPoint::as_f64) } - /// Sets the `URI` that identifies the associated media playlist. + /// The maximum frame rate for all the video in the variant stream. /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # Panic /// - /// stream.set_uri("https://www.google.com/"); - /// assert_eq!(stream.uri(), &"https://www.google.com/".to_string()); - /// ``` - pub fn set_uri(&mut self, value: T) -> &mut Self { - self.uri = value.to_string(); - self - } - - /// Sets the maximum frame rate for all the video in the variant stream. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.frame_rate(), None); - /// - /// stream.set_frame_rate(Some(59.9)); - /// assert_eq!(stream.frame_rate(), Some(59.9)); - /// ``` - pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { - self.frame_rate = value.map(Into::into); - self - } - - /// Returns the maximum frame rate for all the video in the variant stream. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.frame_rate(), None); - /// - /// stream.set_frame_rate(Some(59.9)); - /// assert_eq!(stream.frame_rate(), Some(59.9)); - /// ``` - pub fn frame_rate(&self) -> Option { self.frame_rate.map(|v| v.as_f64()) } - - /// Returns the group identifier for the audio in the variant stream. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.audio(), &None); - /// - /// stream.set_audio(Some("audio")); - /// assert_eq!(stream.audio(), &Some("audio".to_string())); - /// ``` - pub const fn audio(&self) -> &Option { &self.audio } - - /// Sets the group identifier for the audio in the variant stream. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.audio(), &None); - /// - /// stream.set_audio(Some("audio")); - /// assert_eq!(stream.audio(), &Some("audio".to_string())); - /// ``` - pub fn set_audio>(&mut self, value: Option) -> &mut Self { - self.audio = value.map(Into::into); - self - } - - /// Returns the group identifier for the subtitles in the variant stream. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.subtitles(), &None); - /// - /// stream.set_subtitles(Some("subs")); - /// assert_eq!(stream.subtitles(), &Some("subs".to_string())); - /// ``` - pub const fn subtitles(&self) -> &Option { &self.subtitles } - - /// Sets the group identifier for the subtitles in the variant stream. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.subtitles(), &None); - /// - /// stream.set_subtitles(Some("subs")); - /// assert_eq!(stream.subtitles(), &Some("subs".to_string())); - /// ``` - pub fn set_subtitles>(&mut self, value: Option) -> &mut Self { - self.subtitles = value.map(Into::into); - self - } - - /// Returns the value of [`ClosedCaptions`] attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// use hls_m3u8::types::ClosedCaptions; - /// - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.closed_captions(), &None); - /// - /// stream.set_closed_captions(Some(ClosedCaptions::None)); - /// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None)); - /// ``` - pub const fn closed_captions(&self) -> &Option { &self.closed_captions } - - /// Sets the value of [`ClosedCaptions`] attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// use hls_m3u8::types::ClosedCaptions; - /// - /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// # assert_eq!(stream.closed_captions(), &None); - /// - /// stream.set_closed_captions(Some(ClosedCaptions::None)); - /// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None)); - /// ``` - pub fn set_closed_captions(&mut self, value: Option) -> &mut Self { - self.closed_captions = value; + /// This function panics, if the float is infinite or negative. + pub fn set_frame_rate>(&mut self, value_0: Option) -> &mut Self { + self.frame_rate = value_0.map(|v| { + DecimalFloatingPoint::new(v.into()).expect("the float must be positive and finite") + }); self } } @@ -308,18 +189,23 @@ impl RequiredVersion for ExtXStreamInf { impl fmt::Display for ExtXStreamInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.stream_inf)?; + if let Some(value) = &self.frame_rate { write!(f, ",FRAME-RATE={:.3}", value.as_f64())?; } + if let Some(value) = &self.audio { write!(f, ",AUDIO={}", quote(value))?; } + if let Some(value) = &self.subtitles { write!(f, ",SUBTITLES={}", quote(value))?; } + if let Some(value) = &self.closed_captions { write!(f, ",CLOSED-CAPTIONS={}", value)?; } + write!(f, "\n{}", self.uri)?; Ok(()) } diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index c49b9d7..9869dee 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -11,12 +11,6 @@ use crate::RequiredVersion; /// different Renditions of the same Variant Stream or different Variant /// Streams that have [`ExtXDiscontinuity`] tags in their [`Media Playlist`]s. /// -/// Its format is: -/// ```text -/// #EXT-X-DISCONTINUITY-SEQUENCE: -/// ``` -/// where `number` is a [u64]. -/// /// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity /// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: @@ -30,6 +24,7 @@ impl ExtXDiscontinuitySequence { /// Makes a new [ExtXDiscontinuitySequence] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); @@ -40,6 +35,7 @@ impl ExtXDiscontinuitySequence { /// the first media segment that appears in the associated playlist. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); @@ -51,6 +47,7 @@ impl ExtXDiscontinuitySequence { /// Sets the sequence number. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; /// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5); @@ -64,6 +61,7 @@ impl ExtXDiscontinuitySequence { } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXDiscontinuitySequence { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 5463f5e..3202870 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -6,14 +6,10 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// # [4.4.3.4. EXT-X-ENDLIST] +/// /// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be /// added to the [`Media Playlist`] file. /// -/// Its format is: -/// ```text -/// #EXT-X-ENDLIST -/// ``` -/// /// [`Media Segment`]: crate::MediaSegment /// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.3.4. EXT-X-ENDLIST]: @@ -25,6 +21,7 @@ impl ExtXEndList { pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST"; } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXEndList { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index c9c4dc9..d7334cb 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -6,17 +6,13 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] +/// /// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the /// Playlist describes a single I-frame. I-frames are encoded video /// frames, whose decoding does not depend on any other frame. I-frame /// Playlists can be used for trick play, such as fast forward, rapid /// reverse, and scrubbing. /// -/// Its format is: -/// ```text -/// #EXT-X-I-FRAMES-ONLY -/// ``` -/// /// [`Media Segment`]: crate::MediaSegment /// [4.4.3.6. EXT-X-I-FRAMES-ONLY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6 @@ -27,6 +23,7 @@ impl ExtXIFramesOnly { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY"; } +/// This tag requires [`ProtocolVersion::V4`]. impl RequiredVersion for ExtXIFramesOnly { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 5e26eb7..dfac8b7 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -6,6 +6,7 @@ use crate::utils::tag; 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. /// @@ -21,6 +22,7 @@ impl ExtXMediaSequence { /// Makes a new [`ExtXMediaSequence`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMediaSequence; /// let media_sequence = ExtXMediaSequence::new(5); @@ -31,6 +33,7 @@ impl ExtXMediaSequence { /// that appears in the associated playlist. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMediaSequence; /// let media_sequence = ExtXMediaSequence::new(5); @@ -42,6 +45,7 @@ impl ExtXMediaSequence { /// Sets the sequence number. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMediaSequence; /// let mut media_sequence = ExtXMediaSequence::new(5); diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 652afb4..0165154 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -8,11 +8,13 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// # [4.3.3.1. EXT-X-TARGETDURATION] +/// /// The [`ExtXTargetDuration`] tag specifies the maximum [`MediaSegment`] /// duration. /// /// [`MediaSegment`]: crate::MediaSegment -/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 +/// [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); @@ -22,6 +24,7 @@ impl ExtXTargetDuration { /// Makes a new [`ExtXTargetDuration`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXTargetDuration; /// use std::time::Duration; @@ -30,12 +33,14 @@ impl ExtXTargetDuration { /// ``` /// /// # Note + /// /// The nanoseconds part of the [`Duration`] will be discarded. pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) } /// Returns the maximum media segment duration. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXTargetDuration; /// use std::time::Duration; diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index f2aafb4..3a04489 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -11,15 +11,6 @@ use crate::{Error, RequiredVersion}; /// The [`ExtXByteRange`] tag indicates that a [`Media Segment`] is a sub-range /// of the resource identified by its `URI`. /// -/// Its format is: -/// ```text -/// #EXT-X-BYTERANGE:[@] -/// ``` -/// -/// where `n` is a [usize] indicating the length of the sub-range in bytes. -/// If present, `o` is a [usize] indicating the start of the sub-range, -/// as a byte offset from the beginning of the resource. -/// /// [`Media Segment`]: crate::MediaSegment /// [4.4.2.2. EXT-X-BYTERANGE]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2 @@ -32,6 +23,7 @@ impl ExtXByteRange { /// Makes a new [`ExtXByteRange`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXByteRange; /// let byte_range = ExtXByteRange::new(20, Some(5)); @@ -43,6 +35,7 @@ impl ExtXByteRange { /// Converts the [`ExtXByteRange`] to a [`ByteRange`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXByteRange; /// use hls_m3u8::types::ByteRange; @@ -53,6 +46,7 @@ impl ExtXByteRange { pub const fn to_range(&self) -> ByteRange { self.0 } } +/// This tag requires [`ProtocolVersion::V4`]. impl RequiredVersion for ExtXByteRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 4374ef7..61a2bfd 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -5,6 +5,7 @@ use std::time::Duration; use chrono::{DateTime, FixedOffset, SecondsFormat}; use derive_builder::Builder; +use shorthand::ShortHand; use crate::attribute::AttributePairs; use crate::types::{ProtocolVersion, Value}; @@ -12,84 +13,98 @@ use crate::utils::{quote, tag, unquote}; 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 -#[derive(Builder, Debug, Clone, PartialEq, PartialOrd)] +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd)] #[builder(setter(into))] +#[shorthand(enable(must_use, into))] pub struct ExtXDateRange { /// 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 [`ExtXDateRange`]s with the same class /// attribute value must adhere to these semantics. /// /// # Note + /// /// This attribute is optional. + #[builder(setter(strip_option), default)] class: Option, /// The date at which the [`ExtXDateRange`] begins. /// /// # Note + /// /// This attribute is required. start_date: DateTime, - #[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>, #[builder(setter(strip_option), default)] + end_date: Option>, /// 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, #[builder(setter(strip_option), default)] + duration: Option, /// 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. + #[builder(setter(strip_option), default)] planned_duration: Option, - #[builder(setter(strip_option), default)] - /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 + /// You can read about this attribute here + /// /// /// # Note + /// /// This attribute is optional. + #[builder(setter(strip_option), default)] scte35_cmd: Option, - #[builder(setter(strip_option), default)] - /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 + /// You can read about this attribute here + /// /// /// # Note + /// /// This attribute is optional. + #[builder(setter(strip_option), default)] scte35_out: Option, - #[builder(setter(strip_option), default)] - /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 + /// You can read about this attribute here + /// /// /// # Note + /// /// This attribute is optional. + #[builder(setter(strip_option), default)] 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 [`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, #[builder(default)] + end_on_next: bool, /// 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 @@ -97,7 +112,10 @@ pub struct ExtXDateRange { /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. /// /// # Note + /// /// This attribute is optional. + #[builder(default)] + #[shorthand(enable(collection_magic, get_mut))] client_attributes: BTreeMap, } @@ -127,6 +145,7 @@ impl ExtXDateRange { /// Makes a new [`ExtXDateRange`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXDateRange; /// use chrono::offset::TimeZone; @@ -159,535 +178,6 @@ impl ExtXDateRange { /// 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`]. @@ -776,6 +266,7 @@ 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))?; } diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 5a40451..588ed37 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -6,14 +6,10 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// # [4.4.2.3. EXT-X-DISCONTINUITY] +/// /// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the /// [`Media Segment`] that follows it and the one that preceded it. /// -/// Its format is: -/// ```text -/// #EXT-X-DISCONTINUITY -/// ``` -/// /// [`Media Segment`]: crate::MediaSegment /// [4.4.2.3. EXT-X-DISCONTINUITY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3 @@ -24,6 +20,7 @@ impl ExtXDiscontinuity { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXDiscontinuity { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index fc1cb51..60af89d 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -25,6 +25,7 @@ impl ExtInf { /// Makes a new [`ExtInf`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; @@ -41,6 +42,7 @@ impl ExtInf { /// Makes a new [`ExtInf`] tag with the given title. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; @@ -57,6 +59,7 @@ impl ExtInf { /// Returns the duration of the associated media segment. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; @@ -70,6 +73,7 @@ impl ExtInf { /// Sets the duration of the associated media segment. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; @@ -88,6 +92,7 @@ impl ExtInf { /// Returns the title of the associated media segment. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; @@ -101,6 +106,7 @@ impl ExtInf { /// Sets the title of the associated media segment. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; @@ -117,6 +123,8 @@ impl ExtInf { } } +/// This tag requires [`ProtocolVersion::V1`], if the duration does not have +/// nanoseconds, otherwise it requires [`ProtocolVersion::V3`]. impl RequiredVersion for ExtInf { fn required_version(&self) -> ProtocolVersion { if self.duration.subsec_nanos() == 0 { diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index 4ebe50b..b875861 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -15,6 +15,7 @@ use crate::{Error, RequiredVersion}; /// same [`KeyFormat`] attribute (or the end of the Playlist file). /// /// # Note +/// /// In case of an empty key ([`EncryptionMethod::None`]), /// all attributes will be ignored. /// @@ -31,6 +32,7 @@ impl ExtXKey { /// Makes a new [`ExtXKey`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; @@ -49,6 +51,7 @@ impl ExtXKey { /// Makes a new [`ExtXKey`] tag without a decryption key. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXKey; /// let key = ExtXKey::empty(); @@ -69,6 +72,7 @@ impl ExtXKey { /// [`None`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; @@ -82,6 +86,8 @@ impl ExtXKey { pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } } +/// This tag requires the [`ProtocolVersion`] returned by +/// [`DecryptionKey::required_version`]. impl RequiredVersion for ExtXKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index e8237a6..ec8a50e 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str::FromStr; +use shorthand::ShortHand; + use crate::attribute::AttributePairs; use crate::tags::ExtXKey; use crate::types::{ByteRange, ProtocolVersion}; @@ -14,10 +16,45 @@ use crate::{Encrypted, Error, RequiredVersion}; /// /// [`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)] +#[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[shorthand(enable(must_use, into))] pub struct ExtXMap { + /// 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"); + /// # assert_eq!( + /// # map.uri(), + /// # &"https://prod.mediaspace.com/init.bin".to_string() + /// # ); + /// map.set_uri("https://www.google.com/init.bin"); + /// + /// assert_eq!(map.uri(), &"https://www.google.com/init.bin".to_string()); + /// ``` uri: String, + /// 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))); + /// ``` + #[shorthand(enable(copy))] range: Option, + #[shorthand(enable(skip))] keys: Vec, } @@ -27,6 +64,7 @@ impl ExtXMap { /// Makes a new [`ExtXMap`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMap; /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); @@ -42,6 +80,7 @@ impl ExtXMap { /// Makes a new [`ExtXMap`] tag with the given range. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXMap; /// use hls_m3u8::types::ByteRange; @@ -58,76 +97,6 @@ impl ExtXMap { keys: vec![], } } - - /// 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(&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 { 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) -> &mut Self { - self.range = value; - self - } } impl Encrypted for ExtXMap { diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 23ccc0b..a948ee9 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -9,11 +9,13 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// /// The [`ExtXProgramDateTime`] tag associates the first sample of a /// [`Media Segment`] with an absolute date and/or time. /// /// [`Media Segment`]: crate::MediaSegment -/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 +/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: +/// https://tools.ietf.org/html/rfc8216#section-4.3.2.6 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXProgramDateTime(DateTime); @@ -23,6 +25,7 @@ impl ExtXProgramDateTime { /// Makes a new [`ExtXProgramDateTime`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXProgramDateTime; /// use chrono::{FixedOffset, TimeZone}; @@ -41,6 +44,7 @@ impl ExtXProgramDateTime { /// segment. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXProgramDateTime; /// use chrono::{FixedOffset, TimeZone}; @@ -65,6 +69,7 @@ impl ExtXProgramDateTime { /// Sets the date-time of the first sample of the associated media segment. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXProgramDateTime; /// use chrono::{FixedOffset, TimeZone}; diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 47b0f66..570c6bf 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -5,9 +5,10 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] +/// # [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// -/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1 +/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: +/// https://tools.ietf.org/html/rfc8216#section-4.3.5.1 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct ExtXIndependentSegments; @@ -15,6 +16,7 @@ impl ExtXIndependentSegments { pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS"; } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXIndependentSegments { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 42d0f07..994b4ed 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -6,7 +6,7 @@ use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; use crate::utils::{parse_yes_or_no, tag}; use crate::{Error, RequiredVersion}; -/// [4.3.5.2. EXT-X-START] +/// # [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)] @@ -21,9 +21,11 @@ impl ExtXStart { /// Makes a new [`ExtXStart`] tag. /// /// # Panic + /// /// Panics if the time_offset value is infinite. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::new(20.123456); @@ -38,9 +40,11 @@ impl ExtXStart { /// Makes a new [`ExtXStart`] tag with the given `precise` flag. /// /// # Panic + /// /// Panics if the time_offset value is infinite. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::with_precise(20.123456, true); @@ -56,6 +60,7 @@ impl ExtXStart { /// Returns the time offset of the media segments in the playlist. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::new(20.123456); @@ -66,6 +71,7 @@ impl ExtXStart { /// Sets the time offset of the media segments in the playlist. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let mut start = ExtXStart::new(20.123456); @@ -84,6 +90,7 @@ impl ExtXStart { /// presentation times are prior to the specified time offset. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::with_precise(20.123456, true); @@ -94,6 +101,7 @@ impl ExtXStart { /// Sets the `precise` flag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let mut start = ExtXStart::new(20.123456); @@ -109,6 +117,7 @@ impl ExtXStart { } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXStart { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/traits.rs b/src/traits.rs index af8c5ca..a80de7f 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -4,6 +4,7 @@ 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; @@ -76,7 +77,9 @@ pub trait Encrypted { /// Returns `true`, if the tag is encrypted. /// /// # Note + /// /// This will return `true`, if any of the keys satisfies + /// /// ```text /// key.method() != EncryptionMethod::None /// ``` @@ -92,6 +95,7 @@ pub trait Encrypted { /// Returns `false`, if the tag is not encrypted. /// /// # Note + /// /// This is the inverse of [`is_encrypted`]. /// /// [`is_encrypted`]: #method.is_encrypted @@ -99,7 +103,9 @@ pub trait Encrypted { } /// # Example +/// /// Implementing it: +/// /// ``` /// # use hls_m3u8::RequiredVersion; /// use hls_m3u8::types::ProtocolVersion; @@ -122,6 +128,7 @@ 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; diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 795514d..8ea8ddc 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str::FromStr; +use shorthand::ShortHand; + use crate::Error; /// Byte range. @@ -8,9 +10,40 @@ use crate::Error; /// See: [4.3.2.2. EXT-X-BYTERANGE] /// /// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 -#[derive(Copy, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] +#[derive(ShortHand, Copy, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] +#[shorthand(enable(must_use))] pub struct ByteRange { + /// The length of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// # + /// let mut range = ByteRange::new(20, Some(3)); + /// # assert_eq!(range.length(), 20); + /// + /// range.set_length(10); + /// assert_eq!(range.length(), 10); + /// ``` length: usize, + /// The start of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// # + /// let mut range = ByteRange::new(20, None); + /// # assert_eq!(range.start(), None); + /// + /// range.set_start(Some(3)); + /// assert_eq!(range.start(), Some(3)); + /// ``` + // + // this is a workaround until this issue is fixed: + // https://github.com/Luro02/shorthand/issues/20 + #[shorthand(enable(copy), disable(option_as_ref))] start: Option, } @@ -18,73 +51,22 @@ impl ByteRange { /// Creates a new [`ByteRange`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::ByteRange; /// ByteRange::new(22, Some(12)); /// ``` pub const fn new(length: usize, start: Option) -> Self { Self { length, start } } - - /// Returns the length of the range. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::ByteRange; - /// # - /// assert_eq!(ByteRange::new(20, Some(3)).length(), 20); - /// ``` - pub const fn length(&self) -> usize { self.length } - - /// Sets the length of the range. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::ByteRange; - /// # - /// let mut range = ByteRange::new(20, Some(3)); - /// - /// # assert_eq!(range.length(), 20); - /// range.set_length(10); - /// assert_eq!(range.length(), 10); - /// ``` - pub fn set_length(&mut self, value: usize) -> &mut Self { - self.length = value; - self - } - - /// Returns the start of the range. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::ByteRange; - /// # - /// assert_eq!(ByteRange::new(20, Some(3)).start(), Some(3)); - /// ``` - pub const fn start(&self) -> Option { self.start } - - /// Sets the start of the range. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::ByteRange; - /// # - /// let mut range = ByteRange::new(20, None); - /// - /// # assert_eq!(range.start(), None); - /// range.set_start(Some(3)); - /// assert_eq!(range.start(), Some(3)); - /// ``` - pub fn set_start(&mut self, value: Option) -> &mut Self { - self.start = value; - self - } } impl fmt::Display for ByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.length)?; - if let Some(x) = self.start { - write!(f, "@{}", x)?; + + if let Some(value) = self.start { + write!(f, "@{}", value)?; } + Ok(()) } } @@ -94,6 +76,7 @@ impl FromStr for ByteRange { fn from_str(s: &str) -> Result { let tokens = s.splitn(2, '@').collect::>(); + if tokens.is_empty() { return Err(Error::invalid_input()); } @@ -107,6 +90,7 @@ impl FromStr for ByteRange { None } }; + Ok(Self::new(length, start)) } } @@ -118,23 +102,32 @@ mod tests { #[test] fn test_display() { - let byte_range = ByteRange { - length: 0, - start: Some(5), - }; - assert_eq!(byte_range.to_string(), "0@5".to_string()); + assert_eq!( + ByteRange { + length: 0, + start: Some(5), + } + .to_string(), + "0@5".to_string() + ); - let byte_range = ByteRange { - length: 99999, - start: Some(2), - }; - assert_eq!(byte_range.to_string(), "99999@2".to_string()); + assert_eq!( + ByteRange { + length: 99999, + start: Some(2), + } + .to_string(), + "99999@2".to_string() + ); - let byte_range = ByteRange { - length: 99999, - start: None, - }; - assert_eq!(byte_range.to_string(), "99999".to_string()); + assert_eq!( + ByteRange { + length: 99999, + start: None, + } + .to_string(), + "99999".to_string() + ); } #[test] diff --git a/src/types/channels.rs b/src/types/channels.rs index d56fd01..e8136d3 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -6,13 +6,17 @@ use crate::Error; /// Specifies a list of parameters. /// /// # `MediaType::Audio` +/// /// The first parameter is a count of audio channels expressed as a [`u64`], /// indicating the maximum number of independent, simultaneous audio channels -/// present in any [`MediaSegment`] in the rendition. For example, an -/// `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute. +/// present in any [`MediaSegment`] in the rendition. +/// +/// For example, an `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute. /// /// # Example +/// /// Creating a `CHANNELS="6"` attribute +/// /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); @@ -34,6 +38,7 @@ impl Channels { /// Makes a new [`Channels`] struct. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); @@ -48,6 +53,7 @@ impl Channels { /// Returns the channel number. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); @@ -59,6 +65,7 @@ impl Channels { /// Sets the channel number. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(3); @@ -77,9 +84,10 @@ impl FromStr for Channels { fn from_str(input: &str) -> Result { let parameters = input.split('/').collect::>(); + let channel_number = parameters .first() - .ok_or_else(|| Error::missing_attribute("First parameter of channels!"))? + .ok_or_else(|| Error::missing_attribute("first parameter of channels"))? .parse() .map_err(Error::parse_int)?; @@ -93,6 +101,7 @@ impl FromStr for Channels { impl fmt::Display for Channels { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.channel_number)?; + if !self.unknown.is_empty() { write!(f, "{}", self.unknown.join(","))?; } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index af0ac36..53aa3c7 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -51,6 +51,7 @@ impl Deref for DecimalFloatingPoint { fn deref(&self) -> &Self::Target { &self.0 } } +#[doc(hidden)] impl From for DecimalFloatingPoint { fn from(value: f64) -> Self { let mut result = value; @@ -64,6 +65,7 @@ impl From for DecimalFloatingPoint { } } +#[doc(hidden)] impl From for DecimalFloatingPoint { fn from(value: f32) -> Self { f64::from(value).into() } } @@ -102,15 +104,13 @@ mod tests { #[test] pub fn test_parser() { - let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap(); assert_eq!( - decimal_floating_point, + DecimalFloatingPoint::new(22.0).unwrap(), "22".parse::().unwrap() ); - let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap(); assert_eq!( - decimal_floating_point, + DecimalFloatingPoint::new(4.1).unwrap(), "4.1".parse::().unwrap() ); diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs deleted file mode 100644 index 50e44c1..0000000 --- a/src/types/decimal_resolution.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::str::FromStr; - -use derive_more::Display; - -use crate::Error; - -/// 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 struct DecimalResolution { - width: usize, - height: usize, -} - -impl DecimalResolution { - /// Creates a new [`DecimalResolution`]. - pub const fn new(width: usize, height: usize) -> Self { Self { width, height } } - - /// Horizontal pixel dimension. - pub const fn width(&self) -> usize { self.width } - - /// Sets Horizontal pixel dimension. - pub fn set_width(&mut self, value: usize) -> &mut Self { - self.width = value; - self - } - - /// Vertical pixel dimension. - pub const fn height(&self) -> usize { self.height } - - /// Sets Vertical pixel dimension. - pub fn set_height(&mut self, value: usize) -> &mut Self { - self.height = value; - self - } -} - -/// [`DecimalResolution`] can be constructed from a tuple; `(width, height)`. -impl From<(usize, usize)> for DecimalResolution { - fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) } -} - -impl FromStr for DecimalResolution { - type Err = Error; - - fn from_str(input: &str) -> Result { - let tokens = input.splitn(2, 'x').collect::>(); - - if tokens.len() != 2 { - return Err(Error::custom(format!( - "InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}", - input, - ))); - } - - Ok(Self { - width: tokens[0].parse().map_err(Error::parse_int)?, - height: tokens[1].parse().map_err(Error::parse_int)?, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_display() { - assert_eq!( - DecimalResolution::new(1920, 1080).to_string(), - "1920x1080".to_string() - ); - - assert_eq!( - DecimalResolution::new(1280, 720).to_string(), - "1280x720".to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - DecimalResolution::new(1920, 1080), - "1920x1080".parse::().unwrap() - ); - - assert_eq!( - DecimalResolution::new(1280, 720), - "1280x720".parse::().unwrap() - ); - - assert!("1280".parse::().is_err()); - } - - #[test] - fn test_width() { - assert_eq!(DecimalResolution::new(1920, 1080).width(), 1920); - assert_eq!(DecimalResolution::new(1920, 1080).set_width(12).width(), 12); - } - - #[test] - fn test_height() { - assert_eq!(DecimalResolution::new(1920, 1080).height(), 1080); - assert_eq!( - DecimalResolution::new(1920, 1080).set_height(12).height(), - 12 - ); - } - - #[test] - fn test_from() { - assert_eq!( - DecimalResolution::from((1920, 1080)), - DecimalResolution::new(1920, 1080) - ); - } -} diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index f5933d3..526624e 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -2,36 +2,138 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; +use shorthand::ShortHand; use crate::attribute::AttributePairs; -use crate::types::{ - EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion, -}; -use crate::utils::{quote, unquote}; +use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; +use crate::utils::{parse_iv_from_str, quote, unquote}; use crate::{Error, RequiredVersion}; -#[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`] +/// A [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`] /// and [`ExtXKey`]. /// /// [`ExtXSessionKey`]: crate::tags::ExtXSessionKey /// [`ExtXKey`]: crate::tags::ExtXKey +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[builder(setter(into), build_fn(validate = "Self::validate"))] +#[shorthand(enable(must_use, into))] pub struct DecryptionKey { - /// The [EncryptionMethod]. + /// The [`EncryptionMethod`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + /// + /// # Note + /// + /// This attribute is required. + #[shorthand(enable(copy))] pub(crate) method: EncryptionMethod, - #[builder(setter(into, strip_option), default)] /// An `URI`, that specifies how to obtain the key. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_uri(Some("http://www.google.com/")); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + /// + /// # Note + /// + /// This attribute is required, if the [`EncryptionMethod`] is not `None`. + #[builder(setter(into, strip_option), default)] pub(crate) uri: Option, + /// The IV (Initialization Vector) for the key. + /// + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// # assert_eq!(key.iv(), None); + /// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])); + /// + /// assert_eq!( + /// key.iv(), + /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + /// ); + /// ``` + /// + /// # Note + /// + /// This attribute is optional. #[builder(setter(into, strip_option), default)] - /// The IV (Initialization Vector) attribute. - pub(crate) iv: Option, - #[builder(setter(into, strip_option), default)] - /// A string that specifies how the key is + // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 + #[shorthand(enable(copy), disable(option_as_ref))] + pub(crate) iv: Option<[u8; 16]>, + /// [`KeyFormat`] specifies how the key is /// represented in the resource identified by the `URI`. - pub(crate) key_format: Option, + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; + /// + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_key_format(Some(KeyFormat::Identity)); + /// + /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); + /// ``` + /// + /// # Note + /// + /// This attribute is optional. + #[builder(setter(into, strip_option), default)] + #[shorthand(enable(copy))] + pub(crate) key_format: Option, + /// The [`KeyFormatVersions`] attribute. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; + /// + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// Some(&KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) + /// ); + /// ``` + /// + /// # Note + /// + /// This attribute is optional. #[builder(setter(into, strip_option), default)] - /// The [KeyFormatVersions] attribute. pub(crate) key_format_versions: Option, } @@ -48,6 +150,7 @@ impl DecryptionKey { /// Makes a new [`DecryptionKey`]. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; @@ -64,213 +167,14 @@ impl DecryptionKey { } } - /// Returns the [`EncryptionMethod`]. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// assert_eq!(key.method(), EncryptionMethod::Aes128); - /// ``` - pub const fn method(&self) -> EncryptionMethod { self.method } - /// Returns a Builder to build a [DecryptionKey]. pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } - - /// Sets the [`EncryptionMethod`]. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() - /// ); - /// ``` - pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { - self.method = value; - self - } - - /// Returns an `URI`, that specifies how to obtain the key. - /// - /// # Note - /// This attribute is required, if the [EncryptionMethod] is not `None`. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// assert_eq!(key.uri(), &Some("https://www.example.com/".to_string())); - /// ``` - pub const fn uri(&self) -> &Option { &self.uri } - - /// Sets the `URI` attribute. - /// - /// # Note - /// This attribute is required, if the [`EncryptionMethod`] is not `None`. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_uri(Some("http://www.google.com/")); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() - /// ); - /// ``` - pub fn set_uri(&mut self, value: Option) -> &mut Self { - self.uri = value.map(|v| v.to_string()); - self - } - - /// Returns the IV (Initialization Vector) attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// # assert_eq!(key.iv(), None); - /// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])); - /// - /// assert_eq!( - /// key.iv(), - /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - /// ); - /// ``` - pub fn iv(&self) -> Option<[u8; 16]> { - if let Some(iv) = &self.iv { - Some(iv.to_slice()) - } else { - None - } - } - - /// Sets the `IV` attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607" - /// .to_string() - /// ); - /// ``` - pub fn set_iv(&mut self, value: Option) -> &mut Self - where - T: Into<[u8; 16]>, - { - self.iv = value.map(|v| InitializationVector(v.into())); - self - } - - /// Returns a string that specifies how the key is - /// represented in the resource identified by the `URI`. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format(Some(KeyFormat::Identity)); - /// - /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); - /// ``` - pub const fn key_format(&self) -> Option { self.key_format } - - /// Sets the [`KeyFormat`] attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format(Some(KeyFormat::Identity)); - /// - /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); - /// ``` - pub fn set_key_format>(&mut self, value: Option) -> &mut Self { - self.key_format = value.map(Into::into); - self - } - - /// Returns the [`KeyFormatVersions`] attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); - /// - /// assert_eq!( - /// key.key_format_versions(), - /// &Some(KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) - /// ); - /// ``` - pub const fn key_format_versions(&self) -> &Option { - &self.key_format_versions - } - - /// Sets the [`KeyFormatVersions`] attribute. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"" - /// .to_string() - /// ); - /// ``` - pub fn set_key_format_versions>( - &mut self, - value: Option, - ) -> &mut Self { - self.key_format_versions = value.map(Into::into); - self - } } +/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or +/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is +/// specified. +/// Otherwise [`ProtocolVersion::V1`] is required. impl RequiredVersion for DecryptionKey { fn required_version(&self) -> ProtocolVersion { if self.key_format.is_some() || self.key_format_versions.is_some() { @@ -305,7 +209,7 @@ impl FromStr for DecryptionKey { uri = Some(unquoted_uri); } } - "IV" => iv = Some(value.parse()?), + "IV" => iv = Some(parse_iv_from_str(value)?), "KEYFORMAT" => key_format = Some(value.parse()?), "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), _ => { @@ -338,12 +242,16 @@ impl fmt::Display for DecryptionKey { if self.method == EncryptionMethod::None { return Ok(()); } + if let Some(uri) = &self.uri { write!(f, ",URI={}", quote(uri))?; } + if let Some(value) = &self.iv { - write!(f, ",IV={}", value)?; + // TODO: use hex::encode_to_slice + write!(f, ",IV=0x{}", hex::encode(&value))?; } + if let Some(value) = &self.key_format { write!(f, ",KEYFORMAT={}", quote(value))?; } @@ -353,6 +261,7 @@ impl fmt::Display for DecryptionKey { write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?; } } + Ok(()) } } diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index a555009..acb4cc0 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -9,14 +9,15 @@ use strum::{Display, EnumString}; #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum EncryptionMethod { - /// `None` means that [MediaSegment]s are not encrypted. + /// `None` means that the [`MediaSegment`]s are not encrypted. /// /// [MediaSegment]: crate::MediaSegment None, /// `Aes128` signals that the [MediaSegment]s are completely encrypted - /// using the Advanced Encryption Standard ([AES_128]) with a 128-bit + /// using the Advanced Encryption Standard ([AES-128]) with a 128-bit /// key, Cipher Block Chaining (CBC), and /// [Public-Key Cryptography Standards #7 (PKCS7)] padding. + /// /// CBC is restarted on each segment boundary, using either the /// Initialization Vector (IV) attribute value or the Media Sequence /// Number as the IV. @@ -37,8 +38,8 @@ pub enum EncryptionMethod { /// and Enhanced [AC-3] media streams is described in the HTTP /// Live Streaming (HLS) [SampleEncryption specification]. /// - /// [MediaSegment]: crate::MediaSegment - /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [`MediaSegment`]: crate::MediaSegment + /// [AES-128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf /// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC /// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264 /// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496 diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs deleted file mode 100644 index 971bcc9..0000000 --- a/src/types/initialization_vector.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::fmt; -use std::ops::Deref; -use std::str::FromStr; - -use crate::Error; - -/// Initialization vector. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InitializationVector(pub [u8; 16]); - -impl InitializationVector { - /// Converts the [InitializationVector] to a slice. - pub const fn to_slice(&self) -> [u8; 16] { self.0 } -} - -impl From<[u8; 16]> for InitializationVector { - fn from(value: [u8; 16]) -> Self { Self(value) } -} - -impl Deref for InitializationVector { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl AsRef<[u8]> for InitializationVector { - fn as_ref(&self) -> &[u8] { &self.0 } -} - -impl fmt::Display for InitializationVector { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "0x")?; - for b in &self.0 { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl FromStr for InitializationVector { - type Err = Error; - - fn from_str(input: &str) -> Result { - if !(input.starts_with("0x") || input.starts_with("0X")) { - return Err(Error::invalid_input()); - } - if input.len() - 2 != 32 { - return Err(Error::invalid_input()); - } - - let mut result = [0; 16]; - for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() { - let d = std::str::from_utf8(c).map_err(Error::custom)?; - let b = u8::from_str_radix(d, 16).map_err(Error::custom)?; - result[i] = b; - } - - Ok(Self(result)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_display() { - assert_eq!( - "0x10ef8f758ca555115584bb5b3c687f52".to_string(), - InitializationVector([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 - ]) - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - "0x10ef8f758ca555115584bb5b3c687f52" - .parse::() - .unwrap(), - InitializationVector([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 - ]) - ); - - assert_eq!( - "0X10ef8f758ca555115584bb5b3c687f52" - .parse::() - .unwrap(), - InitializationVector([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 - ]) - ); - - assert_eq!( - "0X10EF8F758CA555115584BB5B3C687F52" - .parse::() - .unwrap(), - InitializationVector([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 - ]) - ); - - assert!("garbage".parse::().is_err()); - assert!("0xgarbage".parse::().is_err()); - assert!("0x12".parse::().is_err()); - assert!("0X10EF8F758CA555115584BB5B3C687F5Z" - .parse::() - .is_err()); - } - - #[test] - fn test_as_ref() { - assert_eq!( - InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).as_ref(), - &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - ); - } - - #[test] - fn test_deref() { - assert_eq!( - InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).deref(), - &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - ); - } - - #[test] - fn test_from() { - assert_eq!( - InitializationVector::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), - InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn test_to_slice() { - assert_eq!( - InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to_slice(), - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - ); - } -} diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 3ffac49..3995db0 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -5,9 +5,9 @@ use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] /// [`KeyFormat`] specifies, how the key is represented in the /// resource identified by the `URI`. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum KeyFormat { /// The key is a single packed array of 16 octets in binary format. Identity, diff --git a/src/types/mod.rs b/src/types/mod.rs index a1df2bd..5bcb7e9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,16 +3,15 @@ mod byte_range; mod channels; mod closed_captions; mod decimal_floating_point; -mod decimal_resolution; mod decryption_key; mod encryption_method; mod hdcp_level; mod in_stream_id; -mod initialization_vector; mod key_format; mod key_format_versions; mod media_type; mod protocol_version; +mod resolution; mod signed_decimal_floating_point; mod stream_inf; mod value; @@ -21,16 +20,15 @@ pub use byte_range::*; pub use channels::*; pub use closed_captions::*; pub(crate) use decimal_floating_point::*; -pub(crate) use decimal_resolution::*; pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; -pub use initialization_vector::*; pub use key_format::*; pub use key_format_versions::*; pub use media_type::*; pub use protocol_version::*; +pub use resolution::*; 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 1ea2a0e..5776ac1 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use crate::Error; /// # [7. Protocol Version Compatibility] +/// /// The [`ProtocolVersion`] specifies, which m3u8 revision is required, to parse /// a certain tag correctly. /// @@ -26,6 +27,7 @@ impl ProtocolVersion { /// this library. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::ProtocolVersion; /// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7); @@ -66,6 +68,7 @@ impl FromStr for ProtocolVersion { } } +/// The default is [`ProtocolVersion::V1`]. impl Default for ProtocolVersion { fn default() -> Self { Self::V1 } } diff --git a/src/types/resolution.rs b/src/types/resolution.rs new file mode 100644 index 0000000..b4ad382 --- /dev/null +++ b/src/types/resolution.rs @@ -0,0 +1,104 @@ +use std::str::FromStr; + +use derive_more::Display; +use shorthand::ShortHand; + +use crate::Error; + +/// This is a simple wrapper type for the display resolution. +/// +/// For example Full HD has a resolution of 1920x1080. +/// +/// See: [4.2. Attribute Lists] +/// +/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 +#[derive(ShortHand, Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)] +#[display(fmt = "{}x{}", width, height)] +#[shorthand(enable(must_use))] +pub struct Resolution { + /// Horizontal pixel dimension. + width: usize, + /// Vertical pixel dimension. + height: usize, +} + +impl Resolution { + /// Creates a new [`Resolution`]. + pub const fn new(width: usize, height: usize) -> Self { Self { width, height } } +} + +/// A [`Resolution`] can be constructed from a tuple `(width, height)`. +impl From<(usize, usize)> for Resolution { + fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) } +} + +impl FromStr for Resolution { + type Err = Error; + + fn from_str(input: &str) -> Result { + let tokens = input.splitn(2, 'x').collect::>(); + + if tokens.len() != 2 { + return Err(Error::custom(format!( + "InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}", + input, + ))); + } + + Ok(Self { + width: tokens[0].parse().map_err(Error::parse_int)?, + height: tokens[1].parse().map_err(Error::parse_int)?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_display() { + assert_eq!( + Resolution::new(1920, 1080).to_string(), + "1920x1080".to_string() + ); + + assert_eq!( + Resolution::new(1280, 720).to_string(), + "1280x720".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + Resolution::new(1920, 1080), + "1920x1080".parse::().unwrap() + ); + + assert_eq!( + Resolution::new(1280, 720), + "1280x720".parse::().unwrap() + ); + + assert!("1280".parse::().is_err()); + } + + #[test] + fn test_width() { + assert_eq!(Resolution::new(1920, 1080).width(), 1920); + assert_eq!(Resolution::new(1920, 1080).set_width(12).width(), 12); + } + + #[test] + fn test_height() { + assert_eq!(Resolution::new(1920, 1080).height(), 1080); + assert_eq!(Resolution::new(1920, 1080).set_height(12).height(), 12); + } + + #[test] + fn test_from() { + assert_eq!(Resolution::from((1920, 1080)), Resolution::new(1920, 1080)); + } +} diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 54a58e5..93cbb0c 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -13,6 +13,7 @@ impl SignedDecimalFloatingPoint { /// 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() || value.is_nan() { diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index afe462e..15d1af3 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -2,44 +2,133 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; +use shorthand::ShortHand; use crate::attribute::AttributePairs; -use crate::types::{DecimalResolution, HdcpLevel}; +use crate::types::{HdcpLevel, Resolution}; use crate::utils::{quote, unquote}; use crate::Error; /// # [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(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] +#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] #[builder(setter(into, strip_option))] #[builder(derive(Debug, PartialEq))] +#[shorthand(enable(must_use, into))] pub struct StreamInf { - /// The maximum bandwidth of the stream. + /// The peak segment bit rate of the variant stream. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamInf; + /// # + /// let mut stream = StreamInf::new(20); + /// + /// stream.set_bandwidth(5); + /// assert_eq!(stream.bandwidth(), 5); + /// ``` + /// + /// # Note + /// + /// This field is required. + #[shorthand(disable(into))] bandwidth: u64, - #[builder(default)] /// The average bandwidth of the stream. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamInf; + /// # + /// let mut stream = StreamInf::new(20); + /// + /// stream.set_average_bandwidth(Some(300)); + /// assert_eq!(stream.average_bandwidth(), Some(300)); + /// ``` + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] + #[shorthand(enable(copy), disable(into, option_as_ref))] average_bandwidth: Option, + /// A string that represents the list of codec types contained the variant + /// stream. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamInf; + /// # + /// let mut stream = StreamInf::new(20); + /// + /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + /// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string())); + /// ``` + /// + /// # Note + /// + /// This field is optional. #[builder(default)] - /// Every media format in any of the renditions specified by the Variant - /// Stream. codecs: Option, - #[builder(default)] /// The resolution of the stream. - resolution: Option, + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamInf; + /// use hls_m3u8::types::Resolution; + /// + /// let mut stream = StreamInf::new(20); + /// + /// stream.set_resolution(Some((1920, 1080))); + /// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080))); + /// # stream.set_resolution(Some((1280, 10))); + /// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10))); + /// ``` + /// + /// # Note + /// + /// This field is optional. #[builder(default)] - /// High-bandwidth Digital Content Protection + #[shorthand(enable(copy))] + resolution: Option, + /// High-bandwidth Digital Content Protection level of the variant stream. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::{HdcpLevel, StreamInf}; + /// # + /// let mut stream = StreamInf::new(20); + /// + /// stream.set_hdcp_level(Some(HdcpLevel::None)); + /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); + /// ``` + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] + #[shorthand(enable(copy), disable(into))] hdcp_level: Option, - #[builder(default)] /// It indicates the set of video renditions, that should be used when /// playing the presentation. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] video: Option, } impl StreamInf { /// Creates a new [`StreamInf`]. /// - /// # Examples + /// # Example + /// /// ``` /// # use hls_m3u8::types::StreamInf; /// # @@ -55,183 +144,6 @@ impl StreamInf { video: None, } } - - /// Returns the peak segment bit rate of the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// assert_eq!(stream.bandwidth(), 20); - /// ``` - pub const fn bandwidth(&self) -> u64 { self.bandwidth } - - /// Sets the peak segment bit rate of the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_bandwidth(5); - /// assert_eq!(stream.bandwidth(), 5); - /// ``` - pub fn set_bandwidth(&mut self, value: u64) -> &mut Self { - self.bandwidth = value; - self - } - - /// Returns the group identifier for the video in the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// assert_eq!(stream.video(), &None); - /// ``` - pub const fn video(&self) -> &Option { &self.video } - - /// Sets the group identifier for the video in the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_video(Some("video")); - /// assert_eq!(stream.video(), &Some("video".to_string())); - /// ``` - pub fn set_video(&mut self, value: Option) -> &mut Self { - self.video = value.map(|v| v.to_string()); - self - } - - /// Returns the average segment bit rate of the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// assert_eq!(stream.average_bandwidth(), None); - /// ``` - pub const fn average_bandwidth(&self) -> Option { self.average_bandwidth } - - /// Sets the average segment bit rate of the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_average_bandwidth(Some(300)); - /// assert_eq!(stream.average_bandwidth(), Some(300)); - /// ``` - pub fn set_average_bandwidth(&mut self, value: Option) -> &mut Self { - self.average_bandwidth = value; - self - } - - /// A string that represents the list of codec types contained the variant - /// stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// assert_eq!(stream.codecs(), &None); - /// ``` - pub const fn codecs(&self) -> &Option { &self.codecs } - - /// A string that represents the list of codec types contained the variant - /// stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - /// assert_eq!(stream.codecs(), &Some("mp4a.40.2,avc1.4d401e".to_string())); - /// ``` - pub fn set_codecs(&mut self, value: Option) -> &mut Self { - self.codecs = value.map(|v| v.to_string()); - self - } - - /// Returns the resolution of the stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// assert_eq!(stream.resolution(), None); - /// ``` - pub fn resolution(&self) -> Option<(usize, usize)> { - if let Some(res) = &self.resolution { - Some((res.width(), res.height())) - } else { - None - } - } - - /// Sets the resolution of the stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_resolution(1920, 1080); - /// assert_eq!(stream.resolution(), Some((1920, 1080))); - /// # stream.set_resolution(1280, 10); - /// # assert_eq!(stream.resolution(), Some((1280, 10))); - /// ``` - pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self { - if let Some(res) = &mut self.resolution { - res.set_width(width); - res.set_height(height); - } else { - self.resolution = Some(DecimalResolution::new(width, height)); - } - self - } - - /// The HDCP level of the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// assert_eq!(stream.hdcp_level(), None); - /// ``` - pub const fn hdcp_level(&self) -> Option { self.hdcp_level } - - /// The HDCP level of the variant stream. - /// - /// # Examples - /// ``` - /// # use hls_m3u8::types::{HdcpLevel, StreamInf}; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_hdcp_level(Some(HdcpLevel::None)); - /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); - /// ``` - pub fn set_hdcp_level>(&mut self, value: Option) -> &mut Self { - self.hdcp_level = value.map(Into::into); - self - } } impl fmt::Display for StreamInf { @@ -311,7 +223,7 @@ mod tests { let mut stream_inf = StreamInf::new(200); stream_inf.set_average_bandwidth(Some(15)); stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - stream_inf.set_resolution(1920, 1080); + stream_inf.set_resolution(Some((1920, 1080))); stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); stream_inf.set_video(Some("video")); @@ -332,7 +244,7 @@ mod tests { let mut stream_inf = StreamInf::new(200); stream_inf.set_average_bandwidth(Some(15)); stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - stream_inf.set_resolution(1920, 1080); + stream_inf.set_resolution(Some((1920, 1080))); stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); stream_inf.set_video(Some("video")); diff --git a/src/utils.rs b/src/utils.rs index d21d9e6..e794544 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -32,6 +32,29 @@ impl_from![ u8, i8, u16, i16, u32, i32, f32, f64 => crate::types::SignedDecimalFloatingPoint ]; +pub(crate) fn parse_iv_from_str(input: &str) -> crate::Result<[u8; 16]> { + if !(input.starts_with("0x") || input.starts_with("0X")) { + return Err(Error::invalid_input()); + } + + if input.len() - 2 != 32 { + return Err(Error::invalid_input()); + } + + let mut result = [0; 16]; + + // TODO: + // hex::decode_to_slice(value.as_bytes()[2..], &mut result)?; + + for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() { + let d = core::str::from_utf8(c).map_err(Error::custom)?; + let b = u8::from_str_radix(d, 16).map_err(Error::custom)?; + result[i] = b; + } + + Ok(result) +} + pub(crate) fn parse_yes_or_no>(s: T) -> crate::Result { match s.as_ref() { "YES" => Ok(true), From 006f36ff47f1417dbd5678c198d06ae07bc48729 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 2 Feb 2020 13:50:56 +0100 Subject: [PATCH 007/112] collect unsupported tags #36 closes #36 --- src/master_playlist.rs | 15 ++++++++++++++- src/media_playlist.rs | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index b081256..ce56b7c 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -68,6 +68,13 @@ pub struct MasterPlaylist { /// This tag is optional. #[builder(default)] session_key_tags: Vec, + /// A list of tags that are unknown. + /// + /// # Note + /// + /// This tag is optional. + #[builder(default)] + unknown_tags: Vec, } impl MasterPlaylist { @@ -244,6 +251,10 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", value)?; } + for t in &self.unknown_tags { + writeln!(f, "{}", t)?; + } + Ok(()) } } @@ -259,6 +270,7 @@ impl FromStr for MasterPlaylist { let mut i_frame_stream_inf_tags = vec![]; let mut session_data_tags = vec![]; let mut session_key_tags = vec![]; + let mut unknown_tags = vec![]; for (i, line) in input.parse::()?.into_iter().enumerate() { match line { @@ -320,7 +332,7 @@ impl FromStr for MasterPlaylist { _ => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. - // TODO: collect custom tags + unknown_tags.push(tag.to_string()); } } } @@ -335,6 +347,7 @@ impl FromStr for MasterPlaylist { builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags); builder.session_data_tags(session_data_tags); builder.session_key_tags(session_key_tags); + builder.unknown_tags(unknown_tags); builder.build().map_err(Error::builder) } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index f0c5bab..680d6c5 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -98,6 +98,13 @@ pub struct MediaPlaylist { /// `Duration::from_secs(0)`. #[builder(default = "Duration::from_secs(0)")] allowable_excess_duration: Duration, + /// A list of unknown tags. + /// + /// # Note + /// + /// This field is optional. + #[builder(default)] + unknown_tags: Vec, } impl MediaPlaylistBuilder { @@ -257,6 +264,10 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", value)?; } + for value in &self.unknown_tags { + writeln!(f, "{}", value)?; + } + Ok(()) } } @@ -270,6 +281,7 @@ fn parse_media_playlist( let mut has_partial_segment = false; let mut has_discontinuity_tag = false; + let mut unknown_tags = vec![]; let mut available_key_tags: Vec = vec![]; @@ -369,9 +381,11 @@ fn parse_media_playlist( Tag::ExtXStart(t) => { builder.start_tag(t); } - Tag::Unknown(_) | Tag::ExtXVersion(_) => { + Tag::ExtXVersion(_) => {} + Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. + unknown_tags.push(tag.to_string()); } } } @@ -389,6 +403,7 @@ fn parse_media_playlist( return Err(Error::invalid_input()); } + builder.unknown_tags(unknown_tags); builder.segments(segments); builder.build().map_err(Error::builder) } From e6a1103d247b4d1286db476cd815409896e2033e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 2 Feb 2020 14:33:57 +0100 Subject: [PATCH 008/112] rewrite `Lines` to reduce allocations --- src/lib.rs | 7 ++- src/line.rs | 103 +++++++++++++++++++---------------------- src/master_playlist.rs | 4 +- src/media_playlist.rs | 4 +- src/media_segment.rs | 2 +- 5 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 45b1bc5..fc1726f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,12 @@ clippy::nursery, clippy::cargo )] -#![allow(clippy::multiple_crate_versions, clippy::must_use_candidate)] +#![allow( + clippy::multiple_crate_versions, + clippy::must_use_candidate, + clippy::module_name_repetitions, + clippy::default_trait_access +)] #![warn( missing_docs, missing_copy_implementations, diff --git a/src/line.rs b/src/line.rs index f36782a..51b0de3 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,93 +1,86 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::tags; use crate::Error; -#[derive(Debug, Default)] -pub struct Lines(Vec); - -impl Lines { - pub fn new() -> Self { Self::default() } +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub(crate) struct Lines<'a> { + buffer: &'a str, + // the line at which the iterator currently is + position: usize, } -impl FromStr for Lines { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut result = Self::new(); +impl<'a> Iterator for Lines<'a> { + type Item = crate::Result; + fn next(&mut self) -> Option { let mut stream_inf = false; let mut stream_inf_line = None; - for l in input.lines() { - let raw_line = l.trim(); + for line in self.buffer.lines().skip(self.position) { + let line = line.trim(); + self.position += 1; - if raw_line.is_empty() { + if line.is_empty() { continue; } - let line = { - if raw_line.starts_with(tags::ExtXStreamInf::PREFIX) { - stream_inf = true; - stream_inf_line = Some(raw_line); + if line.starts_with(tags::ExtXStreamInf::PREFIX) { + stream_inf = true; + stream_inf_line = Some(line); - continue; - } 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, raw_line).parse()?); - stream_inf_line = None; - res - } else { - continue; + continue; + } else if line.starts_with("#EXT") { + match line.parse() { + Ok(value) => return Some(Ok(Line::Tag(value))), + Err(e) => return Some(Err(e)), + } + } else if 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 { + match format!("{}\n{}", first_line, line).parse() { + Ok(value) => { + return Some(Ok(Line::Tag(value))); + } + Err(e) => return Some(Err(e)), } } else { - Line::Uri(raw_line.to_string()) + continue; } + } else { + return Some(Ok(Line::Uri(line.to_string()))); } - }; - - result.push(line); + } } - Ok(result) + None } } -impl IntoIterator for Lines { - type IntoIter = ::std::vec::IntoIter; - type Item = Line; - - fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } -} - -impl Deref for Lines { - type Target = Vec; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Lines { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +impl<'a> From<&'a str> for Lines<'a> { + fn from(buffer: &'a str) -> Self { + Self { + buffer, + position: 0, + } + } } #[derive(Debug, Clone, PartialEq)] -pub enum Line { +pub(crate) enum Line { Tag(Tag), Uri(String), } #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] -pub enum Tag { +pub(crate) enum Tag { ExtM3u(tags::ExtM3u), ExtXVersion(tags::ExtXVersion), ExtInf(tags::ExtInf), diff --git a/src/master_playlist.rs b/src/master_playlist.rs index ce56b7c..d21ef1a 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -272,8 +272,8 @@ impl FromStr for MasterPlaylist { let mut session_key_tags = vec![]; let mut unknown_tags = vec![]; - for (i, line) in input.parse::()?.into_iter().enumerate() { - match line { + for (i, line) in Lines::from(input).enumerate() { + match line? { Line::Tag(tag) => { if i == 0 { if tag != Tag::ExtM3u(ExtM3u) { diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 680d6c5..cf8b05d 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -285,8 +285,8 @@ fn parse_media_playlist( let mut available_key_tags: Vec = vec![]; - for (i, line) in input.parse::()?.into_iter().enumerate() { - match line { + for (i, line) in Lines::from(input).enumerate() { + match line? { Line::Tag(tag) => { if i == 0 { if tag != Tag::ExtM3u(ExtM3u) { diff --git a/src/media_segment.rs b/src/media_segment.rs index 803086d..ec7889c 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -14,7 +14,7 @@ use crate::{Encrypted, RequiredVersion}; #[builder(setter(into, strip_option))] #[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MediaSegment { - /// Sets all [`ExtXKey`] tags. + /// All [`ExtXKey`] tags. #[builder(default)] keys: Vec, /// The [`ExtXMap`] tag associated with the media segment. From 2471737455fb8d83a21edeeb6d0dc893a1a52c3b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 2 Feb 2020 15:23:47 +0100 Subject: [PATCH 009/112] update dependencies --- Cargo.toml | 14 +++++++------- src/tags/master_playlist/i_frame_stream_inf.rs | 17 +++++------------ src/tags/master_playlist/session_key.rs | 15 +++------------ src/tags/master_playlist/stream_inf.rs | 16 ++++------------ src/tags/media_playlist/target_duration.rs | 11 +++-------- src/tags/media_segment/byte_range.rs | 15 +++------------ src/tags/media_segment/key.rs | 15 +++------------ src/tags/media_segment/program_date_time.rs | 15 +++------------ src/types/decimal_floating_point.rs | 11 ++--------- src/types/key_format_versions.rs | 15 +++------------ src/types/signed_decimal_floating_point.rs | 10 ++-------- 11 files changed, 38 insertions(+), 116 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2868525..62db739 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,13 +17,13 @@ codecov = { repository = "sile/hls_m3u8" } [dependencies] thiserror = "1.0" -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" +derive_builder = "0.9" +chrono = "0.4" +strum = { version = "0.17", features = ["derive"] } +derive_more = "0.99" +hex = "0.4" shorthand = "0.1" [dev-dependencies] -clap = "2.33.0" -pretty_assertions = "0.6.1" +clap = "2.33" +pretty_assertions = "0.6" diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 6fee28c..1a7f749 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -1,7 +1,8 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; + use crate::attribute::AttributePairs; use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder}; use crate::utils::{quote, tag, unquote}; @@ -18,9 +19,11 @@ use crate::{Error, RequiredVersion}; /// [`Master Playlist`]: crate::MasterPlaylist /// [`Media Playlist`]: crate::MediaPlaylist /// [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)] +#[derive(Deref, DerefMut, PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { uri: String, + #[deref] + #[deref_mut] stream_inf: StreamInf, } @@ -180,16 +183,6 @@ impl FromStr for ExtXIFrameStreamInf { } } -impl Deref for ExtXIFrameStreamInf { - type Target = StreamInf; - - fn deref(&self) -> &Self::Target { &self.stream_inf } -} - -impl DerefMut for ExtXIFrameStreamInf { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream_inf } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 0f23184..a46b015 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,7 +1,8 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; + use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -16,7 +17,7 @@ use crate::{Error, RequiredVersion}; /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXSessionKey(DecryptionKey); impl ExtXSessionKey { @@ -74,16 +75,6 @@ impl FromStr for ExtXSessionKey { } } -impl Deref for ExtXSessionKey { - type Target = DecryptionKey; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for ExtXSessionKey { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index df7b350..fa51342 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -1,7 +1,7 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; use shorthand::ShortHand; use crate::attribute::AttributePairs; @@ -23,7 +23,7 @@ use crate::{Error, RequiredVersion}; /// 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(ShortHand, PartialOrd, Debug, Clone, PartialEq)] +#[derive(Deref, DerefMut, ShortHand, PartialOrd, Debug, Clone, PartialEq)] #[shorthand(enable(must_use, into))] pub struct ExtXStreamInf { /// The `URI` that identifies the associated media playlist. @@ -37,6 +37,8 @@ pub struct ExtXStreamInf { /// The value of the [`ClosedCaptions`] attribute. closed_captions: Option, #[shorthand(enable(skip))] + #[deref] + #[deref_mut] stream_inf: StreamInf, } @@ -249,16 +251,6 @@ impl FromStr for ExtXStreamInf { } } -impl Deref for ExtXStreamInf { - type Target = StreamInf; - - fn deref(&self) -> &Self::Target { &self.stream_inf } -} - -impl DerefMut for ExtXStreamInf { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream_inf } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 0165154..16c631b 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -1,8 +1,9 @@ use std::fmt; -use std::ops::Deref; use std::str::FromStr; use std::time::Duration; +use derive_more::Deref; + use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -15,7 +16,7 @@ use crate::{Error, RequiredVersion}; /// [`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)] +#[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] pub struct ExtXTargetDuration(Duration); impl ExtXTargetDuration { @@ -57,12 +58,6 @@ 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()) diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 3a04489..9af41ca 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,7 +1,8 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; + use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -14,7 +15,7 @@ use crate::{Error, RequiredVersion}; /// [`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, PartialOrd, Ord)] +#[derive(Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { @@ -51,16 +52,6 @@ impl RequiredVersion for ExtXByteRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } -impl Deref for ExtXByteRange { - type Target = ByteRange; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for ExtXByteRange { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index b875861..acf3e02 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,7 +1,8 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; + use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -23,7 +24,7 @@ use crate::{Error, RequiredVersion}; /// [`ExtXMap`]: crate::tags::ExtXMap /// [`Media Segment`]: crate::MediaSegment /// [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)] +#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXKey(DecryptionKey); impl ExtXKey { @@ -105,16 +106,6 @@ impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } } -impl Deref for ExtXKey { - type Target = DecryptionKey; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for ExtXKey { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index a948ee9..ff91b00 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,8 +1,8 @@ use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; use chrono::{DateTime, FixedOffset, SecondsFormat}; +use derive_more::{Deref, DerefMut}; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -16,7 +16,7 @@ use crate::{Error, RequiredVersion}; /// [`Media Segment`]: crate::MediaSegment /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: /// https://tools.ietf.org/html/rfc8216#section-4.3.2.6 -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Deref, DerefMut, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXProgramDateTime(DateTime); impl ExtXProgramDateTime { @@ -124,20 +124,11 @@ impl FromStr for ExtXProgramDateTime { } } -impl Deref for ExtXProgramDateTime { - type Target = DateTime; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for ExtXProgramDateTime { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - #[cfg(test)] mod test { use super::*; use chrono::{Datelike, TimeZone}; + use core::ops::DerefMut; use pretty_assertions::assert_eq; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 53aa3c7..454fb66 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -1,7 +1,6 @@ -use core::ops::Deref; use core::str::FromStr; -use derive_more::Display; +use derive_more::{Deref, Display}; use crate::Error; @@ -11,7 +10,7 @@ use crate::Error; /// /// [4.2. Attribute Lists]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.2 -#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)] +#[derive(Deref, Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)] pub(crate) struct DecimalFloatingPoint(f64); impl DecimalFloatingPoint { @@ -45,12 +44,6 @@ impl FromStr for DecimalFloatingPoint { } } -impl Deref for DecimalFloatingPoint { - type Target = f64; - - fn deref(&self) -> &Self::Target { &self.0 } -} - #[doc(hidden)] impl From for DecimalFloatingPoint { fn from(value: f64) -> Self { diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 2b4fc95..7fada99 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -1,8 +1,9 @@ use std::convert::Infallible; use std::fmt; -use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; + use crate::types::ProtocolVersion; use crate::utils::{quote, unquote}; use crate::RequiredVersion; @@ -12,7 +13,7 @@ use crate::RequiredVersion; /// [`KeyFormat`] is defined. /// /// [`KeyFormat`]: crate::types::KeyFormat -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[derive(Deref, DerefMut, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct KeyFormatVersions(Vec); impl KeyFormatVersions { @@ -37,16 +38,6 @@ impl Default for KeyFormatVersions { fn default() -> Self { Self(vec![1]) } } -impl Deref for KeyFormatVersions { - type Target = Vec; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for KeyFormatVersions { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - /// This tag requires [`ProtocolVersion::V5`]. impl RequiredVersion for KeyFormatVersions { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 93cbb0c..62aa32a 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,4 +1,4 @@ -use core::ops::Deref; +use derive_more::Deref; use derive_more::{Display, FromStr}; /// Signed decimal floating-point number. @@ -6,7 +6,7 @@ use derive_more::{Display, FromStr}; /// See: [4.2. Attribute Lists] /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)] +#[derive(Deref, Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)] pub(crate) struct SignedDecimalFloatingPoint(f64); impl SignedDecimalFloatingPoint { @@ -28,12 +28,6 @@ impl SignedDecimalFloatingPoint { pub const fn as_f64(self) -> f64 { self.0 } } -impl Deref for SignedDecimalFloatingPoint { - type Target = f64; - - fn deref(&self) -> &Self::Target { &self.0 } -} - #[cfg(test)] mod tests { use super::*; From aae380954519c85d6d8eec6df3103edb36fe047e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 6 Feb 2020 12:24:40 +0100 Subject: [PATCH 010/112] remove `Copy` trait from Lines Copy should not be implemented for types that implement Iterator, because this would be confusing. https://rust-lang.github.io/rust-clippy/master/#copy_iterator --- src/line.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/line.rs b/src/line.rs index 51b0de3..26dae79 100644 --- a/src/line.rs +++ b/src/line.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use crate::tags; use crate::Error; -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] pub(crate) struct Lines<'a> { buffer: &'a str, // the line at which the iterator currently is From 1b0eb562246855769dc6f1a0bc803fac0088f60e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 6 Feb 2020 12:27:48 +0100 Subject: [PATCH 011/112] remove unnecessary allocations --- src/line.rs | 46 ++++++++++++---------------- src/master_playlist.rs | 16 +++------- src/media_playlist.rs | 12 +++----- src/tags/basic/m3u.rs | 21 +------------ src/tags/basic/mod.rs | 2 +- src/tags/media_segment/byte_range.rs | 18 +---------- src/tags/media_segment/inf.rs | 29 +++++------------- src/types/byte_range.rs | 21 +++++-------- src/types/channels.rs | 6 ++-- src/types/resolution.rs | 22 ++++++------- 10 files changed, 59 insertions(+), 134 deletions(-) diff --git a/src/line.rs b/src/line.rs index 26dae79..90cb3b4 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::fmt; use std::str::FromStr; @@ -12,7 +13,7 @@ pub(crate) struct Lines<'a> { } impl<'a> Iterator for Lines<'a> { - type Item = crate::Result; + type Item = crate::Result>; fn next(&mut self) -> Option { let mut stream_inf = false; @@ -32,10 +33,7 @@ impl<'a> Iterator for Lines<'a> { continue; } else if line.starts_with("#EXT") { - match line.parse() { - Ok(value) => return Some(Ok(Line::Tag(value))), - Err(e) => return Some(Err(e)), - } + return Some(Tag::try_from(line).map(Line::Tag)); } else if line.starts_with('#') { continue; // ignore comments } else { @@ -44,17 +42,15 @@ impl<'a> Iterator for Lines<'a> { stream_inf = false; if let Some(first_line) = stream_inf_line { - match format!("{}\n{}", first_line, line).parse() { - Ok(value) => { - return Some(Ok(Line::Tag(value))); - } - Err(e) => return Some(Err(e)), - } + return Some( + tags::ExtXStreamInf::from_str(&format!("{}\n{}", first_line, line)) + .map(|v| Line::Tag(Tag::ExtXStreamInf(v))), + ); } else { continue; } } else { - return Some(Ok(Line::Uri(line.to_string()))); + return Some(Ok(Line::Uri(line))); } } } @@ -73,15 +69,14 @@ impl<'a> From<&'a str> for Lines<'a> { } #[derive(Debug, Clone, PartialEq)] -pub(crate) enum Line { - Tag(Tag), - Uri(String), +pub(crate) enum Line<'a> { + Tag(Tag<'a>), + Uri(&'a str), } #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] -pub(crate) enum Tag { - ExtM3u(tags::ExtM3u), +pub(crate) enum Tag<'a> { ExtXVersion(tags::ExtXVersion), ExtInf(tags::ExtInf), ExtXByteRange(tags::ExtXByteRange), @@ -103,13 +98,12 @@ pub(crate) enum Tag { ExtXSessionKey(tags::ExtXSessionKey), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), - Unknown(String), + Unknown(&'a str), } -impl fmt::Display for Tag { +impl<'a> fmt::Display for Tag<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { - Self::ExtM3u(value) => value.fmt(f), Self::ExtXVersion(value) => value.fmt(f), Self::ExtInf(value) => value.fmt(f), Self::ExtXByteRange(value) => value.fmt(f), @@ -136,13 +130,11 @@ impl fmt::Display for Tag { } } -impl FromStr for Tag { - type Err = Error; +impl<'a> TryFrom<&'a str> for Tag<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { - if input.starts_with(tags::ExtM3u::PREFIX) { - input.parse().map(Self::ExtM3u) - } else if input.starts_with(tags::ExtXVersion::PREFIX) { + fn try_from(input: &'a str) -> Result { + if input.starts_with(tags::ExtXVersion::PREFIX) { input.parse().map(Self::ExtXVersion) } else if input.starts_with(tags::ExtInf::PREFIX) { input.parse().map(Self::ExtInf) @@ -185,7 +177,7 @@ impl FromStr for Tag { } else if input.starts_with(tags::ExtXStart::PREFIX) { input.parse().map(Self::ExtXStart) } else { - Ok(Self::Unknown(input.to_string())) + Ok(Self::Unknown(input)) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index d21ef1a..cb88465 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -11,6 +11,7 @@ use crate::tags::{ ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; +use crate::utils::tag; use crate::{Error, RequiredVersion}; /// Master playlist. @@ -263,6 +264,7 @@ impl FromStr for MasterPlaylist { type Err = Error; fn from_str(input: &str) -> Result { + let input = tag(input, ExtM3u::PREFIX)?; let mut builder = Self::builder(); let mut media_tags = vec![]; @@ -272,23 +274,15 @@ impl FromStr for MasterPlaylist { let mut session_key_tags = vec![]; let mut unknown_tags = vec![]; - for (i, line) in Lines::from(input).enumerate() { + for line in Lines::from(input) { match line? { Line::Tag(tag) => { - if i == 0 { - if tag != Tag::ExtM3u(ExtM3u) { - return Err(Error::invalid_input()); - } - continue; - } match tag { - Tag::ExtM3u(_) => { - return Err(Error::invalid_input()); - } Tag::ExtXVersion(_) => { // This tag can be ignored, because the // MasterPlaylist will automatically set the - // ExtXVersion tag to correct version! + // ExtXVersion tag to the minimum required version + // TODO: this might be verified? } Tag::ExtInf(_) | Tag::ExtXByteRange(_) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index cf8b05d..ea9bf53 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -12,6 +12,7 @@ use crate::tags::{ ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; use crate::types::ProtocolVersion; +use crate::utils::tag; use crate::{Encrypted, Error, RequiredVersion}; /// Media playlist. @@ -276,6 +277,8 @@ fn parse_media_playlist( input: &str, builder: &mut MediaPlaylistBuilder, ) -> crate::Result { + let input = tag(input, "#EXTM3U")?; + let mut segment = MediaSegment::builder(); let mut segments = vec![]; @@ -285,17 +288,10 @@ fn parse_media_playlist( let mut available_key_tags: Vec = vec![]; - for (i, line) in Lines::from(input).enumerate() { + for line in Lines::from(input) { match line? { Line::Tag(tag) => { - if i == 0 { - if tag != Tag::ExtM3u(ExtM3u) { - return Err(Error::custom("m3u8 doesn't start with #EXTM3U")); - } - continue; - } match tag { - Tag::ExtM3u(_) => return Err(Error::invalid_input()), Tag::ExtInf(t) => { has_partial_segment = true; segment.inf_tag(t); diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index f930ddc..b23032e 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -11,31 +11,12 @@ use crate::{Error, RequiredVersion}; /// Playlist file. /// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. /// -/// # Examples -/// -/// Parsing from a [`str`]: -/// -/// ``` -/// # use hls_m3u8::tags::ExtM3u; -/// # -/// assert_eq!("#EXTM3U".parse::()?, ExtM3u); -/// # Ok::<(), Box>(()) -/// ``` -/// -/// Converting to a [`str`]: -/// -/// ``` -/// # use hls_m3u8::tags::ExtM3u; -/// # -/// assert_eq!("#EXTM3U".to_string(), ExtM3u.to_string()); -/// ``` -/// /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist /// [`M3U`]: https://en.wikipedia.org/wiki/M3U /// [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; +pub(crate) struct ExtM3u; impl ExtM3u { pub(crate) const PREFIX: &'static str = "#EXTM3U"; diff --git a/src/tags/basic/mod.rs b/src/tags/basic/mod.rs index e23eb19..642560b 100644 --- a/src/tags/basic/mod.rs +++ b/src/tags/basic/mod.rs @@ -1,5 +1,5 @@ mod m3u; mod version; -pub use m3u::*; +pub(crate) use m3u::*; pub use version::*; diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 9af41ca..ce1b733 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -66,23 +66,7 @@ impl FromStr for ExtXByteRange { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - let tokens = input.splitn(2, '@').collect::>(); - - if tokens.is_empty() { - return Err(Error::invalid_input()); - } - - let length = tokens[0].parse()?; - - let start = { - if tokens.len() == 2 { - Some(tokens[1].parse()?) - } else { - None - } - }; - - Ok(Self::new(length, start)) + Ok(Self(ByteRange::from_str(input)?)) } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 60af89d..39d4591 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -151,29 +151,14 @@ impl FromStr for ExtInf { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - let tokens = input.splitn(2, ',').collect::>(); + let mut input = tag(input, Self::PREFIX)?.splitn(2, ','); - if tokens.is_empty() { - return Err(Error::custom(format!( - "failed to parse #EXTINF tag, couldn't split input: {:?}", - input - ))); - } - - let duration = Duration::from_secs_f64(tokens[0].parse()?); - - let title = { - if tokens.len() >= 2 { - if tokens[1].trim().is_empty() { - None - } else { - Some(tokens[1].to_string()) - } - } else { - None - } - }; + let duration = Duration::from_secs_f64(input.next().unwrap().parse()?); + let title = input + .next() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + .map(|value| value.to_string()); Ok(Self { duration, title }) } diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 8ea8ddc..7d07045 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -74,22 +74,15 @@ impl fmt::Display for ByteRange { impl FromStr for ByteRange { type Err = Error; - fn from_str(s: &str) -> Result { - let tokens = s.splitn(2, '@').collect::>(); + fn from_str(input: &str) -> Result { + let mut input = input.splitn(2, '@'); - if tokens.is_empty() { - return Err(Error::invalid_input()); - } + let length = input + .next() + .ok_or_else(|| Error::custom("missing length for #EXT-X-BYTERANGE")) + .and_then(|s| s.parse().map_err(Error::parse_int))?; - let length = tokens[0].parse()?; - - let start = { - if tokens.len() == 2 { - Some(tokens[1].parse()?) - } else { - None - } - }; + let start = input.next().map(str::parse).transpose()?; Ok(Self::new(length, start)) } diff --git a/src/types/channels.rs b/src/types/channels.rs index e8136d3..56c1520 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -83,17 +83,17 @@ impl FromStr for Channels { type Err = Error; fn from_str(input: &str) -> Result { - let parameters = input.split('/').collect::>(); + let mut parameters = input.split('/'); let channel_number = parameters - .first() + .next() .ok_or_else(|| Error::missing_attribute("first parameter of channels"))? .parse() .map_err(Error::parse_int)?; Ok(Self { channel_number, - unknown: parameters[1..].iter().map(|v| v.to_string()).collect(), + unknown: parameters.map(|v| (*v).to_string()).collect(), }) } } diff --git a/src/types/resolution.rs b/src/types/resolution.rs index b4ad382..26ad26e 100644 --- a/src/types/resolution.rs +++ b/src/types/resolution.rs @@ -36,19 +36,19 @@ impl FromStr for Resolution { type Err = Error; fn from_str(input: &str) -> Result { - let tokens = input.splitn(2, 'x').collect::>(); + let mut input = input.splitn(2, 'x'); - if tokens.len() != 2 { - return Err(Error::custom(format!( - "InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}", - input, - ))); - } + let width = input + .next() + .ok_or_else(|| Error::custom("missing width for `Resolution` or an invalid input")) + .and_then(|v| v.parse().map_err(Error::parse_int))?; - Ok(Self { - width: tokens[0].parse().map_err(Error::parse_int)?, - height: tokens[1].parse().map_err(Error::parse_int)?, - }) + let height = input + .next() + .ok_or_else(|| Error::custom("missing height for `Resolution` or an invalid input")) + .and_then(|v| v.parse().map_err(Error::parse_int))?; + + Ok(Self { width, height }) } } From 5de47561b1e64dea14c144f9ed335f3600b66722 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 6 Feb 2020 12:28:54 +0100 Subject: [PATCH 012/112] some minor improvements --- src/media_playlist.rs | 23 +++++++------- src/tags/basic/m3u.rs | 1 + src/tags/basic/version.rs | 12 ++++++-- .../master_playlist/i_frame_stream_inf.rs | 2 +- .../media_playlist/discontinuity_sequence.rs | 5 +++- src/utils.rs | 30 +++++++++++++++++-- 6 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index ea9bf53..d43f5a7 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -419,17 +419,18 @@ mod tests { #[test] fn too_large_segment_duration_test() { - let playlist = r#" - #EXTM3U - #EXT-X-TARGETDURATION:8 - #EXT-X-VERSION:3 - #EXTINF:9.009, - http://media.example.com/first.ts - #EXTINF:9.509, - http://media.example.com/second.ts - #EXTINF:3.003, - http://media.example.com/third.ts - #EXT-X-ENDLIST"#; + let playlist = concat!( + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:8\n", + "#EXT-X-VERSION:3\n", + "#EXTINF:9.009,\n", + "http://media.example.com/first.ts\n", + "#EXTINF:9.509,\n", + "http://media.example.com/second.ts\n", + "#EXTINF:3.003,\n", + "http://media.example.com/third.ts\n", + "#EXT-X-ENDLIST\n" + ); // Error (allowable segment duration = target duration = 8) assert!(playlist.parse::().is_err()); diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index b23032e..e2a8b78 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -53,6 +53,7 @@ mod test { #[test] fn test_parser() { assert_eq!("#EXTM3U".parse::().unwrap(), ExtM3u); + assert!("#EXTM2U".parse::().is_err()); } #[test] diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index be37b45..dc867e8 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -14,6 +14,7 @@ use crate::{Error, RequiredVersion}; /// # Examples /// /// Parsing from a [`str`]: +/// /// ``` /// # use hls_m3u8::tags::ExtXVersion; /// # @@ -25,7 +26,9 @@ use crate::{Error, RequiredVersion}; /// ); /// # Ok::<(), Box>(()) /// ``` +/// /// Converting to a [`str`]: +/// /// ``` /// # use hls_m3u8::tags::ExtXVersion; /// # @@ -49,6 +52,7 @@ impl ExtXVersion { /// Makes a new [`ExtXVersion`] tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXVersion; /// use hls_m3u8::types::ProtocolVersion; @@ -60,6 +64,7 @@ impl ExtXVersion { /// Returns the [`ProtocolVersion`] of the playlist, containing this tag. /// /// # Example + /// /// ``` /// # use hls_m3u8::tags::ExtXVersion; /// use hls_m3u8::types::ProtocolVersion; @@ -78,11 +83,14 @@ impl RequiredVersion for ExtXVersion { } impl fmt::Display for ExtXVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // + write!(f, "{}{}", Self::PREFIX, self.0) + } } impl Default for ExtXVersion { - fn default() -> Self { Self(ProtocolVersion::V1) } + fn default() -> Self { Self(ProtocolVersion::default()) } } impl From for ExtXVersion { diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 1a7f749..0343564 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -27,8 +27,8 @@ pub struct ExtXIFrameStreamInf { stream_inf: StreamInf, } -#[derive(Default, Debug, Clone, PartialEq)] /// Builder for [`ExtXIFrameStreamInf`]. +#[derive(Default, Debug, Clone, PartialEq)] pub struct ExtXIFrameStreamInfBuilder { uri: Option, stream_inf: StreamInfBuilder, diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 9869dee..d3ec395 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -67,7 +67,10 @@ impl RequiredVersion for ExtXDiscontinuitySequence { } impl fmt::Display for ExtXDiscontinuitySequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // + write!(f, "{}{}", Self::PREFIX, self.0) + } } impl FromStr for ExtXDiscontinuitySequence { diff --git a/src/utils.rs b/src/utils.rs index e794544..5f11764 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -90,6 +90,7 @@ pub(crate) fn quote(value: T) -> String { /// it will remove it and return the rest of the input. /// /// # Error +/// /// This function will return `Error::MissingTag`, if the input doesn't start /// with the tag, that has been passed to this function. pub(crate) fn tag(input: &str, tag: T) -> crate::Result<&str> @@ -99,8 +100,8 @@ where if !input.trim().starts_with(tag.as_ref()) { return Err(Error::missing_tag(tag.as_ref(), input)); } - let result = input.split_at(tag.as_ref().len()).1; - Ok(result) + + Ok(input.trim().split_at(tag.as_ref().len()).1) } #[cfg(test)] @@ -145,5 +146,30 @@ mod tests { assert_eq!(input, "SampleString"); assert!(tag(input, "B").is_err()); + + assert_eq!( + tag( + concat!( + "\n #EXTM3U\n", + " #EXT-X-TARGETDURATION:5220\n", + " #EXTINF:0,\n", + " http://media.example.com/entire1.ts\n", + " #EXTINF:5220,\n", + " http://media.example.com/entire2.ts\n", + " #EXT-X-ENDLIST" + ), + "#EXTM3U" + ) + .unwrap(), + concat!( + "\n", + " #EXT-X-TARGETDURATION:5220\n", + " #EXTINF:0,\n", + " http://media.example.com/entire1.ts\n", + " #EXTINF:5220,\n", + " http://media.example.com/entire2.ts\n", + " #EXT-X-ENDLIST" + ) + ); } } From 66c0b8dd0c76ac7cf81451e0b6fe1cb6a5f52119 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 6 Feb 2020 17:02:44 +0100 Subject: [PATCH 013/112] improve Iterator types --- src/attribute.rs | 2 +- src/line.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 50a6d17..f60b60c 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,6 +1,6 @@ use core::iter::FusedIterator; -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug)] pub(crate) struct AttributePairs<'a> { string: &'a str, index: usize, diff --git a/src/line.rs b/src/line.rs index 90cb3b4..299f5be 100644 --- a/src/line.rs +++ b/src/line.rs @@ -5,11 +5,9 @@ use std::str::FromStr; use crate::tags; use crate::Error; -#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] +#[derive(Debug, Clone)] pub(crate) struct Lines<'a> { - buffer: &'a str, - // the line at which the iterator currently is - position: usize, + lines: ::core::str::Lines<'a>, } impl<'a> Iterator for Lines<'a> { @@ -19,9 +17,8 @@ impl<'a> Iterator for Lines<'a> { let mut stream_inf = false; let mut stream_inf_line = None; - for line in self.buffer.lines().skip(self.position) { + while let Some(line) = self.lines.next() { let line = line.trim(); - self.position += 1; if line.is_empty() { continue; @@ -62,8 +59,7 @@ impl<'a> Iterator for Lines<'a> { impl<'a> From<&'a str> for Lines<'a> { fn from(buffer: &'a str) -> Self { Self { - buffer, - position: 0, + lines: buffer.lines(), } } } From 4e298f76ef0fe8c59be8481ddf00d50f3d2e9f0d Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 8 Feb 2020 10:45:26 +0100 Subject: [PATCH 014/112] use cargo deny --- .github/workflows/cargo_deny.yml | 8 ++ deny.toml | 189 +++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 .github/workflows/cargo_deny.yml create mode 100644 deny.toml diff --git a/.github/workflows/cargo_deny.yml b/.github/workflows/cargo_deny.yml new file mode 100644 index 0000000..d7ffbae --- /dev/null +++ b/.github/workflows/cargo_deny.yml @@ -0,0 +1,8 @@ +name: cargo deny +on: [push, pull_request] +jobs: + cargo-deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: EmbarkStudios/cargo-deny-action@v0 diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..880fd2a --- /dev/null +++ b/deny.toml @@ -0,0 +1,189 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url of the advisory database to use +db-url = "https://github.com/rustsec/advisory-db" +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explictly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + #"Apache-2.0 WITH LLVM-exception", +] +# List of explictly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "warn" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# THe optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] From ec07e6b64ce3eb1d283e3c11079db4ba56afe5b3 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 8 Feb 2020 10:45:48 +0100 Subject: [PATCH 015/112] improve travis --- .travis.yml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1a30fe2..af43e8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,13 @@ language: rust cache: cargo -before_cache: | - cargo install cargo-tarpaulin || echo "cargo-tarpaulin already installed" - cargo install cargo-update || echo "cargo-update already installed" - cargo install cargo-audit || echo "cargo-audit already installed" - cargo install-update --all +before_cache: + - cargo install cargo-tarpaulin || echo "cargo-tarpaulin already installed" + - cargo install cargo-update || echo "cargo-update already installed" + - cargo install cargo-audit || echo "cargo-audit already installed" + - cargo install-update --all + # Travis can't cache files that are not readable by "others" + - chmod -R a+r $HOME/.cargo # before_cache: # - rm -rf /home/travis/.cargo/registry @@ -19,15 +21,16 @@ matrix: allow_failures: - rust: nightly -script: | - cargo clean - cargo build - cargo test +script: + - cargo clean + - cargo build + - cargo test # it's enough to run this once: - if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then - cargo audit - fi + - | + if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then + cargo audit + fi after_success: | # this does require a -Z flag for Doctests, which is unstable! From acbe7e73da0a08d089ad1a15a8c9f4c5d8cd14cb Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:45:58 +0100 Subject: [PATCH 016/112] disable examples --- Cargo.toml | 1 - examples/parse.rs | 30 ++++-------------------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 62db739..1429f9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,5 +25,4 @@ hex = "0.4" shorthand = "0.1" [dev-dependencies] -clap = "2.33" pretty_assertions = "0.6" diff --git a/examples/parse.rs b/examples/parse.rs index 16c960b..9f549a8 100644 --- a/examples/parse.rs +++ b/examples/parse.rs @@ -1,29 +1,7 @@ -use clap::{App, Arg}; -use hls_m3u8::{MasterPlaylist, MediaPlaylist}; -use std::io::{self, Read}; +// use hls_m3u8::{MasterPlaylist, MediaPlaylist}; +// use std::fs::File; +// use std::io::Read; fn main() { - let matches = App::new("parse") - .arg( - Arg::with_name("M3U8_TYPE") - .long("m3u8-type") - .takes_value(true) - .default_value("media") - .possible_values(&["media", "master"]), - ) - .get_matches(); - let mut m3u8 = String::new(); - io::stdin().read_to_string(&mut m3u8).unwrap(); - - match matches.value_of("M3U8_TYPE").unwrap() { - "media" => { - let playlist: MediaPlaylist = m3u8.parse().unwrap(); - println!("{}", playlist); - } - "master" => { - let playlist: MasterPlaylist = m3u8.parse().unwrap(); - println!("{}", playlist); - } - _ => unreachable!(), - } + unimplemented!(); } From 9cc162ece7dcd7aac2ebeca0487714f7f8692beb Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:46:22 +0100 Subject: [PATCH 017/112] format `Cargo.toml` --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1429f9e..135db77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,17 +12,17 @@ edition = "2018" categories = ["parser"] [badges] -travis-ci = { repository = "sile/hls_m3u8" } codecov = { repository = "sile/hls_m3u8" } +travis-ci = { repository = "sile/hls_m3u8" } [dependencies] -thiserror = "1.0" -derive_builder = "0.9" chrono = "0.4" -strum = { version = "0.17", features = ["derive"] } +derive_builder = "0.9" derive_more = "0.99" hex = "0.4" shorthand = "0.1" +strum = { version = "0.17", features = ["derive"] } +thiserror = "1.0" [dev-dependencies] pretty_assertions = "0.6" From 101878a083cc98bb6363ea0b258dadfc0fa83b47 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:47:01 +0100 Subject: [PATCH 018/112] improvements to error --- src/error.rs | 61 ++++++++++++++-------------------------------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2cf9a75..f86caf4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,33 +1,27 @@ use std::fmt; use thiserror::Error; - -use crate::types::ProtocolVersion; +//use crate::types::ProtocolVersion; /// This crate specific `Result` type. pub type Result = std::result::Result; -/// The [`ErrorKind`]. #[derive(Debug, Error, Clone, PartialEq)] +#[non_exhaustive] enum ErrorKind { - /// A required value is missing. - #[error("A value is missing for the attribute {}", _0)] + #[error("a value is missing for the attribute {}", _0)] MissingValue(String), - /// Error for anything. - #[error("Invalid Input")] + #[error("invalid input")] InvalidInput, #[error("{}", _0)] - /// Failed to parse a String to int. ParseIntError(::std::num::ParseIntError), #[error("{}", _0)] - /// Failed to parse a String to float. ParseFloatError(::std::num::ParseFloatError), - /// A tag is missing, that is required at the start of the input. - #[error("Expected `{}` at the start of {:?}", tag, input)] + #[error("expected `{}` at the start of {:?}", tag, input)] MissingTag { /// The required tag. tag: String, @@ -36,56 +30,34 @@ enum ErrorKind { }, #[error("{}", _0)] - /// A custom error. Custom(String), - /// Unmatched Group - #[error("Unmatched Group: {:?}", _0)] + #[error("unmatched group: {:?}", _0)] UnmatchedGroup(String), - /// Unknown m3u8 version. This library supports up to ProtocolVersion 7. - #[error("Unknown protocol version {:?}", _0)] + #[error("unknown protocol version {:?}", _0)] UnknownProtocolVersion(String), - /// Some io error - #[error("{}", _0)] - Io(String), - - /// This error occurs, if there is a ProtocolVersion mismatch. - #[error("required_version: {:?}, specified_version: {:?}", _0, _1)] - VersionError(ProtocolVersion, ProtocolVersion), - - /// An attribute is missing. - #[error("Missing Attribute: {}", _0)] + // #[error("required_version: {:?}, specified_version: {:?}", _0, _1)] + // VersionError(ProtocolVersion, ProtocolVersion), + #[error("missing attribute: {}", _0)] MissingAttribute(String), - /// An unexpected value. - #[error("Unexpected Attribute: {:?}", _0)] + #[error("unexpected attribute: {:?}", _0)] UnexpectedAttribute(String), - /// An unexpected tag. - #[error("Unexpected Tag: {:?}", _0)] + #[error("unexpected tag: {:?}", _0)] UnexpectedTag(String), - /// An error from the [`chrono`] crate. #[error("{}", _0)] ChronoParseError(chrono::ParseError), - /// An error from a Builder. - #[error("BuilderError: {}", _0)] + #[error("builder error: {}", _0)] Builder(String), + #[doc(hidden)] #[error("{}", _0)] Hex(hex::FromHexError), - - /// Hints that destructuring should not be exhaustive. - /// - /// This enum may grow additional variants, so this makes sure clients - /// don't count on exhaustive matching. (Otherwise, adding a new variant - /// could break existing code.) - #[doc(hidden)] - #[error("Invalid error")] - __Nonexhaustive, } /// The Error type of this library. @@ -100,6 +72,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt(f) } } +#[allow(clippy::needless_pass_by_value)] impl Error { const fn new(inner: ErrorKind) -> Self { Self { inner } } @@ -119,7 +92,7 @@ impl Error { Self::new(ErrorKind::UnexpectedTag(value.to_string())) } - pub(crate) fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) } + pub(crate) const fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) } pub(crate) fn parse_int(value: ::std::num::ParseIntError) -> Self { Self::new(ErrorKind::ParseIntError(value)) @@ -148,8 +121,6 @@ impl Error { Self::new(ErrorKind::UnknownProtocolVersion(value.to_string())) } - pub(crate) fn io(value: T) -> Self { Self::new(ErrorKind::Io(value.to_string())) } - pub(crate) fn builder(value: T) -> Self { Self::new(ErrorKind::Builder(value.to_string())) } From 90ff18e2b338dffb1731a116b321f74127d486ba Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 13:13:41 +0100 Subject: [PATCH 019/112] implement Float and UFloat --- src/tags/shared/start.rs | 102 ++++++------ src/types/decimal_floating_point.rs | 137 ---------------- src/types/float.rs | 173 ++++++++++++++++++++ src/types/mod.rs | 10 +- src/types/signed_decimal_floating_point.rs | 94 ----------- src/types/ufloat.rs | 182 +++++++++++++++++++++ src/utils.rs | 21 --- 7 files changed, 409 insertions(+), 310 deletions(-) delete mode 100644 src/types/decimal_floating_point.rs create mode 100644 src/types/float.rs delete mode 100644 src/types/signed_decimal_floating_point.rs create mode 100644 src/types/ufloat.rs diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 994b4ed..1b2afb2 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -1,18 +1,35 @@ use std::fmt; use std::str::FromStr; +use shorthand::ShortHand; + use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; +use crate::types::{Float, ProtocolVersion}; use crate::utils::{parse_yes_or_no, tag}; 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)] +#[derive(ShortHand, PartialOrd, Debug, Clone, Copy, PartialEq)] +#[shorthand(enable(must_use))] pub struct ExtXStart { - time_offset: SignedDecimalFloatingPoint, - precise: bool, + #[shorthand(enable(skip))] + time_offset: Float, + /// Returns whether clients should not render media stream whose + /// presentation times are prior to the specified time offset. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// let mut start = ExtXStart::new(20.123456); + /// # assert_eq!(start.is_precise(), false); + /// start.set_is_precise(true); + /// + /// assert_eq!(start.is_precise(), true); + /// ``` + is_precise: bool, } impl ExtXStart { @@ -20,9 +37,9 @@ impl ExtXStart { /// Makes a new [`ExtXStart`] tag. /// - /// # Panic + /// # Panics /// - /// Panics if the time_offset value is infinite. + /// Panics if the `time_offset` is infinite or [`NaN`]. /// /// # Example /// @@ -30,30 +47,34 @@ impl ExtXStart { /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::new(20.123456); /// ``` - pub fn new(time_offset: f64) -> Self { + /// + /// [`NaN`]: core::f64::NAN + pub fn new(time_offset: f32) -> Self { Self { - time_offset: SignedDecimalFloatingPoint::new(time_offset), - precise: false, + time_offset: Float::new(time_offset), + is_precise: false, } } /// Makes a new [`ExtXStart`] tag with the given `precise` flag. /// - /// # Panic + /// # Panics /// - /// Panics if the time_offset value is infinite. + /// Panics if the `time_offset` is infinite or [`NaN`]. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::with_precise(20.123456, true); - /// assert_eq!(start.precise(), true); + /// assert_eq!(start.is_precise(), true); /// ``` - pub fn with_precise(time_offset: f64, precise: bool) -> Self { + /// + /// [`NaN`]: core::f64::NAN + pub fn with_precise(time_offset: f32, is_precise: bool) -> Self { Self { - time_offset: SignedDecimalFloatingPoint::new(time_offset), - precise, + time_offset: Float::new(time_offset), + is_precise, } } @@ -64,9 +85,10 @@ impl ExtXStart { /// ``` /// # use hls_m3u8::tags::ExtXStart; /// let start = ExtXStart::new(20.123456); + /// /// assert_eq!(start.time_offset(), 20.123456); /// ``` - pub const fn time_offset(&self) -> f64 { self.time_offset.as_f64() } + pub const fn time_offset(self) -> f32 { self.time_offset.as_f32() } /// Sets the time offset of the media segments in the playlist. /// @@ -81,38 +103,8 @@ impl ExtXStart { /// /// assert_eq!(start.time_offset(), 1.0); /// ``` - pub fn set_time_offset(&mut self, value: f64) -> &mut Self { - self.time_offset = SignedDecimalFloatingPoint::new(value); - self - } - - /// Returns whether clients should not render media stream whose - /// presentation times are prior to the specified time offset. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXStart; - /// let start = ExtXStart::with_precise(20.123456, true); - /// assert_eq!(start.precise(), true); - /// ``` - pub const fn precise(&self) -> bool { self.precise } - - /// Sets the `precise` flag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXStart; - /// let mut start = ExtXStart::new(20.123456); - /// # assert_eq!(start.precise(), false); - /// - /// start.set_precise(true); - /// - /// assert_eq!(start.precise(), true); - /// ``` - pub fn set_precise(&mut self, value: bool) -> &mut Self { - self.precise = value; + pub fn set_time_offset(&mut self, value: f32) -> &mut Self { + self.time_offset = Float::new(value); self } } @@ -126,9 +118,11 @@ impl fmt::Display for ExtXStart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TIME-OFFSET={}", self.time_offset)?; - if self.precise { + + if self.is_precise { write!(f, ",PRECISE=YES")?; } + Ok(()) } } @@ -140,12 +134,12 @@ impl FromStr for ExtXStart { let input = tag(input, Self::PREFIX)?; let mut time_offset = None; - let mut precise = false; + let mut is_precise = false; for (key, value) in AttributePairs::new(input) { match key { - "TIME-OFFSET" => time_offset = Some((value.parse())?), - "PRECISE" => precise = (parse_yes_or_no(value))?, + "TIME-OFFSET" => time_offset = Some(value.parse()?), + "PRECISE" => is_precise = parse_yes_or_no(value)?, _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized @@ -154,11 +148,11 @@ impl FromStr for ExtXStart { } } - let time_offset = time_offset.ok_or_else(|| Error::missing_value("EXT-X-TIME-OFFSET"))?; + let time_offset = time_offset.ok_or_else(|| Error::missing_value("TIME-OFFSET"))?; Ok(Self { time_offset, - precise, + is_precise, }) } } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs deleted file mode 100644 index 454fb66..0000000 --- a/src/types/decimal_floating_point.rs +++ /dev/null @@ -1,137 +0,0 @@ -use core::str::FromStr; - -use derive_more::{Deref, Display}; - -use crate::Error; - -/// Non-negative decimal floating-point number. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.2 -#[derive(Deref, Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)] -pub(crate) struct DecimalFloatingPoint(f64); - -impl DecimalFloatingPoint { - /// Makes a new [`DecimalFloatingPoint`] instance. - /// - /// # Errors - /// - /// The given value must have a positive sign and be finite, - /// otherwise this function will return an error that has the kind - /// `ErrorKind::InvalidInput`. - pub fn new(value: f64) -> crate::Result { - if value.is_sign_negative() || value.is_infinite() || value.is_nan() { - return Err(Error::invalid_input()); - } - Ok(Self(value)) - } - - pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) } - - /// Converts [`DecimalFloatingPoint`] to [`f64`]. - pub const fn as_f64(self) -> f64 { self.0 } -} - -// this trait is implemented manually, so it doesn't construct a -// [`DecimalFloatingPoint`], with a negative value. -impl FromStr for DecimalFloatingPoint { - type Err = Error; - - fn from_str(input: &str) -> Result { - Self::new(input.parse().map_err(Error::parse_float)?) - } -} - -#[doc(hidden)] -impl From for DecimalFloatingPoint { - fn from(value: f64) -> Self { - let mut result = value; - - // guard against the unlikely case of an infinite value... - if result.is_infinite() || result.is_nan() { - result = 0.0; - } - - Self(result.abs()) - } -} - -#[doc(hidden)] -impl From for DecimalFloatingPoint { - 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),* ) => { - use ::core::convert::From; - - #[test] - fn test_from() { - $( - assert_eq!( - DecimalFloatingPoint::from($input), - DecimalFloatingPoint::new(1.0).unwrap(), - ); - )* - } - } - } - - 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() { - let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap(); - assert_eq!(decimal_floating_point.to_string(), "22".to_string()); - - let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap(); - assert_eq!(decimal_floating_point.to_string(), "4.1".to_string()); - } - - #[test] - pub fn test_parser() { - assert_eq!( - DecimalFloatingPoint::new(22.0).unwrap(), - "22".parse::().unwrap() - ); - - assert_eq!( - DecimalFloatingPoint::new(4.1).unwrap(), - "4.1".parse::().unwrap() - ); - - assert!("1#".parse::().is_err()); - assert!("-1.0".parse::().is_err()); - } - - #[test] - fn test_new() { - assert!(DecimalFloatingPoint::new(::std::f64::INFINITY).is_err()); - assert!(DecimalFloatingPoint::new(-1.0).is_err()); - } - - #[test] - fn test_as_f64() { - assert_eq!(DecimalFloatingPoint::new(1.0).unwrap().as_f64(), 1.0); - } - - #[test] - fn test_from_inf() { - assert_eq!( - DecimalFloatingPoint::from(::std::f64::INFINITY), - DecimalFloatingPoint::new(0.0).unwrap() - ); - } - - #[test] - fn test_deref() { - assert_eq!(DecimalFloatingPoint::from(0.1).floor(), 0.0); - } -} diff --git a/src/types/float.rs b/src/types/float.rs new file mode 100644 index 0000000..48d1e26 --- /dev/null +++ b/src/types/float.rs @@ -0,0 +1,173 @@ +use core::convert::TryFrom; +use core::str::FromStr; + +use derive_more::{Deref, Display}; + +use crate::Error; + +/// This is a wrapper type around an [`f32`] that can not be constructed +/// with [`NaN`], [`INFINITY`] or [`NEG_INFINITY`]. +/// +/// [`NaN`]: core::f32::NAN +/// [`INFINITY`]: core::f32::INFINITY +/// [`NEG_INFINITY`]: core::f32::NEG_INFINITY +#[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)] +pub struct Float(f32); + +impl Float { + /// Makes a new [`Float`] from an [`f32`]. + /// + /// # Panics + /// + /// If the given float is infinite or [`NaN`]. + /// + /// # Examples + /// + /// ``` + /// use hls_m3u8::types::Float; + /// + /// let float = Float::new(1.0); + /// ``` + /// + /// This would panic: + /// + /// ```should_panic + /// use core::f32::NAN; + /// use hls_m3u8::types::Float; + /// + /// let float = Float::new(NAN); + /// ``` + /// + /// [`NaN`]: core::f32::NAN + pub fn new(float: f32) -> Self { + if float.is_infinite() { + panic!("float must be finite: `{}`", float); + } + + if float.is_nan() { + panic!("float must not be `NaN`"); + } + + Self(float) + } + + /// Returns the underlying [`f32`]. + pub const fn as_f32(self) -> f32 { self.0 } +} + +impl FromStr for Float { + type Err = Error; + + fn from_str(input: &str) -> Result { + let float = f32::from_str(input).map_err(Error::parse_float)?; + Self::try_from(float) + } +} + +impl TryFrom for Float { + type Error = Error; + + fn try_from(float: f32) -> Result { + if float.is_infinite() { + return Err(Error::custom(format!("float must be finite: `{}`", float))); + } + + if float.is_nan() { + return Err(Error::custom("float must not be `NaN`")); + } + + Ok(Self(float)) + } +} + +macro_rules! implement_from { + ( $( $type:tt ),+ ) => { + $( + impl ::core::convert::From<$type> for Float { + fn from(value: $type) -> Self { + Self(value as f32) + } + } + )+ + } +} + +implement_from!(i16, u16, i8, u8); + +// convenience implementation to compare f32 with a Float. +impl PartialEq for Float { + fn eq(&self, other: &f32) -> bool { &self.0 == other } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_display() { + assert_eq!(Float::new(22.0).to_string(), "22".to_string()); + assert_eq!( + Float::new(3.14159265359).to_string(), + "3.1415927".to_string() + ); + assert_eq!( + Float::new(-3.14159265359).to_string(), + "-3.1415927".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!(Float::new(22.0), Float::from_str("22").unwrap()); + assert_eq!(Float::new(-22.0), Float::from_str("-22").unwrap()); + assert_eq!( + Float::new(3.14159265359), + Float::from_str("3.14159265359").unwrap() + ); + assert!(Float::from_str("1#").is_err()); + assert!(Float::from_str("NaN").is_err()); + assert!(Float::from_str("inf").is_err()); + assert!(Float::from_str("-inf").is_err()); + } + + #[test] + #[should_panic = "float must be finite: `inf`"] + fn test_new_infinite() { Float::new(::core::f32::INFINITY); } + + #[test] + #[should_panic = "float must be finite: `-inf`"] + fn test_new_neg_infinite() { Float::new(::core::f32::NEG_INFINITY); } + + #[test] + #[should_panic = "float must not be `NaN`"] + fn test_new_nan() { Float::new(::core::f32::NAN); } + + #[test] + fn test_partial_eq() { + assert_eq!(Float::new(1.1), 1.1); + } + + #[test] + fn test_as_f32() { + assert_eq!(Float::new(1.1).as_f32(), 1.1_f32); + } + + #[test] + fn test_from() { + assert_eq!(Float::from(-1_i8), Float::new(-1.0)); + assert_eq!(Float::from(1_u8), Float::new(1.0)); + assert_eq!(Float::from(-1_i16), Float::new(-1.0)); + assert_eq!(Float::from(1_u16), Float::new(1.0)); + } + + #[test] + fn test_try_from() { + assert_eq!(Float::try_from(1.1_f32).unwrap(), Float::new(1.1)); + assert_eq!(Float::try_from(-1.1_f32).unwrap(), Float::new(-1.1)); + + assert!(Float::try_from(::core::f32::INFINITY).is_err()); + assert!(Float::try_from(::core::f32::NAN).is_err()); + assert!(Float::try_from(::core::f32::NEG_INFINITY).is_err()); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 5bcb7e9..e0af60d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,7 +2,6 @@ mod byte_range; mod channels; mod closed_captions; -mod decimal_floating_point; mod decryption_key; mod encryption_method; mod hdcp_level; @@ -12,14 +11,15 @@ mod key_format_versions; mod media_type; mod protocol_version; mod resolution; -mod signed_decimal_floating_point; mod stream_inf; mod value; +mod float; +mod ufloat; + pub use byte_range::*; pub use channels::*; pub use closed_captions::*; -pub(crate) use decimal_floating_point::*; pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; @@ -29,6 +29,8 @@ pub use key_format_versions::*; pub use media_type::*; pub use protocol_version::*; pub use resolution::*; -pub(crate) use signed_decimal_floating_point::*; pub use stream_inf::*; pub use value::*; + +pub use float::Float; +pub use ufloat::UFloat; diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs deleted file mode 100644 index 62aa32a..0000000 --- a/src/types/signed_decimal_floating_point.rs +++ /dev/null @@ -1,94 +0,0 @@ -use derive_more::Deref; -use derive_more::{Display, FromStr}; - -/// Signed decimal floating-point number. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Deref, Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)] -pub(crate) struct SignedDecimalFloatingPoint(f64); - -impl SignedDecimalFloatingPoint { - /// 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() || 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`]. - pub const fn as_f64(self) -> f64 { self.0 } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - macro_rules! test_from { - ( $( $input:expr => $output:expr ),* ) => { - use ::core::convert::From; - - #[test] - fn test_from() { - $( - assert_eq!( - $input, - $output, - ); - )* - } - } - } - - test_from![ - 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.0_f64.to_string() - ); - } - - #[test] - #[should_panic] - fn test_new_panic() { SignedDecimalFloatingPoint::new(::std::f64::INFINITY); } - - #[test] - fn test_parser() { - assert_eq!( - SignedDecimalFloatingPoint::new(1.0), - "1.0".parse::().unwrap() - ); - - assert!("garbage".parse::().is_err()); - } - - #[test] - fn test_as_f64() { - assert_eq!(SignedDecimalFloatingPoint::new(1.0).as_f64(), 1.0); - } - - #[test] - fn test_deref() { - assert_eq!(SignedDecimalFloatingPoint::from(0.1).floor(), 0.0); - } -} diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs new file mode 100644 index 0000000..ad4d180 --- /dev/null +++ b/src/types/ufloat.rs @@ -0,0 +1,182 @@ +use core::convert::TryFrom; +use core::str::FromStr; + +use derive_more::{Deref, Display}; + +use crate::Error; + +/// This is a wrapper type around an [`f32`] that can not be constructed +/// with a negative float (ex. `-1.1`), [`NaN`], [`INFINITY`] or +/// [`NEG_INFINITY`]. +/// +/// [`NaN`]: core::f32::NaN +/// [`INFINITY`]: core::f32::INFINITY +/// [`NEG_INFINITY`]: core::f32::NEG_INFINITY +#[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)] +pub struct UFloat(f32); + +impl UFloat { + /// Makes a new [`UFloat`] from an [`f32`]. + /// + /// # Panics + /// + /// If the given float is negative, infinite or [`NaN`]. + /// + /// # Examples + /// + /// ``` + /// use hls_m3u8::types::UFloat; + /// + /// let float = UFloat::new(1.0); + /// ``` + /// + /// This would panic: + /// + /// ```should_panic + /// use hls_m3u8::types::UFloat; + /// + /// let float = UFloat::new(-1.0); + /// ``` + /// + /// [`NaN`]: core::f32::NAN + pub fn new(float: f32) -> Self { + if float.is_infinite() { + panic!("float must be finite: `{}`", float); + } + + if float.is_nan() { + panic!("float must not be `NaN`"); + } + + if float.is_sign_negative() { + panic!("float must be positive: `{}`", float); + } + + Self(float) + } + + /// Returns the underlying [`f32`]. + pub const fn as_f32(self) -> f32 { self.0 } +} + +impl FromStr for UFloat { + type Err = Error; + + fn from_str(input: &str) -> Result { + let float = f32::from_str(input).map_err(Error::parse_float)?; + Self::try_from(float) + } +} + +impl TryFrom for UFloat { + type Error = Error; + + fn try_from(float: f32) -> Result { + if float.is_infinite() { + return Err(Error::custom(format!("float must be finite: `{}`", float))); + } + + if float.is_nan() { + return Err(Error::custom("float must not be `NaN`")); + } + + if float.is_sign_negative() { + return Err(Error::custom(format!( + "float must be positive: `{}`", + float + ))); + } + + Ok(Self(float)) + } +} + +macro_rules! implement_from { + ( $( $type:tt ),+ ) => { + $( + impl ::core::convert::From<$type> for UFloat { + fn from(value: $type) -> Self { + Self(value as f32) + } + } + )+ + } +} + +implement_from!(u16, u8); + +// convenience implementation to compare f32 with a Float. +impl PartialEq for UFloat { + fn eq(&self, other: &f32) -> bool { &self.0 == other } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_display() { + assert_eq!(UFloat::new(22.0).to_string(), "22".to_string()); + assert_eq!( + UFloat::new(3.14159265359).to_string(), + "3.1415927".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!(UFloat::new(22.0), UFloat::from_str("22").unwrap()); + assert_eq!( + UFloat::new(3.14159265359), + UFloat::from_str("3.14159265359").unwrap() + ); + assert!(UFloat::from_str("1#").is_err()); + assert!(UFloat::from_str("-1.0").is_err()); + assert!(UFloat::from_str("NaN").is_err()); + assert!(UFloat::from_str("inf").is_err()); + assert!(UFloat::from_str("-inf").is_err()); + } + + #[test] + #[should_panic = "float must be positive: `-1.1`"] + fn test_new_negative() { UFloat::new(-1.1); } + + #[test] + #[should_panic = "float must be finite: `inf`"] + fn test_new_infinite() { UFloat::new(::core::f32::INFINITY); } + + #[test] + #[should_panic = "float must be finite: `-inf`"] + fn test_new_neg_infinite() { UFloat::new(::core::f32::NEG_INFINITY); } + + #[test] + #[should_panic = "float must not be `NaN`"] + fn test_new_nan() { UFloat::new(::core::f32::NAN); } + + #[test] + fn test_partial_eq() { + assert_eq!(UFloat::new(1.1), 1.1); + } + + #[test] + fn test_as_f32() { + assert_eq!(UFloat::new(1.1).as_f32(), 1.1_f32); + } + + #[test] + fn test_from() { + assert_eq!(UFloat::from(1_u8), UFloat::new(1.0)); + assert_eq!(UFloat::from(1_u16), UFloat::new(1.0)); + } + + #[test] + fn test_try_from() { + assert_eq!(UFloat::try_from(1.1_f32).unwrap(), UFloat::new(1.1)); + + assert!(UFloat::try_from(-1.1_f32).is_err()); + assert!(UFloat::try_from(::core::f32::INFINITY).is_err()); + assert!(UFloat::try_from(::core::f32::NAN).is_err()); + assert!(UFloat::try_from(::core::f32::NEG_INFINITY).is_err()); + } +} diff --git a/src/utils.rs b/src/utils.rs index 5f11764..989d029 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,27 +11,6 @@ macro_rules! required_version { } } -macro_rules! impl_from { - ( $($( $type:tt ),* => $target:path ),* ) => { - use ::core::convert::From; - - $( // repeat $target - $( // repeat $type - impl From<$type> for $target { - fn from(value: $type) -> Self { - Self::from_f64_unchecked(value.into()) - } - } - )* - )* - }; -} - -impl_from![ - u8, u16, u32 => crate::types::DecimalFloatingPoint, - u8, i8, u16, i16, u32, i32, f32, f64 => crate::types::SignedDecimalFloatingPoint -]; - pub(crate) fn parse_iv_from_str(input: &str) -> crate::Result<[u8; 16]> { if !(input.starts_with("0x") || input.starts_with("0X")) { return Err(Error::invalid_input()); From e6f5091f1b2357c8f67bb51df484eb6c3beb481c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 13:20:39 +0100 Subject: [PATCH 020/112] implement VariantStream --- src/line.rs | 83 ++- src/master_playlist.rs | 248 +++++---- src/media_playlist.rs | 85 ++-- .../master_playlist/i_frame_stream_inf.rs | 263 ---------- src/tags/master_playlist/mod.rs | 6 +- src/tags/master_playlist/stream_inf.rs | 304 ----------- src/tags/master_playlist/variant_stream.rs | 286 +++++++++++ src/types/closed_captions.rs | 28 +- src/types/mod.rs | 4 +- src/types/stream_data.rs | 370 ++++++++++++++ src/types/stream_inf.rs | 278 ---------- src/types/ufloat.rs | 2 +- tests/master_playlist.rs | 477 ++++++++++-------- tests/media_playlist.rs | 36 +- tests/playlist.rs | 19 +- 15 files changed, 1201 insertions(+), 1288 deletions(-) delete mode 100644 src/tags/master_playlist/i_frame_stream_inf.rs delete mode 100644 src/tags/master_playlist/stream_inf.rs create mode 100644 src/tags/master_playlist/variant_stream.rs create mode 100644 src/types/stream_data.rs delete mode 100644 src/types/stream_inf.rs diff --git a/src/line.rs b/src/line.rs index 299f5be..c6ba9d2 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,65 +1,48 @@ -use std::convert::TryFrom; -use std::fmt; -use std::str::FromStr; +use core::convert::TryFrom; +use core::fmt; +use core::str::FromStr; use crate::tags; use crate::Error; #[derive(Debug, Clone)] pub(crate) struct Lines<'a> { - lines: ::core::str::Lines<'a>, + lines: ::core::iter::FilterMap<::core::str::Lines<'a>, fn(&'a str) -> Option<&'a str>>, } impl<'a> Iterator for Lines<'a> { type Item = crate::Result>; fn next(&mut self) -> Option { - let mut stream_inf = false; - let mut stream_inf_line = None; + let line = self.lines.next()?; - while let Some(line) = self.lines.next() { - let line = line.trim(); + if line.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) { + let uri = self.lines.next()?; - if line.is_empty() { - continue; - } - - if line.starts_with(tags::ExtXStreamInf::PREFIX) { - stream_inf = true; - stream_inf_line = Some(line); - - continue; - } else if line.starts_with("#EXT") { - return Some(Tag::try_from(line).map(Line::Tag)); - } else if 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 { - return Some( - tags::ExtXStreamInf::from_str(&format!("{}\n{}", first_line, line)) - .map(|v| Line::Tag(Tag::ExtXStreamInf(v))), - ); - } else { - continue; - } - } else { - return Some(Ok(Line::Uri(line))); - } - } + Some( + tags::VariantStream::from_str(&format!("{}\n{}", line, uri)) + .map(|v| Line::Tag(Tag::VariantStream(v))), + ) + } else if line.starts_with("#EXT") { + Some(Tag::try_from(line).map(Line::Tag)) + } else if line.starts_with('#') { + Some(Ok(Line::Comment(line))) + } else { + Some(Ok(Line::Uri(line))) } - - None } } impl<'a> From<&'a str> for Lines<'a> { fn from(buffer: &'a str) -> Self { Self { - lines: buffer.lines(), + lines: buffer.lines().filter_map(|line| { + if line.trim().is_empty() { + None + } else { + Some(line.trim()) + } + }), } } } @@ -67,6 +50,7 @@ impl<'a> From<&'a str> for Lines<'a> { #[derive(Debug, Clone, PartialEq)] pub(crate) enum Line<'a> { Tag(Tag<'a>), + Comment(&'a str), Uri(&'a str), } @@ -88,12 +72,11 @@ pub(crate) enum Tag<'a> { ExtXPlaylistType(tags::ExtXPlaylistType), ExtXIFramesOnly(tags::ExtXIFramesOnly), ExtXMedia(tags::ExtXMedia), - ExtXStreamInf(tags::ExtXStreamInf), - ExtXIFrameStreamInf(tags::ExtXIFrameStreamInf), ExtXSessionData(tags::ExtXSessionData), ExtXSessionKey(tags::ExtXSessionKey), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), + VariantStream(tags::VariantStream), Unknown(&'a str), } @@ -115,8 +98,7 @@ impl<'a> fmt::Display for Tag<'a> { 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::VariantStream(value) => value.fmt(f), Self::ExtXSessionData(value) => value.fmt(f), Self::ExtXSessionKey(value) => value.fmt(f), Self::ExtXIndependentSegments(value) => value.fmt(f), @@ -160,10 +142,13 @@ impl<'a> TryFrom<&'a str> for Tag<'a> { input.parse().map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { input.parse().map(Self::ExtXMedia).map_err(Error::custom) - } else if input.starts_with(tags::ExtXStreamInf::PREFIX) { - input.parse().map(Self::ExtXStreamInf) - } else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - input.parse().map(Self::ExtXIFrameStreamInf) + } else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME) + || input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) + { + input + .parse() + .map(Self::VariantStream) + .map_err(Error::custom) } else if input.starts_with(tags::ExtXSessionData::PREFIX) { input.parse().map(Self::ExtXSessionData) } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index cb88465..f906667 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -7,68 +7,80 @@ use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::tags::{ - ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, - ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, + ExtM3u, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, + ExtXVersion, VariantStream, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// Master playlist. +/// The master playlist describes all of the available variants for your +/// content. Each variant is a version of the stream at a particular bitrate +/// and is contained in a separate playlist. #[derive(ShortHand, Debug, Clone, Builder, PartialEq)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] #[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MasterPlaylist { - /// The [`ExtXIndependentSegments`] tag of the playlist. + /// The [`ExtXIndependentSegments`] tag signals that all media samples in a + /// [`MediaSegment`] can be decoded without information from other segments. + /// + /// # Note + /// + /// This tag is optional. + /// + /// If this tag is specified it will apply to every [`MediaSegment`] in + /// every [`MediaPlaylist`] in the [`MasterPlaylist`]. + /// + /// [`MediaSegment`]: crate::MediaSegment + /// [`MediaPlaylist`]: crate::MediaPlaylist + #[builder(default)] + independent_segments: Option, + /// The [`ExtXStart`] tag indicates a preferred point at which to start + /// playing a Playlist. /// /// # Note /// /// This tag is optional. #[builder(default)] - independent_segments_tag: Option, - /// The [`ExtXStart`] tag of the playlist. + start: Option, + /// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, + /// that contain alternative renditions of the same content. + /// + /// For example, three [`ExtXMedia`] tags can be used to identify audio-only + /// [`MediaPlaylist`]s, that contain English, French, and Spanish + /// renditions of the same presentation. Or, two [`ExtXMedia`] tags can + /// be used to identify video-only [`MediaPlaylist`]s that show two + /// different camera angles. + /// + /// # Note + /// + /// This tag is optional. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + #[builder(default)] + media: Vec, + /// A list of all streams of this [`MasterPlaylist`]. /// /// # Note /// /// This tag is optional. #[builder(default)] - start_tag: Option, - /// The [`ExtXMedia`] tags of the playlist. - /// - /// # Note - /// - /// This tag is optional. - #[builder(default)] - media_tags: Vec, - /// The [`ExtXStreamInf`] tags of the playlist. - /// - /// # Note - /// - /// This tag is optional. - #[builder(default)] - stream_inf_tags: Vec, - /// The [`ExtXIFrameStreamInf`] tags of the playlist. - /// - /// # Note - /// - /// This tag is optional. - #[builder(default)] - i_frame_stream_inf_tags: Vec, + variants: Vec, /// The [`ExtXSessionData`] tags of the playlist. /// /// # Note /// /// This tag is optional. #[builder(default)] - session_data_tags: Vec, + session_data: Vec, /// The [`ExtXSessionKey`] tags of the playlist. /// /// # Note /// /// This tag is optional. #[builder(default)] - session_key_tags: Vec, + session_keys: Vec, /// A list of tags that are unknown. /// /// # Note @@ -79,6 +91,7 @@ pub struct MasterPlaylist { } impl MasterPlaylist { + // TODO: finish builder example! /// Returns a builder for a [`MasterPlaylist`]. /// /// # Example @@ -88,7 +101,7 @@ impl MasterPlaylist { /// use hls_m3u8::MasterPlaylist; /// /// MasterPlaylist::builder() - /// .start_tag(ExtXStart::new(20.123456)) + /// .start(ExtXStart::new(20.123456)) /// .build()?; /// # Ok::<(), Box>(()) /// ``` @@ -98,101 +111,128 @@ impl MasterPlaylist { impl RequiredVersion for MasterPlaylist { 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 + self.independent_segments, + self.start, + self.media, + self.variants, + self.session_data, + self.session_keys ] } } impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), 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())?; + self.validate_variants().map_err(|e| e.to_string())?; self.validate_session_data_tags() .map_err(|e| e.to_string())?; Ok(()) } - fn validate_stream_inf_tags(&self) -> crate::Result<()> { - if let Some(value) = &self.stream_inf_tags { - let mut has_none_closed_captions = false; + fn validate_variants(&self) -> crate::Result<()> { + if let Some(variants) = &self.variants { + self.validate_stream_inf(variants)?; + self.validate_i_frame_stream_inf(variants)?; + } - for t in value { - if let Some(group_id) = t.audio() { + Ok(()) + } + + fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { + let mut has_none_closed_captions = false; + + for t in value { + if let VariantStream::ExtXStreamInf { + audio, + subtitles, + closed_captions, + stream_data, + .. + } = &t + { + if let Some(group_id) = &audio { if !self.check_media_group(MediaType::Audio, group_id) { return Err(Error::unmatched_group(group_id)); } } - if let Some(group_id) = t.video() { + if let Some(group_id) = &stream_data.video() { if !self.check_media_group(MediaType::Video, group_id) { return Err(Error::unmatched_group(group_id)); } } - if let Some(group_id) = t.subtitles() { + if let Some(group_id) = &subtitles { if !self.check_media_group(MediaType::Subtitles, group_id) { return Err(Error::unmatched_group(group_id)); } } - match &t.closed_captions() { - Some(ClosedCaptions::GroupId(group_id)) => { - if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id)); + + if let Some(closed_captions) = &closed_captions { + match &closed_captions { + ClosedCaptions::GroupId(group_id) => { + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + ClosedCaptions::None => { + has_none_closed_captions = true; } } - Some(ClosedCaptions::None) => { - has_none_closed_captions = true; - } - _ => {} } } - if has_none_closed_captions - && !value - .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) - { - return Err(Error::invalid_input()); - } } + + if has_none_closed_captions + && !value.iter().all(|t| { + if let VariantStream::ExtXStreamInf { + closed_captions, .. + } = &t + { + closed_captions == &Some(ClosedCaptions::None) + } else { + false + } + }) + { + return Err(Error::invalid_input()); + } + Ok(()) } - fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { - if let Some(value) = &self.i_frame_stream_inf_tags { - for t in value { - if let Some(group_id) = t.video() { + fn validate_i_frame_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { + for t in value { + if let VariantStream::ExtXIFrame { stream_data, .. } = &t { + if let Some(group_id) = stream_data.video() { if !self.check_media_group(MediaType::Video, group_id) { return Err(Error::unmatched_group(group_id)); } } } } + Ok(()) } fn validate_session_data_tags(&self) -> crate::Result<()> { let mut set = HashSet::new(); - if let Some(value) = &self.session_data_tags { + + if let Some(value) = &self.session_data { for t in value { if !set.insert((t.data_id(), t.language())) { return Err(Error::custom(format!("Conflict: {}", t))); } } } + Ok(()) } - fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { - if let Some(value) = &self.media_tags { + fn check_media_group>(&self, media_type: MediaType, group_id: T) -> bool { + if let Some(value) = &self.media { value .iter() - .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) + .any(|t| t.media_type() == media_type && t.group_id().as_str() == group_id.as_ref()) } else { false } @@ -206,13 +246,12 @@ impl RequiredVersion for MasterPlaylistBuilder { // 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 + self.independent_segments.flatten(), + self.start.flatten(), + self.media, + self.variants, + self.session_data, + self.session_keys ] } } @@ -220,35 +259,32 @@ impl RequiredVersion for MasterPlaylistBuilder { impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; + if self.required_version() != ProtocolVersion::V1 { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } - for t in &self.media_tags { + for t in &self.media { writeln!(f, "{}", t)?; } - for t in &self.stream_inf_tags { + for t in &self.variants { writeln!(f, "{}", t)?; } - for t in &self.i_frame_stream_inf_tags { + for t in &self.session_data { writeln!(f, "{}", t)?; } - for t in &self.session_data_tags { + for t in &self.session_keys { writeln!(f, "{}", t)?; } - for t in &self.session_key_tags { - writeln!(f, "{}", t)?; - } - - if let Some(value) = &self.independent_segments_tag { + if let Some(value) = &self.independent_segments { writeln!(f, "{}", value)?; } - if let Some(value) = &self.start_tag { + if let Some(value) = &self.start { writeln!(f, "{}", value)?; } @@ -267,11 +303,10 @@ impl FromStr for MasterPlaylist { let input = tag(input, ExtM3u::PREFIX)?; let mut builder = Self::builder(); - let mut media_tags = vec![]; - let mut stream_inf_tags = vec![]; - let mut i_frame_stream_inf_tags = vec![]; - let mut session_data_tags = vec![]; - let mut session_key_tags = vec![]; + let mut media = vec![]; + let mut variants = vec![]; + let mut session_data = vec![]; + let mut session_keys = vec![]; let mut unknown_tags = vec![]; for line in Lines::from(input) { @@ -303,25 +338,22 @@ impl FromStr for MasterPlaylist { ))); } Tag::ExtXMedia(t) => { - media_tags.push(t); + media.push(t); } - Tag::ExtXStreamInf(t) => { - stream_inf_tags.push(t); - } - Tag::ExtXIFrameStreamInf(t) => { - i_frame_stream_inf_tags.push(t); + Tag::VariantStream(t) => { + variants.push(t); } Tag::ExtXSessionData(t) => { - session_data_tags.push(t); + session_data.push(t); } Tag::ExtXSessionKey(t) => { - session_key_tags.push(t); + session_keys.push(t); } Tag::ExtXIndependentSegments(t) => { - builder.independent_segments_tag(t); + builder.independent_segments(t); } Tag::ExtXStart(t) => { - builder.start_tag(t); + builder.start(t); } _ => { // [6.3.1. General Client Responsibilities] @@ -333,14 +365,14 @@ impl FromStr for MasterPlaylist { Line::Uri(uri) => { return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); } + _ => {} } } - builder.media_tags(media_tags); - builder.stream_inf_tags(stream_inf_tags); - builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags); - builder.session_data_tags(session_data_tags); - builder.session_key_tags(session_key_tags); + builder.media(media); + builder.variants(variants); + builder.session_data(session_data); + builder.session_keys(session_keys); builder.unknown_tags(unknown_tags); builder.build().map_err(Error::builder) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index d43f5a7..ea1e57d 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -27,56 +27,56 @@ pub struct MediaPlaylist { /// /// This field is required. #[shorthand(enable(copy))] - target_duration_tag: ExtXTargetDuration, + target_duration: ExtXTargetDuration, /// Sets the [`ExtXMediaSequence`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - media_sequence_tag: Option, + media_sequence: Option, /// Sets the [`ExtXDiscontinuitySequence`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - discontinuity_sequence_tag: Option, + discontinuity_sequence: Option, /// Sets the [`ExtXPlaylistType`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - playlist_type_tag: Option, + playlist_type: Option, /// Sets the [`ExtXIFramesOnly`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - i_frames_only_tag: Option, + i_frames_only: Option, /// Sets the [`ExtXIndependentSegments`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - independent_segments_tag: Option, + independent_segments: Option, /// Sets the [`ExtXStart`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - start_tag: Option, + start: Option, /// Sets the [`ExtXEndList`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - end_list_tag: Option, + end_list: Option, /// A list of all [`MediaSegment`]s. /// /// # Note @@ -110,7 +110,7 @@ pub struct MediaPlaylist { impl MediaPlaylistBuilder { fn validate(&self) -> Result<(), String> { - if let Some(target_duration) = &self.target_duration_tag { + if let Some(target_duration) = &self.target_duration { self.validate_media_segments(target_duration.duration()) .map_err(|e| e.to_string())?; } @@ -189,14 +189,14 @@ impl MediaPlaylistBuilder { 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.target_duration, + self.media_sequence, + self.discontinuity_sequence, + self.playlist_type, + self.i_frames_only, + self.independent_segments, + self.start, + self.end_list, self.segments ] } @@ -210,14 +210,14 @@ impl MediaPlaylist { 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.target_duration, + self.media_sequence, + self.discontinuity_sequence, + self.playlist_type, + self.i_frames_only, + self.independent_segments, + self.start, + self.end_list, self.segments ] } @@ -231,29 +231,29 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } - writeln!(f, "{}", self.target_duration_tag)?; + writeln!(f, "{}", self.target_duration)?; - if let Some(value) = &self.media_sequence_tag { + if let Some(value) = &self.media_sequence { writeln!(f, "{}", value)?; } - if let Some(value) = &self.discontinuity_sequence_tag { + if let Some(value) = &self.discontinuity_sequence { writeln!(f, "{}", value)?; } - if let Some(value) = &self.playlist_type_tag { + if let Some(value) = &self.playlist_type { writeln!(f, "{}", value)?; } - if let Some(value) = &self.i_frames_only_tag { + if let Some(value) = &self.i_frames_only { writeln!(f, "{}", value)?; } - if let Some(value) = &self.independent_segments_tag { + if let Some(value) = &self.independent_segments { writeln!(f, "{}", value)?; } - if let Some(value) = &self.start_tag { + if let Some(value) = &self.start { writeln!(f, "{}", value)?; } @@ -261,7 +261,7 @@ impl fmt::Display for MediaPlaylist { write!(f, "{}", segment)?; } - if let Some(value) = &self.end_list_tag { + if let Some(value) = &self.end_list { writeln!(f, "{}", value)?; } @@ -341,10 +341,10 @@ fn parse_media_playlist( segment.date_range_tag(t); } Tag::ExtXTargetDuration(t) => { - builder.target_duration_tag(t); + builder.target_duration(t); } Tag::ExtXMediaSequence(t) => { - builder.media_sequence_tag(t); + builder.media_sequence(t); } Tag::ExtXDiscontinuitySequence(t) => { if segments.is_empty() { @@ -353,29 +353,28 @@ fn parse_media_playlist( if has_discontinuity_tag { return Err(Error::invalid_input()); } - builder.discontinuity_sequence_tag(t); + builder.discontinuity_sequence(t); } Tag::ExtXEndList(t) => { - builder.end_list_tag(t); + builder.end_list(t); } Tag::ExtXPlaylistType(t) => { - builder.playlist_type_tag(t); + builder.playlist_type(t); } Tag::ExtXIFramesOnly(t) => { - builder.i_frames_only_tag(t); + builder.i_frames_only(t); } Tag::ExtXMedia(_) - | Tag::ExtXStreamInf(_) - | Tag::ExtXIFrameStreamInf(_) + | Tag::VariantStream(_) | Tag::ExtXSessionData(_) | Tag::ExtXSessionKey(_) => { return Err(Error::unexpected_tag(tag)); } Tag::ExtXIndependentSegments(t) => { - builder.independent_segments_tag(t); + builder.independent_segments(t); } Tag::ExtXStart(t) => { - builder.start_tag(t); + builder.start(t); } Tag::ExtXVersion(_) => {} Tag::Unknown(_) => { diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs deleted file mode 100644 index 0343564..0000000 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ /dev/null @@ -1,263 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_more::{Deref, DerefMut}; - -use crate::attribute::AttributePairs; -use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder}; -use crate::utils::{quote, tag, unquote}; -use crate::{Error, RequiredVersion}; - -/// # [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. -/// -/// I-frames are encoded video frames, whose decoding -/// does not depend on any other frame. -/// -/// [`Master Playlist`]: crate::MasterPlaylist -/// [`Media Playlist`]: crate::MediaPlaylist -/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 -#[derive(Deref, DerefMut, PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXIFrameStreamInf { - uri: String, - #[deref] - #[deref_mut] - stream_inf: StreamInf, -} - -/// Builder for [`ExtXIFrameStreamInf`]. -#[derive(Default, Debug, Clone, PartialEq)] -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)?, - }) - } -} - -impl ExtXIFrameStreamInf { - pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; - - /// Makes a new [`ExtXIFrameStreamInf`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); - /// ``` - pub fn new(uri: T, bandwidth: u64) -> Self { - 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 - /// - /// ``` - /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); - /// assert_eq!(stream.uri(), &"https://www.example.com".to_string()); - /// ``` - /// - /// [`media playlist`]: crate::MediaPlaylist - pub const fn uri(&self) -> &String { &self.uri } - - /// Sets the `URI`, that identifies the associated [`media playlist`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// # - /// let mut stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); - /// - /// stream.set_uri("../new/uri"); - /// assert_eq!(stream.uri(), &"../new/uri".to_string()); - /// ``` - /// - /// [`media playlist`]: crate::MediaPlaylist - pub fn set_uri(&mut self, value: T) -> &mut Self { - self.uri = value.to_string(); - self - } -} - -/// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXIFrameStreamInf { - fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } -} - -impl fmt::Display for ExtXIFrameStreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={},{}", quote(&self.uri), self.stream_inf)?; - Ok(()) - } -} - -impl FromStr for ExtXIFrameStreamInf { - type Err = Error; - - fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - - let mut uri = None; - - for (key, value) in AttributePairs::new(input) { - if key == "URI" { - uri = Some(unquote(value)); - } - } - - let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; - - Ok(Self { - uri, - stream_inf: input.parse()?, - }) - } -} - -#[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(Some((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() { - assert_eq!( - ExtXIFrameStreamInf::new("foo", 1000).to_string(), - "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000" - .parse::() - .unwrap(), - ExtXIFrameStreamInf::new("foo", 1000) - ); - - assert!("garbage".parse::().is_err()); - } - - #[test] - fn test_required_version() { - assert_eq!( - ExtXIFrameStreamInf::new("foo", 1000).required_version(), - ProtocolVersion::V1 - ); - } - - #[test] - fn test_deref() { - assert_eq!( - ExtXIFrameStreamInf::new("https://www.example.com", 20).average_bandwidth(), - None - ) - } - - #[test] - fn test_deref_mut() { - assert_eq!( - ExtXIFrameStreamInf::new("https://www.example.com", 20) - .set_average_bandwidth(Some(4)) - .average_bandwidth(), - Some(4) - ) - } -} diff --git a/src/tags/master_playlist/mod.rs b/src/tags/master_playlist/mod.rs index 293509a..b95f91c 100644 --- a/src/tags/master_playlist/mod.rs +++ b/src/tags/master_playlist/mod.rs @@ -1,11 +1,9 @@ -mod i_frame_stream_inf; mod media; mod session_data; mod session_key; -mod stream_inf; +mod variant_stream; -pub use i_frame_stream_inf::*; pub use media::*; pub use session_data::*; pub use session_key::*; -pub use stream_inf::*; +pub use variant_stream::*; diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs deleted file mode 100644 index fa51342..0000000 --- a/src/tags/master_playlist/stream_inf.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_more::{Deref, DerefMut}; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{ - ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder, -}; -use crate::utils::{quote, tag, unquote}; -use crate::{Error, RequiredVersion}; - -/// # [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(Deref, DerefMut, ShortHand, PartialOrd, Debug, Clone, PartialEq)] -#[shorthand(enable(must_use, into))] -pub struct ExtXStreamInf { - /// The `URI` that identifies the associated media playlist. - uri: String, - #[shorthand(enable(skip))] - frame_rate: Option, - /// The group identifier for the audio in the variant stream. - audio: Option, - /// The group identifier for the subtitles in the variant stream. - subtitles: Option, - /// The value of the [`ClosedCaptions`] attribute. - closed_captions: Option, - #[shorthand(enable(skip))] - #[deref] - #[deref_mut] - 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)?, - }) - } -} - -impl ExtXStreamInf { - pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; - - /// Creates a new [`ExtXStreamInf`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// ``` - pub fn new(uri: T, bandwidth: u64) -> Self { - Self { - uri: uri.to_string(), - frame_rate: None, - audio: None, - subtitles: None, - closed_captions: None, - stream_inf: StreamInf::new(bandwidth), - } - } - - /// Returns a builder for [`ExtXStreamInf`]. - pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() } - - /// The maximum frame rate for all the video in the variant stream. - #[must_use] - #[inline] - pub fn frame_rate(&self) -> Option { self.frame_rate.map(DecimalFloatingPoint::as_f64) } - - /// The maximum frame rate for all the video in the variant stream. - /// - /// # Panic - /// - /// This function panics, if the float is infinite or negative. - pub fn set_frame_rate>(&mut self, value_0: Option) -> &mut Self { - self.frame_rate = value_0.map(|v| { - DecimalFloatingPoint::new(v.into()).expect("the float must be positive and finite") - }); - self - } -} - -/// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXStreamInf { - fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } -} - -impl fmt::Display for ExtXStreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.stream_inf)?; - - if let Some(value) = &self.frame_rate { - write!(f, ",FRAME-RATE={:.3}", value.as_f64())?; - } - - if let Some(value) = &self.audio { - write!(f, ",AUDIO={}", quote(value))?; - } - - if let Some(value) = &self.subtitles { - write!(f, ",SUBTITLES={}", quote(value))?; - } - - if let Some(value) = &self.closed_captions { - write!(f, ",CLOSED-CAPTIONS={}", value)?; - } - - write!(f, "\n{}", self.uri)?; - Ok(()) - } -} - -impl FromStr for ExtXStreamInf { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut lines = input.lines(); - let first_line = lines - .next() - .ok_or_else(|| Error::missing_value("first_line"))?; - let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?; - - let input = tag(first_line, Self::PREFIX)?; - - let mut frame_rate = None; - let mut audio = None; - let mut subtitles = None; - let mut closed_captions = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "FRAME-RATE" => frame_rate = Some((value.parse())?), - "AUDIO" => audio = Some(unquote(value)), - "SUBTITLES" => subtitles = Some(unquote(value)), - "CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()), - _ => {} - } - } - - Ok(Self { - uri: uri.to_string(), - frame_rate, - audio, - subtitles, - closed_captions, - stream_inf: input.parse()?, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_parser() { - let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com" - .parse::() - .unwrap(); - - assert_eq!( - stream_inf, - ExtXStreamInf::new("http://www.example.com", 1000) - ); - } - - #[test] - fn test_display() { - assert_eq!( - ExtXStreamInf::new("http://www.example.com/", 1000).to_string(), - "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() - ); - } - - #[test] - fn test_required_version() { - assert_eq!( - ProtocolVersion::V1, - ExtXStreamInf::new("http://www.example.com", 1000).required_version() - ); - } - - #[test] - fn test_deref() { - assert_eq!( - ExtXStreamInf::new("http://www.example.com", 1000).bandwidth(), - 1000 - ); - } - - #[test] - fn test_deref_mut() { - assert_eq!( - ExtXStreamInf::new("http://www.example.com", 1000) - .set_bandwidth(1) - .bandwidth(), - 1 - ); - } -} diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs new file mode 100644 index 0000000..ecbd2c4 --- /dev/null +++ b/src/tags/master_playlist/variant_stream.rs @@ -0,0 +1,286 @@ +use core::fmt; +use core::ops::Deref; +use core::str::FromStr; + +use crate::attribute::AttributePairs; +use crate::traits::RequiredVersion; +use crate::types::{ClosedCaptions, ProtocolVersion, StreamData, UFloat}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + +/// A server MAY offer multiple Media Playlist files to provide different +/// encodings of the same presentation. If it does so, it SHOULD provide +/// a Master Playlist file that lists each Variant Stream to allow +/// clients to switch between encodings dynamically. +/// +/// Master Playlists describe regular Variant Streams with EXT-X-STREAM- +/// INF tags and I-frame Variant Streams with EXT-X-I-FRAME-STREAM-INF +/// tags. +/// +/// If an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag contains +/// the CODECS attribute, the attribute value MUST include every media +/// format [RFC6381] present in any Media Segment in any of the +/// Renditions specified by the Variant Stream. +/// +/// The server MUST meet the following constraints when producing Variant +/// Streams in order to allow clients to switch between them seamlessly: +/// +/// o Each Variant Stream MUST present the same content. +/// +/// +/// o Matching content in Variant Streams MUST have matching timestamps. +/// This allows clients to synchronize the media. +/// +/// o Matching content in Variant Streams MUST have matching +/// Discontinuity Sequence Numbers (see Section 4.3.3.3). +/// +/// o Each Media Playlist in each Variant Stream MUST have the same +/// target duration. The only exceptions are SUBTITLES Renditions and +/// Media Playlists containing an EXT-X-I-FRAMES-ONLY tag, which MAY +/// have different target durations if they have an EXT-X-PLAYLIST- +/// TYPE of VOD. +/// +/// o Content that appears in a Media Playlist of one Variant Stream but +/// not in another MUST appear either at the beginning or at the end +/// of the Media Playlist file and MUST NOT be longer than the target +/// duration. +/// +/// o If any Media Playlists have an EXT-X-PLAYLIST-TYPE tag, all Media +/// Playlists MUST have an EXT-X-PLAYLIST-TYPE tag with the same +/// value. +/// +/// o If the Playlist contains an EXT-X-PLAYLIST-TYPE tag with the value +/// of VOD, the first segment of every Media Playlist in every Variant +/// Stream MUST start at the same media timestamp. +/// +/// o If any Media Playlist in a Master Playlist contains an EXT-X- +/// PROGRAM-DATE-TIME tag, then all Media Playlists in that Master +/// Playlist MUST contain EXT-X-PROGRAM-DATE-TIME tags with consistent +/// mappings of date and time to media timestamps. +/// +/// o Each Variant Stream MUST contain the same set of Date Ranges, each +/// one identified by an EXT-X-DATERANGE tag(s) with the same ID +/// attribute value and containing the same set of attribute/value +/// pairs. +/// +/// In addition, for broadest compatibility, Variant Streams SHOULD +/// contain the same encoded audio bitstream. This allows clients to +/// switch between Variant Streams without audible glitching. +/// +/// The rules for Variant Streams also apply to alternative Renditions +/// (see Section 4.3.4.2.1). +/// +/// [RFC6381]: https://tools.ietf.org/html/rfc6381 +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub enum VariantStream { + ExtXIFrame { + /// The URI identifies the I-frame [`MediaPlaylist`] file. + /// That Playlist file must contain an [`ExtXIFramesOnly`] tag. + /// + /// # Note + /// + /// This field is required. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + uri: String, + /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and + /// [`VariantStream::ExtXIFrame`]. + /// + /// # Note + /// + /// This field is optional. + stream_data: StreamData, + }, + ExtXStreamInf { + /// The URI specifies a [`MediaPlaylist`] that carries a rendition of + /// the [`VariantStream`]. Clients that do not support multiple video + /// renditions should play this rendition. + /// + /// # Note + /// + /// This field is required. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + uri: String, + /// The value is an unsigned float describing the maximum frame + /// rate for all the video in the [`VariantStream`]. + /// + /// # Note + /// + /// Specifying the frame rate is optional, but is recommended if the + /// [`VariantStream`] includes video. It should be specified if any + /// video exceeds 30 frames per second. + frame_rate: Option, + /// It indicates the set of audio renditions that should be used when + /// playing the presentation. + /// + /// It must match the value of the [`ExtXMedia::group_id`] of an + /// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose + /// [`ExtXMedia::media_type`] is [`MediaType::Audio`]. + /// + /// # Note + /// + /// This field is optional. + /// + /// [`ExtXMedia`]: crate::tags::ExtXMedia + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + /// [`MediaType::Audio`]: crate::types::MediaType::Audio + audio: Option, + /// It indicates the set of subtitle renditions that can be used when + /// playing the presentation. + /// + /// It must match the value of the [`ExtXMedia::group_id`] of an + /// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose + /// [`ExtXMedia::media_type`] is [`MediaType::Subtitles`]. + /// + /// # Note + /// + /// This field is optional. + /// + /// [`ExtXMedia`]: crate::tags::ExtXMedia + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + /// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles + subtitles: Option, + /// It indicates the set of closed-caption renditions that can be used + /// when playing the presentation. + /// + /// # Note + /// + /// This field is optional. + closed_captions: Option, + /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and + /// [`VariantStream::ExtXIFrame`]. + /// + /// # Note + /// + /// This field is optional. + stream_data: StreamData, + }, +} + +impl VariantStream { + pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; + pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:"; +} + +/// This tag requires [`ProtocolVersion::V1`]. +impl RequiredVersion for VariantStream { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } +} + +impl fmt::Display for VariantStream { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Self::ExtXIFrame { uri, stream_data } => { + write!(f, "{}", Self::PREFIX_EXTXIFRAME)?; + write!(f, "URI={},{}", quote(uri), stream_data)?; + } + Self::ExtXStreamInf { + uri, + frame_rate, + audio, + subtitles, + closed_captions, + stream_data, + } => { + write!(f, "{}{}", Self::PREFIX_EXTXSTREAMINF, stream_data)?; + + if let Some(value) = frame_rate { + write!(f, ",FRAME-RATE={:.3}", value.as_f32())?; + } + + if let Some(value) = audio { + write!(f, ",AUDIO={}", quote(value))?; + } + + if let Some(value) = subtitles { + write!(f, ",SUBTITLES={}", quote(value))?; + } + + if let Some(value) = closed_captions { + write!(f, ",CLOSED-CAPTIONS={}", value)?; + } + + write!(f, "\n{}", uri)?; + } + } + + Ok(()) + } +} + +impl FromStr for VariantStream { + type Err = Error; + + fn from_str(input: &str) -> Result { + if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) { + let uri = AttributePairs::new(input) + .find_map(|(key, value)| { + if key == "URI" { + Some(unquote(value)) + } else { + None + } + }) + .ok_or_else(|| Error::missing_value("URI"))?; + + Ok(Self::ExtXIFrame { + uri, + stream_data: input.parse()?, + }) + } else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) { + let mut lines = input.lines(); + let first_line = lines + .next() + .ok_or_else(|| Error::missing_value("first_line"))?; + let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?; + + let mut frame_rate = None; + let mut audio = None; + let mut subtitles = None; + let mut closed_captions = None; + + for (key, value) in AttributePairs::new(first_line) { + match key { + "FRAME-RATE" => frame_rate = Some(value.parse()?), + "AUDIO" => audio = Some(unquote(value)), + "SUBTITLES" => subtitles = Some(unquote(value)), + "CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()), + _ => {} + } + } + + Ok(Self::ExtXStreamInf { + uri: uri.to_string(), + frame_rate, + audio, + subtitles, + closed_captions, + stream_data: first_line.parse()?, + }) + } else { + // TODO: custom error type? + attach input data + Err(Error::custom(format!( + "invalid start of input, expected either {:?} or {:?}", + Self::PREFIX_EXTXIFRAME, + Self::PREFIX_EXTXSTREAMINF + ))) + } + } +} + +impl Deref for VariantStream { + type Target = StreamData; + + fn deref(&self) -> &Self::Target { + match &self { + Self::ExtXIFrame { stream_data, .. } | Self::ExtXStreamInf { stream_data, .. } => { + stream_data + } + } + } +} diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 93bf543..ae2350b 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -5,14 +5,32 @@ use std::str::FromStr; use crate::utils::{quote, unquote}; /// The identifier of a closed captions group or its absence. -/// -/// See: [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[allow(missing_docs)] +#[non_exhaustive] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum ClosedCaptions { + /// It indicates the set of closed-caption renditions that can be used when + /// playing the presentation. + /// + /// The [`String`] must match [`ExtXMedia::group_id`] elsewhere in the + /// Playlist and it's [`ExtXMedia::media_type`] must be + /// [`MediaType::ClosedCaptions`]. + /// + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + /// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions GroupId(String), + /// [`ClosedCaptions::None`] indicates that there are no closed captions in + /// any [`VariantStream`] in the [`MasterPlaylist`], therefore all + /// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a + /// value of [`ClosedCaptions::None`]. + /// + /// Having [`ClosedCaptions`] in one [`VariantStream`] but not in another + /// can trigger playback inconsistencies. + /// + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`VariantStream`]: crate::tags::VariantStream + /// [`VariantStream::ExtXStreamInf`]: + /// crate::tags::VariantStream::ExtXStreamInf None, } diff --git a/src/types/mod.rs b/src/types/mod.rs index e0af60d..2f00d48 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -11,7 +11,7 @@ mod key_format_versions; mod media_type; mod protocol_version; mod resolution; -mod stream_inf; +mod stream_data; mod value; mod float; @@ -29,7 +29,7 @@ pub use key_format_versions::*; pub use media_type::*; pub use protocol_version::*; pub use resolution::*; -pub use stream_inf::*; +pub use stream_data::*; pub use value::*; pub use float::Float; diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs new file mode 100644 index 0000000..cb4e7bd --- /dev/null +++ b/src/types/stream_data.rs @@ -0,0 +1,370 @@ +use core::fmt; +use core::str::FromStr; + +use derive_builder::Builder; +use shorthand::ShortHand; + +use crate::attribute::AttributePairs; +use crate::types::{HdcpLevel, Resolution}; +use crate::utils::{quote, unquote}; +use crate::Error; + +/// The [`StreamData`] struct contains the data that is shared between both +/// variants of the [`VariantStream`]. +/// +/// [`VariantStream`]: crate::tags::VariantStream +#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] +#[builder(setter(strip_option))] +#[builder(derive(Debug, PartialEq))] +#[shorthand(enable(must_use, into))] +pub struct StreamData { + /// The peak segment bitrate of the [`VariantStream`] in bits per second. + /// + /// If all the [`MediaSegment`]s in a [`VariantStream`] have already been + /// created, the bandwidth value must be the largest sum of peak segment + /// bitrates that is produced by any playable combination of renditions. + /// + /// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just + /// the peak segment bit rate of that [`MediaPlaylist`].) + /// + /// An inaccurate value can cause playback stalls or prevent clients from + /// playing the variant. If the [`MasterPlaylist`] is to be made available + /// before all [`MediaSegment`]s in the presentation have been encoded, the + /// bandwidth value should be the bandwidth value of a representative + /// period of similar content, encoded using the same settings. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_bandwidth(5); + /// assert_eq!(stream.bandwidth(), 5); + /// ``` + /// + /// # Note + /// + /// This field is required. + /// + /// [`VariantStream`]: crate::tags::VariantStream + /// [`MediaSegment`]: crate::MediaSegment + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist + #[shorthand(disable(into))] + bandwidth: u64, + /// The average bandwidth of the stream in bits per second. + /// + /// It represents the average segment bitrate of the [`VariantStream`]. If + /// all the [`MediaSegment`]s in a [`VariantStream`] have already been + /// created, the average bandwidth must be the largest sum of average + /// segment bitrates that is produced by any playable combination of + /// renditions. + /// + /// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just + /// the average segment bitrate of that [`MediaPlaylist`].) + /// + /// An inaccurate value can cause playback stalls or prevent clients from + /// playing the variant. If the [`MasterPlaylist`] is to be made available + /// before all [`MediaSegment`]s in the presentation have been encoded, the + /// average bandwidth should be the average bandwidth of a representative + /// period of similar content, encoded using the same settings. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_average_bandwidth(Some(300)); + /// assert_eq!(stream.average_bandwidth(), Some(300)); + /// ``` + /// + /// # Note + /// + /// This field is optional. + /// + /// [`MediaSegment`]: crate::MediaSegment + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default)] + #[shorthand(enable(copy), disable(into, option_as_ref))] + average_bandwidth: Option, + /// A string that represents a list of formats, where each format specifies + /// a media sample type that is present in one or more renditions specified + /// by the [`VariantStream`]. + /// + /// Valid format identifiers are those in the ISO Base Media File Format + /// Name Space defined by "The 'Codecs' and 'Profiles' Parameters for + /// "Bucket" Media Types" [RFC6381]. + /// + /// For example, a stream containing AAC low complexity (AAC-LC) audio and + /// H.264 Main Profile Level 3.0 video would have a codecs value of + /// "mp4a.40.2,avc1.4d401e". + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + /// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string())); + /// ``` + /// + /// # Note + /// + /// This field is optional, but every instance of + /// [`VariantStream::ExtXStreamInf`] should include a codecs attribute. + /// + /// [`VariantStream`]: crate::tags::VariantStream + /// [`VariantStream::ExtXStreamInf`]: + /// crate::tags::VariantStream::ExtXStreamInf + /// [RFC6381]: https://tools.ietf.org/html/rfc6381 + #[builder(default, setter(into))] + codecs: Option, + /// The resolution of the stream. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// use hls_m3u8::types::Resolution; + /// + /// let mut stream = StreamData::new(20); + /// + /// stream.set_resolution(Some((1920, 1080))); + /// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080))); + /// # stream.set_resolution(Some((1280, 10))); + /// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10))); + /// ``` + /// + /// # Note + /// + /// This field is optional, but it is recommended if the [`VariantStream`] + /// includes video. + /// + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default, setter(into))] + #[shorthand(enable(copy))] + resolution: Option, + /// High-bandwidth Digital Content Protection level of the + /// [`VariantStream`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// use hls_m3u8::types::HdcpLevel; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_hdcp_level(Some(HdcpLevel::None)); + /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); + /// ``` + /// + /// # Note + /// + /// This field is optional. + /// + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default)] + #[shorthand(enable(copy), disable(into))] + hdcp_level: Option, + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. + /// + /// It must match the value of the [`ExtXMedia::group_id`] attribute + /// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose + /// [`ExtXMedia::media_type`] attribute is video. It indicates the set of + /// video renditions that should be used when playing the presentation. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_video(Some("video_01")); + /// assert_eq!(stream.video(), Some(&"video_01".to_string())); + /// ``` + /// + /// # Note + /// + /// This field is optional. + /// + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`ExtXMedia`]: crate::tags::ExtXMedia + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + #[builder(default, setter(into))] + video: Option, +} + +impl StreamData { + /// Creates a new [`StreamData`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let stream = StreamData::new(20); + /// ``` + pub const fn new(bandwidth: u64) -> Self { + Self { + bandwidth, + average_bandwidth: None, + codecs: None, + resolution: None, + hdcp_level: None, + video: None, + } + } + + /// Returns a builder for [`StreamData`]. + /// + /// # Example + /// + /// ``` + /// use hls_m3u8::types::{HdcpLevel, StreamData}; + /// + /// StreamData::builder() + /// .bandwidth(200) + /// .average_bandwidth(15) + /// .codecs("mp4a.40.2,avc1.4d401e") + /// .resolution((1920, 1080)) + /// .hdcp_level(HdcpLevel::Type0) + /// .video("video_01") + /// .build()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() } +} + +impl fmt::Display for StreamData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "BANDWIDTH={}", self.bandwidth)?; + + if let Some(value) = &self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", value)?; + } + if let Some(value) = &self.codecs { + write!(f, ",CODECS={}", quote(value))?; + } + if let Some(value) = &self.resolution { + write!(f, ",RESOLUTION={}", value)?; + } + if let Some(value) = &self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", value)?; + } + if let Some(value) = &self.video { + write!(f, ",VIDEO={}", quote(value))?; + } + Ok(()) + } +} + +impl FromStr for StreamData { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut bandwidth = None; + let mut average_bandwidth = None; + let mut codecs = None; + let mut resolution = None; + let mut hdcp_level = None; + let mut video = None; + + for (key, value) in AttributePairs::new(input) { + match key { + "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), + "AVERAGE-BANDWIDTH" => { + average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) + } + "CODECS" => codecs = Some(unquote(value)), + "RESOLUTION" => resolution = Some(value.parse()?), + "HDCP-LEVEL" => { + hdcp_level = Some(value.parse::().map_err(Error::strum)?) + } + "VIDEO" => video = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized + // AttributeName. + } + } + } + + let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?; + + Ok(Self { + bandwidth, + average_bandwidth, + codecs, + resolution, + hdcp_level, + video, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_display() { + let mut stream_data = StreamData::new(200); + stream_data.set_average_bandwidth(Some(15)); + stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_data.set_resolution(Some((1920, 1080))); + stream_data.set_hdcp_level(Some(HdcpLevel::Type0)); + stream_data.set_video(Some("video")); + + assert_eq!( + stream_data.to_string(), + concat!( + "BANDWIDTH=200,", + "AVERAGE-BANDWIDTH=15,", + "CODECS=\"mp4a.40.2,avc1.4d401e\",", + "RESOLUTION=1920x1080,", + "HDCP-LEVEL=TYPE-0,", + "VIDEO=\"video\"" + ) + .to_string() + ); + } + + #[test] + fn test_parser() { + let mut stream_data = StreamData::new(200); + stream_data.set_average_bandwidth(Some(15)); + stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_data.set_resolution(Some((1920, 1080))); + stream_data.set_hdcp_level(Some(HdcpLevel::Type0)); + stream_data.set_video(Some("video")); + + assert_eq!( + stream_data, + concat!( + "BANDWIDTH=200,", + "AVERAGE-BANDWIDTH=15,", + "CODECS=\"mp4a.40.2,avc1.4d401e\",", + "RESOLUTION=1920x1080,", + "HDCP-LEVEL=TYPE-0,", + "VIDEO=\"video\"" + ) + .parse() + .unwrap() + ); + + assert!("garbage".parse::().is_err()); + } +} diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs deleted file mode 100644 index 15d1af3..0000000 --- a/src/types/stream_inf.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_builder::Builder; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{HdcpLevel, Resolution}; -use crate::utils::{quote, unquote}; -use crate::Error; - -/// # [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(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] -#[builder(setter(into, strip_option))] -#[builder(derive(Debug, PartialEq))] -#[shorthand(enable(must_use, into))] -pub struct StreamInf { - /// The peak segment bit rate of the variant stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_bandwidth(5); - /// assert_eq!(stream.bandwidth(), 5); - /// ``` - /// - /// # Note - /// - /// This field is required. - #[shorthand(disable(into))] - bandwidth: u64, - /// The average bandwidth of the stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_average_bandwidth(Some(300)); - /// assert_eq!(stream.average_bandwidth(), Some(300)); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - #[shorthand(enable(copy), disable(into, option_as_ref))] - average_bandwidth: Option, - /// A string that represents the list of codec types contained the variant - /// stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - /// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string())); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - codecs: Option, - /// The resolution of the stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// use hls_m3u8::types::Resolution; - /// - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_resolution(Some((1920, 1080))); - /// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080))); - /// # stream.set_resolution(Some((1280, 10))); - /// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10))); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - #[shorthand(enable(copy))] - resolution: Option, - /// High-bandwidth Digital Content Protection level of the variant stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::{HdcpLevel, StreamInf}; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_hdcp_level(Some(HdcpLevel::None)); - /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - #[shorthand(enable(copy), disable(into))] - hdcp_level: Option, - /// It indicates the set of video renditions, that should be used when - /// playing the presentation. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - video: Option, -} - -impl StreamInf { - /// Creates a new [`StreamInf`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// ``` - pub const fn new(bandwidth: u64) -> Self { - Self { - bandwidth, - average_bandwidth: None, - codecs: None, - resolution: None, - hdcp_level: None, - video: None, - } - } -} - -impl fmt::Display for StreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BANDWIDTH={}", self.bandwidth)?; - - if let Some(value) = &self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", value)?; - } - if let Some(value) = &self.codecs { - write!(f, ",CODECS={}", quote(value))?; - } - if let Some(value) = &self.resolution { - write!(f, ",RESOLUTION={}", value)?; - } - if let Some(value) = &self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", value)?; - } - if let Some(value) = &self.video { - write!(f, ",VIDEO={}", quote(value))?; - } - Ok(()) - } -} - -impl FromStr for StreamInf { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut bandwidth = None; - let mut average_bandwidth = None; - let mut codecs = None; - let mut resolution = None; - let mut hdcp_level = None; - let mut video = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), - "AVERAGE-BANDWIDTH" => { - average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) - } - "CODECS" => codecs = Some(unquote(value)), - "RESOLUTION" => resolution = Some(value.parse()?), - "HDCP-LEVEL" => { - hdcp_level = Some(value.parse::().map_err(Error::strum)?) - } - "VIDEO" => video = Some(unquote(value)), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized - // AttributeName. - } - } - } - - let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?; - - Ok(Self { - bandwidth, - average_bandwidth, - codecs, - resolution, - hdcp_level, - video, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_display() { - let mut stream_inf = StreamInf::new(200); - stream_inf.set_average_bandwidth(Some(15)); - stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - stream_inf.set_resolution(Some((1920, 1080))); - stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); - stream_inf.set_video(Some("video")); - - assert_eq!( - stream_inf.to_string(), - "BANDWIDTH=200,\ - AVERAGE-BANDWIDTH=15,\ - CODECS=\"mp4a.40.2,avc1.4d401e\",\ - RESOLUTION=1920x1080,\ - HDCP-LEVEL=TYPE-0,\ - VIDEO=\"video\"" - .to_string() - ); - } - - #[test] - fn test_parser() { - let mut stream_inf = StreamInf::new(200); - stream_inf.set_average_bandwidth(Some(15)); - stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - stream_inf.set_resolution(Some((1920, 1080))); - stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); - stream_inf.set_video(Some("video")); - - assert_eq!( - stream_inf, - "BANDWIDTH=200,\ - AVERAGE-BANDWIDTH=15,\ - CODECS=\"mp4a.40.2,avc1.4d401e\",\ - RESOLUTION=1920x1080,\ - HDCP-LEVEL=TYPE-0,\ - VIDEO=\"video\"" - .parse() - .unwrap() - ); - - assert_eq!( - stream_inf, - "BANDWIDTH=200,\ - AVERAGE-BANDWIDTH=15,\ - CODECS=\"mp4a.40.2,avc1.4d401e\",\ - RESOLUTION=1920x1080,\ - HDCP-LEVEL=TYPE-0,\ - VIDEO=\"video\",\ - UNKNOWN=\"value\"" - .parse() - .unwrap() - ); - - assert!("garbage".parse::().is_err()); - } -} diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index ad4d180..1313fcc 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -9,7 +9,7 @@ use crate::Error; /// with a negative float (ex. `-1.1`), [`NaN`], [`INFINITY`] or /// [`NEG_INFINITY`]. /// -/// [`NaN`]: core::f32::NaN +/// [`NaN`]: core::f32::NAN /// [`INFINITY`]: core::f32::INFINITY /// [`NEG_INFINITY`]: core::f32::NEG_INFINITY #[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)] diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs index fb2ecfc..2d35d35 100644 --- a/tests/master_playlist.rs +++ b/tests/master_playlist.rs @@ -1,5 +1,5 @@ -use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf}; -use hls_m3u8::types::MediaType; +use hls_m3u8::tags::{ExtXMedia, VariantStream}; +use hls_m3u8::types::{MediaType, StreamData}; use hls_m3u8::MasterPlaylist; use pretty_assertions::assert_eq; @@ -7,45 +7,71 @@ 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::() - .unwrap(); + let master_playlist = concat!( + "#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::() + .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(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "http://example.com/low.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(1280000) + .average_bandwidth(1000000) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/mid.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(2560000) + .average_bandwidth(2000000) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/hi.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(7680000) + .average_bandwidth(6000000) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/audio-only.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, ]) .build() .unwrap(), @@ -56,62 +82,75 @@ fn test_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::() - .unwrap(); + let master_playlist = concat!( + "#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", + // this one: + "#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::() + .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(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "low/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(1280000) + }, + VariantStream::ExtXIFrame { + uri: "low/iframe.m3u8".into(), + stream_data: StreamData::new(86000), + }, + VariantStream::ExtXStreamInf { + uri: "mid/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(2560000) + }, + VariantStream::ExtXIFrame { + uri: "mid/iframe.m3u8".into(), + stream_data: StreamData::new(150000), + }, + VariantStream::ExtXStreamInf { + uri: "hi/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(7680000) + }, + VariantStream::ExtXIFrame { + uri: "hi/iframe.m3u8".into(), + stream_data: StreamData::new(550000), + }, + VariantStream::ExtXStreamInf { + uri: "audio-only.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, ]) .build() .unwrap(), @@ -123,33 +162,32 @@ fn test_master_playlist_with_i_frames() { 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::() - .unwrap(); + let master_playlist = concat!( + "#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::() + .unwrap(); assert_eq!( MasterPlaylist::builder() - .media_tags(vec![ + .media(vec![ ExtXMedia::builder() .media_type(MediaType::Audio) .group_id("aac") @@ -181,35 +219,55 @@ fn test_master_playlist_with_alternative_audio() { .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(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "low/video-only.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(1280000) + .codecs("...") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "mid/video-only.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(2560000) + .codecs("...") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "hi/video-only.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(7680000) + .codecs("...") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "main/english-audio.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, ]) .build() .unwrap(), @@ -220,48 +278,39 @@ fn test_master_playlist_with_alternative_audio() { #[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::() - .unwrap(); + let master_playlist = concat!( + "#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\"\n", + "hi/main/audio-video.m3u8" + ) + .parse::() + .unwrap(); assert_eq!( MasterPlaylist::builder() - .media_tags(vec![ + .media(vec![ // low ExtXMedia::builder() .media_type(MediaType::Video) @@ -341,28 +390,46 @@ fn test_master_playlist_with_alternative_video() { .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(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "low/main/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(1280000) + .codecs("...") + .video("low") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "mid/main/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(2560000) + .codecs("...") + .video("mid") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "hi/main/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(7680000) + .codecs("...") + .video("hi") + .build() + .unwrap() + }, ]) .build() .unwrap(), diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index 7f8a860..c87bff2 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -6,26 +6,28 @@ 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::() - .unwrap(); + let media_playlist = concat!( + "#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\n" + ) + .parse::() + .unwrap(); assert_eq!( MediaPlaylist::builder() - .target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10))) - .media_sequence_tag(ExtXMediaSequence::new(0)) + .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .media_sequence(ExtXMediaSequence::new(0)) .segments(vec![ MediaSegment::builder() .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) diff --git a/tests/playlist.rs b/tests/playlist.rs index 25ccabd..c272a4a 100644 --- a/tests/playlist.rs +++ b/tests/playlist.rs @@ -7,18 +7,19 @@ use std::time::Duration; #[test] fn test_simple_playlist() { - let playlist = r#" - #EXTM3U - #EXT-X-TARGETDURATION:5220 - #EXTINF:0, - http://media.example.com/entire1.ts - #EXTINF:5220, - http://media.example.com/entire2.ts - #EXT-X-ENDLIST"#; + let playlist = concat!( + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:5220\n", + "#EXTINF:0,\n", + "http://media.example.com/entire1.ts\n", + "#EXTINF:5220,\n", + "http://media.example.com/entire2.ts\n", + "#EXT-X-ENDLIST\n" + ); let media_playlist = playlist.parse::().unwrap(); assert_eq!( - media_playlist.target_duration_tag(), + media_playlist.target_duration(), ExtXTargetDuration::new(Duration::from_secs(5220)) ); From 3a388e398584b55e35155d8a9d0a2508cf4e0d8e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 13:21:48 +0100 Subject: [PATCH 021/112] improvements to code --- src/lib.rs | 2 +- src/media_playlist.rs | 1 + src/tags/master_playlist/media.rs | 16 +++--- src/tags/master_playlist/session_data.rs | 20 ++++--- src/tags/master_playlist/session_key.rs | 6 +-- .../media_playlist/discontinuity_sequence.rs | 54 ++++++++----------- src/tags/media_segment/date_range.rs | 10 ++-- src/tags/media_segment/inf.rs | 8 +-- src/tags/media_segment/key.rs | 11 ++-- src/tags/media_segment/map.rs | 8 +-- src/tags/shared/independent_segments.rs | 5 ++ src/tags/shared/mod.rs | 8 +++ src/types/decryption_key.rs | 22 +++++--- src/types/encryption_method.rs | 15 +++--- src/types/hdcp_level.rs | 1 + src/types/in_stream_id.rs | 4 +- src/types/key_format.rs | 3 +- src/types/key_format_versions.rs | 7 ++- src/types/media_type.rs | 1 + src/types/protocol_version.rs | 1 + src/types/value.rs | 3 +- src/utils.rs | 26 +++++---- 22 files changed, 132 insertions(+), 100 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fc1726f..3659b68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ missing_docs, missing_copy_implementations, missing_debug_implementations, - trivial_casts, // TODO (needed?) + trivial_casts, trivial_numeric_casts )] //! [HLS] m3u8 parser/generator. diff --git a/src/media_playlist.rs b/src/media_playlist.rs index ea1e57d..124e45f 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -391,6 +391,7 @@ fn parse_media_playlist( segment = MediaSegment::builder(); has_partial_segment = false; } + _ => {} } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 7a5e2cc..d122930 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -12,10 +12,10 @@ 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. +/// that contain alternative renditions of the same content. /// /// For example, three [`ExtXMedia`] tags can be used to identify audio-only -/// [`Media Playlist`]s, that contain English, French, and Spanish Renditions +/// [`Media Playlist`]s, that contain English, French, and Spanish renditions /// of the same presentation. Or, two [`ExtXMedia`] tags can be used to /// identify video-only [`Media Playlist`]s that show two different camera /// angles. @@ -35,7 +35,7 @@ pub struct ExtXMedia { /// This attribute is **required**. #[shorthand(enable(copy))] media_type: MediaType, - /// The `URI` that identifies the [`Media Playlist`]. + /// An `URI` to a [`Media Playlist`]. /// /// # Note /// @@ -184,14 +184,18 @@ impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; /// Makes a new [`ExtXMedia`] tag. - pub fn new(media_type: MediaType, group_id: T, name: T) -> Self { + pub fn new(media_type: MediaType, group_id: T, name: K) -> Self + where + T: Into, + K: Into, + { Self { media_type, uri: None, - group_id: group_id.to_string(), + group_id: group_id.into(), language: None, assoc_language: None, - name: name.to_string(), + name: name.into(), is_default: false, is_autoselect: false, is_forced: false, diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 206b4da..9e53e8b 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -13,7 +13,7 @@ use crate::{Error, RequiredVersion}; #[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] pub enum SessionData { /// A String, that contains the data identified by - /// [`data_id`]. + /// [`data_id`](ExtXSessionData::data_id). /// If a [`language`] is specified, the value /// should contain a human-readable string written in the specified /// language. @@ -51,7 +51,7 @@ pub struct ExtXSessionData { /// /// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation data_id: String, - /// The data associated with the [`data_id`]. + /// The data associated with the [`data_id`](ExtXSessionData::data_id). /// For more information look [`here`](SessionData). /// /// # Note @@ -70,6 +70,7 @@ impl ExtXSessionData { /// Makes a new [`ExtXSessionData`] tag. /// /// # Example + /// /// ``` /// use hls_m3u8::tags::{ExtXSessionData, SessionData}; /// @@ -78,9 +79,9 @@ impl ExtXSessionData { /// SessionData::Uri("https://www.example.com/".to_string()), /// ); /// ``` - pub fn new(data_id: T, data: SessionData) -> Self { + pub fn new>(data_id: T, data: SessionData) -> Self { Self { - data_id: data_id.to_string(), + data_id: data_id.into(), data, language: None, } @@ -89,6 +90,7 @@ impl ExtXSessionData { /// Returns a new Builder for [`ExtXSessionData`]. /// /// # Example + /// /// ``` /// use hls_m3u8::tags::{ExtXSessionData, SessionData}; /// @@ -123,11 +125,15 @@ impl ExtXSessionData { /// "english", /// ); /// ``` - pub fn with_language(data_id: T, data: SessionData, language: T) -> Self { + pub fn with_language(data_id: T, data: SessionData, language: K) -> Self + where + T: Into, + K: Into, + { Self { - data_id: data_id.to_string(), + data_id: data_id.into(), data, - language: Some(language.to_string()), + language: Some(language.into()), } } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index a46b015..ecbd270 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -40,7 +40,7 @@ impl ExtXSessionKey { /// /// let session_key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// ``` - pub fn new(method: EncryptionMethod, uri: T) -> Self { + pub fn new>(method: EncryptionMethod, uri: T) -> Self { if method == EncryptionMethod::None { panic!("The EncryptionMethod is not allowed to be None"); } @@ -49,8 +49,8 @@ impl ExtXSessionKey { } } -/// This tag requires the version returned by -/// [`DecryptionKey::required_version`]. +/// This tag requires the same [`ProtocolVersion`] that is returned by +/// `DecryptionKey::required_version`. impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index d3ec395..cf71520 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str::FromStr; +use shorthand::ShortHand; + use crate::types::ProtocolVersion; use crate::utils::tag; use crate::RequiredVersion; @@ -15,22 +17,9 @@ use crate::RequiredVersion; /// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3 -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct ExtXDiscontinuitySequence(u64); - -impl ExtXDiscontinuitySequence { - pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; - - /// Makes a new [ExtXDiscontinuitySequence] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; - /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); - /// ``` - pub const fn new(seq_num: u64) -> Self { Self(seq_num) } - +#[derive(ShortHand, Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[shorthand(enable(must_use))] +pub struct ExtXDiscontinuitySequence { /// Returns the discontinuity sequence number of /// the first media segment that appears in the associated playlist. /// @@ -38,27 +27,26 @@ impl ExtXDiscontinuitySequence { /// /// ``` /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; - /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); - /// - /// assert_eq!(discontinuity_sequence.seq_num(), 5); - /// ``` - pub const fn seq_num(self) -> u64 { self.0 } - - /// Sets the sequence number. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; /// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5); /// /// discontinuity_sequence.set_seq_num(10); /// assert_eq!(discontinuity_sequence.seq_num(), 10); /// ``` - pub fn set_seq_num(&mut self, value: u64) -> &mut Self { - self.0 = value; - self - } + seq_num: u64, +} + +impl ExtXDiscontinuitySequence { + pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; + + /// Makes a new [`ExtXDiscontinuitySequence`] tag. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// ``` + pub const fn new(seq_num: u64) -> Self { Self { seq_num } } } /// This tag requires [`ProtocolVersion::V1`]. @@ -69,7 +57,7 @@ impl RequiredVersion for ExtXDiscontinuitySequence { impl fmt::Display for ExtXDiscontinuitySequence { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // - write!(f, "{}{}", Self::PREFIX, self.0) + write!(f, "{}{}", Self::PREFIX, self.seq_num) } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 61a2bfd..f357c9a 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -121,7 +121,7 @@ pub struct ExtXDateRange { impl ExtXDateRangeBuilder { /// Inserts a key value pair. - pub fn insert_client_attribute>( + pub fn insert_client_attribute, V: Into>( &mut self, key: K, value: V, @@ -131,7 +131,7 @@ impl ExtXDateRangeBuilder { } if let Some(client_attributes) = &mut self.client_attributes { - client_attributes.insert(key.to_string(), value.into()); + client_attributes.insert(key.into(), value.into()); } else { unreachable!(); } @@ -160,9 +160,9 @@ impl ExtXDateRange { /// .and_hms_milli(14, 54, 23, 31), /// ); /// ``` - pub fn new(id: T, start_date: DateTime) -> Self { + pub fn new>(id: T, start_date: DateTime) -> Self { Self { - id: id.to_string(), + id: id.into(), class: None, start_date, end_date: None, @@ -281,7 +281,7 @@ impl fmt::Display for ExtXDateRange { write!( f, ",END-DATE={}", - quote(value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + quote(&value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) )?; } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 39d4591..8d47376 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -49,10 +49,10 @@ impl ExtInf { /// /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// ``` - pub fn with_title(duration: Duration, title: T) -> Self { + pub fn with_title>(duration: Duration, title: T) -> Self { Self { duration, - title: Some(title.to_string()), + title: Some(title.into()), } } @@ -156,9 +156,9 @@ impl FromStr for ExtInf { let duration = Duration::from_secs_f64(input.next().unwrap().parse()?); let title = input .next() - .map(|value| value.trim()) + .map(str::trim) .filter(|value| !value.is_empty()) - .map(|value| value.to_string()); + .map(ToString::to_string); Ok(Self { duration, title }) } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index acf3e02..d44dec2 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -45,7 +45,7 @@ impl ExtXKey { /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\"" /// ); /// ``` - pub fn new(method: EncryptionMethod, uri: T) -> Self { + pub fn new>(method: EncryptionMethod, uri: T) -> Self { Self(DecryptionKey::new(method, uri)) } @@ -87,8 +87,8 @@ impl ExtXKey { pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } } -/// This tag requires the [`ProtocolVersion`] returned by -/// [`DecryptionKey::required_version`]. +/// This tag requires the same [`ProtocolVersion`] that is returned by +/// `DecryptionKey::required_version`. impl RequiredVersion for ExtXKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } @@ -103,7 +103,10 @@ impl FromStr for ExtXKey { } impl fmt::Display for ExtXKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // + write!(f, "{}{}", Self::PREFIX, self.0) + } } #[cfg(test)] diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index ec8a50e..1ab3a93 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -69,9 +69,9 @@ impl ExtXMap { /// # use hls_m3u8::tags::ExtXMap; /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); /// ``` - pub fn new(uri: T) -> Self { + pub fn new>(uri: T) -> Self { Self { - uri: uri.to_string(), + uri: uri.into(), range: None, keys: vec![], } @@ -90,9 +90,9 @@ impl ExtXMap { /// ByteRange::new(9, Some(2)), /// ); /// ``` - pub fn with_range(uri: T, range: ByteRange) -> Self { + pub fn with_range>(uri: T, range: ByteRange) -> Self { Self { - uri: uri.to_string(), + uri: uri.into(), range: Some(range), keys: vec![], } diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 570c6bf..4bf445f 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -7,8 +7,13 @@ use crate::{Error, RequiredVersion}; /// # [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// +/// The [`ExtXIndependentSegments`] tag signals that all media samples in a +/// [`MediaSegment`] can be decoded without information from other segments. +/// /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: /// https://tools.ietf.org/html/rfc8216#section-4.3.5.1 +/// +/// [`MediaSegment`]: crate::MediaSegment #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct ExtXIndependentSegments; diff --git a/src/tags/shared/mod.rs b/src/tags/shared/mod.rs index 956efe7..ce2f2d2 100644 --- a/src/tags/shared/mod.rs +++ b/src/tags/shared/mod.rs @@ -1,3 +1,11 @@ +//! The tags in this section can appear in either Master Playlists or +//! Media Playlists. If one of these tags appears in a Master Playlist, +//! it should not appear in any Media Playlist referenced by that Master +//! Playlist. A tag that appears in both must have the same value; +//! otherwise, clients should ignore the value in the Media Playlist(s). +//! +//! These tags must not appear more than once in a Playlist. If a tag +//! appears more than once, clients must fail to parse the Playlist. mod independent_segments; mod start; diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 526624e..275899a 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -18,7 +18,9 @@ use crate::{Error, RequiredVersion}; #[builder(setter(into), build_fn(validate = "Self::validate"))] #[shorthand(enable(must_use, into))] pub struct DecryptionKey { - /// The [`EncryptionMethod`]. + /// HLS supports multiple encryption methods for a segment. + /// + /// For example `AES-128`. /// /// # Example /// @@ -41,9 +43,10 @@ pub struct DecryptionKey { /// This attribute is required. #[shorthand(enable(copy))] pub(crate) method: EncryptionMethod, - /// An `URI`, that specifies how to obtain the key. + /// An `URI` that specifies how to obtain the key. /// /// # Example + /// /// ``` /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; @@ -63,8 +66,10 @@ pub struct DecryptionKey { /// This attribute is required, if the [`EncryptionMethod`] is not `None`. #[builder(setter(into, strip_option), default)] pub(crate) uri: Option, - /// The IV (Initialization Vector) for the key. + /// An IV (initialization vector) is used to prevent repetitions between + /// segments of encrypted data. /// + /// /// /// # Example /// @@ -90,7 +95,7 @@ pub struct DecryptionKey { // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 #[shorthand(enable(copy), disable(option_as_ref))] pub(crate) iv: Option<[u8; 16]>, - /// [`KeyFormat`] specifies how the key is + /// The [`KeyFormat`] specifies how the key is /// represented in the resource identified by the `URI`. /// /// # Example @@ -157,23 +162,25 @@ impl DecryptionKey { /// /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// ``` - pub fn new(method: EncryptionMethod, uri: T) -> Self { + #[doc(hidden)] + pub fn new>(method: EncryptionMethod, uri: T) -> Self { Self { method, - uri: Some(uri.to_string()), + uri: Some(uri.into()), iv: None, key_format: None, key_format_versions: None, } } - /// Returns a Builder to build a [DecryptionKey]. + /// Returns a Builder to build a [`DecryptionKey`]. pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } } /// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or /// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is /// specified. +/// /// Otherwise [`ProtocolVersion::V1`] is required. impl RequiredVersion for DecryptionKey { fn required_version(&self) -> ProtocolVersion { @@ -187,6 +194,7 @@ impl RequiredVersion for DecryptionKey { } } +#[doc(hidden)] impl FromStr for DecryptionKey { type Err = Error; diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index acb4cc0..6d4f1ea 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -5,15 +5,16 @@ use strum::{Display, EnumString}; /// See: [4.3.2.4. EXT-X-KEY] /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +#[non_exhaustive] #[allow(missing_docs)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum EncryptionMethod { /// `None` means that the [`MediaSegment`]s are not encrypted. /// - /// [MediaSegment]: crate::MediaSegment + /// [`MediaSegment`]: crate::MediaSegment None, - /// `Aes128` signals that the [MediaSegment]s are completely encrypted + /// `Aes128` signals that the [`MediaSegment`]s are completely encrypted /// using the Advanced Encryption Standard ([AES-128]) with a 128-bit /// key, Cipher Block Chaining (CBC), and /// [Public-Key Cryptography Standards #7 (PKCS7)] padding. @@ -22,14 +23,14 @@ pub enum EncryptionMethod { /// Initialization Vector (IV) attribute value or the Media Sequence /// Number as the IV. /// - /// [MediaSegment]: crate::MediaSegment - /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [`MediaSegment`]: crate::MediaSegment + /// [AES-128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf /// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652 #[strum(serialize = "AES-128")] Aes128, - /// `SampleAes` means that the [MediaSegment]s + /// `SampleAes` means that the [`MediaSegment`]s /// contain media samples, such as audio or video, that are encrypted - /// using the Advanced Encryption Standard ([AES_128]). How these media + /// using the Advanced Encryption Standard ([`AES-128`]). How these media /// streams are encrypted and encapsulated in a segment depends on the /// media encoding and the media format of the segment. fMP4 Media /// Segments are encrypted using the 'cbcs' scheme of @@ -39,7 +40,7 @@ pub enum EncryptionMethod { /// Live Streaming (HLS) [SampleEncryption specification]. /// /// [`MediaSegment`]: crate::MediaSegment - /// [AES-128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [`AES-128`]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf /// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC /// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264 /// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496 diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index 1d03e2a..9796cfd 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -5,6 +5,7 @@ use strum::{Display, EnumString}; /// See: [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 +#[non_exhaustive] #[allow(missing_docs)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index b39ce5c..6f1fd1a 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -5,9 +5,11 @@ use strum::{Display, EnumString}; /// See: [4.3.4.1. EXT-X-MEDIA] /// /// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +#[non_exhaustive] #[allow(missing_docs)] -#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "UPPERCASE")] +#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] +#[non_exhaustive] pub enum InStreamId { Cc1, Cc2, diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 3995db0..f3b995a 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -7,6 +7,7 @@ use crate::{Error, RequiredVersion}; /// [`KeyFormat`] specifies, how the key is represented in the /// resource identified by the `URI`. +#[non_exhaustive] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum KeyFormat { /// The key is a single packed array of 16 octets in binary format. @@ -28,7 +29,7 @@ impl FromStr for KeyFormat { } impl fmt::Display for KeyFormat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote("identity")) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote(&"identity")) } } /// This tag requires [`ProtocolVersion::V5`]. diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 7fada99..0bd1189 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -31,7 +31,10 @@ impl KeyFormatVersions { /// Returns `true`, if [`KeyFormatVersions`] has the default value of /// `vec![1]`. - pub fn is_default(&self) -> bool { self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() } + pub fn is_default(&self) -> bool { + // + self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() + } } impl Default for KeyFormatVersions { @@ -73,7 +76,7 @@ impl fmt::Display for KeyFormatVersions { // vec![1, 2, 3] -> "1/2/3" self.0 .iter() - .map(|v| v.to_string()) + .map(ToString::to_string) .collect::>() .join("/") ) diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 19a999f..c18cae3 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -1,6 +1,7 @@ use strum::{Display, EnumString}; /// Specifies the media type. +#[non_exhaustive] #[allow(missing_docs)] #[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 5776ac1..c9e1af7 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -10,6 +10,7 @@ use crate::Error; /// /// [7. Protocol Version Compatibility]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-7 +#[non_exhaustive] #[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ProtocolVersion { diff --git a/src/types/value.rs b/src/types/value.rs index 98eebe0..2a20662 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -6,8 +6,9 @@ use hex; use crate::utils::{quote, unquote}; use crate::Error; -#[derive(Debug, Clone, PartialEq, PartialOrd)] /// A [`Value`]. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, PartialOrd)] pub enum Value { /// A [`String`]. String(String), diff --git a/src/utils.rs b/src/utils.rs index 989d029..3b2a375 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use crate::Error; +use core::iter; macro_rules! required_version { ( $( $tag:expr ),* ) => { @@ -22,14 +23,7 @@ pub(crate) fn parse_iv_from_str(input: &str) -> crate::Result<[u8; 16]> { let mut result = [0; 16]; - // TODO: - // hex::decode_to_slice(value.as_bytes()[2..], &mut result)?; - - for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() { - let d = core::str::from_utf8(c).map_err(Error::custom)?; - let b = u8::from_str_radix(d, 16).map_err(Error::custom)?; - result[i] = b; - } + hex::decode_to_slice(&input.as_bytes()[2..], &mut result).map_err(Error::hex)?; Ok(result) } @@ -50,19 +44,23 @@ pub(crate) fn parse_yes_or_no>(s: T) -> crate::Result { /// /// Therefore it is safe to simply remove any occurence of those characters. /// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2) -pub(crate) fn unquote(value: T) -> String { +pub(crate) fn unquote>(value: T) -> String { value - .to_string() - .replace("\"", "") - .replace("\n", "") - .replace("\r", "") + .as_ref() + .chars() + .filter(|c| *c != '"' && *c != '\n' && *c != '\r') + .collect() } /// Puts a string inside quotes. +#[allow(clippy::needless_pass_by_value)] pub(crate) fn quote(value: T) -> String { // the replace is for the case, that quote is called on an already quoted // string, which could cause problems! - format!("\"{}\"", value.to_string().replace("\"", "")) + iter::once('"') + .chain(value.to_string().chars().filter(|c| *c != '"')) + .chain(iter::once('"')) + .collect() } /// Checks, if the given tag is at the start of the input. If this is the case, From 9b61f74b9d9914e1153db654cff455cd8d64257c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 13:51:37 +0100 Subject: [PATCH 022/112] fix documentation --- src/tags/master_playlist/variant_stream.rs | 101 +++++++++++---------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index ecbd2c4..b10266c 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -8,71 +8,71 @@ use crate::types::{ClosedCaptions, ProtocolVersion, StreamData, UFloat}; use crate::utils::{quote, tag, unquote}; use crate::Error; -/// A server MAY offer multiple Media Playlist files to provide different -/// encodings of the same presentation. If it does so, it SHOULD provide -/// a Master Playlist file that lists each Variant Stream to allow +/// A server may offer multiple [`MediaPlaylist`] files to provide different +/// encodings of the same presentation. If it does so, it should provide +/// a [`MasterPlaylist`] that lists each [`VariantStream`] to allow /// clients to switch between encodings dynamically. /// -/// Master Playlists describe regular Variant Streams with EXT-X-STREAM- -/// INF tags and I-frame Variant Streams with EXT-X-I-FRAME-STREAM-INF -/// tags. +/// The server must meet the following constraints when producing +/// [`VariantStream`]s in order to allow clients to switch between them +/// seamlessly: /// -/// If an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag contains -/// the CODECS attribute, the attribute value MUST include every media -/// format [RFC6381] present in any Media Segment in any of the -/// Renditions specified by the Variant Stream. +/// - Each [`VariantStream`] must present the same content. /// -/// The server MUST meet the following constraints when producing Variant -/// Streams in order to allow clients to switch between them seamlessly: +/// - Matching content in [`VariantStream`]s must have matching timestamps. This +/// allows clients to synchronize the media. /// -/// o Each Variant Stream MUST present the same content. +/// - Matching content in [`VariantStream`]s must have matching +/// [`ExtXDiscontinuitySequence`]. /// +/// - Each [`MediaPlaylist`] in each [`VariantStream`] must have the same target +/// duration. The only exceptions are subtitle renditions and +/// [`MediaPlaylist`]s containing an [`ExtXIFramesOnly`] tag, which may have +/// different target durations if they have [`ExtXPlaylistType::Vod`]. /// -/// o Matching content in Variant Streams MUST have matching timestamps. -/// This allows clients to synchronize the media. +/// - Content that appears in a [`MediaPlaylist`] of one [`VariantStream`] but +/// not in another must appear either at the beginning or at the end of the +/// [`MediaPlaylist`] and must not be longer than the target duration. /// -/// o Matching content in Variant Streams MUST have matching -/// Discontinuity Sequence Numbers (see Section 4.3.3.3). +/// - If any [`MediaPlaylist`]s have an [`ExtXPlaylistType`] tag, all +/// [`MediaPlaylist`]s must have an [`ExtXPlaylistType`] tag with the same +/// value. /// -/// o Each Media Playlist in each Variant Stream MUST have the same -/// target duration. The only exceptions are SUBTITLES Renditions and -/// Media Playlists containing an EXT-X-I-FRAMES-ONLY tag, which MAY -/// have different target durations if they have an EXT-X-PLAYLIST- -/// TYPE of VOD. +/// - If the Playlist contains an [`ExtXPlaylistType`] tag with the value of +/// VOD, the first segment of every [`MediaPlaylist`] in every +/// [`VariantStream`] must start at the same media timestamp. /// -/// o Content that appears in a Media Playlist of one Variant Stream but -/// not in another MUST appear either at the beginning or at the end -/// of the Media Playlist file and MUST NOT be longer than the target -/// duration. +/// - If any [`MediaPlaylist`] in a [`MasterPlaylist`] contains an +/// [`ExtXProgramDateTime`] tag, then all [`MediaPlaylist`]s in that +/// [`MasterPlaylist`] must contain [`ExtXProgramDateTime`] tags with +/// consistent mappings of date and time to media timestamps. /// -/// o If any Media Playlists have an EXT-X-PLAYLIST-TYPE tag, all Media -/// Playlists MUST have an EXT-X-PLAYLIST-TYPE tag with the same -/// value. +/// - Each [`VariantStream`] must contain the same set of Date Ranges, each one +/// identified by an [`ExtXDateRange`] tag(s) with the same ID attribute value +/// and containing the same set of attribute/value pairs. /// -/// o If the Playlist contains an EXT-X-PLAYLIST-TYPE tag with the value -/// of VOD, the first segment of every Media Playlist in every Variant -/// Stream MUST start at the same media timestamp. -/// -/// o If any Media Playlist in a Master Playlist contains an EXT-X- -/// PROGRAM-DATE-TIME tag, then all Media Playlists in that Master -/// Playlist MUST contain EXT-X-PROGRAM-DATE-TIME tags with consistent -/// mappings of date and time to media timestamps. -/// -/// o Each Variant Stream MUST contain the same set of Date Ranges, each -/// one identified by an EXT-X-DATERANGE tag(s) with the same ID -/// attribute value and containing the same set of attribute/value -/// pairs. -/// -/// In addition, for broadest compatibility, Variant Streams SHOULD -/// contain the same encoded audio bitstream. This allows clients to -/// switch between Variant Streams without audible glitching. -/// -/// The rules for Variant Streams also apply to alternative Renditions -/// (see Section 4.3.4.2.1). +/// In addition, for broadest compatibility, [`VariantStream`]s should +/// contain the same encoded audio bitstream. This allows clients to +/// switch between [`VariantStream`]s without audible glitching. /// /// [RFC6381]: https://tools.ietf.org/html/rfc6381 +/// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence +/// [`ExtXPlaylistType::Vod`]: crate::tags::ExtXPlaylistType::Vod +/// [`MediaPlaylist`]: crate::MediaPlaylist +/// [`MasterPlaylist`]: crate::MasterPlaylist +/// [`ExtXDateRange`]: crate::tags::ExtXDateRange +/// [`ExtXProgramDateTime`]: crate::tags::ExtXProgramDateTime +/// [`ExtXPlaylistType`]: crate::tags::ExtXPlaylistType +/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly #[derive(Debug, Clone, PartialEq, PartialOrd)] pub enum VariantStream { + /// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`] + /// file containing the I-frames of a multimedia presentation. + /// It stands alone, in that it does not apply to a particular URI in the + /// [`MasterPlaylist`]. + /// + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist ExtXIFrame { /// The URI identifies the I-frame [`MediaPlaylist`] file. /// That Playlist file must contain an [`ExtXIFramesOnly`] tag. @@ -82,6 +82,7 @@ pub enum VariantStream { /// This field is required. /// /// [`MediaPlaylist`]: crate::MediaPlaylist + /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly uri: String, /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and /// [`VariantStream::ExtXIFrame`]. @@ -91,6 +92,8 @@ pub enum VariantStream { /// This field is optional. stream_data: StreamData, }, + /// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is + /// a set of renditions that can be combined to play the presentation. ExtXStreamInf { /// The URI specifies a [`MediaPlaylist`] that carries a rendition of /// the [`VariantStream`]. Clients that do not support multiple video From 94d85d922fbd826e64d207a2cdc67eab0bb3cb5b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 10 Feb 2020 13:54:51 +0100 Subject: [PATCH 023/112] fix github actions --- .github/workflows/rust.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ec69f4a..b7ec752 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,12 +7,13 @@ jobs: rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v2 + - name: Install latest nightly + uses: actions-rs/toolchain@v1 with: toolchain: nightly - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 + - uses: actions-rs/cargo@v2 with: command: fmt args: --all -- --check @@ -20,12 +21,13 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v2 + - name: Install latest nightly + uses: actions-rs/toolchain@v1 with: toolchain: stable - run: rustup component add clippy - - uses: actions-rs/cargo@v1 + - uses: actions-rs/cargo@v2 with: command: clippy # args: -- -D warnings From 25f9691c750d3fcaf5ac0fdb94f2f0efca1659ca Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:01:42 +0100 Subject: [PATCH 024/112] improve error --- Cargo.toml | 1 + src/error.rs | 159 ++++++++++++------ .../media_playlist/discontinuity_sequence.rs | 12 +- src/tags/media_playlist/media_sequence.rs | 4 +- src/tags/media_playlist/target_duration.rs | 4 +- src/tags/media_segment/date_range.rs | 8 +- src/tags/media_segment/inf.rs | 8 +- src/types/byte_range.rs | 9 +- src/types/channels.rs | 8 +- src/types/float.rs | 2 +- src/types/resolution.rs | 4 +- src/types/stream_data.rs | 14 +- src/types/ufloat.rs | 2 +- 13 files changed, 168 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 135db77..ff1ef16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ hex = "0.4" shorthand = "0.1" strum = { version = "0.17", features = ["derive"] } thiserror = "1.0" +backtrace = { version = "0.3", features = ["std"], optional = true } [dev-dependencies] pretty_assertions = "0.6" diff --git a/src/error.rs b/src/error.rs index f86caf4..bba33d1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@ use std::fmt; +#[cfg(feature = "backtrace")] +use backtrace::Backtrace; use thiserror::Error; + //use crate::types::ProtocolVersion; /// This crate specific `Result` type. @@ -9,19 +12,25 @@ pub type Result = std::result::Result; #[derive(Debug, Error, Clone, PartialEq)] #[non_exhaustive] enum ErrorKind { - #[error("a value is missing for the attribute {}", _0)] - MissingValue(String), + #[error("a value is missing for the attribute {value}")] + MissingValue { value: String }, #[error("invalid input")] InvalidInput, - #[error("{}", _0)] - ParseIntError(::std::num::ParseIntError), + #[error("{source}: {input:?}")] + ParseIntError { + input: String, + source: ::std::num::ParseIntError, + }, - #[error("{}", _0)] - ParseFloatError(::std::num::ParseFloatError), + #[error("{source}: {input:?}")] + ParseFloatError { + input: String, + source: ::std::num::ParseFloatError, + }, - #[error("expected `{}` at the start of {:?}", tag, input)] + #[error("expected `{tag}` at the start of {input:?}")] MissingTag { /// The required tag. tag: String, @@ -29,41 +38,46 @@ enum ErrorKind { input: String, }, - #[error("{}", _0)] + #[error("{0}")] Custom(String), - #[error("unmatched group: {:?}", _0)] + #[error("unmatched group: {0:?}")] UnmatchedGroup(String), - #[error("unknown protocol version {:?}", _0)] + #[error("unknown protocol version {0:?}")] UnknownProtocolVersion(String), // #[error("required_version: {:?}, specified_version: {:?}", _0, _1)] // VersionError(ProtocolVersion, ProtocolVersion), - #[error("missing attribute: {}", _0)] - MissingAttribute(String), + #[error("missing attribute: {attribute:?}")] + MissingAttribute { attribute: String }, - #[error("unexpected attribute: {:?}", _0)] - UnexpectedAttribute(String), + #[error("unexpected attribute: {attribute:?}")] + UnexpectedAttribute { attribute: String }, - #[error("unexpected tag: {:?}", _0)] - UnexpectedTag(String), + #[error("unexpected tag: {tag:?}")] + UnexpectedTag { tag: String }, - #[error("{}", _0)] - ChronoParseError(chrono::ParseError), + #[error("{source}")] + Chrono { source: chrono::ParseError }, - #[error("builder error: {}", _0)] - Builder(String), + #[error("builder error: {message}")] + Builder { message: String }, - #[doc(hidden)] - #[error("{}", _0)] - Hex(hex::FromHexError), + #[error("{source}")] + Hex { source: hex::FromHexError }, } /// The Error type of this library. #[derive(Debug)] pub struct Error { inner: ErrorKind, + #[cfg(feature = "backtrace")] + backtrace: Backtrace, +} + +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl std::error::Error for Error {} @@ -74,32 +88,53 @@ impl fmt::Display for Error { #[allow(clippy::needless_pass_by_value)] impl Error { - const fn new(inner: ErrorKind) -> Self { Self { inner } } + fn new(inner: ErrorKind) -> Self { + Self { + inner, + #[cfg(feature = "backtrace")] + backtrace: Backtrace::new(), + } + } pub(crate) fn custom(value: T) -> Self { Self::new(ErrorKind::Custom(value.to_string())) } pub(crate) fn missing_value(value: T) -> Self { - Self::new(ErrorKind::MissingValue(value.to_string())) + Self::new(ErrorKind::MissingValue { + value: value.to_string(), + }) } pub(crate) fn unexpected_attribute(value: T) -> Self { - Self::new(ErrorKind::UnexpectedAttribute(value.to_string())) + Self::new(ErrorKind::UnexpectedAttribute { + attribute: value.to_string(), + }) } pub(crate) fn unexpected_tag(value: T) -> Self { - Self::new(ErrorKind::UnexpectedTag(value.to_string())) + Self::new(ErrorKind::UnexpectedTag { + tag: value.to_string(), + }) } - pub(crate) const fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) } + pub(crate) fn invalid_input() -> Self { Self::new(ErrorKind::InvalidInput) } - pub(crate) fn parse_int(value: ::std::num::ParseIntError) -> Self { - Self::new(ErrorKind::ParseIntError(value)) + pub(crate) fn parse_int(input: T, source: ::std::num::ParseIntError) -> Self { + Self::new(ErrorKind::ParseIntError { + input: input.to_string(), + source, + }) } - pub(crate) fn parse_float(value: ::std::num::ParseFloatError) -> Self { - Self::new(ErrorKind::ParseFloatError(value)) + pub(crate) fn parse_float( + input: T, + source: ::std::num::ParseFloatError, + ) -> Self { + Self::new(ErrorKind::ParseFloatError { + input: input.to_string(), + source, + }) } pub(crate) fn missing_tag(tag: T, input: U) -> Self @@ -122,36 +157,66 @@ impl Error { } pub(crate) fn builder(value: T) -> Self { - Self::new(ErrorKind::Builder(value.to_string())) + Self::new(ErrorKind::Builder { + message: value.to_string(), + }) } pub(crate) fn missing_attribute(value: T) -> Self { - Self::new(ErrorKind::MissingAttribute(value.to_string())) + Self::new(ErrorKind::MissingAttribute { + attribute: value.to_string(), + }) } // third party crates: - pub(crate) fn chrono(value: chrono::format::ParseError) -> Self { - Self::new(ErrorKind::ChronoParseError(value)) + pub(crate) fn chrono(source: chrono::format::ParseError) -> Self { + Self::new(ErrorKind::Chrono { source }) } - pub(crate) fn hex(value: hex::FromHexError) -> Self { Self::new(ErrorKind::Hex(value)) } + pub(crate) fn hex(source: hex::FromHexError) -> Self { + // + Self::new(ErrorKind::Hex { source }) + } pub(crate) fn strum(value: strum::ParseError) -> Self { Self::new(ErrorKind::Custom(value.to_string())) } } -#[doc(hidden)] -impl From<::std::num::ParseIntError> for Error { - fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int(value) } -} - -#[doc(hidden)] -impl From<::std::num::ParseFloatError> for Error { - fn from(value: ::std::num::ParseFloatError) -> Self { Self::parse_float(value) } -} - #[doc(hidden)] impl From<::strum::ParseError> for Error { fn from(value: ::strum::ParseError) -> Self { Self::strum(value) } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_parse_float_error() { + assert_eq!( + Error::parse_float( + "1.x234", + "1.x234" + .parse::() + .expect_err("this should not parse as a float!") + ) + .to_string(), + "invalid float literal: \"1.x234\"".to_string() + ); + } + + #[test] + fn test_parse_int_error() { + assert_eq!( + Error::parse_int( + "1x", + "1x".parse::() + .expect_err("this should not parse as an usize!") + ) + .to_string(), + "invalid digit found in string: \"1x\"".to_string() + ); + } +} diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index cf71520..b1f777b 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -5,6 +5,7 @@ use shorthand::ShortHand; use crate::types::ProtocolVersion; use crate::utils::tag; +use crate::Error; use crate::RequiredVersion; /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] @@ -62,10 +63,12 @@ impl fmt::Display for ExtXDiscontinuitySequence { } impl FromStr for ExtXDiscontinuitySequence { - type Err = crate::Error; + type Err = Error; fn from_str(input: &str) -> Result { - let seq_num = tag(input, Self::PREFIX)?.parse()?; + let input = tag(input, Self::PREFIX)?; + let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?; + Ok(Self::new(seq_num)) } } @@ -97,6 +100,11 @@ mod test { ExtXDiscontinuitySequence::new(123), "#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap() ); + + assert_eq!( + ExtXDiscontinuitySequence::from_str("#EXT-X-DISCONTINUITY-SEQUENCE:12A"), + Err(Error::parse_int("12A", "12A".parse::().expect_err(""))) + ); } #[test] diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index dfac8b7..0536af6 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -72,7 +72,9 @@ impl FromStr for ExtXMediaSequence { type Err = Error; fn from_str(input: &str) -> Result { - let seq_num = tag(input, Self::PREFIX)?.parse()?; + let input = tag(input, Self::PREFIX)?; + let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?; + Ok(Self::new(seq_num)) } } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 16c631b..4c548ee 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -68,7 +68,9 @@ impl FromStr for ExtXTargetDuration { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?.parse()?; + let input = tag(input, Self::PREFIX)?; + let input = input.parse().map_err(|e| Error::parse_int(input, e))?; + Ok(Self::new(Duration::from_secs(input))) } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index f357c9a..3d0f129 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -211,10 +211,14 @@ impl FromStr for ExtXDateRange { "START-DATE" => start_date = Some(unquote(value)), "END-DATE" => end_date = Some(unquote(value).parse().map_err(Error::chrono)?), "DURATION" => { - duration = Some(Duration::from_secs_f64(value.parse()?)); + duration = Some(Duration::from_secs_f64( + value.parse().map_err(|e| Error::parse_float(value, e))?, + )); } "PLANNED-DURATION" => { - planned_duration = Some(Duration::from_secs_f64(value.parse()?)); + planned_duration = Some(Duration::from_secs_f64( + value.parse().map_err(|e| Error::parse_float(value, e))?, + )); } "SCTE35-CMD" => scte35_cmd = Some(unquote(value)), "SCTE35-OUT" => scte35_out = Some(unquote(value)), diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 8d47376..89eb958 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -153,7 +153,13 @@ impl FromStr for ExtInf { fn from_str(input: &str) -> Result { let mut input = tag(input, Self::PREFIX)?.splitn(2, ','); - let duration = Duration::from_secs_f64(input.next().unwrap().parse()?); + let duration = input.next().unwrap(); + let duration = Duration::from_secs_f64( + duration + .parse() + .map_err(|e| Error::parse_float(duration, e))?, + ); + let title = input .next() .map(str::trim) diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 7d07045..d5018e2 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -79,10 +79,13 @@ impl FromStr for ByteRange { let length = input .next() - .ok_or_else(|| Error::custom("missing length for #EXT-X-BYTERANGE")) - .and_then(|s| s.parse().map_err(Error::parse_int))?; + .ok_or_else(|| Error::custom("missing length"))?; + let length = length.parse().map_err(|e| Error::parse_int(length, e))?; - let start = input.next().map(str::parse).transpose()?; + let start = input + .next() + .map(|v| v.parse().map_err(|e| Error::parse_int(v, e))) + .transpose()?; Ok(Self::new(length, start)) } diff --git a/src/types/channels.rs b/src/types/channels.rs index 56c1520..2f1630c 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -85,11 +85,11 @@ impl FromStr for Channels { fn from_str(input: &str) -> Result { let mut parameters = input.split('/'); - let channel_number = parameters + let param_1 = parameters .next() - .ok_or_else(|| Error::missing_attribute("first parameter of channels"))? - .parse() - .map_err(Error::parse_int)?; + .ok_or_else(|| Error::missing_attribute("first parameter of channels"))?; + + let channel_number = param_1.parse().map_err(|e| Error::parse_int(param_1, e))?; Ok(Self { channel_number, diff --git a/src/types/float.rs b/src/types/float.rs index 48d1e26..d8c8ed9 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -59,7 +59,7 @@ impl FromStr for Float { type Err = Error; fn from_str(input: &str) -> Result { - let float = f32::from_str(input).map_err(Error::parse_float)?; + let float = f32::from_str(input).map_err(|e| Error::parse_float(input, e))?; Self::try_from(float) } } diff --git a/src/types/resolution.rs b/src/types/resolution.rs index 26ad26e..2e8cb67 100644 --- a/src/types/resolution.rs +++ b/src/types/resolution.rs @@ -41,12 +41,12 @@ impl FromStr for Resolution { let width = input .next() .ok_or_else(|| Error::custom("missing width for `Resolution` or an invalid input")) - .and_then(|v| v.parse().map_err(Error::parse_int))?; + .and_then(|v| v.parse().map_err(|e| Error::parse_int(v, e)))?; let height = input .next() .ok_or_else(|| Error::custom("missing height for `Resolution` or an invalid input")) - .and_then(|v| v.parse().map_err(Error::parse_int))?; + .and_then(|v| v.parse().map_err(|e| Error::parse_int(v, e)))?; Ok(Self { width, height }) } diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs index cb4e7bd..8aad849 100644 --- a/src/types/stream_data.rs +++ b/src/types/stream_data.rs @@ -283,9 +283,19 @@ impl FromStr for StreamData { for (key, value) in AttributePairs::new(input) { match key { - "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), + "BANDWIDTH" => { + bandwidth = Some( + value + .parse::() + .map_err(|e| Error::parse_int(value, e))?, + ); + } "AVERAGE-BANDWIDTH" => { - average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) + average_bandwidth = Some( + value + .parse::() + .map_err(|e| Error::parse_int(value, e))?, + ) } "CODECS" => codecs = Some(unquote(value)), "RESOLUTION" => resolution = Some(value.parse()?), diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index 1313fcc..3eb2f44 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -63,7 +63,7 @@ impl FromStr for UFloat { type Err = Error; fn from_str(input: &str) -> Result { - let float = f32::from_str(input).map_err(Error::parse_float)?; + let float = f32::from_str(input).map_err(|e| Error::parse_float(input, e))?; Self::try_from(float) } } From 8cced1ac535abed97b0baf5c42d87ef6ce2c4fb6 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:05:18 +0100 Subject: [PATCH 025/112] some improvements --- .travis.yml | 2 +- src/line.rs | 47 +-- src/master_playlist.rs | 271 ++++++++++++++---- src/media_segment.rs | 20 +- src/tags/basic/m3u.rs | 6 +- src/tags/basic/version.rs | 6 +- src/tags/master_playlist/media.rs | 16 +- src/tags/master_playlist/session_data.rs | 6 +- src/tags/master_playlist/session_key.rs | 10 +- .../media_playlist/discontinuity_sequence.rs | 8 +- src/tags/media_playlist/end_list.rs | 8 +- src/tags/media_playlist/media_sequence.rs | 5 +- src/tags/media_playlist/playlist_type.rs | 16 +- src/tags/shared/mod.rs | 13 +- src/traits.rs | 23 +- src/utils.rs | 26 ++ 16 files changed, 331 insertions(+), 152 deletions(-) diff --git a/.travis.yml b/.travis.yml index af43e8e..982e71a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,6 @@ script: after_success: | # this does require a -Z flag for Doctests, which is unstable! if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - cargo tarpaulin --run-types Tests Doctests --out Xml + cargo tarpaulin --ignore-panics --ignore-tests --run-types Tests Doctests --out Xml bash <(curl -s https://codecov.io/bash) fi diff --git a/src/line.rs b/src/line.rs index c6ba9d2..1c675e4 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,7 +1,9 @@ use core::convert::TryFrom; -use core::fmt; +use core::iter::FusedIterator; use core::str::FromStr; +use derive_more::Display; + use crate::tags; use crate::Error; @@ -33,16 +35,14 @@ impl<'a> Iterator for Lines<'a> { } } +impl<'a> FusedIterator for Lines<'a> {} + impl<'a> From<&'a str> for Lines<'a> { fn from(buffer: &'a str) -> Self { Self { - lines: buffer.lines().filter_map(|line| { - if line.trim().is_empty() { - None - } else { - Some(line.trim()) - } - }), + lines: buffer + .lines() + .filter_map(|line| Some(line.trim()).filter(|v| !v.is_empty())), } } } @@ -55,7 +55,8 @@ pub(crate) enum Line<'a> { } #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] +#[display(fmt = "{}")] pub(crate) enum Tag<'a> { ExtXVersion(tags::ExtXVersion), ExtInf(tags::ExtInf), @@ -80,34 +81,6 @@ pub(crate) enum Tag<'a> { Unknown(&'a str), } -impl<'a> fmt::Display for Tag<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { - 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::VariantStream(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), - } - } -} - impl<'a> TryFrom<&'a str> for Tag<'a> { type Error = Error; diff --git a/src/master_playlist.rs b/src/master_playlist.rs index f906667..9d9906f 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -15,8 +15,9 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// The master playlist describes all of the available variants for your -/// content. Each variant is a version of the stream at a particular bitrate -/// and is contained in a separate playlist. +/// content. +/// Each variant is a version of the stream at a particular bitrate and is +/// contained in a separate playlist. #[derive(ShortHand, Debug, Clone, Builder, PartialEq)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] @@ -67,21 +68,24 @@ pub struct MasterPlaylist { /// This tag is optional. #[builder(default)] variants: Vec, - /// The [`ExtXSessionData`] tags of the playlist. + /// The [`ExtXSessionData`] tag allows arbitrary session data to be + /// carried in a [`MasterPlaylist`]. /// /// # Note /// /// This tag is optional. #[builder(default)] session_data: Vec, - /// The [`ExtXSessionKey`] tags of the playlist. + /// This is a list of [`ExtXSessionKey`]s, that allows the client to preload + /// these keys without having to read the [`MediaPlaylist`]s first. /// /// # Note /// /// This tag is optional. #[builder(default)] session_keys: Vec, - /// A list of tags that are unknown. + /// This is a list of all tags that could not be identified while parsing + /// the input. /// /// # Note /// @@ -140,7 +144,7 @@ impl MasterPlaylistBuilder { } fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { - let mut has_none_closed_captions = false; + let mut closed_captions_none = false; for t in value { if let VariantStream::ExtXStreamInf { @@ -170,33 +174,26 @@ impl MasterPlaylistBuilder { if let Some(closed_captions) = &closed_captions { match &closed_captions { ClosedCaptions::GroupId(group_id) => { + if closed_captions_none { + return Err(Error::custom( + "If one ClosedCaptions is None all have to be None!", + )); + } + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { return Err(Error::unmatched_group(group_id)); } } - ClosedCaptions::None => { - has_none_closed_captions = true; + _ => { + if !closed_captions_none { + closed_captions_none = true; + } } } } } } - if has_none_closed_captions - && !value.iter().all(|t| { - if let VariantStream::ExtXStreamInf { - closed_captions, .. - } = &t - { - closed_captions == &Some(ClosedCaptions::None) - } else { - false - } - }) - { - return Err(Error::invalid_input()); - } - Ok(()) } @@ -218,6 +215,8 @@ impl MasterPlaylistBuilder { let mut set = HashSet::new(); if let Some(value) = &self.session_data { + set.reserve(value.len()); + for t in value { if !set.insert((t.data_id(), t.language())) { return Err(Error::custom(format!("Conflict: {}", t))); @@ -264,20 +263,20 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } - for t in &self.media { - writeln!(f, "{}", t)?; + for value in &self.media { + writeln!(f, "{}", value)?; } - for t in &self.variants { - writeln!(f, "{}", t)?; + for value in &self.variants { + writeln!(f, "{}", value)?; } - for t in &self.session_data { - writeln!(f, "{}", t)?; + for value in &self.session_data { + writeln!(f, "{}", value)?; } - for t in &self.session_keys { - writeln!(f, "{}", t)?; + for value in &self.session_keys { + writeln!(f, "{}", value)?; } if let Some(value) = &self.independent_segments { @@ -288,8 +287,8 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", value)?; } - for t in &self.unknown_tags { - writeln!(f, "{}", t)?; + for value in &self.unknown_tags { + writeln!(f, "{}", value)?; } Ok(()) @@ -382,40 +381,194 @@ impl FromStr for MasterPlaylist { #[cfg(test)] mod tests { use super::*; + use crate::types::StreamData; use pretty_assertions::assert_eq; #[test] fn test_parser() { - "#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" + assert_eq!( + concat!( + "#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(); + .unwrap(), + MasterPlaylist::builder() + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "http://example.com/low/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(150000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((416, 234)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/lo_mid/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(240000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((416, 234)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/hi_mid/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(440000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((416, 234)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/high/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(640000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((640, 360)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/audio/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(64000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, + ]) + .build() + .unwrap() + ); } #[test] fn test_display() { - 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); + assert_eq!( + concat!( + "#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" + ) + .to_string(), + MasterPlaylist::builder() + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "http://example.com/low/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(150000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((416, 234)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/lo_mid/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(240000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((416, 234)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/hi_mid/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(440000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((416, 234)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/high/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(640000) + .codecs("avc1.42e00a,mp4a.40.2") + .resolution((640, 360)) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/audio/index.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(64000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, + ]) + .build() + .unwrap() + .to_string() + ); } } diff --git a/src/media_segment.rs b/src/media_segment.rs index ec7889c..884cc1e 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -41,9 +41,7 @@ pub struct MediaSegment { } impl MediaSegment { - /// Returns a builder for a [`MasterPlaylist`]. - /// - /// [`MasterPlaylist`]: crate::MasterPlaylist + /// Returns a builder for a [`MediaSegment`]. pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } } @@ -131,13 +129,15 @@ mod tests { .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() + concat!( + "#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 e2a8b78..e5b78ea 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -9,10 +9,10 @@ use crate::{Error, RequiredVersion}; /// /// 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`]. +/// It is the at the start of every [`MediaPlaylist`] and [`MasterPlaylist`]. /// -/// [`Media Playlist`]: crate::MediaPlaylist -/// [`Master Playlist`]: crate::MasterPlaylist +/// [`MediaPlaylist`]: crate::MediaPlaylist +/// [`MasterPlaylist`]: crate::MasterPlaylist /// [`M3U`]: https://en.wikipedia.org/wiki/M3U /// [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)] diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index dc867e8..8108cf7 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -8,7 +8,7 @@ use crate::{Error, RequiredVersion}; /// # [4.3.1.2. EXT-X-VERSION] /// /// The [`ExtXVersion`] tag indicates the compatibility version of the -/// [`Master Playlist`] or [`Media Playlist`] file. +/// [`MasterPlaylist`] or [`MediaPlaylist`] file. /// It applies to the entire Playlist. /// /// # Examples @@ -40,8 +40,8 @@ use crate::{Error, RequiredVersion}; /// ); /// ``` /// -/// [`Media Playlist`]: crate::MediaPlaylist -/// [`Master Playlist`]: crate::MasterPlaylist +/// [`MediaPlaylist`]: crate::MediaPlaylist +/// [`MasterPlaylist`]: crate::MasterPlaylist /// [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); diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index d122930..f83d9b6 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -11,16 +11,16 @@ use crate::{Error, RequiredVersion}; /// # [4.4.5.1. EXT-X-MEDIA] /// -/// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s, +/// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, /// that contain alternative renditions of the same content. /// /// For example, three [`ExtXMedia`] tags can be used to identify audio-only -/// [`Media Playlist`]s, that contain English, French, and Spanish renditions +/// [`MediaPlaylist`]s, that contain English, French, and Spanish renditions /// of the same presentation. Or, two [`ExtXMedia`] tags can be used to -/// identify video-only [`Media Playlist`]s that show two different camera +/// identify video-only [`MediaPlaylist`]s that show two different camera /// angles. /// -/// [`Media Playlist`]: crate::MediaPlaylist +/// [`MediaPlaylist`]: crate::MediaPlaylist /// [4.4.5.1. EXT-X-MEDIA]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.1 #[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash)] @@ -35,7 +35,7 @@ pub struct ExtXMedia { /// This attribute is **required**. #[shorthand(enable(copy))] media_type: MediaType, - /// An `URI` to a [`Media Playlist`]. + /// An `URI` to a [`MediaPlaylist`]. /// /// # Note /// @@ -44,7 +44,7 @@ pub struct ExtXMedia { /// - This attribute is **not allowed**, if the [`MediaType`] is /// [`MediaType::ClosedCaptions`]. /// - /// [`Media Playlist`]: crate::MediaPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(setter(strip_option), default)] uri: Option, /// The identifier that specifies the group to which the rendition @@ -112,9 +112,9 @@ pub struct ExtXMedia { #[builder(default)] is_forced: bool, /// An [`InStreamId`] specifies a rendition within the - /// segments in the [`Media Playlist`]. + /// segments in the [`MediaPlaylist`]. /// - /// [`Media Playlist`]: crate::MediaPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(setter(strip_option), default)] #[shorthand(enable(copy))] instream_id: Option, diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 9e53e8b..75ad365 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -31,9 +31,9 @@ pub enum SessionData { /// # [4.3.4.4. EXT-X-SESSION-DATA] /// /// The [`ExtXSessionData`] tag allows arbitrary session data to be -/// carried in a [`Master Playlist`]. +/// carried in a [`MasterPlaylist`]. /// -/// [`Master Playlist`]: crate::MasterPlaylist +/// [`MasterPlaylist`]: crate::MasterPlaylist /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 #[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[builder(setter(into))] @@ -58,7 +58,7 @@ pub struct ExtXSessionData { /// /// This field is required. data: SessionData, - /// The `language` attribute identifies the language of [`SessionData`]. + /// The `language` attribute identifies the language of the [`SessionData`]. /// See [rfc5646](https://tools.ietf.org/html/rfc5646). #[builder(setter(into, strip_option), default)] language: Option, diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index ecbd270..9f0e1d8 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -9,13 +9,13 @@ use crate::{Error, RequiredVersion}; /// # [4.3.4.5. EXT-X-SESSION-KEY] /// -/// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s -/// to be specified in a [`Master Playlist`]. This allows the client to -/// preload these keys without having to read the [`Media Playlist`]s +/// The [`ExtXSessionKey`] tag allows encryption keys from [`MediaPlaylist`]s +/// to be specified in a [`MasterPlaylist`]. This allows the client to +/// preload these keys without having to read the [`MediaPlaylist`]s /// first. /// -/// [`Media Playlist`]: crate::MediaPlaylist -/// [`Master Playlist`]: crate::MasterPlaylist +/// [`MediaPlaylist`]: crate::MediaPlaylist +/// [`MasterPlaylist`]: crate::MasterPlaylist /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXSessionKey(DecryptionKey); diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index b1f777b..a0a0349 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -11,11 +11,13 @@ use crate::RequiredVersion; /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// /// The [`ExtXDiscontinuitySequence`] tag allows synchronization between -/// different Renditions of the same Variant Stream or different Variant -/// Streams that have [`ExtXDiscontinuity`] tags in their [`Media Playlist`]s. +/// different renditions of the same [`VariantStream`] or different +/// [`VariantStream`]s that have [`ExtXDiscontinuity`] tags in their +/// [`MediaPlaylist`]s. /// +/// [`VariantStream`]: crate::tags::VariantStream /// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity -/// [`Media Playlist`]: crate::MediaPlaylist +/// [`MediaPlaylist`]: crate::MediaPlaylist /// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3 #[derive(ShortHand, Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 3202870..e848be3 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -7,11 +7,11 @@ use crate::{Error, RequiredVersion}; /// # [4.4.3.4. EXT-X-ENDLIST] /// -/// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be -/// added to the [`Media Playlist`] file. +/// The [`ExtXEndList`] tag indicates, that no more [`MediaSegment`]s will be +/// added to the [`MediaPlaylist`] file. /// -/// [`Media Segment`]: crate::MediaSegment -/// [`Media Playlist`]: crate::MediaPlaylist +/// [`MediaSegment`]: crate::MediaSegment +/// [`MediaPlaylist`]: 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, PartialOrd, Ord)] diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 0536af6..c7cb4ca 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -65,7 +65,10 @@ impl RequiredVersion for ExtXMediaSequence { } impl fmt::Display for ExtXMediaSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // + write!(f, "{}{}", Self::PREFIX, self.0) + } } impl FromStr for ExtXMediaSequence { diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 69c2e52..f81ad10 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -8,22 +8,22 @@ use crate::{Error, RequiredVersion}; /// # [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`]. +/// [`MediaPlaylist`]. It applies to the entire [`MediaPlaylist`]. /// -/// [`Media Playlist`]: crate::MediaPlaylist +/// [`MediaPlaylist`]: 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 Segment`]s - /// can only be added to the end of the [`Media Playlist`]. + /// If the [`ExtXPlaylistType`] is Event, [`MediaSegment`]s + /// can only be added to the end of the [`MediaPlaylist`]. /// - /// [`Media Segment`]: crate::MediaSegment - /// [`Media Playlist`]: crate::MediaPlaylist + /// [`MediaSegment`]: crate::MediaSegment + /// [`MediaPlaylist`]: crate::MediaPlaylist Event, /// If the [`ExtXPlaylistType`] is Video On Demand (Vod), - /// the [`Media Playlist`] cannot change. + /// the [`MediaPlaylist`] cannot change. /// - /// [`Media Playlist`]: crate::MediaPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist Vod, } diff --git a/src/tags/shared/mod.rs b/src/tags/shared/mod.rs index ce2f2d2..052f0eb 100644 --- a/src/tags/shared/mod.rs +++ b/src/tags/shared/mod.rs @@ -1,11 +1,14 @@ -//! The tags in this section can appear in either Master Playlists or -//! Media Playlists. If one of these tags appears in a Master Playlist, -//! it should not appear in any Media Playlist referenced by that Master -//! Playlist. A tag that appears in both must have the same value; -//! otherwise, clients should ignore the value in the Media Playlist(s). +//! The tags in this section can appear in either [`MasterPlaylist`]s or +//! [`MediaPlaylist`]s. If one of these tags appears in a [`MasterPlaylist`], +//! it should not appear in any [`MediaPlaylist`] referenced by that +//! [`MasterPlaylist`]. A tag that appears in both must have the same value; +//! otherwise, clients should ignore the value in the [`MediaPlaylist`](s). //! //! These tags must not appear more than once in a Playlist. If a tag //! appears more than once, clients must fail to parse the Playlist. +//! +//! [`MediaPlaylist`]: crate::MediaPlaylist +//! [`MasterPlaylist`]: crate::MasterPlaylist mod independent_segments; mod start; diff --git a/src/traits.rs b/src/traits.rs index a80de7f..a92a9af 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -87,6 +87,7 @@ pub trait Encrypted { if self.keys().is_empty() { return false; } + self.keys() .iter() .any(|k| k.method() != EncryptionMethod::None) @@ -140,7 +141,7 @@ pub trait RequiredVersion { impl RequiredVersion for Vec { fn required_version(&self) -> ProtocolVersion { self.iter() - .map(|v| v.required_version()) + .map(RequiredVersion::required_version) .max() // return ProtocolVersion::V1, if the iterator is empty: .unwrap_or_default() @@ -150,8 +151,26 @@ impl RequiredVersion for Vec { impl RequiredVersion for Option { fn required_version(&self) -> ProtocolVersion { self.iter() - .map(|v| v.required_version()) + .map(RequiredVersion::required_version) .max() .unwrap_or_default() } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_required_version_trait() { + struct Example; + + impl RequiredVersion for Example { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V3 } + } + + assert_eq!(Example.required_version(), ProtocolVersion::V3); + assert_eq!(Example.introduced_version(), ProtocolVersion::V3); + } +} diff --git a/src/utils.rs b/src/utils.rs index 3b2a375..7ff9ddd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -86,6 +86,32 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn test_parse_iv_from_str() { + assert_eq!( + parse_iv_from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(), + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ] + ); + + assert_eq!( + parse_iv_from_str("0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(), + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ] + ); + + // missing `0x` at the start: + assert!(parse_iv_from_str("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").is_err()); + // too small: + assert!(parse_iv_from_str("0xFF").is_err()); + // too large: + assert!(parse_iv_from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").is_err()); + } + #[test] fn test_parse_yes_or_no() { assert!(parse_yes_or_no("YES").unwrap()); From b2c997d04d189f836a6645071785263a26129f34 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 16 Feb 2020 12:50:52 +0100 Subject: [PATCH 026/112] remove `_tag` suffix from MediaSegment fields --- src/media_playlist.rs | 30 ++++++------ src/media_segment.rs | 53 +++++++++++----------- src/tags/media_playlist/target_duration.rs | 4 ++ tests/media_playlist.rs | 48 ++++++++++---------- tests/playlist.rs | 50 +++++++++----------- 5 files changed, 92 insertions(+), 93 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 124e45f..9861b7e 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -124,7 +124,7 @@ impl MediaPlaylistBuilder { if let Some(segments) = &self.segments { for s in segments { // CHECK: `#EXT-X-TARGETDURATION` - let segment_duration = s.inf_tag().duration(); + let segment_duration = s.inf().duration(); let rounded_segment_duration = { if segment_duration.subsec_nanos() < 500_000_000 { Duration::from_secs(segment_duration.as_secs()) @@ -152,7 +152,7 @@ impl MediaPlaylistBuilder { } // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(tag) = s.byte_range_tag() { + if let Some(tag) = s.byte_range() { if tag.to_range().start().is_none() { let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?; if last_uri != s.uri() { @@ -286,7 +286,7 @@ fn parse_media_playlist( let mut has_discontinuity_tag = false; let mut unknown_tags = vec![]; - let mut available_key_tags: Vec = vec![]; + let mut available_keys: Vec = vec![]; for line in Lines::from(input) { match line? { @@ -294,25 +294,25 @@ fn parse_media_playlist( match tag { Tag::ExtInf(t) => { has_partial_segment = true; - segment.inf_tag(t); + segment.inf(t); } Tag::ExtXByteRange(t) => { has_partial_segment = true; - segment.byte_range_tag(t); + segment.byte_range(t); } Tag::ExtXDiscontinuity(t) => { has_discontinuity_tag = true; has_partial_segment = true; - segment.discontinuity_tag(t); + segment.discontinuity(t); } Tag::ExtXKey(t) => { has_partial_segment = true; - if available_key_tags.is_empty() { + if available_keys.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 + available_keys = available_keys .into_iter() .map(|k| { if t.key_format() == k.key_format() { @@ -323,22 +323,22 @@ fn parse_media_playlist( }) .collect(); } else { - available_key_tags.push(t); + available_keys.push(t); } } Tag::ExtXMap(mut t) => { has_partial_segment = true; - t.set_keys(available_key_tags.clone()); - segment.map_tag(t); + t.set_keys(available_keys.clone()); + segment.map(t); } Tag::ExtXProgramDateTime(t) => { has_partial_segment = true; - segment.program_date_time_tag(t); + segment.program_date_time(t); } Tag::ExtXDateRange(t) => { has_partial_segment = true; - segment.date_range_tag(t); + segment.date_range(t); } Tag::ExtXTargetDuration(t) => { builder.target_duration(t); @@ -350,9 +350,11 @@ fn parse_media_playlist( if segments.is_empty() { return Err(Error::invalid_input()); } + if has_discontinuity_tag { return Err(Error::invalid_input()); } + builder.discontinuity_sequence(t); } Tag::ExtXEndList(t) => { @@ -386,7 +388,7 @@ fn parse_media_playlist( } Line::Uri(uri) => { segment.uri(uri); - segment.keys(available_key_tags.clone()); + segment.keys(available_keys.clone()); segments.push(segment.build().map_err(Error::builder)?); segment = MediaSegment::builder(); has_partial_segment = false; diff --git a/src/media_segment.rs b/src/media_segment.rs index 884cc1e..5d6bb59 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -19,22 +19,22 @@ pub struct MediaSegment { keys: Vec, /// The [`ExtXMap`] tag associated with the media segment. #[builder(default)] - map_tag: Option, + map: Option, /// The [`ExtXByteRange`] tag associated with the [`MediaSegment`]. #[builder(default)] - byte_range_tag: Option, + byte_range: Option, /// The [`ExtXDateRange`] tag associated with the media segment. #[builder(default)] - date_range_tag: Option, + date_range: Option, /// The [`ExtXDiscontinuity`] tag associated with the media segment. #[builder(default)] - discontinuity_tag: Option, + discontinuity: Option, /// The [`ExtXProgramDateTime`] tag associated with the media /// segment. #[builder(default)] - program_date_time_tag: Option, + program_date_time: Option, /// The [`ExtInf`] tag associated with the [`MediaSegment`]. - inf_tag: ExtInf, + inf: ExtInf, /// The `URI` of the [`MediaSegment`]. #[shorthand(enable(into))] uri: String, @@ -47,12 +47,13 @@ impl MediaSegment { impl MediaSegmentBuilder { /// Pushes an [`ExtXKey`] tag. - pub fn push_key_tag>(&mut self, value: VALUE) -> &mut Self { - if let Some(key_tags) = &mut self.keys { - key_tags.push(value.into()); + pub fn push_key>(&mut self, value: VALUE) -> &mut Self { + if let Some(keys) = &mut self.keys { + keys.push(value.into()); } else { self.keys = Some(vec![value.into()]); } + self } } @@ -63,27 +64,27 @@ impl fmt::Display for MediaSegment { writeln!(f, "{}", value)?; } - if let Some(value) = &self.map_tag { + if let Some(value) = &self.map { writeln!(f, "{}", value)?; } - if let Some(value) = &self.byte_range_tag { + if let Some(value) = &self.byte_range { writeln!(f, "{}", value)?; } - if let Some(value) = &self.date_range_tag { + if let Some(value) = &self.date_range { writeln!(f, "{}", value)?; } - if let Some(value) = &self.discontinuity_tag { + if let Some(value) = &self.discontinuity { writeln!(f, "{}", value)?; } - if let Some(value) = &self.program_date_time_tag { + if let Some(value) = &self.program_date_time { writeln!(f, "{}", value)?; } - writeln!(f, "{}", self.inf_tag)?; // TODO: there might be a `,` missing + writeln!(f, "{}", self.inf)?; // TODO: there might be a `,` missing writeln!(f, "{}", self.uri)?; Ok(()) } @@ -93,12 +94,12 @@ impl RequiredVersion for MediaSegment { fn required_version(&self) -> ProtocolVersion { 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 + self.map, + self.byte_range, + self.date_range, + self.discontinuity, + self.program_date_time, + self.inf ] } } @@ -120,11 +121,11 @@ mod tests { 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))) + .map(ExtXMap::new("https://www.example.com/")) + .byte_range(ExtXByteRange::new(20, Some(5))) + //.date_range() // TODO! + .discontinuity(ExtXDiscontinuity) + .inf(ExtInf::new(Duration::from_secs(4))) .uri("http://www.uri.com/") .build() .unwrap() diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 4c548ee..aec2ee4 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -75,6 +75,10 @@ impl FromStr for ExtXTargetDuration { } } +impl From for ExtXTargetDuration { + fn from(value: Duration) -> Self { Self::new(value) } +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index c87bff2..e597bdc 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -6,50 +6,48 @@ use pretty_assertions::assert_eq; #[test] fn test_media_playlist_with_byterange() { - let media_playlist = concat!( - "#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\n" - ) - .parse::() - .unwrap(); - assert_eq!( MediaPlaylist::builder() .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) .media_sequence(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))) + .inf(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range(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))) + .inf(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range(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)) + .inf(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range(ExtXByteRange::new(69864, None)) .uri("video.ts") .build() .unwrap(), ]) .build() .unwrap(), - media_playlist + concat!( + "#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\n" + ) + .parse::() + .unwrap() ) } diff --git a/tests/playlist.rs b/tests/playlist.rs index c272a4a..925f4bf 100644 --- a/tests/playlist.rs +++ b/tests/playlist.rs @@ -1,10 +1,11 @@ //! Credits go to //! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py -use hls_m3u8::tags::*; -use hls_m3u8::MediaPlaylist; - use std::time::Duration; +use hls_m3u8::tags::{ExtInf, ExtXEndList}; +use hls_m3u8::{MediaPlaylist, MediaSegment}; +use pretty_assertions::assert_eq; + #[test] fn test_simple_playlist() { let playlist = concat!( @@ -17,31 +18,24 @@ fn test_simple_playlist() { "#EXT-X-ENDLIST\n" ); - let media_playlist = playlist.parse::().unwrap(); assert_eq!( - media_playlist.target_duration(), - ExtXTargetDuration::new(Duration::from_secs(5220)) - ); - - assert_eq!(media_playlist.segments().len(), 2); - - assert_eq!( - media_playlist.segments()[0].inf_tag(), - &ExtInf::new(Duration::from_secs(0)) - ); - - assert_eq!( - media_playlist.segments()[1].inf_tag(), - &ExtInf::new(Duration::from_secs(5220)) - ); - - assert_eq!( - media_playlist.segments()[0].uri(), - &"http://media.example.com/entire1.ts".to_string() - ); - - assert_eq!( - media_playlist.segments()[1].uri(), - &"http://media.example.com/entire2.ts".to_string() + MediaPlaylist::builder() + .target_duration(Duration::from_secs(5220)) + .segments(vec![ + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(0))) + .uri("http://media.example.com/entire1.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(5220))) + .uri("http://media.example.com/entire2.ts") + .build() + .unwrap(), + ]) + .end_list(ExtXEndList) + .build() + .unwrap(), + playlist.parse::().unwrap(), ); } From b54b17df736d39aed02f9cabf7eb840320cc046b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 16 Feb 2020 12:51:49 +0100 Subject: [PATCH 027/112] fix key parsing and printing --- src/media_playlist.rs | 66 +++++++++++++++++++++++++++++++------------ src/media_segment.rs | 8 ++---- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 9861b7e..bbfe944 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::fmt; use std::str::FromStr; use std::time::Duration; @@ -257,7 +258,33 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", value)?; } + // most likely only 1 ExtXKey will be in the HashSet: + let mut available_keys = HashSet::with_capacity(1); + for segment in &self.segments { + for key in segment.keys() { + // the key is new: + if available_keys.insert(key) { + let mut remove_key = None; + + // an old key might be removed: + for k in &available_keys { + if k.key_format() == key.key_format() && &key != k { + remove_key = Some(k.clone()); + break; + } + } + + if let Some(k) = remove_key { + // this should always be true: + let res = available_keys.remove(k); + debug_assert!(res); + } + + writeln!(f, "{}", key)?; + } + } + write!(f, "{}", segment)?; } @@ -305,25 +332,28 @@ fn parse_media_playlist( has_partial_segment = true; segment.discontinuity(t); } - Tag::ExtXKey(t) => { + Tag::ExtXKey(key) => { has_partial_segment = true; - if available_keys.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_keys = available_keys - .into_iter() - .map(|k| { - if t.key_format() == k.key_format() { - t.clone() - } else { - k - } - }) - .collect(); - } else { - available_keys.push(t); + + // An ExtXKey applies to every MediaSegment 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). + + let mut is_new_key = true; + + for old_key in &mut available_keys { + if old_key.key_format() == key.key_format() { + *old_key = key.clone(); + is_new_key = false; + // there are no keys with the same key_format in available_keys + // so the loop can stop here: + break; + } + } + + if is_new_key { + available_keys.push(key); } } Tag::ExtXMap(mut t) => { diff --git a/src/media_segment.rs b/src/media_segment.rs index 5d6bb59..f0b4145 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -60,9 +60,7 @@ impl MediaSegmentBuilder { impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for value in &self.keys { - writeln!(f, "{}", value)?; - } + // self.keys will be printed by MediaPlaylist! if let Some(value) = &self.map { writeln!(f, "{}", value)?; @@ -120,7 +118,7 @@ mod tests { fn test_display() { assert_eq!( MediaSegment::builder() - .keys(vec![ExtXKey::empty()]) + //.keys(vec![ExtXKey::empty()]) .map(ExtXMap::new("https://www.example.com/")) .byte_range(ExtXByteRange::new(20, Some(5))) //.date_range() // TODO! @@ -131,7 +129,7 @@ mod tests { .unwrap() .to_string(), concat!( - "#EXT-X-KEY:METHOD=NONE\n", + //"#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", From d3c238df92bd78758ba55708c648d360c63ff1cc Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 16 Feb 2020 17:09:40 +0100 Subject: [PATCH 028/112] impl Ord, Eq and Hash for (U)Float --- src/types/float.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++ src/types/ufloat.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/types/float.rs b/src/types/float.rs index d8c8ed9..c118f69 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,3 +1,4 @@ +use core::cmp::Ordering; use core::convert::TryFrom; use core::str::FromStr; @@ -99,6 +100,47 @@ impl PartialEq for Float { fn eq(&self, other: &f32) -> bool { &self.0 == other } } +// In order to implement `Eq` a struct has to satisfy +// the following requirements: +// - reflexive: a == a; +// - symmetric: a == b implies b == a; and +// - transitive: a == b and b == c implies a == c. +// +// The symmetric and transitive parts are already satisfied +// through `PartialEq`. The reflexive part is not satisfied for f32, +// because `f32::NAN` never equals `f32::NAN`. (`assert!(f32::NAN, f32::NAN)`) +// +// It is ensured, that this struct can not be constructed +// with NaN so all of the above requirements are satisfied and therefore Eq can +// be soundly implemented. +impl Eq for Float {} + +impl Ord for Float { + #[inline] + #[must_use] + fn cmp(&self, other: &Self) -> Ordering { + if *self < *other { + Ordering::Less + } else if *self == *other { + Ordering::Equal + } else { + Ordering::Greater + } + } +} + +#[doc(hidden)] +impl ::core::hash::Hash for Float { + fn hash(&self, state: &mut H) + where + H: ::core::hash::Hasher, + { + // this should be totally fine (definitely not the most + // efficient implementation as this requires an allocation) + state.write(self.to_string().as_bytes()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -170,4 +212,11 @@ mod tests { assert!(Float::try_from(::core::f32::NAN).is_err()); assert!(Float::try_from(::core::f32::NEG_INFINITY).is_err()); } + + #[test] + fn test_eq() { + struct _AssertEq + where + Float: Eq; + } } diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index 3eb2f44..a954810 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -1,3 +1,4 @@ +use core::cmp::Ordering; use core::convert::TryFrom; use core::str::FromStr; @@ -110,6 +111,47 @@ impl PartialEq for UFloat { fn eq(&self, other: &f32) -> bool { &self.0 == other } } +// In order to implement `Eq` a struct has to satisfy +// the following requirements: +// - reflexive: a == a; +// - symmetric: a == b implies b == a; and +// - transitive: a == b and b == c implies a == c. +// +// The symmetric and transitive parts are already satisfied +// through `PartialEq`. The reflexive part is not satisfied for f32, +// because `f32::NAN` never equals `f32::NAN`. (`assert!(f32::NAN, f32::NAN)`) +// +// It is ensured, that this struct can not be constructed +// with NaN so all of the above requirements are satisfied and therefore Eq can +// be soundly implemented. +impl Eq for UFloat {} + +impl Ord for UFloat { + #[inline] + #[must_use] + fn cmp(&self, other: &Self) -> Ordering { + if *self < *other { + Ordering::Less + } else if *self == *other { + Ordering::Equal + } else { + Ordering::Greater + } + } +} + +#[doc(hidden)] +impl ::core::hash::Hash for UFloat { + fn hash(&self, state: &mut H) + where + H: ::core::hash::Hasher, + { + // this should be totally fine (definitely not the most + // efficient implementation as this requires an allocation) + state.write(self.to_string().as_bytes()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -179,4 +221,11 @@ mod tests { assert!(UFloat::try_from(::core::f32::NAN).is_err()); assert!(UFloat::try_from(::core::f32::NEG_INFINITY).is_err()); } + + #[test] + fn test_eq() { + struct _AssertEq + where + UFloat: Eq; + } } From a96367e3faef3159ac3837c2cc62796aea2b749a Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 16 Feb 2020 17:14:28 +0100 Subject: [PATCH 029/112] improve tests #25 --- src/master_playlist.rs | 43 ++- tests/playlist.rs | 41 --- tests/{master_playlist.rs => rfc8216.rs} | 427 ++++++++++++++++------- 3 files changed, 334 insertions(+), 177 deletions(-) delete mode 100644 tests/playlist.rs rename tests/{master_playlist.rs => rfc8216.rs} (51%) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 9d9906f..c0ecd43 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -481,24 +481,6 @@ mod tests { #[test] fn test_display() { assert_eq!( - concat!( - "#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" - ) - .to_string(), MasterPlaylist::builder() .variants(vec![ VariantStream::ExtXStreamInf { @@ -568,7 +550,30 @@ mod tests { ]) .build() .unwrap() - .to_string() + .to_string(), + concat!( + "#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" + ) + .to_string() ); } } diff --git a/tests/playlist.rs b/tests/playlist.rs deleted file mode 100644 index 925f4bf..0000000 --- a/tests/playlist.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Credits go to -//! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py -use std::time::Duration; - -use hls_m3u8::tags::{ExtInf, ExtXEndList}; -use hls_m3u8::{MediaPlaylist, MediaSegment}; -use pretty_assertions::assert_eq; - -#[test] -fn test_simple_playlist() { - let playlist = concat!( - "#EXTM3U\n", - "#EXT-X-TARGETDURATION:5220\n", - "#EXTINF:0,\n", - "http://media.example.com/entire1.ts\n", - "#EXTINF:5220,\n", - "http://media.example.com/entire2.ts\n", - "#EXT-X-ENDLIST\n" - ); - - assert_eq!( - MediaPlaylist::builder() - .target_duration(Duration::from_secs(5220)) - .segments(vec![ - MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(0))) - .uri("http://media.example.com/entire1.ts") - .build() - .unwrap(), - MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(5220))) - .uri("http://media.example.com/entire2.ts") - .build() - .unwrap(), - ]) - .end_list(ExtXEndList) - .build() - .unwrap(), - playlist.parse::().unwrap(), - ); -} diff --git a/tests/master_playlist.rs b/tests/rfc8216.rs similarity index 51% rename from tests/master_playlist.rs rename to tests/rfc8216.rs index 2d35d35..5390f55 100644 --- a/tests/master_playlist.rs +++ b/tests/rfc8216.rs @@ -1,27 +1,173 @@ -use hls_m3u8::tags::{ExtXMedia, VariantStream}; -use hls_m3u8::types::{MediaType, StreamData}; -use hls_m3u8::MasterPlaylist; +// https://tools.ietf.org/html/rfc8216#section-8 +use std::time::Duration; +use hls_m3u8::tags::{ + ExtInf, ExtXEndList, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream, +}; +use hls_m3u8::types::{EncryptionMethod, MediaType, StreamData}; +use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; -#[test] -fn test_master_playlist() { - // https://tools.ietf.org/html/rfc8216#section-8.4 - let master_playlist = concat!( - "#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::() - .unwrap(); +macro_rules! generate_tests { + ( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => { + $( + #[test] + fn $fnname() { + assert_eq!($struct, $str.parse().unwrap()); - assert_eq!( + assert_eq!($struct.to_string(), $str.to_string()); + } + )+ + } +} + +generate_tests! [ + test_simple_playlist => { + MediaPlaylist::builder() + .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .segments(vec![ + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(9.009))) + .uri("http://media.example.com/first.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(9.009))) + .uri("http://media.example.com/second.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(3.003))) + .uri("http://media.example.com/third.ts") + .build() + .unwrap(), + ]) + .end_list(ExtXEndList) + .build() + .unwrap(), + concat!( + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-TARGETDURATION:10\n", + "#EXTINF:9.009,\n", + "http://media.example.com/first.ts\n", + "#EXTINF:9.009,\n", + "http://media.example.com/second.ts\n", + "#EXTINF:3.003,\n", + "http://media.example.com/third.ts\n", + "#EXT-X-ENDLIST\n" + ) + }, + test_live_media_playlist_using_https => { + MediaPlaylist::builder() + .target_duration(ExtXTargetDuration::new(Duration::from_secs(8))) + .media_sequence(ExtXMediaSequence::new(2680)) + .segments(vec![ + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(7.975))) + .uri("https://priv.example.com/fileSequence2680.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(7.941))) + .uri("https://priv.example.com/fileSequence2681.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(7.975))) + .uri("https://priv.example.com/fileSequence2682.ts") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + concat!( + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-TARGETDURATION:8\n", + "#EXT-X-MEDIA-SEQUENCE:2680\n", + "#EXTINF:7.975,\n", + "https://priv.example.com/fileSequence2680.ts\n", + "#EXTINF:7.941,\n", + "https://priv.example.com/fileSequence2681.ts\n", + "#EXTINF:7.975,\n", + "https://priv.example.com/fileSequence2682.ts\n", + ) + }, + test_media_playlist_with_encrypted_segments => { + MediaPlaylist::builder() + .target_duration(ExtXTargetDuration::new(Duration::from_secs(15))) + .media_sequence(ExtXMediaSequence::new(7794)) + .segments(vec![ + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(2.833))) + .keys(vec![ + ExtXKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + ) + ]) + .uri("http://media.example.com/fileSequence52-A.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(15.0))) + .keys(vec![ + ExtXKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + ) + ]) + .uri("http://media.example.com/fileSequence52-B.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(13.333))) + .keys(vec![ + ExtXKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + ) + ]) + .uri("http://media.example.com/fileSequence52-C.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(15.0))) + .keys(vec![ + ExtXKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=53" + ) + ]) + .uri("http://media.example.com/fileSequence53-A.ts") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + concat!( + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-TARGETDURATION:15\n", + "#EXT-X-MEDIA-SEQUENCE:7794\n", + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=52\"\n", + + "#EXTINF:2.833,\n", + "http://media.example.com/fileSequence52-A.ts\n", + "#EXTINF:15,\n", + "http://media.example.com/fileSequence52-B.ts\n", + "#EXTINF:13.333,\n", + "http://media.example.com/fileSequence52-C.ts\n", + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=53\"\n", + + "#EXTINF:15,\n", + "http://media.example.com/fileSequence53-A.ts\n" + ) + }, + test_master_playlist => { MasterPlaylist::builder() .variants(vec![ VariantStream::ExtXStreamInf { @@ -75,32 +221,19 @@ fn test_master_playlist() { ]) .build() .unwrap(), - master_playlist - ); -} - -#[test] -fn test_master_playlist_with_i_frames() { - // https://tools.ietf.org/html/rfc8216#section-8.5 - let master_playlist = concat!( - "#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", - // this one: - "#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::() - .unwrap(); - - assert_eq!( + concat!( + "#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\n" + ) + }, + test_master_playlist_with_i_frames => { MasterPlaylist::builder() .variants(vec![ VariantStream::ExtXStreamInf { @@ -154,38 +287,22 @@ fn test_master_playlist_with_i_frames() { ]) .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 = concat!( - "#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::() - .unwrap(); - - assert_eq!( + concat!( + "#EXTM3U\n", + "#EXT-X-STREAM-INF:BANDWIDTH=1280000\n", + "low/audio-video.m3u8\n", + "#EXT-X-I-FRAME-STREAM-INF:URI=\"low/iframe.m3u8\",BANDWIDTH=86000\n", + "#EXT-X-STREAM-INF:BANDWIDTH=2560000\n", + "mid/audio-video.m3u8\n", + "#EXT-X-I-FRAME-STREAM-INF:URI=\"mid/iframe.m3u8\",BANDWIDTH=150000\n", + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n", + "hi/audio-video.m3u8\n", + "#EXT-X-I-FRAME-STREAM-INF:URI=\"hi/iframe.m3u8\",BANDWIDTH=550000\n", + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n", + "audio-only.m3u8\n" + ) + }, + test_master_playlist_with_alternative_audio => { MasterPlaylist::builder() .media(vec![ ExtXMedia::builder() @@ -271,44 +388,46 @@ fn test_master_playlist_with_alternative_audio() { ]) .build() .unwrap(), - master_playlist - ); -} + concat!( + "#EXTM3U\n", + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"main/english-audio.m3u8\",", + "GROUP-ID=\"aac\",", + "LANGUAGE=\"en\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES\n", -#[test] -fn test_master_playlist_with_alternative_video() { - // https://tools.ietf.org/html/rfc8216#section-8.7 - let master_playlist = concat!( - "#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\"\n", - "hi/main/audio-video.m3u8" - ) - .parse::() - .unwrap(); + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"main/german-audio.m3u8\",", + "GROUP-ID=\"aac\",", + "LANGUAGE=\"de\",", + "NAME=\"Deutsch\",", + "AUTOSELECT=YES\n", - assert_eq!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"commentary/audio-only.m3u8\",", + "GROUP-ID=\"aac\",", + "LANGUAGE=\"en\",", + "NAME=\"Commentary\"\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\n" + ) + }, + test_master_playlist_with_alternative_video => { MasterPlaylist::builder() .media(vec![ // low @@ -433,6 +552,80 @@ fn test_master_playlist_with_alternative_video() { ]) .build() .unwrap(), - master_playlist - ); -} + concat!( + "#EXTM3U\n", + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"low/main/audio-video.m3u8\",", + "GROUP-ID=\"low\",", + "NAME=\"Main\",", + "DEFAULT=YES,", + "AUTOSELECT=YES", + "\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"low/centerfield/audio-video.m3u8\",", + "GROUP-ID=\"low\",", + "NAME=\"Centerfield\"", + "\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"low/dugout/audio-video.m3u8\",", + "GROUP-ID=\"low\",", + "NAME=\"Dugout\"", + "\n", + + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"mid/main/audio-video.m3u8\",", + "GROUP-ID=\"mid\",", + "NAME=\"Main\",", + "DEFAULT=YES,", + "AUTOSELECT=YES\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"mid/centerfield/audio-video.m3u8\",", + "GROUP-ID=\"mid\",", + "NAME=\"Centerfield\"\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"mid/dugout/audio-video.m3u8\",", + "GROUP-ID=\"mid\",", + "NAME=\"Dugout\"\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"hi/main/audio-video.m3u8\",", + "GROUP-ID=\"hi\",", + "NAME=\"Main\",", + "DEFAULT=YES,", + "AUTOSELECT=YES\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"hi/centerfield/audio-video.m3u8\",", + "GROUP-ID=\"hi\",", + "NAME=\"Centerfield\"\n", + + "#EXT-X-MEDIA:", + "TYPE=VIDEO,", + "URI=\"hi/dugout/audio-video.m3u8\",", + "GROUP-ID=\"hi\",", + "NAME=\"Dugout\"\n", + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n", + "low/main/audio-video.m3u8\n", + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n", + "mid/main/audio-video.m3u8\n", + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"\n", + "hi/main/audio-video.m3u8\n", + ) + } +]; From c39d10413771a9d94edf040de57f0e14118f294f Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 10:45:04 +0100 Subject: [PATCH 030/112] remove DecryptionKey --- src/tags/master_playlist/session_key.rs | 141 ++++--- src/tags/master_playlist/variant_stream.rs | 2 +- src/tags/media_segment/key.rs | 417 ++++++++++++++++++-- src/types/decryption_key.rs | 434 --------------------- src/types/mod.rs | 2 - 5 files changed, 480 insertions(+), 516 deletions(-) delete mode 100644 src/types/decryption_key.rs diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 9f0e1d8..ce93168 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,9 +1,11 @@ +use core::convert::TryFrom; use std::fmt; use std::str::FromStr; use derive_more::{Deref, DerefMut}; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; +use crate::tags::ExtXKey; +use crate::types::{EncryptionMethod, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -18,7 +20,7 @@ use crate::{Error, RequiredVersion}; /// [`MasterPlaylist`]: crate::MasterPlaylist /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXSessionKey(DecryptionKey); +pub struct ExtXSessionKey(ExtXKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; @@ -35,17 +37,34 @@ impl ExtXSessionKey { /// # Example /// /// ``` - /// # use hls_m3u8::tags::ExtXSessionKey; + /// # use hls_m3u8::tags::{ExtXSessionKey, ExtXKey}; /// use hls_m3u8::types::EncryptionMethod; /// - /// let session_key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// ExtXSessionKey::new(ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com/", + /// )); /// ``` - pub fn new>(method: EncryptionMethod, uri: T) -> Self { - if method == EncryptionMethod::None { - panic!("The EncryptionMethod is not allowed to be None"); + pub fn new(inner: ExtXKey) -> Self { + if inner.method() == EncryptionMethod::None { + panic!("the encryption method should never be `None`"); } - Self(DecryptionKey::new(method, uri)) + Self(inner) + } +} + +impl TryFrom for ExtXSessionKey { + type Error = Error; + + fn try_from(value: ExtXKey) -> Result { + if value.method() == EncryptionMethod::None { + return Err(Error::custom( + "the encryption method should never be `None`", + )); + } + + Ok(Self(value)) } } @@ -57,12 +76,13 @@ impl RequiredVersion for ExtXSessionKey { impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.0.method == EncryptionMethod::None { - // TODO: this is bad practice, this function should never fail! - return Err(fmt::Error); - } - - write!(f, "{}{}", Self::PREFIX, self.0) + // TODO: this is not the most elegant solution + write!( + f, + "{}{}", + Self::PREFIX, + self.0.to_string().replacen(ExtXKey::PREFIX, "", 1) + ) } } @@ -70,8 +90,7 @@ impl FromStr for ExtXSessionKey { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - Ok(Self(input.parse()?)) + Ok(Self(ExtXKey::parse_from_str(tag(input, Self::PREFIX)?)?)) } } @@ -83,62 +102,75 @@ mod test { #[test] fn test_display() { - let mut key = ExtXSessionKey::new( + let mut key = ExtXSessionKey::new(ExtXKey::new( EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", - ); + )); + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ])); assert_eq!( key.to_string(), - "#EXT-X-SESSION-KEY:METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52" - .to_string() + concat!( + "#EXT-X-SESSION-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52" + ) + .to_string() ); } #[test] fn test_parser() { assert_eq!( - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# - .parse::() - .unwrap(), - ExtXSessionKey::new( + concat!( + "#EXT-X-SESSION-KEY:", + "METHOD=AES-128,", + "URI=\"https://priv.example.com/key.php?r=52\"" + ) + .parse::() + .unwrap(), + ExtXSessionKey::new(ExtXKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ); - let mut key = ExtXSessionKey::new( + let mut key = ExtXSessionKey::new(ExtXKey::new( EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", - ); + )); key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ])); assert_eq!( - "#EXT-X-SESSION-KEY:METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0X10ef8f758ca555115584bb5b3c687f52" - .parse::() - .unwrap(), + concat!( + "#EXT-X-SESSION-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0X10ef8f758ca555115584bb5b3c687f52" + ) + .parse::() + .unwrap(), key ); key.set_key_format(Some(KeyFormat::Identity)); assert_eq!( - "#EXT-X-SESSION-KEY:\ - METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\"" - .parse::() - .unwrap(), + concat!( + "#EXT-X-SESSION-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\"", + ) + .parse::() + .unwrap(), key ) } @@ -146,8 +178,11 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") - .required_version(), + ExtXSessionKey::new(ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/" + )) + .required_version(), ProtocolVersion::V1 ); } @@ -155,18 +190,15 @@ mod test { // ExtXSessionKey::new should panic, if the provided // EncryptionMethod is None! #[test] - #[should_panic] - fn test_new_panic() { ExtXSessionKey::new(EncryptionMethod::None, ""); } - - #[test] - #[should_panic] - fn test_display_err() { - ExtXSessionKey(DecryptionKey::new(EncryptionMethod::None, "")).to_string(); - } + #[should_panic = "the encryption method should never be `None`"] + fn test_new_panic() { ExtXSessionKey::new(ExtXKey::new(EncryptionMethod::None, "")); } #[test] fn test_deref() { - let key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + let key = ExtXSessionKey::new(ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/", + )); assert_eq!(key.method(), EncryptionMethod::Aes128); assert_eq!(key.uri(), Some(&"https://www.example.com/".into())); @@ -174,7 +206,10 @@ mod test { #[test] fn test_deref_mut() { - let mut key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + let mut key = ExtXSessionKey::new(ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/", + )); key.set_method(EncryptionMethod::None); assert_eq!(key.method(), EncryptionMethod::None); diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index b10266c..fab983e 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -64,7 +64,7 @@ use crate::Error; /// [`ExtXProgramDateTime`]: crate::tags::ExtXProgramDateTime /// [`ExtXPlaylistType`]: crate::tags::ExtXPlaylistType /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub enum VariantStream { /// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`] /// file containing the I-frames of a multimedia presentation. diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index d44dec2..87d12fc 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,10 +1,12 @@ use std::fmt; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; +use derive_builder::Builder; +use shorthand::ShortHand; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; -use crate::utils::tag; +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; +use crate::utils::{parse_iv_from_str, quote, tag, unquote}; use crate::{Error, RequiredVersion}; /// # [4.3.2.4. EXT-X-KEY] @@ -24,8 +26,136 @@ use crate::{Error, RequiredVersion}; /// [`ExtXMap`]: crate::tags::ExtXMap /// [`Media Segment`]: crate::MediaSegment /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ExtXKey(DecryptionKey); +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[builder(setter(into), build_fn(validate = "Self::validate"))] +#[shorthand(enable(must_use, into))] +pub struct ExtXKey { + /// HLS supports multiple [`EncryptionMethod`]s for a [`MediaSegment`]. + /// + /// For example `AES-128`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!(key.method(), EncryptionMethod::SampleAes); + /// ``` + /// + /// # Note + /// + /// This attribute is required. + #[shorthand(enable(copy))] + pub(crate) method: EncryptionMethod, + /// An `URI` that specifies how to obtain the key. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_uri(Some("http://www.google.com/")); + /// + /// assert_eq!(key.uri(), Some(&"http://www.google.com/".to_string())); + /// ``` + /// + /// # Note + /// + /// This attribute is required, if the [`EncryptionMethod`] is not `None`. + #[builder(setter(into, strip_option), default)] + pub(crate) uri: Option, + /// An IV (initialization vector) is used to prevent repetitions between + /// segments of encrypted data. + /// + /// + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// # assert_eq!(key.iv(), None); + /// + /// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])); + /// + /// assert_eq!( + /// key.iv(), + /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + /// ); + /// ``` + /// + /// # Note + /// + /// This attribute is optional. + #[builder(setter(into, strip_option), default)] + // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 + #[shorthand(enable(copy), disable(option_as_ref))] + pub(crate) iv: Option<[u8; 16]>, + /// The [`KeyFormat`] specifies how the key is + /// represented in the resource identified by the `URI`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; + /// + /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_key_format(Some(KeyFormat::Identity)); + /// + /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); + /// ``` + /// + /// # Note + /// + /// This attribute is optional. + #[builder(setter(into, strip_option), default)] + #[shorthand(enable(copy))] + pub(crate) key_format: Option, + /// The [`KeyFormatVersions`] attribute. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; + /// + /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// + /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// Some(&KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) + /// ); + /// ``` + /// + /// # Note + /// + /// This attribute is optional. + #[builder(setter(into, strip_option), default)] + pub(crate) key_format_versions: Option, +} + +impl ExtXKeyBuilder { + fn validate(&self) -> Result<(), String> { + if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { + return Err(Error::custom("missing URL").to_string()); + } + Ok(()) + } +} impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; @@ -46,9 +176,36 @@ impl ExtXKey { /// ); /// ``` pub fn new>(method: EncryptionMethod, uri: T) -> Self { - Self(DecryptionKey::new(method, uri)) + Self { + method, + uri: Some(uri.into()), + iv: None, + key_format: None, + key_format_versions: None, + } } + /// Returns a Builder to build an [`ExtXKey`]. + /// + /// # Example + /// + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; + /// + /// ExtXKey::builder() + /// .method(EncryptionMethod::Aes128) + /// .uri("https://www.example.com/") + /// .iv([ + /// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + /// ]) + /// .key_format(KeyFormat::Identity) + /// .key_format_versions(vec![1, 2, 3, 4, 5]) + /// .build()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn builder() -> ExtXKeyBuilder { ExtXKeyBuilder::default() } + /// Makes a new [`ExtXKey`] tag without a decryption key. /// /// # Example @@ -60,13 +217,13 @@ impl ExtXKey { /// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE"); /// ``` pub const fn empty() -> Self { - Self(DecryptionKey { + Self { method: EncryptionMethod::None, uri: None, iv: None, key_format: None, key_format_versions: None, - }) + } } /// Returns whether the [`EncryptionMethod`] is @@ -84,13 +241,71 @@ impl ExtXKey { /// ``` /// /// [`None`]: EncryptionMethod::None - pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } + pub fn is_empty(&self) -> bool { self.method() == EncryptionMethod::None } } -/// This tag requires the same [`ProtocolVersion`] that is returned by -/// `DecryptionKey::required_version`. +/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or +/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is +/// specified. +/// +/// Otherwise [`ProtocolVersion::V1`] is required. impl RequiredVersion for ExtXKey { - fn required_version(&self) -> ProtocolVersion { self.0.required_version() } + fn required_version(&self) -> ProtocolVersion { + if self.key_format.is_some() || self.key_format_versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } + } +} + +impl ExtXKey { + /// Parses a String without verifying the starting tag + pub(crate) fn parse_from_str(input: &str) -> crate::Result { + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut key_format = None; + let mut key_format_versions = None; + + for (key, value) in AttributePairs::new(input) { + match key { + "METHOD" => method = Some(value.parse().map_err(Error::strum)?), + "URI" => { + let unquoted_uri = unquote(value); + + if unquoted_uri.trim().is_empty() { + uri = None; + } else { + uri = Some(unquoted_uri); + } + } + "IV" => iv = Some(parse_iv_from_str(value)?), + "KEYFORMAT" => key_format = Some(value.parse()?), + "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized + // AttributeName. + } + } + } + + let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; + if method != EncryptionMethod::None && uri.is_none() { + return Err(Error::missing_value("URI")); + } + + Ok(Self { + method, + uri, + iv, + key_format, + key_format_versions, + }) + } } impl FromStr for ExtXKey { @@ -98,14 +313,40 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - Ok(Self(input.parse()?)) + Self::parse_from_str(input) } } impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // - write!(f, "{}{}", Self::PREFIX, self.0) + write!(f, "{}", Self::PREFIX)?; + + write!(f, "METHOD={}", self.method)?; + + if self.method == EncryptionMethod::None { + return Ok(()); + } + + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + + if let Some(value) = &self.iv { + // TODO: use hex::encode_to_slice + write!(f, ",IV=0x{}", hex::encode(&value))?; + } + + if let Some(value) = &self.key_format { + write!(f, ",KEYFORMAT={}", quote(value))?; + } + + if let Some(key_format_versions) = &self.key_format_versions { + if !key_format_versions.is_default() { + write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?; + } + } + + Ok(()) } } @@ -115,6 +356,36 @@ mod test { use crate::types::{EncryptionMethod, KeyFormat}; use pretty_assertions::assert_eq; + #[test] + fn test_builder() { + assert_eq!( + ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,]) + .key_format(KeyFormat::Identity) + .key_format_versions(vec![1, 2, 3, 4, 5]) + .build() + .unwrap() + .to_string(), + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\",", + "KEYFORMATVERSIONS=\"1/2/3/4/5\"", + ) + .to_string() + ); + + assert!(ExtXKey::builder().build().is_err()); + assert!(ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .build() + .is_err()); + } + #[test] fn test_display() { assert_eq!( @@ -132,28 +403,122 @@ mod test { key.set_key_format_versions(Some(vec![1, 2, 3])); assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); + + assert_eq!( + ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .build() + .unwrap() + .to_string(), + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52" + ) + .to_string() + ); } #[test] fn test_parser() { assert_eq!( - "#EXT-X-KEY:\ - METHOD=AES-128,\ - URI=\"https://priv.example.com/key.php?r=52\"" - .parse::() - .unwrap(), + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"https://priv.example.com/key.php?r=52\"" + ) + .parse::() + .unwrap(), ExtXKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" ) ); - let mut key = ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", + assert_eq!( + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0X10ef8f758ca555115584bb5b3c687f52" + ) + .parse::() + .unwrap(), + ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .build() + .unwrap() + ); + + assert_eq!( + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0X10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\",", + "KEYFORMATVERSIONS=\"1/2/3\"" + ) + .parse::() + .unwrap(), + ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .key_format(KeyFormat::Identity) + .key_format_versions(vec![1, 2, 3]) + .build() + .unwrap() + ); + + assert_eq!( + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"http://www.example.com\",", + "UNKNOWNTAG=abcd" + ) + .parse::() + .unwrap(), + ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com") + ); + assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::().is_err()); + assert!("garbage".parse::().is_err()); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/").required_version(), + ProtocolVersion::V1 + ); + + assert_eq!( + ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .key_format(KeyFormat::Identity) + .key_format_versions(vec![1, 2, 3]) + .build() + .unwrap() + .required_version(), + ProtocolVersion::V5 + ); + + assert_eq!( + ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + .build() + .unwrap() + .required_version(), + ProtocolVersion::V2 ); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs deleted file mode 100644 index 275899a..0000000 --- a/src/types/decryption_key.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_builder::Builder; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; -use crate::utils::{parse_iv_from_str, quote, unquote}; -use crate::{Error, RequiredVersion}; - -/// A [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`] -/// and [`ExtXKey`]. -/// -/// [`ExtXSessionKey`]: crate::tags::ExtXSessionKey -/// [`ExtXKey`]: crate::tags::ExtXKey -#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[builder(setter(into), build_fn(validate = "Self::validate"))] -#[shorthand(enable(must_use, into))] -pub struct DecryptionKey { - /// HLS supports multiple encryption methods for a segment. - /// - /// For example `AES-128`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is required. - #[shorthand(enable(copy))] - pub(crate) method: EncryptionMethod, - /// An `URI` that specifies how to obtain the key. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_uri(Some("http://www.google.com/")); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is required, if the [`EncryptionMethod`] is not `None`. - #[builder(setter(into, strip_option), default)] - pub(crate) uri: Option, - /// An IV (initialization vector) is used to prevent repetitions between - /// segments of encrypted data. - /// - /// - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// # assert_eq!(key.iv(), None); - /// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])); - /// - /// assert_eq!( - /// key.iv(), - /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 - #[shorthand(enable(copy), disable(option_as_ref))] - pub(crate) iv: Option<[u8; 16]>, - /// The [`KeyFormat`] specifies how the key is - /// represented in the resource identified by the `URI`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format(Some(KeyFormat::Identity)); - /// - /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - #[shorthand(enable(copy))] - pub(crate) key_format: Option, - /// The [`KeyFormatVersions`] attribute. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); - /// - /// assert_eq!( - /// key.key_format_versions(), - /// Some(&KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - pub(crate) key_format_versions: Option, -} - -impl DecryptionKeyBuilder { - fn validate(&self) -> Result<(), String> { - if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { - return Err(Error::custom("Missing URL").to_string()); - } - Ok(()) - } -} - -impl DecryptionKey { - /// Makes a new [`DecryptionKey`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// ``` - #[doc(hidden)] - pub fn new>(method: EncryptionMethod, uri: T) -> Self { - Self { - method, - uri: Some(uri.into()), - iv: None, - key_format: None, - key_format_versions: None, - } - } - - /// Returns a Builder to build a [`DecryptionKey`]. - pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } -} - -/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or -/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is -/// specified. -/// -/// Otherwise [`ProtocolVersion::V1`] is required. -impl RequiredVersion for DecryptionKey { - fn required_version(&self) -> ProtocolVersion { - if self.key_format.is_some() || self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} - -#[doc(hidden)] -impl FromStr for DecryptionKey { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "METHOD" => method = Some(value.parse().map_err(Error::strum)?), - "URI" => { - let unquoted_uri = unquote(value); - - if unquoted_uri.trim().is_empty() { - uri = None; - } else { - uri = Some(unquoted_uri); - } - } - "IV" => iv = Some(parse_iv_from_str(value)?), - "KEYFORMAT" => key_format = Some(value.parse()?), - "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized - // AttributeName. - } - } - } - - let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; - if method != EncryptionMethod::None && uri.is_none() { - return Err(Error::missing_value("URI")); - } - - Ok(Self { - method, - uri, - iv, - key_format, - key_format_versions, - }) - } -} - -impl fmt::Display for DecryptionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "METHOD={}", self.method)?; - - if self.method == EncryptionMethod::None { - return Ok(()); - } - - if let Some(uri) = &self.uri { - write!(f, ",URI={}", quote(uri))?; - } - - if let Some(value) = &self.iv { - // TODO: use hex::encode_to_slice - write!(f, ",IV=0x{}", hex::encode(&value))?; - } - - if let Some(value) = &self.key_format { - write!(f, ",KEYFORMAT={}", quote(value))?; - } - - if let Some(key_format_versions) = &self.key_format_versions { - if !key_format_versions.is_default() { - write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?; - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::types::EncryptionMethod; - use pretty_assertions::assert_eq; - - #[test] - fn test_builder() { - let key = DecryptionKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .iv([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]) - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3, 4, 5]) - .build() - .unwrap(); - - assert_eq!( - key.to_string(), - "METHOD=AES-128,\ - URI=\"https://www.example.com/\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\",\ - KEYFORMATVERSIONS=\"1/2/3/4/5\"\ - " - .to_string() - ); - - assert!(DecryptionKey::builder().build().is_err()); - assert!(DecryptionKey::builder() - .method(EncryptionMethod::Aes128) - .build() - .is_err()); - } - - #[test] - fn test_display() { - let mut key = DecryptionKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", - ); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - - assert_eq!( - key.to_string(), - "METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52" - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - "METHOD=AES-128,\ - URI=\"https://priv.example.com/key.php?r=52\"" - .parse::() - .unwrap(), - DecryptionKey::new( - EncryptionMethod::Aes128, - "https://priv.example.com/key.php?r=52" - ) - ); - - let mut key = DecryptionKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", - ); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - - assert_eq!( - "METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0X10ef8f758ca555115584bb5b3c687f52" - .parse::() - .unwrap(), - key - ); - - let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - key.set_key_format(Some(KeyFormat::Identity)); - - assert_eq!( - "METHOD=AES-128,\ - URI=\"http://www.example.com\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\"" - .parse::() - .unwrap(), - key - ); - - key.set_key_format_versions(Some(vec![1, 2, 3])); - assert_eq!( - "METHOD=AES-128,\ - URI=\"http://www.example.com\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\",\ - KEYFORMATVERSIONS=\"1/2/3\"" - .parse::() - .unwrap(), - key - ); - - assert_eq!( - "METHOD=AES-128,\ - URI=\"http://www.example.com\",\ - UNKNOWNTAG=abcd" - .parse::() - .unwrap(), - DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com") - ); - assert!("METHOD=AES-128,URI=".parse::().is_err()); - assert!("garbage".parse::().is_err()); - } - - #[test] - fn test_required_version() { - assert_eq!( - DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") - .required_version(), - ProtocolVersion::V1 - ); - - assert_eq!( - DecryptionKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3]) - .build() - .unwrap() - .required_version(), - ProtocolVersion::V5 - ); - - assert_eq!( - DecryptionKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - .build() - .unwrap() - .required_version(), - ProtocolVersion::V2 - ); - } -} diff --git a/src/types/mod.rs b/src/types/mod.rs index 2f00d48..5bb0b04 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,7 +2,6 @@ mod byte_range; mod channels; mod closed_captions; -mod decryption_key; mod encryption_method; mod hdcp_level; mod in_stream_id; @@ -20,7 +19,6 @@ mod ufloat; pub use byte_range::*; pub use channels::*; pub use closed_captions::*; -pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; From 86bb573c9736b1c6463bc277cb80cffffbe1743f Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 20:41:31 +0100 Subject: [PATCH 031/112] various improvements to InStreamId --- src/tags/master_playlist/media.rs | 11 +++++++++-- src/types/in_stream_id.rs | 28 ++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index f83d9b6..e30b079 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -111,10 +111,17 @@ pub struct ExtXMedia { /// essential to play. #[builder(default)] is_forced: bool, - /// An [`InStreamId`] specifies a rendition within the - /// segments in the [`MediaPlaylist`]. + /// An [`InStreamId`] identifies a rendition within the + /// [`MediaSegment`]s in a [`MediaPlaylist`]. + /// + /// # Note + /// + /// This attribute is required, if the [`ExtXMedia::media_type`] is + /// [`MediaType::ClosedCaptions`]. For all other [`ExtXMedia::media_type`] + /// the [`InStreamId`] must not be specified! /// /// [`MediaPlaylist`]: crate::MediaPlaylist + /// [`MediaSegment`]: crate::MediaSegment #[builder(setter(strip_option), default)] #[shorthand(enable(copy))] instream_id: Option, diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 6f1fd1a..7d36d6a 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -1,15 +1,26 @@ use strum::{Display, EnumString}; -/// Identifier of a rendition within the segments in a media playlist. +use crate::traits::RequiredVersion; +use crate::types::ProtocolVersion; + +/// Identifier of a rendition within the [`MediaSegment`]s in a +/// [`MediaPlaylist`]. /// -/// See: [4.3.4.1. EXT-X-MEDIA] +/// The variants [`InStreamId::Cc1`], [`InStreamId::Cc2`], [`InStreamId::Cc3`], +/// and [`InStreamId::Cc4`] identify a Line 21 Data Services channel ([CEA608]). /// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +/// The `Service` variants identify a Digital Television Closed Captioning +/// ([CEA708]) service block number. The `Service` variants range from +/// [`InStreamId::Service1`] to [`InStreamId::Service63`]. +/// +/// [CEA608]: https://tools.ietf.org/html/rfc8216#ref-CEA608 +/// [CEA708]: https://tools.ietf.org/html/rfc8216#ref-CEA708 +/// [`MediaSegment`]: crate::MediaSegment +/// [`MediaPlaylist`]: crate::MediaPlaylist #[non_exhaustive] #[allow(missing_docs)] #[strum(serialize_all = "UPPERCASE")] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] -#[non_exhaustive] pub enum InStreamId { Cc1, Cc2, @@ -80,6 +91,15 @@ pub enum InStreamId { Service63, } +impl RequiredVersion for InStreamId { + fn required_version(&self) -> ProtocolVersion { + match &self { + Self::Cc1 | Self::Cc2 | Self::Cc3 | Self::Cc4 => ProtocolVersion::V1, + _ => ProtocolVersion::V7, + } + } +} + #[cfg(test)] mod tests { use super::*; From 30e8009af1233e9b6136b85f754db28f21e204ce Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 20:42:14 +0100 Subject: [PATCH 032/112] fix Float and UFloat --- src/types/float.rs | 95 ++++++++++++++++++++++++++++++++++++--------- src/types/ufloat.rs | 51 ++++++++++++++++++++---- 2 files changed, 120 insertions(+), 26 deletions(-) diff --git a/src/types/float.rs b/src/types/float.rs index c118f69..e14b9c5 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -12,7 +12,7 @@ use crate::Error; /// [`NaN`]: core::f32::NAN /// [`INFINITY`]: core::f32::INFINITY /// [`NEG_INFINITY`]: core::f32::NEG_INFINITY -#[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)] +#[derive(Deref, Default, Debug, Copy, Clone, Display, PartialOrd)] pub struct Float(f32); impl Float { @@ -95,8 +95,14 @@ macro_rules! implement_from { implement_from!(i16, u16, i8, u8); +impl PartialEq for Float { + #[inline] + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} + // convenience implementation to compare f32 with a Float. impl PartialEq for Float { + #[inline] fn eq(&self, other: &f32) -> bool { &self.0 == other } } @@ -117,11 +123,10 @@ impl Eq for Float {} impl Ord for Float { #[inline] - #[must_use] fn cmp(&self, other: &Self) -> Ordering { - if *self < *other { + if self.0 < other.0 { Ordering::Less - } else if *self == *other { + } else if self == other { Ordering::Equal } else { Ordering::Greater @@ -129,15 +134,43 @@ impl Ord for Float { } } +/// The output of Hash cannot be relied upon to be stable. The same version of +/// rust can return different values in different architectures. This is not a +/// property of the Hasher that you’re using but instead of the way Hash happens +/// to be implemented for the type you’re using (e.g., the current +/// implementation of Hash for slices of integers returns different values in +/// big and little-endian architectures). +/// +/// See #[doc(hidden)] impl ::core::hash::Hash for Float { fn hash(&self, state: &mut H) where H: ::core::hash::Hasher, { - // this should be totally fine (definitely not the most - // efficient implementation as this requires an allocation) - state.write(self.to_string().as_bytes()) + // this implementation assumes, that the internal float is: + // - not NaN + // - neither negative nor positive infinity + + // to validate those assumptions debug_assertions are here + // (those will be removed in a release build) + debug_assert!(self.0.is_finite()); + debug_assert!(!self.0.is_nan()); + + // this implementation is based on + // https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/33 + // + // The important points are: + // - NaN == NaN (Float does not allow NaN, so this should be satisfied) + // - +0 == -0 + + if self.0 == 0.0 || self.0 == -0.0 { + state.write(&0.0_f32.to_be_bytes()); + } else { + // I do not think it matters to differentiate between architectures, that use + // big endian by default and those, that use little endian. + state.write(&self.to_be_bytes()) + } } } @@ -146,6 +179,42 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn test_ord() { + assert_eq!(Float::new(1.1).cmp(&Float::new(1.1)), Ordering::Equal); + assert_eq!(Float::new(1.1).cmp(&Float::new(2.1)), Ordering::Less); + assert_eq!(Float::new(1.1).cmp(&Float::new(0.1)), Ordering::Greater); + } + + #[test] + fn test_partial_ord() { + assert_eq!( + Float::new(1.1).partial_cmp(&Float::new(1.1)), + Some(Ordering::Equal) + ); + assert_eq!( + Float::new(1.1).partial_cmp(&Float::new(2.1)), + Some(Ordering::Less) + ); + assert_eq!( + Float::new(1.1).partial_cmp(&Float::new(0.1)), + Some(Ordering::Greater) + ); + } + + #[test] + fn test_eq() { + struct _AssertEq + where + Float: Eq; + } + + #[test] + fn test_partial_eq() { + assert_eq!(Float::new(1.0).eq(&Float::new(1.0)), true); + assert_eq!(Float::new(1.0).eq(&Float::new(33.3)), false); + } + #[test] fn test_display() { assert_eq!(Float::new(22.0).to_string(), "22".to_string()); @@ -185,11 +254,6 @@ mod tests { #[should_panic = "float must not be `NaN`"] fn test_new_nan() { Float::new(::core::f32::NAN); } - #[test] - fn test_partial_eq() { - assert_eq!(Float::new(1.1), 1.1); - } - #[test] fn test_as_f32() { assert_eq!(Float::new(1.1).as_f32(), 1.1_f32); @@ -212,11 +276,4 @@ mod tests { assert!(Float::try_from(::core::f32::NAN).is_err()); assert!(Float::try_from(::core::f32::NEG_INFINITY).is_err()); } - - #[test] - fn test_eq() { - struct _AssertEq - where - Float: Eq; - } } diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index a954810..4678d89 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -13,7 +13,7 @@ use crate::Error; /// [`NaN`]: core::f32::NAN /// [`INFINITY`]: core::f32::INFINITY /// [`NEG_INFINITY`]: core::f32::NEG_INFINITY -#[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)] +#[derive(Deref, Default, Debug, Copy, Clone, PartialOrd, Display)] pub struct UFloat(f32); impl UFloat { @@ -106,8 +106,16 @@ macro_rules! implement_from { implement_from!(u16, u8); +// This has to be implemented explicitly, because `Hash` is also implemented +// manually and both implementations have to agree according to clippy. +impl PartialEq for UFloat { + #[inline] + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} + // convenience implementation to compare f32 with a Float. impl PartialEq for UFloat { + #[inline] fn eq(&self, other: &f32) -> bool { &self.0 == other } } @@ -128,11 +136,10 @@ impl Eq for UFloat {} impl Ord for UFloat { #[inline] - #[must_use] fn cmp(&self, other: &Self) -> Ordering { - if *self < *other { + if self.0 < other.0 { Ordering::Less - } else if *self == *other { + } else if self == other { Ordering::Equal } else { Ordering::Greater @@ -140,15 +147,41 @@ impl Ord for UFloat { } } +/// The output of Hash cannot be relied upon to be stable. The same version of +/// rust can return different values in different architectures. This is not a +/// property of the Hasher that you’re using but instead of the way Hash happens +/// to be implemented for the type you’re using (e.g., the current +/// implementation of Hash for slices of integers returns different values in +/// big and little-endian architectures). +/// +/// See #[doc(hidden)] impl ::core::hash::Hash for UFloat { fn hash(&self, state: &mut H) where H: ::core::hash::Hasher, { - // this should be totally fine (definitely not the most - // efficient implementation as this requires an allocation) - state.write(self.to_string().as_bytes()) + // this implementation assumes, that the internal float is: + // - positive + // - not NaN + // - neither negative nor positive infinity + + // to validate those assumptions debug_assertions are here + // (those will be removed in a release build) + debug_assert!(self.0.is_sign_positive()); + debug_assert!(self.0.is_finite()); + debug_assert!(!self.0.is_nan()); + + // this implementation is based on + // https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/33 + // + // The important points are: + // - NaN == NaN (UFloat does not allow NaN, so this should be satisfied) + // - +0 != -0 (UFloat does not allow negative numbers, so this is fine too) + + // I do not think it matters to differentiate between architectures, that use + // big endian by default and those, that use little endian. + state.write(&self.to_be_bytes()) } } @@ -184,6 +217,10 @@ mod tests { #[should_panic = "float must be positive: `-1.1`"] fn test_new_negative() { UFloat::new(-1.1); } + #[test] + #[should_panic = "float must be positive: `0`"] + fn test_new_negative_zero() { UFloat::new(-0.0); } + #[test] #[should_panic = "float must be finite: `inf`"] fn test_new_infinite() { UFloat::new(::core::f32::INFINITY); } From 070a62f9ad6592126632df2c0c59ea684cdf5363 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 20:42:44 +0100 Subject: [PATCH 033/112] improvements to ClosedCaptions --- src/types/closed_captions.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index ae2350b..0948b13 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -19,7 +19,7 @@ pub enum ClosedCaptions { /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type /// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions GroupId(String), - /// [`ClosedCaptions::None`] indicates that there are no closed captions in + /// This variant indicates that there are no closed captions in /// any [`VariantStream`] in the [`MasterPlaylist`], therefore all /// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a /// value of [`ClosedCaptions::None`]. @@ -34,6 +34,34 @@ pub enum ClosedCaptions { None, } +impl ClosedCaptions { + /// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`]. + /// + /// # Example + /// + /// ``` + /// use hls_m3u8::types::ClosedCaptions; + /// + /// assert_eq!( + /// ClosedCaptions::group_id("vg1"), + /// ClosedCaptions::GroupId("vg1".into()) + /// ); + /// ``` + pub fn group_id>(value: I) -> Self { + // + Self::GroupId(value.into()) + } +} + +impl> PartialEq for ClosedCaptions { + fn eq(&self, other: &T) -> bool { + match &self { + Self::GroupId(value) => other.eq(value), + Self::None => other.eq("NONE"), + } + } +} + impl fmt::Display for ClosedCaptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { From f404e68d1ccc802dc003a0e414dfa535729ac186 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 20:43:12 +0100 Subject: [PATCH 034/112] add VariantStream::is_associated --- src/tags/master_playlist/variant_stream.rs | 184 ++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index fab983e..9723b08 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -3,8 +3,9 @@ use core::ops::Deref; use core::str::FromStr; use crate::attribute::AttributePairs; +use crate::tags::ExtXMedia; use crate::traits::RequiredVersion; -use crate::types::{ClosedCaptions, ProtocolVersion, StreamData, UFloat}; +use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, StreamData, UFloat}; use crate::utils::{quote, tag, unquote}; use crate::Error; @@ -168,11 +169,98 @@ pub enum VariantStream { impl VariantStream { pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:"; + + /// Checks if a [`VariantStream`] and an [`ExtXMedia`] element are + /// associated. + /// + /// # Example + /// + /// ``` + /// use hls_m3u8::tags::{ExtXMedia, VariantStream}; + /// use hls_m3u8::types::{ClosedCaptions, MediaType, StreamData}; + /// + /// let variant_stream = VariantStream::ExtXStreamInf { + /// uri: "https://www.example.com/init.bin".into(), + /// frame_rate: None, + /// audio: Some("ag1".into()), + /// subtitles: Some("sg1".into()), + /// closed_captions: Some(ClosedCaptions::group_id("cc1")), + /// stream_data: StreamData::builder() + /// .bandwidth(1_110_000) + /// .video("vg1") + /// .build() + /// .unwrap(), + /// }; + /// + /// assert!(variant_stream.is_associated( + /// &ExtXMedia::builder() + /// .media_type(MediaType::Audio) + /// .group_id("ag1") + /// .name("audio example") + /// .build() + /// .unwrap(), + /// )); + /// ``` + pub fn is_associated(&self, media: &ExtXMedia) -> bool { + match &self { + Self::ExtXIFrame { stream_data, .. } => { + if let MediaType::Video = media.media_type() { + if let Some(value) = stream_data.video() { + return value == media.group_id(); + } + } + + false + } + Self::ExtXStreamInf { + audio, + subtitles, + closed_captions, + stream_data, + .. + } => { + match media.media_type() { + MediaType::Audio => audio.as_ref().map_or(false, |v| v == media.group_id()), + MediaType::Video => { + stream_data.video().map_or(false, |v| v == media.group_id()) + } + MediaType::Subtitles => { + subtitles.as_ref().map_or(false, |v| v == media.group_id()) + } + MediaType::ClosedCaptions => { + closed_captions + .as_ref() + .map_or(false, |v| v == media.group_id()) + } + } + } + } + } } /// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for VariantStream { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } + + fn introduced_version(&self) -> ProtocolVersion { + match &self { + Self::ExtXStreamInf { + audio, + subtitles, + stream_data, + .. + } => { + if stream_data.introduced_version() >= ProtocolVersion::V4 { + stream_data.introduced_version() + } else if audio.is_some() || subtitles.is_some() { + ProtocolVersion::V4 + } else { + ProtocolVersion::V1 + } + } + Self::ExtXIFrame { stream_data, .. } => stream_data.introduced_version(), + } + } } impl fmt::Display for VariantStream { @@ -287,3 +375,97 @@ impl Deref for VariantStream { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::InStreamId; + //use pretty_assertions::assert_eq; + + #[test] + fn test_required_version() { + assert_eq!( + VariantStream::ExtXStreamInf { + uri: "https://www.example.com/init.bin".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(1_110_000) + } + .required_version(), + ProtocolVersion::V1 + ); + } + + #[test] + fn test_is_associated() { + let mut variant_stream = VariantStream::ExtXStreamInf { + uri: "https://www.example.com/init.bin".into(), + frame_rate: None, + audio: Some("ag1".into()), + subtitles: Some("sg1".into()), + closed_captions: Some(ClosedCaptions::group_id("cc1")), + stream_data: StreamData::builder() + .bandwidth(1_110_000) + .video("vg1") + .build() + .unwrap(), + }; + + assert!(variant_stream.is_associated( + &ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("ag1") + .name("audio example") + .build() + .unwrap(), + )); + + assert!(variant_stream.is_associated( + &ExtXMedia::builder() + .media_type(MediaType::Subtitles) + .uri("https://www.example.com/sg1.ssa") + .group_id("sg1") + .name("subtitle example") + .build() + .unwrap(), + )); + + assert!(variant_stream.is_associated( + &ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("cc1") + .name("closed captions example") + .instream_id(InStreamId::Cc1) + .build() + .unwrap(), + )); + + if let VariantStream::ExtXStreamInf { + closed_captions, .. + } = &mut variant_stream + { + *closed_captions = Some(ClosedCaptions::None); + } + + assert!(variant_stream.is_associated( + &ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("NONE") + .name("closed captions example") + .instream_id(InStreamId::Cc1) + .build() + .unwrap(), + )); + + assert!(variant_stream.is_associated( + &ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("vg1") + .name("video example") + .build() + .unwrap(), + )); + } +} From 53049478851630f66db9bf0d1cd8cf9250467942 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 20:44:09 +0100 Subject: [PATCH 035/112] various minor improvements --- src/master_playlist.rs | 2 ++ src/media_playlist.rs | 2 +- src/tags/media_segment/key.rs | 13 +++++++++---- src/tags/media_segment/program_date_time.rs | 4 ++-- src/tags/shared/mod.rs | 11 ----------- src/types/stream_data.rs | 19 ++++++++++++++++--- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index c0ecd43..37d79c4 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -82,6 +82,8 @@ pub struct MasterPlaylist { /// # Note /// /// This tag is optional. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] session_keys: Vec, /// This is a list of all tags that could not be identified while parsing diff --git a/src/media_playlist.rs b/src/media_playlist.rs index bbfe944..0209226 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -270,7 +270,7 @@ impl fmt::Display for MediaPlaylist { // an old key might be removed: for k in &available_keys { if k.key_format() == key.key_format() && &key != k { - remove_key = Some(k.clone()); + remove_key = Some(*k); break; } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index 87d12fc..379276b 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -50,6 +50,8 @@ pub struct ExtXKey { /// # Note /// /// This attribute is required. + /// + /// [`MediaSegment`]: crate::MediaSegment #[shorthand(enable(copy))] pub(crate) method: EncryptionMethod, /// An `URI` that specifies how to obtain the key. @@ -100,7 +102,7 @@ pub struct ExtXKey { #[builder(setter(into, strip_option), default)] // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 #[shorthand(enable(copy), disable(option_as_ref))] - pub(crate) iv: Option<[u8; 16]>, + pub(crate) iv: Option<[u8; 0x10]>, /// The [`KeyFormat`] specifies how the key is /// represented in the resource identified by the `URI`. /// @@ -151,8 +153,9 @@ pub struct ExtXKey { impl ExtXKeyBuilder { fn validate(&self) -> Result<(), String> { if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { - return Err(Error::custom("missing URL").to_string()); + return Err(Error::missing_value("URL").to_string()); } + Ok(()) } } @@ -332,8 +335,10 @@ impl fmt::Display for ExtXKey { } if let Some(value) = &self.iv { - // TODO: use hex::encode_to_slice - write!(f, ",IV=0x{}", hex::encode(&value))?; + let mut result = [0; 0x10 * 2]; + hex::encode_to_slice(value, &mut result).unwrap(); + + write!(f, ",IV=0x{}", ::core::str::from_utf8(&result).unwrap())?; } if let Some(value) = &self.key_format { diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index ff91b00..bb68f37 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -11,9 +11,9 @@ use crate::{Error, RequiredVersion}; /// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] /// /// The [`ExtXProgramDateTime`] tag associates the first sample of a -/// [`Media Segment`] with an absolute date and/or time. +/// [`MediaSegment`] with an absolute date and/or time. /// -/// [`Media Segment`]: crate::MediaSegment +/// [`MediaSegment`]: crate::MediaSegment /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: /// https://tools.ietf.org/html/rfc8216#section-4.3.2.6 #[derive(Deref, DerefMut, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] diff --git a/src/tags/shared/mod.rs b/src/tags/shared/mod.rs index 052f0eb..956efe7 100644 --- a/src/tags/shared/mod.rs +++ b/src/tags/shared/mod.rs @@ -1,14 +1,3 @@ -//! The tags in this section can appear in either [`MasterPlaylist`]s or -//! [`MediaPlaylist`]s. If one of these tags appears in a [`MasterPlaylist`], -//! it should not appear in any [`MediaPlaylist`] referenced by that -//! [`MasterPlaylist`]. A tag that appears in both must have the same value; -//! otherwise, clients should ignore the value in the [`MediaPlaylist`](s). -//! -//! These tags must not appear more than once in a Playlist. If a tag -//! appears more than once, clients must fail to parse the Playlist. -//! -//! [`MediaPlaylist`]: crate::MediaPlaylist -//! [`MasterPlaylist`]: crate::MasterPlaylist mod independent_segments; mod start; diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs index 8aad849..f8f03e0 100644 --- a/src/types/stream_data.rs +++ b/src/types/stream_data.rs @@ -5,9 +5,9 @@ use derive_builder::Builder; use shorthand::ShortHand; use crate::attribute::AttributePairs; -use crate::types::{HdcpLevel, Resolution}; +use crate::types::{HdcpLevel, ProtocolVersion, Resolution}; use crate::utils::{quote, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// The [`StreamData`] struct contains the data that is shared between both /// variants of the [`VariantStream`]. @@ -15,7 +15,7 @@ use crate::Error; /// [`VariantStream`]: crate::tags::VariantStream #[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] #[builder(setter(strip_option))] -#[builder(derive(Debug, PartialEq))] +#[builder(derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash))] #[shorthand(enable(must_use, into))] pub struct StreamData { /// The peak segment bitrate of the [`VariantStream`] in bits per second. @@ -324,6 +324,19 @@ impl FromStr for StreamData { } } +/// This struct requires [`ProtocolVersion::V1`]. +impl RequiredVersion for StreamData { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } + + fn introduced_version(&self) -> ProtocolVersion { + if self.video.is_some() { + ProtocolVersion::V4 + } else { + ProtocolVersion::V1 + } + } +} + #[cfg(test)] mod tests { use super::*; From 8948f9914bb17c17e5336ef7f94679f5dec924b7 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 21:11:51 +0100 Subject: [PATCH 036/112] improve documentation of ExtXVersion --- src/tags/basic/version.rs | 38 +++----------------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 8108cf7..3b2e3cf 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -5,44 +5,12 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.1.2. EXT-X-VERSION] +/// The compatibility version of a playlist. /// -/// The [`ExtXVersion`] tag indicates the compatibility version of the -/// [`MasterPlaylist`] or [`MediaPlaylist`] file. -/// It applies to the entire Playlist. -/// -/// # Examples -/// -/// Parsing from a [`str`]: -/// -/// ``` -/// # use hls_m3u8::tags::ExtXVersion; -/// # -/// use hls_m3u8::types::ProtocolVersion; -/// -/// assert_eq!( -/// "#EXT-X-VERSION:5".parse::()?, -/// ExtXVersion::new(ProtocolVersion::V5) -/// ); -/// # Ok::<(), Box>(()) -/// ``` -/// -/// Converting to a [`str`]: -/// -/// ``` -/// # use hls_m3u8::tags::ExtXVersion; -/// # -/// use hls_m3u8::types::ProtocolVersion; -/// -/// assert_eq!( -/// "#EXT-X-VERSION:5".to_string(), -/// ExtXVersion::new(ProtocolVersion::V5).to_string() -/// ); -/// ``` +/// It applies to the entire [`MasterPlaylist`] or [`MediaPlaylist`]. /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist -/// [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); @@ -61,7 +29,7 @@ impl ExtXVersion { /// ``` pub const fn new(version: ProtocolVersion) -> Self { Self(version) } - /// Returns the [`ProtocolVersion`] of the playlist, containing this tag. + /// Returns the underlying [`ProtocolVersion`]. /// /// # Example /// From a8c788f4d2a4a8c18b1c66a01983713eb16c8bb7 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 21 Feb 2020 22:06:09 +0100 Subject: [PATCH 037/112] improve documentation and tests of ExtXSessionData --- src/tags/master_playlist/session_data.rs | 312 +++++++++++------------ 1 file changed, 142 insertions(+), 170 deletions(-) diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 75ad365..4c23fd4 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -9,29 +9,26 @@ use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; -/// The data of an [`ExtXSessionData`] tag. +/// The data of [`ExtXSessionData`]. #[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`] is specified, the value - /// should contain a human-readable string written in the specified - /// language. + /// This variant contains the data identified by the + /// [`ExtXSessionData::data_id`]. + /// + /// If a [`language`] is specified, this variant 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`]. + /// An [`URI`], which points to a [`json`] file. /// /// [`json`]: https://tools.ietf.org/html/rfc8259 - /// [`uri`]: https://tools.ietf.org/html/rfc3986 + /// [`URI`]: https://tools.ietf.org/html/rfc3986 Uri(String), } -/// # [4.3.4.4. EXT-X-SESSION-DATA] -/// -/// The [`ExtXSessionData`] tag allows arbitrary session data to be -/// carried in a [`MasterPlaylist`]. +/// Allows arbitrary session data to be carried in a [`MasterPlaylist`]. /// /// [`MasterPlaylist`]: crate::MasterPlaylist /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 @@ -39,8 +36,27 @@ pub enum SessionData { #[builder(setter(into))] #[shorthand(enable(must_use, into))] pub struct ExtXSessionData { - /// Sets the `data_id` attribute, that should conform to a [reverse DNS] - /// naming convention, such as `com.example.movie.title`. + /// This should conform to a [reverse DNS] naming convention, such as + /// `com.example.movie.title`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXSessionData; + /// use hls_m3u8::tags::SessionData; + /// + /// let mut session_data = ExtXSessionData::new( + /// "com.example.movie.title", + /// SessionData::Uri("https://www.example.com/".to_string()), + /// ); + /// + /// session_data.set_data_id("com.ironrust.movie.title"); + /// + /// assert_eq!( + /// session_data.data_id(), + /// &"com.ironrust.movie.title".to_string() + /// ); + /// ``` /// /// # Note /// @@ -51,15 +67,59 @@ pub struct ExtXSessionData { /// /// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation data_id: String, - /// The data associated with the [`data_id`](ExtXSessionData::data_id). - /// For more information look [`here`](SessionData). + /// The [`SessionData`] associated with the + /// [`data_id`](ExtXSessionData::data_id). + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXSessionData; + /// use hls_m3u8::tags::SessionData; + /// + /// let mut session_data = ExtXSessionData::new( + /// "com.example.movie.title", + /// SessionData::Uri("https://www.example.com/".to_string()), + /// ); + /// + /// session_data.set_data(SessionData::Uri( + /// "https://www.example.com/data.json".to_string(), + /// )); + /// + /// assert_eq!( + /// session_data.data(), + /// &SessionData::Uri("https://www.example.com/data.json".to_string()) + /// ); + /// ``` /// /// # Note /// /// This field is required. + #[shorthand(disable(into))] data: SessionData, /// The `language` attribute identifies the language of the [`SessionData`]. - /// See [rfc5646](https://tools.ietf.org/html/rfc5646). + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXSessionData; + /// use hls_m3u8::tags::SessionData; + /// + /// let mut session_data = ExtXSessionData::new( + /// "com.example.movie.title", + /// SessionData::Uri("https://www.example.com/".to_string()), + /// ); + /// + /// session_data.set_language(Some("en")); + /// + /// assert_eq!(session_data.language(), Some(&"en".to_string())); + /// ``` + /// + /// # Note + /// + /// This field is optional and the provided value should conform to + /// [RFC5646]. + /// + /// [RFC5646]: https://tools.ietf.org/html/rfc5646 #[builder(setter(into, strip_option), default)] language: Option, } @@ -72,7 +132,8 @@ impl ExtXSessionData { /// # Example /// /// ``` - /// use hls_m3u8::tags::{ExtXSessionData, SessionData}; + /// # use hls_m3u8::tags::ExtXSessionData; + /// use hls_m3u8::tags::SessionData; /// /// ExtXSessionData::new( /// "com.example.movie.title", @@ -87,28 +148,20 @@ impl ExtXSessionData { } } - /// Returns a new Builder for [`ExtXSessionData`]. + /// Returns a builder for [`ExtXSessionData`]. /// /// # Example /// /// ``` - /// use hls_m3u8::tags::{ExtXSessionData, SessionData}; + /// # use hls_m3u8::tags::ExtXSessionData; + /// use hls_m3u8::tags::SessionData; /// /// let session_data = ExtXSessionData::builder() /// .data_id("com.example.movie.title") /// .data(SessionData::Value("some data".to_string())) - /// .language("english") - /// .build() - /// .expect("Failed to build an ExtXSessionData tag."); - /// - /// assert_eq!( - /// session_data, - /// ExtXSessionData::with_language( - /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), - /// "english" - /// ) - /// ); + /// .language("en") + /// .build()?; + /// # Ok::<(), Box>(()) /// ``` pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() } @@ -117,12 +170,13 @@ impl ExtXSessionData { /// # Example /// /// ``` - /// use hls_m3u8::tags::{ExtXSessionData, SessionData}; + /// # use hls_m3u8::tags::ExtXSessionData; + /// use hls_m3u8::tags::SessionData; /// /// let session_data = ExtXSessionData::with_language( /// "com.example.movie.title", /// SessionData::Value("some data".to_string()), - /// "english", + /// "en", /// ); /// ``` pub fn with_language(data_id: T, data: SessionData, language: K) -> Self @@ -214,159 +268,77 @@ mod test { use super::*; use pretty_assertions::assert_eq; - #[test] - fn test_display() { - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"com.example.lyrics\",\ - URI=\"lyrics.json\"" - .to_string(), - ExtXSessionData::new( - "com.example.lyrics", - SessionData::Uri("lyrics.json".to_string()) - ) - .to_string() - ); + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"com.example.title\",\ - VALUE=\"This is an example\",\ - LANGUAGE=\"en\"" - .to_string(), - ExtXSessionData::with_language( - "com.example.title", - SessionData::Value("This is an example".to_string()), - "en" - ) - .to_string() - ); + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"com.example.title\",\ - VALUE=\"Este es un ejemplo\",\ - LANGUAGE=\"es\"" - .to_string(), - ExtXSessionData::with_language( - "com.example.title", - SessionData::Value("Este es un ejemplo".to_string()), - "es" - ) - .to_string() - ); + assert!("#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + LANGUAGE=\"baz\"" + .parse::() + .is_err()); - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - VALUE=\"bar\"" - .to_string(), - ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string() - ); + assert!("#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + LANGUAGE=\"baz\",\ + VALUE=\"VALUE\",\ + URI=\"https://www.example.com/\"" + .parse::() + .is_err()); + } - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - URI=\"bar\"" - .to_string(), - ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string() - ); - - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - VALUE=\"bar\",\ - LANGUAGE=\"baz\"" - .to_string(), - ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") - .to_string() - ); + } } - #[test] - fn test_parser() { - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"com.example.lyrics\",\ - URI=\"lyrics.json\"" - .parse::() - .unwrap(), + generate_tests! { + { ExtXSessionData::new( "com.example.lyrics", - SessionData::Uri("lyrics.json".to_string()) + SessionData::Uri("lyrics.json".into()) + ), + concat!( + "#EXT-X-SESSION-DATA:", + "DATA-ID=\"com.example.lyrics\",", + "URI=\"lyrics.json\"" ) - ); - - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"com.example.title\",\ - LANGUAGE=\"en\",\ - VALUE=\"This is an example\"" - .parse::() - .unwrap(), + }, + { ExtXSessionData::with_language( "com.example.title", - SessionData::Value("This is an example".to_string()), + SessionData::Value("This is an example".into()), "en" + ), + concat!( + "#EXT-X-SESSION-DATA:", + "DATA-ID=\"com.example.title\",", + "VALUE=\"This is an example\",", + "LANGUAGE=\"en\"" ) - ); - - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"com.example.title\",\ - LANGUAGE=\"es\",\ - VALUE=\"Este es un ejemplo\"" - .parse::() - .unwrap(), + }, + { ExtXSessionData::with_language( "com.example.title", - SessionData::Value("Este es un ejemplo".to_string()), + SessionData::Value("Este es un ejemplo".into()), "es" + ), + concat!( + "#EXT-X-SESSION-DATA:", + "DATA-ID=\"com.example.title\",", + "VALUE=\"Este es un ejemplo\",", + "LANGUAGE=\"es\"" ) - ); - - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - VALUE=\"bar\"" - .parse::() - .unwrap(), - ExtXSessionData::new("foo", SessionData::Value("bar".into())) - ); - - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - URI=\"bar\"" - .parse::() - .unwrap(), - ExtXSessionData::new("foo", SessionData::Uri("bar".into())) - ); - - assert_eq!( - "#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - VALUE=\"bar\",\ - LANGUAGE=\"baz\",\ - UNKNOWN=TAG" - .parse::() - .unwrap(), - ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") - ); - - assert!("#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - LANGUAGE=\"baz\"" - .parse::() - .is_err()); - - assert!("#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - LANGUAGE=\"baz\",\ - VALUE=\"VALUE\",\ - URI=\"https://www.example.com/\"" - .parse::() - .is_err()); + } } #[test] From 651db2e18b22aa58db7dbb48919a0dcbb502b69c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 23 Feb 2020 18:53:47 +0100 Subject: [PATCH 038/112] fix ci --- .github/workflows/rust.yml | 65 ++++++++++++++++++++++---------------- .travis.yml | 2 +- deny.toml | 2 +- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b7ec752..3ea991a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,33 +1,44 @@ -name: Rust +name: rust # Trigger the workflow on push or pull request on: [push, pull_request] jobs: - rustfmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install latest nightly - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v2 - with: - command: fmt - args: --all -- --check + ci: + runs-on: ubuntu-latest - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install latest nightly - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - run: rustup component add clippy - - uses: actions-rs/cargo@v2 - with: - command: clippy - # args: -- -D warnings + strategy: + matrix: + rust: + - stable + - beta + - nightly + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - uses: actions-rs/cargo@v1 + with: + command: build + args: --all-features + + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features + + - uses: actions-rs/cargo@v1 + with: + command: doc + args: --all-features + + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/.travis.yml b/.travis.yml index 982e71a..e276f81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,6 @@ script: after_success: | # this does require a -Z flag for Doctests, which is unstable! if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - cargo tarpaulin --ignore-panics --ignore-tests --run-types Tests Doctests --out Xml + cargo tarpaulin -f --ignore-panics --ignore-tests --run-types Tests Doctests --out Xml bash <(curl -s https://codecov.io/bash) fi diff --git a/deny.toml b/deny.toml index 880fd2a..407794b 100644 --- a/deny.toml +++ b/deny.toml @@ -40,7 +40,7 @@ vulnerability = "deny" # The lint level for unmaintained crates unmaintained = "warn" # The lint level for crates that have been yanked from their source registry -yanked = "warn" +# yanked = "warn" # The lint level for crates with security notices. Note that as of # 2019-12-17 there are no security notice advisories in # https://github.com/rustsec/advisory-db From 5972216323bbdedfd8c05285d9f84039afd168e9 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 23 Feb 2020 18:56:41 +0100 Subject: [PATCH 039/112] improve documentation and tests of ByteRange --- src/media_playlist.rs | 4 +- src/media_segment.rs | 2 +- src/tags/media_segment/byte_range.rs | 198 ++++++-- src/tags/media_segment/map.rs | 26 +- src/types/byte_range.rs | 666 +++++++++++++++++++++++---- tests/media_playlist.rs | 6 +- 6 files changed, 764 insertions(+), 138 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 0209226..f7855ed 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -153,8 +153,8 @@ impl MediaPlaylistBuilder { } // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(tag) = s.byte_range() { - if tag.to_range().start().is_none() { + if let Some(range) = s.byte_range() { + if range.start().is_none() { let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?; if last_uri != s.uri() { return Err(Error::invalid_input()); diff --git a/src/media_segment.rs b/src/media_segment.rs index f0b4145..804588a 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -120,7 +120,7 @@ mod tests { MediaSegment::builder() //.keys(vec![ExtXKey::empty()]) .map(ExtXMap::new("https://www.example.com/")) - .byte_range(ExtXByteRange::new(20, Some(5))) + .byte_range(ExtXByteRange::from(5..25)) //.date_range() // TODO! .discontinuity(ExtXDiscontinuity) .inf(ExtInf::new(Duration::from_secs(4))) diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index ce1b733..eecf458 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,39 +1,120 @@ use std::fmt; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; +use core::ops::{Add, AddAssign, Sub, SubAssign}; + +use derive_more::{AsMut, AsRef, Deref, DerefMut, From}; use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.2.2. EXT-X-BYTERANGE] +/// Indicates that a [`MediaSegment`] is a sub-range of the resource identified +/// by its `URI`. /// -/// The [`ExtXByteRange`] tag indicates that a [`Media Segment`] is a sub-range -/// of the resource identified by its `URI`. +/// # Example /// -/// [`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(Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// Constructing an [`ExtXByteRange`]: +/// +/// ``` +/// # use hls_m3u8::tags::ExtXByteRange; +/// assert_eq!(ExtXByteRange::from(22..55), ExtXByteRange::from(22..=54)); +/// ``` +/// +/// It is also possible to omit the start, in which case it assumes that the +/// [`ExtXByteRange`] starts at the byte after the end of the previous +/// [`ExtXByteRange`] or 0 if there is no previous one. +/// +/// ``` +/// # use hls_m3u8::tags::ExtXByteRange; +/// assert_eq!(ExtXByteRange::from(..55), ExtXByteRange::from(..=54)); +/// ``` +/// +/// [`MediaSegment`]: crate::MediaSegment +#[derive( + AsRef, AsMut, From, Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +#[from(forward)] pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; - /// Makes a new [`ExtXByteRange`] tag. + /// Adds `num` to the `start` and `end` of the range. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXByteRange; - /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// let range = ExtXByteRange::from(10..22); + /// let nrange = range.saturating_add(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c + 5)); /// ``` - pub const fn new(length: usize, start: Option) -> Self { - Self(ByteRange::new(length, start)) - } + /// + /// # Overflow + /// + /// If the range is saturated it will not overflow and instead + /// stay at it's current value. + /// + /// ``` + /// # use hls_m3u8::tags::ExtXByteRange; + /// let range = ExtXByteRange::from(5..usize::max_value()); + /// + /// // this would cause the end to overflow + /// let nrange = range.saturating_add(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[inline] + #[must_use] + pub fn saturating_add(self, num: usize) -> Self { Self(self.0.saturating_add(num)) } - /// Converts the [`ExtXByteRange`] to a [`ByteRange`]. + /// Subtracts `num` from the `start` and `end` of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXByteRange; + /// let range = ExtXByteRange::from(10..22); + /// let nrange = range.saturating_sub(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c - 5)); + /// ``` + /// + /// # Underflow + /// + /// If the range is saturated it will not underflow and instead stay + /// at it's current value. + /// + /// ``` + /// # use hls_m3u8::tags::ExtXByteRange; + /// let range = ExtXByteRange::from(0..10); + /// + /// // this would cause the start to underflow + /// let nrange = range.saturating_sub(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[inline] + #[must_use] + pub fn saturating_sub(self, num: usize) -> Self { Self(self.0.saturating_sub(num)) } + + /// Returns a shared reference to the underlying [`ByteRange`]. /// /// # Example /// @@ -41,10 +122,14 @@ impl ExtXByteRange { /// # use hls_m3u8::tags::ExtXByteRange; /// use hls_m3u8::types::ByteRange; /// - /// let byte_range = ExtXByteRange::new(20, Some(5)); - /// let range: ByteRange = byte_range.to_range(); + /// assert_eq!( + /// ExtXByteRange::from(2..11).as_byte_range(), + /// &ByteRange::from(2..11) + /// ); /// ``` - pub const fn to_range(&self) -> ByteRange { self.0 } + #[inline] + #[must_use] + pub const fn as_byte_range(&self) -> &ByteRange { &self.0 } } /// This tag requires [`ProtocolVersion::V4`]. @@ -52,6 +137,48 @@ impl RequiredVersion for ExtXByteRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } +impl Into for ExtXByteRange { + fn into(self) -> ByteRange { self.0 } +} + +impl Sub for ExtXByteRange +where + ByteRange: Sub, +{ + type Output = Self; + + #[must_use] + #[inline] + fn sub(self, rhs: T) -> Self::Output { Self(self.0.sub(rhs)) } +} + +impl SubAssign for ExtXByteRange +where + ByteRange: SubAssign, +{ + #[inline] + fn sub_assign(&mut self, other: T) { self.0.sub_assign(other); } +} + +impl Add for ExtXByteRange +where + ByteRange: Add, +{ + type Output = Self; + + #[must_use] + #[inline] + fn add(self, rhs: T) -> Self::Output { Self(self.0.add(rhs)) } +} + +impl AddAssign for ExtXByteRange +where + ByteRange: AddAssign, +{ + #[inline] + fn add_assign(&mut self, other: T) { self.0.add_assign(other); } +} + impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; @@ -77,57 +204,52 @@ mod test { #[test] fn test_display() { - let byte_range = ExtXByteRange::new(0, Some(5)); - assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string()); - - let byte_range = ExtXByteRange::new(99999, Some(2)); assert_eq!( - byte_range.to_string(), - "#EXT-X-BYTERANGE:99999@2".to_string() + ExtXByteRange::from(2..15).to_string(), + "#EXT-X-BYTERANGE:13@2".to_string() ); - let byte_range = ExtXByteRange::new(99999, None); - assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string()); + assert_eq!( + ExtXByteRange::from(..22).to_string(), + "#EXT-X-BYTERANGE:22".to_string() + ); } #[test] fn test_parser() { - let byte_range = ExtXByteRange::new(99999, Some(2)); assert_eq!( - byte_range, - "#EXT-X-BYTERANGE:99999@2".parse::().unwrap() + ExtXByteRange::from(2..15), + "#EXT-X-BYTERANGE:13@2".parse().unwrap() ); - let byte_range = ExtXByteRange::new(99999, None); assert_eq!( - byte_range, - "#EXT-X-BYTERANGE:99999".parse::().unwrap() + ExtXByteRange::from(..22), + "#EXT-X-BYTERANGE:22".parse().unwrap() ); } #[test] fn test_deref() { - let byte_range = ExtXByteRange::new(0, Some(22)); + let byte_range = ExtXByteRange::from(0..22); - assert_eq!(byte_range.length(), 0); - assert_eq!(byte_range.start(), Some(22)); + assert_eq!(byte_range.len(), 22); + assert_eq!(byte_range.start(), Some(0)); } #[test] fn test_deref_mut() { - let mut byte_range = ExtXByteRange::new(0, Some(22)); + let mut byte_range = ExtXByteRange::from(10..110); - byte_range.set_length(100); byte_range.set_start(Some(50)); - assert_eq!(byte_range.length(), 100); + assert_eq!(byte_range.len(), 60); assert_eq!(byte_range.start(), Some(50)); } #[test] fn test_required_version() { assert_eq!( - ExtXByteRange::new(20, Some(5)).required_version(), + ExtXByteRange::from(5..20).required_version(), ProtocolVersion::V4 ); } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 1ab3a93..fa89c13 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -44,13 +44,10 @@ pub struct ExtXMap { /// # 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)), - /// ); + /// let mut map = ExtXMap::with_range("https://prod.mediaspace.com/init.bin", ..9); /// - /// map.set_range(Some(ByteRange::new(1, None))); - /// assert_eq!(map.range(), Some(ByteRange::new(1, None))); + /// map.set_range(Some(2..5)); + /// assert_eq!(map.range(), Some(ByteRange::from(2..5))); /// ``` #[shorthand(enable(copy))] range: Option, @@ -85,15 +82,12 @@ impl ExtXMap { /// # 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)), - /// ); + /// ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11); /// ``` - pub fn with_range>(uri: T, range: ByteRange) -> Self { + pub fn with_range, B: Into>(uri: I, range: B) -> Self { Self { uri: uri.into(), - range: Some(range), + range: Some(range.into()), keys: vec![], } } @@ -173,7 +167,7 @@ mod test { ); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).to_string(), + ExtXMap::with_range("foo", ByteRange::from(2..11)).to_string(), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(), ); } @@ -186,11 +180,11 @@ mod test { ); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + ExtXMap::with_range("foo", ByteRange::from(2..11)), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap() ); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + ExtXMap::with_range("foo", ByteRange::from(2..11)), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED" .parse() .unwrap() @@ -201,7 +195,7 @@ mod test { fn test_required_version() { assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).required_version(), + ExtXMap::with_range("foo", ByteRange::from(2..11)).required_version(), ProtocolVersion::V6 ); } diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index d5018e2..b0004b1 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,67 +1,394 @@ -use std::fmt; -use std::str::FromStr; +use core::convert::TryInto; +use core::fmt; +use core::ops::{ + Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub, + SubAssign, +}; +use core::str::FromStr; use shorthand::ShortHand; use crate::Error; -/// Byte range. +/// A range of bytes, which can be seen as either `..end` or `start..end`. /// -/// See: [4.3.2.2. EXT-X-BYTERANGE] +/// It can be constructed from `..end` and `start..end`: /// -/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +/// ``` +/// use hls_m3u8::types::ByteRange; +/// +/// let range = ByteRange::from(10..20); +/// let range = ByteRange::from(..20); +/// ``` #[derive(ShortHand, Copy, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] -#[shorthand(enable(must_use))] +#[shorthand(enable(must_use, copy), disable(option_as_ref, set))] pub struct ByteRange { - /// The length of the range. + /// Returns the `start` of the [`ByteRange`], if there is one. /// /// # Example /// /// ``` /// # use hls_m3u8::types::ByteRange; - /// # - /// let mut range = ByteRange::new(20, Some(3)); - /// # assert_eq!(range.length(), 20); - /// - /// range.set_length(10); - /// assert_eq!(range.length(), 10); + /// assert_eq!(ByteRange::from(0..5).start(), Some(0)); + /// assert_eq!(ByteRange::from(..5).start(), None); /// ``` - length: usize, - /// The start of the range. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::ByteRange; - /// # - /// let mut range = ByteRange::new(20, None); - /// # assert_eq!(range.start(), None); - /// - /// range.set_start(Some(3)); - /// assert_eq!(range.start(), Some(3)); - /// ``` - // - // this is a workaround until this issue is fixed: - // https://github.com/Luro02/shorthand/issues/20 - #[shorthand(enable(copy), disable(option_as_ref))] start: Option, + /// Returns the `end` of the [`ByteRange`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// assert_eq!(ByteRange::from(0..5).start(), Some(0)); + /// assert_eq!(ByteRange::from(..5).start(), None); + /// ``` + end: usize, } impl ByteRange { - /// Creates a new [`ByteRange`]. + /// Changes the length of the [`ByteRange`]. /// /// # Example /// /// ``` /// # use hls_m3u8::types::ByteRange; - /// ByteRange::new(22, Some(12)); + /// let mut range = ByteRange::from(0..5); + /// range.set_len(2); + /// + /// assert_eq!(range, ByteRange::from(0..2)); + /// + /// range.set_len(200); + /// assert_eq!(range, ByteRange::from(0..200)); /// ``` - pub const fn new(length: usize, start: Option) -> Self { Self { length, start } } + /// + /// # Note + /// + /// The `start` will not be changed. + pub fn set_len(&mut self, new_len: usize) { + // the new_len can be either greater or smaller than `self.len()`. + // if new_len is larger `checked_sub` will return `None` + if let Some(value) = self.len().checked_sub(new_len) { + self.end -= value; + } else { + self.end += new_len.saturating_sub(self.len()); + } + } + + /// Sets the `start` of the [`ByteRange`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// assert_eq!(ByteRange::from(0..5).set_start(Some(5)).start(), Some(5)); + /// assert_eq!(ByteRange::from(..5).set_start(Some(2)).start(), Some(2)); + /// ``` + /// + /// # Panics + /// + /// This function will panic, if the `new_start` is larger, than the + /// [`end`](ByteRange::end). + pub fn set_start(&mut self, new_start: Option) -> &mut Self { + if new_start.map_or(false, |s| s > self.end) { + panic!("attempt to make the start larger than the end"); + } + + self.start = new_start; + + self + } + + /// Adds `num` to the `start` and `end` of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(10..22); + /// let nrange = range.saturating_add(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c + 5)); + /// ``` + /// + /// # Overflow + /// + /// If the range is saturated it will not overflow and instead stay + /// at it's current value. + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(5..usize::max_value()); + /// + /// // this would cause the end to overflow + /// let nrange = range.saturating_add(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[must_use] + pub fn saturating_add(mut self, num: usize) -> Self { + if let Some(start) = self.start { + // add the number to the start + if let (Some(start), Some(end)) = (start.checked_add(num), self.end.checked_add(num)) { + self.start = Some(start); + self.end = end; + } else { + // it is ensured at construction that the start will never be larger than the + // end. This clause can therefore be only reached if the end overflowed. + // -> It is only possible to add `usize::max_value() - end` to the start. + if let Some(start) = start.checked_add(usize::max_value() - self.end) { + self.start = Some(start); + self.end = usize::max_value(); + } else { + // both end + start overflowed -> do not change anything + } + } + } else { + self.end = self.end.saturating_add(num); + } + + self + } + + /// Subtracts `num` from the `start` and `end` of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(10..22); + /// let nrange = range.saturating_sub(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c - 5)); + /// ``` + /// + /// # Underflow + /// + /// If the range is saturated it will not underflow and instead stay + /// at it's current value. + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(0..10); + /// + /// // this would cause the start to underflow + /// let nrange = range.saturating_sub(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[must_use] + pub fn saturating_sub(mut self, num: usize) -> Self { + if let Some(start) = self.start { + // subtract the number from the start + if let (Some(start), Some(end)) = (start.checked_sub(num), self.end.checked_sub(num)) { + self.start = Some(start); + self.end = end; + } else { + // it is ensured at construction that the start will never be larger, than the + // end so this clause will only be reached, if the start underflowed. + // -> can at most subtract `start` from `end` + if let Some(end) = self.end.checked_sub(start) { + self.start = Some(0); + self.end = end; + } else { + // both end + start underflowed + // -> do not change anything + } + } + } else { + self.end = self.end.saturating_sub(num); + } + + self + } + + /// Returns the length, which is calculated by subtracting the `end` from + /// the `start`. If the `start` is `None` a 0 is assumed. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(1..16); + /// + /// assert_eq!(range.len(), 15); + /// ``` + #[inline] + #[must_use] + pub fn len(&self) -> usize { self.end.saturating_sub(self.start.unwrap_or(0)) } + + /// Returns `true` if the length is zero. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(12..12); + /// + /// assert_eq!(range.is_empty(), true); + /// ``` + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { self.len() == 0 } +} + +impl Sub for ByteRange { + type Output = Self; + + #[must_use] + #[inline] + fn sub(self, rhs: usize) -> Self::Output { + Self { + start: self.start.map(|lhs| lhs - rhs), + end: self.end - rhs, + } + } +} + +impl SubAssign for ByteRange { + #[inline] + fn sub_assign(&mut self, other: usize) { *self = >::sub(*self, other); } +} + +impl Add for ByteRange { + type Output = Self; + + #[must_use] + #[inline] + fn add(self, rhs: usize) -> Self::Output { + Self { + start: self.start.map(|lhs| lhs + rhs), + end: self.end + rhs, + } + } +} + +impl AddAssign for ByteRange { + #[inline] + fn add_assign(&mut self, other: usize) { *self = >::add(*self, other); } +} + +macro_rules! impl_from_ranges { + ( $( $type:tt ),* ) => { + $( + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: Range<$type>) -> Self { + if range.start > range.end { + panic!("the range start must be smaller than the end"); + } + + Self { + start: Some(range.start as usize), + end: range.end as usize, + } + } + } + + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: RangeInclusive<$type>) -> Self { + let (start, end) = range.into_inner(); + + if start > end { + panic!("the range start must be smaller than the end"); + } + + Self { + start: Some(start as usize), + end: (end as usize).saturating_add(1), + } + } + } + + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: RangeTo<$type>) -> Self { + Self { + start: None, + end: range.end as usize, + } + } + } + + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: RangeToInclusive<$type>) -> Self { + Self { + start: None, + end: (range.end as usize).saturating_add(1), + } + } + } + )* + } +} + +// TODO: replace with generics as soon as overlapping trait implementations are +// stable (`Into for usize` is reserved for upstream crates ._.) +impl_from_ranges![u64, u32, u16, u8, usize, i32]; + +#[must_use] +impl RangeBounds for ByteRange { + fn start_bound(&self) -> Bound<&usize> { + if let Some(start) = &self.start { + Bound::Included(start) + } else { + Bound::Unbounded + } + } + + #[inline] + fn end_bound(&self) -> Bound<&usize> { Bound::Excluded(&self.end) } +} + +/// This conversion will fail if the start of the [`ByteRange`] is `Some`. +impl TryInto> for ByteRange { + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + if self.start.is_some() { + return Err(Error::custom("A `RangeTo` (`..end`) does not have a start")); + } + + Ok(RangeTo { end: self.end }) + } +} + +/// This conversion will fail if the start of the [`ByteRange`] is `None`. +impl TryInto> for ByteRange { + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + if self.start.is_none() { + return Err(Error::custom( + "A `Range` (`start..end`) has to have a start.", + )); + } + + Ok(Range { + start: self.start.unwrap(), + end: self.end, + }) + } } impl fmt::Display for ByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.length)?; + write!(f, "{}", self.len())?; if let Some(value) = self.start { write!(f, "@{}", value)?; @@ -77,17 +404,20 @@ impl FromStr for ByteRange { fn from_str(input: &str) -> Result { let mut input = input.splitn(2, '@'); - let length = input - .next() - .ok_or_else(|| Error::custom("missing length"))?; - let length = length.parse().map_err(|e| Error::parse_int(length, e))?; + let length = input.next().unwrap(); + let length = length + .parse::() + .map_err(|e| Error::parse_int(length, e))?; let start = input .next() - .map(|v| v.parse().map_err(|e| Error::parse_int(v, e))) + .map(|v| v.parse::().map_err(|e| Error::parse_int(v, e))) .transpose()?; - Ok(Self::new(length, start)) + Ok(Self { + start, + end: start.unwrap_or(0) + length, + }) } } @@ -97,59 +427,239 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn test_display() { + #[should_panic = "the range start must be smaller than the end"] + fn test_from_range_panic() { let _ = ByteRange::from(6..0); } + + #[test] + #[should_panic = "the range start must be smaller than the end"] + fn test_from_range_inclusive_panic() { let _ = ByteRange::from(6..=0); } + + #[test] + fn test_from_ranges() { + assert_eq!(ByteRange::from(1..10), ByteRange::from(1..=9)); + assert_eq!(ByteRange::from(..10), ByteRange::from(..=9)); + } + + #[test] + fn test_range_bounds() { + assert_eq!(ByteRange::from(0..10).start_bound(), Bound::Included(&0)); + assert_eq!(ByteRange::from(..10).start_bound(), Bound::Unbounded); + + assert_eq!(ByteRange::from(0..10).end_bound(), Bound::Excluded(&10)); + assert_eq!(ByteRange::from(..10).end_bound(), Bound::Excluded(&10)); + } + + #[test] + fn test_try_into() { + assert_eq!(ByteRange::from(1..4).try_into(), Ok(1..4)); + assert_eq!(ByteRange::from(..4).try_into(), Ok(..4)); + + assert!(TryInto::>::try_into(ByteRange::from(1..4)).is_err()); + assert!(TryInto::>::try_into(ByteRange::from(..4)).is_err()); + } + + #[test] + fn test_add_assign() { + let mut range = ByteRange::from(5..10); + range += 5; + + assert_eq!(range, ByteRange::from(10..15)); + } + + #[test] + #[should_panic = "attempt to add with overflow"] + fn test_add_assign_panic() { + let mut range = ByteRange::from(4..usize::max_value()); + range += 5; + + unreachable!(); + } + + #[test] + fn test_sub_assign() { + let mut range = ByteRange::from(10..20); + range -= 5; + + assert_eq!(range, ByteRange::from(5..15)); + } + + #[test] + #[should_panic = "attempt to subtract with overflow"] + fn test_sub_assign_panic() { + let mut range = ByteRange::from(4..10); + range -= 5; + + unreachable!(); + } + + #[test] + #[should_panic = "attempt to make the start larger than the end"] + fn test_set_start() { let _ = ByteRange::from(4..10).set_start(Some(11)); } + + #[test] + fn test_add() { + // normal addition + assert_eq!(ByteRange::from(5..10) + 5, ByteRange::from(10..15)); + assert_eq!(ByteRange::from(..10) + 5, ByteRange::from(..15)); + + // adding 0 + assert_eq!(ByteRange::from(5..10) + 0, ByteRange::from(5..10)); + assert_eq!(ByteRange::from(..10) + 0, ByteRange::from(..10)); + } + + #[test] + #[should_panic = "attempt to add with overflow"] + fn test_add_panic() { let _ = ByteRange::from(usize::max_value()..usize::max_value()) + 1; } + + #[test] + fn test_sub() { + // normal subtraction + assert_eq!(ByteRange::from(5..10) - 4, ByteRange::from(1..6)); + assert_eq!(ByteRange::from(..10) - 4, ByteRange::from(..6)); + + // subtracting 0 + assert_eq!(ByteRange::from(0..0) - 0, ByteRange::from(0..0)); + assert_eq!(ByteRange::from(2..3) - 0, ByteRange::from(2..3)); + + assert_eq!(ByteRange::from(..0) - 0, ByteRange::from(..0)); + assert_eq!(ByteRange::from(..3) - 0, ByteRange::from(..3)); + } + + #[test] + #[should_panic = "attempt to subtract with overflow"] + fn test_sub_panic() { let _ = ByteRange::from(0..0) - 1; } + + #[test] + fn test_saturating_add() { + // normal addition assert_eq!( - ByteRange { - length: 0, - start: Some(5), - } - .to_string(), - "0@5".to_string() + ByteRange::from(5..10).saturating_add(5), + ByteRange::from(10..15) + ); + assert_eq!( + ByteRange::from(..10).saturating_add(5), + ByteRange::from(..15) + ); + + // adding 0 + assert_eq!( + ByteRange::from(6..11).saturating_add(0), + ByteRange::from(6..11) + ); + assert_eq!( + ByteRange::from(..11).saturating_add(0), + ByteRange::from(..11) ); assert_eq!( - ByteRange { - length: 99999, - start: Some(2), - } - .to_string(), - "99999@2".to_string() + ByteRange::from(0..0).saturating_add(0), + ByteRange::from(0..0) + ); + assert_eq!(ByteRange::from(..0).saturating_add(0), ByteRange::from(..0)); + + // overflow + assert_eq!( + ByteRange::from(usize::max_value()..usize::max_value()).saturating_add(1), + ByteRange::from(usize::max_value()..usize::max_value()) + ); + assert_eq!( + ByteRange::from(..usize::max_value()).saturating_add(1), + ByteRange::from(..usize::max_value()) ); assert_eq!( - ByteRange { - length: 99999, - start: None, - } - .to_string(), - "99999".to_string() + ByteRange::from(usize::max_value() - 5..usize::max_value()).saturating_add(1), + ByteRange::from(usize::max_value() - 5..usize::max_value()) + ); + + // overflow, but something can be added to the range: + assert_eq!( + ByteRange::from(usize::max_value() - 5..usize::max_value() - 3).saturating_add(4), + ByteRange::from(usize::max_value() - 2..usize::max_value()) + ); + + assert_eq!( + ByteRange::from(..usize::max_value() - 3).saturating_add(4), + ByteRange::from(..usize::max_value()) ); } + #[test] + fn test_saturating_sub() { + // normal subtraction + assert_eq!( + ByteRange::from(5..10).saturating_sub(4), + ByteRange::from(1..6) + ); + + // subtracting 0 + assert_eq!( + ByteRange::from(0..0).saturating_sub(0), + ByteRange::from(0..0) + ); + assert_eq!( + ByteRange::from(2..3).saturating_sub(0), + ByteRange::from(2..3) + ); + + // the start underflows + assert_eq!( + ByteRange::from(0..5).saturating_sub(4), + ByteRange::from(0..5) + ); + + // the start underflows, but one can still subtract something from it + assert_eq!( + ByteRange::from(1..5).saturating_sub(2), + ByteRange::from(0..4) + ); + + // both start and end underflow + assert_eq!( + ByteRange::from(1..3).saturating_sub(5), + ByteRange::from(0..2) + ); + + // both start + end are 0 + underflow + assert_eq!( + ByteRange::from(0..0).saturating_sub(1), + ByteRange::from(0..0) + ); + + // half open ranges: + assert_eq!(ByteRange::from(..6).saturating_sub(2), ByteRange::from(..4)); + assert_eq!(ByteRange::from(..5).saturating_sub(0), ByteRange::from(..5)); + assert_eq!(ByteRange::from(..0).saturating_sub(0), ByteRange::from(..0)); + + assert_eq!(ByteRange::from(..0).saturating_sub(1), ByteRange::from(..0)); + } + + #[test] + fn test_display() { + assert_eq!(ByteRange::from(0..5).to_string(), "5@0".to_string()); + + assert_eq!( + ByteRange::from(2..100001).to_string(), + "99999@2".to_string() + ); + + assert_eq!(ByteRange::from(..99999).to_string(), "99999".to_string()); + } + #[test] fn test_parser() { + assert_eq!(ByteRange::from(2..22), "20@2".parse().unwrap()); + + assert_eq!(ByteRange::from(..300), "300".parse().unwrap()); + assert_eq!( - ByteRange { - length: 99999, - start: Some(2), - }, - "99999@2".parse::().unwrap() + ByteRange::from_str("a"), + Err(Error::parse_int("a", "a".parse::().unwrap_err())) ); assert_eq!( - ByteRange { - length: 99999, - start: Some(2), - }, - "99999@2".parse::().unwrap() - ); - - assert_eq!( - ByteRange { - length: 99999, - start: None, - }, - "99999".parse::().unwrap() + ByteRange::from_str("1@a"), + Err(Error::parse_int("a", "a".parse::().unwrap_err())) ); assert!("".parse::().is_err()); diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index e597bdc..c43c131 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -13,19 +13,19 @@ fn test_media_playlist_with_byterange() { .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) - .byte_range(ExtXByteRange::new(75232, Some(0))) + .byte_range(ExtXByteRange::from(0..75232)) .uri("video.ts") .build() .unwrap(), MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) - .byte_range(ExtXByteRange::new(82112, Some(752321))) + .byte_range(ExtXByteRange::from(752321..82112 + 752321)) .uri("video.ts") .build() .unwrap(), MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) - .byte_range(ExtXByteRange::new(69864, None)) + .byte_range(ExtXByteRange::from(..69864)) .uri("video.ts") .build() .unwrap(), From e1c10d27f7d093aaa176add2fc7d1d3bfe23b161 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 23 Feb 2020 18:57:13 +0100 Subject: [PATCH 040/112] improve code coverage of `(U)Float` --- src/types/float.rs | 25 +++++++++++++++++++++ src/types/ufloat.rs | 54 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/types/float.rs b/src/types/float.rs index e14b9c5..d03621d 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -177,6 +177,7 @@ impl ::core::hash::Hash for Float { #[cfg(test)] mod tests { use super::*; + use core::hash::{Hash, Hasher}; use pretty_assertions::assert_eq; #[test] @@ -202,6 +203,29 @@ mod tests { ); } + #[test] + fn test_hash() { + let mut hasher_left = std::collections::hash_map::DefaultHasher::new(); + let mut hasher_right = std::collections::hash_map::DefaultHasher::new(); + + assert_eq!( + Float::new(0.0).hash(&mut hasher_left), + Float::new(-0.0).hash(&mut hasher_right) + ); + + assert_eq!(hasher_left.finish(), hasher_right.finish()); + + let mut hasher_left = std::collections::hash_map::DefaultHasher::new(); + let mut hasher_right = std::collections::hash_map::DefaultHasher::new(); + + assert_eq!( + Float::new(1.0).hash(&mut hasher_left), + Float::new(1.0).hash(&mut hasher_right) + ); + + assert_eq!(hasher_left.finish(), hasher_right.finish()); + } + #[test] fn test_eq() { struct _AssertEq @@ -213,6 +237,7 @@ mod tests { fn test_partial_eq() { assert_eq!(Float::new(1.0).eq(&Float::new(1.0)), true); assert_eq!(Float::new(1.0).eq(&Float::new(33.3)), false); + assert_eq!(Float::new(1.1), 1.1); } #[test] diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index 4678d89..eb18e00 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -188,6 +188,7 @@ impl ::core::hash::Hash for UFloat { #[cfg(test)] mod tests { use super::*; + use core::hash::{Hash, Hasher}; use pretty_assertions::assert_eq; #[test] @@ -213,6 +214,49 @@ mod tests { assert!(UFloat::from_str("-inf").is_err()); } + #[test] + fn test_hash() { + let mut hasher_left = std::collections::hash_map::DefaultHasher::new(); + let mut hasher_right = std::collections::hash_map::DefaultHasher::new(); + + assert_eq!( + UFloat::new(1.0).hash(&mut hasher_left), + UFloat::new(1.0).hash(&mut hasher_right) + ); + + assert_eq!(hasher_left.finish(), hasher_right.finish()); + } + + #[test] + fn test_ord() { + assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(1.1)), Ordering::Equal); + assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(2.1)), Ordering::Less); + assert_eq!(UFloat::new(1.1).cmp(&UFloat::new(0.1)), Ordering::Greater); + } + + #[test] + fn test_partial_ord() { + assert_eq!( + UFloat::new(1.1).partial_cmp(&UFloat::new(1.1)), + Some(Ordering::Equal) + ); + assert_eq!( + UFloat::new(1.1).partial_cmp(&UFloat::new(2.1)), + Some(Ordering::Less) + ); + assert_eq!( + UFloat::new(1.1).partial_cmp(&UFloat::new(0.1)), + Some(Ordering::Greater) + ); + } + + #[test] + fn test_partial_eq() { + assert_eq!(UFloat::new(1.0).eq(&UFloat::new(1.0)), true); + assert_eq!(UFloat::new(1.0).eq(&UFloat::new(33.3)), false); + assert_eq!(UFloat::new(1.1), 1.1); + } + #[test] #[should_panic = "float must be positive: `-1.1`"] fn test_new_negative() { UFloat::new(-1.1); } @@ -233,11 +277,6 @@ mod tests { #[should_panic = "float must not be `NaN`"] fn test_new_nan() { UFloat::new(::core::f32::NAN); } - #[test] - fn test_partial_eq() { - assert_eq!(UFloat::new(1.1), 1.1); - } - #[test] fn test_as_f32() { assert_eq!(UFloat::new(1.1).as_f32(), 1.1_f32); @@ -253,7 +292,10 @@ mod tests { fn test_try_from() { assert_eq!(UFloat::try_from(1.1_f32).unwrap(), UFloat::new(1.1)); - assert!(UFloat::try_from(-1.1_f32).is_err()); + assert_eq!( + UFloat::try_from(-1.1_f32), + Err(Error::custom("float must be positive: `-1.1`")) + ); assert!(UFloat::try_from(::core::f32::INFINITY).is_err()); assert!(UFloat::try_from(::core::f32::NAN).is_err()); assert!(UFloat::try_from(::core::f32::NEG_INFINITY).is_err()); From 03b0d2cf0c4bdfca967699f2f63798fedf5d2ec4 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 23 Feb 2020 19:00:03 +0100 Subject: [PATCH 041/112] remove `cargo deny` action --- .github/workflows/cargo_deny.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/workflows/cargo_deny.yml diff --git a/.github/workflows/cargo_deny.yml b/.github/workflows/cargo_deny.yml deleted file mode 100644 index d7ffbae..0000000 --- a/.github/workflows/cargo_deny.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: cargo deny -on: [push, pull_request] -jobs: - cargo-deny: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: EmbarkStudios/cargo-deny-action@v0 From 88a5fa4460c52cd5d029dd407097e0ef96d5a71b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 23 Feb 2020 23:19:54 +0100 Subject: [PATCH 042/112] improve `Channels` --- src/types/channels.rs | 122 +++++++++++++----------------------------- 1 file changed, 38 insertions(+), 84 deletions(-) diff --git a/src/types/channels.rs b/src/types/channels.rs index 2f1630c..6e7e148 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -1,37 +1,33 @@ use core::fmt; use core::str::FromStr; +use shorthand::ShortHand; + use crate::Error; -/// Specifies a list of parameters. +/// The maximum number of independent, simultaneous audio channels present in +/// any [`MediaSegment`] in the rendition. /// -/// # `MediaType::Audio` -/// -/// The first parameter is a count of audio channels expressed as a [`u64`], -/// indicating the maximum number of independent, simultaneous audio channels -/// present in any [`MediaSegment`] in the rendition. -/// -/// For example, an `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute. -/// -/// # Example -/// -/// Creating a `CHANNELS="6"` attribute -/// -/// ``` -/// # use hls_m3u8::types::Channels; -/// let mut channels = Channels::new(6); -/// -/// assert_eq!( -/// format!("CHANNELS=\"{}\"", channels), -/// "CHANNELS=\"6\"".to_string() -/// ); -/// ``` +/// For example, an `AC-3 5.1` rendition would have a maximum channel number of +/// 6. /// /// [`MediaSegment`]: crate::MediaSegment -#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(ShortHand, Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[shorthand(enable(must_use))] pub struct Channels { - channel_number: u64, - unknown: Vec, + /// The maximum number of independent simultaneous audio channels. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::Channels; + /// let mut channels = Channels::new(6); + /// # assert_eq!(channels.number(), 6); + /// + /// channels.set_number(5); + /// assert_eq!(channels.number(), 5); + /// ``` + number: u64, } impl Channels { @@ -41,70 +37,29 @@ impl Channels { /// /// ``` /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(6); + /// let channels = Channels::new(6); + /// + /// println!("CHANNELS=\"{}\"", channels); + /// # assert_eq!(format!("CHANNELS=\"{}\"", channels), "CHANNELS=\"6\"".to_string()); /// ``` - pub fn new(value: u64) -> Self { - Self { - channel_number: value, - unknown: vec![], - } - } - - /// Returns the channel number. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(6); - /// - /// assert_eq!(channels.channel_number(), 6); - /// ``` - pub const fn channel_number(&self) -> u64 { self.channel_number } - - /// Sets the channel number. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(3); - /// - /// channels.set_channel_number(6); - /// assert_eq!(channels.channel_number(), 6) - /// ``` - pub fn set_channel_number(&mut self, value: u64) -> &mut Self { - self.channel_number = value; - self - } + //#[inline] + #[must_use] + pub const fn new(number: u64) -> Self { Self { number } } } impl FromStr for Channels { type Err = Error; fn from_str(input: &str) -> Result { - let mut parameters = input.split('/'); - - let param_1 = parameters - .next() - .ok_or_else(|| Error::missing_attribute("first parameter of channels"))?; - - let channel_number = param_1.parse().map_err(|e| Error::parse_int(param_1, e))?; - - Ok(Self { - channel_number, - unknown: parameters.map(|v| (*v).to_string()).collect(), - }) + Ok(Self::new( + input.parse().map_err(|e| Error::parse_int(input, e))?, + )) } } impl fmt::Display for Channels { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.channel_number)?; - - if !self.unknown.is_empty() { - write!(f, "{}", self.unknown.join(","))?; - } + write!(f, "{}", self.number)?; Ok(()) } @@ -117,17 +72,16 @@ mod tests { #[test] fn test_display() { - let mut channels = Channels::new(6); - assert_eq!(channels.to_string(), "6".to_string()); + assert_eq!(Channels::new(6).to_string(), "6".to_string()); - channels.set_channel_number(7); - assert_eq!(channels.to_string(), "7".to_string()); + assert_eq!(Channels::new(7).to_string(), "7".to_string()); } #[test] fn test_parser() { - assert_eq!("6".parse::().unwrap(), Channels::new(6)); - assert!("garbage".parse::().is_err()); - assert!("".parse::().is_err()); + assert_eq!(Channels::new(6), Channels::from_str("6").unwrap()); + + assert!(Channels::from_str("garbage").is_err()); + assert!(Channels::from_str("").is_err()); } } From dae826b4e5fccf98f406ee240335434d433b666b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:19:37 +0100 Subject: [PATCH 043/112] improve (U)Float --- src/types/float.rs | 18 ++++++++++++------ src/types/ufloat.rs | 21 +++++++++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/types/float.rs b/src/types/float.rs index d03621d..7a1d262 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -2,17 +2,17 @@ use core::cmp::Ordering; use core::convert::TryFrom; use core::str::FromStr; -use derive_more::{Deref, Display}; +use derive_more::{AsRef, Deref, Display}; use crate::Error; -/// This is a wrapper type around an [`f32`] that can not be constructed +/// A wrapper type around an [`f32`] that can not be constructed /// with [`NaN`], [`INFINITY`] or [`NEG_INFINITY`]. /// /// [`NaN`]: core::f32::NAN /// [`INFINITY`]: core::f32::INFINITY /// [`NEG_INFINITY`]: core::f32::NEG_INFINITY -#[derive(Deref, Default, Debug, Copy, Clone, Display, PartialOrd)] +#[derive(AsRef, Deref, Default, Debug, Copy, Clone, Display, PartialOrd)] pub struct Float(f32); impl Float { @@ -25,16 +25,15 @@ impl Float { /// # Examples /// /// ``` - /// use hls_m3u8::types::Float; - /// + /// # use hls_m3u8::types::Float; /// let float = Float::new(1.0); /// ``` /// /// This would panic: /// /// ```should_panic + /// # use hls_m3u8::types::Float; /// use core::f32::NAN; - /// use hls_m3u8::types::Float; /// /// let float = Float::new(NAN); /// ``` @@ -53,6 +52,13 @@ impl Float { } /// Returns the underlying [`f32`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::Float; + /// assert_eq!(Float::new(1.1_f32).as_f32(), 1.1_f32); + /// ``` pub const fn as_f32(self) -> f32 { self.0 } } diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index eb18e00..36da75a 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -2,18 +2,18 @@ use core::cmp::Ordering; use core::convert::TryFrom; use core::str::FromStr; -use derive_more::{Deref, Display}; +use derive_more::{AsRef, Deref, Display}; use crate::Error; -/// This is a wrapper type around an [`f32`] that can not be constructed -/// with a negative float (ex. `-1.1`), [`NaN`], [`INFINITY`] or +/// A wrapper type around an [`f32`], that can not be constructed +/// with a negative float (e.g. `-1.1`), [`NaN`], [`INFINITY`] or /// [`NEG_INFINITY`]. /// /// [`NaN`]: core::f32::NAN /// [`INFINITY`]: core::f32::INFINITY /// [`NEG_INFINITY`]: core::f32::NEG_INFINITY -#[derive(Deref, Default, Debug, Copy, Clone, PartialOrd, Display)] +#[derive(AsRef, Deref, Default, Debug, Copy, Clone, PartialOrd, Display)] pub struct UFloat(f32); impl UFloat { @@ -26,16 +26,14 @@ impl UFloat { /// # Examples /// /// ``` - /// use hls_m3u8::types::UFloat; - /// + /// # use hls_m3u8::types::UFloat; /// let float = UFloat::new(1.0); /// ``` /// /// This would panic: /// /// ```should_panic - /// use hls_m3u8::types::UFloat; - /// + /// # use hls_m3u8::types::UFloat; /// let float = UFloat::new(-1.0); /// ``` /// @@ -57,6 +55,13 @@ impl UFloat { } /// Returns the underlying [`f32`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::UFloat; + /// assert_eq!(UFloat::new(1.1_f32).as_f32(), 1.1_f32); + /// ``` pub const fn as_f32(self) -> f32 { self.0 } } From 49c5b5334cebe709de0200dd7d97cf5b6933dedd Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:36:04 +0100 Subject: [PATCH 044/112] improve documentation and tests of HdcpLevel --- src/types/hdcp_level.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index 9796cfd..90ea014 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -1,17 +1,22 @@ use strum::{Display, EnumString}; -/// HDCP level. +/// HDCP ([`High-bandwidth Digital Content Protection`]) level. /// -/// See: [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 +/// [`High-bandwidth Digital Content Protection`]: +/// https://www.digital-cp.com/sites/default/files/specifications/HDCP%20on%20HDMI%20Specification%20Rev2_2_Final1.pdf #[non_exhaustive] -#[allow(missing_docs)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum HdcpLevel { + /// The associated [`VariantStream`] could fail to play unless the output is + /// protected by High-bandwidth Digital Content Protection ([`HDCP`]) Type 0 + /// or equivalent. + /// + /// [`VariantStream`]: crate::tags::VariantStream + /// [`HDCP`]: https://www.digital-cp.com/sites/default/files/specifications/HDCP%20on%20HDMI%20Specification%20Rev2_2_Final1.pdf #[strum(serialize = "TYPE-0")] Type0, + /// The content does not require output copy protection. None, } @@ -22,20 +27,14 @@ mod tests { #[test] fn test_display() { - let level = HdcpLevel::Type0; - assert_eq!(level.to_string(), "TYPE-0".to_string()); - - let level = HdcpLevel::None; - assert_eq!(level.to_string(), "NONE".to_string()); + assert_eq!(HdcpLevel::Type0.to_string(), "TYPE-0".to_string()); + assert_eq!(HdcpLevel::None.to_string(), "NONE".to_string()); } #[test] fn test_parser() { - let level = HdcpLevel::Type0; - assert_eq!(level, "TYPE-0".parse::().unwrap()); - - let level = HdcpLevel::None; - assert_eq!(level, "NONE".parse::().unwrap()); + assert_eq!(HdcpLevel::Type0, "TYPE-0".parse().unwrap()); + assert_eq!(HdcpLevel::None, "NONE".parse().unwrap()); assert!("unk".parse::().is_err()); } From cdb6367dbd26a92ee6eb05454a4d8ec137886f9c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:41:30 +0100 Subject: [PATCH 045/112] improve documentation for ProtocolVersion --- src/types/protocol_version.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index c9e1af7..114d5e8 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -3,13 +3,8 @@ use std::str::FromStr; use crate::Error; -/// # [7. Protocol Version Compatibility] -/// -/// The [`ProtocolVersion`] specifies, which m3u8 revision is required, to parse -/// a certain tag correctly. -/// -/// [7. Protocol Version Compatibility]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-7 +/// The [`ProtocolVersion`] specifies which `m3u8` revision is required, to +/// parse a certain tag correctly. #[non_exhaustive] #[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -24,7 +19,7 @@ pub enum ProtocolVersion { } impl ProtocolVersion { - /// Returns the newest [`ProtocolVersion`], that is supported by + /// Returns the latest [`ProtocolVersion`] that is supported by /// this library. /// /// # Example From 0be0c7ddfba79997d26c3184d3a5a58f7179c4e8 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 13:00:20 +0100 Subject: [PATCH 046/112] improve documentation and tests for Resolution --- src/types/resolution.rs | 47 +++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/types/resolution.rs b/src/types/resolution.rs index 2e8cb67..84c554b 100644 --- a/src/types/resolution.rs +++ b/src/types/resolution.rs @@ -5,33 +5,61 @@ use shorthand::ShortHand; use crate::Error; -/// This is a simple wrapper type for the display resolution. +/// The number of distinct pixels in each dimension that can be displayed (e.g. +/// 1920x1080). /// /// For example Full HD has a resolution of 1920x1080. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(ShortHand, Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)] #[display(fmt = "{}x{}", width, height)] #[shorthand(enable(must_use))] pub struct Resolution { /// Horizontal pixel dimension. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::Resolution; + /// let mut resolution = Resolution::new(1280, 720); + /// + /// resolution.set_width(1000); + /// assert_eq!(resolution.width(), 1000); + /// ``` width: usize, /// Vertical pixel dimension. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::Resolution; + /// let mut resolution = Resolution::new(1280, 720); + /// + /// resolution.set_height(800); + /// assert_eq!(resolution.height(), 800); + /// ``` height: usize, } impl Resolution { - /// Creates a new [`Resolution`]. + /// Constructs a new [`Resolution`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::Resolution; + /// let resolution = Resolution::new(1920, 1080); + /// ``` + #[must_use] pub const fn new(width: usize, height: usize) -> Self { Self { width, height } } } -/// A [`Resolution`] can be constructed from a tuple `(width, height)`. impl From<(usize, usize)> for Resolution { fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) } } +impl Into<(usize, usize)> for Resolution { + fn into(self) -> (usize, usize) { (self.width, self.height) } +} + impl FromStr for Resolution { type Err = Error; @@ -101,4 +129,9 @@ mod tests { fn test_from() { assert_eq!(Resolution::from((1920, 1080)), Resolution::new(1920, 1080)); } + + #[test] + fn test_into() { + assert_eq!((1920, 1080), Resolution::new(1920, 1080).into()); + } } From c7419c864fc076bb39df44e44aeddc1db75a34b1 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 14:09:26 +0100 Subject: [PATCH 047/112] improve StreamData --- src/master_playlist.rs | 20 ++++---- src/types/codecs.rs | 99 ++++++++++++++++++++++++++++++++++++++++ src/types/mod.rs | 2 + src/types/stream_data.rs | 37 +++++++++------ tests/rfc8216.rs | 18 ++++---- 5 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 src/types/codecs.rs diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 37d79c4..1376e10 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -418,7 +418,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(150000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((416, 234)) .build() .unwrap() @@ -431,7 +431,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(240000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((416, 234)) .build() .unwrap() @@ -444,7 +444,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(440000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((416, 234)) .build() .unwrap() @@ -457,7 +457,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(640000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((640, 360)) .build() .unwrap() @@ -470,7 +470,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(64000) - .codecs("mp4a.40.5") + .codecs(&["mp4a.40.5"]) .build() .unwrap() }, @@ -493,7 +493,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(150000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((416, 234)) .build() .unwrap() @@ -506,7 +506,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(240000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((416, 234)) .build() .unwrap() @@ -519,7 +519,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(440000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((416, 234)) .build() .unwrap() @@ -532,7 +532,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(640000) - .codecs("avc1.42e00a,mp4a.40.2") + .codecs(&["avc1.42e00a", "mp4a.40.2"]) .resolution((640, 360)) .build() .unwrap() @@ -545,7 +545,7 @@ mod tests { closed_captions: None, stream_data: StreamData::builder() .bandwidth(64000) - .codecs("mp4a.40.5") + .codecs(&["mp4a.40.5"]) .build() .unwrap() }, diff --git a/src/types/codecs.rs b/src/types/codecs.rs new file mode 100644 index 0000000..f9cf17f --- /dev/null +++ b/src/types/codecs.rs @@ -0,0 +1,99 @@ +use core::fmt; +use core::str::FromStr; + +use derive_more::{AsMut, AsRef, Deref, DerefMut}; + +use crate::Error; + +/// A list of formats, where each format specifies a media sample type that is +/// present in one or more renditions specified by the [`VariantStream`]. +/// +/// Valid format identifiers are those in the ISO Base Media File Format Name +/// Space defined by "The 'Codecs' and 'Profiles' Parameters for "Bucket" Media +/// Types" ([RFC6381]). +/// +/// For example, a stream containing AAC low complexity (AAC-LC) audio and H.264 +/// Main Profile Level 3.0 video would be +/// +/// ``` +/// # use hls_m3u8::types::Codecs; +/// let codecs = Codecs::from(&["mp4a.40.2", "avc1.4d401e"]); +/// ``` +/// +/// [RFC6381]: https://tools.ietf.org/html/rfc6381 +/// [`VariantStream`]: crate::tags::VariantStream +#[derive(AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Codecs { + list: Vec, +} + +impl Codecs { + /// Makes a new (empty) [`Codecs`] struct. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::Codecs; + /// let codecs = Codecs::new(); + /// ``` + #[inline] + #[must_use] + pub fn new() -> Self { Self { list: Vec::new() } } +} + +impl fmt::Display for Codecs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(codec) = self.list.iter().next() { + write!(f, "{}", codec)?; + + for codec in self.list.iter().skip(1) { + write!(f, ",{}", codec)?; + } + } + + Ok(()) + } +} +impl FromStr for Codecs { + type Err = Error; + + fn from_str(input: &str) -> Result { + Ok(Self { + list: input.split(',').map(|s| s.into()).collect(), + }) + } +} + +impl, I: IntoIterator> From for Codecs { + fn from(value: I) -> Self { + Self { + list: value.into_iter().map(|s| s.as_ref().to_string()).collect(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from() { + assert_eq!(Codecs::from(Vec::<&str>::new()), Codecs::new()); + } + + #[test] + fn test_display() { + assert_eq!( + Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"]).to_string(), + "mp4a.40.2,avc1.4d401e".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + Codecs::from_str("mp4a.40.2,avc1.4d401e").unwrap(), + Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"]) + ); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 5bb0b04..65ffcf4 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,6 +2,7 @@ mod byte_range; mod channels; mod closed_captions; +mod codecs; mod encryption_method; mod hdcp_level; mod in_stream_id; @@ -19,6 +20,7 @@ mod ufloat; pub use byte_range::*; pub use channels::*; pub use closed_captions::*; +pub use codecs::*; pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs index f8f03e0..624fff9 100644 --- a/src/types/stream_data.rs +++ b/src/types/stream_data.rs @@ -5,7 +5,7 @@ use derive_builder::Builder; use shorthand::ShortHand; use crate::attribute::AttributePairs; -use crate::types::{HdcpLevel, ProtocolVersion, Resolution}; +use crate::types::{Codecs, HdcpLevel, ProtocolVersion, Resolution}; use crate::utils::{quote, unquote}; use crate::{Error, RequiredVersion}; @@ -93,27 +93,34 @@ pub struct StreamData { #[builder(default)] #[shorthand(enable(copy), disable(into, option_as_ref))] average_bandwidth: Option, - /// A string that represents a list of formats, where each format specifies - /// a media sample type that is present in one or more renditions specified - /// by the [`VariantStream`]. + /// A list of formats, where each format specifies a media sample type that + /// is present in one or more renditions specified by the [`VariantStream`]. /// /// Valid format identifiers are those in the ISO Base Media File Format /// Name Space defined by "The 'Codecs' and 'Profiles' Parameters for - /// "Bucket" Media Types" [RFC6381]. + /// "Bucket" Media Types" ([RFC6381]). /// /// For example, a stream containing AAC low complexity (AAC-LC) audio and - /// H.264 Main Profile Level 3.0 video would have a codecs value of - /// "mp4a.40.2,avc1.4d401e". + /// H.264 Main Profile Level 3.0 video would be + /// + /// ``` + /// # use hls_m3u8::types::Codecs; + /// let codecs = Codecs::from(&["mp4a.40.2", "avc1.4d401e"]); + /// ``` /// /// # Example /// /// ``` /// # use hls_m3u8::types::StreamData; - /// # + /// use hls_m3u8::types::Codecs; + /// /// let mut stream = StreamData::new(20); /// - /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - /// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string())); + /// stream.set_codecs(Some(&["mp4a.40.2", "avc1.4d401e"])); + /// assert_eq!( + /// stream.codecs(), + /// Some(&Codecs::from(&["mp4a.40.2", "avc1.4d401e"])) + /// ); /// ``` /// /// # Note @@ -126,7 +133,7 @@ pub struct StreamData { /// crate::tags::VariantStream::ExtXStreamInf /// [RFC6381]: https://tools.ietf.org/html/rfc6381 #[builder(default, setter(into))] - codecs: Option, + codecs: Option, /// The resolution of the stream. /// /// # Example @@ -237,7 +244,7 @@ impl StreamData { /// StreamData::builder() /// .bandwidth(200) /// .average_bandwidth(15) - /// .codecs("mp4a.40.2,avc1.4d401e") + /// .codecs(&["mp4a.40.2", "avc1.4d401e"]) /// .resolution((1920, 1080)) /// .hdcp_level(HdcpLevel::Type0) /// .video("video_01") @@ -297,7 +304,7 @@ impl FromStr for StreamData { .map_err(|e| Error::parse_int(value, e))?, ) } - "CODECS" => codecs = Some(unquote(value)), + "CODECS" => codecs = Some(unquote(value).parse()?), "RESOLUTION" => resolution = Some(value.parse()?), "HDCP-LEVEL" => { hdcp_level = Some(value.parse::().map_err(Error::strum)?) @@ -346,7 +353,7 @@ mod tests { fn test_display() { let mut stream_data = StreamData::new(200); stream_data.set_average_bandwidth(Some(15)); - stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_data.set_codecs(Some(&["mp4a.40.2", "avc1.4d401e"])); stream_data.set_resolution(Some((1920, 1080))); stream_data.set_hdcp_level(Some(HdcpLevel::Type0)); stream_data.set_video(Some("video")); @@ -369,7 +376,7 @@ mod tests { fn test_parser() { let mut stream_data = StreamData::new(200); stream_data.set_average_bandwidth(Some(15)); - stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_data.set_codecs(Some(&["mp4a.40.2", "avc1.4d401e"])); stream_data.set_resolution(Some((1920, 1080))); stream_data.set_hdcp_level(Some(HdcpLevel::Type0)); stream_data.set_video(Some("video")); diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index 5390f55..707329a 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -214,7 +214,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(65000) - .codecs("mp4a.40.5") + .codecs(&["mp4a.40.5"]) .build() .unwrap() }, @@ -280,7 +280,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(65000) - .codecs("mp4a.40.5") + .codecs(&["mp4a.40.5"]) .build() .unwrap() }, @@ -345,7 +345,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(1280000) - .codecs("...") + .codecs(&["..."]) .build() .unwrap() }, @@ -357,7 +357,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(2560000) - .codecs("...") + .codecs(&["..."]) .build() .unwrap() }, @@ -369,7 +369,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(7680000) - .codecs("...") + .codecs(&["..."]) .build() .unwrap() }, @@ -381,7 +381,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(65000) - .codecs("mp4a.40.5") + .codecs(&["mp4a.40.5"]) .build() .unwrap() }, @@ -518,7 +518,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(1280000) - .codecs("...") + .codecs(&["..."]) .video("low") .build() .unwrap() @@ -531,7 +531,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(2560000) - .codecs("...") + .codecs(&["..."]) .video("mid") .build() .unwrap() @@ -544,7 +544,7 @@ generate_tests! [ closed_captions: None, stream_data: StreamData::builder() .bandwidth(7680000) - .codecs("...") + .codecs(&["..."]) .video("hi") .build() .unwrap() From 11ac527fca20311444923b11c147a7f41b3004f5 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 14:28:14 +0100 Subject: [PATCH 048/112] improve documentation of EncryptionMethod --- src/types/encryption_method.rs | 50 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 6d4f1ea..1dd977c 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -1,43 +1,46 @@ use strum::{Display, EnumString}; -/// Encryption method. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +/// The encryption method. #[non_exhaustive] #[allow(missing_docs)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum EncryptionMethod { - /// `None` means that the [`MediaSegment`]s are not encrypted. + /// The [`MediaSegment`]s are not encrypted. /// /// [`MediaSegment`]: crate::MediaSegment None, - /// `Aes128` signals that the [`MediaSegment`]s are completely encrypted - /// using the Advanced Encryption Standard ([AES-128]) with a 128-bit - /// key, Cipher Block Chaining (CBC), and - /// [Public-Key Cryptography Standards #7 (PKCS7)] padding. + /// The [`MediaSegment`]s are completely encrypted using the Advanced + /// Encryption Standard ([AES-128]) with a 128-bit key, Cipher Block + /// Chaining (CBC), and [Public-Key Cryptography Standards #7 (PKCS7)] + /// padding. /// /// CBC is restarted on each segment boundary, using either the - /// Initialization Vector (IV) attribute value or the Media Sequence - /// Number as the IV. + /// Initialization Vector (IV) or the Media Sequence Number as the IV + /// + /// ``` + /// # let media_sequence_number = 5; + /// # assert_eq!( + /// format!("0x{:032x}", media_sequence_number) + /// # , "00000000000000000000000000000005".to_string()); + /// ``` /// /// [`MediaSegment`]: crate::MediaSegment /// [AES-128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf /// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652 #[strum(serialize = "AES-128")] Aes128, - /// `SampleAes` means that the [`MediaSegment`]s - /// contain media samples, such as audio or video, that are encrypted - /// using the Advanced Encryption Standard ([`AES-128`]). How these media - /// streams are encrypted and encapsulated in a segment depends on the - /// media encoding and the media format of the segment. fMP4 Media - /// Segments are encrypted using the 'cbcs' scheme of - /// [Common Encryption]. Encryption of other Media Segment - /// formats containing [H.264], [AAC], [AC-3], - /// and Enhanced [AC-3] media streams is described in the HTTP - /// Live Streaming (HLS) [SampleEncryption specification]. + /// The [`MediaSegment`]s contain media samples, such as audio or video, + /// that are encrypted using the Advanced Encryption Standard ([`AES-128`]). + /// + /// How these media streams are encrypted and encapsulated in a segment + /// depends on the media encoding and the media format of the segment. + /// + /// `fMP4` [`MediaSegment`]s are encrypted using the `cbcs` scheme of + /// [Common Encryption]. + /// Encryption of other [`MediaSegment`] formats containing [H.264], [AAC], + /// [AC-3], and Enhanced [AC-3] media streams is described in the + /// [HTTP Live Streaming (HLS) SampleEncryption specification]. /// /// [`MediaSegment`]: crate::MediaSegment /// [`AES-128`]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf @@ -45,7 +48,8 @@ pub enum EncryptionMethod { /// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264 /// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496 /// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3 - /// [SampleEncryption specification]: https://tools.ietf.org/html/rfc8216#ref-SampleEnc + /// [HTTP Live Streaming (HLS) SampleEncryption specification]: + /// https://tools.ietf.org/html/rfc8216#ref-SampleEnc SampleAes, } From 90783fdd9df6cd203f87aaf144653317a0ed4d70 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 14:32:28 +0100 Subject: [PATCH 049/112] improve documentation of InstreamId --- src/types/in_stream_id.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 7d36d6a..ee35989 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -91,6 +91,9 @@ pub enum InStreamId { Service63, } +/// The variants [`InStreamId::Cc1`], [`InStreamId::Cc2`], [`InStreamId::Cc3`] +/// and [`InStreamId::Cc4`] require [`ProtocolVersion::V1`], the other +/// [`ProtocolVersion::V7`]. impl RequiredVersion for InStreamId { fn required_version(&self) -> ProtocolVersion { match &self { From dc12db9fad692d3ca220af8334390ba721b07370 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 14:49:20 +0100 Subject: [PATCH 050/112] improve KeyFormatVersions --- src/types/key_format_versions.rs | 67 ++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 0bd1189..214b725 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -1,4 +1,3 @@ -use std::convert::Infallible; use std::fmt; use std::str::FromStr; @@ -6,9 +5,10 @@ use derive_more::{Deref, DerefMut}; 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 numbers that can be used to indicate which version(s) /// this instance complies with, if more than one version of a particular /// [`KeyFormat`] is defined. /// @@ -18,9 +18,27 @@ pub struct KeyFormatVersions(Vec); impl KeyFormatVersions { /// Makes a new [`KeyFormatVersions`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let key_format_versions = KeyFormatVersions::new(); + /// ``` + #[inline] + #[must_use] pub fn new() -> Self { Self::default() } - /// Add a value to the [`KeyFormatVersions`]. + /// Add a value to the end of [`KeyFormatVersions`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut key_format_versions = KeyFormatVersions::new(); + /// + /// key_format_versions.push(1); + /// ``` pub fn push(&mut self, value: usize) { if self.is_default() { self.0 = vec![value]; @@ -31,6 +49,14 @@ impl KeyFormatVersions { /// Returns `true`, if [`KeyFormatVersions`] has the default value of /// `vec![1]`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// assert!(KeyFormatVersions::from(vec![1]).is_default()); + /// ``` + #[must_use] pub fn is_default(&self) -> bool { // self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() @@ -47,13 +73,13 @@ impl RequiredVersion for KeyFormatVersions { } impl FromStr for KeyFormatVersions { - type Err = Infallible; + type Err = Error; fn from_str(input: &str) -> Result { let mut result = unquote(input) .split('/') - .filter_map(|v| v.parse().ok()) - .collect::>(); + .map(|v| v.parse().map_err(|e| Error::parse_int(v, e))) + .collect::, Error>>()?; if result.is_empty() { result.push(1); @@ -69,23 +95,22 @@ impl fmt::Display for KeyFormatVersions { return write!(f, "{}", quote("1")); } - write!( - f, - "{}", - quote( - // vec![1, 2, 3] -> "1/2/3" - self.0 - .iter() - .map(ToString::to_string) - .collect::>() - .join("/") - ) - ) + if let Some(value) = self.0.iter().next() { + write!(f, "\"{}", value)?; + + for value in self.0.iter().skip(1) { + write!(f, "/{}", value)?; + } + + write!(f, "\"")?; + } + + Ok(()) } } -impl>> From for KeyFormatVersions { - fn from(value: T) -> Self { Self(value.into()) } +impl> From for KeyFormatVersions { + fn from(value: I) -> Self { Self(value.into_iter().collect()) } } #[cfg(test)] @@ -101,7 +126,6 @@ mod tests { ); assert_eq!(KeyFormatVersions::from(vec![]).to_string(), quote("1")); - assert_eq!(KeyFormatVersions::new().to_string(), quote("1")); } @@ -115,6 +139,7 @@ mod tests { assert_eq!(KeyFormatVersions::from(vec![1]), "1".parse().unwrap()); assert_eq!(KeyFormatVersions::from(vec![1, 2]), "1/2".parse().unwrap()); + assert!("1/b".parse::().is_err()); } #[test] From f7d81a55c91c74af8c079822ccde891b807fbe3b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 16:16:40 +0100 Subject: [PATCH 051/112] improve documentation of ExtXIndependentSegments --- src/tags/shared/independent_segments.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 4bf445f..9f7a0bd 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -5,13 +5,8 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] -/// -/// The [`ExtXIndependentSegments`] tag signals that all media samples in a -/// [`MediaSegment`] can be decoded without information from other segments. -/// -/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: -/// https://tools.ietf.org/html/rfc8216#section-4.3.5.1 +/// Signals that all media samples in a [`MediaSegment`] can be decoded without +/// information from other segments. /// /// [`MediaSegment`]: crate::MediaSegment #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] From 9273e6c16cbaa342f335c425da30d289491a0084 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 16:30:43 +0100 Subject: [PATCH 052/112] add must_use attributes --- src/lib.rs | 1 - src/master_playlist.rs | 2 ++ src/media_playlist.rs | 2 ++ src/tags/basic/version.rs | 2 ++ src/tags/master_playlist/media.rs | 2 ++ src/tags/master_playlist/session_data.rs | 3 +++ src/tags/master_playlist/session_key.rs | 1 + src/tags/master_playlist/variant_stream.rs | 1 + src/tags/media_playlist/discontinuity_sequence.rs | 1 + src/tags/media_playlist/media_sequence.rs | 2 ++ src/tags/media_playlist/target_duration.rs | 2 ++ src/tags/media_segment/date_range.rs | 2 ++ src/tags/media_segment/inf.rs | 4 ++++ src/tags/media_segment/program_date_time.rs | 2 ++ src/types/float.rs | 2 ++ src/types/protocol_version.rs | 2 ++ src/types/stream_data.rs | 2 ++ src/types/ufloat.rs | 2 ++ 18 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3659b68..c15d512 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ )] #![allow( clippy::multiple_crate_versions, - clippy::must_use_candidate, clippy::module_name_repetitions, clippy::default_trait_access )] diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 1376e10..4499db2 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -111,6 +111,8 @@ impl MasterPlaylist { /// .build()?; /// # Ok::<(), Box>(()) /// ``` + #[must_use] + #[inline] pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index f7855ed..fdd167d 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -205,6 +205,8 @@ impl RequiredVersion for MediaPlaylistBuilder { impl MediaPlaylist { /// Returns a builder for [`MediaPlaylist`]. + #[must_use] + #[inline] pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 3b2e3cf..e758f01 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -27,6 +27,7 @@ impl ExtXVersion { /// /// let version = ExtXVersion::new(ProtocolVersion::V2); /// ``` + #[must_use] pub const fn new(version: ProtocolVersion) -> Self { Self(version) } /// Returns the underlying [`ProtocolVersion`]. @@ -42,6 +43,7 @@ impl ExtXVersion { /// ProtocolVersion::V6 /// ); /// ``` + #[must_use] pub const fn version(self) -> ProtocolVersion { self.0 } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index e30b079..e59a00f 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -191,6 +191,7 @@ impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; /// Makes a new [`ExtXMedia`] tag. + #[must_use] pub fn new(media_type: MediaType, group_id: T, name: K) -> Self where T: Into, @@ -213,6 +214,7 @@ impl ExtXMedia { } /// Returns a builder for [`ExtXMedia`]. + #[must_use] pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() } } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 4c23fd4..8d9058c 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -140,6 +140,7 @@ impl ExtXSessionData { /// SessionData::Uri("https://www.example.com/".to_string()), /// ); /// ``` + #[must_use] pub fn new>(data_id: T, data: SessionData) -> Self { Self { data_id: data_id.into(), @@ -163,6 +164,7 @@ impl ExtXSessionData { /// .build()?; /// # Ok::<(), Box>(()) /// ``` + #[must_use] pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() } /// Makes a new [`ExtXSessionData`] tag, with the given language. @@ -179,6 +181,7 @@ impl ExtXSessionData { /// "en", /// ); /// ``` + #[must_use] pub fn with_language(data_id: T, data: SessionData, language: K) -> Self where T: Into, diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index ce93168..973911f 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -45,6 +45,7 @@ impl ExtXSessionKey { /// "https://www.example.com/", /// )); /// ``` + #[must_use] pub fn new(inner: ExtXKey) -> Self { if inner.method() == EncryptionMethod::None { panic!("the encryption method should never be `None`"); diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index 9723b08..bc2dde4 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -201,6 +201,7 @@ impl VariantStream { /// .unwrap(), /// )); /// ``` + #[must_use] pub fn is_associated(&self, media: &ExtXMedia) -> bool { match &self { Self::ExtXIFrame { stream_data, .. } => { diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index a0a0349..96972ed 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -49,6 +49,7 @@ impl ExtXDiscontinuitySequence { /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); /// ``` + #[must_use] pub const fn new(seq_num: u64) -> Self { Self { seq_num } } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index c7cb4ca..224ca88 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -27,6 +27,7 @@ impl ExtXMediaSequence { /// # use hls_m3u8::tags::ExtXMediaSequence; /// let media_sequence = ExtXMediaSequence::new(5); /// ``` + #[must_use] pub const fn new(seq_num: u64) -> Self { Self(seq_num) } /// Returns the sequence number of the first media segment, @@ -40,6 +41,7 @@ impl ExtXMediaSequence { /// /// assert_eq!(media_sequence.seq_num(), 5); /// ``` + #[must_use] pub const fn seq_num(self) -> u64 { self.0 } /// Sets the sequence number. diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index aec2ee4..9ef6e3c 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -36,6 +36,7 @@ impl ExtXTargetDuration { /// # Note /// /// The nanoseconds part of the [`Duration`] will be discarded. + #[must_use] pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) } /// Returns the maximum media segment duration. @@ -50,6 +51,7 @@ impl ExtXTargetDuration { /// /// assert_eq!(target_duration.duration(), Duration::from_secs(2)); /// ``` + #[must_use] pub const fn duration(&self) -> Duration { self.0 } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 3d0f129..337322b 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -160,6 +160,7 @@ impl ExtXDateRange { /// .and_hms_milli(14, 54, 23, 31), /// ); /// ``` + #[must_use] pub fn new>(id: T, start_date: DateTime) -> Self { Self { id: id.into(), @@ -177,6 +178,7 @@ impl ExtXDateRange { } /// Returns a builder for [`ExtXDateRange`]. + #[must_use] pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 89eb958..7703e62 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -32,6 +32,7 @@ impl ExtInf { /// /// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// ``` + #[must_use] pub const fn new(duration: Duration) -> Self { Self { duration, @@ -49,6 +50,7 @@ impl ExtInf { /// /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// ``` + #[must_use] pub fn with_title>(duration: Duration, title: T) -> Self { Self { duration, @@ -68,6 +70,7 @@ impl ExtInf { /// /// assert_eq!(ext_inf.duration(), Duration::from_secs(5)); /// ``` + #[must_use] pub const fn duration(&self) -> Duration { self.duration } /// Sets the duration of the associated media segment. @@ -101,6 +104,7 @@ impl ExtInf { /// /// assert_eq!(ext_inf.title(), &Some("title".to_string())); /// ``` + #[must_use] pub const fn title(&self) -> &Option { &self.title } /// Sets the title of the associated media segment. diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index bb68f37..36caca7 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -38,6 +38,7 @@ impl ExtXProgramDateTime { /// .and_hms_milli(14, 54, 23, 31), /// ); /// ``` + #[must_use] pub const fn new(date_time: DateTime) -> Self { Self(date_time) } /// Returns the date-time of the first sample of the associated media @@ -64,6 +65,7 @@ impl ExtXProgramDateTime { /// .and_hms_milli(14, 54, 23, 31) /// ); /// ``` + #[must_use] pub const fn date_time(&self) -> DateTime { self.0 } /// Sets the date-time of the first sample of the associated media segment. diff --git a/src/types/float.rs b/src/types/float.rs index 7a1d262..b74b607 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -39,6 +39,7 @@ impl Float { /// ``` /// /// [`NaN`]: core::f32::NAN + #[must_use] pub fn new(float: f32) -> Self { if float.is_infinite() { panic!("float must be finite: `{}`", float); @@ -59,6 +60,7 @@ impl Float { /// # use hls_m3u8::types::Float; /// assert_eq!(Float::new(1.1_f32).as_f32(), 1.1_f32); /// ``` + #[must_use] pub const fn as_f32(self) -> f32 { self.0 } } diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 114d5e8..21f7471 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -28,6 +28,8 @@ impl ProtocolVersion { /// # use hls_m3u8::types::ProtocolVersion; /// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7); /// ``` + #[must_use] + #[inline] pub const fn latest() -> Self { Self::V7 } } diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs index 624fff9..516c513 100644 --- a/src/types/stream_data.rs +++ b/src/types/stream_data.rs @@ -223,6 +223,7 @@ impl StreamData { /// # /// let stream = StreamData::new(20); /// ``` + #[must_use] pub const fn new(bandwidth: u64) -> Self { Self { bandwidth, @@ -251,6 +252,7 @@ impl StreamData { /// .build()?; /// # Ok::<(), Box>(()) /// ``` + #[must_use] pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() } } diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index 36da75a..aadf249 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -38,6 +38,7 @@ impl UFloat { /// ``` /// /// [`NaN`]: core::f32::NAN + #[must_use] pub fn new(float: f32) -> Self { if float.is_infinite() { panic!("float must be finite: `{}`", float); @@ -62,6 +63,7 @@ impl UFloat { /// # use hls_m3u8::types::UFloat; /// assert_eq!(UFloat::new(1.1_f32).as_f32(), 1.1_f32); /// ``` + #[must_use] pub const fn as_f32(self) -> f32 { self.0 } } From 6ef8182f2ce9efa6e16a47eec370078bd0d5ae15 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 16:44:02 +0100 Subject: [PATCH 053/112] improvments to ExtXStart --- src/master_playlist.rs | 5 +- src/tags/media_segment/date_range.rs | 154 ++++++++++++--------------- src/tags/shared/start.rs | 108 ++++++++----------- 3 files changed, 118 insertions(+), 149 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 4499db2..cfc1156 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -103,11 +103,12 @@ impl MasterPlaylist { /// # Example /// /// ``` + /// # use hls_m3u8::MasterPlaylist; /// use hls_m3u8::tags::ExtXStart; - /// use hls_m3u8::MasterPlaylist; + /// use hls_m3u8::types::Float; /// /// MasterPlaylist::builder() - /// .start(ExtXStart::new(20.123456)) + /// .start(ExtXStart::new(Float::new(20.3))) /// .build()?; /// # Ok::<(), Box>(()) /// ``` diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 337322b..061489d 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -326,53 +326,70 @@ impl fmt::Display for ExtXDateRange { #[cfg(test)] mod test { use super::*; + use crate::types::Float; 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" + macro_rules! generate_tests { + ( $( { $left:expr, $right:expr } ),* $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($left.to_string(), $right.to_string()); + )* + } + + #[test] + fn test_parser() { + $( + assert_eq!($left, $right.parse().unwrap()); + )* + assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO" + .parse::() + .is_err()); + + assert!("garbage".parse::().is_err()); + assert!("".parse::().is_err()); + + assert!(concat!( + "#EXT-X-DATERANGE:", + "ID=\"test_id\",", + "START-DATE=\"2014-03-05T11:15:00Z\",", + "END-ON-NEXT=YES" + ) .parse::() - .unwrap(), + .is_err()); + } + } + } + + generate_tests! { + { 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" - ) + .scte35_out(concat!( + "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(), + concat!( + "#EXT-X-DATERANGE:", + "ID=\"splice-6FFFFFF0\",", + "START-DATE=\"2014-03-05T11:15:00Z\",", + "PLANNED-DURATION=59.993,", + "SCTE35-OUT=0xFC002F0000000000FF000014056F", + "FFFFF000E011622DCAFF000052636200000000000", + "A0008029896F50000008700000000" + ) + }, + { ExtXDateRange::builder() .id("test_id") .class("test_class") @@ -380,61 +397,28 @@ mod test { .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) + .insert_client_attribute("X-CUSTOM", Float::new(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" - ) + .unwrap(), + concat!( + "#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] diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 1b2afb2..0d676c7 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -8,22 +8,42 @@ use crate::types::{Float, ProtocolVersion}; use crate::utils::{parse_yes_or_no, tag}; use crate::{Error, RequiredVersion}; -/// # [4.3.5.2. EXT-X-START] +/// This tag indicates a preferred point at which to start +/// playing a Playlist. /// -/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 +/// By default, clients should start playback at this point when beginning a +/// playback session. #[derive(ShortHand, PartialOrd, Debug, Clone, Copy, PartialEq)] #[shorthand(enable(must_use))] pub struct ExtXStart { - #[shorthand(enable(skip))] - time_offset: Float, - /// Returns whether clients should not render media stream whose - /// presentation times are prior to the specified time offset. + /// The time offset of the [`MediaSegment`]s in the playlist. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXStart; - /// let mut start = ExtXStart::new(20.123456); + /// use hls_m3u8::types::Float; + /// + /// let mut start = ExtXStart::new(Float::new(20.123456)); + /// # assert_eq!(start.time_offset(), Float::new(20.123456)); + /// + /// start.set_time_offset(Float::new(1.0)); + /// assert_eq!(start.time_offset(), Float::new(1.0)); + /// ``` + /// + /// [`MediaSegment`]: crate::MediaSegment + #[shorthand(enable(copy))] + time_offset: Float, + /// Whether clients should not render media stream whose presentation times + /// are prior to the specified time offset. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// use hls_m3u8::types::Float; + /// + /// let mut start = ExtXStart::new(Float::new(20.123456)); /// # assert_eq!(start.is_precise(), false); /// start.set_is_precise(true); /// @@ -37,76 +57,40 @@ impl ExtXStart { /// Makes a new [`ExtXStart`] tag. /// - /// # Panics - /// - /// Panics if the `time_offset` is infinite or [`NaN`]. - /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXStart; - /// let start = ExtXStart::new(20.123456); - /// ``` + /// use hls_m3u8::types::Float; /// - /// [`NaN`]: core::f64::NAN - pub fn new(time_offset: f32) -> Self { + /// let start = ExtXStart::new(Float::new(20.123456)); + /// ``` + #[must_use] + pub const fn new(time_offset: Float) -> Self { Self { - time_offset: Float::new(time_offset), + time_offset, is_precise: false, } } /// Makes a new [`ExtXStart`] tag with the given `precise` flag. /// - /// # Panics - /// - /// Panics if the `time_offset` is infinite or [`NaN`]. - /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXStart; - /// let start = ExtXStart::with_precise(20.123456, true); + /// use hls_m3u8::types::Float; + /// + /// let start = ExtXStart::with_precise(Float::new(20.123456), true); /// assert_eq!(start.is_precise(), true); /// ``` - /// - /// [`NaN`]: core::f64::NAN - pub fn with_precise(time_offset: f32, is_precise: bool) -> Self { + #[must_use] + pub const fn with_precise(time_offset: Float, is_precise: bool) -> Self { Self { - time_offset: Float::new(time_offset), + time_offset, is_precise, } } - - /// Returns the time offset of the media segments in the playlist. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXStart; - /// let start = ExtXStart::new(20.123456); - /// - /// assert_eq!(start.time_offset(), 20.123456); - /// ``` - pub const fn time_offset(self) -> f32 { self.time_offset.as_f32() } - - /// Sets the time offset of the media segments in the playlist. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXStart; - /// let mut start = ExtXStart::new(20.123456); - /// # assert_eq!(start.time_offset(), 20.123456); - /// - /// start.set_time_offset(1.0); - /// - /// assert_eq!(start.time_offset(), 1.0); - /// ``` - pub fn set_time_offset(&mut self, value: f32) -> &mut Self { - self.time_offset = Float::new(value); - self - } } /// This tag requires [`ProtocolVersion::V1`]. @@ -165,12 +149,12 @@ mod test { #[test] fn test_display() { assert_eq!( - ExtXStart::new(-1.23).to_string(), + ExtXStart::new(Float::new(-1.23)).to_string(), "#EXT-X-START:TIME-OFFSET=-1.23".to_string(), ); assert_eq!( - ExtXStart::with_precise(1.23, true).to_string(), + ExtXStart::with_precise(Float::new(1.23), true).to_string(), "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".to_string(), ); } @@ -178,12 +162,12 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXStart::new(-1.23).required_version(), + ExtXStart::new(Float::new(-1.23)).required_version(), ProtocolVersion::V1, ); assert_eq!( - ExtXStart::with_precise(1.23, true).required_version(), + ExtXStart::with_precise(Float::new(1.23), true).required_version(), ProtocolVersion::V1, ); } @@ -191,17 +175,17 @@ mod test { #[test] fn test_parser() { assert_eq!( - ExtXStart::new(-1.23), + ExtXStart::new(Float::new(-1.23)), "#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(), ); assert_eq!( - ExtXStart::with_precise(1.23, true), + ExtXStart::with_precise(Float::new(1.23), true), "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(), ); assert_eq!( - ExtXStart::with_precise(1.23, true), + ExtXStart::with_precise(Float::new(1.23), true), "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG" .parse() .unwrap(), From 6333a8050738325140b541431b40c75b6e6fc4f0 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 16:45:10 +0100 Subject: [PATCH 054/112] minor improvements --- src/lib.rs | 4 ++-- src/media_segment.rs | 2 ++ src/tags/master_playlist/media.rs | 10 ++-------- src/tags/master_playlist/session_key.rs | 2 +- src/tags/media_segment/key.rs | 4 ++++ src/tags/media_segment/map.rs | 3 ++- src/types/codecs.rs | 6 ++++-- src/types/encryption_method.rs | 2 +- src/types/float.rs | 6 +++--- src/types/ufloat.rs | 10 +++++----- 10 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c15d512..f673384 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,6 @@ )] //! [HLS] m3u8 parser/generator. //! -//! [HLS]: https://tools.ietf.org/html/rfc8216 -//! //! # Examples //! //! ``` @@ -38,6 +36,8 @@ //! //! assert!(m3u8.parse::().is_ok()); //! ``` +//! +//! [HLS]: https://tools.ietf.org/html/rfc8216 pub use error::Error; pub use master_playlist::MasterPlaylist; diff --git a/src/media_segment.rs b/src/media_segment.rs index 804588a..e23de4e 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -42,6 +42,8 @@ pub struct MediaSegment { impl MediaSegment { /// Returns a builder for a [`MediaSegment`]. + #[must_use] + #[inline] pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index e59a00f..b9aa18a 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -220,14 +220,8 @@ impl ExtXMedia { impl RequiredVersion for ExtXMedia { fn required_version(&self) -> ProtocolVersion { - match self.instream_id { - None - | Some(InStreamId::Cc1) - | Some(InStreamId::Cc2) - | Some(InStreamId::Cc3) - | Some(InStreamId::Cc4) => ProtocolVersion::V1, - _ => ProtocolVersion::V7, - } + self.instream_id + .map_or(ProtocolVersion::V1, |i| i.required_version()) } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 973911f..f8eedea 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -192,7 +192,7 @@ mod test { // EncryptionMethod is None! #[test] #[should_panic = "the encryption method should never be `None`"] - fn test_new_panic() { ExtXSessionKey::new(ExtXKey::new(EncryptionMethod::None, "")); } + fn test_new_panic() { let _ = ExtXSessionKey::new(ExtXKey::new(EncryptionMethod::None, "")); } #[test] fn test_deref() { diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index 379276b..d1c5590 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -178,6 +178,7 @@ impl ExtXKey { /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\"" /// ); /// ``` + #[must_use] pub fn new>(method: EncryptionMethod, uri: T) -> Self { Self { method, @@ -207,6 +208,7 @@ impl ExtXKey { /// .build()?; /// # Ok::<(), Box>(()) /// ``` + #[must_use] pub fn builder() -> ExtXKeyBuilder { ExtXKeyBuilder::default() } /// Makes a new [`ExtXKey`] tag without a decryption key. @@ -219,6 +221,7 @@ impl ExtXKey { /// /// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE"); /// ``` + #[must_use] pub const fn empty() -> Self { Self { method: EncryptionMethod::None, @@ -244,6 +247,7 @@ impl ExtXKey { /// ``` /// /// [`None`]: EncryptionMethod::None + #[must_use] pub fn is_empty(&self) -> bool { self.method() == EncryptionMethod::None } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index fa89c13..e5d5d0c 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -145,7 +145,8 @@ impl FromStr for ExtXMap { } } - let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?; + let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; + Ok(Self { uri, range, diff --git a/src/types/codecs.rs b/src/types/codecs.rs index f9cf17f..db36e20 100644 --- a/src/types/codecs.rs +++ b/src/types/codecs.rs @@ -22,7 +22,9 @@ use crate::Error; /// /// [RFC6381]: https://tools.ietf.org/html/rfc6381 /// [`VariantStream`]: crate::tags::VariantStream -#[derive(AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, +)] pub struct Codecs { list: Vec, } @@ -38,7 +40,7 @@ impl Codecs { /// ``` #[inline] #[must_use] - pub fn new() -> Self { Self { list: Vec::new() } } + pub const fn new() -> Self { Self { list: Vec::new() } } } impl fmt::Display for Codecs { diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 1dd977c..3f8cfa0 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -22,7 +22,7 @@ pub enum EncryptionMethod { /// # let media_sequence_number = 5; /// # assert_eq!( /// format!("0x{:032x}", media_sequence_number) - /// # , "00000000000000000000000000000005".to_string()); + /// # , "0x00000000000000000000000000000005".to_string()); /// ``` /// /// [`MediaSegment`]: crate::MediaSegment diff --git a/src/types/float.rs b/src/types/float.rs index b74b607..7e2a8d9 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -277,15 +277,15 @@ mod tests { #[test] #[should_panic = "float must be finite: `inf`"] - fn test_new_infinite() { Float::new(::core::f32::INFINITY); } + fn test_new_infinite() { let _ = Float::new(::core::f32::INFINITY); } #[test] #[should_panic = "float must be finite: `-inf`"] - fn test_new_neg_infinite() { Float::new(::core::f32::NEG_INFINITY); } + fn test_new_neg_infinite() { let _ = Float::new(::core::f32::NEG_INFINITY); } #[test] #[should_panic = "float must not be `NaN`"] - fn test_new_nan() { Float::new(::core::f32::NAN); } + fn test_new_nan() { let _ = Float::new(::core::f32::NAN); } #[test] fn test_as_f32() { diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index aadf249..25cb13f 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -266,23 +266,23 @@ mod tests { #[test] #[should_panic = "float must be positive: `-1.1`"] - fn test_new_negative() { UFloat::new(-1.1); } + fn test_new_negative() { let _ = UFloat::new(-1.1); } #[test] #[should_panic = "float must be positive: `0`"] - fn test_new_negative_zero() { UFloat::new(-0.0); } + fn test_new_negative_zero() { let _ = UFloat::new(-0.0); } #[test] #[should_panic = "float must be finite: `inf`"] - fn test_new_infinite() { UFloat::new(::core::f32::INFINITY); } + fn test_new_infinite() { let _ = UFloat::new(::core::f32::INFINITY); } #[test] #[should_panic = "float must be finite: `-inf`"] - fn test_new_neg_infinite() { UFloat::new(::core::f32::NEG_INFINITY); } + fn test_new_neg_infinite() { let _ = UFloat::new(::core::f32::NEG_INFINITY); } #[test] #[should_panic = "float must not be `NaN`"] - fn test_new_nan() { UFloat::new(::core::f32::NAN); } + fn test_new_nan() { let _ = UFloat::new(::core::f32::NAN); } #[test] fn test_as_f32() { From a262c77c582d8e8e730f9b3e60bb0924390ddf90 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 16:45:32 +0100 Subject: [PATCH 055/112] improvements to `Value` --- src/types/value.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/types/value.rs b/src/types/value.rs index 2a20662..3c1b379 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -1,21 +1,20 @@ use std::fmt; use std::str::FromStr; -use hex; - +use crate::types::Float; use crate::utils::{quote, unquote}; use crate::Error; -/// A [`Value`]. +/// A `Value`. #[non_exhaustive] -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub enum Value { - /// A [`String`]. + /// A `String`. String(String), /// A sequence of bytes. Hex(Vec), - /// A floating point number, that's neither NaN nor infinite! - Float(f64), + /// A floating point number, that's neither NaN nor infinite. + Float(Float), } impl fmt::Display for Value { @@ -46,8 +45,8 @@ impl FromStr for Value { } } -impl From for Value { - fn from(value: f64) -> Self { Self::Float(value) } +impl> From for Value { + fn from(value: T) -> Self { Self::Float(value.into()) } } impl From> for Value { @@ -62,10 +61,6 @@ impl From<&str> for Value { fn from(value: &str) -> Self { Self::String(unquote(value)) } } -// impl> From for Value { -// fn from(value: T) -> Self { Self::Hex(value.as_ref().into()) } -// } - #[cfg(test)] mod tests { use super::*; @@ -73,7 +68,7 @@ mod tests { #[test] fn test_display() { - assert_eq!(Value::Float(1.1).to_string(), "1.1".to_string()); + assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string()); assert_eq!( Value::String("&str".to_string()).to_string(), "\"&str\"".to_string() @@ -86,7 +81,7 @@ mod tests { #[test] fn test_parser() { - assert_eq!(Value::Float(1.1), "1.1".parse().unwrap()); + assert_eq!(Value::Float(Float::new(1.1)), "1.1".parse().unwrap()); assert_eq!( Value::String("&str".to_string()), "\"&str\"".parse().unwrap() @@ -98,7 +93,7 @@ mod tests { #[test] fn test_from() { - assert_eq!(Value::from(1.0_f64), Value::Float(1.0)); + assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0))); assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string())); assert_eq!( Value::from("&str".to_string()), From afd9e0437cb5443dfdfcb69fd884a8cd0a23bc96 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 24 Feb 2020 16:46:57 +0100 Subject: [PATCH 056/112] change name of github action --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3ea991a..2f88f4d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,4 +1,4 @@ -name: rust +name: Rust # Trigger the workflow on push or pull request on: [push, pull_request] From e338f5f95f385a697f7825a322aec19599443f86 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 8 Mar 2020 10:00:39 +0100 Subject: [PATCH 057/112] finer grained clippy lints --- src/lib.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f673384..5b92abf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,12 +2,30 @@ #![warn( clippy::pedantic, // clippy::nursery, - clippy::cargo + clippy::cargo, + clippy::inline_always, )] #![allow( clippy::multiple_crate_versions, clippy::module_name_repetitions, - clippy::default_trait_access + clippy::default_trait_access, + clippy::unnecessary_operation // temporary until derive-builder uses #[allow(clippy::all)] +)] +#![warn( + clippy::clone_on_ref_ptr, + clippy::decimal_literal_representation, + clippy::get_unwrap, + clippy::option_expect_used, + clippy::unneeded_field_pattern, + clippy::wrong_pub_self_convention +)] +// those should not be present in production code: +#![deny( + clippy::print_stdout, + clippy::todo, + clippy::unimplemented, + clippy::dbg_macro, + clippy::use_debug )] #![warn( missing_docs, From 187174042dba7e3526d64c709e076e719b474c2b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 16 Mar 2020 11:17:52 +0100 Subject: [PATCH 058/112] use chars instead of bytes in the attribute parser --- src/attribute.rs | 146 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 105 insertions(+), 41 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index f60b60c..b3961b1 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -14,7 +14,7 @@ impl<'a> Iterator for AttributePairs<'a> { type Item = (&'a str, &'a str); fn next(&mut self) -> Option { - // return `None`, if there are no more chars + // return `None`, if there are no more bytes self.string.as_bytes().get(self.index + 1)?; let key = { @@ -23,41 +23,55 @@ impl<'a> Iterator for AttributePairs<'a> { // the key ends at an `=`: let end = self .string - .bytes() - .skip(self.index) - .position(|i| i == b'=')? - + start; + .char_indices() + .skip_while(|(i, _)| *i < self.index) + .find_map(|(i, c)| if c == '=' { Some(i) } else { None })?; - // advance the index to the 2nd char after the end of the key - // (this will skip the `=`) + // advance the index to the char after the end of the key (to skip the `=`) + // NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte. self.index = end + 1; - core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + ::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() }; let value = { let start = self.index; - let mut end = 0; // find the end of the value by searching for `,`. // it should ignore `,` that are inside double quotes. let mut inside_quotes = false; - while let Some(item) = self.string.as_bytes().get(start + end) { - end += 1; - if *item == b'"' { - inside_quotes = !inside_quotes; - } else if *item == b',' && !inside_quotes { - self.index += 1; - end -= 1; - break; + let end = { + let mut result = self.string.len(); + + for (i, c) in self + .string + .char_indices() + .skip_while(|(i, _)| *i < self.index) + { + // if a quote is encountered + if c == '"' { + // update variable + inside_quotes = !inside_quotes; + // terminate if a comma is encountered, which is not in a + // quote + } else if c == ',' && !inside_quotes { + // move the index past the comma + self.index += 1; + // the result is the index of the comma (comma is not included in the + // resulting string) + result = i; + break; + } } - } + + result + }; self.index += end; - end += start; + self.index -= start; - core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + ::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() }; Some((key.trim(), value.trim())) @@ -69,10 +83,15 @@ impl<'a> Iterator for AttributePairs<'a> { // each `=` in the remaining str is an iteration // this also ignores `=` inside quotes! let mut inside_quotes = false; - for c in self.string.as_bytes().iter().skip(self.index) { - if *c == b'=' && !inside_quotes { + + for (_, c) in self + .string + .char_indices() + .skip_while(|(i, _)| *i < self.index) + { + if c == '=' && !inside_quotes { remaining += 1; - } else if *c == b'"' { + } else if c == '"' { inside_quotes = !inside_quotes; } } @@ -92,44 +111,53 @@ mod test { #[test] fn test_attributes() { let mut attributes = AttributePairs::new("KEY=VALUE,PAIR=YES"); + assert_eq!((2, Some(2)), attributes.size_hint()); - assert_eq!(Some(("KEY", "VALUE")), attributes.next()); + assert_eq!(attributes.next(), Some(("KEY", "VALUE"))); + assert_eq!((1, Some(1)), attributes.size_hint()); - assert_eq!(Some(("PAIR", "YES")), attributes.next()); + assert_eq!(attributes.next(), Some(("PAIR", "YES"))); + assert_eq!((0, Some(0)), attributes.size_hint()); - assert_eq!(None, attributes.next()); + assert_eq!(attributes.next(), None); let mut attributes = AttributePairs::new("garbage"); assert_eq!((0, Some(0)), attributes.size_hint()); - assert_eq!(None, attributes.next()); + assert_eq!(attributes.next(), None); let mut attributes = AttributePairs::new("KEY=,=VALUE,=,"); + assert_eq!((3, Some(3)), attributes.size_hint()); - assert_eq!(Some(("KEY", "")), attributes.next()); + assert_eq!(attributes.next(), Some(("KEY", ""))); + assert_eq!((2, Some(2)), attributes.size_hint()); - assert_eq!(Some(("", "VALUE")), attributes.next()); + assert_eq!(attributes.next(), Some(("", "VALUE"))); + assert_eq!((1, Some(1)), attributes.size_hint()); - assert_eq!(Some(("", "")), attributes.next()); + assert_eq!(attributes.next(), Some(("", ""))); + assert_eq!((0, Some(0)), attributes.size_hint()); - assert_eq!(None, attributes.next()); + assert_eq!(attributes.next(), None); // test quotes: let mut attributes = AttributePairs::new("KEY=\"VALUE,\","); + assert_eq!((1, Some(1)), attributes.size_hint()); - assert_eq!(Some(("KEY", "\"VALUE,\"")), attributes.next()); + assert_eq!(attributes.next(), Some(("KEY", "\"VALUE,\""))); + assert_eq!((0, Some(0)), attributes.size_hint()); - assert_eq!(None, attributes.next()); + assert_eq!(attributes.next(), None); // test with chars, that are larger, than 1 byte - let mut attributes = AttributePairs::new( - "LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES", - ); + let mut attributes = AttributePairs::new(concat!( + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + )); - assert_eq!(Some(("LANGUAGE", "\"fre\"")), attributes.next()); - assert_eq!(Some(("NAME", "\"Français\"")), attributes.next()); - assert_eq!(Some(("AUTOSELECT", "YES")), attributes.next()); + assert_eq!(attributes.next(), Some(("LANGUAGE", "\"fre\""))); + assert_eq!(attributes.next(), Some(("NAME", "\"Français\""))); + assert_eq!(attributes.next(), Some(("AUTOSELECT", "YES"))); } #[test] @@ -140,5 +168,41 @@ mod test { assert_eq!(pairs.next(), Some(("BAR", "\"baz,qux\""))); assert_eq!(pairs.next(), Some(("ABC", "12.3"))); assert_eq!(pairs.next(), None); + + // stress test with foreign input + // got it from https://generator.lorem-ipsum.info/_chinese + + let mut pairs = AttributePairs::new(concat!( + "載抗留囲軽来実基供全必式覧領意度振。=著地内方満職控努作期投綱研本模,", + "後文図様改表宮能本園半参裁報作神掲索=\"針支年得率新賞現報発援白少動面。矢拉年世掲注索政平定他込\",", + "ध्वनि स्थिति और्४५० नीचे =देखने लाभो द्वारा करके(विशेष" + )); + + assert_eq!((3, Some(3)), pairs.size_hint()); + assert_eq!( + pairs.next(), + Some(( + "載抗留囲軽来実基供全必式覧領意度振。", + "著地内方満職控努作期投綱研本模" + )) + ); + + assert_eq!((2, Some(2)), pairs.size_hint()); + assert_eq!( + pairs.next(), + Some(( + "後文図様改表宮能本園半参裁報作神掲索", + "\"針支年得率新賞現報発援白少動面。矢拉年世掲注索政平定他込\"" + )) + ); + + assert_eq!((1, Some(1)), pairs.size_hint()); + assert_eq!( + pairs.next(), + Some(("ध्वनि स्थिति और्४५० नीचे", "देखने लाभो द्वारा करके(विशेष")) + ); + + assert_eq!((0, Some(0)), pairs.size_hint()); + assert_eq!(pairs.next(), None); } } From 4e41585cbd5e0c450dfca67244090f445b7e6f8a Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 15:39:07 +0100 Subject: [PATCH 059/112] improve ExtXMedia --- src/master_playlist.rs | 6 +- src/tags/master_playlist/media.rs | 423 ++++++++++++++------- src/tags/master_playlist/variant_stream.rs | 4 +- 3 files changed, 282 insertions(+), 151 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index cfc1156..7e6b88a 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -234,9 +234,9 @@ impl MasterPlaylistBuilder { fn check_media_group>(&self, media_type: MediaType, group_id: T) -> bool { if let Some(value) = &self.media { - value - .iter() - .any(|t| t.media_type() == media_type && t.group_id().as_str() == group_id.as_ref()) + value.iter().any(|media| { + media.media_type == media_type && media.group_id().as_str() == group_id.as_ref() + }) } else { false } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index b9aa18a..8444b3d 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -9,69 +9,113 @@ use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::{Error, RequiredVersion}; -/// # [4.4.5.1. EXT-X-MEDIA] +/// An [`ExtXMedia`] tag is an alternative rendition of a [`VariantStream`]. /// -/// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, -/// that contain alternative renditions of the same content. -/// -/// For example, three [`ExtXMedia`] tags can be used to identify audio-only -/// [`MediaPlaylist`]s, that contain English, French, and Spanish renditions -/// of the same presentation. Or, two [`ExtXMedia`] tags can be used to -/// identify video-only [`MediaPlaylist`]s that show two different camera -/// angles. +/// For example an [`ExtXMedia`] tag can be used to specify different audio +/// languages (e.g. english is the default and there also exists an +/// [`ExtXMedia`] stream with a german audio). /// /// [`MediaPlaylist`]: crate::MediaPlaylist -/// [4.4.5.1. EXT-X-MEDIA]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.1 +/// [`VariantStream`]: crate::tags::VariantStream #[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash)] #[shorthand(enable(must_use, into))] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] pub struct ExtXMedia { - /// The [`MediaType`] that is associated with this tag. + /// The [`MediaType`] associated with this tag. /// - /// # Note + /// ### Note /// - /// This attribute is **required**. - #[shorthand(enable(copy))] - media_type: MediaType, + /// This field is required. + #[shorthand(enable(skip))] + pub media_type: MediaType, /// An `URI` to a [`MediaPlaylist`]. /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel"); + /// # assert_eq!(media.uri(), None); + /// + /// media.set_uri(Some("https://www.example.com/stream1.m3u8")); + /// + /// assert_eq!( + /// media.uri(), + /// Some(&"https://www.example.com/stream1.m3u8".to_string()) + /// ); + /// ``` + /// /// # Note /// - /// - This attribute is **required**, if the [`MediaType`] is + /// - This field is required, if the [`ExtXMedia::media_type`] is /// [`MediaType::Subtitles`]. - /// - This attribute is **not allowed**, if the [`MediaType`] is + /// - This field is not allowed, if the [`ExtXMedia::media_type`] is /// [`MediaType::ClosedCaptions`]. /// + /// An absent value indicates that the media data for this rendition is + /// included in the [`MediaPlaylist`] of any + /// [`VariantStream::ExtXStreamInf`] tag with the same `group_id` of + /// this [`ExtXMedia`] instance. + /// /// [`MediaPlaylist`]: crate::MediaPlaylist + /// [`VariantStream::ExtXStreamInf`]: + /// crate::tags::VariantStream::ExtXStreamInf #[builder(setter(strip_option), default)] uri: Option, /// The identifier that specifies the group to which the rendition /// belongs. /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel"); + /// + /// media.set_group_id("ag2"); + /// + /// assert_eq!(media.group_id(), &"ag2".to_string()); + /// ``` + /// /// # Note /// - /// This attribute is **required**. + /// This field is required. group_id: String, /// The name of the primary language used in the rendition. /// The value has to conform to [`RFC5646`]. /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel"); + /// + /// media.set_language(Some("en")); + /// + /// assert_eq!(media.language(), Some(&"en".to_string())); + /// ``` + /// /// # Note /// - /// This attribute is **optional**. + /// This field is optional. /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 #[builder(setter(strip_option), default)] language: Option, /// The name of a language associated with the rendition. /// An associated language is often used in a different role, than the - /// language specified by the [`language`] attribute (e.g., written versus + /// language specified by the [`language`] field (e.g., written versus /// spoken, or a fallback dialect). /// /// # Note /// - /// This attribute is **optional**. + /// This field is optional. /// /// [`language`]: #method.language #[builder(setter(strip_option), default)] @@ -80,9 +124,9 @@ pub struct ExtXMedia { /// /// # Note /// - /// This attribute is **required**. + /// This field is required. /// - /// If the [`language`] attribute is present, this attribute should be in + /// If the [`language`] field is present, this field should be in /// that language. /// /// [`language`]: #method.language @@ -92,61 +136,82 @@ pub struct ExtXMedia { /// this rendition of the content in the absence of information /// from the user indicating a different choice. /// - /// # Note + /// ### Note /// - /// This attribute is **optional**, its absence indicates an implicit value + /// This field is optional, its absence indicates an implicit value /// of `false`. #[builder(default)] - is_default: bool, + #[shorthand(enable(skip))] + pub is_default: bool, /// Whether the client may choose to play this rendition in the absence of /// explicit user preference. /// - /// # Note + /// ### Note /// - /// This attribute is **optional**, its absence indicates an implicit value + /// This field is optional, its absence indicates an implicit value /// of `false`. #[builder(default)] - is_autoselect: bool, + #[shorthand(enable(skip))] + pub is_autoselect: bool, /// Whether the rendition contains content that is considered /// essential to play. #[builder(default)] - is_forced: bool, + #[shorthand(enable(skip))] + pub is_forced: bool, /// An [`InStreamId`] identifies a rendition within the /// [`MediaSegment`]s in a [`MediaPlaylist`]. /// - /// # Note + /// ### Note /// - /// This attribute is required, if the [`ExtXMedia::media_type`] is - /// [`MediaType::ClosedCaptions`]. For all other [`ExtXMedia::media_type`] - /// the [`InStreamId`] must not be specified! + /// This field is required, if the media type is + /// [`MediaType::ClosedCaptions`]. For all other media types the + /// [`InStreamId`] must not be specified! /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MediaSegment`]: crate::MediaSegment #[builder(setter(strip_option), default)] - #[shorthand(enable(copy))] - instream_id: Option, - /// The characteristics attribute, containing one or more Uniform Type - /// Identifiers (UTI) separated by comma. + #[shorthand(enable(skip))] + pub instream_id: Option, + /// The characteristics field contains one or more Uniform Type + /// Identifiers ([`UTI`]) separated by a comma. /// Each [`UTI`] indicates an individual characteristic of the Rendition. /// - /// A [`subtitles`] rendition may include the following characteristics: - /// "public.accessibility.transcribes-spoken-dialog", - /// "public.accessibility.describes-music-and-sound", and - /// "public.easy-to-read" (which indicates that the subtitles have + /// An `ExtXMedia` instance with [`MediaType::Subtitles`] may include the + /// following characteristics: + /// - `"public.accessibility.transcribes-spoken-dialog"`, + /// - `"public.accessibility.describes-music-and-sound"`, and + /// - `"public.easy-to-read"` (which indicates that the subtitles have /// been edited for ease of reading). /// - /// An AUDIO Rendition MAY include the following characteristic: - /// "public.accessibility.describes-video". + /// An `ExtXMedia` instance with [`MediaType::Audio`] may include the + /// following characteristic: + /// - `"public.accessibility.describes-video"` /// - /// The characteristics attribute may include private UTIs. + /// The characteristics field may include private UTIs. + /// + /// # Note + /// + /// This field is optional. /// /// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI - /// [`subtitles`]: crate::types::MediaType::Subtitles #[builder(setter(strip_option), default)] characteristics: Option, - /// The [`Channels`]. + /// A count of audio channels indicating the maximum number of independent, + /// simultaneous audio channels present in any [`MediaSegment`] in the + /// rendition. + /// + /// ### Note + /// + /// This field is optional, but every instance of [`ExtXMedia`] with + /// [`MediaType::Audio`] should have this field. If the [`MasterPlaylist`] + /// contains two renditions with the same codec, but a different number of + /// channels, then the channels field is required. + /// + /// [`MediaSegment`]: crate::MediaSegment + /// [`MasterPlaylist`]: crate::MasterPlaylist #[builder(setter(strip_option), default)] - channels: Option, + #[shorthand(enable(skip))] + pub channels: Option, } impl ExtXMediaBuilder { @@ -168,7 +233,9 @@ impl ExtXMediaBuilder { return Err(Error::missing_attribute("INSTREAM-ID").to_string()); } } else if self.instream_id.is_some() { - return Err(Error::unexpected_attribute("INSTREAM-ID").to_string()); + return Err(Error::custom( + "InStreamId should only be specified for an ExtXMedia tag with `MediaType::ClosedCaptions`" + ).to_string()); } if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { @@ -190,7 +257,21 @@ impl ExtXMediaBuilder { impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; - /// Makes a new [`ExtXMedia`] tag. + /// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the + /// identifier that specifies the group to which the rendition belongs + /// (group id) and a human-readable description of the rendition. If the + /// [`language`] is specified it should be in that language. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let media = ExtXMedia::new(MediaType::Video, "vg1", "1080p video stream"); + /// ``` + /// + /// [`language`]: #method.language #[must_use] pub fn new(media_type: MediaType, group_id: T, name: K) -> Self where @@ -214,10 +295,36 @@ impl ExtXMedia { } /// Returns a builder for [`ExtXMedia`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let media = ExtXMedia::builder() + /// .media_type(MediaType::Subtitles) + /// .uri("french/ed.ttml") + /// .group_id("subs") + /// .language("fra") + /// .assoc_language("fra") + /// .name("French") + /// .is_autoselect(true) + /// .is_forced(true) + /// // concat! joins multiple `&'static str` + /// .characteristics(concat!( + /// "public.accessibility.transcribes-spoken-dialog,", + /// "public.accessibility.describes-music-and-sound" + /// )) + /// .build()?; + /// # Ok::<(), Box>(()) + /// ``` #[must_use] pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() } } +/// This tag requires either `ProtocolVersion::V1` or if there is an +/// `instream_id` it requires it's version. impl RequiredVersion for ExtXMedia { fn required_version(&self) -> ProtocolVersion { self.instream_id @@ -338,10 +445,8 @@ mod test { #[test] fn test_display_and_parse() { - // TODO: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist - macro_rules! generate_tests { - ( $( { $media:expr, $string:tt } ),* $(,)* ) => { + ( $( { $media:expr, $string:expr } ),* $(,)* ) => { $( assert_eq!( $media.to_string(), @@ -368,14 +473,16 @@ mod test { .uri("eng/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"eng/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"eng/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -388,13 +495,15 @@ mod test { .is_autoselect(true) .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"fre/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"fre/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -407,13 +516,15 @@ mod test { .uri("sp/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"sp/prog_index.m3u8\",\ - GROUP-ID=\"audio\",\ - LANGUAGE=\"sp\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"sp/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"sp\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -426,14 +537,16 @@ mod test { .uri("englo/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"englo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"englo/prog_index.m3u8\",", + "GROUP-ID=\"audio-lo\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -446,13 +559,15 @@ mod test { .uri("frelo/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"frelo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"frelo/prog_index.m3u8\",", + "GROUP-ID=\"audio-lo\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -465,13 +580,15 @@ mod test { .uri("splo/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"splo/prog_index.m3u8\",\ - GROUP-ID=\"audio-lo\",\ - LANGUAGE=\"es\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"splo/prog_index.m3u8\",", + "GROUP-ID=\"audio-lo\",", + "LANGUAGE=\"es\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -484,14 +601,16 @@ mod test { .uri("eng/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"eng/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"eng\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"eng/prog_index.m3u8\",", + "GROUP-ID=\"audio-hi\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -504,13 +623,15 @@ mod test { .uri("fre/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"fre/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"fre\",\ - NAME=\"Français\",\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"fre/prog_index.m3u8\",", + "GROUP-ID=\"audio-hi\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -523,13 +644,15 @@ mod test { .uri("sp/prog_index.m3u8") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - URI=\"sp/prog_index.m3u8\",\ - GROUP-ID=\"audio-hi\",\ - LANGUAGE=\"es\",\ - NAME=\"Espanol\",\ - AUTOSELECT=YES" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"sp/prog_index.m3u8\",", + "GROUP-ID=\"audio-hi\",", + "LANGUAGE=\"es\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES" + ) }, { ExtXMedia::builder() @@ -542,14 +665,16 @@ mod test { .channels(Channels::new(2)) .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=AUDIO,\ - GROUP-ID=\"audio-aacl-312\",\ - LANGUAGE=\"en\",\ - NAME=\"English\",\ - DEFAULT=YES,\ - AUTOSELECT=YES,\ - CHANNELS=\"2\"" + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "GROUP-ID=\"audio-aacl-312\",", + "LANGUAGE=\"en\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES,", + "CHANNELS=\"2\"" + ) }, { ExtXMedia::builder() @@ -565,17 +690,21 @@ mod test { -dialog,public.accessibility.describes-music-and-sound") .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=SUBTITLES,\ - URI=\"french/ed.ttml\",\ - GROUP-ID=\"subs\",\ - LANGUAGE=\"fra\",\ - ASSOC-LANGUAGE=\"fra\",\ - NAME=\"French\",\ - AUTOSELECT=YES,\ - FORCED=YES,\ - CHARACTERISTICS=\"public.accessibility.\ - transcribes-spoken-dialog,public.accessibility.describes-music-and-sound\"" + concat!( + "#EXT-X-MEDIA:", + "TYPE=SUBTITLES,", + "URI=\"french/ed.ttml\",", + "GROUP-ID=\"subs\",", + "LANGUAGE=\"fra\",", + "ASSOC-LANGUAGE=\"fra\",", + "NAME=\"French\",", + "AUTOSELECT=YES,", + "FORCED=YES,", + "CHARACTERISTICS=\"", + "public.accessibility.transcribes-spoken-dialog,", + "public.accessibility.describes-music-and-sound", + "\"" + ) }, { ExtXMedia::builder() @@ -587,13 +716,15 @@ mod test { .is_autoselect(true) .build() .unwrap(), - "#EXT-X-MEDIA:\ - TYPE=CLOSED-CAPTIONS,\ - GROUP-ID=\"cc\",\ - LANGUAGE=\"sp\",\ - NAME=\"CC2\",\ - AUTOSELECT=YES,\ - INSTREAM-ID=\"CC2\"" + concat!( + "#EXT-X-MEDIA:", + "TYPE=CLOSED-CAPTIONS,", + "GROUP-ID=\"cc\",", + "LANGUAGE=\"sp\",", + "NAME=\"CC2\",", + "AUTOSELECT=YES,", + "INSTREAM-ID=\"CC2\"" + ) }, { ExtXMedia::new(MediaType::Audio, "foo", "bar"), diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index bc2dde4..4207b14 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -205,7 +205,7 @@ impl VariantStream { pub fn is_associated(&self, media: &ExtXMedia) -> bool { match &self { Self::ExtXIFrame { stream_data, .. } => { - if let MediaType::Video = media.media_type() { + if let MediaType::Video = media.media_type { if let Some(value) = stream_data.video() { return value == media.group_id(); } @@ -220,7 +220,7 @@ impl VariantStream { stream_data, .. } => { - match media.media_type() { + match media.media_type { MediaType::Audio => audio.as_ref().map_or(false, |v| v == media.group_id()), MediaType::Video => { stream_data.video().map_or(false, |v| v == media.group_id()) From 78edff9341bca3f330a9e5e68f1e2632b4c19731 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 15:48:02 +0100 Subject: [PATCH 060/112] improve ExtXSessionKey --- src/tags/master_playlist/session_key.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index f8eedea..a3db6a9 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -9,13 +9,15 @@ use crate::types::{EncryptionMethod, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.4.5. EXT-X-SESSION-KEY] -/// /// The [`ExtXSessionKey`] tag allows encryption keys from [`MediaPlaylist`]s /// to be specified in a [`MasterPlaylist`]. This allows the client to /// preload these keys without having to read the [`MediaPlaylist`]s /// first. /// +/// If an [`ExtXSessionKey`] is used, the values of [`ExtXKey::method`], +/// [`ExtXKey::key_format`] and [`ExtXKey::key_format_versions`] must match any +/// [`ExtXKey`] with the same uri field. +/// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 @@ -70,7 +72,7 @@ impl TryFrom for ExtXSessionKey { } /// This tag requires the same [`ProtocolVersion`] that is returned by -/// `DecryptionKey::required_version`. +/// `ExtXKey::required_version`. impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } From 025add6dc3b809fdadf2f98dc19fcefa465ae855 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 15:54:53 +0100 Subject: [PATCH 061/112] improve VariantStream --- src/tags/master_playlist/variant_stream.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index 4207b14..6492c0d 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -377,6 +377,10 @@ impl Deref for VariantStream { } } +impl PartialEq<&VariantStream> for VariantStream { + fn eq(&self, other: &&Self) -> bool { self.eq(*other) } +} + #[cfg(test)] mod tests { use super::*; From a797e401ed37aa2482536fb12d139c4e93a4e673 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 15:58:43 +0100 Subject: [PATCH 062/112] improve MasterPlaylist --- src/master_playlist.rs | 476 ++++++++++++++++++++++++++++++----------- 1 file changed, 349 insertions(+), 127 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 7e6b88a..272a5b7 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -3,7 +3,6 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; -use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::tags::{ @@ -16,37 +15,112 @@ use crate::{Error, RequiredVersion}; /// The master playlist describes all of the available variants for your /// content. +/// /// Each variant is a version of the stream at a particular bitrate and is -/// contained in a separate playlist. -#[derive(ShortHand, Debug, Clone, Builder, PartialEq)] +/// contained in a separate playlist called [`MediaPlaylist`]. +/// +/// # Examples +/// +/// A [`MasterPlaylist`] can be parsed from a `str`: +/// +/// ``` +/// use core::str::FromStr; +/// use hls_m3u8::MasterPlaylist; +/// +/// // the concat! macro joins multiple `&'static str`. +/// let master_playlist = concat!( +/// "#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::()?; +/// +/// println!("{}", master_playlist.has_independent_segments); +/// # Ok::<(), hls_m3u8::Error>(()) +/// ``` +/// +/// or it can be constructed through a builder +/// +/// ``` +/// # use hls_m3u8::MasterPlaylist; +/// use hls_m3u8::tags::{ExtXStart, VariantStream}; +/// use hls_m3u8::types::{Float, StreamData}; +/// +/// MasterPlaylist::builder() +/// .variant_streams(vec![ +/// VariantStream::ExtXStreamInf { +/// uri: "http://example.com/low/index.m3u8".into(), +/// frame_rate: None, +/// audio: None, +/// subtitles: None, +/// closed_captions: None, +/// stream_data: StreamData::builder() +/// .bandwidth(150000) +/// .codecs(&["avc1.42e00a", "mp4a.40.2"]) +/// .resolution((416, 234)) +/// .build() +/// .unwrap(), +/// }, +/// VariantStream::ExtXStreamInf { +/// uri: "http://example.com/lo_mid/index.m3u8".into(), +/// frame_rate: None, +/// audio: None, +/// subtitles: None, +/// closed_captions: None, +/// stream_data: StreamData::builder() +/// .bandwidth(240000) +/// .codecs(&["avc1.42e00a", "mp4a.40.2"]) +/// .resolution((416, 234)) +/// .build() +/// .unwrap(), +/// }, +/// ]) +/// .has_independent_segments(true) +/// .start(ExtXStart::new(Float::new(1.23))) +/// .build()?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// [`MediaPlaylist`]: crate::MediaPlaylist +#[derive(Debug, Clone, Builder, PartialEq, Default)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] -#[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MasterPlaylist { - /// The [`ExtXIndependentSegments`] tag signals that all media samples in a - /// [`MediaSegment`] can be decoded without information from other segments. + /// Indicates that all media samples in a [`MediaSegment`] can be + /// decoded without information from other segments. /// - /// # Note + /// ### Note /// - /// This tag is optional. - /// - /// If this tag is specified it will apply to every [`MediaSegment`] in - /// every [`MediaPlaylist`] in the [`MasterPlaylist`]. + /// This field is optional and by default `false`. If the field is `true`, + /// it applies to every [`MediaSegment`] in every [`MediaPlaylist`] of this + /// [`MasterPlaylist`]. /// /// [`MediaSegment`]: crate::MediaSegment /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - independent_segments: Option, - /// The [`ExtXStart`] tag indicates a preferred point at which to start - /// playing a Playlist. + pub has_independent_segments: bool, + /// A preferred point at which to start playing a playlist. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional and by default the playlist should be played from + /// the start. #[builder(default)] - start: Option, - /// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, - /// that contain alternative renditions of the same content. + pub start: Option, + /// A list of all [`ExtXMedia`] tags, which describe an alternative + /// rendition. /// /// For example, three [`ExtXMedia`] tags can be used to identify audio-only /// [`MediaPlaylist`]s, that contain English, French, and Spanish @@ -54,76 +128,165 @@ pub struct MasterPlaylist { /// be used to identify video-only [`MediaPlaylist`]s that show two /// different camera angles. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. /// /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - media: Vec, + pub media: Vec, /// A list of all streams of this [`MasterPlaylist`]. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. #[builder(default)] - variants: Vec, + pub variant_streams: Vec, /// The [`ExtXSessionData`] tag allows arbitrary session data to be /// carried in a [`MasterPlaylist`]. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. #[builder(default)] - session_data: Vec, - /// This is a list of [`ExtXSessionKey`]s, that allows the client to preload + pub session_data: Vec, + /// A list of [`ExtXSessionKey`]s, that allows the client to preload /// these keys without having to read the [`MediaPlaylist`]s first. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. /// /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - session_keys: Vec, - /// This is a list of all tags that could not be identified while parsing - /// the input. + pub session_keys: Vec, + /// A list of all tags that could not be identified while parsing the input. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. #[builder(default)] - unknown_tags: Vec, + pub unknown_tags: Vec, + #[builder(default, field(private))] + __non_exhaustive: (), } impl MasterPlaylist { - // TODO: finish builder example! /// Returns a builder for a [`MasterPlaylist`]. /// /// # Example /// /// ``` /// # use hls_m3u8::MasterPlaylist; - /// use hls_m3u8::tags::ExtXStart; - /// use hls_m3u8::types::Float; + /// use hls_m3u8::tags::{ExtXStart, VariantStream}; + /// use hls_m3u8::types::{Float, StreamData}; /// /// MasterPlaylist::builder() - /// .start(ExtXStart::new(Float::new(20.3))) + /// .variant_streams(vec![ + /// VariantStream::ExtXStreamInf { + /// uri: "http://example.com/low/index.m3u8".into(), + /// frame_rate: None, + /// audio: None, + /// subtitles: None, + /// closed_captions: None, + /// stream_data: StreamData::builder() + /// .bandwidth(150000) + /// .codecs(&["avc1.42e00a", "mp4a.40.2"]) + /// .resolution((416, 234)) + /// .build() + /// .unwrap(), + /// }, + /// VariantStream::ExtXStreamInf { + /// uri: "http://example.com/lo_mid/index.m3u8".into(), + /// frame_rate: None, + /// audio: None, + /// subtitles: None, + /// closed_captions: None, + /// stream_data: StreamData::builder() + /// .bandwidth(240000) + /// .codecs(&["avc1.42e00a", "mp4a.40.2"]) + /// .resolution((416, 234)) + /// .build() + /// .unwrap(), + /// }, + /// ]) + /// .has_independent_segments(true) + /// .start(ExtXStart::new(Float::new(1.23))) /// .build()?; /// # Ok::<(), Box>(()) /// ``` #[must_use] #[inline] pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } + + /// Returns all streams, which have an audio group id. + pub fn audio_streams(&self) -> impl Iterator { + self.variant_streams.iter().filter(|stream| { + if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream { + true + } else { + false + } + }) + } + + /// Returns all streams, which have a video group id. + pub fn video_streams(&self) -> impl Iterator { + self.variant_streams.iter().filter(|stream| { + if let VariantStream::ExtXStreamInf { stream_data, .. } = stream { + stream_data.video().is_some() + } else if let VariantStream::ExtXIFrame { stream_data, .. } = stream { + stream_data.video().is_some() + } else { + false + } + }) + } + + /// Returns all streams, which have no group id. + pub fn unassociated_streams(&self) -> impl Iterator { + self.variant_streams.iter().filter(|stream| { + if let VariantStream::ExtXStreamInf { + stream_data, + audio: None, + subtitles: None, + closed_captions: None, + .. + } = stream + { + stream_data.video().is_none() + } else if let VariantStream::ExtXIFrame { stream_data, .. } = stream { + stream_data.video().is_none() + } else { + false + } + }) + } + + /// Returns all `ExtXMedia` tags, associated with the provided stream. + pub fn associated_with<'a>( + &'a self, + stream: &'a VariantStream, + ) -> impl Iterator + 'a { + self.media + .iter() + .filter(move |media| stream.is_associated(media)) + } } impl RequiredVersion for MasterPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ - self.independent_segments, + { + if self.has_independent_segments { + Some(ExtXIndependentSegments) + } else { + None + } + }, self.start, self.media, - self.variants, + self.variant_streams, self.session_data, self.session_keys ] @@ -132,82 +295,72 @@ impl RequiredVersion for MasterPlaylist { impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { - self.validate_variants().map_err(|e| e.to_string())?; + if let Some(variant_streams) = &self.variant_streams { + self.validate_variants(variant_streams) + .map_err(|e| e.to_string())?; + } + self.validate_session_data_tags() .map_err(|e| e.to_string())?; Ok(()) } - fn validate_variants(&self) -> crate::Result<()> { - if let Some(variants) = &self.variants { - self.validate_stream_inf(variants)?; - self.validate_i_frame_stream_inf(variants)?; - } - - Ok(()) - } - - fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { + fn validate_variants(&self, variant_streams: &[VariantStream]) -> crate::Result<()> { let mut closed_captions_none = false; - for t in value { - if let VariantStream::ExtXStreamInf { - audio, - subtitles, - closed_captions, - stream_data, - .. - } = &t - { - if let Some(group_id) = &audio { - if !self.check_media_group(MediaType::Audio, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = &stream_data.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = &subtitles { - if !self.check_media_group(MediaType::Subtitles, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - - if let Some(closed_captions) = &closed_captions { - match &closed_captions { - ClosedCaptions::GroupId(group_id) => { - if closed_captions_none { - return Err(Error::custom( - "If one ClosedCaptions is None all have to be None!", - )); - } - - if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id)); - } + for variant in variant_streams { + match &variant { + VariantStream::ExtXStreamInf { + audio, + subtitles, + closed_captions, + stream_data, + .. + } => { + if let Some(group_id) = &audio { + if !self.check_media_group(MediaType::Audio, group_id) { + return Err(Error::unmatched_group(group_id)); } - _ => { - if !closed_captions_none { - closed_captions_none = true; + } + + if let Some(group_id) = &stream_data.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + + if let Some(group_id) = &subtitles { + if !self.check_media_group(MediaType::Subtitles, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + + if let Some(closed_captions) = &closed_captions { + match &closed_captions { + ClosedCaptions::GroupId(group_id) => { + if closed_captions_none { + return Err(Error::custom("ClosedCaptions has to be `None`")); + } + + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + _ => { + if !closed_captions_none { + closed_captions_none = true; + } } } } } - } - } - Ok(()) - } - - fn validate_i_frame_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { - for t in value { - if let VariantStream::ExtXIFrame { stream_data, .. } = &t { - if let Some(group_id) = stream_data.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); + VariantStream::ExtXIFrame { stream_data, .. } => { + if let Some(group_id) = stream_data.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } } } } @@ -219,12 +372,12 @@ impl MasterPlaylistBuilder { fn validate_session_data_tags(&self) -> crate::Result<()> { let mut set = HashSet::new(); - if let Some(value) = &self.session_data { - set.reserve(value.len()); + if let Some(values) = &self.session_data { + set.reserve(values.len()); - for t in value { - if !set.insert((t.data_id(), t.language())) { - return Err(Error::custom(format!("Conflict: {}", t))); + for tag in values { + if !set.insert((tag.data_id(), tag.language())) { + return Err(Error::custom(format!("conflict: {}", tag))); } } } @@ -250,10 +403,16 @@ impl RequiredVersion for MasterPlaylistBuilder { // not for Option>) // https://github.com/rust-lang/chalk/issues/12 required_version![ - self.independent_segments.flatten(), + { + if self.has_independent_segments.unwrap_or(false) { + Some(ExtXIndependentSegments) + } else { + None + } + }, self.start.flatten(), self.media, - self.variants, + self.variant_streams, self.session_data, self.session_keys ] @@ -272,7 +431,7 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", value)?; } - for value in &self.variants { + for value in &self.variant_streams { writeln!(f, "{}", value)?; } @@ -284,8 +443,8 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", value)?; } - if let Some(value) = &self.independent_segments { - writeln!(f, "{}", value)?; + if self.has_independent_segments { + writeln!(f, "{}", ExtXIndependentSegments)?; } if let Some(value) = &self.start { @@ -308,7 +467,7 @@ impl FromStr for MasterPlaylist { let mut builder = Self::builder(); let mut media = vec![]; - let mut variants = vec![]; + let mut variant_streams = vec![]; let mut session_data = vec![]; let mut session_keys = vec![]; let mut unknown_tags = vec![]; @@ -336,16 +495,13 @@ impl FromStr for MasterPlaylist { | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { - return Err(Error::custom(format!( - "This tag isn't allowed in a master playlist: {}", - tag - ))); + return Err(Error::unexpected_tag(tag)); } Tag::ExtXMedia(t) => { media.push(t); } Tag::VariantStream(t) => { - variants.push(t); + variant_streams.push(t); } Tag::ExtXSessionData(t) => { session_data.push(t); @@ -353,8 +509,8 @@ impl FromStr for MasterPlaylist { Tag::ExtXSessionKey(t) => { session_keys.push(t); } - Tag::ExtXIndependentSegments(t) => { - builder.independent_segments(t); + Tag::ExtXIndependentSegments(_) => { + builder.has_independent_segments(true); } Tag::ExtXStart(t) => { builder.start(t); @@ -367,14 +523,14 @@ impl FromStr for MasterPlaylist { } } Line::Uri(uri) => { - return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); + return Err(Error::custom(format!("unexpected uri: {:?}", uri))); } _ => {} } } builder.media(media); - builder.variants(variants); + builder.variant_streams(variant_streams); builder.session_data(session_data); builder.session_keys(session_keys); builder.unknown_tags(unknown_tags); @@ -389,6 +545,72 @@ mod tests { use crate::types::StreamData; use pretty_assertions::assert_eq; + #[test] + fn test_audio_streams() { + let astreams = vec![ + VariantStream::ExtXStreamInf { + uri: "http://example.com/low/index.m3u8".into(), + frame_rate: None, + audio: Some("ag0".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(150000) + .codecs(&["avc1.42e00a", "mp4a.40.2"]) + .resolution((416, 234)) + .build() + .unwrap(), + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/lo_mid/index.m3u8".into(), + frame_rate: None, + audio: Some("ag1".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(240000) + .codecs(&["avc1.42e00a", "mp4a.40.2"]) + .resolution((416, 234)) + .build() + .unwrap(), + }, + ]; + + let master_playlist = MasterPlaylist::builder() + .variant_streams(astreams.clone()) + .media(vec![ + ExtXMedia::builder() + .media_type(MediaType::Audio) + .uri("https://www.example.com/ag0.m3u8") + .group_id("ag0") + .language("english") + .name("alternative rendition for ag0") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .uri("https://www.example.com/ag1.m3u8") + .group_id("ag1") + .language("english") + .name("alternative rendition for ag1") + .build() + .unwrap(), + ]) + .build() + .unwrap(); + + assert_eq!( + master_playlist.variant_streams, + master_playlist.audio_streams().collect::>() + ); + + let mut audio_streams = master_playlist.audio_streams(); + + assert_eq!(audio_streams.next(), Some(&astreams[0])); + assert_eq!(audio_streams.next(), Some(&astreams[1])); + assert_eq!(audio_streams.next(), None); + } + #[test] fn test_parser() { assert_eq!( @@ -412,7 +634,7 @@ mod tests { .parse::() .unwrap(), MasterPlaylist::builder() - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "http://example.com/low/index.m3u8".into(), frame_rate: None, @@ -487,7 +709,7 @@ mod tests { fn test_display() { assert_eq!( MasterPlaylist::builder() - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "http://example.com/low/index.m3u8".into(), frame_rate: None, From ff807940b26819d8c13326e543ed15d832948a2c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 15:58:59 +0100 Subject: [PATCH 063/112] add more tests --- tests/master_playlist.rs | 129 ++++++++++++++++++ tests/media_playlist.rs | 288 +++++++++++++++++++++++++++++++++++++-- tests/rfc8216.rs | 12 +- 3 files changed, 412 insertions(+), 17 deletions(-) create mode 100644 tests/master_playlist.rs diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs new file mode 100644 index 0000000..5a6cd4c --- /dev/null +++ b/tests/master_playlist.rs @@ -0,0 +1,129 @@ +use hls_m3u8::tags::{ExtXMedia, VariantStream}; +use hls_m3u8::types::{MediaType, StreamData}; +use hls_m3u8::MasterPlaylist; + +use pretty_assertions::assert_eq; + +macro_rules! generate_tests { + ( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => { + $( + #[test] + fn $fnname() { + assert_eq!($struct, $str.parse().unwrap()); + + assert_eq!($struct.to_string(), $str.to_string()); + } + )+ + } +} + +generate_tests! { + test_alternate_audio => { + MasterPlaylist::builder() + .media(vec![ + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("sp") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + ]) + .variant_streams(vec![ + VariantStream::ExtXStreamInf { + uri: "lo/prog_index.m3u8".into(), + frame_rate: None, + audio: Some("audio".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(195023) + .codecs(&["avc1.42e00a", "mp4a.40.2"]) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "hi/prog_index.m3u8".into(), + frame_rate: None, + audio: Some("audio".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(591680) + .codecs(&["avc1.42e01e", "mp4a.40.2"]) + .build() + .unwrap() + } + ]) + .build() + .unwrap(), + concat!( + "#EXTM3U\n", + + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"eng/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES", + "\n", + + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"fre/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES", + "\n", + + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"sp/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"sp\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES", + "\n", + + "#EXT-X-STREAM-INF:", + "BANDWIDTH=195023,", + "CODECS=\"avc1.42e00a,mp4a.40.2\",", + "AUDIO=\"audio\"", + "\n", + "lo/prog_index.m3u8\n", + + "#EXT-X-STREAM-INF:", + "BANDWIDTH=591680,", + "CODECS=\"avc1.42e01e,mp4a.40.2\",", + "AUDIO=\"audio\"", + "\n", + "hi/prog_index.m3u8\n" + ) + } +} diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index c43c131..126b0c5 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -1,12 +1,31 @@ +//! Some tests of this file are from +//! +//! +//! TODO: the rest of the tests + use std::time::Duration; -use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration}; +use hls_m3u8::tags::{ + ExtInf, ExtXByteRange, ExtXEndList, ExtXMediaSequence, ExtXPlaylistType, ExtXTargetDuration, +}; use hls_m3u8::{MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; -#[test] -fn test_media_playlist_with_byterange() { - assert_eq!( +macro_rules! generate_tests { + ( $( $fnname:ident => { $struct:expr, $str:expr }),+ $(,)* ) => { + $( + #[test] + fn $fnname() { + assert_eq!($struct, $str.parse().unwrap()); + + assert_eq!($struct.to_string(), $str.to_string()); + } + )+ + } +} + +generate_tests! { + test_media_playlist_with_byterange => { MediaPlaylist::builder() .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) .media_sequence(ExtXMediaSequence::new(0)) @@ -34,20 +53,267 @@ fn test_media_playlist_with_byterange() { .unwrap(), concat!( "#EXTM3U\n", - "#EXT-X-TARGETDURATION:10\n", "#EXT-X-VERSION:4\n", + "#EXT-X-TARGETDURATION:10\n", "#EXT-X-MEDIA-SEQUENCE:0\n", - "#EXTINF:10.0,\n", + "#EXT-X-BYTERANGE:75232@0\n", + "#EXTINF:10,\n", "video.ts\n", + "#EXT-X-BYTERANGE:82112@752321\n", - "#EXTINF:10.0,\n", + "#EXTINF:10,\n", "video.ts\n", - "#EXTINF:10.0,\n", + "#EXT-X-BYTERANGE:69864\n", + "#EXTINF:10,\n", "video.ts\n" ) - .parse::() - .unwrap() - ) + }, + test_absolute_uris => { + MediaPlaylist::builder() + .playlist_type(ExtXPlaylistType::Vod) + .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .segments(vec![ + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .uri("http://example.com/00001.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .uri("https://example.com/00002.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .uri("//example.com/00003.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .uri("http://example.com/00004.ts") + .build() + .unwrap(), + ]) + // TODO: currently this is treated as a comment + // .unknown(vec![ + // "#ZEN-TOTAL-DURATION:57.9911".into() + // ]) + .end_list(ExtXEndList) + .build() + .unwrap(), + concat!( + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:10\n", + "#EXT-X-PLAYLIST-TYPE:VOD\n", + "#EXTINF:10,\n", + "http://example.com/00001.ts\n", + "#EXTINF:10,\n", + "https://example.com/00002.ts\n", + "#EXTINF:10,\n", + "//example.com/00003.ts\n", + "#EXTINF:10,\n", + "http://example.com/00004.ts\n", + //"#ZEN-TOTAL-DURATION:57.9911\n", + "#EXT-X-ENDLIST\n" + ) + }, + test_allow_cache => { + MediaPlaylist::builder() + .target_duration(Duration::from_secs(10)) + .media_sequence(0) + .playlist_type(ExtXPlaylistType::Vod) + .segments(vec![ + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .uri("hls_450k_video.ts") + .byte_range(0..522_828) + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(522_828..1_110_328) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(1_110_328..1_823_412) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(1_823_412..2_299_992) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(2_299_992..2_835_604) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(2_835_604..3_042_780) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(3_042_780..3_498_680) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(3_498_680..4_155_928) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(4_155_928..4_727_636) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(4_727_636..5_212_676) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(5_212_676..5_921_812) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(5_921_812..6_651_816) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(6_651_816..7_108_092) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(7_108_092..7_576_776) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(7_576_776..8_021_772) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs(10))) + .byte_range(8_021_772..8_353_216) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf(ExtInf::new(Duration::from_secs_f64(1.4167))) + .byte_range(8_353_216..8_397_772) + .uri("hls_450k_video.ts") + .build() + .unwrap(), + ]) + .end_list(ExtXEndList) + .unknown(vec![ + // deprecated tag: + "#EXT-X-ALLOW-CACHE:YES".into() + ]) + .build() + .unwrap(), + concat!( + "#EXTM3U\n", + "#EXT-X-VERSION:4\n", + "#EXT-X-TARGETDURATION:10\n", + "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXT-X-PLAYLIST-TYPE:VOD\n", + + "#EXT-X-BYTERANGE:522828@0\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:587500@522828\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:713084@1110328\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:476580@1823412\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:535612@2299992\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:207176@2835604\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:455900@3042780\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:657248@3498680\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:571708@4155928\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:485040@4727636\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:709136@5212676\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:730004@5921812\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:456276@6651816\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:468684@7108092\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:444996@7576776\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:331444@8021772\n", + "#EXTINF:10,\n", + "hls_450k_video.ts\n", + + "#EXT-X-BYTERANGE:44556@8353216\n", + "#EXTINF:1.4167,\n", + "hls_450k_video.ts\n", + + "#EXT-X-ALLOW-CACHE:YES\n", + "#EXT-X-ENDLIST\n" + ) + }, + } diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index 707329a..cae2c2c 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -21,7 +21,7 @@ macro_rules! generate_tests { } } -generate_tests! [ +generate_tests! { test_simple_playlist => { MediaPlaylist::builder() .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) @@ -169,7 +169,7 @@ generate_tests! [ }, test_master_playlist => { MasterPlaylist::builder() - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "http://example.com/low.m3u8".into(), frame_rate: None, @@ -235,7 +235,7 @@ generate_tests! [ }, test_master_playlist_with_i_frames => { MasterPlaylist::builder() - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "low/audio-video.m3u8".into(), frame_rate: None, @@ -336,7 +336,7 @@ generate_tests! [ .build() .unwrap(), ]) - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "low/video-only.m3u8".into(), frame_rate: None, @@ -509,7 +509,7 @@ generate_tests! [ .build() .unwrap(), ]) - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "low/main/audio-video.m3u8".into(), frame_rate: None, @@ -628,4 +628,4 @@ generate_tests! [ "hi/main/audio-video.m3u8\n", ) } -]; +} From 1b016752506294097693a9c05e7b4eb19e07f902 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 16:13:38 +0100 Subject: [PATCH 064/112] improve ExtXMap --- src/tags/media_segment/map.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index e5d5d0c..1161289 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -9,13 +9,28 @@ use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; use crate::{Encrypted, Error, RequiredVersion}; -/// # [4.3.2.5. EXT-X-MAP] +/// The [`ExtXMap`] tag specifies how to obtain the [Media Initialization +/// Section], required to parse the applicable [`MediaSegment`]s. /// -/// The [`ExtXMap`] tag specifies how to obtain the Media Initialization -/// Section, required to parse the applicable [`MediaSegment`]s. +/// It applies to every [`MediaSegment`] that appears after it in the playlist +/// until the next [`ExtXMap`] tag or until the end of the playlist. /// +/// An [`ExtXMap`] tag should be supplied for [`MediaSegment`]s in playlists +/// with the [`ExtXIFramesOnly`] tag when the first [`MediaSegment`] (i.e., +/// I-frame) in the playlist (or the first segment following an +/// [`ExtXDiscontinuity`] tag) does not immediately follow the Media +/// Initialization Section at the beginning of its resource. +/// +/// If the Media Initialization Section declared by an [`ExtXMap`] tag is +/// encrypted with [`EncryptionMethod::Aes128`], the IV attribute of +/// the [`ExtXKey`] tag that applies to the [`ExtXMap`] is required. +/// +/// [Media Initialization Section]: https://tools.ietf.org/html/rfc8216#section-3 /// [`MediaSegment`]: crate::MediaSegment -/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 +/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly +/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity +/// [`EncryptionMethod::Aes128`]: crate::types::EncryptionMethod::Aes128 +/// [`MediaPlaylist`]: crate::MediaPlaylist #[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[shorthand(enable(must_use, into))] pub struct ExtXMap { @@ -99,7 +114,14 @@ impl Encrypted for ExtXMap { fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } } -/// This tag requires [`ProtocolVersion::V6`]. +/// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the +/// [`ExtXIFramesOnly`] tag requires [`ProtocolVersion::V5`] or +/// greater. Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that does not +/// contain the [`ExtXIFramesOnly`] tag requires [`ProtocolVersion::V6`] or +/// greater. +/// +/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly +/// [`MediaPlaylist`]: crate::MediaPlaylist impl RequiredVersion for ExtXMap { // this should return ProtocolVersion::V5, if it does not contain an // EXT-X-I-FRAMES-ONLY! From b2fb58559c1379dfae9f9a6fa0420d9cd9ed9709 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 20 Mar 2020 12:05:16 +0100 Subject: [PATCH 065/112] make chrono optional #49 --- .travis.yml | 1 + Cargo.toml | 10 +- src/error.rs | 2 + src/tags/media_segment/date_range.rs | 182 +++++++++++++++----- src/tags/media_segment/program_date_time.rs | 164 ++++++++++-------- 5 files changed, 235 insertions(+), 124 deletions(-) diff --git a/.travis.yml b/.travis.yml index e276f81..fb78ec8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ script: - cargo clean - cargo build - cargo test + - cargo test --features chrono # it's enough to run this once: - | diff --git a/Cargo.toml b/Cargo.toml index ff1ef16..796490f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,16 @@ codecov = { repository = "sile/hls_m3u8" } travis-ci = { repository = "sile/hls_m3u8" } [dependencies] -chrono = "0.4" +chrono = { version = "0.4", optional = true } +backtrace = { version = "0.3", features = ["std"], optional = true } + derive_builder = "0.9" -derive_more = "0.99" hex = "0.4" +thiserror = "1.0" + +derive_more = "0.99" shorthand = "0.1" strum = { version = "0.17", features = ["derive"] } -thiserror = "1.0" -backtrace = { version = "0.3", features = ["std"], optional = true } [dev-dependencies] pretty_assertions = "0.6" diff --git a/src/error.rs b/src/error.rs index bba33d1..85fc169 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,7 @@ enum ErrorKind { UnexpectedTag { tag: String }, #[error("{source}")] + #[cfg(feature = "chrono")] Chrono { source: chrono::ParseError }, #[error("builder error: {message}")] @@ -169,6 +170,7 @@ impl Error { } // third party crates: + #[cfg(feature = "chrono")] pub(crate) fn chrono(source: chrono::format::ParseError) -> Self { Self::new(ErrorKind::Chrono { source }) } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 061489d..ce6f71a 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -3,6 +3,7 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; +#[cfg(feature = "chrono")] use chrono::{DateTime, FixedOffset, SecondsFormat}; use derive_builder::Builder; use shorthand::ShortHand; @@ -12,13 +13,8 @@ use crate::types::{ProtocolVersion, Value}; use crate::utils::{quote, tag, unquote}; 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 +/// 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. #[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd)] #[builder(setter(into))] #[shorthand(enable(must_use, into))] @@ -43,6 +39,7 @@ pub struct ExtXDateRange { /// # Note /// /// This attribute is required. + #[cfg(feature = "chrono")] start_date: DateTime, /// The date at which the [`ExtXDateRange`] ends. It must be equal to or /// later than the value of the [`start-date`] attribute. @@ -52,8 +49,12 @@ pub struct ExtXDateRange { /// This attribute is optional. /// /// [`start-date`]: #method.start_date + #[cfg(feature = "chrono")] #[builder(setter(strip_option), default)] end_date: Option>, + #[cfg(not(feature = "chrono"))] + #[builder(setter(strip_option), default)] + end_date: Option, /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., /// crossing a finish line) should be represented with a duration of 0. /// @@ -145,27 +146,48 @@ impl ExtXDateRange { /// Makes a new [`ExtXDateRange`] tag. /// /// # 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), - /// ); - /// ``` + #[cfg_attr( + feature = "chrono", + doc = r#" +``` +# 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), +); +``` +"# + )] + #[cfg_attr( + not(feature = "chrono"), + doc = r#" +``` +# use hls_m3u8::tags::ExtXDateRange; + +let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00"); +``` + "# + )] #[must_use] - pub fn new>(id: T, start_date: DateTime) -> Self { + pub fn new, #[cfg(not(feature = "chrono"))] I: Into>( + id: T, + #[cfg(feature = "chrono")] start_date: DateTime, + #[cfg(not(feature = "chrono"))] start_date: I, + ) -> Self { Self { id: id.into(), class: None, + #[cfg(feature = "chrono")] start_date, + #[cfg(not(feature = "chrono"))] + start_date: start_date.into(), end_date: None, duration: None, planned_duration: None, @@ -210,8 +232,26 @@ impl FromStr for ExtXDateRange { match key { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), - "START-DATE" => start_date = Some(unquote(value)), - "END-DATE" => end_date = Some(unquote(value).parse().map_err(Error::chrono)?), + "START-DATE" => { + #[cfg(feature = "chrono")] + { + start_date = Some(unquote(value).parse().map_err(Error::chrono)?) + } + #[cfg(not(feature = "chrono"))] + { + start_date = Some(unquote(value)) + } + } + "END-DATE" => { + #[cfg(feature = "chrono")] + { + end_date = Some(unquote(value).parse().map_err(Error::chrono)?) + } + #[cfg(not(feature = "chrono"))] + { + end_date = Some(unquote(value)) + } + } "DURATION" => { duration = Some(Duration::from_secs_f64( value.parse().map_err(|e| Error::parse_float(value, e))?, @@ -244,10 +284,7 @@ impl FromStr for ExtXDateRange { } let id = id.ok_or_else(|| Error::missing_value("ID"))?; - let start_date = start_date - .ok_or_else(|| Error::missing_value("START-DATE"))? - .parse() - .map_err(Error::chrono)?; + let start_date = start_date.ok_or_else(|| Error::missing_value("START-DATE"))?; if end_on_next && class.is_none() { return Err(Error::invalid_input()); @@ -277,20 +314,36 @@ impl fmt::Display for ExtXDateRange { 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 { + #[cfg(feature = "chrono")] + { write!( f, - ",END-DATE={}", - quote(&value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + ",START-DATE={}", + quote(&self.start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true)) )?; } + #[cfg(not(feature = "chrono"))] + { + write!(f, ",START-DATE={}", quote(&self.start_date))?; + } + + if let Some(value) = &self.end_date { + #[cfg(feature = "chrono")] + { + write!( + f, + ",END-DATE={}", + quote(&value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + } + + #[cfg(not(feature = "chrono"))] + { + write!(f, ",END-DATE={}", quote(&value))?; + } + } + if let Some(value) = &self.duration { write!(f, ",DURATION={}", value.as_secs_f64())?; } @@ -327,9 +380,11 @@ impl fmt::Display for ExtXDateRange { mod test { use super::*; use crate::types::Float; + #[cfg(feature = "chrono")] use chrono::offset::TimeZone; use pretty_assertions::assert_eq; + #[cfg(feature = "chrono")] const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds macro_rules! generate_tests { @@ -369,7 +424,16 @@ mod test { { ExtXDateRange::builder() .id("splice-6FFFFFF0") - .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .start_date({ + #[cfg(feature = "chrono")] + { + FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0) + } + #[cfg(not(feature = "chrono"))] + { + "2014-03-05T11:15:00Z" + } + }) .planned_duration(Duration::from_secs_f64(59.993)) .scte35_out(concat!( "0xFC002F0000000000FF00001", @@ -393,8 +457,26 @@ mod test { 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)) + .start_date({ + #[cfg(feature = "chrono")] + { + FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0) + } + #[cfg(not(feature = "chrono"))] + { + "2014-03-05T11:15:00Z" + } + }) + .end_date({ + #[cfg(feature = "chrono")] + { + FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0) + } + #[cfg(not(feature = "chrono"))] + { + "2014-03-05T11:16:00Z" + } + }) .duration(Duration::from_secs_f64(60.1)) .planned_duration(Duration::from_secs_f64(59.993)) .insert_client_attribute("X-CUSTOM", Float::new(45.3)) @@ -424,12 +506,18 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXDateRange::new( - "id", - FixedOffset::east(8 * HOURS_IN_SECS) - .ymd(2010, 2, 19) - .and_hms_milli(14, 54, 23, 31) - ) + ExtXDateRange::new("id", { + #[cfg(feature = "chrono")] + { + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + } + #[cfg(not(feature = "chrono"))] + { + "2010-02-19T14:54:23.031+08:00" + } + }) .required_version(), ProtocolVersion::V1 ); diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 36caca7..6352172 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,7 +1,9 @@ use std::fmt; use std::str::FromStr; +#[cfg(feature = "chrono")] use chrono::{DateTime, FixedOffset, SecondsFormat}; +#[cfg(feature = "chrono")] use derive_more::{Deref, DerefMut}; use crate::types::ProtocolVersion; @@ -16,8 +18,18 @@ use crate::{Error, RequiredVersion}; /// [`MediaSegment`]: crate::MediaSegment /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: /// https://tools.ietf.org/html/rfc8216#section-4.3.2.6 -#[derive(Deref, DerefMut, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ExtXProgramDateTime(DateTime); +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))] +pub struct ExtXProgramDateTime { + /// The date-time of the first sample of the associated media segment. + #[cfg(feature = "chrono")] + #[cfg_attr(feature = "chrono", deref_mut, deref)] + pub date_time: DateTime, + /// The date-time of the first sample of the associated media segment. + #[cfg(not(feature = "chrono"))] + pub date_time: String, + __non_exhaustive: (), +} impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; @@ -39,67 +51,28 @@ impl ExtXProgramDateTime { /// ); /// ``` #[must_use] - pub const fn new(date_time: DateTime) -> Self { Self(date_time) } + #[cfg(feature = "chrono")] + pub const fn new(date_time: DateTime) -> Self { + Self { + date_time, + __non_exhaustive: (), + } + } - /// Returns the date-time of the first sample of the associated media - /// segment. + /// Makes a new [`ExtXProgramDateTime`] tag. /// /// # 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) - /// ); + /// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00"); /// ``` - #[must_use] - pub const fn date_time(&self) -> DateTime { 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) -> &mut Self { - self.0 = value; - self + #[cfg(not(feature = "chrono"))] + pub fn new>(date_time: T) -> Self { + Self { + date_time: date_time.into(), + __non_exhaustive: (), + } } } @@ -110,7 +83,16 @@ impl RequiredVersion for ExtXProgramDateTime { impl fmt::Display for ExtXProgramDateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let date_time = self.0.to_rfc3339_opts(SecondsFormat::Millis, true); + let date_time = { + #[cfg(feature = "chrono")] + { + self.date_time.to_rfc3339_opts(SecondsFormat::Millis, true) + } + #[cfg(not(feature = "chrono"))] + { + &self.date_time + } + }; write!(f, "{}{}", Self::PREFIX, date_time) } } @@ -121,7 +103,17 @@ impl FromStr for ExtXProgramDateTime { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - let date_time = DateTime::parse_from_rfc3339(input).map_err(Error::chrono)?; + let date_time = { + #[cfg(feature = "chrono")] + { + DateTime::parse_from_rfc3339(input).map_err(Error::chrono)? + } + #[cfg(not(feature = "chrono"))] + { + input + } + }; + Ok(Self::new(date_time)) } } @@ -129,20 +121,30 @@ impl FromStr for ExtXProgramDateTime { #[cfg(test)] mod test { use super::*; + #[cfg(feature = "chrono")] use chrono::{Datelike, TimeZone}; + #[cfg(feature = "chrono")] use core::ops::DerefMut; use pretty_assertions::assert_eq; + #[cfg(feature = "chrono")] const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds #[test] fn test_display() { assert_eq!( - ExtXProgramDateTime::new( - FixedOffset::east(8 * HOURS_IN_SECS) - .ymd(2010, 2, 19) - .and_hms_milli(14, 54, 23, 31) - ) + ExtXProgramDateTime::new({ + #[cfg(feature = "chrono")] + { + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + } + #[cfg(not(feature = "chrono"))] + { + "2010-02-19T14:54:23.031+08:00" + } + }) .to_string(), "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string() ); @@ -151,11 +153,18 @@ mod test { #[test] fn test_parser() { assert_eq!( - ExtXProgramDateTime::new( - FixedOffset::east(8 * HOURS_IN_SECS) - .ymd(2010, 2, 19) - .and_hms_milli(14, 54, 23, 31) - ), + ExtXProgramDateTime::new({ + #[cfg(feature = "chrono")] + { + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + } + #[cfg(not(feature = "chrono"))] + { + "2010-02-19T14:54:23.031+08:00" + } + }), "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00" .parse::() .unwrap() @@ -165,17 +174,25 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXProgramDateTime::new( - FixedOffset::east(8 * HOURS_IN_SECS) - .ymd(2010, 2, 19) - .and_hms_milli(14, 54, 23, 31), - ) + ExtXProgramDateTime::new({ + #[cfg(feature = "chrono")] + { + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + } + #[cfg(not(feature = "chrono"))] + { + "2010-02-19T14:54:23.031+08:00" + } + }) .required_version(), ProtocolVersion::V1 ); } #[test] + #[cfg(feature = "chrono")] fn test_deref() { assert_eq!( ExtXProgramDateTime::new( @@ -189,6 +206,7 @@ mod test { } #[test] + #[cfg(feature = "chrono")] fn test_deref_mut() { assert_eq!( ExtXProgramDateTime::new( From 02d363daa1e61285bffc73b9c3491927125cb623 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 23 Mar 2020 12:00:02 +0100 Subject: [PATCH 066/112] slight changes to tests --- src/tags/master_playlist/media.rs | 565 +++++++++++++++--------------- 1 file changed, 282 insertions(+), 283 deletions(-) diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 8444b3d..a0e57f0 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -443,294 +443,293 @@ mod test { use super::*; use pretty_assertions::assert_eq; - #[test] - fn test_display_and_parse() { - macro_rules! generate_tests { - ( $( { $media:expr, $string:expr } ),* $(,)* ) => { + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { $( - assert_eq!( - $media.to_string(), - $string.to_string() - ); + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } - assert_eq!( - $media, - $string.parse::().unwrap(), - ); - )* + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ } } + } - generate_tests! { - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("eng/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"eng/prog_index.m3u8\",", - "GROUP-ID=\"audio\",", - "LANGUAGE=\"eng\",", - "NAME=\"English\",", - "DEFAULT=YES,", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .uri("fre/prog_index.m3u8") - .group_id("audio") - .language("fre") - .name("Français") - .is_default(false) - .is_autoselect(true) - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"fre/prog_index.m3u8\",", - "GROUP-ID=\"audio\",", - "LANGUAGE=\"fre\",", - "NAME=\"Français\",", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio") - .language("sp") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("sp/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"sp/prog_index.m3u8\",", - "GROUP-ID=\"audio\",", - "LANGUAGE=\"sp\",", - "NAME=\"Espanol\",", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("englo/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"englo/prog_index.m3u8\",", - "GROUP-ID=\"audio-lo\",", - "LANGUAGE=\"eng\",", - "NAME=\"English\",", - "DEFAULT=YES,", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("frelo/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"frelo/prog_index.m3u8\",", - "GROUP-ID=\"audio-lo\",", - "LANGUAGE=\"fre\",", - "NAME=\"Français\",", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-lo") - .language("es") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("splo/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"splo/prog_index.m3u8\",", - "GROUP-ID=\"audio-lo\",", - "LANGUAGE=\"es\",", - "NAME=\"Espanol\",", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("eng") - .name("English") - .is_autoselect(true) - .is_default(true) - .uri("eng/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"eng/prog_index.m3u8\",", - "GROUP-ID=\"audio-hi\",", - "LANGUAGE=\"eng\",", - "NAME=\"English\",", - "DEFAULT=YES,", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("fre") - .name("Français") - .is_autoselect(true) - .is_default(false) - .uri("fre/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"fre/prog_index.m3u8\",", - "GROUP-ID=\"audio-hi\",", - "LANGUAGE=\"fre\",", - "NAME=\"Français\",", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-hi") - .language("es") - .name("Espanol") - .is_autoselect(true) - .is_default(false) - .uri("sp/prog_index.m3u8") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "URI=\"sp/prog_index.m3u8\",", - "GROUP-ID=\"audio-hi\",", - "LANGUAGE=\"es\",", - "NAME=\"Espanol\",", - "AUTOSELECT=YES" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Audio) - .group_id("audio-aacl-312") - .language("en") - .name("English") - .is_autoselect(true) - .is_default(true) - .channels(Channels::new(2)) - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=AUDIO,", - "GROUP-ID=\"audio-aacl-312\",", - "LANGUAGE=\"en\",", - "NAME=\"English\",", - "DEFAULT=YES,", - "AUTOSELECT=YES,", - "CHANNELS=\"2\"" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::Subtitles) - .uri("french/ed.ttml") - .group_id("subs") - .language("fra") - .assoc_language("fra") - .name("French") - .is_autoselect(true) - .is_forced(true) - .characteristics("public.accessibility.transcribes-spoken\ - -dialog,public.accessibility.describes-music-and-sound") - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=SUBTITLES,", - "URI=\"french/ed.ttml\",", - "GROUP-ID=\"subs\",", - "LANGUAGE=\"fra\",", - "ASSOC-LANGUAGE=\"fra\",", - "NAME=\"French\",", - "AUTOSELECT=YES,", - "FORCED=YES,", - "CHARACTERISTICS=\"", - "public.accessibility.transcribes-spoken-dialog,", - "public.accessibility.describes-music-and-sound", - "\"" - ) - }, - { - ExtXMedia::builder() - .media_type(MediaType::ClosedCaptions) - .group_id("cc") - .language("sp") - .name("CC2") - .instream_id(InStreamId::Cc2) - .is_autoselect(true) - .build() - .unwrap(), - concat!( - "#EXT-X-MEDIA:", - "TYPE=CLOSED-CAPTIONS,", - "GROUP-ID=\"cc\",", - "LANGUAGE=\"sp\",", - "NAME=\"CC2\",", - "AUTOSELECT=YES,", - "INSTREAM-ID=\"CC2\"" - ) - }, - { - ExtXMedia::new(MediaType::Audio, "foo", "bar"), - "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" - }, - }; + generate_tests! { + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"eng/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .uri("fre/prog_index.m3u8") + .group_id("audio") + .language("fre") + .name("Français") + .is_default(false) + .is_autoselect(true) + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"fre/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("sp") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"sp/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"sp\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("englo/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"englo/prog_index.m3u8\",", + "GROUP-ID=\"audio-lo\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("frelo/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"frelo/prog_index.m3u8\",", + "GROUP-ID=\"audio-lo\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("splo/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"splo/prog_index.m3u8\",", + "GROUP-ID=\"audio-lo\",", + "LANGUAGE=\"es\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"eng/prog_index.m3u8\",", + "GROUP-ID=\"audio-hi\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"fre/prog_index.m3u8\",", + "GROUP-ID=\"audio-hi\",", + "LANGUAGE=\"fre\",", + "NAME=\"Français\",", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"sp/prog_index.m3u8\",", + "GROUP-ID=\"audio-hi\",", + "LANGUAGE=\"es\",", + "NAME=\"Espanol\",", + "AUTOSELECT=YES" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-aacl-312") + .language("en") + .name("English") + .is_autoselect(true) + .is_default(true) + .channels(Channels::new(2)) + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "GROUP-ID=\"audio-aacl-312\",", + "LANGUAGE=\"en\",", + "NAME=\"English\",", + "DEFAULT=YES,", + "AUTOSELECT=YES,", + "CHANNELS=\"2\"" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::Subtitles) + .uri("french/ed.ttml") + .group_id("subs") + .language("fra") + .assoc_language("fra") + .name("French") + .is_autoselect(true) + .is_forced(true) + .characteristics("public.accessibility.transcribes-spoken\ + -dialog,public.accessibility.describes-music-and-sound") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=SUBTITLES,", + "URI=\"french/ed.ttml\",", + "GROUP-ID=\"subs\",", + "LANGUAGE=\"fra\",", + "ASSOC-LANGUAGE=\"fra\",", + "NAME=\"French\",", + "AUTOSELECT=YES,", + "FORCED=YES,", + "CHARACTERISTICS=\"", + "public.accessibility.transcribes-spoken-dialog,", + "public.accessibility.describes-music-and-sound", + "\"" + ) + }, + { + ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("cc") + .language("sp") + .name("CC2") + .instream_id(InStreamId::Cc2) + .is_autoselect(true) + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=CLOSED-CAPTIONS,", + "GROUP-ID=\"cc\",", + "LANGUAGE=\"sp\",", + "NAME=\"CC2\",", + "AUTOSELECT=YES,", + "INSTREAM-ID=\"CC2\"" + ) + }, + { + ExtXMedia::new(MediaType::Audio, "foo", "bar"), + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" + }, } #[test] From 7025114e363dd24f535d1f821ff48bf27196d942 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 23 Mar 2020 13:34:26 +0100 Subject: [PATCH 067/112] rewrite keys (ExtXKey, ExtXSessionKey) and Encrypted trait --- src/media_playlist.rs | 6 +- src/media_segment.rs | 15 +- src/tags/master_playlist/session_key.rs | 186 ++----- src/tags/media_segment/key.rs | 703 +++++++++--------------- src/tags/media_segment/map.rs | 46 +- src/traits.rs | 136 ++--- src/types/decryption_key.rs | 317 +++++++++++ src/types/encryption_method.rs | 10 - tests/rfc8216.rs | 18 +- 9 files changed, 708 insertions(+), 729 deletions(-) create mode 100644 src/types/decryption_key.rs diff --git a/src/media_playlist.rs b/src/media_playlist.rs index fdd167d..bd58c9f 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -10,11 +10,11 @@ use crate::line::{Line, Lines, Tag}; use crate::media_segment::MediaSegment; use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, - ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, + ExtXKey, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::ProtocolVersion; +use crate::types::{EncryptionMethod, ProtocolVersion}; use crate::utils::tag; -use crate::{Encrypted, Error, RequiredVersion}; +use crate::{Error, RequiredVersion}; /// Media playlist. #[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] diff --git a/src/media_segment.rs b/src/media_segment.rs index e23de4e..4f21171 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -6,8 +6,8 @@ use shorthand::ShortHand; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, }; -use crate::types::ProtocolVersion; -use crate::{Encrypted, RequiredVersion}; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::{Decryptable, RequiredVersion}; /// Media segment. #[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] @@ -104,10 +104,11 @@ impl RequiredVersion for MediaSegment { } } -impl Encrypted for MediaSegment { - fn keys(&self) -> &Vec { &self.keys } - - fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +impl Decryptable for MediaSegment { + fn keys(&self) -> Vec<&DecryptionKey> { + // + self.keys.iter().filter_map(ExtXKey::as_ref).collect() + } } #[cfg(test)] @@ -120,7 +121,6 @@ mod tests { fn test_display() { assert_eq!( MediaSegment::builder() - //.keys(vec![ExtXKey::empty()]) .map(ExtXMap::new("https://www.example.com/")) .byte_range(ExtXByteRange::from(5..25)) //.date_range() // TODO! @@ -131,7 +131,6 @@ mod tests { .unwrap() .to_string(), concat!( - //"#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", diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index a3db6a9..d3ecb8d 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -2,10 +2,10 @@ use core::convert::TryFrom; use std::fmt; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; +use derive_more::{AsMut, AsRef, From}; use crate::tags::ExtXKey; -use crate::types::{EncryptionMethod, ProtocolVersion}; +use crate::types::{DecryptionKey, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -20,72 +20,52 @@ use crate::{Error, RequiredVersion}; /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist -/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 -#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXSessionKey(ExtXKey); +#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[from(forward)] +pub struct ExtXSessionKey(pub DecryptionKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; /// Makes a new [`ExtXSessionKey`] tag. /// - /// # Panic - /// - /// An [`ExtXSessionKey`] should only be used, - /// if the segments of the stream are encrypted. - /// Therefore this function will panic, - /// if the `method` is [`EncryptionMethod::None`]. - /// /// # Example /// /// ``` - /// # use hls_m3u8::tags::{ExtXSessionKey, ExtXKey}; - /// use hls_m3u8::types::EncryptionMethod; + /// # use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// - /// ExtXSessionKey::new(ExtXKey::new( + /// let session_key = ExtXSessionKey::new(DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/", /// )); /// ``` #[must_use] - pub fn new(inner: ExtXKey) -> Self { - if inner.method() == EncryptionMethod::None { - panic!("the encryption method should never be `None`"); - } - - Self(inner) - } + #[inline] + pub const fn new(inner: DecryptionKey) -> Self { Self(inner) } } impl TryFrom for ExtXSessionKey { type Error = Error; fn try_from(value: ExtXKey) -> Result { - if value.method() == EncryptionMethod::None { - return Err(Error::custom( - "the encryption method should never be `None`", - )); + if let ExtXKey(Some(inner)) = value { + Ok(Self(inner)) + } else { + Err(Error::custom("missing decryption key")) } - - Ok(Self(value)) } } /// This tag requires the same [`ProtocolVersion`] that is returned by -/// `ExtXKey::required_version`. +/// `DecryptionKey::required_version`. impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // TODO: this is not the most elegant solution - write!( - f, - "{}{}", - Self::PREFIX, - self.0.to_string().replacen(ExtXKey::PREFIX, "", 1) - ) + write!(f, "{}{}", Self::PREFIX, self.0.to_string()) } } @@ -93,7 +73,7 @@ impl FromStr for ExtXSessionKey { type Err = Error; fn from_str(input: &str) -> Result { - Ok(Self(ExtXKey::parse_from_str(tag(input, Self::PREFIX)?)?)) + Ok(Self(DecryptionKey::from_str(tag(input, Self::PREFIX)?)?)) } } @@ -103,68 +83,55 @@ mod test { use crate::types::{EncryptionMethod, KeyFormat}; use pretty_assertions::assert_eq; - #[test] - fn test_display() { - let mut key = ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", - )); + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ + } + } + } - assert_eq!( - key.to_string(), + generate_tests! { + { + ExtXSessionKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .build() + .unwrap(), + ), concat!( "#EXT-X-SESSION-KEY:", "METHOD=AES-128,", "URI=\"https://www.example.com/hls-key/key.bin\",", "IV=0x10ef8f758ca555115584bb5b3c687f52" ) - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - concat!( - "#EXT-X-SESSION-KEY:", - "METHOD=AES-128,", - "URI=\"https://priv.example.com/key.php?r=52\"" - ) - .parse::() - .unwrap(), - ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://priv.example.com/key.php?r=52" - )) - ); - - let mut key = ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", - )); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - - assert_eq!( - concat!( - "#EXT-X-SESSION-KEY:", - "METHOD=AES-128,", - "URI=\"https://www.example.com/hls-key/key.bin\",", - "IV=0X10ef8f758ca555115584bb5b3c687f52" - ) - .parse::() - .unwrap(), - key - ); - - key.set_key_format(Some(KeyFormat::Identity)); - - assert_eq!( + }, + { + ExtXSessionKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .format(KeyFormat::Identity) + .build() + .unwrap(), + ), concat!( "#EXT-X-SESSION-KEY:", "METHOD=AES-128,", @@ -172,16 +139,13 @@ mod test { "IV=0x10ef8f758ca555115584bb5b3c687f52,", "KEYFORMAT=\"identity\"", ) - .parse::() - .unwrap(), - key - ) + } } #[test] fn test_required_version() { assert_eq!( - ExtXSessionKey::new(ExtXKey::new( + ExtXSessionKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://www.example.com/" )) @@ -189,34 +153,4 @@ mod test { ProtocolVersion::V1 ); } - - // ExtXSessionKey::new should panic, if the provided - // EncryptionMethod is None! - #[test] - #[should_panic = "the encryption method should never be `None`"] - fn test_new_panic() { let _ = ExtXSessionKey::new(ExtXKey::new(EncryptionMethod::None, "")); } - - #[test] - fn test_deref() { - let key = ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/", - )); - - assert_eq!(key.method(), EncryptionMethod::Aes128); - assert_eq!(key.uri(), Some(&"https://www.example.com/".into())); - } - - #[test] - fn test_deref_mut() { - let mut key = ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/", - )); - - key.set_method(EncryptionMethod::None); - assert_eq!(key.method(), EncryptionMethod::None); - key.set_uri(Some("https://www.github.com/")); - assert_eq!(key.uri(), Some(&"https://www.github.com/".into())); - } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index d1c5590..a3f2989 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,254 +1,166 @@ use std::fmt; use std::str::FromStr; -use derive_builder::Builder; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; -use crate::utils::{parse_iv_from_str, quote, tag, unquote}; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.2.4. EXT-X-KEY] +/// Specifies how to decrypt encrypted data from the server. /// -/// [`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). -/// -/// # Note -/// -/// In case of an empty key ([`EncryptionMethod::None`]), -/// all attributes will be ignored. -/// -/// [`KeyFormat`]: crate::types::KeyFormat -/// [`ExtXMap`]: crate::tags::ExtXMap -/// [`Media Segment`]: crate::MediaSegment -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[builder(setter(into), build_fn(validate = "Self::validate"))] -#[shorthand(enable(must_use, into))] -pub struct ExtXKey { - /// HLS supports multiple [`EncryptionMethod`]s for a [`MediaSegment`]. - /// - /// For example `AES-128`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!(key.method(), EncryptionMethod::SampleAes); - /// ``` - /// - /// # Note - /// - /// This attribute is required. - /// - /// [`MediaSegment`]: crate::MediaSegment - #[shorthand(enable(copy))] - pub(crate) method: EncryptionMethod, - /// An `URI` that specifies how to obtain the key. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_uri(Some("http://www.google.com/")); - /// - /// assert_eq!(key.uri(), Some(&"http://www.google.com/".to_string())); - /// ``` - /// - /// # Note - /// - /// This attribute is required, if the [`EncryptionMethod`] is not `None`. - #[builder(setter(into, strip_option), default)] - pub(crate) uri: Option, - /// An IV (initialization vector) is used to prevent repetitions between - /// segments of encrypted data. - /// - /// - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// # assert_eq!(key.iv(), None); - /// - /// key.set_iv(Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])); - /// - /// assert_eq!( - /// key.iv(), - /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 - #[shorthand(enable(copy), disable(option_as_ref))] - pub(crate) iv: Option<[u8; 0x10]>, - /// The [`KeyFormat`] specifies how the key is - /// represented in the resource identified by the `URI`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format(Some(KeyFormat::Identity)); - /// - /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - #[shorthand(enable(copy))] - pub(crate) key_format: Option, - /// The [`KeyFormatVersions`] attribute. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); - /// - /// assert_eq!( - /// key.key_format_versions(), - /// Some(&KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - pub(crate) key_format_versions: Option, -} - -impl ExtXKeyBuilder { - fn validate(&self) -> Result<(), String> { - if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { - return Err(Error::missing_value("URL").to_string()); - } - - Ok(()) - } -} +/// An unencrypted segment should be marked with [`ExtXKey::empty`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub struct ExtXKey(pub Option); impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; - /// Makes a new [`ExtXKey`] tag. + /// Constructs an [`ExtXKey`] tag. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod, KeyFormat}; /// - /// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// let key = ExtXKey::new( + /// DecryptionKey::builder() + /// .method(EncryptionMethod::Aes128) + /// .uri("https://www.example.com/") + /// .iv([ + /// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + /// ]) + /// .format(KeyFormat::Identity) + /// .versions(vec![1, 2, 3, 4, 5]) + /// .build()?, + /// ); + /// # Ok::<(), String>(()) + /// ``` + #[must_use] + #[inline] + pub const fn new(inner: DecryptionKey) -> Self { Self(Some(inner)) } + + /// Constructs an empty [`ExtXKey`], which signals that a segment is + /// unencrypted. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// assert_eq!(ExtXKey::empty(), ExtXKey(None)); + /// ``` + #[must_use] + #[inline] + pub const fn empty() -> Self { Self(None) } + + /// Returns `true` if the key is not empty. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let k = ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url", + /// )); + /// assert_eq!(k.is_some(), true); + /// + /// let k = ExtXKey::empty(); + /// assert_eq!(k.is_some(), false); + /// ``` + #[must_use] + #[inline] + pub fn is_some(&self) -> bool { self.0.is_some() } + + /// Returns `true` if the key is empty. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let k = ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url", + /// )); + /// assert_eq!(k.is_none(), false); + /// + /// let k = ExtXKey::empty(); + /// assert_eq!(k.is_none(), true); + /// ``` + #[must_use] + #[inline] + pub fn is_none(&self) -> bool { self.0.is_none() } + + /// Returns the underlying [`DecryptionKey`], if there is one. + /// + /// # Panics + /// + /// Panics if there is no underlying decryption key. + /// + /// # Examples + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let k = ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url", + /// )); /// /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\"" + /// k.unwrap(), + /// DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.url") + /// ); + /// ``` + /// + /// ```{.should_panic} + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::DecryptionKey; + /// + /// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics + /// ``` + #[must_use] + pub fn unwrap(self) -> DecryptionKey { + match self.0 { + Some(v) => v, + None => panic!("called `ExtXKey::unwrap()` on an empty key"), + } + } + + /// Returns a reference to the underlying [`DecryptionKey`]. + #[must_use] + #[inline] + pub fn as_ref(&self) -> Option<&DecryptionKey> { self.0.as_ref() } + + /// Converts an [`ExtXKey`] into an `Option`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// assert_eq!(ExtXKey::empty().into_option(), None); + /// + /// assert_eq!( + /// ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url" + /// )) + /// .into_option(), + /// Some(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url" + /// )) /// ); /// ``` #[must_use] - pub fn new>(method: EncryptionMethod, uri: T) -> Self { - Self { - method, - uri: Some(uri.into()), - iv: None, - key_format: None, - key_format_versions: None, - } - } - - /// Returns a Builder to build an [`ExtXKey`]. - /// - /// # Example - /// - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// ExtXKey::builder() - /// .method(EncryptionMethod::Aes128) - /// .uri("https://www.example.com/") - /// .iv([ - /// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - /// ]) - /// .key_format(KeyFormat::Identity) - /// .key_format_versions(vec![1, 2, 3, 4, 5]) - /// .build()?; - /// # Ok::<(), Box>(()) - /// ``` - #[must_use] - pub fn builder() -> ExtXKeyBuilder { ExtXKeyBuilder::default() } - - /// Makes a new [`ExtXKey`] tag without a decryption key. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// let key = ExtXKey::empty(); - /// - /// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE"); - /// ``` - #[must_use] - pub const fn empty() -> Self { - Self { - method: EncryptionMethod::None, - uri: None, - iv: None, - key_format: None, - key_format_versions: None, - } - } - - /// Returns whether the [`EncryptionMethod`] is - /// [`None`]. - /// - /// # Example - /// - /// ``` - /// # 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 - #[must_use] - pub fn is_empty(&self) -> bool { self.method() == EncryptionMethod::None } + #[inline] + pub fn into_option(self) -> Option { self.0 } } /// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or @@ -258,60 +170,9 @@ impl ExtXKey { /// Otherwise [`ProtocolVersion::V1`] is required. impl RequiredVersion for ExtXKey { fn required_version(&self) -> ProtocolVersion { - if self.key_format.is_some() || self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} - -impl ExtXKey { - /// Parses a String without verifying the starting tag - pub(crate) fn parse_from_str(input: &str) -> crate::Result { - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "METHOD" => method = Some(value.parse().map_err(Error::strum)?), - "URI" => { - let unquoted_uri = unquote(value); - - if unquoted_uri.trim().is_empty() { - uri = None; - } else { - uri = Some(unquoted_uri); - } - } - "IV" => iv = Some(parse_iv_from_str(value)?), - "KEYFORMAT" => key_format = Some(value.parse()?), - "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized - // AttributeName. - } - } - } - - let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; - if method != EncryptionMethod::None && uri.is_none() { - return Err(Error::missing_value("URI")); - } - - Ok(Self { - method, - uri, - iv, - key_format, - key_format_versions, - }) + self.0 + .as_ref() + .map_or(ProtocolVersion::V1, |i| i.required_version()) } } @@ -320,42 +181,36 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - Self::parse_from_str(input) + + if input.trim() == "METHOD=NONE" { + Ok(Self(None)) + } else { + Ok(DecryptionKey::from_str(input)?.into()) + } } } +impl From> for ExtXKey { + fn from(value: Option) -> Self { Self(value) } +} + +impl From for ExtXKey { + fn from(value: DecryptionKey) -> Self { Self(Some(value)) } +} + +impl From for ExtXKey { + fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) } +} + impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "METHOD={}", self.method)?; - - if self.method == EncryptionMethod::None { - return Ok(()); + if let Some(value) = &self.0 { + write!(f, "{}", value) + } else { + write!(f, "METHOD=NONE") } - - if let Some(uri) = &self.uri { - write!(f, ",URI={}", quote(uri))?; - } - - if let Some(value) = &self.iv { - let mut result = [0; 0x10 * 2]; - hex::encode_to_slice(value, &mut result).unwrap(); - - write!(f, ",IV=0x{}", ::core::str::from_utf8(&result).unwrap())?; - } - - if let Some(value) = &self.key_format { - write!(f, ",KEYFORMAT={}", quote(value))?; - } - - if let Some(key_format_versions) = &self.key_format_versions { - if !key_format_versions.is_default() { - write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?; - } - } - - Ok(()) } } @@ -365,168 +220,130 @@ mod test { use crate::types::{EncryptionMethod, KeyFormat}; use pretty_assertions::assert_eq; - #[test] - fn test_builder() { - assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,]) - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3, 4, 5]) - .build() - .unwrap() - .to_string(), + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } + + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ + + assert_eq!( + ExtXKey::new( + DecryptionKey::new( + EncryptionMethod::Aes128, + "http://www.example.com" + ) + ), + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"http://www.example.com\",", + "UNKNOWNTAG=abcd" + ).parse().unwrap(), + ); + assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::().is_err()); + assert!("garbage".parse::().is_err()); + } + } + } + + generate_tests! { + { + ExtXKey::empty(), + "#EXT-X-KEY:METHOD=NONE" + }, + { + ExtXKey::new(DecryptionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + )), concat!( "#EXT-X-KEY:", "METHOD=AES-128,", - "URI=\"https://www.example.com/\",", - "IV=0x10ef8f758ca555115584bb5b3c687f52,", - "KEYFORMAT=\"identity\",", - "KEYFORMATVERSIONS=\"1/2/3/4/5\"", + "URI=\"https://priv.example.com/key.php?r=52\"" ) - .to_string() - ); - - assert!(ExtXKey::builder().build().is_err()); - assert!(ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .build() - .is_err()); - } - - #[test] - fn test_display() { - assert_eq!( - ExtXKey::empty().to_string(), - "#EXT-X-KEY:METHOD=NONE".to_string() - ); - - let mut key = ExtXKey::empty(); - // it is expected, that all attributes will be ignored for an empty key! - key.set_key_format(Some(KeyFormat::Identity)); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - key.set_uri(Some("https://www.example.com")); - key.set_key_format_versions(Some(vec![1, 2, 3])); - - assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); - - assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/hls-key/key.bin") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) - .build() - .unwrap() - .to_string(), + }, + { + ExtXKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .build() + .unwrap() + ), concat!( "#EXT-X-KEY:", "METHOD=AES-128,", "URI=\"https://www.example.com/hls-key/key.bin\",", "IV=0x10ef8f758ca555115584bb5b3c687f52" ) - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - concat!( - "#EXT-X-KEY:", - "METHOD=AES-128,", - "URI=\"https://priv.example.com/key.php?r=52\"" - ) - .parse::() - .unwrap(), + }, + { ExtXKey::new( - EncryptionMethod::Aes128, - "https://priv.example.com/key.php?r=52" - ) - ); - - assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap() + ), concat!( "#EXT-X-KEY:", "METHOD=AES-128,", "URI=\"https://www.example.com/hls-key/key.bin\",", - "IV=0X10ef8f758ca555115584bb5b3c687f52" - ) - .parse::() - .unwrap(), - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/hls-key/key.bin") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) - .build() - .unwrap() - ); - - assert_eq!( - concat!( - "#EXT-X-KEY:", - "METHOD=AES-128,", - "URI=\"https://www.example.com/hls-key/key.bin\",", - "IV=0X10ef8f758ca555115584bb5b3c687f52,", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", "KEYFORMAT=\"identity\",", "KEYFORMATVERSIONS=\"1/2/3\"" ) - .parse::() - .unwrap(), - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/hls-key/key.bin") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3]) - .build() - .unwrap() - ); - - assert_eq!( - concat!( - "#EXT-X-KEY:", - "METHOD=AES-128,", - "URI=\"http://www.example.com\",", - "UNKNOWNTAG=abcd" - ) - .parse::() - .unwrap(), - ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com") - ); - assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::().is_err()); - assert!("garbage".parse::().is_err()); + }, } #[test] fn test_required_version() { assert_eq!( - ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/").required_version(), + ExtXKey::new(DecryptionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/" + )) + .required_version(), ProtocolVersion::V1 ); assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3]) - .build() - .unwrap() - .required_version(), + ExtXKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap() + ) + .required_version(), ProtocolVersion::V5 ); assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - .build() - .unwrap() - .required_version(), + ExtXKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + .build() + .unwrap() + ) + .required_version(), ProtocolVersion::V2 ); } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 1161289..abc7803 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -5,9 +5,9 @@ use shorthand::ShortHand; use crate::attribute::AttributePairs; use crate::tags::ExtXKey; -use crate::types::{ByteRange, ProtocolVersion}; +use crate::types::{ByteRange, DecryptionKey, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; -use crate::{Encrypted, Error, RequiredVersion}; +use crate::{Decryptable, Error, RequiredVersion}; /// The [`ExtXMap`] tag specifies how to obtain the [Media Initialization /// Section], required to parse the applicable [`MediaSegment`]s. @@ -36,38 +36,12 @@ use crate::{Encrypted, Error, RequiredVersion}; pub struct ExtXMap { /// 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"); - /// # assert_eq!( - /// # map.uri(), - /// # &"https://prod.mediaspace.com/init.bin".to_string() - /// # ); - /// map.set_uri("https://www.google.com/init.bin"); - /// - /// assert_eq!(map.uri(), &"https://www.google.com/init.bin".to_string()); - /// ``` uri: String, /// 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", ..9); - /// - /// map.set_range(Some(2..5)); - /// assert_eq!(map.range(), Some(ByteRange::from(2..5))); - /// ``` #[shorthand(enable(copy))] range: Option, #[shorthand(enable(skip))] - keys: Vec, + pub(crate) keys: Vec, } impl ExtXMap { @@ -108,10 +82,11 @@ impl ExtXMap { } } -impl Encrypted for ExtXMap { - fn keys(&self) -> &Vec { &self.keys } - - fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +impl Decryptable for ExtXMap { + fn keys(&self) -> Vec<&DecryptionKey> { + // + self.keys.iter().filter_map(ExtXKey::as_ref).collect() + } } /// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the @@ -224,8 +199,7 @@ mod test { } #[test] - fn test_encrypted() { - assert_eq!(ExtXMap::new("foo").keys(), &vec![]); - assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]); + fn test_decryptable() { + assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey>::new()); } } diff --git a/src/traits.rs b/src/traits.rs index a92a9af..4175588 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,106 +1,54 @@ -use crate::tags::ExtXKey; -use crate::types::{EncryptionMethod, ProtocolVersion}; +use std::collections::{BTreeMap, HashMap}; -/// 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, -/// } -/// -/// // Implementing the trait is very simple: -/// // Simply expose the internal buffer, that contains all the keys. -/// impl Encrypted for ExampleTag { -/// fn keys(&self) -> &Vec { &self.keys } -/// -/// fn keys_mut(&mut self) -> &mut Vec { &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; +use crate::types::{DecryptionKey, ProtocolVersion}; - /// Returns an exclusive reference to all keys, that can be used to decrypt - /// this tag. - fn keys_mut(&mut self) -> &mut Vec; +mod private { + pub trait Sealed {} + impl Sealed for crate::MediaSegment {} + impl Sealed for crate::tags::ExtXMap {} +} - /// Sets all keys, that can be used to decrypt this tag. - fn set_keys(&mut self, value: Vec) -> &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. +/// Signals that a type or some of the asssociated data might need to be +/// decrypted. +/// +/// # Note +/// +/// You are not supposed to implement this trait, therefore it is "sealed". +pub trait Decryptable: private::Sealed { + /// Returns all keys, associated with the type. /// - /// # Note + /// # Example /// - /// 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; - } + /// use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::{ByteRange, EncryptionMethod}; + /// use hls_m3u8::Decryptable; + /// + /// let map = ExtXMap::with_range("https://www.example.url/", ByteRange::from(2..11)); + /// + /// for key in map.keys() { + /// if key.method == EncryptionMethod::Aes128 { + /// // fetch content with the uri and decrypt the result + /// break; + /// } + /// } + /// ``` + #[must_use] + fn keys(&self) -> Vec<&DecryptionKey>; - self.keys() - .iter() - .any(|k| k.method() != EncryptionMethod::None) + /// Most of the time only a single key is provided, so instead of iterating + /// through all keys, one might as well just get the first key. + #[must_use] + fn first_key(&self) -> Option<&DecryptionKey> { + ::keys(self).first().copied() } - /// 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() } + /// Returns the number of keys. + #[must_use] + fn len(&self) -> usize { ::keys(self).len() } + + #[must_use] + fn is_empty(&self) -> bool { ::len(self) == 0 } } /// # Example diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs new file mode 100644 index 0000000..68396a3 --- /dev/null +++ b/src/types/decryption_key.rs @@ -0,0 +1,317 @@ +use std::fmt; +use std::str::FromStr; + +use derive_builder::Builder; +use shorthand::ShortHand; + +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; +use crate::utils::{parse_iv_from_str, quote, unquote}; +use crate::{Error, RequiredVersion}; + +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[builder(setter(into), build_fn(validate = "Self::validate"))] +#[shorthand(enable(must_use, into))] +pub struct DecryptionKey { + /// HLS supports multiple [`EncryptionMethod`]s for a [`MediaSegment`]. + /// + /// For example `AES-128`. + /// + /// ## Note + /// + /// This field is required. + /// + /// [`MediaSegment`]: crate::MediaSegment + //#[shorthand(enable(skip))] + #[shorthand(enable(copy))] + pub method: EncryptionMethod, + /// An `URI` that specifies how to obtain the key. + /// + /// ## Note + /// + /// This attribute is required, if the [`EncryptionMethod`] is not `None`. + #[builder(setter(into, strip_option), default)] + pub(crate) uri: String, + /// An IV (initialization vector) is used to prevent repetitions between + /// segments of encrypted data. + /// + /// ## Note + /// + /// This field is optional. + #[builder(setter(into, strip_option), default)] + // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 + #[shorthand(enable(copy), disable(option_as_ref))] + pub(crate) iv: Option<[u8; 0x10]>, + /// The [`KeyFormat`] specifies how the key is + /// represented in the resource identified by the `URI`. + /// + /// ## Note + /// + /// This field is optional. + #[builder(setter(into, strip_option), default)] + #[shorthand(enable(copy))] + pub format: Option, + /// The [`KeyFormatVersions`] attribute. + /// + /// ## Note + /// + /// This field is optional. + #[builder(setter(into, strip_option), default)] + pub versions: Option, +} + +impl DecryptionKey { + #[must_use] + #[inline] + pub fn new>(method: EncryptionMethod, uri: I) -> Self { + Self { + method, + uri: uri.into(), + iv: None, + format: None, + versions: None, + } + } + + #[must_use] + #[inline] + pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } +} + +/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or +/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is +/// specified. +/// +/// Otherwise [`ProtocolVersion::V1`] is required. +impl RequiredVersion for DecryptionKey { + fn required_version(&self) -> ProtocolVersion { + if self.format.is_some() || self.versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } + } +} + +impl FromStr for DecryptionKey { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut format = None; + let mut versions = None; + + for (key, value) in AttributePairs::new(input) { + match key { + "METHOD" => method = Some(value.parse().map_err(Error::strum)?), + "URI" => { + let unquoted_uri = unquote(value); + + if !unquoted_uri.trim().is_empty() { + uri = Some(unquoted_uri); + } + } + "IV" => iv = Some(parse_iv_from_str(value)?), + "KEYFORMAT" => format = Some(value.parse()?), + "KEYFORMATVERSIONS" => versions = Some(value.parse()?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized + // AttributeName. + } + } + } + + let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; + let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; + + Ok(Self { + method, + uri, + iv, + format, + versions, + }) + } +} + +impl fmt::Display for DecryptionKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?; + + if let Some(value) = &self.iv { + let mut result = [0; 0x10 * 2]; + ::hex::encode_to_slice(value, &mut result).unwrap(); + + write!(f, ",IV=0x{}", ::core::str::from_utf8(&result).unwrap())?; + } + + if let Some(value) = &self.format { + write!(f, ",KEYFORMAT={}", quote(value))?; + } + + if let Some(value) = &self.versions { + if !value.is_default() { + write!(f, ",KEYFORMATVERSIONS={}", value)?; + } + } + + Ok(()) + } +} + +impl DecryptionKeyBuilder { + fn validate(&self) -> Result<(), String> { + // a decryption key must contain a uri and a method + if self.method.is_none() { + return Err(Error::missing_field("DecryptionKey", "method").to_string()); + } else if self.uri.is_none() { + return Err(Error::missing_field("DecryptionKey", "uri").to_string()); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::{EncryptionMethod, KeyFormat}; + use pretty_assertions::assert_eq; + + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } + + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ + + assert_eq!( + DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"), + concat!( + "METHOD=AES-128,", + "URI=\"http://www.example.com\",", + "UNKNOWNTAG=abcd" + ).parse().unwrap(), + ); + assert!("METHOD=AES-128,URI=".parse::().is_err()); + assert!("garbage".parse::().is_err()); + } + } + } + + #[test] + fn test_builder() { + let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + key.set_iv(Some([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ])); + key.format = Some(KeyFormat::Identity); + key.versions = Some(vec![1, 2, 3, 4, 5].into()); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3, 4, 5]) + .build() + .unwrap(), + key + ); + + assert!(DecryptionKey::builder().build().is_err()); + assert!(DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .build() + .is_err()); + } + + generate_tests! { + { + DecryptionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + ), + concat!( + "METHOD=AES-128,", + "URI=\"https://priv.example.com/key.php?r=52\"" + ) + }, + { + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .build() + .unwrap(), + concat!( + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52" + ) + }, + { + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap(), + concat!( + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\",", + "KEYFORMATVERSIONS=\"1/2/3\"" + ) + }, + } + + #[test] + fn test_required_version() { + assert_eq!( + DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") + .required_version(), + ProtocolVersion::V1 + ); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap() + .required_version(), + ProtocolVersion::V5 + ); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + .build() + .unwrap() + .required_version(), + ProtocolVersion::V2 + ); + } +} diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 3f8cfa0..cedb5d1 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -6,10 +6,6 @@ use strum::{Display, EnumString}; #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum EncryptionMethod { - /// The [`MediaSegment`]s are not encrypted. - /// - /// [`MediaSegment`]: crate::MediaSegment - None, /// The [`MediaSegment`]s are completely encrypted using the Advanced /// Encryption Standard ([AES-128]) with a 128-bit key, Cipher Block /// Chaining (CBC), and [Public-Key Cryptography Standards #7 (PKCS7)] @@ -65,7 +61,6 @@ mod tests { EncryptionMethod::SampleAes.to_string(), "SAMPLE-AES".to_string() ); - assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string()); } #[test] @@ -80,11 +75,6 @@ mod tests { "SAMPLE-AES".parse::().unwrap() ); - assert_eq!( - EncryptionMethod::None, - "NONE".parse::().unwrap() - ); - assert!("unknown".parse::().is_err()); } } diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index cae2c2c..9d16727 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -4,7 +4,7 @@ use std::time::Duration; use hls_m3u8::tags::{ ExtInf, ExtXEndList, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream, }; -use hls_m3u8::types::{EncryptionMethod, MediaType, StreamData}; +use hls_m3u8::types::{DecryptionKey, EncryptionMethod, MediaType, StreamData}; use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -102,10 +102,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(2.833))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ]) .uri("http://media.example.com/fileSequence52-A.ts") .build() @@ -113,10 +113,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(15.0))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ]) .uri("http://media.example.com/fileSequence52-B.ts") .build() @@ -124,10 +124,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(13.333))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ]) .uri("http://media.example.com/fileSequence52-C.ts") .build() @@ -135,10 +135,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(15.0))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=53" - ) + )) ]) .uri("http://media.example.com/fileSequence53-A.ts") .build() From b8fd4c15d5089e7b57b778780551dd2006c11333 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:08:32 +0100 Subject: [PATCH 068/112] rewrite KeyFormatVersions --- src/types/key_format_versions.rs | 621 ++++++++++++++++++++++++++++--- 1 file changed, 568 insertions(+), 53 deletions(-) diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 214b725..494f4ac 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -1,8 +1,11 @@ +use std::cmp::Ordering; use std::fmt; +use std::hash::{Hash, Hasher}; +use std::iter::{Extend, FromIterator}; +use std::ops::{Index, IndexMut}; +use std::slice::SliceIndex; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; - use crate::types::ProtocolVersion; use crate::utils::{quote, unquote}; use crate::Error; @@ -12,18 +15,49 @@ use crate::RequiredVersion; /// this instance complies with, if more than one version of a particular /// [`KeyFormat`] is defined. /// +/// ## Note on maximum size +/// +/// To reduce the memory usage and to make this struct implement [`Copy`], a +/// fixed size array is used internally (`[u8; 9]`), which can store a maximum +/// number of 9 `u8` numbers. +/// +/// If you encounter any m3u8 file, which fails to parse, because the buffer is +/// too small, feel free to [make an issue](https://github.com/sile/hls_m3u8/issues). +/// +/// ## Example +/// +/// ``` +/// use hls_m3u8::types::KeyFormatVersions; +/// +/// assert_eq!( +/// KeyFormatVersions::from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).to_string(), +/// "\"255/255/255/255/255/255/255/255/255\"".to_string() +/// ); +/// ``` +/// /// [`KeyFormat`]: crate::types::KeyFormat -#[derive(Deref, DerefMut, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct KeyFormatVersions(Vec); +#[derive(Debug, Clone, Copy)] +pub struct KeyFormatVersions { + // NOTE(Luro02): if the current array is not big enough one can easily increase + // the number of elements or change the type to something bigger, + // but it would be kinda wasteful to use a `Vec` here, which requires + // allocations and has a size of at least 24 bytes + // (::std::mem::size_of::>() = 24). + buffer: [u8; 9], + // Indicates the number of used items in the array. + len: u8, +} impl KeyFormatVersions { - /// Makes a new [`KeyFormatVersions`]. + /// Constructs an empty [`KeyFormatVersions`]. /// /// # Example /// /// ``` /// # use hls_m3u8::types::KeyFormatVersions; - /// let key_format_versions = KeyFormatVersions::new(); + /// let versions = KeyFormatVersions::new(); + /// + /// assert_eq!(versions, KeyFormatVersions::default()); /// ``` #[inline] #[must_use] @@ -31,40 +65,299 @@ impl KeyFormatVersions { /// Add a value to the end of [`KeyFormatVersions`]. /// + /// # Panics + /// + /// This function panics, if you try to push more elements, than + /// [`KeyFormatVersions::remaining`] returns. + /// /// # Example /// /// ``` /// # use hls_m3u8::types::KeyFormatVersions; - /// let mut key_format_versions = KeyFormatVersions::new(); + /// let mut versions = KeyFormatVersions::new(); /// - /// key_format_versions.push(1); + /// versions.push(1); + /// assert_eq!(versions, KeyFormatVersions::from([1])); /// ``` - pub fn push(&mut self, value: usize) { - if self.is_default() { - self.0 = vec![value]; + /// + /// This will panic, because it exceeded the maximum number of elements: + /// + /// ```{.should_panic} + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut versions = KeyFormatVersions::new(); + /// + /// for _ in 0..=versions.capacity() { + /// versions.push(1); // <- panics + /// } + /// ``` + pub fn push(&mut self, value: u8) { + if self.len as usize == self.buffer.len() { + panic!("reached maximum number of elements in KeyFormatVersions"); + } + + self.buffer[self.len()] = value; + self.len += 1; + } + + /// `KeyFormatVersions` has a limited capacity and this function returns how + /// many elements can be pushed, until it panics. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut versions = KeyFormatVersions::new(); + /// + /// assert_eq!(versions.remaining(), versions.capacity()); + /// + /// versions.push(1); + /// versions.push(2); + /// versions.push(3); + /// assert_eq!(versions.remaining(), 6); + /// ``` + #[inline] + #[must_use] + pub fn remaining(&self) -> usize { self.capacity().saturating_sub(self.len()) } + + /// Returns the number of elements. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut versions = KeyFormatVersions::new(); + /// + /// assert_eq!(versions.len(), 0); + /// + /// versions.push(2); + /// assert_eq!(versions.len(), 1); + /// ``` + #[inline] + #[must_use] + pub const fn len(&self) -> usize { self.len as usize } + + /// Returns the total number of elements that can be stored. + /// + /// # Note + /// + /// It should not be relied on that this function will always return 9. In + /// the future this number might increase. + #[inline] + #[must_use] + pub fn capacity(&self) -> usize { self.buffer.len() } + + /// Shortens the internal array to the provided length. + /// + /// # Note + /// + /// If `len` is greater than the current length, this has no effect. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut versions = KeyFormatVersions::from([1, 2, 3, 4, 5, 6]); + /// versions.truncate(3); + /// + /// assert_eq!(versions, KeyFormatVersions::from([1, 2, 3])); + /// ``` + pub fn truncate(&mut self, len: usize) { + if len > self.len() { + return; + } + + self.len = len as u8; + } + + /// Returns `true` if there are no elements. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut versions = KeyFormatVersions::new(); + /// + /// assert_eq!(versions.is_empty(), true); + /// + /// versions.push(2); + /// assert_eq!(versions.is_empty(), false); + /// ``` + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { self.len() == 0 } + + /// Removes the last element and returns it, or `None` if it is empty. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::KeyFormatVersions; + /// let mut versions = KeyFormatVersions::new(); + /// + /// assert_eq!(versions.pop(), None); + /// + /// versions.push(2); + /// assert_eq!(versions.pop(), Some(2)); + /// assert_eq!(versions.is_empty(), true); + /// ``` + pub fn pop(&mut self) -> Option { + if self.is_empty() { + None } else { - self.0.push(value); + self.len -= 1; + Some(self.buffer[self.len()]) } } - /// Returns `true`, if [`KeyFormatVersions`] has the default value of - /// `vec![1]`. + /// Returns `true`, if it is either empty or has a length of 1 and the first + /// element is 1. /// /// # Example /// /// ``` /// # use hls_m3u8::types::KeyFormatVersions; - /// assert!(KeyFormatVersions::from(vec![1]).is_default()); + /// let mut versions = KeyFormatVersions::new(); + /// + /// assert_eq!(versions.is_default(), true); + /// + /// versions.push(1); + /// assert_eq!(versions.is_default(), true); + /// + /// assert_eq!(KeyFormatVersions::default().is_default(), true); /// ``` #[must_use] pub fn is_default(&self) -> bool { - // - self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() + self.is_empty() || (self.buffer[self.len().saturating_sub(1)] == 1 && self.len() == 1) + } +} + +impl PartialEq for KeyFormatVersions { + fn eq(&self, other: &Self) -> bool { + if self.len() == other.len() { + // only compare the parts in the buffer, that are used: + self.as_ref() == self.as_ref() + } else { + false + } + } +} + +impl Eq for KeyFormatVersions {} + +impl PartialOrd for KeyFormatVersions { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(::cmp(self, other)) + } +} + +impl Ord for KeyFormatVersions { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { self.as_ref().cmp(other.as_ref()) } +} + +impl Hash for KeyFormatVersions { + fn hash(&self, state: &mut H) { + state.write_usize(self.len()); + self.as_ref().hash(state); + } +} + +impl AsRef<[u8]> for KeyFormatVersions { + #[inline] + #[must_use] + fn as_ref(&self) -> &[u8] { &self.buffer[..self.len()] } +} + +impl AsMut<[u8]> for KeyFormatVersions { + #[inline] + #[must_use] + fn as_mut(&mut self) -> &mut [u8] { + // this temporary variable is required, because the compiler does not resolve + // the borrow to it's value immediately, so there is a shared borrow and + // therefore no exclusive borrow can be made. + let len = self.len(); + &mut self.buffer[..len] + } +} + +impl Extend for KeyFormatVersions { + fn extend>(&mut self, iter: I) { + for element in iter { + if self.remaining() == 0 { + break; + } + + self.push(element); + } + } +} + +impl<'a> Extend<&'a u8> for KeyFormatVersions { + fn extend>(&mut self, iter: I) { + >::extend(self, iter.into_iter().copied()) + } +} + +impl> Index for KeyFormatVersions { + type Output = I::Output; + + #[inline] + fn index(&self, index: I) -> &Self::Output { self.as_ref().index(index) } +} + +impl> IndexMut for KeyFormatVersions { + #[inline] + fn index_mut(&mut self, index: I) -> &mut Self::Output { self.as_mut().index_mut(index) } +} + +impl IntoIterator for KeyFormatVersions { + type IntoIter = IntoIter; + type Item = u8; + + fn into_iter(self) -> Self::IntoIter { self.into() } +} + +impl FromIterator for KeyFormatVersions { + fn from_iter>(iter: I) -> Self { + let mut result = Self::default(); + // an array like [0; 9] as empty + let mut is_empty = true; + + for item in iter { + if item != 0 { + is_empty = false; + } + + if result.remaining() == 0 { + break; + } + + result.push(item); + } + + if is_empty { + return Self::default(); + } + + result + } +} + +impl<'a> FromIterator<&'a u8> for KeyFormatVersions { + fn from_iter>(iter: I) -> Self { + >::from_iter(iter.into_iter().copied()) } } impl Default for KeyFormatVersions { - fn default() -> Self { Self(vec![1]) } + #[inline] + fn default() -> Self { + Self { + buffer: [0; 9], + len: 0, + } + } } /// This tag requires [`ProtocolVersion::V5`]. @@ -76,41 +369,98 @@ impl FromStr for KeyFormatVersions { type Err = Error; fn from_str(input: &str) -> Result { - let mut result = unquote(input) + let mut result = Self::default(); + + for item in unquote(input) .split('/') .map(|v| v.parse().map_err(|e| Error::parse_int(v, e))) - .collect::, Error>>()?; + { + let item = item?; + + if result.remaining() == 0 { + return Err(Error::custom( + "reached maximum number of elements in KeyFormatVersions", + )); + } + + result.push(item); + } if result.is_empty() { result.push(1); } - Ok(Self(result)) + Ok(result) } } impl fmt::Display for KeyFormatVersions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.is_default() { + if self.is_default() || self.is_empty() { return write!(f, "{}", quote("1")); } - if let Some(value) = self.0.iter().next() { - write!(f, "\"{}", value)?; + write!(f, "\"{}", self.buffer[0])?; - for value in self.0.iter().skip(1) { - write!(f, "/{}", value)?; - } - - write!(f, "\"")?; + for item in &self.buffer[1..self.len()] { + write!(f, "/{}", item)?; } + write!(f, "\"")?; + Ok(()) } } -impl> From for KeyFormatVersions { - fn from(value: I) -> Self { Self(value.into_iter().collect()) } +impl> From for KeyFormatVersions { + fn from(value: T) -> Self { Self::from_iter(value.as_ref().iter().map(|i| *i as u8)) } +} + +/// `Iterator` for [`KeyFormatVersions`]. +#[derive(Debug, Clone, PartialEq)] +pub struct IntoIter { + buffer: [T; 9], + position: usize, + len: usize, +} + +impl From for IntoIter { + fn from(value: KeyFormatVersions) -> Self { + Self { + buffer: value.buffer, + position: 0, + len: value.len(), + } + } +} + +impl<'a> From<&'a KeyFormatVersions> for IntoIter { + fn from(value: &'a KeyFormatVersions) -> Self { + Self { + buffer: value.buffer, + position: 0, + len: value.len(), + } + } +} + +impl ExactSizeIterator for IntoIter { + fn len(&self) -> usize { self.len.saturating_sub(self.position) } +} + +impl ::core::iter::FusedIterator for IntoIter {} + +impl Iterator for IntoIter { + type Item = T; + + fn next(&mut self) -> Option { + if self.position == self.len { + return None; + } + + self.position += 1; + Some(self.buffer[self.position - 1]) + } } #[cfg(test)] @@ -118,27 +468,200 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn test_hash() { + let mut hasher_left = std::collections::hash_map::DefaultHasher::new(); + let mut hasher_right = std::collections::hash_map::DefaultHasher::new(); + + assert_eq!( + KeyFormatVersions::from([1, 2, 3]).hash(&mut hasher_left), + KeyFormatVersions::from([1, 2, 3]).hash(&mut hasher_right) + ); + + assert_eq!(hasher_left.finish(), hasher_right.finish()); + } + + #[test] + fn test_ord() { + assert_eq!( + KeyFormatVersions::from([1, 2]).cmp(&KeyFormatVersions::from([1, 2])), + Ordering::Equal + ); + + assert_eq!( + KeyFormatVersions::from([2]).cmp(&KeyFormatVersions::from([1, 2])), + Ordering::Greater + ); + + assert_eq!( + KeyFormatVersions::from([2, 3]).cmp(&KeyFormatVersions::from([1, 2])), + Ordering::Greater + ); + + assert_eq!( + KeyFormatVersions::from([]).cmp(&KeyFormatVersions::from([1, 2])), + Ordering::Less + ); + } + + #[test] + fn test_partial_eq() { + let mut versions = KeyFormatVersions::from([1, 2, 3, 4, 5, 6]); + versions.truncate(3); + + assert_eq!(versions, KeyFormatVersions::from([1, 2, 3])); + } + + #[test] + fn test_as_ref() { + assert_eq!(KeyFormatVersions::new().as_ref(), &[]); + assert_eq!(KeyFormatVersions::from([1, 2, 3]).as_ref(), &[1, 2, 3]); + assert_eq!(KeyFormatVersions::from([]).as_ref(), &[]); + } + + #[test] + fn test_as_mut() { + assert_eq!(KeyFormatVersions::new().as_mut(), &mut []); + assert_eq!(KeyFormatVersions::from([1, 2, 3]).as_mut(), &mut [1, 2, 3]); + assert_eq!(KeyFormatVersions::from([]).as_mut(), &mut []); + } + + #[test] + fn test_index() { + // test index + assert_eq!(&KeyFormatVersions::new()[..], &[]); + assert_eq!(&KeyFormatVersions::from([1, 2, 3])[..2], &[1, 2]); + assert_eq!(&KeyFormatVersions::from([1, 2, 3])[1..2], &[2]); + assert_eq!(&KeyFormatVersions::from([1, 2, 3])[..], &[1, 2, 3]); + + // test index_mut + assert_eq!(&mut KeyFormatVersions::new()[..], &mut []); + assert_eq!(&mut KeyFormatVersions::from([1, 2, 3])[..2], &mut [1, 2]); + assert_eq!(&mut KeyFormatVersions::from([1, 2, 3])[1..2], &mut [2]); + assert_eq!(&mut KeyFormatVersions::from([1, 2, 3])[..], &mut [1, 2, 3]); + } + + #[test] + fn test_extend() { + let mut versions = KeyFormatVersions::new(); + versions.extend(&[1, 2, 3]); + + assert_eq!(versions, KeyFormatVersions::from([1, 2, 3])); + + versions.extend(&[1, 2, 3]); + assert_eq!(versions, KeyFormatVersions::from([1, 2, 3, 1, 2, 3])); + + versions.extend(&[1, 2, 3, 4]); + assert_eq!( + versions, + KeyFormatVersions::from([1, 2, 3, 1, 2, 3, 1, 2, 3]) + ); + } + + #[test] + fn test_default() { + assert_eq!(KeyFormatVersions::default(), KeyFormatVersions::new()); + } + + #[test] + fn test_into_iter() { + assert_eq!(KeyFormatVersions::new().into_iter().next(), None); + assert_eq!(KeyFormatVersions::new().into_iter().len(), 0); + + let mut iterator = KeyFormatVersions::from([1, 2, 3, 4, 5]).into_iter(); + + assert_eq!(iterator.len(), 5); + assert_eq!(iterator.next(), Some(1)); + + assert_eq!(iterator.len(), 4); + assert_eq!(iterator.next(), Some(2)); + + assert_eq!(iterator.len(), 3); + assert_eq!(iterator.next(), Some(3)); + + assert_eq!(iterator.len(), 2); + assert_eq!(iterator.next(), Some(4)); + + assert_eq!(iterator.len(), 1); + assert_eq!(iterator.next(), Some(5)); + + assert_eq!(iterator.len(), 0); + assert_eq!(iterator.next(), None); + } + + #[test] + fn test_from_iter() { + assert_eq!( + { + let mut result = KeyFormatVersions::new(); + result.push(1); + result.push(2); + result.push(3); + result.push(4); + result + }, + KeyFormatVersions::from_iter(&[1, 2, 3, 4]) + ); + + assert_eq!( + { + let mut result = KeyFormatVersions::new(); + result.push(0); + result.push(1); + result.push(2); + result.push(3); + result.push(4); + result + }, + KeyFormatVersions::from_iter(&[0, 1, 2, 3, 4]) + ); + + assert_eq!(KeyFormatVersions::new(), KeyFormatVersions::from_iter(&[])); + + assert_eq!(KeyFormatVersions::new(), KeyFormatVersions::from_iter(&[0])); + assert_eq!( + KeyFormatVersions::new(), + KeyFormatVersions::from_iter(&[0, 0]) + ); + assert_eq!( + { + let mut result = KeyFormatVersions::new(); + result.push(0); + result.push(1); + result.push(2); + result.push(3); + result.push(4); + result.push(5); + result.push(6); + result.push(7); + result.push(8); + result + }, + KeyFormatVersions::from_iter(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + ); + } + #[test] fn test_display() { assert_eq!( - KeyFormatVersions::from(vec![1, 2, 3, 4, 5]).to_string(), + KeyFormatVersions::from([1, 2, 3, 4, 5]).to_string(), quote("1/2/3/4/5") ); - assert_eq!(KeyFormatVersions::from(vec![]).to_string(), quote("1")); + assert_eq!(KeyFormatVersions::from([]).to_string(), quote("1")); assert_eq!(KeyFormatVersions::new().to_string(), quote("1")); } #[test] fn test_parser() { assert_eq!( - KeyFormatVersions::from(vec![1, 2, 3, 4, 5]), + KeyFormatVersions::from([1, 2, 3, 4, 5]), quote("1/2/3/4/5").parse().unwrap() ); - assert_eq!(KeyFormatVersions::from(vec![1]), "1".parse().unwrap()); + assert_eq!(KeyFormatVersions::from([1]), "1".parse().unwrap()); + assert_eq!(KeyFormatVersions::from([1, 2]), "1/2".parse().unwrap()); - assert_eq!(KeyFormatVersions::from(vec![1, 2]), "1/2".parse().unwrap()); assert!("1/b".parse::().is_err()); } @@ -152,28 +675,20 @@ mod tests { #[test] fn test_is_default() { - assert!(KeyFormatVersions::new().is_default()); - assert!(KeyFormatVersions::from(vec![]).is_default()); - assert!(!KeyFormatVersions::from(vec![1, 2, 3]).is_default()); + assert_eq!(KeyFormatVersions::new().is_default(), true); + assert_eq!(KeyFormatVersions::default().is_default(), true); + + assert_eq!(KeyFormatVersions::from([]).is_default(), true); + assert_eq!(KeyFormatVersions::from([1]).is_default(), true); + + assert_eq!(KeyFormatVersions::from([1, 2, 3]).is_default(), false); } #[test] fn test_push() { - let mut key_format_versions = KeyFormatVersions::from(vec![]); - + let mut key_format_versions = KeyFormatVersions::new(); key_format_versions.push(2); - assert_eq!(KeyFormatVersions::from(vec![2]), key_format_versions); - } - #[test] - fn test_deref() { - assert!(!KeyFormatVersions::new().is_empty()); - } - - #[test] - fn test_deref_mut() { - let mut key_format_versions = KeyFormatVersions::from(vec![1, 2, 3]); - key_format_versions.pop(); - assert_eq!(key_format_versions, KeyFormatVersions::from(vec![1, 2])); + assert_eq!(KeyFormatVersions::from([2]), key_format_versions); } } From d1fdb7fec1dfdcff0bd8ced2916ee2bcb51ad5c1 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:21:20 +0100 Subject: [PATCH 069/112] improve ExtXDateRange --- src/tags/media_segment/date_range.rs | 245 +++++++++++++++++++++------ 1 file changed, 190 insertions(+), 55 deletions(-) diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index ce6f71a..768e2b7 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -19,105 +19,159 @@ use crate::{Error, RequiredVersion}; #[builder(setter(into))] #[shorthand(enable(must_use, into))] pub struct ExtXDateRange { - /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// A string that uniquely identifies an [`ExtXDateRange`] in the playlist. /// - /// # Note + /// ## Note /// - /// This attribute is required. + /// This field is required. id: String, /// 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. /// - /// # Note + /// ## Note /// - /// This attribute is optional. + /// This field is optional. #[builder(setter(strip_option), default)] class: Option, /// The date at which the [`ExtXDateRange`] begins. /// - /// # Note + /// ## Note /// - /// This attribute is required. + /// This field is required. #[cfg(feature = "chrono")] + #[shorthand(enable(copy), disable(into))] start_date: DateTime, + /// The date at which the [`ExtXDateRange`] begins. + /// + /// ## Note + /// + /// This field is required. + #[cfg(not(feature = "chrono"))] + start_date: String, /// The date at which the [`ExtXDateRange`] ends. It must be equal to or /// later than the value of the [`start-date`] attribute. /// - /// # Note + /// ## Note /// - /// This attribute is optional. + /// This field is optional. /// /// [`start-date`]: #method.start_date #[cfg(feature = "chrono")] + #[shorthand(enable(copy), disable(into))] #[builder(setter(strip_option), default)] end_date: Option>, + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the start-date field. + /// + /// ## Note + /// + /// This field is optional. + /// + /// [`start-date`]: #method.start_date #[cfg(not(feature = "chrono"))] #[builder(setter(strip_option), default)] end_date: Option, /// 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 + /// ## Note /// - /// This attribute is optional. + /// This field is optional. #[builder(setter(strip_option), default)] - duration: Option, - /// 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. + #[shorthand(enable(skip))] + pub duration: Option, + /// This field indicates the expected duration of an [`ExtXDateRange`], + /// whose actual duration is not yet known. /// - /// # Note + /// ## Note /// - /// This attribute is optional. + /// This field is optional. #[builder(setter(strip_option), default)] - planned_duration: Option, - /// You can read about this attribute here - /// + #[shorthand(enable(skip))] + pub planned_duration: Option, + /// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and + /// Telecommunications Engineers standard that describes the inline + /// insertion of cue tones in mpeg-ts streams. /// - /// # Note + /// SCTE-35 was originally used in the US to signal a local ad insertion + /// opportunity in the transport streams, and in Europe to insert local TV + /// programs (e.g. local news transmissions). It is now used to signal all + /// kinds of program and ad events in linear transport streams and in newer + /// ABR delivery formats such as HLS and DASH. /// - /// This attribute is optional. + /// + /// + /// ## Note + /// + /// This field is optional. #[builder(setter(strip_option), default)] scte35_cmd: Option, - /// You can read about this attribute here - /// + /// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and + /// Telecommunications Engineers standard that describes the inline + /// insertion of cue tones in mpeg-ts streams. /// - /// # Note + /// SCTE-35 was originally used in the US to signal a local ad insertion + /// opportunity in the transport streams, and in Europe to insert local TV + /// programs (e.g. local news transmissions). It is now used to signal all + /// kinds of program and ad events in linear transport streams and in newer + /// ABR delivery formats such as HLS and DASH. /// - /// This attribute is optional. + /// + /// + /// ## Note + /// + /// This field is optional. #[builder(setter(strip_option), default)] scte35_out: Option, - /// You can read about this attribute here - /// + /// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and + /// Telecommunications Engineers standard that describes the inline + /// insertion of cue tones in mpeg-ts streams. /// - /// # Note + /// SCTE-35 was originally used in the US to signal a local ad insertion + /// opportunity in the transport streams, and in Europe to insert local TV + /// programs (e.g. local news transmissions). It is now used to signal all + /// kinds of program and ad events in linear transport streams and in newer + /// ABR delivery formats such as HLS and DASH. /// - /// This attribute is optional. + /// + /// + /// ## Note + /// + /// This field is optional. #[builder(setter(strip_option), default)] scte35_in: Option, - /// 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. + /// This field indicates that the [`ExtXDateRange::end_date`] is equal to + /// the [`ExtXDateRange::start_date`] of the following range. /// - /// # Note + /// The following range is the [`ExtXDateRange`] with the same class, that + /// has the earliest start date after the start date of the range in + /// question. /// - /// This attribute is optional. + /// ## Note + /// + /// This field is optional. #[builder(default)] - end_on_next: bool, + #[shorthand(enable(skip))] + pub end_on_next: bool, /// 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"`. + /// attributes. /// - /// # Note + /// A client-attribute can only consist of uppercase characters (A-Z), + /// numbers (0-9) and `-`. /// - /// This attribute is optional. + /// Clients should use a reverse-dns naming scheme, 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 field is optional. #[builder(default)] - #[shorthand(enable(collection_magic, get_mut))] - client_attributes: BTreeMap, + #[shorthand(enable(collection_magic), disable(set, get))] + pub client_attributes: BTreeMap, } impl ExtXDateRangeBuilder { @@ -170,7 +224,6 @@ let date_range = ExtXDateRange::new( doc = r#" ``` # use hls_m3u8::tags::ExtXDateRange; - let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00"); ``` "# @@ -200,7 +253,61 @@ let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00"); } /// Returns a builder for [`ExtXDateRange`]. + /// + /// # Example + #[cfg_attr( + feature = "chrono", + doc = r#" +``` +# use hls_m3u8::tags::ExtXDateRange; +use std::time::Duration; +use chrono::{FixedOffset, TimeZone}; +use hls_m3u8::types::Float; + +let date_range = 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", Float::new(45.3)) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build()?; +# Ok::<(), String>(()) +``` +"# + )] + #[cfg_attr( + not(feature = "chrono"), + doc = r#" +``` +# use hls_m3u8::tags::ExtXDateRange; +use std::time::Duration; +use hls_m3u8::types::Float; + +let date_range = ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date("2014-03-05T11:15:00Z") + .end_date("2014-03-05T11:16:00Z") + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", Float::new(45.3)) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build()?; +# Ok::<(), String>(()) +``` +"# + )] #[must_use] + #[inline] pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() } } @@ -267,13 +374,23 @@ impl FromStr for ExtXDateRange { "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { if value != "YES" { - return Err(Error::custom("The value of `END-ON-NEXT` has to be `YES`!")); + return Err(Error::custom("`END-ON-NEXT` must be `YES`")); } end_on_next = true; } _ => { if key.starts_with("X-") { - client_attributes.insert(key.to_ascii_uppercase(), value.parse()?); + if key.chars().any(|c| { + c.is_ascii_lowercase() + || !c.is_ascii() + || !(c.is_alphanumeric() || c == '-') + }) { + return Err(Error::custom( + "a client attribute can only consist of uppercase ascii characters, numbers or `-`", + )); + } + + client_attributes.insert(key.to_string(), value.parse()?); } else { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an @@ -287,8 +404,28 @@ impl FromStr for ExtXDateRange { let start_date = start_date.ok_or_else(|| Error::missing_value("START-DATE"))?; if end_on_next && class.is_none() { - return Err(Error::invalid_input()); + return Err(Error::missing_attribute("CLASS")); + } else if end_on_next && duration.is_some() { + return Err(Error::unexpected_attribute("DURATION")); + } else if end_on_next && end_date.is_some() { + return Err(Error::unexpected_attribute("END-DATE")); } + + // TODO: verify this without chrono? + // https://tools.ietf.org/html/rfc8216#section-4.3.2.7 + #[cfg(feature = "chrono")] + { + if let (Some(Ok(duration)), Some(end_date)) = + (duration.map(chrono::Duration::from_std), &end_date) + { + if start_date + duration != *end_date { + return Err(Error::custom( + "end_date must be equal to start_date + duration", + )); + } + } + } + Ok(Self { id, class, @@ -470,11 +607,11 @@ mod test { .end_date({ #[cfg(feature = "chrono")] { - FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0) + FixedOffset::east(0).ymd(2014, 3, 5).and_hms_milli(11, 16, 0, 100) } #[cfg(not(feature = "chrono"))] { - "2014-03-05T11:16:00Z" + "2014-03-05T11:16:00.100Z" } }) .duration(Duration::from_secs_f64(60.1)) @@ -483,7 +620,6 @@ mod test { .scte35_cmd("0xFC002F0000000000FF2") .scte35_out("0xFC002F0000000000FF0") .scte35_in("0xFC002F0000000000FF1") - .end_on_next(true) .build() .unwrap(), concat!( @@ -491,14 +627,13 @@ mod test { "ID=\"test_id\",", "CLASS=\"test_class\",", "START-DATE=\"2014-03-05T11:15:00Z\",", - "END-DATE=\"2014-03-05T11:16:00Z\",", + "END-DATE=\"2014-03-05T11:16:00.100Z\",", "DURATION=60.1,", "PLANNED-DURATION=59.993,", "SCTE35-CMD=0xFC002F0000000000FF2,", "SCTE35-OUT=0xFC002F0000000000FF0,", "SCTE35-IN=0xFC002F0000000000FF1,", - "X-CUSTOM=45.3,", - "END-ON-NEXT=YES" + "X-CUSTOM=45.3", ) }, } From 870a39cdddab6db1d55acc1158e9660dfac89865 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:32:48 +0100 Subject: [PATCH 070/112] internalize ExtXDiscontinuity --- src/tags/media_segment/discontinuity.rs | 12 +++--------- src/tags/media_segment/mod.rs | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 588ed37..2bbb1d8 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -5,16 +5,10 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.2.3. EXT-X-DISCONTINUITY] -/// -/// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the -/// [`Media Segment`] that follows it and the one that preceded it. -/// -/// [`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 +/// The `ExtXDiscontinuity` tag indicates a discontinuity between the +/// `MediaSegment` that follows it and the one that preceded it. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ExtXDiscontinuity; +pub(crate) struct ExtXDiscontinuity; impl ExtXDiscontinuity { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; diff --git a/src/tags/media_segment/mod.rs b/src/tags/media_segment/mod.rs index 9d10bd5..33539b1 100644 --- a/src/tags/media_segment/mod.rs +++ b/src/tags/media_segment/mod.rs @@ -8,7 +8,7 @@ mod program_date_time; pub use byte_range::*; pub use date_range::*; -pub use discontinuity::*; +pub(crate) use discontinuity::*; pub use inf::*; pub use key::*; pub use map::*; From 7c26d2f7f1c14f005a3e50b3acfcfb7796d705e9 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:41:24 +0100 Subject: [PATCH 071/112] slightly improve ExtInf --- src/tags/media_segment/inf.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 7703e62..ba3b241 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -2,19 +2,18 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; +use derive_more::AsRef; + use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.2.1. EXTINF] -/// -/// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies -/// only to the next [`Media Segment`]. +/// Specifies the duration of a [`Media Segment`]. /// /// [`Media Segment`]: crate::media_segment::MediaSegment -/// [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)] +#[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { + #[as_ref] duration: Duration, title: Option, } From fc1136265c7a0de19c5996aafbf73f5d36012025 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:49:16 +0100 Subject: [PATCH 072/112] improve DecryptionKey --- src/tags/master_playlist/session_key.rs | 6 +- src/types/decryption_key.rs | 83 ++++--- src/types/initialization_vector.rs | 305 ++++++++++++++++++++++++ src/types/key_format.rs | 10 +- src/utils.rs | 42 ---- 5 files changed, 366 insertions(+), 80 deletions(-) create mode 100644 src/types/initialization_vector.rs diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index d3ecb8d..0522abe 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -14,14 +14,14 @@ use crate::{Error, RequiredVersion}; /// preload these keys without having to read the [`MediaPlaylist`]s /// first. /// -/// If an [`ExtXSessionKey`] is used, the values of [`ExtXKey::method`], -/// [`ExtXKey::key_format`] and [`ExtXKey::key_format_versions`] must match any +/// If an [`ExtXSessionKey`] is used, the values of [`DecryptionKey::method`], +/// [`DecryptionKey::format`] and [`DecryptionKey::versions`] must match any /// [`ExtXKey`] with the same uri field. /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist +/// [`ExtXKey`]: crate::tags::ExtXKey #[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[from(forward)] pub struct ExtXSessionKey(pub DecryptionKey); impl ExtXSessionKey { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 68396a3..0caf17a 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -5,53 +5,73 @@ use derive_builder::Builder; use shorthand::ShortHand; use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; -use crate::utils::{parse_iv_from_str, quote, unquote}; +use crate::types::{ + EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion, +}; +use crate::utils::{quote, unquote}; use crate::{Error, RequiredVersion}; +/// Specifies how to decrypt encrypted data from the server. #[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[builder(setter(into), build_fn(validate = "Self::validate"))] -#[shorthand(enable(must_use, into))] +#[shorthand(enable(skip, must_use, into))] +#[non_exhaustive] pub struct DecryptionKey { - /// HLS supports multiple [`EncryptionMethod`]s for a [`MediaSegment`]. + /// The encryption method, which has been used to encrypt the data. /// - /// For example `AES-128`. + /// An [`EncryptionMethod::Aes128`] signals that the data is encrypted using + /// the Advanced Encryption Standard (AES) with a 128-bit key, Cipher Block + /// Chaining (CBC), and Public-Key Cryptography Standards #7 (PKCS7) + /// padding. CBC is restarted on each segment boundary, using either the + /// [`DecryptionKey::iv`] field or the [`MediaSegment::number`] as the IV. + /// + /// An [`EncryptionMethod::SampleAes`] means that the [`MediaSegment`]s + /// contain media samples, such as audio or video, that are encrypted using + /// the Advanced Encryption Standard (Aes128). How these media streams are + /// encrypted and encapsulated in a segment depends on the media encoding + /// and the media format of the segment. /// /// ## Note /// /// This field is required. /// + /// [`MediaSegment::number`]: crate::MediaSegment::number /// [`MediaSegment`]: crate::MediaSegment - //#[shorthand(enable(skip))] - #[shorthand(enable(copy))] pub method: EncryptionMethod, - /// An `URI` that specifies how to obtain the key. + /// This uri points to a key file, which contains the cipher key. /// /// ## Note /// - /// This attribute is required, if the [`EncryptionMethod`] is not `None`. + /// This field is required. #[builder(setter(into, strip_option), default)] + #[shorthand(disable(skip))] pub(crate) uri: String, - /// An IV (initialization vector) is used to prevent repetitions between - /// segments of encrypted data. + /// An initialization vector (IV) is a fixed size input that can be used + /// along with a secret key for data encryption. + /// + /// ## Note + /// + /// This field is optional and an absent value indicates that + /// [`MediaSegment::number`] should be used instead. + /// + /// [`MediaSegment::number`]: crate::MediaSegment::number + #[builder(setter(into, strip_option), default)] + pub iv: InitializationVector, + /// A server may offer multiple ways to retrieve a key by providing multiple + /// [`DecryptionKey`]s with different [`KeyFormat`] values. + /// + /// An [`EncryptionMethod::Aes128`] uses 16-octet (16 byte/128 bit) keys. If + /// the format is [`KeyFormat::Identity`], the key file is a single packed + /// array of 16 octets (16 byte/128 bit) in binary format. /// /// ## Note /// /// This field is optional. #[builder(setter(into, strip_option), default)] - // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 - #[shorthand(enable(copy), disable(option_as_ref))] - pub(crate) iv: Option<[u8; 0x10]>, - /// The [`KeyFormat`] specifies how the key is - /// represented in the resource identified by the `URI`. - /// - /// ## Note - /// - /// This field is optional. - #[builder(setter(into, strip_option), default)] - #[shorthand(enable(copy))] pub format: Option, - /// The [`KeyFormatVersions`] attribute. + /// A list of numbers that can be used to indicate which version(s) + /// this instance complies with, if more than one version of a particular + /// [`KeyFormat`] is defined. /// /// ## Note /// @@ -67,7 +87,7 @@ impl DecryptionKey { Self { method, uri: uri.into(), - iv: None, + iv: InitializationVector::default(), format: None, versions: None, } @@ -115,7 +135,7 @@ impl FromStr for DecryptionKey { uri = Some(unquoted_uri); } } - "IV" => iv = Some(parse_iv_from_str(value)?), + "IV" => iv = Some(value.parse()?), "KEYFORMAT" => format = Some(value.parse()?), "KEYFORMATVERSIONS" => versions = Some(value.parse()?), _ => { @@ -128,6 +148,7 @@ impl FromStr for DecryptionKey { let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; + let iv = iv.unwrap_or_default(); Ok(Self { method, @@ -143,11 +164,8 @@ impl fmt::Display for DecryptionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?; - if let Some(value) = &self.iv { - let mut result = [0; 0x10 * 2]; - ::hex::encode_to_slice(value, &mut result).unwrap(); - - write!(f, ",IV=0x{}", ::core::str::from_utf8(&result).unwrap())?; + if let InitializationVector::Aes128(_) = &self.iv { + write!(f, ",IV={}", &self.iv)?; } if let Some(value) = &self.format { @@ -215,9 +233,10 @@ mod test { #[test] fn test_builder() { let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - key.set_iv(Some([ + key.iv = [ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); + ] + .into(); key.format = Some(KeyFormat::Identity); key.versions = Some(vec![1, 2, 3, 4, 5].into()); diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs new file mode 100644 index 0000000..0dcb9be --- /dev/null +++ b/src/types/initialization_vector.rs @@ -0,0 +1,305 @@ +use core::fmt; +use core::str::FromStr; + +use crate::Error; + +/// An initialization vector (IV) is a fixed size input that can be used along +/// with a secret key for data encryption. +/// +/// The use of an IV prevents repetition in encrypted data, making it more +/// difficult for a hacker using a dictionary attack to find patterns and break +/// a cipher. For example, a sequence might appear twice or more within the body +/// of a message. If there are repeated sequences in encrypted data, an attacker +/// could assume that the corresponding sequences in the message were also +/// identical. The IV prevents the appearance of corresponding duplicate +/// character sequences in the ciphertext. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum InitializationVector { + /// An IV for use with Aes128. + Aes128([u8; 0x10]), + /// An [`ExtXKey`] tag with [`KeyFormat::Identity`] that does not have an IV + /// field indicates that the [`MediaSegment::number`] is to be used as the + /// IV when decrypting a `MediaSegment`. + /// + /// [`ExtXKey`]: crate::tags::ExtXKey + /// [`KeyFormat::Identity`]: crate::types::KeyFormat::Identity + /// [`MediaSegment::number`]: crate::MediaSegment::number + Number(u128), + /// Signals that an IV is missing. + Missing, +} + +impl InitializationVector { + /// Returns the IV as an [`u128`]. `None` is returned for + /// [`InitializationVector::Missing`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::InitializationVector; + /// assert_eq!( + /// InitializationVector::Aes128([ + /// 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, + /// 0x90, 0x12 + /// ]) + /// .to_u128(), + /// Some(0x12345678901234567890123456789012) + /// ); + /// + /// assert_eq!(InitializationVector::Number(0x10).to_u128(), Some(0x10)); + /// + /// assert_eq!(InitializationVector::Missing.to_u128(), None); + /// ``` + #[must_use] + pub fn to_u128(&self) -> Option { + match *self { + Self::Aes128(v) => Some(u128::from_be_bytes(v)), + Self::Number(n) => Some(n), + Self::Missing => None, + } + } + + /// Returns the IV as a slice, which can be used to for example decrypt + /// a [`MediaSegment`]. `None` is returned for + /// [`InitializationVector::Missing`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::InitializationVector; + /// assert_eq!( + /// InitializationVector::Aes128([ + /// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + /// 0x0F, 0x10, + /// ]) + /// .to_slice(), + /// Some([ + /// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + /// 0x0F, 0x10, + /// ]) + /// ); + /// + /// assert_eq!( + /// InitializationVector::Number(0x12345678901234567890123456789012).to_slice(), + /// Some([ + /// 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, + /// 0x90, 0x12 + /// ]) + /// ); + /// + /// assert_eq!(InitializationVector::Missing.to_slice(), None); + /// ``` + /// + /// [`MediaSegment`]: crate::MediaSegment + #[must_use] + pub fn to_slice(&self) -> Option<[u8; 0x10]> { + match &self { + Self::Aes128(v) => Some(*v), + Self::Number(v) => Some(v.to_be_bytes()), + Self::Missing => None, + } + } + + /// Returns `true` if the initialization vector is not missing. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::InitializationVector; + /// assert_eq!( + /// InitializationVector::Aes128([ + /// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + /// 0x0F, 0x10, + /// ]) + /// .is_some(), + /// true + /// ); + /// + /// assert_eq!(InitializationVector::Number(4).is_some(), true); + /// + /// assert_eq!(InitializationVector::Missing.is_some(), false); + /// ``` + #[must_use] + #[inline] + pub fn is_some(&self) -> bool { *self != Self::Missing } + + /// Returns `true` if the initialization vector is missing. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::InitializationVector; + /// assert_eq!( + /// InitializationVector::Aes128([ + /// 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + /// 0x0F, 0x10, + /// ]) + /// .is_none(), + /// false + /// ); + /// + /// assert_eq!(InitializationVector::Number(4).is_none(), false); + /// + /// assert_eq!(InitializationVector::Missing.is_none(), true); + /// ``` + #[must_use] + #[inline] + pub fn is_none(&self) -> bool { *self == Self::Missing } +} + +impl Default for InitializationVector { + fn default() -> Self { Self::Missing } +} + +impl From<[u8; 0x10]> for InitializationVector { + fn from(value: [u8; 0x10]) -> Self { Self::Aes128(value) } +} + +impl From> for InitializationVector { + fn from(value: Option<[u8; 0x10]>) -> Self { + match value { + Some(v) => Self::Aes128(v), + None => Self::Missing, + } + } +} + +impl fmt::Display for InitializationVector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Self::Aes128(buffer) => { + let mut result = [0; 0x10 * 2]; + ::hex::encode_to_slice(buffer, &mut result).unwrap(); + + write!(f, "0x{}", ::core::str::from_utf8(&result).unwrap())?; + } + Self::Number(num) => { + write!(f, "InitializationVector::Number({})", num)?; + } + Self::Missing => { + write!(f, "InitializationVector::Missing")?; + } + } + + Ok(()) + } +} + +impl FromStr for InitializationVector { + type Err = Error; + + fn from_str(input: &str) -> Result { + if !(input.starts_with("0x") || input.starts_with("0X")) { + return Err(Error::custom("An IV should either start with `0x` or `0X`")); + } + + if input.len() - 2 != 32 { + return Err(Error::custom( + "An IV must be 32 bytes long + 2 bytes for 0x/0X", + )); + } + + let mut result = [0; 16]; + + ::hex::decode_to_slice(&input.as_bytes()[2..], &mut result).map_err(Error::hex)?; + + Ok(Self::Aes128(result)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_default() { + assert_eq!( + InitializationVector::default(), + InitializationVector::Missing + ); + } + + #[test] + fn test_from() { + assert_eq!( + InitializationVector::from([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ]), + InitializationVector::Aes128([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ]) + ); + + assert_eq!( + InitializationVector::from(None), + InitializationVector::Missing + ); + + assert_eq!( + InitializationVector::from(Some([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ])), + InitializationVector::Aes128([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ]) + ) + } + + #[test] + fn test_display() { + assert_eq!( + InitializationVector::Aes128([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ]) + .to_string(), + "0xffffffffffffffffffffffffffffffff".to_string() + ); + + assert_eq!( + InitializationVector::Number(5).to_string(), + "InitializationVector::Number(5)".to_string() + ); + + assert_eq!( + InitializationVector::Missing.to_string(), + "InitializationVector::Missing".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + InitializationVector::Aes128([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ]), + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".parse().unwrap() + ); + + assert_eq!( + InitializationVector::Aes128([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF + ]), + "0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".parse().unwrap() + ); + + // missing `0x` at the start: + assert!("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + .parse::() + .is_err()); + // too small: + assert!("0xFF".parse::().is_err()); + // too large: + assert!("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + .parse::() + .is_err()); + } +} diff --git a/src/types/key_format.rs b/src/types/key_format.rs index f3b995a..77d89b6 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -5,12 +5,16 @@ use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; -/// [`KeyFormat`] specifies, how the key is represented in the -/// resource identified by the `URI`. +/// Specifies how the key is represented in the resource identified by the +/// `URI`. #[non_exhaustive] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum KeyFormat { - /// The key is a single packed array of 16 octets in binary format. + /// An [`EncryptionMethod::Aes128`] uses 16-octet (16 byte/128 bit) keys. If + /// the format is [`KeyFormat::Identity`], the key file is a single packed + /// array of 16 octets (16 byte/128 bit) in binary format. + /// + /// [`EncryptionMethod::Aes128`]: crate::types::EncryptionMethod::Aes128 Identity, } diff --git a/src/utils.rs b/src/utils.rs index 7ff9ddd..b210675 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,22 +12,6 @@ macro_rules! required_version { } } -pub(crate) fn parse_iv_from_str(input: &str) -> crate::Result<[u8; 16]> { - if !(input.starts_with("0x") || input.starts_with("0X")) { - return Err(Error::invalid_input()); - } - - if input.len() - 2 != 32 { - return Err(Error::invalid_input()); - } - - let mut result = [0; 16]; - - hex::decode_to_slice(&input.as_bytes()[2..], &mut result).map_err(Error::hex)?; - - Ok(result) -} - pub(crate) fn parse_yes_or_no>(s: T) -> crate::Result { match s.as_ref() { "YES" => Ok(true), @@ -86,32 +70,6 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - #[test] - fn test_parse_iv_from_str() { - assert_eq!( - parse_iv_from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(), - [ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF - ] - ); - - assert_eq!( - parse_iv_from_str("0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(), - [ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF - ] - ); - - // missing `0x` at the start: - assert!(parse_iv_from_str("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").is_err()); - // too small: - assert!(parse_iv_from_str("0xFF").is_err()); - // too large: - assert!(parse_iv_from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").is_err()); - } - #[test] fn test_parse_yes_or_no() { assert!(parse_yes_or_no("YES").unwrap()); From ca3ba476c3b000d0248c0e941759c68f6c5728f5 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:56:43 +0100 Subject: [PATCH 073/112] improve ExtXProgramDateTime --- src/tags/media_segment/program_date_time.rs | 32 ++++++++++----------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 6352172..77a8606 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -10,16 +10,23 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// Associates the first sample of a [`MediaSegment`] with an absolute date +/// and/or time. /// -/// The [`ExtXProgramDateTime`] tag associates the first sample of a -/// [`MediaSegment`] with an absolute date and/or time. +/// ## Features +/// +/// By enabling the `chrono` feature the `date_time`-field will change from +/// `String` to `DateTime` and the traits +/// - `Deref>`, +/// - `DerefMut>` +/// - and `Copy` +/// +/// will be derived. /// /// [`MediaSegment`]: crate::MediaSegment -/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: -/// https://tools.ietf.org/html/rfc8216#section-4.3.2.6 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))] +#[non_exhaustive] pub struct ExtXProgramDateTime { /// The date-time of the first sample of the associated media segment. #[cfg(feature = "chrono")] @@ -28,7 +35,6 @@ pub struct ExtXProgramDateTime { /// The date-time of the first sample of the associated media segment. #[cfg(not(feature = "chrono"))] pub date_time: String, - __non_exhaustive: (), } impl ExtXProgramDateTime { @@ -52,12 +58,7 @@ impl ExtXProgramDateTime { /// ``` #[must_use] #[cfg(feature = "chrono")] - pub const fn new(date_time: DateTime) -> Self { - Self { - date_time, - __non_exhaustive: (), - } - } + pub const fn new(date_time: DateTime) -> Self { Self { date_time } } /// Makes a new [`ExtXProgramDateTime`] tag. /// @@ -71,7 +72,6 @@ impl ExtXProgramDateTime { pub fn new>(date_time: T) -> Self { Self { date_time: date_time.into(), - __non_exhaustive: (), } } } @@ -103,7 +103,7 @@ impl FromStr for ExtXProgramDateTime { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - let date_time = { + Ok(Self::new({ #[cfg(feature = "chrono")] { DateTime::parse_from_rfc3339(input).map_err(Error::chrono)? @@ -112,9 +112,7 @@ impl FromStr for ExtXProgramDateTime { { input } - }; - - Ok(Self::new(date_time)) + })) } } From 15cc360a2c9256d7676e1a965af7c641e70804d3 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:03:19 +0100 Subject: [PATCH 074/112] implement missing traits --- src/tags/media_segment/date_range.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 768e2b7..1a30e28 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -15,7 +15,7 @@ use crate::{Error, RequiredVersion}; /// 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. -#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd)] +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[builder(setter(into))] #[shorthand(enable(must_use, into))] pub struct ExtXDateRange { From 42e1afaa47559c9ed1da51ce2207a8f90c41ec6c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:17:03 +0100 Subject: [PATCH 075/112] change ExtXPlaylistType to PlaylistType --- src/line.rs | 7 +-- src/master_playlist.rs | 2 +- src/media_playlist.rs | 10 ++-- src/types/mod.rs | 2 + .../media_playlist => types}/playlist_type.rs | 48 ++++++++----------- tests/media_playlist.rs | 11 ++--- 6 files changed, 35 insertions(+), 45 deletions(-) rename src/{tags/media_playlist => types}/playlist_type.rs (62%) diff --git a/src/line.rs b/src/line.rs index 1c675e4..f7d1616 100644 --- a/src/line.rs +++ b/src/line.rs @@ -5,6 +5,7 @@ use core::str::FromStr; use derive_more::Display; use crate::tags; +use crate::types::PlaylistType; use crate::Error; #[derive(Debug, Clone)] @@ -70,7 +71,7 @@ pub(crate) enum Tag<'a> { ExtXMediaSequence(tags::ExtXMediaSequence), ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence), ExtXEndList(tags::ExtXEndList), - ExtXPlaylistType(tags::ExtXPlaylistType), + PlaylistType(PlaylistType), ExtXIFramesOnly(tags::ExtXIFramesOnly), ExtXMedia(tags::ExtXMedia), ExtXSessionData(tags::ExtXSessionData), @@ -109,8 +110,8 @@ impl<'a> TryFrom<&'a str> for Tag<'a> { input.parse().map(Self::ExtXDiscontinuitySequence) } else if input.starts_with(tags::ExtXEndList::PREFIX) { input.parse().map(Self::ExtXEndList) - } else if input.starts_with(tags::ExtXPlaylistType::PREFIX) { - input.parse().map(Self::ExtXPlaylistType) + } else if input.starts_with(PlaylistType::PREFIX) { + input.parse().map(Self::PlaylistType) } else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) { input.parse().map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 272a5b7..641c434 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -493,7 +493,7 @@ impl FromStr for MasterPlaylist { | Tag::ExtXMediaSequence(_) | Tag::ExtXDiscontinuitySequence(_) | Tag::ExtXEndList(_) - | Tag::ExtXPlaylistType(_) + | Tag::PlaylistType(_) | Tag::ExtXIFramesOnly(_) => { return Err(Error::unexpected_tag(tag)); } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index bd58c9f..9eb76b0 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -10,9 +10,9 @@ use crate::line::{Line, Lines, Tag}; use crate::media_segment::MediaSegment; use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, - ExtXKey, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, + ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::{EncryptionMethod, ProtocolVersion}; +use crate::types::{EncryptionMethod, PlaylistType, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -43,13 +43,13 @@ pub struct MediaPlaylist { /// This field is optional. #[builder(default)] discontinuity_sequence: Option, - /// Sets the [`ExtXPlaylistType`] tag. + /// Sets the [`PlaylistType`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - playlist_type: Option, + playlist_type: Option, /// Sets the [`ExtXIFramesOnly`] tag. /// /// # Note @@ -392,7 +392,7 @@ fn parse_media_playlist( Tag::ExtXEndList(t) => { builder.end_list(t); } - Tag::ExtXPlaylistType(t) => { + Tag::PlaylistType(t) => { builder.playlist_type(t); } Tag::ExtXIFramesOnly(t) => { diff --git a/src/types/mod.rs b/src/types/mod.rs index 65ffcf4..9a3c299 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -13,6 +13,7 @@ mod protocol_version; mod resolution; mod stream_data; mod value; +pub(crate) mod playlist_type; mod float; mod ufloat; @@ -27,6 +28,7 @@ pub use in_stream_id::*; pub use key_format::*; pub use key_format_versions::*; pub use media_type::*; +pub use playlist_type::*; pub use protocol_version::*; pub use resolution::*; pub use stream_data::*; diff --git a/src/tags/media_playlist/playlist_type.rs b/src/types/playlist_type.rs similarity index 62% rename from src/tags/media_playlist/playlist_type.rs rename to src/types/playlist_type.rs index f81ad10..36127f3 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/types/playlist_type.rs @@ -7,36 +7,36 @@ use crate::{Error, RequiredVersion}; /// # [4.3.3.5. EXT-X-PLAYLIST-TYPE] /// -/// The [`ExtXPlaylistType`] tag provides mutability information about the +/// The [`PlaylistType`] tag provides mutability information about the /// [`MediaPlaylist`]. It applies to the entire [`MediaPlaylist`]. /// /// [`MediaPlaylist`]: 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, [`MediaSegment`]s +pub enum PlaylistType { + /// If the [`PlaylistType`] is Event, [`MediaSegment`]s /// can only be added to the end of the [`MediaPlaylist`]. /// /// [`MediaSegment`]: crate::MediaSegment /// [`MediaPlaylist`]: crate::MediaPlaylist Event, - /// If the [`ExtXPlaylistType`] is Video On Demand (Vod), + /// If the [`PlaylistType`] is Video On Demand (Vod), /// the [`MediaPlaylist`] cannot change. /// /// [`MediaPlaylist`]: crate::MediaPlaylist Vod, } -impl ExtXPlaylistType { +impl PlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; } /// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXPlaylistType { +impl RequiredVersion for PlaylistType { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl fmt::Display for ExtXPlaylistType { +impl fmt::Display for PlaylistType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { Self::Event => write!(f, "{}EVENT", Self::PREFIX), @@ -45,7 +45,7 @@ impl fmt::Display for ExtXPlaylistType { } } -impl FromStr for ExtXPlaylistType { +impl FromStr for PlaylistType { type Err = Error; fn from_str(input: &str) -> Result { @@ -53,7 +53,7 @@ impl FromStr for ExtXPlaylistType { match input { "EVENT" => Ok(Self::Event), "VOD" => Ok(Self::Vod), - _ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))), + _ => Err(Error::custom(format!("unknown playlist type: {:?}", input))), } } } @@ -66,48 +66,38 @@ mod test { #[test] fn test_parser() { assert_eq!( - "#EXT-X-PLAYLIST-TYPE:VOD" - .parse::() - .unwrap(), - ExtXPlaylistType::Vod, + "#EXT-X-PLAYLIST-TYPE:VOD".parse::().unwrap(), + PlaylistType::Vod, ); assert_eq!( "#EXT-X-PLAYLIST-TYPE:EVENT" - .parse::() + .parse::() .unwrap(), - ExtXPlaylistType::Event, + PlaylistType::Event, ); - assert!("#EXT-X-PLAYLIST-TYPE:H" - .parse::() - .is_err()); + assert!("#EXT-X-PLAYLIST-TYPE:H".parse::().is_err()); - assert!("garbage".parse::().is_err()); + assert!("garbage".parse::().is_err()); } #[test] fn test_display() { assert_eq!( "#EXT-X-PLAYLIST-TYPE:VOD".to_string(), - ExtXPlaylistType::Vod.to_string(), + PlaylistType::Vod.to_string(), ); assert_eq!( "#EXT-X-PLAYLIST-TYPE:EVENT".to_string(), - ExtXPlaylistType::Event.to_string(), + PlaylistType::Event.to_string(), ); } #[test] fn test_required_version() { - assert_eq!( - ExtXPlaylistType::Vod.required_version(), - ProtocolVersion::V1 - ); - assert_eq!( - ExtXPlaylistType::Event.required_version(), - ProtocolVersion::V1 - ); + assert_eq!(PlaylistType::Vod.required_version(), ProtocolVersion::V1); + assert_eq!(PlaylistType::Event.required_version(), ProtocolVersion::V1); } } diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index 126b0c5..338ead9 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -5,9 +5,8 @@ use std::time::Duration; -use hls_m3u8::tags::{ - ExtInf, ExtXByteRange, ExtXEndList, ExtXMediaSequence, ExtXPlaylistType, ExtXTargetDuration, -}; +use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXEndList, ExtXMediaSequence, ExtXTargetDuration}; +use hls_m3u8::types::PlaylistType; use hls_m3u8::{MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -72,7 +71,7 @@ generate_tests! { }, test_absolute_uris => { MediaPlaylist::builder() - .playlist_type(ExtXPlaylistType::Vod) + .playlist_type(PlaylistType::Vod) .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) .segments(vec![ MediaSegment::builder() @@ -123,7 +122,7 @@ generate_tests! { MediaPlaylist::builder() .target_duration(Duration::from_secs(10)) .media_sequence(0) - .playlist_type(ExtXPlaylistType::Vod) + .playlist_type(PlaylistType::Vod) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs(10))) @@ -137,7 +136,6 @@ generate_tests! { .uri("hls_450k_video.ts") .build() .unwrap(), - MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs(10))) .byte_range(1_110_328..1_823_412) @@ -315,5 +313,4 @@ generate_tests! { "#EXT-X-ENDLIST\n" ) }, - } From ca302ef5432e8ce4cd84e2b38b5f5543aea43764 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:18:34 +0100 Subject: [PATCH 076/112] improve MediaSegment --- src/media_segment.rs | 212 ++++++++++++++++++++---- src/tags/media_segment/mod.rs | 18 +- src/tags/shared/independent_segments.rs | 2 +- src/tags/shared/mod.rs | 6 +- 4 files changed, 196 insertions(+), 42 deletions(-) diff --git a/src/media_segment.rs b/src/media_segment.rs index 4f21171..0053172 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -9,39 +9,176 @@ use crate::tags::{ use crate::types::{DecryptionKey, ProtocolVersion}; use crate::{Decryptable, RequiredVersion}; -/// Media segment. -#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] -#[builder(setter(into, strip_option))] -#[shorthand(enable(must_use, get_mut, collection_magic))] +/// A video is split into smaller chunks called [`MediaSegment`]s, which are +/// specified by a uri and optionally a byte range. +/// +/// Each `MediaSegment` must carry the continuation of the encoded bitstream +/// from the end of the segment with the previous [`MediaSegment::number`], +/// where values in a series such as timestamps and continuity counters must +/// continue uninterrupted. The only exceptions are the first [`MediaSegment`] +/// ever to appear in a [`MediaPlaylist`] and [`MediaSegment`]s that are +/// explicitly signaled as discontinuities. +/// Unmarked media discontinuities can trigger playback errors. +/// +/// Any `MediaSegment` that contains video should include enough information +/// to initialize a video decoder and decode a continuous set of frames that +/// includes the final frame in the segment; network efficiency is optimized if +/// there is enough information in the segment to decode all frames in the +/// segment. +/// +/// For example, any `MediaSegment` containing H.264 video should +/// contain an Instantaneous Decoding Refresh (IDR); frames prior to the first +/// IDR will be downloaded but possibly discarded. +/// +/// [`MediaPlaylist`]: crate::MediaPlaylist +#[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[builder(setter(strip_option))] +#[shorthand(enable(must_use))] pub struct MediaSegment { - /// All [`ExtXKey`] tags. + /// Each [`MediaSegment`] has a number, which allows synchronization between + /// different variants. + /// + /// ## Note + /// + /// This number must not be specified, because it will be assigned + /// automatically by [`MediaPlaylistBuilder::segments`]. The first + /// [`MediaSegment::number`] in a [`MediaPlaylist`] will either be 0 or the + /// number returned by the [`ExtXDiscontinuitySequence`] if one is + /// provided. + /// The following segments will be the previous segment number + 1. + /// + /// [`MediaPlaylistBuilder::segments`]: + /// crate::builder::MediaPlaylistBuilder::segments + /// [`MediaPlaylist`]: crate::MediaPlaylist + /// [`ExtXMediaSequence`]: crate::tags::ExtXMediaSequence + /// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence + #[builder(default, setter(custom))] + #[shorthand(disable(set))] + pub(crate) number: usize, + #[builder(default, setter(custom))] + #[shorthand(enable(skip))] + pub(crate) explicit_number: bool, + /// This field specifies how to decrypt a [`MediaSegment`], which can only + /// be encrypted with one [`EncryptionMethod`], using one [`DecryptionKey`] + /// and [`DecryptionKey::iv`]. + /// + /// However, a server may offer multiple ways to retrieve that key by + /// providing multiple keys with different [`DecryptionKey::format`]s. + /// + /// Any unencrypted segment that is preceded by an encrypted segment must + /// have an [`ExtXKey::empty`]. Otherwise, the client will misinterpret + /// those segments as encrypted. + /// + /// The server may set the HTTP Expires header in the key response to + /// indicate the duration for which the key can be cached. + /// + /// ## Note + /// + /// This field is optional and a missing value or an [`ExtXKey::empty()`] + /// indicates an unencrypted media segment. + /// + /// [`ExtXMap`]: crate::tags::ExtXMap + /// [`KeyFormat`]: crate::types::KeyFormat + /// [`EncryptionMethod`]: crate::types::EncryptionMethod + #[builder(default, setter(into))] + #[shorthand(enable(skip))] + pub keys: Vec, + /// This field specifies how to obtain the Media Initialization Section + /// required to parse the applicable `MediaSegment`s. + /// + /// ## Note + /// + /// This field is optional, but should be specified for media segments in + /// playlists with an [`ExtXIFramesOnly`] tag when the first `MediaSegment` + /// in the playlist (or the first segment following a segment marked with + /// [`MediaSegment::has_discontinuity`]) does not immediately follow the + /// Media Initialization Section at the beginning of its resource. + /// + /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly #[builder(default)] - keys: Vec, - /// The [`ExtXMap`] tag associated with the media segment. + #[shorthand(enable(skip))] + pub map: Option, + /// This field indicates that a `MediaSegment` is a sub-range of the + /// resource identified by its URI. + /// + /// ## Note + /// + /// This field is optional. + #[builder(default, setter(into))] + #[shorthand(enable(skip))] + pub byte_range: Option, + /// This field associates a date-range (i.e., a range of time defined by a + /// starting and ending date) with a set of attribute/value pairs. + /// + /// ## Note + /// + /// This field is optional. #[builder(default)] - map: Option, - /// The [`ExtXByteRange`] tag associated with the [`MediaSegment`]. + #[shorthand(enable(skip))] + pub date_range: Option, + /// This field indicates a discontinuity between the `MediaSegment` that + /// follows it and the one that preceded it. + /// + /// ## Note + /// + /// This field is required if any of the following characteristics change: + /// - file format + /// - number, type, and identifiers of tracks + /// - timestamp, sequence + /// + /// This field should be present if any of the following characteristics + /// change: + /// - encoding parameters + /// - encoding sequence #[builder(default)] - byte_range: Option, - /// The [`ExtXDateRange`] tag associated with the media segment. + #[shorthand(enable(skip))] + pub has_discontinuity: bool, + /// This field associates the first sample of a media segment with an + /// absolute date and/or time. + /// + /// ## Note + /// + /// This field is optional. #[builder(default)] - date_range: Option, - /// The [`ExtXDiscontinuity`] tag associated with the media segment. - #[builder(default)] - discontinuity: Option, - /// The [`ExtXProgramDateTime`] tag associated with the media - /// segment. - #[builder(default)] - program_date_time: Option, - /// The [`ExtInf`] tag associated with the [`MediaSegment`]. - inf: ExtInf, - /// The `URI` of the [`MediaSegment`]. + #[shorthand(enable(skip))] + pub program_date_time: Option, + /// This field indicates the duration of a media segment. + /// + /// ## Note + /// + /// This field is required. + #[shorthand(enable(skip))] + #[builder(setter(into))] + pub inf: ExtInf, + /// The URI of a media segment. + /// + /// ## Note + /// + /// This field is required. + #[builder(setter(into))] #[shorthand(enable(into))] uri: String, } impl MediaSegment { /// Returns a builder for a [`MediaSegment`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::MediaSegment; + /// use hls_m3u8::tags::ExtXMap; + /// use std::time::Duration; + /// + /// let segment = MediaSegment::builder() + /// .map(ExtXMap::new("https://www.example.com/")) + /// .byte_range(5..25) + /// .has_discontinuity(true) + /// .inf(Duration::from_secs(4)) + /// .uri("http://www.uri.com/") + /// .build()?; + /// # Ok::<(), String>(()) + /// ``` #[must_use] #[inline] pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } @@ -58,11 +195,23 @@ impl MediaSegmentBuilder { self } + + /// The number of a [`MediaSegment`]. Normally this should not be set + /// explicitly, because the [`MediaPlaylist::builder`] will automatically + /// apply the correct number. + /// + /// [`MediaPlaylist::builder`]: crate::MediaPlaylist::builder + pub fn number(&mut self, value: Option) -> &mut Self { + self.number = value; + self.explicit_number = Some(value.is_some()); + + self + } } impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // self.keys will be printed by MediaPlaylist! + // NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance. if let Some(value) = &self.map { writeln!(f, "{}", value)?; @@ -76,15 +225,15 @@ impl fmt::Display for MediaSegment { writeln!(f, "{}", value)?; } - if let Some(value) = &self.discontinuity { - writeln!(f, "{}", value)?; + if self.has_discontinuity { + writeln!(f, "{}", ExtXDiscontinuity)?; } if let Some(value) = &self.program_date_time { writeln!(f, "{}", value)?; } - writeln!(f, "{}", self.inf)?; // TODO: there might be a `,` missing + writeln!(f, "{}", self.inf)?; writeln!(f, "{}", self.uri)?; Ok(()) } @@ -97,7 +246,13 @@ impl RequiredVersion for MediaSegment { self.map, self.byte_range, self.date_range, - self.discontinuity, + { + if self.has_discontinuity { + Some(ExtXDiscontinuity) + } else { + None + } + }, self.program_date_time, self.inf ] @@ -123,8 +278,7 @@ mod tests { MediaSegment::builder() .map(ExtXMap::new("https://www.example.com/")) .byte_range(ExtXByteRange::from(5..25)) - //.date_range() // TODO! - .discontinuity(ExtXDiscontinuity) + .has_discontinuity(true) .inf(ExtInf::new(Duration::from_secs(4))) .uri("http://www.uri.com/") .build() diff --git a/src/tags/media_segment/mod.rs b/src/tags/media_segment/mod.rs index 33539b1..500751f 100644 --- a/src/tags/media_segment/mod.rs +++ b/src/tags/media_segment/mod.rs @@ -1,15 +1,15 @@ -mod byte_range; -mod date_range; -mod discontinuity; -mod inf; -mod key; -mod map; -mod program_date_time; +pub(crate) mod byte_range; +pub(crate) mod date_range; +pub(crate) mod discontinuity; +pub(crate) mod inf; +pub(crate) mod key; +pub(crate) mod map; +pub(crate) mod program_date_time; pub use byte_range::*; -pub use date_range::*; +pub use date_range::ExtXDateRange; pub(crate) use discontinuity::*; pub use inf::*; -pub use key::*; +pub use key::ExtXKey; pub use map::*; pub use program_date_time::*; diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 9f7a0bd..4b52c9b 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -10,7 +10,7 @@ use crate::{Error, RequiredVersion}; /// /// [`MediaSegment`]: crate::MediaSegment #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct ExtXIndependentSegments; +pub(crate) struct ExtXIndependentSegments; impl ExtXIndependentSegments { pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS"; diff --git a/src/tags/shared/mod.rs b/src/tags/shared/mod.rs index 956efe7..d2403fc 100644 --- a/src/tags/shared/mod.rs +++ b/src/tags/shared/mod.rs @@ -1,5 +1,5 @@ -mod independent_segments; -mod start; +pub(crate) mod independent_segments; +pub(crate) mod start; -pub use independent_segments::*; +pub(crate) use independent_segments::ExtXIndependentSegments; pub use start::*; From 112c3998b8b741bd25178ac09d429f4baea9cad0 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:27:36 +0100 Subject: [PATCH 077/112] improve ExtXDiscontinuitySequence --- .../media_playlist/discontinuity_sequence.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 96972ed..2909d7b 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -8,23 +8,15 @@ use crate::utils::tag; use crate::Error; use crate::RequiredVersion; -/// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] -/// -/// The [`ExtXDiscontinuitySequence`] tag allows synchronization between -/// different renditions of the same [`VariantStream`] or different -/// [`VariantStream`]s that have [`ExtXDiscontinuity`] tags in their -/// [`MediaPlaylist`]s. +/// Allows synchronization between different renditions of the same +/// [`VariantStream`]. /// /// [`VariantStream`]: crate::tags::VariantStream -/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity -/// [`MediaPlaylist`]: crate::MediaPlaylist -/// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3 #[derive(ShortHand, Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] #[shorthand(enable(must_use))] pub struct ExtXDiscontinuitySequence { /// Returns the discontinuity sequence number of - /// the first media segment that appears in the associated playlist. + /// the first [`MediaSegment`] that appears in the associated playlist. /// /// # Example /// @@ -35,6 +27,8 @@ pub struct ExtXDiscontinuitySequence { /// discontinuity_sequence.set_seq_num(10); /// assert_eq!(discontinuity_sequence.seq_num(), 10); /// ``` + /// + /// [`MediaSegment`]: crate::MediaSegment seq_num: u64, } From 285d2eccb888e4e78015f932112b6525f890d0b5 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:41:36 +0100 Subject: [PATCH 078/112] internalize ExtXEndList --- src/tags/media_playlist/end_list.rs | 10 +++------- src/tags/media_playlist/mod.rs | 2 +- tests/media_playlist.rs | 6 +++--- tests/rfc8216.rs | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index e848be3..91992bc 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -5,17 +5,13 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.3.4. EXT-X-ENDLIST] -/// -/// The [`ExtXEndList`] tag indicates, that no more [`MediaSegment`]s will be -/// added to the [`MediaPlaylist`] file. +/// Indicates that no more [`MediaSegment`]s will be added to the +/// [`MediaPlaylist`] file. /// /// [`MediaSegment`]: crate::MediaSegment /// [`MediaPlaylist`]: 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, PartialOrd, Ord)] -pub struct ExtXEndList; +pub(crate) struct ExtXEndList; impl ExtXEndList { pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST"; diff --git a/src/tags/media_playlist/mod.rs b/src/tags/media_playlist/mod.rs index af04ff9..00dc65d 100644 --- a/src/tags/media_playlist/mod.rs +++ b/src/tags/media_playlist/mod.rs @@ -6,7 +6,7 @@ mod playlist_type; mod target_duration; pub use discontinuity_sequence::*; -pub use end_list::*; +pub(crate) use end_list::*; pub use i_frames_only::*; pub use media_sequence::*; pub use playlist_type::*; diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index 338ead9..26dc141 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -5,7 +5,7 @@ use std::time::Duration; -use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXEndList, ExtXMediaSequence, ExtXTargetDuration}; +use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration}; use hls_m3u8::types::PlaylistType; use hls_m3u8::{MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -99,7 +99,7 @@ generate_tests! { // .unknown(vec![ // "#ZEN-TOTAL-DURATION:57.9911".into() // ]) - .end_list(ExtXEndList) + .has_end_list(true) .build() .unwrap(), concat!( @@ -227,7 +227,7 @@ generate_tests! { .build() .unwrap(), ]) - .end_list(ExtXEndList) + .has_end_list(true) .unknown(vec![ // deprecated tag: "#EXT-X-ALLOW-CACHE:YES".into() diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index 9d16727..5db18ec 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -2,7 +2,7 @@ use std::time::Duration; use hls_m3u8::tags::{ - ExtInf, ExtXEndList, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream, + ExtInf, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream, }; use hls_m3u8::types::{DecryptionKey, EncryptionMethod, MediaType, StreamData}; use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment}; @@ -42,7 +42,7 @@ generate_tests! { .build() .unwrap(), ]) - .end_list(ExtXEndList) + .has_end_list(true) .build() .unwrap(), concat!( From c56a56abe8e93490f560c4bee912e93e20c507d7 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:49:53 +0100 Subject: [PATCH 079/112] internalize ExtXIFramesOnly --- src/tags/media_playlist/i_frames_only.rs | 13 +------------ src/tags/media_playlist/mod.rs | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index d7334cb..22971da 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -5,19 +5,8 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] -/// -/// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the -/// Playlist describes a single I-frame. I-frames are encoded video -/// frames, whose decoding does not depend on any other frame. I-frame -/// Playlists can be used for trick play, such as fast forward, rapid -/// reverse, and scrubbing. -/// -/// [`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, PartialOrd, Ord)] -pub struct ExtXIFramesOnly; +pub(crate) struct ExtXIFramesOnly; impl ExtXIFramesOnly { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY"; diff --git a/src/tags/media_playlist/mod.rs b/src/tags/media_playlist/mod.rs index 00dc65d..ca57eae 100644 --- a/src/tags/media_playlist/mod.rs +++ b/src/tags/media_playlist/mod.rs @@ -7,7 +7,7 @@ mod target_duration; pub use discontinuity_sequence::*; pub(crate) use end_list::*; -pub use i_frames_only::*; +pub(crate) use i_frames_only::*; pub use media_sequence::*; pub use playlist_type::*; pub use target_duration::*; From 99b6b23accb2c115d0db5d1cb5e05dc1b1db02f5 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:08:26 +0100 Subject: [PATCH 080/112] internalize ExtMediaSequence --- src/tags/media_playlist/media_sequence.rs | 71 +++-------------------- src/tags/media_playlist/mod.rs | 2 +- tests/media_playlist.rs | 10 ++-- tests/rfc8216.rs | 8 +-- 4 files changed, 17 insertions(+), 74 deletions(-) diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 224ca88..20c59f8 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -5,60 +5,13 @@ use crate::types::ProtocolVersion; use crate::utils::tag; 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. -/// -/// [Media Segment]: crate::MediaSegment -/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2 -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct ExtXMediaSequence(u64); +/// Indicates the Media Sequence Number of the first `MediaSegment` that +/// appears in a `MediaPlaylist`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ExtXMediaSequence(pub usize); impl ExtXMediaSequence { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; - - /// Makes a new [`ExtXMediaSequence`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMediaSequence; - /// let media_sequence = ExtXMediaSequence::new(5); - /// ``` - #[must_use] - pub const fn new(seq_num: u64) -> Self { Self(seq_num) } - - /// Returns the sequence number of the first media segment, - /// that appears in the associated playlist. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMediaSequence; - /// let media_sequence = ExtXMediaSequence::new(5); - /// - /// assert_eq!(media_sequence.seq_num(), 5); - /// ``` - #[must_use] - pub const fn seq_num(self) -> u64 { self.0 } - - /// Sets the sequence number. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMediaSequence; - /// let mut media_sequence = ExtXMediaSequence::new(5); - /// - /// media_sequence.set_seq_num(10); - /// assert_eq!(media_sequence.seq_num(), 10); - /// ``` - pub fn set_seq_num(&mut self, value: u64) -> &mut Self { - self.0 = value; - self - } } /// This tag requires [`ProtocolVersion::V1`]. @@ -80,7 +33,7 @@ impl FromStr for ExtXMediaSequence { let input = tag(input, Self::PREFIX)?; let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?; - Ok(Self::new(seq_num)) + Ok(Self(seq_num)) } } @@ -92,7 +45,7 @@ mod test { #[test] fn test_display() { assert_eq!( - ExtXMediaSequence::new(123).to_string(), + ExtXMediaSequence(123).to_string(), "#EXT-X-MEDIA-SEQUENCE:123".to_string() ); } @@ -100,7 +53,7 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXMediaSequence::new(123).required_version(), + ExtXMediaSequence(123).required_version(), ProtocolVersion::V1 ); } @@ -108,16 +61,8 @@ mod test { #[test] fn test_parser() { assert_eq!( - ExtXMediaSequence::new(123), + ExtXMediaSequence(123), "#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap() ); } - - #[test] - fn test_seq_num() { - let mut sequence = ExtXMediaSequence::new(123); - assert_eq!(sequence.seq_num(), 123); - sequence.set_seq_num(1); - assert_eq!(sequence.seq_num(), 1); - } } diff --git a/src/tags/media_playlist/mod.rs b/src/tags/media_playlist/mod.rs index ca57eae..985b6aa 100644 --- a/src/tags/media_playlist/mod.rs +++ b/src/tags/media_playlist/mod.rs @@ -8,6 +8,6 @@ mod target_duration; pub use discontinuity_sequence::*; pub(crate) use end_list::*; pub(crate) use i_frames_only::*; -pub use media_sequence::*; pub use playlist_type::*; +pub(crate) use media_sequence::*; pub use target_duration::*; diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index 26dc141..b359a10 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -5,7 +5,7 @@ use std::time::Duration; -use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration}; +use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXTargetDuration}; use hls_m3u8::types::PlaylistType; use hls_m3u8::{MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -26,8 +26,8 @@ macro_rules! generate_tests { generate_tests! { test_media_playlist_with_byterange => { MediaPlaylist::builder() + .media_sequence(1) .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) - .media_sequence(ExtXMediaSequence::new(0)) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) @@ -54,7 +54,7 @@ generate_tests! { "#EXTM3U\n", "#EXT-X-VERSION:4\n", "#EXT-X-TARGETDURATION:10\n", - "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXT-X-MEDIA-SEQUENCE:1\n", "#EXT-X-BYTERANGE:75232@0\n", "#EXTINF:10,\n", @@ -121,7 +121,7 @@ generate_tests! { test_allow_cache => { MediaPlaylist::builder() .target_duration(Duration::from_secs(10)) - .media_sequence(0) + .media_sequence(1) .playlist_type(PlaylistType::Vod) .segments(vec![ MediaSegment::builder() @@ -238,7 +238,7 @@ generate_tests! { "#EXTM3U\n", "#EXT-X-VERSION:4\n", "#EXT-X-TARGETDURATION:10\n", - "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXT-X-MEDIA-SEQUENCE:1\n", "#EXT-X-PLAYLIST-TYPE:VOD\n", "#EXT-X-BYTERANGE:522828@0\n", diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index 5db18ec..41a19bd 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -1,9 +1,7 @@ // https://tools.ietf.org/html/rfc8216#section-8 use std::time::Duration; -use hls_m3u8::tags::{ - ExtInf, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream, -}; +use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, ExtXTargetDuration, VariantStream}; use hls_m3u8::types::{DecryptionKey, EncryptionMethod, MediaType, StreamData}; use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -61,7 +59,7 @@ generate_tests! { test_live_media_playlist_using_https => { MediaPlaylist::builder() .target_duration(ExtXTargetDuration::new(Duration::from_secs(8))) - .media_sequence(ExtXMediaSequence::new(2680)) + .media_sequence(2680) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(7.975))) @@ -97,7 +95,7 @@ generate_tests! { test_media_playlist_with_encrypted_segments => { MediaPlaylist::builder() .target_duration(ExtXTargetDuration::new(Duration::from_secs(15))) - .media_sequence(ExtXMediaSequence::new(7794)) + .media_sequence(7794) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(2.833))) From f48876ee0786de1f782b7a063a96a9769c8d58b4 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:21:11 +0100 Subject: [PATCH 081/112] internalize ExtXDiscontinuitySequence --- .../media_playlist/discontinuity_sequence.rs | 55 +++---------------- src/tags/media_playlist/mod.rs | 2 +- 2 files changed, 9 insertions(+), 48 deletions(-) diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 2909d7b..0dbe218 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,50 +1,19 @@ use std::fmt; use std::str::FromStr; -use shorthand::ShortHand; - use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; -use crate::RequiredVersion; +use crate::{Error, RequiredVersion}; /// Allows synchronization between different renditions of the same /// [`VariantStream`]. /// /// [`VariantStream`]: crate::tags::VariantStream -#[derive(ShortHand, Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -#[shorthand(enable(must_use))] -pub struct ExtXDiscontinuitySequence { - /// Returns the discontinuity sequence number of - /// the first [`MediaSegment`] that appears in the associated playlist. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; - /// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5); - /// - /// discontinuity_sequence.set_seq_num(10); - /// assert_eq!(discontinuity_sequence.seq_num(), 10); - /// ``` - /// - /// [`MediaSegment`]: crate::MediaSegment - seq_num: u64, -} +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub(crate) struct ExtXDiscontinuitySequence(pub usize); impl ExtXDiscontinuitySequence { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; - - /// Makes a new [`ExtXDiscontinuitySequence`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; - /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); - /// ``` - #[must_use] - pub const fn new(seq_num: u64) -> Self { Self { seq_num } } } /// This tag requires [`ProtocolVersion::V1`]. @@ -55,7 +24,7 @@ impl RequiredVersion for ExtXDiscontinuitySequence { impl fmt::Display for ExtXDiscontinuitySequence { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // - write!(f, "{}{}", Self::PREFIX, self.seq_num) + write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -66,7 +35,7 @@ impl FromStr for ExtXDiscontinuitySequence { let input = tag(input, Self::PREFIX)?; let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?; - Ok(Self::new(seq_num)) + Ok(Self(seq_num)) } } @@ -78,7 +47,7 @@ mod test { #[test] fn test_display() { assert_eq!( - ExtXDiscontinuitySequence::new(123).to_string(), + ExtXDiscontinuitySequence(123).to_string(), "#EXT-X-DISCONTINUITY-SEQUENCE:123".to_string() ); } @@ -86,7 +55,7 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXDiscontinuitySequence::new(123).required_version(), + ExtXDiscontinuitySequence(123).required_version(), ProtocolVersion::V1 ) } @@ -94,7 +63,7 @@ mod test { #[test] fn test_parser() { assert_eq!( - ExtXDiscontinuitySequence::new(123), + ExtXDiscontinuitySequence(123), "#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap() ); @@ -103,12 +72,4 @@ mod test { Err(Error::parse_int("12A", "12A".parse::().expect_err(""))) ); } - - #[test] - fn test_seq_num() { - let mut sequence = ExtXDiscontinuitySequence::new(123); - assert_eq!(sequence.seq_num(), 123); - sequence.set_seq_num(1); - assert_eq!(sequence.seq_num(), 1); - } } diff --git a/src/tags/media_playlist/mod.rs b/src/tags/media_playlist/mod.rs index 985b6aa..f896a93 100644 --- a/src/tags/media_playlist/mod.rs +++ b/src/tags/media_playlist/mod.rs @@ -5,7 +5,7 @@ mod media_sequence; mod playlist_type; mod target_duration; -pub use discontinuity_sequence::*; +pub(crate) use discontinuity_sequence::*; pub(crate) use end_list::*; pub(crate) use i_frames_only::*; pub use playlist_type::*; From 429f3f8c3d3d24433c77b003857499e9fa9ed344 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:37:47 +0100 Subject: [PATCH 082/112] internalize ExtXTargetDuration --- src/tags/media_playlist/mod.rs | 14 ++--- src/tags/media_playlist/target_duration.rs | 69 ++++------------------ src/tags/mod.rs | 2 +- tests/media_playlist.rs | 6 +- tests/rfc8216.rs | 8 +-- 5 files changed, 24 insertions(+), 75 deletions(-) diff --git a/src/tags/media_playlist/mod.rs b/src/tags/media_playlist/mod.rs index f896a93..bb4e53a 100644 --- a/src/tags/media_playlist/mod.rs +++ b/src/tags/media_playlist/mod.rs @@ -1,13 +1,11 @@ -mod discontinuity_sequence; -mod end_list; -mod i_frames_only; -mod media_sequence; -mod playlist_type; -mod target_duration; +pub(crate) mod discontinuity_sequence; +pub(crate) mod end_list; +pub(crate) mod i_frames_only; +pub(crate) mod media_sequence; +pub(crate) mod target_duration; pub(crate) use discontinuity_sequence::*; pub(crate) use end_list::*; pub(crate) use i_frames_only::*; -pub use playlist_type::*; pub(crate) use media_sequence::*; -pub use target_duration::*; +pub(crate) use target_duration::*; diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 9ef6e3c..4d97561 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -2,57 +2,16 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use derive_more::Deref; - use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.3.1. EXT-X-TARGETDURATION] -/// -/// The [`ExtXTargetDuration`] tag specifies the maximum [`MediaSegment`] -/// duration. -/// -/// [`MediaSegment`]: crate::MediaSegment -/// [4.3.3.1. EXT-X-TARGETDURATION]: -/// https://tools.ietf.org/html/rfc8216#section-4.3.3.1 -#[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] -pub struct ExtXTargetDuration(Duration); +/// Specifies the maximum `MediaSegment` duration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] +pub(crate) struct ExtXTargetDuration(pub Duration); impl ExtXTargetDuration { pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; - - /// Makes a new [`ExtXTargetDuration`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXTargetDuration; - /// use std::time::Duration; - /// - /// let target_duration = ExtXTargetDuration::new(Duration::from_secs(20)); - /// ``` - /// - /// # Note - /// - /// The nanoseconds part of the [`Duration`] will be discarded. - #[must_use] - pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) } - - /// Returns the maximum media segment duration. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXTargetDuration; - /// use std::time::Duration; - /// - /// let target_duration = ExtXTargetDuration::new(Duration::from_nanos(2_000_000_000)); - /// - /// assert_eq!(target_duration.duration(), Duration::from_secs(2)); - /// ``` - #[must_use] - pub const fn duration(&self) -> Duration { self.0 } } /// This tag requires [`ProtocolVersion::V1`]. @@ -70,17 +29,14 @@ impl FromStr for ExtXTargetDuration { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - let input = input.parse().map_err(|e| Error::parse_int(input, e))?; + let input = tag(input, Self::PREFIX)? + .parse() + .map_err(|e| Error::parse_int(input, e))?; - Ok(Self::new(Duration::from_secs(input))) + Ok(Self(Duration::from_secs(input))) } } -impl From for ExtXTargetDuration { - fn from(value: Duration) -> Self { Self::new(value) } -} - #[cfg(test)] mod test { use super::*; @@ -89,7 +45,7 @@ mod test { #[test] fn test_display() { assert_eq!( - ExtXTargetDuration::new(Duration::from_secs(5)).to_string(), + ExtXTargetDuration(Duration::from_secs(5)).to_string(), "#EXT-X-TARGETDURATION:5".to_string() ); } @@ -97,7 +53,7 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXTargetDuration::new(Duration::from_secs(5)).required_version(), + ExtXTargetDuration(Duration::from_secs(5)).required_version(), ProtocolVersion::V1 ); } @@ -105,13 +61,8 @@ mod test { #[test] fn test_parser() { assert_eq!( - ExtXTargetDuration::new(Duration::from_secs(5)), + ExtXTargetDuration(Duration::from_secs(5)), "#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/mod.rs b/src/tags/mod.rs index 35ae4c4..ae45d74 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -10,6 +10,6 @@ mod shared; pub use basic::*; pub use master_playlist::*; -pub use media_playlist::*; +pub(crate) use media_playlist::*; pub use media_segment::*; pub use shared::*; diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index b359a10..ae8a955 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -5,7 +5,7 @@ use std::time::Duration; -use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXTargetDuration}; +use hls_m3u8::tags::{ExtInf, ExtXByteRange}; use hls_m3u8::types::PlaylistType; use hls_m3u8::{MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -27,7 +27,7 @@ generate_tests! { test_media_playlist_with_byterange => { MediaPlaylist::builder() .media_sequence(1) - .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .target_duration(Duration::from_secs(10)) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) @@ -72,7 +72,7 @@ generate_tests! { test_absolute_uris => { MediaPlaylist::builder() .playlist_type(PlaylistType::Vod) - .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .target_duration(Duration::from_secs(10)) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs(10))) diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index 41a19bd..174c5a8 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -1,7 +1,7 @@ // https://tools.ietf.org/html/rfc8216#section-8 use std::time::Duration; -use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, ExtXTargetDuration, VariantStream}; +use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, VariantStream}; use hls_m3u8::types::{DecryptionKey, EncryptionMethod, MediaType, StreamData}; use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -22,7 +22,7 @@ macro_rules! generate_tests { generate_tests! { test_simple_playlist => { MediaPlaylist::builder() - .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .target_duration(Duration::from_secs(10)) .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(9.009))) @@ -58,7 +58,7 @@ generate_tests! { }, test_live_media_playlist_using_https => { MediaPlaylist::builder() - .target_duration(ExtXTargetDuration::new(Duration::from_secs(8))) + .target_duration(Duration::from_secs(8)) .media_sequence(2680) .segments(vec![ MediaSegment::builder() @@ -94,7 +94,7 @@ generate_tests! { }, test_media_playlist_with_encrypted_segments => { MediaPlaylist::builder() - .target_duration(ExtXTargetDuration::new(Duration::from_secs(15))) + .target_duration(Duration::from_secs(15)) .media_sequence(7794) .segments(vec![ MediaSegment::builder() From 72c0ff9c757e681b523a7e10d784ed0573ddcb62 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:58:21 +0100 Subject: [PATCH 083/112] rename MediaSegment::inf to MediaSegment::duration --- src/media_segment.rs | 10 ++++----- tests/media_playlist.rs | 48 ++++++++++++++++++++--------------------- tests/rfc8216.rs | 20 ++++++++--------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/media_segment.rs b/src/media_segment.rs index 0053172..ec78722 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -149,7 +149,7 @@ pub struct MediaSegment { /// This field is required. #[shorthand(enable(skip))] #[builder(setter(into))] - pub inf: ExtInf, + pub duration: ExtInf, /// The URI of a media segment. /// /// ## Note @@ -174,7 +174,7 @@ impl MediaSegment { /// .map(ExtXMap::new("https://www.example.com/")) /// .byte_range(5..25) /// .has_discontinuity(true) - /// .inf(Duration::from_secs(4)) + /// .duration(Duration::from_secs(4)) /// .uri("http://www.uri.com/") /// .build()?; /// # Ok::<(), String>(()) @@ -233,7 +233,7 @@ impl fmt::Display for MediaSegment { writeln!(f, "{}", value)?; } - writeln!(f, "{}", self.inf)?; + writeln!(f, "{}", self.duration)?; writeln!(f, "{}", self.uri)?; Ok(()) } @@ -254,7 +254,7 @@ impl RequiredVersion for MediaSegment { } }, self.program_date_time, - self.inf + self.duration ] } } @@ -279,7 +279,7 @@ mod tests { .map(ExtXMap::new("https://www.example.com/")) .byte_range(ExtXByteRange::from(5..25)) .has_discontinuity(true) - .inf(ExtInf::new(Duration::from_secs(4))) + .duration(ExtInf::new(Duration::from_secs(4))) .uri("http://www.uri.com/") .build() .unwrap() diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index ae8a955..7477910 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -30,19 +30,19 @@ generate_tests! { .target_duration(Duration::from_secs(10)) .segments(vec![ MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(10.0))) + .duration(ExtInf::new(Duration::from_secs_f64(10.0))) .byte_range(ExtXByteRange::from(0..75232)) .uri("video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(10.0))) + .duration(ExtInf::new(Duration::from_secs_f64(10.0))) .byte_range(ExtXByteRange::from(752321..82112 + 752321)) .uri("video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(10.0))) + .duration(ExtInf::new(Duration::from_secs_f64(10.0))) .byte_range(ExtXByteRange::from(..69864)) .uri("video.ts") .build() @@ -75,22 +75,22 @@ generate_tests! { .target_duration(Duration::from_secs(10)) .segments(vec![ MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .uri("http://example.com/00001.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .uri("https://example.com/00002.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .uri("//example.com/00003.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .uri("http://example.com/00004.ts") .build() .unwrap(), @@ -125,103 +125,103 @@ generate_tests! { .playlist_type(PlaylistType::Vod) .segments(vec![ MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .uri("hls_450k_video.ts") .byte_range(0..522_828) .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(522_828..1_110_328) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(1_110_328..1_823_412) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(1_823_412..2_299_992) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(2_299_992..2_835_604) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(2_835_604..3_042_780) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(3_042_780..3_498_680) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(3_498_680..4_155_928) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(4_155_928..4_727_636) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(4_727_636..5_212_676) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(5_212_676..5_921_812) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(5_921_812..6_651_816) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(6_651_816..7_108_092) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(7_108_092..7_576_776) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(7_576_776..8_021_772) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs(10))) + .duration(ExtInf::new(Duration::from_secs(10))) .byte_range(8_021_772..8_353_216) .uri("hls_450k_video.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(1.4167))) + .duration(ExtInf::new(Duration::from_secs_f64(1.4167))) .byte_range(8_353_216..8_397_772) .uri("hls_450k_video.ts") .build() diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index 174c5a8..cec0e96 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -25,17 +25,17 @@ generate_tests! { .target_duration(Duration::from_secs(10)) .segments(vec![ MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(9.009))) + .duration(ExtInf::new(Duration::from_secs_f64(9.009))) .uri("http://media.example.com/first.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(9.009))) + .duration(ExtInf::new(Duration::from_secs_f64(9.009))) .uri("http://media.example.com/second.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(3.003))) + .duration(ExtInf::new(Duration::from_secs_f64(3.003))) .uri("http://media.example.com/third.ts") .build() .unwrap(), @@ -62,17 +62,17 @@ generate_tests! { .media_sequence(2680) .segments(vec![ MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(7.975))) + .duration(ExtInf::new(Duration::from_secs_f64(7.975))) .uri("https://priv.example.com/fileSequence2680.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(7.941))) + .duration(ExtInf::new(Duration::from_secs_f64(7.941))) .uri("https://priv.example.com/fileSequence2681.ts") .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(7.975))) + .duration(ExtInf::new(Duration::from_secs_f64(7.975))) .uri("https://priv.example.com/fileSequence2682.ts") .build() .unwrap(), @@ -98,7 +98,7 @@ generate_tests! { .media_sequence(7794) .segments(vec![ MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(2.833))) + .duration(ExtInf::new(Duration::from_secs_f64(2.833))) .keys(vec![ ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, @@ -109,7 +109,7 @@ generate_tests! { .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(15.0))) + .duration(ExtInf::new(Duration::from_secs_f64(15.0))) .keys(vec![ ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, @@ -120,7 +120,7 @@ generate_tests! { .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(13.333))) + .duration(ExtInf::new(Duration::from_secs_f64(13.333))) .keys(vec![ ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, @@ -131,7 +131,7 @@ generate_tests! { .build() .unwrap(), MediaSegment::builder() - .inf(ExtInf::new(Duration::from_secs_f64(15.0))) + .duration(ExtInf::new(Duration::from_secs_f64(15.0))) .keys(vec![ ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, From c268fa3a8278c3baead95986c5a552aa79a6534a Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 14:10:27 +0100 Subject: [PATCH 084/112] rewrite MediaPlaylist --- src/media_playlist.rs | 664 ++++++++++++++++++++++++++++++++---------- 1 file changed, 506 insertions(+), 158 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 9eb76b0..e213c20 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,10 +1,9 @@ -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; use std::fmt; use std::str::FromStr; use std::time::Duration; use derive_builder::Builder; -use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::media_segment::MediaSegment; @@ -17,102 +16,122 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// Media playlist. -#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] -#[builder(build_fn(validate = "Self::validate"))] -#[builder(setter(into, strip_option))] -#[shorthand(enable(must_use, collection_magic, get_mut))] +#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] +#[builder(build_fn(skip), setter(strip_option))] +#[non_exhaustive] pub struct MediaPlaylist { - /// The [`ExtXTargetDuration`] tag of the playlist. + /// Specifies the maximum [`MediaSegment::duration`]. A typical target + /// duration is 10 seconds. /// - /// # Note + /// ### Note /// /// This field is required. - #[shorthand(enable(copy))] - target_duration: ExtXTargetDuration, - /// Sets the [`ExtXMediaSequence`] tag. + pub target_duration: Duration, + /// The [`MediaSegment::number`] of the first [`MediaSegment`] that + /// appears in a [`MediaPlaylist`]. /// - /// # Note + /// ### Note + /// + /// This field is optional and by default a value of 0 is assumed. + #[builder(default)] + pub media_sequence: usize, + /// Allows synchronization between different renditions of the same + /// [`VariantStream`]. + /// + /// ### Note + /// + /// This field is optional and by default a vaule of 0 is assumed. + /// + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default)] + pub discontinuity_sequence: usize, + /// Provides mutability information about a [`MediaPlaylist`]. + /// + /// - [`PlaylistType::Vod`] indicates that the playlist must not change. + /// + /// - [`PlaylistType::Event`] indicates that the server does not change or + /// delete any part of the playlist, but may append new lines to it. + /// + /// ### Note + /// + /// This field is optional. + #[builder(default, setter(into))] + pub playlist_type: Option, + /// Indicates that each [`MediaSegment`] in the playlist describes a single + /// I-frame. I-frames are encoded video frames, whose decoding does not + /// depend on any other frame. I-frame Playlists can be used for trick + /// play, such as fast forward, rapid reverse, and scrubbing. + /// + /// ### Note /// /// This field is optional. #[builder(default)] - media_sequence: Option, - /// Sets the [`ExtXDiscontinuitySequence`] tag. + pub has_i_frames_only: bool, + /// This indicates that all media samples in a [`MediaSegment`] can be + /// decoded without information from other segments. /// - /// # Note + /// ### Note + /// + /// This field is optional and by default `false`. If the value is `true` it + /// applies to every [`MediaSegment`] in this [`MediaPlaylist`]. + #[builder(default)] + pub has_independent_segments: bool, + /// Indicates a preferred point at which to start playing a playlist. By + /// default, clients should start playback at this point when beginning a + /// playback session. + /// + /// ### Note /// /// This field is optional. + #[builder(default, setter(into))] + pub start: Option, + /// Indicates that no more [`MediaSegment`]s will be added to the + /// [`MediaPlaylist`] file. + /// + /// ### Note + /// + /// This field is optional and by default `false`. + /// A `false` indicates that the client should reload the [`MediaPlaylist`] + /// from the server, until a playlist is encountered, where this field is + /// `true`. #[builder(default)] - discontinuity_sequence: Option, - /// Sets the [`PlaylistType`] tag. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - playlist_type: Option, - /// Sets the [`ExtXIFramesOnly`] tag. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - i_frames_only: Option, - /// Sets the [`ExtXIndependentSegments`] tag. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - independent_segments: Option, - /// Sets the [`ExtXStart`] tag. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - start: Option, - /// Sets the [`ExtXEndList`] tag. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - end_list: Option, + pub has_end_list: bool, /// A list of all [`MediaSegment`]s. /// - /// # Note + /// ### Note /// /// This field is required. - segments: Vec, + #[builder(setter(custom))] + pub segments: BTreeMap, /// The allowable excess duration of each media segment in the /// associated playlist. /// - /// # Error + /// ### Error /// /// If there is a media segment of which duration exceeds /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, /// the invocation of `MediaPlaylistBuilder::build()` method will fail. /// /// - /// # Note + /// ### Note /// /// This field is optional and the default value is /// `Duration::from_secs(0)`. #[builder(default = "Duration::from_secs(0)")] - allowable_excess_duration: Duration, + pub allowable_excess_duration: Duration, /// A list of unknown tags. /// - /// # Note + /// ### Note /// /// This field is optional. - #[builder(default)] - unknown_tags: Vec, + #[builder(default, setter(into))] + pub unknown: Vec, } impl MediaPlaylistBuilder { fn validate(&self) -> Result<(), String> { if let Some(target_duration) = &self.target_duration { - self.validate_media_segments(target_duration.duration()) + self.validate_media_segments(*target_duration) .map_err(|e| e.to_string())?; } @@ -123,24 +142,53 @@ impl MediaPlaylistBuilder { let mut last_range_uri = None; if let Some(segments) = &self.segments { - for s in segments { - // CHECK: `#EXT-X-TARGETDURATION` - let segment_duration = s.inf().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) - } - }; + // verify the independent segments + if self.has_independent_segments.unwrap_or(false) { + // If the encryption METHOD is AES-128 and the Playlist contains an EXT- + // X-I-FRAMES-ONLY tag, the entire resource MUST be encrypted using + // AES-128 CBC with PKCS7 padding [RFC5652]. + // + // from the rfc: https://tools.ietf.org/html/rfc8216#section-6.2.3 - let max_segment_duration = { - if let Some(value) = &self.allowable_excess_duration { - target_duration + *value - } else { - target_duration + let is_aes128 = segments + .values() + // convert iterator of segments to iterator of keys + .flat_map(|s| s.keys.iter()) + // filter out all empty keys + .filter_map(ExtXKey::as_ref) + .any(|k| k.method == EncryptionMethod::Aes128); + + if is_aes128 { + for key in segments.values().flat_map(|s| s.keys.iter()) { + if let ExtXKey(Some(key)) = key { + if key.method != EncryptionMethod::Aes128 { + return Err(Error::custom(concat!( + "if any independent segment is encrypted with Aes128,", + " all must be encrypted with Aes128" + ))); + } + } else { + return Err(Error::custom(concat!( + "if any independent segment is encrypted with Aes128,", + " all must be encrypted with Aes128" + ))); + } } - }; + } + } + + for segment in segments.values() { + // CHECK: `#EXT-X-TARGETDURATION` + let segment_duration = segment.duration.duration(); + + // round the duration if it is .5s + let rounded_segment_duration = + Duration::from_secs(segment_duration.as_secs_f64().round() as u64); + + let max_segment_duration = self + .allowable_excess_duration + .as_ref() + .map_or(target_duration, |value| target_duration + *value); if rounded_segment_duration > max_segment_duration { return Err(Error::custom(format!( @@ -148,19 +196,19 @@ impl MediaPlaylistBuilder { segment_duration, max_segment_duration, target_duration, - s.uri() + segment.uri() ))); } // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(range) = s.byte_range() { + if let Some(range) = &segment.byte_range { if range.start().is_none() { - let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?; - if last_uri != s.uri() { + // TODO: error messages + if last_range_uri.ok_or_else(Error::invalid_input)? != segment.uri() { return Err(Error::invalid_input()); } } else { - last_range_uri = Some(s.uri()); + last_range_uri = Some(segment.uri()); } } else { last_range_uri = None; @@ -171,13 +219,20 @@ impl MediaPlaylistBuilder { Ok(()) } - /// 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 { - segments.push(value.into()); - } else { - self.segments = Some(vec![value.into()]); - } + /// Adds a media segment to the resulting playlist and assigns the next free + /// [`MediaSegment::number`] to the segment. + pub fn push_segment(&mut self, segment: MediaSegment) -> &mut Self { + let segments = self.segments.get_or_insert_with(BTreeMap::new); + + let number = { + if segment.explicit_number { + segment.number + } else { + segments.keys().last().copied().unwrap_or(0) + 1 + } + }; + + segments.insert(number, segment); self } @@ -185,19 +240,150 @@ impl MediaPlaylistBuilder { pub fn parse(&mut self, input: &str) -> crate::Result { parse_media_playlist(input, self) } + + /// Adds segments to the resulting playlist and assigns a + /// [`MediaSegment::number`] to each segment. + /// + /// ## Note + /// + /// The [`MediaSegment::number`] will be assigned based on the order of the + /// input (e.g. the first element will be 0, second element 1, ..) or if a + /// number has been set explicitly. This function assumes, that all segments + /// will be present in the final media playlist and the following is only + /// possible if the segment is marked with `ExtXDiscontinuity`. + pub fn segments(&mut self, segments: Vec) -> &mut Self { + // media segments are numbered starting at either 0 or the discontinuity + // sequence, but it might not be available at the moment. + // + // -> final numbering will be applied in the build function + self.segments = Some(segments.into_iter().enumerate().collect()); + self + } + + pub fn build(&self) -> Result { + // validate builder + self.validate()?; + + let sequence_number = self.media_sequence.unwrap_or(0); + + let segments = self + .segments + .as_ref() + .ok_or_else(|| "missing field `segments`".to_string())?; + + // insert all explictly numbered segments into the result + let mut result_segments = segments + .iter() + .filter_map(|(_, s)| { + if s.explicit_number { + Some((s.number, s.clone())) + } else { + None + } + }) + .collect::>(); + + // no segment should exist before the sequence_number + if let Some(first_segment) = result_segments.keys().min() { + if sequence_number > *first_segment { + return Err(format!( + "there should be no segment ({}) before the sequence_number ({})", + first_segment, sequence_number, + )); + } + } + + let mut position = sequence_number; + for segment in segments + .iter() + .filter_map(|(_, s)| if s.explicit_number { None } else { Some(s) }) + { + while result_segments.contains_key(&position) { + position += 1; + } + + let mut segment = segment.clone(); + segment.number = position; + + result_segments.insert(segment.number, segment); + position += 1; + } + + let mut previous_n = None; + + for n in result_segments.keys() { + if let Some(previous_n) = previous_n { + if previous_n + 1 != *n { + return Err(format!("missing segment ({})", previous_n + 1)); + } + } + + previous_n = Some(n); + } + + Ok(MediaPlaylist { + target_duration: self + .target_duration + .ok_or_else(|| "missing field `target_duration`".to_string())?, + media_sequence: self.media_sequence.unwrap_or(0), + discontinuity_sequence: self.discontinuity_sequence.unwrap_or(0), + playlist_type: self.playlist_type.unwrap_or(None), + has_i_frames_only: self.has_i_frames_only.unwrap_or(false), + has_independent_segments: self.has_independent_segments.unwrap_or(false), + start: self.start.unwrap_or(None), + has_end_list: self.has_end_list.unwrap_or(false), + segments: result_segments, + allowable_excess_duration: self + .allowable_excess_duration + .unwrap_or_else(|| Duration::from_secs(0)), + unknown: self.unknown.clone().unwrap_or_else(Vec::new), + }) + } } impl RequiredVersion for MediaPlaylistBuilder { fn required_version(&self) -> ProtocolVersion { required_version![ - self.target_duration, - self.media_sequence, - self.discontinuity_sequence, + self.target_duration.map(ExtXTargetDuration), + { + if self.media_sequence.unwrap_or(0) != 0 { + Some(ExtXMediaSequence(self.media_sequence.unwrap_or(0))) + } else { + None + } + }, + { + if self.discontinuity_sequence.unwrap_or(0) != 0 { + Some(ExtXDiscontinuitySequence( + self.discontinuity_sequence.unwrap_or(0), + )) + } else { + None + } + }, self.playlist_type, - self.i_frames_only, - self.independent_segments, + { + if self.has_i_frames_only.unwrap_or(false) { + Some(ExtXIFramesOnly) + } else { + None + } + }, + { + if self.has_independent_segments.unwrap_or(false) { + Some(ExtXIndependentSegments) + } else { + None + } + }, self.start, - self.end_list, + { + if self.has_end_list.unwrap_or(false) { + Some(ExtXEndList) + } else { + None + } + }, self.segments ] } @@ -208,19 +394,55 @@ impl MediaPlaylist { #[must_use] #[inline] pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } + + /// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment + /// duration together. + pub fn duration(&self) -> Duration { + self.segments.values().map(|s| s.duration.duration()).sum() + } } impl RequiredVersion for MediaPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ - self.target_duration, - self.media_sequence, - self.discontinuity_sequence, + ExtXTargetDuration(self.target_duration), + { + if self.media_sequence != 0 { + Some(ExtXMediaSequence(self.media_sequence)) + } else { + None + } + }, + { + if self.discontinuity_sequence != 0 { + Some(ExtXDiscontinuitySequence(self.discontinuity_sequence)) + } else { + None + } + }, self.playlist_type, - self.i_frames_only, - self.independent_segments, + { + if self.has_i_frames_only { + Some(ExtXIFramesOnly) + } else { + None + } + }, + { + if self.has_independent_segments { + Some(ExtXIndependentSegments) + } else { + None + } + }, self.start, - self.end_list, + { + if self.has_end_list { + Some(ExtXEndList) + } else { + None + } + }, self.segments ] } @@ -234,55 +456,75 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } - writeln!(f, "{}", self.target_duration)?; + writeln!(f, "{}", ExtXTargetDuration(self.target_duration))?; - if let Some(value) = &self.media_sequence { - writeln!(f, "{}", value)?; + if self.media_sequence != 0 { + writeln!(f, "{}", ExtXMediaSequence(self.media_sequence))?; } - if let Some(value) = &self.discontinuity_sequence { - writeln!(f, "{}", value)?; + if self.discontinuity_sequence != 0 { + writeln!( + f, + "{}", + ExtXDiscontinuitySequence(self.discontinuity_sequence) + )?; } if let Some(value) = &self.playlist_type { writeln!(f, "{}", value)?; } - if let Some(value) = &self.i_frames_only { - writeln!(f, "{}", value)?; + if self.has_i_frames_only { + writeln!(f, "{}", ExtXIFramesOnly)?; } - if let Some(value) = &self.independent_segments { - writeln!(f, "{}", value)?; + if self.has_independent_segments { + writeln!(f, "{}", ExtXIndependentSegments)?; } if let Some(value) = &self.start { writeln!(f, "{}", value)?; } - // most likely only 1 ExtXKey will be in the HashSet: - let mut available_keys = HashSet::with_capacity(1); + let mut available_keys = HashSet::new(); - for segment in &self.segments { - for key in segment.keys() { + for segment in self.segments.values() { + for key in &segment.keys { // the key is new: - if available_keys.insert(key) { - let mut remove_key = None; + if let ExtXKey(Some(decryption_key)) = key { + // TODO: this piece should be linted by clippy? - // an old key might be removed: - for k in &available_keys { - if k.key_format() == key.key_format() && &key != k { - remove_key = Some(*k); - break; + // next segment will be encrypted, so the segment can not have an empty key + available_keys.remove(&ExtXKey::empty()); + + // only do something if a key has been overwritten + if available_keys.insert(key) { + let mut remove_key = None; + + // an old key might be removed: + for k in &available_keys { + if let ExtXKey(Some(dk)) = k { + if dk.format == decryption_key.format && &key != k { + remove_key = Some(*k); + break; + } + } else { + unreachable!("empty keys should not exist in `available_keys`"); + } } - } - if let Some(k) = remove_key { - // this should always be true: - let res = available_keys.remove(k); - debug_assert!(res); - } + if let Some(k) = remove_key { + // this should always be true: + let res = available_keys.remove(k); + debug_assert!(res); + } + writeln!(f, "{}", key)?; + } + } else { + // the next segment is not encrypted, so remove all available keys + available_keys.clear(); + available_keys.insert(key); writeln!(f, "{}", key)?; } } @@ -290,12 +532,12 @@ impl fmt::Display for MediaPlaylist { write!(f, "{}", segment)?; } - if let Some(value) = &self.end_list { + for value in &self.unknown { writeln!(f, "{}", value)?; } - for value in &self.unknown_tags { - writeln!(f, "{}", value)?; + if self.has_end_list { + writeln!(f, "{}", ExtXEndList)?; } Ok(()) @@ -313,9 +555,8 @@ fn parse_media_playlist( let mut has_partial_segment = false; let mut has_discontinuity_tag = false; - let mut unknown_tags = vec![]; - - let mut available_keys: Vec = vec![]; + let mut unknown = vec![]; + let mut available_keys = HashSet::new(); for line in Lines::from(input) { match line? { @@ -323,16 +564,16 @@ fn parse_media_playlist( match tag { Tag::ExtInf(t) => { has_partial_segment = true; - segment.inf(t); + segment.duration(t); } Tag::ExtXByteRange(t) => { has_partial_segment = true; segment.byte_range(t); } - Tag::ExtXDiscontinuity(t) => { + Tag::ExtXDiscontinuity(_) => { has_discontinuity_tag = true; has_partial_segment = true; - segment.discontinuity(t); + segment.has_discontinuity(true); } Tag::ExtXKey(key) => { has_partial_segment = true; @@ -343,25 +584,43 @@ fn parse_media_playlist( // same KEYFORMAT attribute (or the end of the Playlist file). let mut is_new_key = true; + let mut remove = None; - for old_key in &mut available_keys { - if old_key.key_format() == key.key_format() { - *old_key = key.clone(); - is_new_key = false; - // there are no keys with the same key_format in available_keys - // so the loop can stop here: - break; + if let ExtXKey(Some(decryption_key)) = &key { + for old_key in &available_keys { + if let ExtXKey(Some(old_decryption_key)) = &old_key { + if old_decryption_key.format == decryption_key.format { + // remove the old key + remove = Some(old_key.clone()); + + // there are no keys with the same format in + // available_keys so the loop can stop here: + break; + } + } else { + // remove an empty key + remove = Some(ExtXKey::empty()); + break; + } } + } else { + available_keys.clear(); + available_keys.insert(ExtXKey::empty()); + is_new_key = false; + } + + if let Some(key) = &remove { + available_keys.remove(key); } if is_new_key { - available_keys.push(key); + available_keys.insert(key); } } Tag::ExtXMap(mut t) => { has_partial_segment = true; - t.set_keys(available_keys.clone()); + t.keys = available_keys.iter().cloned().collect(); segment.map(t); } Tag::ExtXProgramDateTime(t) => { @@ -373,10 +632,10 @@ fn parse_media_playlist( segment.date_range(t); } Tag::ExtXTargetDuration(t) => { - builder.target_duration(t); + builder.target_duration(t.0); } Tag::ExtXMediaSequence(t) => { - builder.media_sequence(t); + builder.media_sequence(t.0); } Tag::ExtXDiscontinuitySequence(t) => { if segments.is_empty() { @@ -387,16 +646,16 @@ fn parse_media_playlist( return Err(Error::invalid_input()); } - builder.discontinuity_sequence(t); + builder.discontinuity_sequence(t.0); } - Tag::ExtXEndList(t) => { - builder.end_list(t); + Tag::ExtXEndList(_) => { + builder.has_end_list(true); } Tag::PlaylistType(t) => { builder.playlist_type(t); } - Tag::ExtXIFramesOnly(t) => { - builder.i_frames_only(t); + Tag::ExtXIFramesOnly(_) => { + builder.has_i_frames_only(true); } Tag::ExtXMedia(_) | Tag::VariantStream(_) @@ -404,8 +663,8 @@ fn parse_media_playlist( | Tag::ExtXSessionKey(_) => { return Err(Error::unexpected_tag(tag)); } - Tag::ExtXIndependentSegments(t) => { - builder.independent_segments(t); + Tag::ExtXIndependentSegments(_) => { + builder.has_independent_segments(true); } Tag::ExtXStart(t) => { builder.start(t); @@ -414,14 +673,15 @@ fn parse_media_playlist( Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. - unknown_tags.push(tag.to_string()); + unknown.push(tag.to_string()); } } } Line::Uri(uri) => { segment.uri(uri); - segment.keys(available_keys.clone()); + segment.keys(available_keys.iter().cloned().collect::>()); segments.push(segment.build().map_err(Error::builder)?); + segment = MediaSegment::builder(); has_partial_segment = false; } @@ -433,7 +693,7 @@ fn parse_media_playlist( return Err(Error::invalid_input()); } - builder.unknown_tags(unknown_tags); + builder.unknown(unknown); builder.segments(segments); builder.build().map_err(Error::builder) } @@ -476,10 +736,98 @@ mod tests { .is_err()); // Ok (allowable segment duration = 10) - MediaPlaylist::builder() + assert_eq!( + MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(2)) + .parse(playlist) + .unwrap(), + MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(2)) + .target_duration(Duration::from_secs(8)) + .segments(vec![ + MediaSegment::builder() + .duration(Duration::from_secs_f64(9.009)) + .uri("http://media.example.com/first.ts") + .build() + .unwrap(), + MediaSegment::builder() + .duration(Duration::from_secs_f64(9.509)) + .uri("http://media.example.com/second.ts") + .build() + .unwrap(), + MediaSegment::builder() + .duration(Duration::from_secs_f64(3.003)) + .uri("http://media.example.com/third.ts") + .build() + .unwrap(), + ]) + .has_end_list(true) + .build() + .unwrap() + ); + } + + #[test] + fn test_segment_number_simple() { + let playlist = MediaPlaylist::builder() .allowable_excess_duration(Duration::from_secs(2)) - .parse(playlist) + .target_duration(Duration::from_secs(8)) + .segments(vec![ + MediaSegment::builder() + .duration(Duration::from_secs_f64(9.009)) + .uri("http://media.example.com/first.ts") + .build() + .unwrap(), + MediaSegment::builder() + .duration(Duration::from_secs_f64(9.509)) + .uri("http://media.example.com/second.ts") + .build() + .unwrap(), + MediaSegment::builder() + .duration(Duration::from_secs_f64(3.003)) + .uri("http://media.example.com/third.ts") + .build() + .unwrap(), + ]) + .build() .unwrap(); + + let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number)); + assert_eq!(segments.next(), Some((0, 0))); + assert_eq!(segments.next(), Some((1, 1))); + assert_eq!(segments.next(), Some((2, 2))); + assert_eq!(segments.next(), None); + } + + #[test] + fn test_segment_number_sequence() { + let playlist = MediaPlaylist::builder() + .target_duration(Duration::from_secs(8)) + .media_sequence(2680) + .segments(vec![ + MediaSegment::builder() + .duration(Duration::from_secs_f64(7.975)) + .uri("https://priv.example.com/fileSequence2680.ts") + .build() + .unwrap(), + MediaSegment::builder() + .duration(Duration::from_secs_f64(7.941)) + .uri("https://priv.example.com/fileSequence2681.ts") + .build() + .unwrap(), + MediaSegment::builder() + .duration(Duration::from_secs_f64(7.975)) + .uri("https://priv.example.com/fileSequence2682.ts") + .build() + .unwrap(), + ]) + .build() + .unwrap(); + let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number)); + assert_eq!(segments.next(), Some((2680, 2680))); + assert_eq!(segments.next(), Some((2681, 2681))); + assert_eq!(segments.next(), Some((2682, 2682))); + assert_eq!(segments.next(), None); } #[test] From 7a63c2dcf248c11386920195e5002d870eea7c36 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 14:10:59 +0100 Subject: [PATCH 085/112] add builder module --- src/lib.rs | 21 ++++++++++++++++++ src/tags/basic/mod.rs | 4 ++-- src/tags/master_playlist/mod.rs | 12 +++++------ src/tags/mod.rs | 10 ++++----- src/types/mod.rs | 38 ++++++++++++++++++--------------- 5 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5b92abf..3b7e3c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,27 @@ pub use master_playlist::MasterPlaylist; pub use media_playlist::MediaPlaylist; pub use media_segment::MediaSegment; +pub mod builder { + pub use crate::master_playlist::MasterPlaylistBuilder; + pub use crate::media_playlist::MediaPlaylistBuilder; + pub use crate::media_segment::MediaSegmentBuilder; + + pub mod tags { + // master playlist + pub use crate::tags::master_playlist::media::ExtXMediaBuilder; + pub use crate::tags::master_playlist::session_data::ExtXSessionDataBuilder; + + // media segment + pub use crate::tags::media_segment::date_range::ExtXDateRangeBuilder; + + // media playlist + } + + pub mod types { + pub use crate::types::decryption_key::DecryptionKeyBuilder; + pub use crate::types::stream_data::StreamDataBuilder; + } +} pub mod tags; pub mod types; diff --git a/src/tags/basic/mod.rs b/src/tags/basic/mod.rs index 642560b..98e87f2 100644 --- a/src/tags/basic/mod.rs +++ b/src/tags/basic/mod.rs @@ -1,5 +1,5 @@ -mod m3u; -mod version; +pub(crate) mod m3u; +pub(crate) mod version; pub(crate) use m3u::*; pub use version::*; diff --git a/src/tags/master_playlist/mod.rs b/src/tags/master_playlist/mod.rs index b95f91c..d2005d0 100644 --- a/src/tags/master_playlist/mod.rs +++ b/src/tags/master_playlist/mod.rs @@ -1,9 +1,9 @@ -mod media; -mod session_data; -mod session_key; -mod variant_stream; +pub(crate) mod media; +pub(crate) mod session_data; +pub(crate) mod session_key; +pub(crate) mod variant_stream; -pub use media::*; -pub use session_data::*; +pub use media::ExtXMedia; +pub use session_data::{ExtXSessionData, SessionData}; pub use session_key::*; pub use variant_stream::*; diff --git a/src/tags/mod.rs b/src/tags/mod.rs index ae45d74..0dcda36 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -2,11 +2,11 @@ //! //! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3 -mod basic; -mod master_playlist; -mod media_playlist; -mod media_segment; -mod shared; +pub(crate) mod basic; +pub(crate) mod master_playlist; +pub(crate) mod media_playlist; +pub(crate) mod media_segment; +pub(crate) mod shared; pub use basic::*; pub use master_playlist::*; diff --git a/src/types/mod.rs b/src/types/mod.rs index 9a3c299..87188d7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,37 +1,41 @@ //! Miscellaneous types. -mod byte_range; -mod channels; -mod closed_captions; -mod codecs; -mod encryption_method; -mod hdcp_level; -mod in_stream_id; -mod key_format; -mod key_format_versions; -mod media_type; -mod protocol_version; -mod resolution; -mod stream_data; -mod value; +pub(crate) mod byte_range; +pub(crate) mod channels; +pub(crate) mod closed_captions; +pub(crate) mod codecs; +pub(crate) mod decryption_key; +pub(crate) mod encryption_method; +pub(crate) mod hdcp_level; +pub(crate) mod in_stream_id; +pub(crate) mod initialization_vector; +pub(crate) mod key_format; +pub(crate) mod key_format_versions; +pub(crate) mod media_type; pub(crate) mod playlist_type; +pub(crate) mod protocol_version; +pub(crate) mod resolution; +pub(crate) mod stream_data; +pub(crate) mod value; -mod float; -mod ufloat; +pub(crate) mod float; +pub(crate) mod ufloat; pub use byte_range::*; pub use channels::*; pub use closed_captions::*; pub use codecs::*; +pub use decryption_key::DecryptionKey; pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; +pub use initialization_vector::*; pub use key_format::*; pub use key_format_versions::*; pub use media_type::*; pub use playlist_type::*; pub use protocol_version::*; pub use resolution::*; -pub use stream_data::*; +pub use stream_data::StreamData; pub use value::*; pub use float::Float; From cc48478b05adfd35e439745d0ba33930370a0483 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 14:11:11 +0100 Subject: [PATCH 086/112] some minor fixes --- src/error.rs | 7 +++++++ src/traits.rs | 47 +++++++++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/error.rs b/src/error.rs index 85fc169..01a0bfe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -107,6 +107,13 @@ impl Error { }) } + pub(crate) fn missing_field(strct: D, field: T) -> Self { + Self::new(ErrorKind::Custom(format!( + "the field `{}` is missing for `{}`", + field, strct + ))) + } + pub(crate) fn unexpected_attribute(value: T) -> Self { Self::new(ErrorKind::UnexpectedAttribute { attribute: value.to_string(), diff --git a/src/traits.rs b/src/traits.rs index 4175588..dd459a3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -39,40 +39,23 @@ pub trait Decryptable: private::Sealed { /// Most of the time only a single key is provided, so instead of iterating /// through all keys, one might as well just get the first key. #[must_use] + #[inline] fn first_key(&self) -> Option<&DecryptionKey> { ::keys(self).first().copied() } /// Returns the number of keys. #[must_use] + #[inline] fn len(&self) -> usize { ::keys(self).len() } + /// Returns `true`, if the number of keys is zero. #[must_use] + #[inline] fn is_empty(&self) -> bool { ::len(self) == 0 } } -/// # 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); -/// ``` +#[doc(hidden)] pub trait RequiredVersion { /// Returns the protocol compatibility version that this tag requires. /// @@ -80,9 +63,11 @@ pub trait RequiredVersion { /// /// This is for the latest working [`ProtocolVersion`] and a client, that /// only supports an older version would break. + #[must_use] fn required_version(&self) -> ProtocolVersion; /// The protocol version, in which the tag has been introduced. + #[must_use] fn introduced_version(&self) -> ProtocolVersion { self.required_version() } } @@ -96,6 +81,15 @@ impl RequiredVersion for Vec { } } +impl RequiredVersion for BTreeMap { + fn required_version(&self) -> ProtocolVersion { + self.values() + .map(RequiredVersion::required_version) + .max() + .unwrap_or_default() + } +} + impl RequiredVersion for Option { fn required_version(&self) -> ProtocolVersion { self.iter() @@ -105,6 +99,15 @@ impl RequiredVersion for Option { } } +impl RequiredVersion for HashMap { + fn required_version(&self) -> ProtocolVersion { + self.values() + .map(RequiredVersion::required_version) + .max() + .unwrap_or_default() + } +} + #[cfg(test)] mod tests { use super::*; From 24c5ad8199c3f394518bdd936173e5b2736d8ec3 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 14:56:46 +0100 Subject: [PATCH 087/112] fix github actions syntax --- .github/workflows/rust.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2f88f4d..2e637e0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,28 +17,28 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.rust }} - override: true - components: rustfmt, clippy + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-features + with: + command: build + args: --all-features - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + with: + command: test + args: --all-features - uses: actions-rs/cargo@v1 - with: - command: doc - args: --all-features + with: + command: doc + args: --all-features - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + with: + command: fmt + args: --all -- --check From 6cd9fe70648ab19176fa47697ce7f23dc6a85865 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 15:57:43 +0100 Subject: [PATCH 088/112] fix broken documentation --- src/tags/master_playlist/variant_stream.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index 6492c0d..c3b2b46 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -29,19 +29,18 @@ use crate::Error; /// - Each [`MediaPlaylist`] in each [`VariantStream`] must have the same target /// duration. The only exceptions are subtitle renditions and /// [`MediaPlaylist`]s containing an [`ExtXIFramesOnly`] tag, which may have -/// different target durations if they have [`ExtXPlaylistType::Vod`]. +/// different target durations if they have [`PlaylistType::Vod`]. /// /// - Content that appears in a [`MediaPlaylist`] of one [`VariantStream`] but /// not in another must appear either at the beginning or at the end of the /// [`MediaPlaylist`] and must not be longer than the target duration. /// -/// - If any [`MediaPlaylist`]s have an [`ExtXPlaylistType`] tag, all -/// [`MediaPlaylist`]s must have an [`ExtXPlaylistType`] tag with the same -/// value. +/// - If any [`MediaPlaylist`]s have an [`PlaylistType`] tag, all +/// [`MediaPlaylist`]s must have an [`PlaylistType`] tag with the same value. /// -/// - If the Playlist contains an [`ExtXPlaylistType`] tag with the value of -/// VOD, the first segment of every [`MediaPlaylist`] in every -/// [`VariantStream`] must start at the same media timestamp. +/// - If the Playlist contains an [`PlaylistType`] tag with the value of VOD, +/// the first segment of every [`MediaPlaylist`] in every [`VariantStream`] +/// must start at the same media timestamp. /// /// - If any [`MediaPlaylist`] in a [`MasterPlaylist`] contains an /// [`ExtXProgramDateTime`] tag, then all [`MediaPlaylist`]s in that @@ -58,12 +57,12 @@ use crate::Error; /// /// [RFC6381]: https://tools.ietf.org/html/rfc6381 /// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence -/// [`ExtXPlaylistType::Vod`]: crate::tags::ExtXPlaylistType::Vod +/// [`PlaylistType::Vod`]: crate::types::PlaylistType::Vod /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist /// [`ExtXDateRange`]: crate::tags::ExtXDateRange /// [`ExtXProgramDateTime`]: crate::tags::ExtXProgramDateTime -/// [`ExtXPlaylistType`]: crate::tags::ExtXPlaylistType +/// [`PlaylistType`]: crate::types::PlaylistType /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub enum VariantStream { From 20072c2695019388a27b61eed1006bbe97f64aa3 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 25 Mar 2020 16:13:40 +0100 Subject: [PATCH 089/112] implement missing traits --- src/master_playlist.rs | 2 +- src/media_playlist.rs | 2 +- src/tags/master_playlist/media.rs | 2 +- src/tags/shared/start.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 641c434..9ba81b0 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -94,7 +94,7 @@ use crate::{Error, RequiredVersion}; /// ``` /// /// [`MediaPlaylist`]: crate::MediaPlaylist -#[derive(Debug, Clone, Builder, PartialEq, Default)] +#[derive(Builder, Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] pub struct MasterPlaylist { diff --git a/src/media_playlist.rs b/src/media_playlist.rs index e213c20..86a585e 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -16,7 +16,7 @@ use crate::utils::tag; use crate::{Error, RequiredVersion}; /// Media playlist. -#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] +#[derive(Builder, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[builder(build_fn(skip), setter(strip_option))] #[non_exhaustive] pub struct MediaPlaylist { diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index a0e57f0..63da25b 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -17,7 +17,7 @@ use crate::{Error, RequiredVersion}; /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`VariantStream`]: crate::tags::VariantStream -#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[shorthand(enable(must_use, into))] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 0d676c7..07203ba 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -13,7 +13,7 @@ use crate::{Error, RequiredVersion}; /// /// By default, clients should start playback at this point when beginning a /// playback session. -#[derive(ShortHand, PartialOrd, Debug, Clone, Copy, PartialEq)] +#[derive(ShortHand, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Ord, Hash)] #[shorthand(enable(must_use))] pub struct ExtXStart { /// The time offset of the [`MediaSegment`]s in the playlist. From e187c9dc7cc96a2937d1059bc88b5da97c09df34 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 28 Mar 2020 10:46:07 +0100 Subject: [PATCH 090/112] improve readability --- src/master_playlist.rs | 24 +++------- src/media_playlist.rs | 104 +++++++++-------------------------------- src/utils.rs | 39 ++++++++++++++++ 3 files changed, 69 insertions(+), 98 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 9ba81b0..9ea96e0 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -10,7 +10,7 @@ use crate::tags::{ ExtXVersion, VariantStream, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; -use crate::utils::tag; +use crate::utils::{tag, BoolExt}; use crate::{Error, RequiredVersion}; /// The master playlist describes all of the available variants for your @@ -97,6 +97,7 @@ use crate::{Error, RequiredVersion}; #[derive(Builder, Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] +#[non_exhaustive] pub struct MasterPlaylist { /// Indicates that all media samples in a [`MediaSegment`] can be /// decoded without information from other segments. @@ -167,8 +168,6 @@ pub struct MasterPlaylist { /// This field is optional. #[builder(default)] pub unknown_tags: Vec, - #[builder(default, field(private))] - __non_exhaustive: (), } impl MasterPlaylist { @@ -277,13 +276,8 @@ impl MasterPlaylist { impl RequiredVersion for MasterPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ - { - if self.has_independent_segments { - Some(ExtXIndependentSegments) - } else { - None - } - }, + self.has_independent_segments + .athen_some(ExtXIndependentSegments), self.start, self.media, self.variant_streams, @@ -403,13 +397,9 @@ impl RequiredVersion for MasterPlaylistBuilder { // not for Option>) // https://github.com/rust-lang/chalk/issues/12 required_version![ - { - if self.has_independent_segments.unwrap_or(false) { - Some(ExtXIndependentSegments) - } else { - None - } - }, + self.has_independent_segments + .unwrap_or(false) + .athen_some(ExtXIndependentSegments), self.start.flatten(), self.media, self.variant_streams, diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 86a585e..e4c9fc1 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -11,8 +11,10 @@ use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::{EncryptionMethod, PlaylistType, ProtocolVersion}; -use crate::utils::tag; +use crate::types::{ + DecryptionKey, EncryptionMethod, InitializationVector, KeyFormat, PlaylistType, ProtocolVersion, +}; +use crate::utils::{tag, BoolExt}; use crate::{Error, RequiredVersion}; /// Media playlist. @@ -274,13 +276,7 @@ impl MediaPlaylistBuilder { // insert all explictly numbered segments into the result let mut result_segments = segments .iter() - .filter_map(|(_, s)| { - if s.explicit_number { - Some((s.number, s.clone())) - } else { - None - } - }) + .filter_map(|(_, s)| s.explicit_number.athen(|| (s.number, s.clone()))) .collect::>(); // no segment should exist before the sequence_number @@ -345,45 +341,19 @@ impl RequiredVersion for MediaPlaylistBuilder { fn required_version(&self) -> ProtocolVersion { required_version![ self.target_duration.map(ExtXTargetDuration), - { - if self.media_sequence.unwrap_or(0) != 0 { - Some(ExtXMediaSequence(self.media_sequence.unwrap_or(0))) - } else { - None - } - }, - { - if self.discontinuity_sequence.unwrap_or(0) != 0 { - Some(ExtXDiscontinuitySequence( - self.discontinuity_sequence.unwrap_or(0), - )) - } else { - None - } - }, + (self.media_sequence.unwrap_or(0) != 0) + .athen(|| ExtXMediaSequence(self.media_sequence.unwrap_or(0))), + (self.discontinuity_sequence.unwrap_or(0) != 0) + .athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence.unwrap_or(0))), self.playlist_type, - { - if self.has_i_frames_only.unwrap_or(false) { - Some(ExtXIFramesOnly) - } else { - None - } - }, - { - if self.has_independent_segments.unwrap_or(false) { - Some(ExtXIndependentSegments) - } else { - None - } - }, + self.has_i_frames_only + .unwrap_or(false) + .athen_some(ExtXIFramesOnly), + self.has_independent_segments + .unwrap_or(false) + .athen_some(ExtXIndependentSegments), self.start, - { - if self.has_end_list.unwrap_or(false) { - Some(ExtXEndList) - } else { - None - } - }, + self.has_end_list.unwrap_or(false).athen_some(ExtXEndList), self.segments ] } @@ -406,43 +376,15 @@ impl RequiredVersion for MediaPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ ExtXTargetDuration(self.target_duration), - { - if self.media_sequence != 0 { - Some(ExtXMediaSequence(self.media_sequence)) - } else { - None - } - }, - { - if self.discontinuity_sequence != 0 { - Some(ExtXDiscontinuitySequence(self.discontinuity_sequence)) - } else { - None - } - }, + (self.media_sequence != 0).athen(|| ExtXMediaSequence(self.media_sequence)), + (self.discontinuity_sequence != 0) + .athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence)), self.playlist_type, - { - if self.has_i_frames_only { - Some(ExtXIFramesOnly) - } else { - None - } - }, - { - if self.has_independent_segments { - Some(ExtXIndependentSegments) - } else { - None - } - }, + self.has_i_frames_only.athen_some(ExtXIFramesOnly), + self.has_independent_segments + .athen_some(ExtXIndependentSegments), self.start, - { - if self.has_end_list { - Some(ExtXEndList) - } else { - None - } - }, + self.has_end_list.athen_some(ExtXEndList), self.segments ] } diff --git a/src/utils.rs b/src/utils.rs index b210675..b7e628f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,45 @@ use crate::Error; use core::iter; +/// This is an extension trait that adds the below method to `bool`. +/// Those methods are already planned for the standard library, but are not +/// stable at the time of writing this comment. +/// +/// The current status can be seen here: +/// +/// +/// This trait exists to allow publishing a new version (requires stable +/// release) and the functions are prefixed with an `a` to prevent naming +/// conflicts with the coming std functions. +// TODO: replace this trait with std version as soon as it is stabilized +pub(crate) trait BoolExt { + #[must_use] + fn athen_some(self, t: T) -> Option; + + #[must_use] + fn athen T>(self, f: F) -> Option; +} + +impl BoolExt for bool { + #[inline] + fn athen_some(self, t: T) -> Option { + if self { + Some(t) + } else { + None + } + } + + #[inline] + fn athen T>(self, f: F) -> Option { + if self { + Some(f()) + } else { + None + } + } +} + macro_rules! required_version { ( $( $tag:expr ),* ) => { ::core::iter::empty() From 8eb45dceb7f35e2df10043f26623bd1498372835 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 28 Mar 2020 10:47:52 +0100 Subject: [PATCH 091/112] insert iv based on segmentnumber --- src/media_playlist.rs | 45 +++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index e4c9fc1..3c2a06e 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -301,6 +301,22 @@ impl MediaPlaylistBuilder { let mut segment = segment.clone(); segment.number = position; + // add the segment number as iv, if the iv is missing: + for key in &mut segment.keys { + if let ExtXKey(Some(DecryptionKey { + method, iv, format, .. + })) = key + { + if *method == EncryptionMethod::Aes128 && *iv == InitializationVector::Missing { + if format.is_none() { + *iv = InitializationVector::Number(segment.number as u128); + } else if let Some(KeyFormat::Identity) = format { + *iv = InitializationVector::Number(segment.number as u128); + } + } + } + } + result_segments.insert(segment.number, segment); position += 1; } @@ -428,26 +444,35 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", value)?; } - let mut available_keys = HashSet::new(); + let mut available_keys = HashSet::::new(); for segment in self.segments.values() { for key in &segment.keys { - // the key is new: if let ExtXKey(Some(decryption_key)) = key { - // TODO: this piece should be linted by clippy? - // next segment will be encrypted, so the segment can not have an empty key available_keys.remove(&ExtXKey::empty()); + let mut decryption_key = decryption_key.clone(); + let key = { + if let InitializationVector::Number(_) = decryption_key.iv { + // set the iv from a segment number to missing + // this does reduce the output size and the correct iv + // is automatically set, when parsing. + decryption_key.iv = InitializationVector::Missing; + } + + ExtXKey(Some(decryption_key.clone())) + }; + // only do something if a key has been overwritten - if available_keys.insert(key) { + if available_keys.insert(key.clone()) { let mut remove_key = None; // an old key might be removed: for k in &available_keys { if let ExtXKey(Some(dk)) = k { - if dk.format == decryption_key.format && &key != k { - remove_key = Some(*k); + if dk.format == decryption_key.format && key != *k { + remove_key = Some(k.clone()); break; } } else { @@ -457,7 +482,7 @@ impl fmt::Display for MediaPlaylist { if let Some(k) = remove_key { // this should always be true: - let res = available_keys.remove(k); + let res = available_keys.remove(&k); debug_assert!(res); } @@ -466,7 +491,7 @@ impl fmt::Display for MediaPlaylist { } else { // the next segment is not encrypted, so remove all available keys available_keys.clear(); - available_keys.insert(key); + available_keys.insert(ExtXKey::empty()); writeln!(f, "{}", key)?; } } @@ -632,7 +657,7 @@ fn parse_media_playlist( } if has_partial_segment { - return Err(Error::invalid_input()); + return Err(Error::custom("Missing URI for the last `MediaSegment`")); } builder.unknown(unknown); From 9a2cacf024577251f068e53bae86c6752a264458 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 28 Mar 2020 10:48:17 +0100 Subject: [PATCH 092/112] fix some clippy lints --- src/media_playlist.rs | 1 + src/types/key_format_versions.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 3c2a06e..855aea8 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -383,6 +383,7 @@ impl MediaPlaylist { /// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment /// duration together. + #[must_use] pub fn duration(&self) -> Duration { self.segments.values().map(|s| s.duration.duration()).sum() } diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 494f4ac..601fdf6 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -184,7 +184,7 @@ impl KeyFormatVersions { /// ``` #[inline] #[must_use] - pub fn is_empty(&self) -> bool { self.len() == 0 } + pub const fn is_empty(&self) -> bool { self.len() == 0 } /// Removes the last element and returns it, or `None` if it is empty. /// From 899aea7fc1e6676343ef8cc3fa08b1b79bfc6e58 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 28 Mar 2020 10:51:19 +0100 Subject: [PATCH 093/112] finish documentation --- src/lib.rs | 3 +++ src/media_playlist.rs | 5 +++++ src/types/decryption_key.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 3b7e3c6..70edb06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,11 +62,13 @@ pub use master_playlist::MasterPlaylist; pub use media_playlist::MediaPlaylist; pub use media_segment::MediaSegment; +/// Builder structs pub mod builder { pub use crate::master_playlist::MasterPlaylistBuilder; pub use crate::media_playlist::MediaPlaylistBuilder; pub use crate::media_segment::MediaSegmentBuilder; + /// Builder structs for tags pub mod tags { // master playlist pub use crate::tags::master_playlist::media::ExtXMediaBuilder; @@ -78,6 +80,7 @@ pub mod builder { // media playlist } + /// Builder structs for types pub mod types { pub use crate::types::decryption_key::DecryptionKeyBuilder; pub use crate::types::stream_data::StreamDataBuilder; diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 855aea8..9fb4aad 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -262,6 +262,11 @@ impl MediaPlaylistBuilder { self } + /// Builds a new `MediaPlaylist`. + /// + /// # Errors + /// + /// If a required field has not been initialized. pub fn build(&self) -> Result { // validate builder self.validate()?; diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 0caf17a..53bff38 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -81,6 +81,17 @@ pub struct DecryptionKey { } impl DecryptionKey { + /// Creates a new `DecryptionKey` from an uri pointing to the key data and + /// an `EncryptionMethod`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.uri/key"); + /// ``` #[must_use] #[inline] pub fn new>(method: EncryptionMethod, uri: I) -> Self { @@ -93,6 +104,25 @@ impl DecryptionKey { } } + /// Returns a builder for a `DecryptionKey`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; + /// + /// let key = DecryptionKey::builder() + /// .method(EncryptionMethod::Aes128) + /// .uri("https://www.example.com/") + /// .iv([ + /// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + /// ]) + /// .format(KeyFormat::Identity) + /// .versions(&[1, 2, 3, 4, 5]) + /// .build()?; + /// # Ok::<(), String>(()) + /// ``` #[must_use] #[inline] pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } From e174fcac9a75caffe4e328cbb704901f399a6347 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 11:02:30 +0200 Subject: [PATCH 094/112] remove redundant deny.toml --- deny.toml | 189 ------------------------------------------------------ 1 file changed, 189 deletions(-) delete mode 100644 deny.toml diff --git a/deny.toml b/deny.toml deleted file mode 100644 index 407794b..0000000 --- a/deny.toml +++ /dev/null @@ -1,189 +0,0 @@ -# This template contains all of the possible sections and their default values - -# Note that all fields that take a lint level have these possible values: -# * deny - An error will be produced and the check will fail -# * warn - A warning will be produced, but the check will not fail -# * allow - No warning or error will be produced, though in some cases a note -# will be - -# The values provided in this template are the default values that will be used -# when any section or field is not specified in your own configuration - -# If 1 or more target triples (and optionally, target_features) are specified, -# only the specified targets will be checked when running `cargo deny check`. -# This means, if a particular package is only ever used as a target specific -# dependency, such as, for example, the `nix` crate only being used via the -# `target_family = "unix"` configuration, that only having windows targets in -# this list would mean the nix crate, as well as any of its exclusive -# dependencies not shared by any other crates, would be ignored, as the target -# list here is effectively saying which targets you are building for. -targets = [ - # The triple can be any string, but only the target triples built in to - # rustc (as of 1.40) can be checked against actual config expressions - #{ triple = "x86_64-unknown-linux-musl" }, - # You can also specify which target_features you promise are enabled for a - # particular target. target_features are currently not validated against - # the actual valid features supported by the target architecture. - #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, -] - -# This section is considered when running `cargo deny check advisories` -# More documentation for the advisories section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html -[advisories] -# The path where the advisory database is cloned/fetched into -db-path = "~/.cargo/advisory-db" -# The url of the advisory database to use -db-url = "https://github.com/rustsec/advisory-db" -# The lint level for security vulnerabilities -vulnerability = "deny" -# The lint level for unmaintained crates -unmaintained = "warn" -# The lint level for crates that have been yanked from their source registry -# yanked = "warn" -# The lint level for crates with security notices. Note that as of -# 2019-12-17 there are no security notice advisories in -# https://github.com/rustsec/advisory-db -notice = "warn" -# A list of advisory IDs to ignore. Note that ignored advisories will still -# output a note when they are encountered. -ignore = [ - #"RUSTSEC-0000-0000", -] -# Threshold for security vulnerabilities, any vulnerability with a CVSS score -# lower than the range specified will be ignored. Note that ignored advisories -# will still output a note when they are encountered. -# * None - CVSS Score 0.0 -# * Low - CVSS Score 0.1 - 3.9 -# * Medium - CVSS Score 4.0 - 6.9 -# * High - CVSS Score 7.0 - 8.9 -# * Critical - CVSS Score 9.0 - 10.0 -#severity-threshold = - -# This section is considered when running `cargo deny check licenses` -# More documentation for the licenses section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html -[licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" -# List of explictly allowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. -allow = [ - "MIT", - "Apache-2.0", - #"Apache-2.0 WITH LLVM-exception", -] -# List of explictly disallowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. -deny = [ - #"Nokia", -] -# Lint level for licenses considered copyleft -copyleft = "warn" -# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses -# * both - The license will be approved if it is both OSI-approved *AND* FSF -# * either - The license will be approved if it is either OSI-approved *OR* FSF -# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF -# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved -# * neither - This predicate is ignored and the default lint level is used -allow-osi-fsf-free = "neither" -# Lint level used when no other predicates are matched -# 1. License isn't in the allow or deny lists -# 2. License isn't copyleft -# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" -default = "deny" -# The confidence threshold for detecting a license from license text. -# The higher the value, the more closely the license text must be to the -# canonical license text of a valid SPDX license file. -# [possible values: any between 0.0 and 1.0]. -confidence-threshold = 0.8 -# Allow 1 or more licenses on a per-crate basis, so that particular licenses -# aren't accepted for every possible crate as with the normal allow list -exceptions = [ - # Each entry is the crate and version constraint, and its specific allow - # list - #{ allow = ["Zlib"], name = "adler32", version = "*" }, -] - -# Some crates don't have (easily) machine readable licensing information, -# adding a clarification entry for it allows you to manually specify the -# licensing information -#[[licenses.clarify]] -# The name of the crate the clarification applies to -#name = "ring" -# THe optional version constraint for the crate -#version = "*" -# The SPDX expression for the license requirements of the crate -#expression = "MIT AND ISC AND OpenSSL" -# One or more files in the crate's source used as the "source of truth" for -# the license expression. If the contents match, the clarification will be used -# when running the license check, otherwise the clarification will be ignored -# and the crate will be checked normally, which may produce warnings or errors -# depending on the rest of your configuration -#license-files = [ - # Each entry is a crate relative path, and the (opaque) hash of its contents - #{ path = "LICENSE", hash = 0xbd0eed23 } -#] - -[licenses.private] -# If true, ignores workspace crates that aren't published, or are only -# published to private registries -ignore = false -# One or more private registries that you might publish crates to, if a crate -# is only published to private registries, and ignore is true, the crate will -# not have its license(s) checked -registries = [ - #"https://sekretz.com/registry -] - -# This section is considered when running `cargo deny check bans`. -# More documentation about the 'bans' section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html -[bans] -# Lint level for when multiple versions of the same crate are detected -multiple-versions = "warn" -# The graph highlighting used when creating dotgraphs for crates -# with multiple versions -# * lowest-version - The path to the lowest versioned duplicate is highlighted -# * simplest-path - The path to the version with the fewest edges is highlighted -# * all - Both lowest-version and simplest-path are used -highlight = "all" -# List of crates that are allowed. Use with care! -allow = [ - #{ name = "ansi_term", version = "=0.11.0" }, -] -# List of crates to deny -deny = [ - # Each entry the name of a crate and a version range. If version is - # not specified, all versions will be matched. - #{ name = "ansi_term", version = "=0.11.0" }, -] -# Certain crates/versions that will be skipped when doing duplicate detection. -skip = [ - #{ name = "ansi_term", version = "=0.11.0" }, -] -# Similarly to `skip` allows you to skip certain crates during duplicate -# detection. Unlike skip, it also includes the entire tree of transitive -# dependencies starting at the specified crate, up to a certain depth, which is -# by default infinite -skip-tree = [ - #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, -] - -# This section is considered when running `cargo deny check sources`. -# More documentation about the 'sources' section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html -[sources] -# Lint level for what to happen when a crate from a crate registry that is not -# in the allow list is encountered -unknown-registry = "warn" -# Lint level for what to happen when a crate from a git repository that is not -# in the allow list is encountered -unknown-git = "warn" -# List of URLs for allowed crate registries. Defaults to the crates.io index -# if not specified. If it is specified but empty, no registries are allowed. -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -# List of URLs for allowed Git repositories -allow-git = [] From 8b3517326b4f6044f4e895411c82786c52e6c88f Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 11:32:59 +0200 Subject: [PATCH 095/112] fix badge in readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 23aa5c1..5979af8 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ hls_m3u8 [![Documentation](https://docs.rs/hls_m3u8/badge.svg)](https://docs.rs/hls_m3u8) [![Build Status](https://travis-ci.org/sile/hls_m3u8.svg?branch=master)](https://travis-ci.org/sile/hls_m3u8) [![Code Coverage](https://codecov.io/gh/sile/hls_m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/sile/hls_m3u8/branch/master) -[![License: Apache](https://img.shields.io/badge/License-Apache%202.0-red.svg)](LICENSE-APACHE) -OR -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +![Crates.io](https://img.shields.io/crates/l/hls_m3u8) [HLS] m3u8 parser/generator. From 3710a9c5c2befdca65b0867d0813180a4aa5d66c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 11:33:16 +0200 Subject: [PATCH 096/112] change github actions --- .github/workflows/rust.yml | 48 ++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2e637e0..de17cff 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,44 +1,36 @@ -name: Rust +name: rust # Trigger the workflow on push or pull request on: [push, pull_request] jobs: - ci: + rustfmt: + name: rustfmt runs-on: ubuntu-latest - - strategy: - matrix: - rust: - - stable - - beta - - nightly - steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: ${{ matrix.rust }} + toolchain: nightly override: true - components: rustfmt, clippy - - - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-features - - - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features - - - uses: actions-rs/cargo@v1 - with: - command: doc - args: --all-features - + components: rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check + + clippy: + name: clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy From 8ece080cda7a73489ad240cd9916041b6a5d4d00 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 12:01:28 +0200 Subject: [PATCH 097/112] remove examples directory --- examples/data/rfc8216_8-1.m3u8 | 12 ------------ examples/data/rfc8216_8-2.m3u8 | 13 ------------- examples/data/rfc8216_8-3.m3u8 | 20 -------------------- examples/data/rfc8216_8-4.m3u8 | 11 ----------- examples/data/rfc8216_8-5.m3u8 | 14 -------------- examples/data/rfc8216_8-6.m3u8 | 14 -------------- examples/data/rfc8216_8-7.m3u8 | 23 ----------------------- examples/parse.rs | 7 ------- 8 files changed, 114 deletions(-) delete mode 100644 examples/data/rfc8216_8-1.m3u8 delete mode 100644 examples/data/rfc8216_8-2.m3u8 delete mode 100644 examples/data/rfc8216_8-3.m3u8 delete mode 100644 examples/data/rfc8216_8-4.m3u8 delete mode 100644 examples/data/rfc8216_8-5.m3u8 delete mode 100644 examples/data/rfc8216_8-6.m3u8 delete mode 100644 examples/data/rfc8216_8-7.m3u8 delete mode 100644 examples/parse.rs diff --git a/examples/data/rfc8216_8-1.m3u8 b/examples/data/rfc8216_8-1.m3u8 deleted file mode 100644 index 549fa9e..0000000 --- a/examples/data/rfc8216_8-1.m3u8 +++ /dev/null @@ -1,12 +0,0 @@ -#EXTM3U -#EXT-X-TARGETDURATION:10 -#EXT-X-VERSION:3 -#EXTINF:9.009, -http://media.example.com/first.ts -#EXTINF:9.009, -http://media.example.com/second.ts -#EXTINF:3.003, -http://media.example.com/third.ts -#EXT-X-ENDLIST - -# 8.1. Simple Media Playlist diff --git a/examples/data/rfc8216_8-2.m3u8 b/examples/data/rfc8216_8-2.m3u8 deleted file mode 100644 index 33dfbe6..0000000 --- a/examples/data/rfc8216_8-2.m3u8 +++ /dev/null @@ -1,13 +0,0 @@ -#EXTM3U -#EXT-X-VERSION:3 -#EXT-X-TARGETDURATION:8 -#EXT-X-MEDIA-SEQUENCE:2680 - -#EXTINF:7.975, -https://priv.example.com/fileSequence2680.ts -#EXTINF:7.941, -https://priv.example.com/fileSequence2681.ts -#EXTINF:7.975, -https://priv.example.com/fileSequence2682.ts - -# 8.2. Live Media Playlist Using HTTPS diff --git a/examples/data/rfc8216_8-3.m3u8 b/examples/data/rfc8216_8-3.m3u8 deleted file mode 100644 index 970a2d4..0000000 --- a/examples/data/rfc8216_8-3.m3u8 +++ /dev/null @@ -1,20 +0,0 @@ -#EXTM3U -#EXT-X-VERSION:3 -#EXT-X-MEDIA-SEQUENCE:7794 -#EXT-X-TARGETDURATION:15 - -#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" - -#EXTINF:2.833, -http://media.example.com/fileSequence52-A.ts -#EXTINF:15.0, -http://media.example.com/fileSequence52-B.ts -#EXTINF:13.333, -http://media.example.com/fileSequence52-C.ts - -#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" - -#EXTINF:15.0, -http://media.example.com/fileSequence53-A.ts - -# 8.3. Playlist with Encrypted Media Segments diff --git a/examples/data/rfc8216_8-4.m3u8 b/examples/data/rfc8216_8-4.m3u8 deleted file mode 100644 index e81a437..0000000 --- a/examples/data/rfc8216_8-4.m3u8 +++ /dev/null @@ -1,11 +0,0 @@ -#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000 -http://example.com/low.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000 -http://example.com/mid.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000 -http://example.com/hi.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" -http://example.com/audio-only.m3u8 - -# 8.4. Master Playlist \ No newline at end of file diff --git a/examples/data/rfc8216_8-5.m3u8 b/examples/data/rfc8216_8-5.m3u8 deleted file mode 100644 index 4f99863..0000000 --- a/examples/data/rfc8216_8-5.m3u8 +++ /dev/null @@ -1,14 +0,0 @@ -#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=1280000 -low/audio-video.m3u8 -#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8" -#EXT-X-STREAM-INF:BANDWIDTH=2560000 -mid/audio-video.m3u8 -#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8" -#EXT-X-STREAM-INF:BANDWIDTH=7680000 -hi/audio-video.m3u8 -#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8" -#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" -audio-only.m3u8 - -# 8.5. Master Playlist with I-Frames diff --git a/examples/data/rfc8216_8-6.m3u8 b/examples/data/rfc8216_8-6.m3u8 deleted file mode 100644 index d90c82f..0000000 --- a/examples/data/rfc8216_8-6.m3u8 +++ /dev/null @@ -1,14 +0,0 @@ -#EXTM3U -#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="main/english-audio.m3u8" -#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",URI="main/german-audio.m3u8" -#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",URI="commentary/audio-only.m3u8" -#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",AUDIO="aac" -low/video-only.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",AUDIO="aac" -mid/video-only.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",AUDIO="aac" -hi/video-only.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac" -main/english-audio.m3u8 - -# 8.6. Master Playlist with Alternative Audio diff --git a/examples/data/rfc8216_8-7.m3u8 b/examples/data/rfc8216_8-7.m3u8 deleted file mode 100644 index 4d06be6..0000000 --- a/examples/data/rfc8216_8-7.m3u8 +++ /dev/null @@ -1,23 +0,0 @@ -#EXTM3U -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" - -#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",VIDEO="low" -low/main/audio-video.m3u8 - -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8" -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8" -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" - -#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",VIDEO="mid" -mid/main/audio-video.m3u8 - -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" -#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" - -#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",VIDEO="hi" -hi/main/audio-video.m3u8 - -# 8.7. Master Playlist with Alternative Video diff --git a/examples/parse.rs b/examples/parse.rs deleted file mode 100644 index 9f549a8..0000000 --- a/examples/parse.rs +++ /dev/null @@ -1,7 +0,0 @@ -// use hls_m3u8::{MasterPlaylist, MediaPlaylist}; -// use std::fs::File; -// use std::io::Read; - -fn main() { - unimplemented!(); -} From 47eccfdef904bd92014006b02a3682fb62f3b016 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 12:57:43 +0200 Subject: [PATCH 098/112] improve panic messages for ByteRange --- src/types/byte_range.rs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index b0004b1..fdb8db6 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -39,8 +39,8 @@ pub struct ByteRange { /// /// ``` /// # use hls_m3u8::types::ByteRange; - /// assert_eq!(ByteRange::from(0..5).start(), Some(0)); - /// assert_eq!(ByteRange::from(..5).start(), None); + /// assert_eq!(ByteRange::from(0..5).end(), 5); + /// assert_eq!(ByteRange::from(..=5).end(), 6); /// ``` end: usize, } @@ -90,7 +90,11 @@ impl ByteRange { /// [`end`](ByteRange::end). pub fn set_start(&mut self, new_start: Option) -> &mut Self { if new_start.map_or(false, |s| s > self.end) { - panic!("attempt to make the start larger than the end"); + panic!( + "attempt to make the start ({}) larger than the end ({})", + new_start.unwrap(), + self.end + ); } self.start = new_start; @@ -288,7 +292,10 @@ macro_rules! impl_from_ranges { impl From> for ByteRange { fn from(range: Range<$type>) -> Self { if range.start > range.end { - panic!("the range start must be smaller than the end"); + panic!( + "the range start ({}) must be smaller than the end ({})", + range.start, range.end + ); } Self { @@ -304,7 +311,10 @@ macro_rules! impl_from_ranges { let (start, end) = range.into_inner(); if start > end { - panic!("the range start must be smaller than the end"); + panic!( + "the range start ({}) must be smaller than the end ({}+1)", + start, end + ); } Self { @@ -361,7 +371,7 @@ impl TryInto> for ByteRange { fn try_into(self) -> Result, Self::Error> { if self.start.is_some() { - return Err(Error::custom("A `RangeTo` (`..end`) does not have a start")); + return Err(Error::custom("a `RangeTo` (`..end`) does not have a start")); } Ok(RangeTo { end: self.end }) @@ -375,7 +385,7 @@ impl TryInto> for ByteRange { fn try_into(self) -> Result, Self::Error> { if self.start.is_none() { return Err(Error::custom( - "A `Range` (`start..end`) has to have a start.", + "a `Range` (`start..end`) has to have a start.", )); } @@ -427,11 +437,11 @@ mod tests { use pretty_assertions::assert_eq; #[test] - #[should_panic = "the range start must be smaller than the end"] + #[should_panic = "the range start (6) must be smaller than the end (0)"] fn test_from_range_panic() { let _ = ByteRange::from(6..0); } #[test] - #[should_panic = "the range start must be smaller than the end"] + #[should_panic = "the range start (6) must be smaller than the end (0+1)"] fn test_from_range_inclusive_panic() { let _ = ByteRange::from(6..=0); } #[test] @@ -493,7 +503,7 @@ mod tests { } #[test] - #[should_panic = "attempt to make the start larger than the end"] + #[should_panic = "attempt to make the start (11) larger than the end (10)"] fn test_set_start() { let _ = ByteRange::from(4..10).set_start(Some(11)); } #[test] From 9eccea8a7fa16f8a2115aa889db67edeab3ad4ed Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 12:58:32 +0200 Subject: [PATCH 099/112] automatically infer start of ByteRange --- src/media_playlist.rs | 23 +++++++++++++++++++++-- tests/media_playlist.rs | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 9fb4aad..892e398 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -8,8 +8,9 @@ use derive_builder::Builder; use crate::line::{Line, Lines, Tag}; use crate::media_segment::MediaSegment; use crate::tags::{ - ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, - ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration, ExtXVersion, + ExtM3u, ExtXByteRange, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, + ExtXIndependentSegments, ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration, + ExtXVersion, }; use crate::types::{ DecryptionKey, EncryptionMethod, InitializationVector, KeyFormat, PlaylistType, ProtocolVersion, @@ -295,6 +296,8 @@ impl MediaPlaylistBuilder { } let mut position = sequence_number; + let mut previous_range: Option = None; + for segment in segments .iter() .filter_map(|(_, s)| if s.explicit_number { None } else { Some(s) }) @@ -322,6 +325,22 @@ impl MediaPlaylistBuilder { } } + // add the lower bound to the byterange automatically + if let Some(range) = &mut segment.byte_range { + if range.start().is_none() { + if let Some(previous_range) = previous_range { + // the end of the previous_range is the start of the next range + *range = range.saturating_add(previous_range.end()); + range.set_start(Some(previous_range.end())); + } else { + // assume that the byte range starts at zero + range.set_start(Some(0)); + } + } + + previous_range = segment.byte_range; + } + result_segments.insert(segment.number, segment); position += 1; } diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index 7477910..b1fb799 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -43,6 +43,7 @@ generate_tests! { .unwrap(), MediaSegment::builder() .duration(ExtInf::new(Duration::from_secs_f64(10.0))) + // 834433..904297 .byte_range(ExtXByteRange::from(..69864)) .uri("video.ts") .build() @@ -64,7 +65,7 @@ generate_tests! { "#EXTINF:10,\n", "video.ts\n", - "#EXT-X-BYTERANGE:69864\n", + "#EXT-X-BYTERANGE:69864@834433\n", "#EXTINF:10,\n", "video.ts\n" ) From 969e5bae9acafb809fd3c2bdcb5f077853fcce5e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 12:58:43 +0200 Subject: [PATCH 100/112] minor code improvements --- src/line.rs | 7 ++----- src/media_segment.rs | 14 +++----------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/line.rs b/src/line.rs index f7d1616..c684585 100644 --- a/src/line.rs +++ b/src/line.rs @@ -115,14 +115,11 @@ impl<'a> TryFrom<&'a str> for Tag<'a> { } else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) { input.parse().map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { - input.parse().map(Self::ExtXMedia).map_err(Error::custom) + input.parse().map(Self::ExtXMedia) } else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME) || input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) { - input - .parse() - .map(Self::VariantStream) - .map_err(Error::custom) + input.parse().map(Self::VariantStream) } else if input.starts_with(tags::ExtXSessionData::PREFIX) { input.parse().map(Self::ExtXSessionData) } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { diff --git a/src/media_segment.rs b/src/media_segment.rs index ec78722..1791eee 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -33,7 +33,7 @@ use crate::{Decryptable, RequiredVersion}; /// [`MediaPlaylist`]: crate::MediaPlaylist #[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)] #[builder(setter(strip_option))] -#[shorthand(enable(must_use))] +#[shorthand(enable(must_use, skip))] pub struct MediaSegment { /// Each [`MediaSegment`] has a number, which allows synchronization between /// different variants. @@ -53,10 +53,9 @@ pub struct MediaSegment { /// [`ExtXMediaSequence`]: crate::tags::ExtXMediaSequence /// [`ExtXDiscontinuitySequence`]: crate::tags::ExtXDiscontinuitySequence #[builder(default, setter(custom))] - #[shorthand(disable(set))] + #[shorthand(disable(set, skip))] pub(crate) number: usize, #[builder(default, setter(custom))] - #[shorthand(enable(skip))] pub(crate) explicit_number: bool, /// This field specifies how to decrypt a [`MediaSegment`], which can only /// be encrypted with one [`EncryptionMethod`], using one [`DecryptionKey`] @@ -81,7 +80,6 @@ pub struct MediaSegment { /// [`KeyFormat`]: crate::types::KeyFormat /// [`EncryptionMethod`]: crate::types::EncryptionMethod #[builder(default, setter(into))] - #[shorthand(enable(skip))] pub keys: Vec, /// This field specifies how to obtain the Media Initialization Section /// required to parse the applicable `MediaSegment`s. @@ -96,7 +94,6 @@ pub struct MediaSegment { /// /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly #[builder(default)] - #[shorthand(enable(skip))] pub map: Option, /// This field indicates that a `MediaSegment` is a sub-range of the /// resource identified by its URI. @@ -105,7 +102,6 @@ pub struct MediaSegment { /// /// This field is optional. #[builder(default, setter(into))] - #[shorthand(enable(skip))] pub byte_range: Option, /// This field associates a date-range (i.e., a range of time defined by a /// starting and ending date) with a set of attribute/value pairs. @@ -114,7 +110,6 @@ pub struct MediaSegment { /// /// This field is optional. #[builder(default)] - #[shorthand(enable(skip))] pub date_range: Option, /// This field indicates a discontinuity between the `MediaSegment` that /// follows it and the one that preceded it. @@ -131,7 +126,6 @@ pub struct MediaSegment { /// - encoding parameters /// - encoding sequence #[builder(default)] - #[shorthand(enable(skip))] pub has_discontinuity: bool, /// This field associates the first sample of a media segment with an /// absolute date and/or time. @@ -140,14 +134,12 @@ pub struct MediaSegment { /// /// This field is optional. #[builder(default)] - #[shorthand(enable(skip))] pub program_date_time: Option, /// This field indicates the duration of a media segment. /// /// ## Note /// /// This field is required. - #[shorthand(enable(skip))] #[builder(setter(into))] pub duration: ExtInf, /// The URI of a media segment. @@ -156,7 +148,7 @@ pub struct MediaSegment { /// /// This field is required. #[builder(setter(into))] - #[shorthand(enable(into))] + #[shorthand(enable(into), disable(skip))] uri: String, } From fb4f6a451e69c4ff14c0eeee9ae0f0463a43bf92 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 29 Mar 2020 13:01:30 +0200 Subject: [PATCH 101/112] include backtrace feature in ci tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fb78ec8..68edb9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ script: - cargo build - cargo test - cargo test --features chrono + - cargo test --features backtrace # it's enough to run this once: - | From e07fb9262dcbd0dd44df45be5518952e71a02df1 Mon Sep 17 00:00:00 2001 From: David Holroyd Date: Tue, 7 Apr 2020 15:30:06 +0100 Subject: [PATCH 102/112] Make START-DATE optional. Although this goes against the wording of RFC8216, the spec includes examples that omit this, and implementations also omit START-DATE when for example signalling an 'explicit-IN' SCTE marker. --- src/tags/media_segment/date_range.rs | 53 +++++++++++++++++----------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 1a30e28..a13ebe2 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -38,17 +38,25 @@ pub struct ExtXDateRange { /// /// ## Note /// - /// This field is required. + /// This field is required by the spec wording, but optional in examples + /// elsewhere in the same document. Some implementations omit it in + /// practise (e.g. for SCTE 'explicit-IN' markers) so it is optional + /// here. #[cfg(feature = "chrono")] #[shorthand(enable(copy), disable(into))] - start_date: DateTime, + #[builder(setter(strip_option), default)] + start_date: Option>, /// The date at which the [`ExtXDateRange`] begins. /// /// ## Note /// - /// This field is required. + /// This field is required by the spec wording, but optional in examples + /// elsewhere in the same document. Some implementations omit it in + /// practise (e.g. for SCTE 'explicit-IN' markers) so it is optional + /// here. #[cfg(not(feature = "chrono"))] - start_date: String, + #[builder(setter(strip_option), default)] + start_date: Option, /// The date at which the [`ExtXDateRange`] ends. It must be equal to or /// later than the value of the [`start-date`] attribute. /// @@ -238,9 +246,9 @@ let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00"); id: id.into(), class: None, #[cfg(feature = "chrono")] - start_date, + start_date: Some(start_date), #[cfg(not(feature = "chrono"))] - start_date: start_date.into(), + start_date: Some(start_date.into()), end_date: None, duration: None, planned_duration: None, @@ -401,7 +409,6 @@ impl FromStr for ExtXDateRange { } let id = id.ok_or_else(|| Error::missing_value("ID"))?; - let start_date = start_date.ok_or_else(|| Error::missing_value("START-DATE"))?; if end_on_next && class.is_none() { return Err(Error::missing_attribute("CLASS")); @@ -415,9 +422,11 @@ impl FromStr for ExtXDateRange { // https://tools.ietf.org/html/rfc8216#section-4.3.2.7 #[cfg(feature = "chrono")] { - if let (Some(Ok(duration)), Some(end_date)) = - (duration.map(chrono::Duration::from_std), &end_date) - { + if let (Some(start_date), Some(Ok(duration)), Some(end_date)) = ( + start_date, + duration.map(chrono::Duration::from_std), + &end_date, + ) { if start_date + duration != *end_date { return Err(Error::custom( "end_date must be equal to start_date + duration", @@ -451,18 +460,20 @@ impl fmt::Display for ExtXDateRange { write!(f, ",CLASS={}", quote(value))?; } - #[cfg(feature = "chrono")] - { - write!( - f, - ",START-DATE={}", - quote(&self.start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true)) - )?; - } + if let Some(value) = &self.start_date { + #[cfg(feature = "chrono")] + { + write!( + f, + ",START-DATE={}", + quote(&value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + } - #[cfg(not(feature = "chrono"))] - { - write!(f, ",START-DATE={}", quote(&self.start_date))?; + #[cfg(not(feature = "chrono"))] + { + write!(f, ",START-DATE={}", quote(&value))?; + } } if let Some(value) = &self.end_date { From 41f81aebb3fb1231311e95fb7a38ceda6665e443 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:15:58 +0200 Subject: [PATCH 103/112] add `html_root_url` --- Cargo.toml | 2 +- src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 796490f..7abd9c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hls_m3u8" -version = "0.2.1" +version = "0.2.1" # remember to update html_root_url authors = ["Takeru Ohta "] description = "HLS m3u8 parser/generator" homepage = "https://github.com/sile/hls_m3u8" diff --git a/src/lib.rs b/src/lib.rs index 70edb06..259849e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#![doc(html_root_url = "https://docs.rs/hls_m3u8/0.2.1")] #![forbid(unsafe_code)] #![warn( clippy::pedantic, // From 3492c529c5c608ae0f3f0302cc04ee1394d7c030 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:19:28 +0200 Subject: [PATCH 104/112] add `version-sync` --- Cargo.toml | 1 + tests/version-number.rs | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 tests/version-number.rs diff --git a/Cargo.toml b/Cargo.toml index 7abd9c0..df43eaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ strum = { version = "0.17", features = ["derive"] } [dev-dependencies] pretty_assertions = "0.6" +version-sync = "0.9" diff --git a/tests/version-number.rs b/tests/version-number.rs new file mode 100644 index 0000000..288592d --- /dev/null +++ b/tests/version-number.rs @@ -0,0 +1,9 @@ +#[test] +fn test_readme_deps() { + version_sync::assert_markdown_deps_updated!("README.md"); +} + +#[test] +fn test_html_root_url() { + version_sync::assert_html_root_url_updated!("src/lib.rs"); +} From 25a01261f50d3d51da219a3102169d14faa4b586 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:20:02 +0200 Subject: [PATCH 105/112] add features section in `Cargo.toml` --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index df43eaf..77d2627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ keywords = ["hls", "m3u8"] edition = "2018" categories = ["parser"] +[features] +default = [] [badges] codecov = { repository = "sile/hls_m3u8" } travis-ci = { repository = "sile/hls_m3u8" } From f90ea7a1217295510ead095ffe51e179ebbd8d4c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:26:50 +0200 Subject: [PATCH 106/112] slightly improve `PlaylistType` --- src/types/playlist_type.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs index 36127f3..2c0293e 100644 --- a/src/types/playlist_type.rs +++ b/src/types/playlist_type.rs @@ -5,13 +5,11 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE] +/// Provides mutability information about the [`MediaPlaylist`]. /// -/// The [`PlaylistType`] tag provides mutability information about the -/// [`MediaPlaylist`]. It applies to the entire [`MediaPlaylist`]. +/// It applies to the entire [`MediaPlaylist`]. /// /// [`MediaPlaylist`]: 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 PlaylistType { /// If the [`PlaylistType`] is Event, [`MediaSegment`]s From f0d91c5e7c2d0d55aa989e2aba2ecdf001aeb1e4 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:43:13 +0200 Subject: [PATCH 107/112] fix rust_2018_idioms --- src/error.rs | 2 +- src/lib.rs | 1 + src/master_playlist.rs | 2 +- src/media_playlist.rs | 2 +- src/media_segment.rs | 2 +- src/tags/basic/m3u.rs | 5 +---- src/tags/basic/version.rs | 2 +- src/tags/master_playlist/media.rs | 2 +- src/tags/master_playlist/session_data.rs | 2 +- src/tags/master_playlist/session_key.rs | 2 +- src/tags/master_playlist/variant_stream.rs | 2 +- src/tags/media_playlist/discontinuity_sequence.rs | 2 +- src/tags/media_playlist/end_list.rs | 2 +- src/tags/media_playlist/i_frames_only.rs | 2 +- src/tags/media_playlist/media_sequence.rs | 2 +- src/tags/media_playlist/target_duration.rs | 2 +- src/tags/media_segment/byte_range.rs | 2 +- src/tags/media_segment/date_range.rs | 2 +- src/tags/media_segment/discontinuity.rs | 2 +- src/tags/media_segment/inf.rs | 2 +- src/tags/media_segment/key.rs | 2 +- src/tags/media_segment/map.rs | 2 +- src/tags/media_segment/program_date_time.rs | 2 +- src/tags/shared/independent_segments.rs | 2 +- src/tags/shared/start.rs | 2 +- src/types/byte_range.rs | 2 +- src/types/channels.rs | 2 +- src/types/closed_captions.rs | 2 +- src/types/codecs.rs | 2 +- src/types/decryption_key.rs | 2 +- src/types/initialization_vector.rs | 2 +- src/types/key_format.rs | 2 +- src/types/key_format_versions.rs | 2 +- src/types/playlist_type.rs | 2 +- src/types/protocol_version.rs | 2 +- src/types/stream_data.rs | 2 +- src/types/value.rs | 2 +- 37 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/error.rs b/src/error.rs index 01a0bfe..1e30f59 100644 --- a/src/error.rs +++ b/src/error.rs @@ -84,7 +84,7 @@ impl PartialEq for Error { impl std::error::Error for Error {} impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) } } #[allow(clippy::needless_pass_by_value)] diff --git a/src/lib.rs b/src/lib.rs index 259849e..7149cd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![doc(html_root_url = "https://docs.rs/hls_m3u8/0.2.1")] #![forbid(unsafe_code)] +#![warn(rust_2018_idioms)] #![warn( clippy::pedantic, // clippy::nursery, diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 9ea96e0..df28c74 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -410,7 +410,7 @@ impl RequiredVersion for MasterPlaylistBuilder { } impl fmt::Display for MasterPlaylist { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; if self.required_version() != ProtocolVersion::V1 { diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 892e398..eb89d75 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -432,7 +432,7 @@ impl RequiredVersion for MediaPlaylist { } impl fmt::Display for MediaPlaylist { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; if self.required_version() != ProtocolVersion::V1 { diff --git a/src/media_segment.rs b/src/media_segment.rs index 1791eee..046b60b 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -202,7 +202,7 @@ impl MediaSegmentBuilder { } impl fmt::Display for MediaSegment { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance. if let Some(value) = &self.map { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index e5b78ea..03bbf09 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -5,8 +5,6 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [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 [`MediaPlaylist`] and [`MasterPlaylist`]. @@ -14,7 +12,6 @@ use crate::{Error, RequiredVersion}; /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist /// [`M3U`]: https://en.wikipedia.org/wiki/M3U -/// [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(crate) struct ExtM3u; @@ -28,7 +25,7 @@ impl RequiredVersion for ExtM3u { } impl fmt::Display for ExtM3u { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX) } } impl FromStr for ExtM3u { diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index e758f01..6345cf7 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -53,7 +53,7 @@ impl RequiredVersion for ExtXVersion { } impl fmt::Display for ExtXVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // write!(f, "{}{}", Self::PREFIX, self.0) } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 63da25b..94ff55d 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -333,7 +333,7 @@ impl RequiredVersion for ExtXMedia { } impl fmt::Display for ExtXMedia { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TYPE={}", self.media_type)?; diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 8d9058c..e762cfb 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -201,7 +201,7 @@ impl RequiredVersion for ExtXSessionData { } impl fmt::Display for ExtXSessionData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "DATA-ID={}", quote(&self.data_id))?; diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 0522abe..0534b0d 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -64,7 +64,7 @@ impl RequiredVersion for ExtXSessionKey { } impl fmt::Display for ExtXSessionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0.to_string()) } } diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index c3b2b46..ba0979e 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -264,7 +264,7 @@ impl RequiredVersion for VariantStream { } impl fmt::Display for VariantStream { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::ExtXIFrame { uri, stream_data } => { write!(f, "{}", Self::PREFIX_EXTXIFRAME)?; diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 0dbe218..9fcdde7 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -22,7 +22,7 @@ impl RequiredVersion for ExtXDiscontinuitySequence { } impl fmt::Display for ExtXDiscontinuitySequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // write!(f, "{}{}", Self::PREFIX, self.0) } diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 91992bc..b57e93d 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -23,7 +23,7 @@ impl RequiredVersion for ExtXEndList { } impl fmt::Display for ExtXEndList { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXEndList { diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 22971da..3e7225f 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -18,7 +18,7 @@ impl RequiredVersion for ExtXIFramesOnly { } impl fmt::Display for ExtXIFramesOnly { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXIFramesOnly { diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 20c59f8..4e36f3a 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -20,7 +20,7 @@ impl RequiredVersion for ExtXMediaSequence { } impl fmt::Display for ExtXMediaSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // write!(f, "{}{}", Self::PREFIX, self.0) } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 4d97561..ad641c1 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -20,7 +20,7 @@ impl RequiredVersion for ExtXTargetDuration { } impl fmt::Display for ExtXTargetDuration { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0.as_secs()) } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index eecf458..9bf5e52 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -180,7 +180,7 @@ where } impl fmt::Display for ExtXByteRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "{}", self.0)?; Ok(()) diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index a13ebe2..014dbea 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -452,7 +452,7 @@ impl FromStr for ExtXDateRange { } impl fmt::Display for ExtXDateRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "ID={}", quote(&self.id))?; diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 2bbb1d8..924bcd5 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -20,7 +20,7 @@ impl RequiredVersion for ExtXDiscontinuity { } impl fmt::Display for ExtXDiscontinuity { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXDiscontinuity { diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index ba3b241..466497b 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -139,7 +139,7 @@ impl RequiredVersion for ExtInf { } impl fmt::Display for ExtInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "{},", self.duration.as_secs_f64())?; diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index a3f2989..e201072 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -203,7 +203,7 @@ impl From for ExtXKey { } impl fmt::Display for ExtXKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; if let Some(value) = &self.0 { diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index abc7803..d1209b3 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -107,7 +107,7 @@ impl RequiredVersion for ExtXMap { } impl fmt::Display for ExtXMap { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "URI={}", quote(&self.uri))?; diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 77a8606..9e123d5 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -82,7 +82,7 @@ impl RequiredVersion for ExtXProgramDateTime { } impl fmt::Display for ExtXProgramDateTime { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let date_time = { #[cfg(feature = "chrono")] { diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 4b52c9b..557d8f0 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -22,7 +22,7 @@ impl RequiredVersion for ExtXIndependentSegments { } impl fmt::Display for ExtXIndependentSegments { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXIndependentSegments { diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 07203ba..c201fc7 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -99,7 +99,7 @@ impl RequiredVersion for ExtXStart { } impl fmt::Display for ExtXStart { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TIME-OFFSET={}", self.time_offset)?; diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index fdb8db6..ca1cce1 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -397,7 +397,7 @@ impl TryInto> for ByteRange { } impl fmt::Display for ByteRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.len())?; if let Some(value) = self.start { diff --git a/src/types/channels.rs b/src/types/channels.rs index 6e7e148..c5cb994 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -58,7 +58,7 @@ impl FromStr for Channels { } impl fmt::Display for Channels { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.number)?; Ok(()) diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 0948b13..049a665 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -63,7 +63,7 @@ impl> PartialEq for ClosedCaptions { } impl fmt::Display for ClosedCaptions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::GroupId(value) => write!(f, "{}", quote(value)), Self::None => write!(f, "NONE"), diff --git a/src/types/codecs.rs b/src/types/codecs.rs index db36e20..6b06ccf 100644 --- a/src/types/codecs.rs +++ b/src/types/codecs.rs @@ -44,7 +44,7 @@ impl Codecs { } impl fmt::Display for Codecs { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(codec) = self.list.iter().next() { write!(f, "{}", codec)?; diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 53bff38..c1cfbac 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -191,7 +191,7 @@ impl FromStr for DecryptionKey { } impl fmt::Display for DecryptionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?; if let InitializationVector::Aes128(_) = &self.iv { diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 0dcb9be..779d3f1 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -166,7 +166,7 @@ impl From> for InitializationVector { } impl fmt::Display for InitializationVector { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::Aes128(buffer) => { let mut result = [0; 0x10 * 2]; diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 77d89b6..ac97478 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -33,7 +33,7 @@ impl FromStr for KeyFormat { } impl fmt::Display for KeyFormat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote(&"identity")) } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", quote(&"identity")) } } /// This tag requires [`ProtocolVersion::V5`]. diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 601fdf6..518c7c5 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -395,7 +395,7 @@ impl FromStr for KeyFormatVersions { } impl fmt::Display for KeyFormatVersions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_default() || self.is_empty() { return write!(f, "{}", quote("1")); } diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs index 2c0293e..2ad1916 100644 --- a/src/types/playlist_type.rs +++ b/src/types/playlist_type.rs @@ -35,7 +35,7 @@ impl RequiredVersion for PlaylistType { } impl fmt::Display for PlaylistType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::Event => write!(f, "{}EVENT", Self::PREFIX), Self::Vod => write!(f, "{}VOD", Self::PREFIX), diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 21f7471..ebfac2c 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -34,7 +34,7 @@ impl ProtocolVersion { } impl fmt::Display for ProtocolVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::V1 => write!(f, "1"), Self::V2 => write!(f, "2"), diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs index 516c513..300aa25 100644 --- a/src/types/stream_data.rs +++ b/src/types/stream_data.rs @@ -257,7 +257,7 @@ impl StreamData { } impl fmt::Display for StreamData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "BANDWIDTH={}", self.bandwidth)?; if let Some(value) = &self.average_bandwidth { diff --git a/src/types/value.rs b/src/types/value.rs index 3c1b379..e7e9563 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -18,7 +18,7 @@ pub enum Value { } impl fmt::Display for Value { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + 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)), From 7a918d31bd73a6882d27f94c92ee88d8cfa4a72b Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:54:56 +0200 Subject: [PATCH 108/112] document crate features --- src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 7149cd9..e991a96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,40 @@ //! assert!(m3u8.parse::().is_ok()); //! ``` //! +//! ## Crate Feature Flags +//! +//! The following crate feature flags are available: +//! +//! - [`backtrace`] (optional) +//! - Enables the backtrace feature for the `Error` type. +//! - This feature depends on the following dependencies: +//! - [`backtrace`] +//! - [`chrono`] (optional) +//! - Enables parsing dates and verifying them. +//! - This feature depends on the following dependencies: +//! - [`chrono`] +//! - The following things will change: +//! - [`ExtXProgramDateTime::date_time`] will change from [`String`] to +//! `DateTime` +//! - [`ExtXDateRange::start_date`] will change from [`String`] to +//! `DateTime` +//! - [`ExtXDateRange::end_date`] will change from [`String`] to +//! `DateTime` +//! +//! They are configured in your `Cargo.toml` and can be enabled like this +//! +//! ```toml +//! hls_m3u8 = { version = "0.3", features = ["chrono", "backtrace"] } +//! ``` +//! +//! [`ExtXProgramDateTime::date_time`]: +//! crate::tags::ExtXProgramDateTime::date_time +//! [`ExtXDateRange::start_date`]: +//! crate::tags::ExtXDateRange::start_date +//! [`ExtXDateRange::end_date`]: +//! crate::tags::ExtXDateRange::end_date +//! [`chrono`]: https://github.com/chronotope/chrono +//! [`backtrace`]: https://github.com/rust-lang/backtrace-rs //! [HLS]: https://tools.ietf.org/html/rfc8216 pub use error::Error; From ec0b5cdb21cd794fccc60661528983b23f4bab27 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 09:28:50 +0200 Subject: [PATCH 109/112] improve rustfmt.toml --- rustfmt.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rustfmt.toml b/rustfmt.toml index 4a92eff..78ef888 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,9 +2,14 @@ error_on_unformatted = true edition = "2018" fn_single_line = true force_multiline_blocks = true + format_code_in_doc_comments = true format_macro_matchers = true +format_macro_bodies = true + match_arm_blocks = true reorder_impl_items = true use_field_init_shorthand = true wrap_comments = true +condense_wildcard_suffixes = true +unstable_features = true From fdc3442bb64f1996fadda4e38654dfd5e2e7e101 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 10:50:41 +0200 Subject: [PATCH 110/112] minor improvements to documentation --- src/tags/master_playlist/session_data.rs | 111 ++++++--------------- src/tags/master_playlist/variant_stream.rs | 4 +- 2 files changed, 35 insertions(+), 80 deletions(-) diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index e762cfb..f7a52bf 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -10,10 +10,9 @@ use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; /// The data of [`ExtXSessionData`]. -#[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SessionData { - /// This variant contains the data identified by the - /// [`ExtXSessionData::data_id`]. + /// Contains the data identified by the [`ExtXSessionData::data_id`]. /// /// If a [`language`] is specified, this variant should contain a /// human-readable string written in the specified language. @@ -31,7 +30,6 @@ pub enum SessionData { /// Allows arbitrary session data to be carried in a [`MasterPlaylist`]. /// /// [`MasterPlaylist`]: crate::MasterPlaylist -/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 #[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[builder(setter(into))] #[shorthand(enable(must_use, into))] @@ -39,25 +37,6 @@ pub struct ExtXSessionData { /// This should conform to a [reverse DNS] naming convention, such as /// `com.example.movie.title`. /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXSessionData; - /// use hls_m3u8::tags::SessionData; - /// - /// let mut session_data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Uri("https://www.example.com/".to_string()), - /// ); - /// - /// session_data.set_data_id("com.ironrust.movie.title"); - /// - /// assert_eq!( - /// session_data.data_id(), - /// &"com.ironrust.movie.title".to_string() - /// ); - /// ``` - /// /// # Note /// /// There is no central registration authority, so a value @@ -70,50 +49,13 @@ pub struct ExtXSessionData { /// The [`SessionData`] associated with the /// [`data_id`](ExtXSessionData::data_id). /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXSessionData; - /// use hls_m3u8::tags::SessionData; - /// - /// let mut session_data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Uri("https://www.example.com/".to_string()), - /// ); - /// - /// session_data.set_data(SessionData::Uri( - /// "https://www.example.com/data.json".to_string(), - /// )); - /// - /// assert_eq!( - /// session_data.data(), - /// &SessionData::Uri("https://www.example.com/data.json".to_string()) - /// ); - /// ``` - /// /// # Note /// /// This field is required. - #[shorthand(disable(into))] - data: SessionData, + #[shorthand(enable(skip))] + pub data: SessionData, /// The `language` attribute identifies the language of the [`SessionData`]. /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXSessionData; - /// use hls_m3u8::tags::SessionData; - /// - /// let mut session_data = ExtXSessionData::new( - /// "com.example.movie.title", - /// SessionData::Uri("https://www.example.com/".to_string()), - /// ); - /// - /// session_data.set_language(Some("en")); - /// - /// assert_eq!(session_data.language(), Some(&"en".to_string())); - /// ``` - /// /// # Note /// /// This field is optional and the provided value should conform to @@ -135,9 +77,9 @@ impl ExtXSessionData { /// # use hls_m3u8::tags::ExtXSessionData; /// use hls_m3u8::tags::SessionData; /// - /// ExtXSessionData::new( + /// let session_data = ExtXSessionData::new( /// "com.example.movie.title", - /// SessionData::Uri("https://www.example.com/".to_string()), + /// SessionData::Uri("https://www.example.com/".into()), /// ); /// ``` #[must_use] @@ -159,10 +101,10 @@ impl ExtXSessionData { /// /// let session_data = ExtXSessionData::builder() /// .data_id("com.example.movie.title") - /// .data(SessionData::Value("some data".to_string())) + /// .data(SessionData::Value("some data".into())) /// .language("en") /// .build()?; - /// # Ok::<(), Box>(()) + /// # Ok::<(), String>(()) /// ``` #[must_use] pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() } @@ -177,7 +119,7 @@ impl ExtXSessionData { /// /// let session_data = ExtXSessionData::with_language( /// "com.example.movie.title", - /// SessionData::Value("some data".to_string()), + /// SessionData::Value("some data".into()), /// "en", /// ); /// ``` @@ -244,17 +186,20 @@ impl FromStr for ExtXSessionData { } let data_id = data_id.ok_or_else(|| Error::missing_value("EXT-X-DATA-ID"))?; + let data = { if let Some(value) = session_value { if uri.is_some() { - return Err(Error::custom("Unexpected URI")); + return Err(Error::custom("unexpected URI")); } else { SessionData::Value(value) } } else if let Some(uri) = uri { SessionData::Uri(uri) } else { - return Err(Error::invalid_input()); + return Err(Error::custom( + "expected either `SessionData::Uri` or `SessionData::Value`", + )); } }; @@ -286,19 +231,27 @@ mod test { assert_eq!($struct, $str.parse().unwrap()); )+ - assert!("#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - LANGUAGE=\"baz\"" + assert!( + concat!( + "#EXT-X-SESSION-DATA:", + "DATA-ID=\"foo\",", + "LANGUAGE=\"baz\"" + ) .parse::() - .is_err()); + .is_err() + ); - assert!("#EXT-X-SESSION-DATA:\ - DATA-ID=\"foo\",\ - LANGUAGE=\"baz\",\ - VALUE=\"VALUE\",\ - URI=\"https://www.example.com/\"" + assert!( + concat!( + "#EXT-X-SESSION-DATA:", + "DATA-ID=\"foo\",", + "LANGUAGE=\"baz\",", + "VALUE=\"VALUE\",", + "URI=\"https://www.example.com/\"" + ) .parse::() - .is_err()); + .is_err() + ); } } diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index ba0979e..64af9dd 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -10,7 +10,9 @@ use crate::utils::{quote, tag, unquote}; use crate::Error; /// A server may offer multiple [`MediaPlaylist`] files to provide different -/// encodings of the same presentation. If it does so, it should provide +/// encodings of the same presentation. +/// +/// If it does so, it should provide /// a [`MasterPlaylist`] that lists each [`VariantStream`] to allow /// clients to switch between encodings dynamically. /// From 34df16f7d6e81a408929f8afec53ad042a778ed7 Mon Sep 17 00:00:00 2001 From: npajkovsky <54894472+npajkovsky@users.noreply.github.com> Date: Thu, 9 Apr 2020 14:10:59 +0200 Subject: [PATCH 111/112] fix EXT-X-MEDIA DEFAULT=yes wrt AUTOSELECT (#1) * fix EXT-X-MEDIA DEFAULT=yes wrt AUTOSELECT In 4.3.4.1. EXT-X-MEDIA section about AUTOSELECT is written If the AUTOSELECT attribute is present, its value MUST be YES if the value of the DEFAULT attribute is YES. That means, that if DEFAULT is YES and AUTOSELECT is *not present*, it ok. Before the patch, incorrect error is emitted If `DEFAULT` is true, `AUTOSELECT` has to be true too, Default: Some(true), Autoselect: None! Signed-off-by: Nikola Pajkovsky * update src/tags/master_playlist/media.rs Co-authored-by: Nikola Pajkovsky Co-authored-by: Lucas <24826124+Luro02@users.noreply.github.com> --- src/tags/master_playlist/media.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 94ff55d..a6aa8a5 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -238,9 +238,9 @@ impl ExtXMediaBuilder { ).to_string()); } - if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { + if self.is_default.unwrap_or(false) && self.is_autoselect.map_or(false, |b| !b) { return Err(Error::custom(format!( - "If `DEFAULT` is true, `AUTOSELECT` has to be true too, Default: {:?}, Autoselect: {:?}!", + "If `DEFAULT` is true, `AUTOSELECT` has to be true too, if present. Default: {:?}, Autoselect: {:?}!", self.is_default, self.is_autoselect )) .to_string()); @@ -462,6 +462,26 @@ mod test { } generate_tests! { + { + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("eng") + .name("English") + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + concat!( + "#EXT-X-MEDIA:", + "TYPE=AUDIO,", + "URI=\"eng/prog_index.m3u8\",", + "GROUP-ID=\"audio\",", + "LANGUAGE=\"eng\",", + "NAME=\"English\",", + "DEFAULT=YES", + ) + }, { ExtXMedia::builder() .media_type(MediaType::Audio) From a085971c423733201a9d17cdeafe18c3c6cc4e81 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Apr 2020 14:18:53 +0200 Subject: [PATCH 112/112] add cargo audit to github actions --- .github/workflows/audit.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/audit.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..4974b63 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,14 @@ +name: Security audit +on: + push: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' +jobs: + security_audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }}