From 4b4cffc248c069ea73aaea9c1e9998e7c390ca3f Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 4 Oct 2019 11:02:21 +0200 Subject: [PATCH 01/15] fix #20 --- src/error.rs | 8 + src/lib.rs | 3 + src/master_playlist.rs | 4 +- src/media_playlist.rs | 208 ++++++++++-------- src/media_segment.rs | 68 +++--- src/tags/basic/m3u.rs | 4 +- src/tags/basic/version.rs | 4 +- .../master_playlist/i_frame_stream_inf.rs | 4 +- src/tags/master_playlist/media.rs | 4 +- src/tags/master_playlist/session_data.rs | 4 +- src/tags/master_playlist/session_key.rs | 4 +- src/tags/master_playlist/stream_inf.rs | 6 +- .../media_playlist/discontinuity_sequence.rs | 3 +- src/tags/media_playlist/end_list.rs | 4 +- src/tags/media_playlist/i_frames_only.rs | 4 +- src/tags/media_playlist/media_sequence.rs | 4 +- src/tags/media_playlist/playlist_type.rs | 4 +- src/tags/media_playlist/target_duration.rs | 4 +- src/tags/media_segment/byte_range.rs | 4 +- src/tags/media_segment/date_range.rs | 7 +- src/tags/media_segment/discontinuity.rs | 4 +- src/tags/media_segment/inf.rs | 4 +- src/tags/media_segment/map.rs | 27 ++- src/tags/media_segment/program_date_time.rs | 4 +- src/tags/shared/independent_segments.rs | 4 +- src/tags/shared/start.rs | 4 +- src/traits.rs | 124 +++++++++++ src/types/decryption_key.rs | 3 +- src/types/key_format.rs | 4 +- src/types/key_format_versions.rs | 4 +- src/types/protocol_version.rs | 24 -- 31 files changed, 354 insertions(+), 207 deletions(-) create mode 100644 src/traits.rs diff --git a/src/error.rs b/src/error.rs index 0d85eca..4a7f48d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -76,6 +76,10 @@ pub enum ErrorKind { /// An unexpected value. UnexpectedAttribute(String), + #[fail(display = "Unexpected Tag: {:?}", _0)] + /// An unexpected tag. + UnexpectedTag(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -119,6 +123,10 @@ impl Error { Self::from(ErrorKind::UnexpectedAttribute(value.to_string())) } + pub(crate) fn unexpected_tag(value: T) -> Self { + Self::from(ErrorKind::UnexpectedTag(value.to_string())) + } + pub(crate) fn invalid_input() -> Self { Self::from(ErrorKind::InvalidInput) } pub(crate) fn parse_int_error(value: T) -> Self { diff --git a/src/lib.rs b/src/lib.rs index 54a0964..2996b1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![feature(option_flattening)] #![warn( //clippy::pedantic, clippy::nursery, @@ -49,6 +50,8 @@ mod line; mod master_playlist; mod media_playlist; mod media_segment; +mod traits; mod utils; pub use error::Result; +pub use traits::*; diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 2af4ff7..865b797 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -10,8 +10,8 @@ use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; -use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, RequiredVersion}; -use crate::Error; +use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; +use crate::{Error, RequiredVersion}; /// Master playlist. #[derive(Debug, Clone, Builder)] diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 31dde47..6630420 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -11,8 +11,8 @@ use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::{ProtocolVersion, RequiredVersion}; -use crate::Error; +use crate::types::ProtocolVersion; +use crate::{Encrypted, Error, RequiredVersion}; /// Media playlist. #[derive(Debug, Clone, Builder)] @@ -28,30 +28,30 @@ pub struct MediaPlaylist { /// The default is the maximum version among the tags in the playlist. #[builder(setter(name = "version"))] version_tag: ExtXVersion, - /// Sets the [ExtXTargetDuration] tag. + /// Sets the [`ExtXTargetDuration`] tag. target_duration_tag: ExtXTargetDuration, #[builder(default)] - /// Sets the [ExtXMediaSequence] tag. + /// Sets the [`ExtXMediaSequence`] tag. media_sequence_tag: Option, #[builder(default)] - /// Sets the [ExtXDiscontinuitySequence] tag. + /// Sets the [`ExtXDiscontinuitySequence`] tag. discontinuity_sequence_tag: Option, #[builder(default)] - /// Sets the [ExtXPlaylistType] tag. + /// Sets the [`ExtXPlaylistType`] tag. playlist_type_tag: Option, #[builder(default)] - /// Sets the [ExtXIFramesOnly] tag. + /// Sets the [`ExtXIFramesOnly`] tag. i_frames_only_tag: Option, #[builder(default)] - /// Sets the [ExtXIndependentSegments] tag. + /// Sets the [`ExtXIndependentSegments`] tag. independent_segments_tag: Option, #[builder(default)] - /// Sets the [ExtXStart] tag. + /// Sets the [`ExtXStart`] tag. start_tag: Option, #[builder(default)] - /// Sets the [ExtXEndList] tag. + /// Sets the [`ExtXEndList`] tag. end_list_tag: Option, - /// Sets all [MediaSegment]s. + /// Sets all [`MediaSegment`]s. segments: Vec, /// Sets the allowable excess duration of each media segment in the /// associated playlist. @@ -69,10 +69,7 @@ pub struct MediaPlaylist { impl MediaPlaylistBuilder { fn validate(&self) -> Result<(), String> { let required_version = self.required_version(); - let specified_version = self - .version_tag - .unwrap_or_else(|| required_version.into()) - .version(); + let specified_version = self.version_tag.map_or(required_version, |p| p.version()); if required_version > specified_version { return Err(Error::custom(format!( @@ -138,72 +135,6 @@ impl MediaPlaylistBuilder { Ok(()) } - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.target_duration_tag - .iter() - .map(|t| t.required_version()), - ) - .chain(self.media_sequence_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.discontinuity_sequence_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.playlist_type_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.i_frames_only_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.independent_segments_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.start_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.end_list_tag.iter().map(|t| { - if let Some(p) = t { - p.required_version() - } else { - ProtocolVersion::V1 - } - })) - .chain(self.segments.iter().map(|t| { - t.iter() - .map(|s| s.required_version()) - .max() - .unwrap_or(ProtocolVersion::V1) - })) - .max() - .unwrap_or_else(ProtocolVersion::latest) - } - /// Adds a media segment to the resulting playlist. pub fn push_segment>(&mut self, value: VALUE) -> &mut Self { if let Some(segments) = &mut self.segments { @@ -214,50 +145,111 @@ impl MediaPlaylistBuilder { self } - /// Parse the rest of the [MediaPlaylist] from an m3u8 file. + /// Parse the rest of the [`MediaPlaylist`] from an m3u8 file. pub fn parse(&mut self, input: &str) -> crate::Result { parse_media_playlist(input, self) } } +impl RequiredVersion for MediaPlaylistBuilder { + fn required_version(&self) -> ProtocolVersion { + iter::empty() + .chain( + self.target_duration_tag + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.media_sequence_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.discontinuity_sequence_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.playlist_type_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.i_frames_only_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.independent_segments_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.start_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain( + self.end_list_tag + .flatten() + .iter() + .map(|p| p.required_version()), + ) + .chain(self.segments.iter().map(|t| { + t.iter() + .map(|p| p.required_version()) + .max() + .unwrap_or(ProtocolVersion::V1) + })) + .max() + .unwrap_or_else(ProtocolVersion::latest) + } +} + impl MediaPlaylist { - /// Creates a [MediaPlaylistBuilder]. + /// Returns a builder for [`MediaPlaylist`]. pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } - /// Returns the `EXT-X-VERSION` tag contained in the playlist. + /// Returns the [`ExtXVersion`] tag contained in the playlist. pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } - /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. + /// Returns the [`ExtXTargetDuration`] tag contained in the playlist. pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. pub const fn media_sequence_tag(&self) -> Option { self.media_sequence_tag } - /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the + /// Returns the [`ExtXDiscontinuitySequence`] tag contained in the /// playlist. pub const fn discontinuity_sequence_tag(&self) -> Option { self.discontinuity_sequence_tag } - /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. + /// Returns the [`ExtXPlaylistType`] tag contained in the playlist. pub const fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } - /// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist. + /// Returns the [`ExtXIFramesOnly`] tag contained in the playlist. pub const fn i_frames_only_tag(&self) -> Option { self.i_frames_only_tag } - /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. + /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } - /// Returns the `EXT-X-START` tag contained in the playlist. + /// Returns the [`ExtXStart`] tag contained in the playlist. pub const fn start_tag(&self) -> Option { self.start_tag } - /// Returns the `EXT-X-ENDLIST` tag contained in the playlist. + /// Returns the [`ExtXEndList`] tag contained in the playlist. pub const fn end_list_tag(&self) -> Option { self.end_list_tag } - /// Returns the media segments contained in the playlist. - pub fn segments(&self) -> &[MediaSegment] { &self.segments } + /// Returns the [`MediaSegment`]s contained in the playlist. + pub const fn segments(&self) -> &Vec { &self.segments } } impl fmt::Display for MediaPlaylist { @@ -306,6 +298,8 @@ fn parse_media_playlist( let mut has_discontinuity_tag = false; let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1 + let mut available_key_tags = vec![]; + for (i, line) in input.parse::()?.into_iter().enumerate() { match line { Line::Tag(tag) => { @@ -336,10 +330,29 @@ fn parse_media_playlist( } Tag::ExtXKey(t) => { has_partial_segment = true; - segment.push_key_tag(t); + if !available_key_tags.is_empty() { + available_key_tags.push(t); + } else { + // An ExtXKey applies to every MediaSegment and to every Media + // Initialization Section declared by an EXT-X-MAP tag, that appears + // between it and the next EXT-X-KEY tag in the Playlist file with the + // same KEYFORMAT attribute (or the end of the Playlist file). + available_key_tags = available_key_tags + .into_iter() + .map(|k| { + if t.key_format() == k.key_format() { + t.clone() + } else { + k + } + }) + .collect(); + } } - Tag::ExtXMap(t) => { + Tag::ExtXMap(mut t) => { has_partial_segment = true; + + t.set_keys(available_key_tags.clone()); segment.map_tag(t); } Tag::ExtXProgramDateTime(t) => { @@ -379,7 +392,7 @@ fn parse_media_playlist( | Tag::ExtXIFrameStreamInf(_) | Tag::ExtXSessionData(_) | Tag::ExtXSessionKey(_) => { - return Err(Error::custom(tag)); + return Err(Error::unexpected_tag(tag)); } Tag::ExtXIndependentSegments(t) => { builder.independent_segments_tag(t); @@ -395,15 +408,18 @@ fn parse_media_playlist( } Line::Uri(uri) => { segment.uri(uri); + segment.keys(available_key_tags.clone()); segments.push(segment.build().map_err(Error::builder_error)?); segment = MediaSegment::builder(); has_partial_segment = false; } } } + if has_partial_segment { return Err(Error::invalid_input()); } + if !has_version { builder.version(ProtocolVersion::V1); } diff --git a/src/media_segment.rs b/src/media_segment.rs index 8f85a3c..7e3722d 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -6,43 +6,44 @@ use derive_builder::Builder; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, }; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; +use crate::{Encrypted, RequiredVersion}; -/// Media segment. #[derive(Debug, Clone, Builder)] #[builder(setter(into, strip_option))] +/// Media segment. pub struct MediaSegment { #[builder(default)] - /// Sets all [ExtXKey] tags. - key_tags: Vec, + /// Sets all [`ExtXKey`] tags. + keys: Vec, #[builder(default)] - /// Sets an [ExtXMap] tag. + /// Sets an [`ExtXMap`] tag. map_tag: Option, #[builder(default)] - /// Sets an [ExtXByteRange] tag. + /// Sets an [`ExtXByteRange`] tag. byte_range_tag: Option, #[builder(default)] - /// Sets an [ExtXDateRange] tag. + /// Sets an [`ExtXDateRange`] tag. date_range_tag: Option, #[builder(default)] - /// Sets an [ExtXDiscontinuity] tag. + /// Sets an [`ExtXDiscontinuity`] tag. discontinuity_tag: Option, #[builder(default)] - /// Sets an [ExtXProgramDateTime] tag. + /// Sets an [`ExtXProgramDateTime`] tag. program_date_time_tag: Option, - /// Sets an [ExtInf] tag. + /// Sets an [`ExtInf`] tag. inf_tag: ExtInf, - /// Sets an Uri. + /// Sets an `URI`. uri: String, } impl MediaSegmentBuilder { - /// Pushes an [ExtXKey] tag. + /// Pushes an [`ExtXKey`] tag. pub fn push_key_tag>(&mut self, value: VALUE) -> &mut Self { - if let Some(key_tags) = &mut self.key_tags { + if let Some(key_tags) = &mut self.keys { key_tags.push(value.into()); } else { - self.key_tags = Some(vec![value.into()]); + self.keys = Some(vec![value.into()]); } self } @@ -50,7 +51,7 @@ impl MediaSegmentBuilder { impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for value in &self.key_tags { + for value in &self.keys { writeln!(f, "{}", value)?; } if let Some(value) = &self.map_tag { @@ -75,41 +76,38 @@ impl fmt::Display for MediaSegment { } impl MediaSegment { - /// Creates a [MediaSegmentBuilder]. + /// Creates a [`MediaSegmentBuilder`]. pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } - /// Returns the URI of the media segment. + /// Returns the `URI` of the media segment. pub const fn uri(&self) -> &String { &self.uri } - /// Returns the `EXT-X-INF` tag associated with the media segment. + /// Returns the [`ExtInf`] tag associated with the media segment. pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } - /// Returns the `EXT-X-BYTERANGE` tag associated with the media segment. + /// Returns the [`ExtXByteRange`] tag associated with the media segment. pub const fn byte_range_tag(&self) -> Option { self.byte_range_tag } - /// Returns the `EXT-X-DATERANGE` tag associated with the media segment. - pub fn date_range_tag(&self) -> Option<&ExtXDateRange> { self.date_range_tag.as_ref() } + /// Returns the [`ExtXDateRange`] tag associated with the media segment. + pub const fn date_range_tag(&self) -> &Option { &self.date_range_tag } - /// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment. + /// Returns the [`ExtXDiscontinuity`] tag associated with the media segment. pub const fn discontinuity_tag(&self) -> Option { self.discontinuity_tag } - /// Returns the `EXT-X-PROGRAM-DATE-TIME` tag associated with the media + /// Returns the [`ExtXProgramDateTime`] tag associated with the media /// segment. - pub fn program_date_time_tag(&self) -> Option<&ExtXProgramDateTime> { - self.program_date_time_tag.as_ref() + pub const fn program_date_time_tag(&self) -> Option { + self.program_date_time_tag } - /// Returns the `EXT-X-MAP` tag associated with the media segment. - pub fn map_tag(&self) -> Option<&ExtXMap> { self.map_tag.as_ref() } - - /// Returns the `EXT-X-KEY` tags associated with the media segment. - pub fn key_tags(&self) -> &[ExtXKey] { &self.key_tags } + /// Returns the [`ExtXMap`] tag associated with the media segment. + pub const fn map_tag(&self) -> &Option { &self.map_tag } } impl RequiredVersion for MediaSegment { fn required_version(&self) -> ProtocolVersion { iter::empty() - .chain(self.key_tags.iter().map(|t| t.required_version())) + .chain(self.keys.iter().map(|t| t.required_version())) .chain(self.map_tag.iter().map(|t| t.required_version())) .chain(self.byte_range_tag.iter().map(|t| t.required_version())) .chain(self.date_range_tag.iter().map(|t| t.required_version())) @@ -121,6 +119,12 @@ impl RequiredVersion for MediaSegment { ) .chain(iter::once(self.inf_tag.required_version())) .max() - .unwrap_or(ProtocolVersion::V7) + .unwrap_or_else(ProtocolVersion::latest) } } + +impl Encrypted for MediaSegment { + fn keys(&self) -> &Vec { &self.keys } + + fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +} diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 2fc1523..07077d3 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.1.1. EXTM3U] /// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]** diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index d473810..54e559f 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.1.2. EXT-X-VERSION] /// The [`ExtXVersion`] tag indicates the compatibility version of the diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index f886430..7b051c6 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -3,9 +3,9 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion, StreamInf}; +use crate::types::{ProtocolVersion, StreamInf}; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.5.3. EXT-X-I-FRAME-STREAM-INF] /// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file, diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 8358609..3f990e9 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -4,9 +4,9 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion, RequiredVersion}; +use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.5.1. EXT-X-MEDIA] /// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s, diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index a6fd617..674947b 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -4,9 +4,9 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// The data of an [ExtXSessionData] tag. #[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index c9776fc..bdd7fdb 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -2,9 +2,9 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVersion}; +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.3.4.5. EXT-X-SESSION-KEY] /// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 4772ae6..065ed01 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -3,11 +3,9 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ - ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, RequiredVersion, StreamInf, -}; +use crate::types::{ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, StreamInf}; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// [4.3.4.2. EXT-X-STREAM-INF] /// diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 1ca9d8e..fbfd851 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,8 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; +use crate::RequiredVersion; /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 119de43..4e62b30 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.4. EXT-X-ENDLIST] /// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 7c52ba4..59aae4b 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] /// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index e80a570..3564955 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.2. EXT-X-MEDIA-SEQUENCE] /// The [`ExtXMediaSequence`] tag indicates the Media Sequence Number of diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 80bcee8..6ce50b4 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.5. EXT-X-PLAYLIST-TYPE] /// diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index c70ab24..83a6aa9 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -2,9 +2,9 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.3.1. EXT-X-TARGETDURATION] /// The [`ExtXTargetDuration`] tag specifies the maximum [`Media Segment`] diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index a79769f..0705d9d 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -2,9 +2,9 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; +use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.2.2. EXT-X-BYTERANGE] /// diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 67c1089..12f15db 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -6,9 +6,9 @@ use std::time::Duration; use chrono::{DateTime, FixedOffset}; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.3.2.7. EXT-X-DATERANGE] /// @@ -106,7 +106,8 @@ impl ExtXDateRange { /// # use hls_m3u8::tags::ExtXDateRange; /// use chrono::offset::TimeZone; /// use chrono::{DateTime, FixedOffset}; -/// use hls_m3u8::types::{ProtocolVersion, RequiredVersion}; +/// use hls_m3u8::types::ProtocolVersion; +/// use hls_m3u8::RequiredVersion; /// /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds /// diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index ee0d607..250ed65 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.2.3. EXT-X-DISCONTINUITY] /// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index e13338f..337ed1f 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -2,9 +2,9 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.4.2.1. EXTINF] /// diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 05de349..dc6552f 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -2,9 +2,10 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; +use crate::tags::ExtXKey; +use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Encrypted, Error, RequiredVersion}; /// # [4.4.2.5. EXT-X-MAP] /// The [`ExtXMap`] tag specifies how to obtain the Media Initialization @@ -22,6 +23,7 @@ use crate::Error; pub struct ExtXMap { uri: String, range: Option, + keys: Vec, } impl ExtXMap { @@ -29,17 +31,19 @@ impl ExtXMap { /// Makes a new [`ExtXMap`] tag. pub fn new(uri: T) -> Self { - ExtXMap { + Self { uri: uri.to_string(), range: None, + keys: vec![], } } /// Makes a new [`ExtXMap`] tag with the given range. pub fn with_range(uri: T, range: ByteRange) -> Self { - ExtXMap { + Self { uri: uri.to_string(), range: Some(range), + keys: vec![], } } @@ -51,6 +55,13 @@ impl ExtXMap { pub const fn range(&self) -> Option { self.range } } +impl Encrypted for ExtXMap { + fn keys(&self) -> &Vec { &self.keys } + + fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +} + +/// This tag requires [`ProtocolVersion::V6`]. impl RequiredVersion for ExtXMap { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } } @@ -59,9 +70,11 @@ impl fmt::Display for ExtXMap { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "URI={}", quote(&self.uri))?; + if let Some(value) = &self.range { write!(f, ",BYTERANGE={}", quote(value))?; } + Ok(()) } } @@ -90,7 +103,11 @@ impl FromStr for ExtXMap { } let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?; - Ok(ExtXMap { uri, range }) + Ok(Self { + uri, + range, + keys: vec![], + }) } } diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 4ce0aa2..9350761 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -4,9 +4,9 @@ use std::str::FromStr; use chrono::{DateTime, FixedOffset}; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] /// The [`ExtXProgramDateTime`] tag associates the first sample of a diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 9655e0f..a617a5f 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 045bfa4..a69d8b1 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -2,9 +2,9 @@ use std::fmt; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, RequiredVersion, SignedDecimalFloatingPoint}; +use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; use crate::utils::{parse_yes_or_no, tag}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// [4.3.5.2. EXT-X-START] /// diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..fa1df6a --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,124 @@ +use crate::tags::ExtXKey; +use crate::types::{EncryptionMethod, ProtocolVersion}; + +/// A trait, that is implemented on all tags, that could be encrypted. +/// +/// # Example +/// ``` +/// use hls_m3u8::tags::ExtXKey; +/// use hls_m3u8::types::EncryptionMethod; +/// use hls_m3u8::Encrypted; +/// +/// struct ExampleTag { +/// keys: Vec, +/// } +/// +/// // 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; + + /// Returns an exclusive reference to all keys, that can be used to decrypt + /// this tag. + fn keys_mut(&mut self) -> &mut Vec; + + /// 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. + /// + /// # Note + /// This will return `true`, if any of the keys satisfies + /// ```text + /// key.method() != EncryptionMethod::None + /// ``` + fn is_encrypted(&self) -> bool { + if self.keys().is_empty() { + return false; + } + self.keys() + .iter() + .any(|k| k.method() != EncryptionMethod::None) + } + + /// Returns `false`, if the tag is not encrypted. + /// + /// # Note + /// This is the inverse of [`is_encrypted`]. + /// + /// [`is_encrypted`]: #method.is_encrypted + fn is_not_encrypted(&self) -> bool { !self.is_encrypted() } +} + +/// # Example +/// Implementing it: +/// ``` +/// # use hls_m3u8::RequiredVersion; +/// use hls_m3u8::types::ProtocolVersion; +/// +/// struct ExampleTag(u64); +/// +/// impl RequiredVersion for ExampleTag { +/// fn required_version(&self) -> ProtocolVersion { +/// if self.0 == 5 { +/// ProtocolVersion::V4 +/// } else { +/// ProtocolVersion::V1 +/// } +/// } +/// } +/// assert_eq!(ExampleTag(5).required_version(), ProtocolVersion::V4); +/// assert_eq!(ExampleTag(2).required_version(), ProtocolVersion::V1); +/// ``` +pub trait RequiredVersion { + /// Returns the protocol compatibility version that this tag requires. + fn required_version(&self) -> ProtocolVersion; +} diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index a6995cd..9476d2a 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -6,10 +6,9 @@ use derive_builder::Builder; use crate::attribute::AttributePairs; use crate::types::{ EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion, - RequiredVersion, }; use crate::utils::{quote, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[builder(setter(into), build_fn(validate = "Self::validate"))] diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 01510db..d1268a4 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] /// [`KeyFormat`] specifies, how the key is represented in the diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 9a871cb..91bc413 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -2,9 +2,9 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::types::ProtocolVersion; use crate::utils::{quote, unquote}; -use crate::Error; +use crate::{Error, RequiredVersion}; /// A list of [usize], that can be used to indicate which version(s) /// this instance complies with, if more than one version of a particular diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index c90c5a5..d81d0ed 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -3,30 +3,6 @@ use std::str::FromStr; use crate::Error; -/// # Example -/// Implementing it: -/// ``` -/// # use hls_m3u8::types::{ProtocolVersion, RequiredVersion}; -/// # -/// struct NewTag(u64); -/// -/// impl RequiredVersion for NewTag { -/// fn required_version(&self) -> ProtocolVersion { -/// if self.0 == 5 { -/// ProtocolVersion::V4 -/// } else { -/// ProtocolVersion::V1 -/// } -/// } -/// } -/// assert_eq!(NewTag(5).required_version(), ProtocolVersion::V4); -/// assert_eq!(NewTag(2).required_version(), ProtocolVersion::V1); -/// ``` -pub trait RequiredVersion { - /// Returns the protocol compatibility version that this tag requires. - fn required_version(&self) -> ProtocolVersion; -} - /// # [7. Protocol Version Compatibility] /// The [`ProtocolVersion`] specifies, which m3u8 revision is required, to parse /// a certain tag correctly. From f96207c93e1fcc771ad8ba9d165dd5a57f38dad4 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 4 Oct 2019 11:19:03 +0200 Subject: [PATCH 02/15] remove `draft` content --- src/types/channels.rs | 111 ++++++++---------------------------------- 1 file changed, 20 insertions(+), 91 deletions(-) diff --git a/src/types/channels.rs b/src/types/channels.rs index b620edb..5cad2dc 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -11,14 +11,6 @@ use crate::Error; /// present in any [`MediaSegment`] in the rendition. For example, an /// `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute. /// -/// The second parameter identifies the encoding of object-based audio used by -/// the rendition. This parameter is a comma-separated list of Audio -/// Object Coding Identifiers. It is optional. An Audio Object -/// Coding Identifier is a string containing characters from the set -/// `[A..Z]`, `[0..9]`, and `'-'`. They are codec-specific. A parameter -/// value of consisting solely of the dash character (`'-'`) indicates -/// that the audio is not object-based. -/// /// # Example /// Creating a `CHANNELS="6"` attribute /// ``` @@ -31,16 +23,11 @@ use crate::Error; /// ); /// ``` /// -/// # Note -/// Currently there are no example playlists in the documentation, -/// or in popular m3u8 libraries, showing a usage for the second parameter -/// of [`Channels`], so if you have one please open an issue on github! -/// /// [`MediaSegment`]: crate::MediaSegment #[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Channels { - first_parameter: u64, - second_parameter: Option>, + channel_number: u64, + unknown: Vec, } impl Channels { @@ -51,77 +38,36 @@ impl Channels { /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); /// ``` - pub const fn new(value: u64) -> Self { + pub fn new(value: u64) -> Self { Self { - first_parameter: value, - second_parameter: None, + channel_number: value, + unknown: vec![], } } - /// Returns the first parameter. + /// Returns the channel number. /// /// # Example /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(6); /// - /// assert_eq!(channels.first_parameter(), 6); + /// assert_eq!(channels.channel_number(), 6); /// ``` - pub const fn first_parameter(&self) -> u64 { self.first_parameter } + pub const fn channel_number(&self) -> u64 { self.channel_number } - /// Sets the first parameter. + /// Sets the channel number. /// /// # Example /// ``` /// # use hls_m3u8::types::Channels; /// let mut channels = Channels::new(3); /// - /// channels.set_first_parameter(6); - /// assert_eq!(channels.first_parameter(), 6) + /// channels.set_channel_number(6); + /// assert_eq!(channels.channel_number(), 6) /// ``` - pub fn set_first_parameter(&mut self, value: u64) -> &mut Self { - self.first_parameter = value; - self - } - - /// Returns the second parameter, if there is any! - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(3); - /// # assert_eq!(channels.second_parameter(), &None); - /// - /// channels.set_second_parameter(Some(vec!["AAC", "MP3"])); - /// assert_eq!( - /// channels.second_parameter(), - /// &Some(vec!["AAC".to_string(), "MP3".to_string()]) - /// ) - /// ``` - /// - /// # Note - /// Currently there is no use for this parameter. - pub const fn second_parameter(&self) -> &Option> { &self.second_parameter } - - /// Sets the second parameter. - /// - /// # Example - /// ``` - /// # use hls_m3u8::types::Channels; - /// let mut channels = Channels::new(3); - /// # assert_eq!(channels.second_parameter(), &None); - /// - /// channels.set_second_parameter(Some(vec!["AAC", "MP3"])); - /// assert_eq!( - /// channels.second_parameter(), - /// &Some(vec!["AAC".to_string(), "MP3".to_string()]) - /// ) - /// ``` - /// - /// # Note - /// Currently there is no use for this parameter. - pub fn set_second_parameter(&mut self, value: Option>) -> &mut Self { - self.second_parameter = value.map(|v| v.into_iter().map(|s| s.to_string()).collect()); + pub fn set_channel_number(&mut self, value: u64) -> &mut Self { + self.channel_number = value; self } } @@ -131,28 +77,23 @@ impl FromStr for Channels { fn from_str(input: &str) -> Result { let parameters = input.split('/').collect::>(); - let first_parameter = parameters + let channel_number = parameters .first() .ok_or_else(|| Error::missing_attribute("First parameter of channels!"))? .parse()?; - let second_parameter = parameters - .get(1) - .map(|v| v.split(',').map(|v| v.to_string()).collect()); - Ok(Self { - first_parameter, - second_parameter, + channel_number, + unknown: parameters[1..].iter().map(|v| v.to_string()).collect(), }) } } impl fmt::Display for Channels { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.first_parameter)?; - - if let Some(second) = &self.second_parameter { - write!(f, "/{}", second.join(","))?; + write!(f, "{}", self.channel_number)?; + if !self.unknown.is_empty() { + write!(f, "{}", self.unknown.join(","))?; } Ok(()) @@ -168,25 +109,13 @@ mod tests { let mut channels = Channels::new(6); assert_eq!(channels.to_string(), "6".to_string()); - channels.set_first_parameter(7); + channels.set_channel_number(7); assert_eq!(channels.to_string(), "7".to_string()); - - assert_eq!( - "6/P,K,J".to_string(), - Channels::new(6) - .set_second_parameter(Some(vec!["P", "K", "J"])) - .to_string() - ); } #[test] fn test_parser() { assert_eq!("6".parse::().unwrap(), Channels::new(6)); - let mut result = Channels::new(6); - result.set_second_parameter(Some(vec!["P", "K", "J"])); - - assert_eq!("6/P,K,J".parse::().unwrap(), result); - assert!("garbage".parse::().is_err()); assert!("".parse::().is_err()); } From 5b44262dc826158167c77f33cffd31117a65e5f5 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 09:44:23 +0200 Subject: [PATCH 03/15] minor changes --- src/master_playlist.rs | 62 ++++++++++--------- src/media_playlist.rs | 6 +- src/tags/basic/m3u.rs | 21 ++++--- src/tags/basic/version.rs | 5 +- .../master_playlist/i_frame_stream_inf.rs | 5 +- src/tags/master_playlist/stream_inf.rs | 1 + 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 865b797..39713b1 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -28,54 +28,59 @@ pub struct MasterPlaylist { /// The default is the maximum version among the tags in the playlist. version_tag: ExtXVersion, #[builder(default)] - /// Sets the [ExtXIndependentSegments] tag. + /// Sets the [`ExtXIndependentSegments`] tag. independent_segments_tag: Option, #[builder(default)] - /// Sets the [ExtXStart] tag. + /// Sets the [`ExtXStart`] tag. start_tag: Option, - /// Sets the [ExtXMedia] tag. + #[builder(default)] + /// Sets the [`ExtXMedia`] tag. media_tags: Vec, - /// Sets all [ExtXStreamInf]s. + #[builder(default)] + /// Sets all [`ExtXStreamInf`]s. stream_inf_tags: Vec, - /// Sets all [ExtXIFrameStreamInf]s. + #[builder(default)] + /// Sets all [`ExtXIFrameStreamInf`]s. i_frame_stream_inf_tags: Vec, - /// Sets all [ExtXSessionData]s. + #[builder(default)] + /// Sets all [`ExtXSessionData`]s. session_data_tags: Vec, - /// Sets all [ExtXSessionKey]s. + #[builder(default)] + /// Sets all [`ExtXSessionKey`]s. session_key_tags: Vec, } impl MasterPlaylist { - /// Returns a Builder for a MasterPlaylist. + /// Returns a Builder for a [`MasterPlaylist`]. pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } - /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } + /// Returns the [`ExtXVersion`] tag contained in the playlist. + pub const fn version(&self) -> ExtXVersion { self.version_tag } - /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. + /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } - /// Returns the `EXT-X-START` tag contained in the playlist. + /// Returns the [`ExtXStart`] tag contained in the playlist. pub const fn start_tag(&self) -> Option { self.start_tag } - /// Returns the `EXT-X-MEDIA` tags contained in the playlist. - pub fn media_tags(&self) -> &[ExtXMedia] { &self.media_tags } + /// Returns the [`ExtXMedia`] tags contained in the playlist. + pub const fn media_tags(&self) -> &Vec { &self.media_tags } - /// Returns the `EXT-X-STREAM-INF` tags contained in the playlist. - pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] { &self.stream_inf_tags } + /// Returns the [`ExtXStreamInf`] tags contained in the playlist. + pub const fn stream_inf_tags(&self) -> &Vec { &self.stream_inf_tags } - /// Returns the `EXT-X-I-FRAME-STREAM-INF` tags contained in the playlist. - pub fn i_frame_stream_inf_tags(&self) -> &[ExtXIFrameStreamInf] { + /// Returns the [`ExtXIFrameStreamInf`] tags contained in the playlist. + pub const fn i_frame_stream_inf_tags(&self) -> &Vec { &self.i_frame_stream_inf_tags } - /// Returns the `EXT-X-SESSION-DATA` tags contained in the playlist. - pub fn session_data_tags(&self) -> &[ExtXSessionData] { &self.session_data_tags } + /// Returns the [`ExtXSessionData`] tags contained in the playlist. + pub const fn session_data_tags(&self) -> &Vec { &self.session_data_tags } - /// Returns the `EXT-X-SESSION-KEY` tags contained in the playlist. - pub fn session_key_tags(&self) -> &[ExtXSessionKey] { &self.session_key_tags } + /// Returns the [`ExtXSessionKey`] tags contained in the playlist. + pub const fn session_key_tags(&self) -> &Vec { &self.session_key_tags } } impl RequiredVersion for MasterPlaylist { @@ -85,10 +90,7 @@ impl RequiredVersion for MasterPlaylist { impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { let required_version = self.required_version(); - let specified_version = self - .version_tag - .unwrap_or_else(|| required_version.into()) - .version(); + let specified_version = self.version_tag.map_or(required_version, |p| p.version()); if required_version > specified_version { return Err(Error::required_version(required_version, specified_version).to_string()); @@ -107,15 +109,15 @@ impl MasterPlaylistBuilder { iter::empty() .chain( self.independent_segments_tag + .flatten() .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), + .map(|p| p.required_version()), ) .chain( self.start_tag + .flatten() .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), + .map(|p| p.required_version()), ) .chain( self.media_tags diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 6630420..dba09db 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -72,11 +72,7 @@ impl MediaPlaylistBuilder { let specified_version = self.version_tag.map_or(required_version, |p| p.version()); if required_version > specified_version { - return Err(Error::custom(format!( - "required_version: {}, specified_version: {}", - required_version, specified_version - )) - .to_string()); + return Err(Error::required_version(required_version, specified_version).to_string()); } if let Some(target_duration) = &self.target_duration_tag { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 07077d3..e4620fe 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -5,7 +5,7 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.1.1. EXTM3U] +/// # [4.3.1.1. EXTM3U] /// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]** /// Playlist file. /// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. @@ -32,8 +32,7 @@ use crate::{Error, RequiredVersion}; /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist /// [`M3U`]: https://en.wikipedia.org/wiki/M3U -/// [4.4.1.1. EXTM3U]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.1.1 +/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ExtM3u; @@ -42,6 +41,15 @@ impl ExtM3u { } /// This tag requires [`ProtocolVersion::V1`]. +/// +/// # Example +/// ``` +/// # use hls_m3u8::tags::ExtM3u; +/// use hls_m3u8::types::ProtocolVersion; +/// use hls_m3u8::RequiredVersion; +/// +/// assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1); +/// ``` impl RequiredVersion for ExtM3u { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -70,11 +78,6 @@ mod test { #[test] fn test_parser() { - assert_eq!("#EXTM3U".parse::().ok(), Some(ExtM3u)); - } - - #[test] - fn test_required_version() { - assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1); + assert_eq!("#EXTM3U".parse::().unwrap(), ExtM3u); } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 54e559f..58bf1e1 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -5,7 +5,7 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.1.2. EXT-X-VERSION] +/// # [4.3.1.2. EXT-X-VERSION] /// The [`ExtXVersion`] tag indicates the compatibility version of the /// [`Master Playlist`] or [`Media Playlist`] file. /// It applies to the entire Playlist. @@ -41,8 +41,7 @@ use crate::{Error, RequiredVersion}; /// /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist -/// [4.4.1.2. EXT-X-VERSION]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.1.2 +/// [4.4.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/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 7b051c6..e795c7f 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -7,7 +7,7 @@ use crate::types::{ProtocolVersion, StreamInf}; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; -/// # [4.4.5.3. EXT-X-I-FRAME-STREAM-INF] +/// # [4.3.5.3. EXT-X-I-FRAME-STREAM-INF] /// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file, /// containing the I-frames of a multimedia presentation. /// @@ -16,8 +16,7 @@ use crate::{Error, RequiredVersion}; /// /// [`Master Playlist`]: crate::MasterPlaylist /// [`Media Playlist`]: crate::MediaPlaylist -/// [4.4.5.3. EXT-X-I-FRAME-STREAM-INF]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.3 +/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { uri: String, diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 065ed01..9c5127c 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -188,6 +188,7 @@ impl ExtXStreamInf { } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXStreamInf { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } From f76b22348225d98592b808cd95024b4544626316 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 12:49:08 +0200 Subject: [PATCH 04/15] made master playlist smarter --- src/master_playlist.rs | 282 +++++++++++------- src/tags/basic/m3u.rs | 1 + src/tags/basic/version.rs | 3 +- .../master_playlist/i_frame_stream_inf.rs | 72 ++++- src/tags/master_playlist/stream_inf.rs | 118 +++++++- src/tags/media_playlist/playlist_type.rs | 26 +- src/tags/media_segment/inf.rs | 11 +- src/traits.rs | 19 ++ src/types/decimal_resolution.rs | 5 +- src/types/stream_inf.rs | 24 +- tests/master_playlist.rs | 115 +++++++ 11 files changed, 539 insertions(+), 137 deletions(-) create mode 100644 tests/master_playlist.rs diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 39713b1..7a5e2c5 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,6 +1,5 @@ use std::collections::HashSet; use std::fmt; -use std::iter; use std::str::FromStr; use derive_builder::Builder; @@ -13,19 +12,13 @@ use crate::tags::{ use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::{Error, RequiredVersion}; -/// Master playlist. -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, PartialEq)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] +/// Master playlist. pub struct MasterPlaylist { - #[builder(default, setter(name = "version"))] - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility - /// version greater than `version`, - /// `build()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. + //#[builder(default, setter(name = "version"))] + #[builder(default, setter(skip))] version_tag: ExtXVersion, #[builder(default)] /// Sets the [`ExtXIndependentSegments`] tag. @@ -37,16 +30,16 @@ pub struct MasterPlaylist { /// Sets the [`ExtXMedia`] tag. media_tags: Vec, #[builder(default)] - /// Sets all [`ExtXStreamInf`]s. + /// Sets all [`ExtXStreamInf`] tags. stream_inf_tags: Vec, #[builder(default)] - /// Sets all [`ExtXIFrameStreamInf`]s. + /// Sets all [`ExtXIFrameStreamInf`] tags. i_frame_stream_inf_tags: Vec, #[builder(default)] - /// Sets all [`ExtXSessionData`]s. + /// Sets all [`ExtXSessionData`] tags. session_data_tags: Vec, #[builder(default)] - /// Sets all [`ExtXSessionKey`]s. + /// Sets all [`ExtXSessionKey`] tags. session_key_tags: Vec, } @@ -54,48 +47,152 @@ impl MasterPlaylist { /// Returns a Builder for a [`MasterPlaylist`]. pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } - /// Returns the [`ExtXVersion`] tag contained in the playlist. - pub const fn version(&self) -> ExtXVersion { self.version_tag } - /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. - pub const fn independent_segments_tag(&self) -> Option { + 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(|v| v.into()); + self + } + /// Returns the [`ExtXStart`] tag contained in the playlist. - pub const fn start_tag(&self) -> Option { self.start_tag } + 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(|v| v.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(|v| v.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(|v| v.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(|v| v.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(|v| v.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(|v| v.into()).collect(); + self + } +} + +macro_rules! required_version { + ( $( $tag:expr ),* ) => { + ::core::iter::empty() + $( + .chain(::core::iter::once($tag.required_version())) + )* + .max() + .unwrap_or_default() + } } impl RequiredVersion for MasterPlaylist { - fn required_version(&self) -> ProtocolVersion { self.version_tag.version() } + fn required_version(&self) -> ProtocolVersion { + required_version![ + self.independent_segments_tag, + self.start_tag, + self.media_tags, + self.stream_inf_tags, + self.i_frame_stream_inf_tags, + self.session_data_tags, + self.session_key_tags + ] + } } impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { - let required_version = self.required_version(); - let specified_version = self.version_tag.map_or(required_version, |p| p.version()); - - if required_version > specified_version { - return Err(Error::required_version(required_version, specified_version).to_string()); - } - self.validate_stream_inf_tags().map_err(|e| e.to_string())?; self.validate_i_frame_stream_inf_tags() .map_err(|e| e.to_string())?; @@ -105,54 +202,6 @@ impl MasterPlaylistBuilder { Ok(()) } - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.independent_segments_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.start_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.media_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.stream_inf_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.i_frame_stream_inf_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.session_data_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .chain( - self.session_key_tags - .iter() - .map(|t| t.iter().map(|t| t.required_version())) - .flatten(), - ) - .max() - .unwrap_or_else(ProtocolVersion::latest) - } - fn validate_stream_inf_tags(&self) -> crate::Result<()> { if let Some(value) = &self.stream_inf_tags { let mut has_none_closed_captions = false; @@ -232,11 +281,29 @@ impl MasterPlaylistBuilder { } } +impl RequiredVersion for MasterPlaylistBuilder { + fn required_version(&self) -> ProtocolVersion { + // TODO: the .flatten() can be removed as soon as `recursive traits` are + // supported. (RequiredVersion is implemented for Option, but + // not for Option>) + // https://github.com/rust-lang/chalk/issues/12 + required_version![ + self.independent_segments_tag.flatten(), + self.start_tag.flatten(), + self.media_tags, + self.stream_inf_tags, + self.i_frame_stream_inf_tags, + self.session_data_tags, + self.session_key_tags + ] + } +} + impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; - if self.version_tag.version() != ProtocolVersion::V1 { - writeln!(f, "{}", self.version_tag)?; + if self.required_version() != ProtocolVersion::V1 { + writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } for t in &self.media_tags { writeln!(f, "{}", t)?; @@ -288,8 +355,12 @@ impl FromStr for MasterPlaylist { Tag::ExtM3u(_) => { return Err(Error::invalid_input()); } - Tag::ExtXVersion(t) => { - builder.version(t.version()); + Tag::ExtXVersion(_) => { + // This tag can be ignored, because the + // MasterPlaylist will automatically set the + // ExtXVersion tag to correct version! + + // builder.version(t.version()); } Tag::ExtInf(_) | Tag::ExtXByteRange(_) @@ -359,36 +430,35 @@ mod tests { #[test] fn test_parser() { - r#"#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/low/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/lo_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/hi_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360 -http://example.com/high/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" -http://example.com/audio/index.m3u8 -"# - .parse::() - .unwrap(); + "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/low/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/lo_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/hi_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n\ + http://example.com/high/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n\ + http://example.com/audio/index.m3u8\n" + .parse::() + .unwrap(); } #[test] fn test_display() { - let input = r#"#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/low/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/lo_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234 -http://example.com/hi_mid/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360 -http://example.com/high/index.m3u8 -#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" -http://example.com/audio/index.m3u8 -"#; + let input = "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/low/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/lo_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ + http://example.com/hi_mid/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n\ + http://example.com/high/index.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n\ + http://example.com/audio/index.m3u8\n"; + let playlist = input.parse::().unwrap(); assert_eq!(playlist.to_string(), input); } diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index e4620fe..1d47d93 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -6,6 +6,7 @@ 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 [`Media Playlist`] and [`Master Playlist`]. diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 58bf1e1..f7f68ca 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -6,6 +6,7 @@ use crate::utils::tag; 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. /// It applies to the entire Playlist. @@ -41,7 +42,7 @@ use crate::{Error, RequiredVersion}; /// /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist -/// [4.4.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 +/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct ExtXVersion(ProtocolVersion); diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index e795c7f..55a42d9 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -3,11 +3,12 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, StreamInf}; +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. /// @@ -23,6 +24,72 @@ pub struct ExtXIFrameStreamInf { stream_inf: StreamInf, } +#[derive(Default, Debug, Clone, PartialEq)] +/// Builder for [`ExtXIFrameStreamInf`]. +pub struct ExtXIFrameStreamInfBuilder { + uri: Option, + stream_inf: StreamInfBuilder, +} + +impl ExtXIFrameStreamInfBuilder { + /// An `URI` to the [`MediaPlaylist`] file. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + pub fn uri>(&mut self, value: T) -> &mut Self { + self.uri = Some(value.into()); + self + } + + /// The maximum bandwidth of the stream. + pub fn bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.bandwidth(value); + self + } + + /// The average bandwidth of the stream. + pub fn average_bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.average_bandwidth(value); + self + } + + /// Every media format in any of the renditions specified by the Variant + /// Stream. + pub fn codecs>(&mut self, value: T) -> &mut Self { + self.stream_inf.codecs(value); + self + } + + /// The resolution of the stream. + pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self { + self.stream_inf.resolution(value); + self + } + + /// High-bandwidth Digital Content Protection + pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self { + self.stream_inf.hdcp_level(value); + self + } + + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. + pub fn video>(&mut self, value: T) -> &mut Self { + self.stream_inf.video(value); + self + } + + /// Build an [`ExtXIFrameStreamInf`]. + pub fn build(&self) -> crate::Result { + Ok(ExtXIFrameStreamInf { + uri: self + .uri + .clone() + .ok_or_else(|| Error::missing_value("frame rate"))?, + stream_inf: self.stream_inf.build().map_err(Error::builder_error)?, + }) + } +} + impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; @@ -40,6 +107,9 @@ impl ExtXIFrameStreamInf { } } + /// Returns a builder for [`ExtXIFrameStreamInf`]. + pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() } + /// Returns the `URI`, that identifies the associated [`media playlist`]. /// /// # Example diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 9c5127c..33cf5c5 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -3,11 +3,22 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::attribute::AttributePairs; -use crate::types::{ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, StreamInf}; +use crate::types::{ + ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder, +}; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; -/// [4.3.4.2. EXT-X-STREAM-INF] +/// # [4.3.4.2. EXT-X-STREAM-INF] +/// +/// The [`ExtXStreamInf`] tag specifies a Variant Stream, which is a set +/// of Renditions that can be combined to play the presentation. The +/// attributes of the tag provide information about the Variant Stream. +/// +/// The URI line that follows the [`ExtXStreamInf`] tag specifies a Media +/// Playlist that carries a rendition of the Variant Stream. The URI +/// line is REQUIRED. Clients that do not support multiple video +/// Renditions SHOULD play this Rendition. /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 #[derive(PartialOrd, Debug, Clone, PartialEq, Eq)] @@ -20,6 +31,104 @@ pub struct ExtXStreamInf { stream_inf: StreamInf, } +#[derive(Default, Debug, Clone)] +/// Builder for [`ExtXStreamInf`]. +pub struct ExtXStreamInfBuilder { + uri: Option, + frame_rate: Option, + audio: Option, + subtitles: Option, + closed_captions: Option, + stream_inf: StreamInfBuilder, +} + +impl ExtXStreamInfBuilder { + /// An `URI` to the [`MediaPlaylist`] file. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + pub fn uri>(&mut self, value: T) -> &mut Self { + self.uri = Some(value.into()); + self + } + + /// Maximum frame rate for all the video in the variant stream. + pub fn frame_rate(&mut self, value: f64) -> &mut Self { + self.frame_rate = Some(value.into()); + self + } + + /// The group identifier for the audio in the variant stream. + pub fn audio>(&mut self, value: T) -> &mut Self { + self.audio = Some(value.into()); + self + } + + /// The group identifier for the subtitles in the variant stream. + pub fn subtitles>(&mut self, value: T) -> &mut Self { + self.subtitles = Some(value.into()); + self + } + + /// The value of [`ClosedCaptions`] attribute. + pub fn closed_captions>(&mut self, value: T) -> &mut Self { + self.closed_captions = Some(value.into()); + self + } + + /// The maximum bandwidth of the stream. + pub fn bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.bandwidth(value); + self + } + + /// The average bandwidth of the stream. + pub fn average_bandwidth(&mut self, value: u64) -> &mut Self { + self.stream_inf.average_bandwidth(value); + self + } + + /// Every media format in any of the renditions specified by the Variant + /// Stream. + pub fn codecs>(&mut self, value: T) -> &mut Self { + self.stream_inf.codecs(value); + self + } + + /// The resolution of the stream. + pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self { + self.stream_inf.resolution(value); + self + } + + /// High-bandwidth Digital Content Protection + pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self { + self.stream_inf.hdcp_level(value); + self + } + + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. + pub fn video>(&mut self, value: T) -> &mut Self { + self.stream_inf.video(value); + self + } + + /// Build an [`ExtXStreamInf`]. + pub fn build(&self) -> crate::Result { + Ok(ExtXStreamInf { + uri: self + .uri + .clone() + .ok_or_else(|| Error::missing_value("frame rate"))?, + frame_rate: self.frame_rate, + audio: self.audio.clone(), + subtitles: self.subtitles.clone(), + closed_captions: self.closed_captions.clone(), + stream_inf: self.stream_inf.build().map_err(Error::builder_error)?, + }) + } +} + impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; @@ -41,6 +150,9 @@ impl ExtXStreamInf { } } + /// Returns a builder for [`ExtXStreamInf`]. + pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() } + /// Returns the `URI` that identifies the associated media playlist. /// /// # Example @@ -169,7 +281,7 @@ impl ExtXStreamInf { /// ``` pub const fn closed_captions(&self) -> &Option { &self.closed_captions } - /// Returns the value of [`ClosedCaptions`] attribute. + /// Sets the value of [`ClosedCaptions`] attribute. /// /// # Example /// ``` diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 6ce50b4..1c22b0a 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -5,26 +5,25 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE] +/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE] /// /// The [`ExtXPlaylistType`] tag provides mutability information about the /// [`Media Playlist`]. It applies to the entire [`Media Playlist`]. /// -/// Its format is: -/// ```text -/// #EXT-X-PLAYLIST-TYPE: -/// ``` -/// -/// [Media Playlist]: crate::MediaPlaylist -/// [4.4.3.5. EXT-X-PLAYLIST-TYPE]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5 +/// [`Media Playlist`]: crate::MediaPlaylist +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExtXPlaylistType { - /// If the [`ExtXPlaylistType`] is Event, Media Segments - /// can only be added to the end of the Media Playlist. + /// If the [`ExtXPlaylistType`] is Event, [`Media Segment`]s + /// can only be added to the end of the [`Media Playlist`]. + /// + /// [`Media Segment`]: crate::MediaSegment + /// [`Media Playlist`]: crate::MediaPlaylist Event, /// If the [`ExtXPlaylistType`] is Video On Demand (Vod), - /// the Media Playlist cannot change. + /// the [`Media Playlist`] cannot change. + /// + /// [`Media Playlist`]: crate::MediaPlaylist Vod, } @@ -32,6 +31,7 @@ impl ExtXPlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXPlaylistType { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -81,6 +81,8 @@ mod test { assert!("#EXT-X-PLAYLIST-TYPE:H" .parse::() .is_err()); + + assert!("garbage".parse::().is_err()); } #[test] diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 337ed1f..92663f3 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -6,20 +6,13 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.2.1. EXTINF] +/// # [4.3.2.1. EXTINF] /// /// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies /// only to the next [`Media Segment`]. /// -/// Its format is: -/// ```text -/// #EXTINF:,[] -/// ``` -/// The title is an optional informative title about the [Media Segment]. -/// /// [`Media Segment`]: crate::media_segment::MediaSegment -/// [4.4.2.1. EXTINF]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1 +/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { duration: Duration, diff --git a/src/traits.rs b/src/traits.rs index fa1df6a..c47defc 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -122,3 +122,22 @@ pub trait RequiredVersion { /// Returns the protocol compatibility version that this tag requires. fn required_version(&self) -> ProtocolVersion; } + +impl<T: RequiredVersion> RequiredVersion for Vec<T> { + fn required_version(&self) -> ProtocolVersion { + self.iter() + .map(|v| v.required_version()) + .max() + // return ProtocolVersion::V1, if the iterator is empty: + .unwrap_or_default() + } +} + +impl<T: RequiredVersion> RequiredVersion for Option<T> { + fn required_version(&self) -> ProtocolVersion { + self.iter() + .map(|v| v.required_version()) + .max() + .unwrap_or_default() + } +} diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index 0deea04..234cc95 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -4,14 +4,15 @@ use derive_more::Display; use crate::Error; -/// Decimal resolution. +/// This is a simple wrapper type for the display resolution. (1920x1080, +/// 1280x720, ...). /// /// See: [4.2. Attribute Lists] /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)] #[display(fmt = "{}x{}", width, height)] -pub(crate) struct DecimalResolution { +pub struct DecimalResolution { width: usize, height: usize, } diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index e40d037..9f48634 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -1,26 +1,44 @@ use std::fmt; use std::str::FromStr; +use derive_builder::Builder; + use crate::attribute::AttributePairs; use crate::types::{DecimalResolution, HdcpLevel}; use crate::utils::{quote, unquote}; use crate::Error; -/// [4.3.4.2. EXT-X-STREAM-INF] +/// # [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] +#[builder(setter(into, strip_option))] +#[builder(derive(Debug, PartialEq))] pub struct StreamInf { + /// The maximum bandwidth of the stream. bandwidth: u64, + #[builder(default)] + /// The average bandwidth of the stream. average_bandwidth: Option<u64>, + #[builder(default)] + /// Every media format in any of the renditions specified by the Variant + /// Stream. codecs: Option<String>, + #[builder(default)] + /// The resolution of the stream. resolution: Option<DecimalResolution>, + #[builder(default)] + /// High-bandwidth Digital Content Protection hdcp_level: Option<HdcpLevel>, + #[builder(default)] + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. video: Option<String>, } impl StreamInf { - /// Creates a new [StreamInf]. + /// Creates a new [`StreamInf`]. + /// /// # Examples /// ``` /// # use hls_m3u8::types::StreamInf; diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs new file mode 100644 index 0000000..18bcf49 --- /dev/null +++ b/tests/master_playlist.rs @@ -0,0 +1,115 @@ +use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXStreamInf}; +use hls_m3u8::MasterPlaylist; + +#[test] +fn test_master_playlist() { + let master_playlist = "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\ + http://example.com/low.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n\ + http://example.com/mid.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n\ + http://example.com/hi.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\ + http://example.com/audio-only.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .average_bandwidth(1000000) + .uri("http://example.com/low.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .average_bandwidth(2000000) + .uri("http://example.com/mid.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .average_bandwidth(6000000) + .uri("http://example.com/hi.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .uri("http://example.com/audio-only.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} + +#[test] +fn test_master_playlist_with_i_frames() { + let master_playlist = "#EXTM3U\n\ + #EXT-X-STREAM-INF:BANDWIDTH=1280000\n\ + low/audio-video.m3u8\n\ + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n\ + #EXT-X-STREAM-INF:BANDWIDTH=2560000\n\ + mid/audio-video.m3u8\n\ + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n\ + #EXT-X-STREAM-INF:BANDWIDTH=7680000\n\ + hi/audio-video.m3u8\n\ + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n\ + #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\ + audio-only.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .uri("low/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .uri("mid/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .uri("hi/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .uri("audio-only.m3u8") + .build() + .unwrap(), + ]) + .i_frame_stream_inf_tags(vec![ + ExtXIFrameStreamInf::builder() + .bandwidth(86000) + .uri("low/iframe.m3u8") + .build() + .unwrap(), + ExtXIFrameStreamInf::builder() + .bandwidth(150000) + .uri("mid/iframe.m3u8") + .build() + .unwrap(), + ExtXIFrameStreamInf::builder() + .bandwidth(550000) + .uri("hi/iframe.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} From 99493446ebf24219e9d14a44f7475c42d05ed429 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 13:15:42 +0200 Subject: [PATCH 05/15] Infallible errors https://doc.rust-lang.org/std/convert/enum.Infallible.html --- src/error.rs | 10 ++++++++++ src/master_playlist.rs | 1 - src/types/closed_captions.rs | 4 ++-- src/types/key_format_versions.rs | 5 +++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/error.rs b/src/error.rs index 4a7f48d..51c01a0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -210,3 +210,13 @@ impl From<::strum::ParseError> for Error { Error::custom(value) // TODO! } } + +impl From<String> for Error { + fn from(value: String) -> Self { Error::custom(value) } +} + +impl From<::core::convert::Infallible> for Error { + fn from(_: ::core::convert::Infallible) -> Self { + Error::custom("An Infallible error has been returned! (this should never happen!)") + } +} diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 7a5e2c5..7ba57bc 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -17,7 +17,6 @@ use crate::{Error, RequiredVersion}; #[builder(setter(into, strip_option))] /// Master playlist. pub struct MasterPlaylist { - //#[builder(default, setter(name = "version"))] #[builder(default, setter(skip))] version_tag: ExtXVersion, #[builder(default)] diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index b3425a4..2c73a99 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,8 +1,8 @@ +use core::convert::Infallible; use std::fmt; use std::str::FromStr; use crate::utils::{quote, unquote}; -use crate::Error; /// The identifier of a closed captions group or its absence. /// @@ -26,7 +26,7 @@ impl fmt::Display for ClosedCaptions { } impl FromStr for ClosedCaptions { - type Err = Error; + type Err = Infallible; fn from_str(input: &str) -> Result<Self, Self::Err> { if input.trim() == "NONE" { diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 91bc413..4c33031 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -1,10 +1,11 @@ +use std::convert::Infallible; use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::{quote, unquote}; -use crate::{Error, RequiredVersion}; +use crate::RequiredVersion; /// A list of [usize], that can be used to indicate which version(s) /// this instance complies with, if more than one version of a particular @@ -51,7 +52,7 @@ impl RequiredVersion for KeyFormatVersions { } impl FromStr for KeyFormatVersions { - type Err = Error; + type Err = Infallible; fn from_str(input: &str) -> Result<Self, Self::Err> { let mut result = unquote(input) From 8d1ed6372b99f8e0eee43cf1ccf6b7fcd43f36eb Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 13:23:41 +0200 Subject: [PATCH 06/15] removed Eq implementation f64 and f32 don't implement Eq for a reason! For example this comparison `1.0 + 2.0 == 3.0` is false for floats, even though it should be true! --- src/line.rs | 4 ++-- src/tags/master_playlist/stream_inf.rs | 2 +- src/tags/shared/start.rs | 2 +- src/types/decimal_floating_point.rs | 2 -- src/types/key_format.rs | 1 + src/types/key_format_versions.rs | 9 +++++---- src/types/signed_decimal_floating_point.rs | 2 -- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/line.rs b/src/line.rs index 8923f44..771e243 100644 --- a/src/line.rs +++ b/src/line.rs @@ -79,14 +79,14 @@ impl DerefMut for Lines { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum Line { Tag(Tag), Uri(String), } #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum Tag { ExtM3u(tags::ExtM3u), ExtXVersion(tags::ExtXVersion), diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 33cf5c5..a273d61 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -21,7 +21,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(PartialOrd, Debug, Clone, PartialEq, Eq)] +#[derive(PartialOrd, Debug, Clone, PartialEq)] pub struct ExtXStreamInf { uri: String, frame_rate: Option<DecimalFloatingPoint>, diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index a69d8b1..54d897a 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -9,7 +9,7 @@ use crate::{Error, RequiredVersion}; /// [4.3.5.2. EXT-X-START] /// /// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 -#[derive(PartialOrd, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(PartialOrd, Debug, Clone, Copy, PartialEq)] pub struct ExtXStart { time_offset: SignedDecimalFloatingPoint, precise: bool, diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 8ab8b20..f12d5c4 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -35,8 +35,6 @@ impl DecimalFloatingPoint { pub const fn as_f64(self) -> f64 { self.0 } } -impl Eq for DecimalFloatingPoint {} - // this trait is implemented manually, so it doesn't construct a // [`DecimalFloatingPoint`], with a negative value. impl FromStr for DecimalFloatingPoint { diff --git a/src/types/key_format.rs b/src/types/key_format.rs index d1268a4..5b534ff 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -31,6 +31,7 @@ impl fmt::Display for KeyFormat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote("identity")) } } +/// This tag requires [`ProtocolVersion::V5`]. impl RequiredVersion for KeyFormat { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 4c33031..16537a9 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -15,10 +15,6 @@ use crate::RequiredVersion; #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct KeyFormatVersions(Vec<usize>); -impl Default for KeyFormatVersions { - fn default() -> Self { Self(vec![1]) } -} - impl KeyFormatVersions { /// Makes a new [`KeyFormatVersions`]. pub fn new() -> Self { Self::default() } @@ -37,6 +33,10 @@ impl KeyFormatVersions { pub fn is_default(&self) -> bool { self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() } } +impl Default for KeyFormatVersions { + fn default() -> Self { Self(vec![1]) } +} + impl Deref for KeyFormatVersions { type Target = Vec<usize>; @@ -47,6 +47,7 @@ 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 541503b..63e9b80 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -33,8 +33,6 @@ impl Deref for SignedDecimalFloatingPoint { fn deref(&self) -> &Self::Target { &self.0 } } -impl Eq for SignedDecimalFloatingPoint {} - #[cfg(test)] mod tests { use super::*; From 4ffd4350f864d697a72fcc367d4d833656ed668c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 14:45:40 +0200 Subject: [PATCH 07/15] improve documentation #31 --- src/master_playlist.rs | 34 ++++++++++++++++++++++++++++++++++ src/tags/media_segment/key.rs | 32 ++++++++++++++++---------------- src/tags/shared/start.rs | 2 +- src/types/decryption_key.rs | 14 +++++++------- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 7ba57bc..e4ea9e4 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -21,29 +21,63 @@ pub struct MasterPlaylist { version_tag: ExtXVersion, #[builder(default)] /// Sets the [`ExtXIndependentSegments`] tag. + /// + /// # Note + /// This tag is optional. independent_segments_tag: Option<ExtXIndependentSegments>, #[builder(default)] /// Sets the [`ExtXStart`] tag. + /// + /// # Note + /// This tag is optional. start_tag: Option<ExtXStart>, #[builder(default)] /// Sets the [`ExtXMedia`] tag. + /// + /// # Note + /// This tag is optional. media_tags: Vec<ExtXMedia>, #[builder(default)] /// Sets all [`ExtXStreamInf`] tags. + /// + /// # Note + /// This tag is optional. stream_inf_tags: Vec<ExtXStreamInf>, #[builder(default)] /// Sets all [`ExtXIFrameStreamInf`] tags. + /// + /// # Note + /// This tag is optional. i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>, #[builder(default)] /// Sets all [`ExtXSessionData`] tags. + /// + /// # Note + /// This tag is optional. session_data_tags: Vec<ExtXSessionData>, #[builder(default)] /// Sets all [`ExtXSessionKey`] tags. + /// + /// # Note + /// This tag is optional. session_key_tags: Vec<ExtXSessionKey>, } 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(()) + /// # } + /// ``` pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } /// Returns the [`ExtXIndependentSegments`] tag contained in the playlist. diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index df15ef2..b5e0528 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -2,31 +2,26 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod}; +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; use crate::utils::tag; -use crate::Error; +use crate::{Error, RequiredVersion}; -/// # [4.4.2.4. EXT-X-KEY] +/// # [4.3.2.4. EXT-X-KEY] +/// /// [`Media Segment`]s may be encrypted. The [`ExtXKey`] tag specifies how to /// decrypt them. It applies to every [`Media Segment`] and to every Media /// Initialization Section declared by an [`ExtXMap`] tag, that appears /// between it and the next [`ExtXKey`] tag in the Playlist file with the /// same [`KeyFormat`] attribute (or the end of the Playlist file). /// -/// The format is: -/// ```text -/// #EXT-X-KEY:<attribute-list> -/// ``` -/// /// # Note -/// In case of an empty key (`EncryptionMethod::None`), +/// In case of an empty key ([`EncryptionMethod::None`]), /// all attributes will be ignored. /// /// [`KeyFormat`]: crate::types::KeyFormat /// [`ExtXMap`]: crate::tags::ExtXMap /// [`Media Segment`]: crate::MediaSegment -/// [4.4.2.4. EXT-X-KEY]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.4 +/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXKey(DecryptionKey); @@ -37,7 +32,7 @@ impl ExtXKey { /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXKey; + /// # use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; /// /// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); @@ -55,8 +50,7 @@ impl ExtXKey { /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// + /// # use hls_m3u8::tags::ExtXKey; /// let key = ExtXKey::empty(); /// /// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE"); @@ -72,20 +66,26 @@ impl ExtXKey { } /// Returns whether the [`EncryptionMethod`] is - /// [`None`](EncryptionMethod::None). + /// [`None`]. /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXKey; + /// # use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; /// /// let key = ExtXKey::empty(); /// /// assert_eq!(key.method() == EncryptionMethod::None, key.is_empty()); /// ``` + /// + /// [`None`]: EncryptionMethod::None pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } } +impl RequiredVersion for ExtXKey { + fn required_version(&self) -> ProtocolVersion { self.0.required_version() } +} + impl FromStr for ExtXKey { type Err = Error; diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 54d897a..701b792 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -26,7 +26,7 @@ impl ExtXStart { /// # Example /// ``` /// # use hls_m3u8::tags::ExtXStart; - /// ExtXStart::new(20.123456); + /// let start = ExtXStart::new(20.123456); /// ``` pub fn new(time_offset: f64) -> Self { Self { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 9476d2a..0d17686 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -45,7 +45,7 @@ impl DecryptionKeyBuilder { } impl DecryptionKey { - /// Makes a new [DecryptionKey]. + /// Makes a new [`DecryptionKey`]. /// /// # Example /// ``` @@ -64,7 +64,7 @@ impl DecryptionKey { } } - /// Returns the [EncryptionMethod]. + /// Returns the [`EncryptionMethod`]. /// /// # Example /// ``` @@ -80,7 +80,7 @@ impl DecryptionKey { /// Returns a Builder to build a [DecryptionKey]. pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } - /// Sets the [EncryptionMethod]. + /// Sets the [`EncryptionMethod`]. /// /// # Example /// ``` @@ -120,7 +120,7 @@ impl DecryptionKey { /// Sets the `URI` attribute. /// /// # Note - /// This attribute is required, if the [EncryptionMethod] is not `None`. + /// This attribute is required, if the [`EncryptionMethod`] is not `None`. /// /// # Example /// ``` @@ -207,7 +207,7 @@ impl DecryptionKey { /// ``` pub const fn key_format(&self) -> Option<KeyFormat> { self.key_format } - /// Sets the [KeyFormat] attribute. + /// Sets the [`KeyFormat`] attribute. /// /// # Example /// ``` @@ -225,7 +225,7 @@ impl DecryptionKey { self } - /// Returns the [KeyFormatVersions] attribute. + /// Returns the [`KeyFormatVersions`] attribute. /// /// # Example /// ``` @@ -245,7 +245,7 @@ impl DecryptionKey { &self.key_format_versions } - /// Sets the [KeyFormatVersions] attribute. + /// Sets the [`KeyFormatVersions`] attribute. /// /// # Example /// ``` From 32876e137115cc21474529debe4cf3a6f9c7bcdc Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 16:08:03 +0200 Subject: [PATCH 08/15] fix some clippy lints --- src/attribute.rs | 12 +-- src/error.rs | 18 ++-- src/line.rs | 94 +++++++++---------- src/master_playlist.rs | 16 ++-- src/tags/basic/m3u.rs | 14 +-- .../master_playlist/i_frame_stream_inf.rs | 2 +- src/tags/master_playlist/media.rs | 11 ++- src/tags/master_playlist/session_data.rs | 18 ++-- src/tags/master_playlist/stream_inf.rs | 6 +- 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_segment/byte_range.rs | 2 +- src/tags/media_segment/discontinuity.rs | 2 +- src/tags/media_segment/inf.rs | 5 +- src/tags/media_segment/map.rs | 5 - src/tags/shared/independent_segments.rs | 2 +- src/types/decimal_floating_point.rs | 4 +- src/types/decimal_resolution.rs | 2 +- src/types/decryption_key.rs | 4 +- src/types/signed_decimal_floating_point.rs | 18 ++-- src/types/stream_inf.rs | 2 +- 22 files changed, 117 insertions(+), 126 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 91d42e6..1a513b7 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -39,7 +39,7 @@ impl FromStr for AttributePairs { type Err = Error; fn from_str(input: &str) -> Result<Self, Self::Err> { - let mut result = AttributePairs::new(); + let mut result = Self::new(); for line in split(input, ',') { let pair = split(line.trim(), '='); @@ -67,16 +67,12 @@ fn split(value: &str, terminator: char) -> Vec<String> { let mut result = vec![]; let mut inside_quotes = false; - let mut temp_string = String::new(); + let mut temp_string = String::with_capacity(1024); for c in value.chars() { match c { '"' => { - if inside_quotes { - inside_quotes = false; - } else { - inside_quotes = true; - } + inside_quotes = !inside_quotes; temp_string.push(c); } k if (k == terminator) => { @@ -84,7 +80,7 @@ fn split(value: &str, terminator: char) -> Vec<String> { temp_string.push(c); } else { result.push(temp_string); - temp_string = String::new(); + temp_string = String::with_capacity(1024); } } _ => { diff --git a/src/error.rs b/src/error.rs index 51c01a0..c661819 100644 --- a/src/error.rs +++ b/src/error.rs @@ -107,11 +107,11 @@ impl fmt::Display for Error { } impl From<ErrorKind> for Error { - fn from(kind: ErrorKind) -> Error { Error::from(Context::new(kind)) } + fn from(kind: ErrorKind) -> Self { Self::from(Context::new(kind)) } } impl From<Context<ErrorKind>> for Error { - fn from(inner: Context<ErrorKind>) -> Error { Error { inner } } + fn from(inner: Context<ErrorKind>) -> Self { Self { inner } } } impl Error { @@ -190,33 +190,33 @@ impl Error { } impl From<::std::num::ParseIntError> for Error { - fn from(value: ::std::num::ParseIntError) -> Self { Error::parse_int_error(value) } + fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int_error(value) } } impl From<::std::num::ParseFloatError> for Error { - fn from(value: ::std::num::ParseFloatError) -> Self { Error::parse_float_error(value) } + fn from(value: ::std::num::ParseFloatError) -> Self { Self::parse_float_error(value) } } impl From<::std::io::Error> for Error { - fn from(value: ::std::io::Error) -> Self { Error::io(value) } + fn from(value: ::std::io::Error) -> Self { Self::io(value) } } impl From<::chrono::ParseError> for Error { - fn from(value: ::chrono::ParseError) -> Self { Error::chrono(value) } + fn from(value: ::chrono::ParseError) -> Self { Self::chrono(value) } } impl From<::strum::ParseError> for Error { fn from(value: ::strum::ParseError) -> Self { - Error::custom(value) // TODO! + Self::custom(value) // TODO! } } impl From<String> for Error { - fn from(value: String) -> Self { Error::custom(value) } + fn from(value: String) -> Self { Self::custom(value) } } impl From<::core::convert::Infallible> for Error { fn from(_: ::core::convert::Infallible) -> Self { - Error::custom("An Infallible error has been returned! (this should never happen!)") + Self::custom("An Infallible error has been returned! (this should never happen!)") } } diff --git a/src/line.rs b/src/line.rs index 771e243..9d66b43 100644 --- a/src/line.rs +++ b/src/line.rs @@ -16,7 +16,7 @@ impl FromStr for Lines { type Err = Error; fn from_str(input: &str) -> Result<Self, Self::Err> { - let mut result = Lines::new(); + let mut result = Self::new(); let mut stream_inf = false; let mut stream_inf_line = None; @@ -116,29 +116,29 @@ pub enum Tag { impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { - Tag::ExtM3u(value) => value.fmt(f), - Tag::ExtXVersion(value) => value.fmt(f), - Tag::ExtInf(value) => value.fmt(f), - Tag::ExtXByteRange(value) => value.fmt(f), - Tag::ExtXDiscontinuity(value) => value.fmt(f), - Tag::ExtXKey(value) => value.fmt(f), - Tag::ExtXMap(value) => value.fmt(f), - Tag::ExtXProgramDateTime(value) => value.fmt(f), - Tag::ExtXDateRange(value) => value.fmt(f), - Tag::ExtXTargetDuration(value) => value.fmt(f), - Tag::ExtXMediaSequence(value) => value.fmt(f), - Tag::ExtXDiscontinuitySequence(value) => value.fmt(f), - Tag::ExtXEndList(value) => value.fmt(f), - Tag::ExtXPlaylistType(value) => value.fmt(f), - Tag::ExtXIFramesOnly(value) => value.fmt(f), - Tag::ExtXMedia(value) => value.fmt(f), - Tag::ExtXStreamInf(value) => value.fmt(f), - Tag::ExtXIFrameStreamInf(value) => value.fmt(f), - Tag::ExtXSessionData(value) => value.fmt(f), - Tag::ExtXSessionKey(value) => value.fmt(f), - Tag::ExtXIndependentSegments(value) => value.fmt(f), - Tag::ExtXStart(value) => value.fmt(f), - Tag::Unknown(value) => value.fmt(f), + Self::ExtM3u(value) => value.fmt(f), + Self::ExtXVersion(value) => value.fmt(f), + Self::ExtInf(value) => value.fmt(f), + Self::ExtXByteRange(value) => value.fmt(f), + Self::ExtXDiscontinuity(value) => value.fmt(f), + Self::ExtXKey(value) => value.fmt(f), + Self::ExtXMap(value) => value.fmt(f), + Self::ExtXProgramDateTime(value) => value.fmt(f), + Self::ExtXDateRange(value) => value.fmt(f), + Self::ExtXTargetDuration(value) => value.fmt(f), + Self::ExtXMediaSequence(value) => value.fmt(f), + Self::ExtXDiscontinuitySequence(value) => value.fmt(f), + Self::ExtXEndList(value) => value.fmt(f), + Self::ExtXPlaylistType(value) => value.fmt(f), + Self::ExtXIFramesOnly(value) => value.fmt(f), + Self::ExtXMedia(value) => value.fmt(f), + Self::ExtXStreamInf(value) => value.fmt(f), + Self::ExtXIFrameStreamInf(value) => value.fmt(f), + Self::ExtXSessionData(value) => value.fmt(f), + Self::ExtXSessionKey(value) => value.fmt(f), + Self::ExtXIndependentSegments(value) => value.fmt(f), + Self::ExtXStart(value) => value.fmt(f), + Self::Unknown(value) => value.fmt(f), } } } @@ -148,51 +148,51 @@ impl FromStr for Tag { fn from_str(input: &str) -> Result<Self, Self::Err> { if input.starts_with(tags::ExtM3u::PREFIX) { - input.parse().map(Tag::ExtM3u) + input.parse().map(Self::ExtM3u) } else if input.starts_with(tags::ExtXVersion::PREFIX) { - input.parse().map(Tag::ExtXVersion) + input.parse().map(Self::ExtXVersion) } else if input.starts_with(tags::ExtInf::PREFIX) { - input.parse().map(Tag::ExtInf) + input.parse().map(Self::ExtInf) } else if input.starts_with(tags::ExtXByteRange::PREFIX) { - input.parse().map(Tag::ExtXByteRange) + input.parse().map(Self::ExtXByteRange) } else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) { - input.parse().map(Tag::ExtXDiscontinuity) + input.parse().map(Self::ExtXDiscontinuity) } else if input.starts_with(tags::ExtXKey::PREFIX) { - input.parse().map(Tag::ExtXKey) + input.parse().map(Self::ExtXKey) } else if input.starts_with(tags::ExtXMap::PREFIX) { - input.parse().map(Tag::ExtXMap) + input.parse().map(Self::ExtXMap) } else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) { - input.parse().map(Tag::ExtXProgramDateTime) + input.parse().map(Self::ExtXProgramDateTime) } else if input.starts_with(tags::ExtXTargetDuration::PREFIX) { - input.parse().map(Tag::ExtXTargetDuration) + input.parse().map(Self::ExtXTargetDuration) } else if input.starts_with(tags::ExtXDateRange::PREFIX) { - input.parse().map(Tag::ExtXDateRange) + input.parse().map(Self::ExtXDateRange) } else if input.starts_with(tags::ExtXMediaSequence::PREFIX) { - input.parse().map(Tag::ExtXMediaSequence) + input.parse().map(Self::ExtXMediaSequence) } else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - input.parse().map(Tag::ExtXDiscontinuitySequence) + input.parse().map(Self::ExtXDiscontinuitySequence) } else if input.starts_with(tags::ExtXEndList::PREFIX) { - input.parse().map(Tag::ExtXEndList) + input.parse().map(Self::ExtXEndList) } else if input.starts_with(tags::ExtXPlaylistType::PREFIX) { - input.parse().map(Tag::ExtXPlaylistType) + input.parse().map(Self::ExtXPlaylistType) } else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) { - input.parse().map(Tag::ExtXIFramesOnly) + input.parse().map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { - input.parse().map(Tag::ExtXMedia).map_err(Error::custom) + input.parse().map(Self::ExtXMedia).map_err(Error::custom) } else if input.starts_with(tags::ExtXStreamInf::PREFIX) { - input.parse().map(Tag::ExtXStreamInf) + input.parse().map(Self::ExtXStreamInf) } else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - input.parse().map(Tag::ExtXIFrameStreamInf) + input.parse().map(Self::ExtXIFrameStreamInf) } else if input.starts_with(tags::ExtXSessionData::PREFIX) { - input.parse().map(Tag::ExtXSessionData) + input.parse().map(Self::ExtXSessionData) } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { - input.parse().map(Tag::ExtXSessionKey) + input.parse().map(Self::ExtXSessionKey) } else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) { - input.parse().map(Tag::ExtXIndependentSegments) + input.parse().map(Self::ExtXIndependentSegments) } else if input.starts_with(tags::ExtXStart::PREFIX) { - input.parse().map(Tag::ExtXStart) + input.parse().map(Self::ExtXStart) } else { - Ok(Tag::Unknown(input.to_string())) + Ok(Self::Unknown(input.to_string())) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index e4ea9e4..8c4e295 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -90,7 +90,7 @@ impl MasterPlaylist { where T: Into<ExtXIndependentSegments>, { - self.independent_segments_tag = value.map(|v| v.into()); + self.independent_segments_tag = value.map(Into::into); self } @@ -102,7 +102,7 @@ impl MasterPlaylist { where T: Into<ExtXStart>, { - self.start_tag = value.map(|v| v.into()); + self.start_tag = value.map(Into::into); self } @@ -120,7 +120,7 @@ impl MasterPlaylist { where T: Into<ExtXMedia>, { - self.media_tags = value.into_iter().map(|v| v.into()).collect(); + self.media_tags = value.into_iter().map(Into::into).collect(); self } @@ -138,7 +138,7 @@ impl MasterPlaylist { where T: Into<ExtXStreamInf>, { - self.stream_inf_tags = value.into_iter().map(|v| v.into()).collect(); + self.stream_inf_tags = value.into_iter().map(Into::into).collect(); self } @@ -158,7 +158,7 @@ impl MasterPlaylist { where T: Into<ExtXIFrameStreamInf>, { - self.i_frame_stream_inf_tags = value.into_iter().map(|v| v.into()).collect(); + self.i_frame_stream_inf_tags = value.into_iter().map(Into::into).collect(); self } @@ -176,7 +176,7 @@ impl MasterPlaylist { where T: Into<ExtXSessionData>, { - self.session_data_tags = value.into_iter().map(|v| v.into()).collect(); + self.session_data_tags = value.into_iter().map(Into::into).collect(); self } @@ -194,7 +194,7 @@ impl MasterPlaylist { where T: Into<ExtXSessionKey>, { - self.session_key_tags = value.into_iter().map(|v| v.into()).collect(); + self.session_key_tags = value.into_iter().map(Into::into).collect(); self } } @@ -367,7 +367,7 @@ impl FromStr for MasterPlaylist { type Err = Error; fn from_str(input: &str) -> Result<Self, Self::Err> { - let mut builder = MasterPlaylist::builder(); + let mut builder = Self::builder(); let mut media_tags = vec![]; let mut stream_inf_tags = vec![]; diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 1d47d93..019818e 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -42,15 +42,6 @@ impl ExtM3u { } /// This tag requires [`ProtocolVersion::V1`]. -/// -/// # Example -/// ``` -/// # use hls_m3u8::tags::ExtM3u; -/// use hls_m3u8::types::ProtocolVersion; -/// use hls_m3u8::RequiredVersion; -/// -/// assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1); -/// ``` impl RequiredVersion for ExtM3u { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -81,4 +72,9 @@ mod test { fn test_parser() { assert_eq!("#EXTM3U".parse::<ExtM3u>().unwrap(), ExtM3u); } + + #[test] + fn test_required_version() { + assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1); + } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 55a42d9..5494015 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -101,7 +101,7 @@ impl ExtXIFrameStreamInf { /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); /// ``` pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self { - ExtXIFrameStreamInf { + Self { uri: uri.to_string(), stream_inf: StreamInf::new(bandwidth), } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 3f990e9..16858e2 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -9,6 +9,7 @@ use crate::utils::{parse_yes_or_no, quote, tag, unquote}; 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. /// @@ -308,7 +309,7 @@ impl ExtXMedia { /// /// [`Media Playlist`]: crate::MediaPlaylist pub fn set_uri<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self { - self.uri = value.map(|v| v.into()); + self.uri = value.map(Into::into); self } @@ -346,7 +347,7 @@ impl ExtXMedia { /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 pub fn set_language<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self { - self.language = value.map(|v| v.into()); + self.language = value.map(Into::into); self } @@ -386,7 +387,7 @@ impl ExtXMedia { /// /// [`language`]: #method.language pub fn set_assoc_language<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self { - self.assoc_language = value.map(|v| v.into()); + self.assoc_language = value.map(Into::into); self } @@ -588,7 +589,7 @@ impl ExtXMedia { /// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI /// [`subtitles`]: crate::types::MediaType::Subtitles pub fn set_characteristics<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self { - self.characteristics = value.map(|v| v.into()); + self.characteristics = value.map(Into::into); self } @@ -623,7 +624,7 @@ impl ExtXMedia { /// assert_eq!(media.channels(), &Some(Channels::new(6))); /// ``` pub fn set_channels<T: Into<Channels>>(&mut self, value: Option<T>) -> &mut Self { - self.channels = value.map(|v| v.into()); + self.channels = value.map(Into::into); self } } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 674947b..9bebffd 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -8,14 +8,17 @@ use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; -/// The data of an [ExtXSessionData] tag. +/// The data of an [`ExtXSessionData`] tag. #[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] pub enum SessionData { /// A String, that contains the data identified by - /// [`data_id`](ExtXSessionData::data_id). - /// If a [`language`](ExtXSessionData::language) is specified, the value + /// [`data_id`]. + /// If a [`language`] is specified, the value /// should contain a human-readable string written in the specified /// language. + /// + /// [`data_id`]: ExtXSessionData::data_id + /// [`language`]: ExtXSessionData::language Value(String), /// An [`uri`], which points to a [`json`]. /// @@ -35,17 +38,20 @@ pub enum SessionData { #[builder(setter(into))] pub struct ExtXSessionData { /// The identifier of the data. - /// For more information look [`here`](ExtXSessionData::set_data_id). + /// For more information look [`here`]. /// /// # Note /// This field is required. + /// + /// [`here`]: ExtXSessionData::set_data_id data_id: String, - /// The data associated with the - /// [`data_id`](ExtXSessionDataBuilder::data_id). + /// The data associated with the [`data_id`]. /// For more information look [`here`](SessionData). /// /// # Note /// This field is required. + /// + /// [`data_id`]: ExtXSessionDataBuilder::data_id data: SessionData, /// The language of the [`data`](ExtXSessionDataBuilder::data). #[builder(setter(into, strip_option), default)] diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index a273d61..cdd65ab 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -191,7 +191,7 @@ impl ExtXStreamInf { /// assert_eq!(stream.frame_rate(), Some(59.9)); /// ``` pub fn set_frame_rate(&mut self, value: Option<f64>) -> &mut Self { - self.frame_rate = value.map(|v| v.into()); + self.frame_rate = value.map(Into::into); self } @@ -233,7 +233,7 @@ impl ExtXStreamInf { /// assert_eq!(stream.audio(), &Some("audio".to_string())); /// ``` pub fn set_audio<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self { - self.audio = value.map(|v| v.into()); + self.audio = value.map(Into::into); self } @@ -262,7 +262,7 @@ impl ExtXStreamInf { /// assert_eq!(stream.subtitles(), &Some("subs".to_string())); /// ``` pub fn set_subtitles<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self { - self.subtitles = value.map(|v| v.into()); + self.subtitles = value.map(Into::into); self } diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 4e62b30..874445b 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -38,7 +38,7 @@ impl FromStr for ExtXEndList { fn from_str(input: &str) -> Result<Self, Self::Err> { tag(input, Self::PREFIX)?; - Ok(ExtXEndList) + Ok(Self) } } diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 59aae4b..ac35ed2 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -40,7 +40,7 @@ impl FromStr for ExtXIFramesOnly { fn from_str(input: &str) -> Result<Self, Self::Err> { tag(input, Self::PREFIX)?; - Ok(ExtXIFramesOnly) + Ok(Self) } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 3564955..e41a0a6 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -74,7 +74,7 @@ impl FromStr for ExtXMediaSequence { fn from_str(input: &str) -> Result<Self, Self::Err> { let seq_num = tag(input, Self::PREFIX)?.parse()?; - Ok(ExtXMediaSequence::new(seq_num)) + Ok(Self::new(seq_num)) } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 0705d9d..3ad2ad1 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -97,7 +97,7 @@ impl FromStr for ExtXByteRange { } }; - Ok(ExtXByteRange::new(length, start)) + Ok(Self::new(length, start)) } } diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 250ed65..90d4e65 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -37,7 +37,7 @@ impl FromStr for ExtXDiscontinuity { fn from_str(input: &str) -> Result<Self, Self::Err> { tag(input, Self::PREFIX)?; - Ok(ExtXDiscontinuity) + Ok(Self) } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 92663f3..505ed63 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -130,10 +130,7 @@ impl RequiredVersion for ExtInf { impl fmt::Display for ExtInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - - let duration = (self.duration.as_secs() as f64) - + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); - write!(f, "{},", duration)?; + write!(f, "{},", self.duration.as_secs_f64())?; if let Some(value) = &self.title { write!(f, "{}", value)?; diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index dc6552f..6f3fdfe 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -11,11 +11,6 @@ use crate::{Encrypted, Error, RequiredVersion}; /// The [`ExtXMap`] tag specifies how to obtain the Media Initialization /// Section, required to parse the applicable [Media Segment]s. /// -/// Its format is: -/// ```text -/// #EXT-X-MAP:<attribute-list> -/// ``` -/// /// [Media Segment]: crate::MediaSegment /// [4.4.2.5. EXT-X-MAP]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.5 diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index a617a5f..07e5f78 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -28,7 +28,7 @@ impl FromStr for ExtXIndependentSegments { fn from_str(input: &str) -> Result<Self, Self::Err> { tag(input, Self::PREFIX)?; - Ok(ExtXIndependentSegments) + Ok(Self) } } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index f12d5c4..8a072db 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -63,7 +63,7 @@ impl From<f64> for DecimalFloatingPoint { } impl From<f32> for DecimalFloatingPoint { - fn from(value: f32) -> Self { (value as f64).into() } + fn from(value: f32) -> Self { f64::from(value).into() } } #[cfg(test)] @@ -86,7 +86,7 @@ mod tests { } } - test_from![1u8, 1u16, 1u32, 1.0f32, -1.0f32, 1.0f64, -1.0f64]; + test_from![1_u8, 1_u16, 1_u32, 1.0_f32, -1.0_f32, 1.0_f64, -1.0_f64]; #[test] pub fn test_display() { diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index 234cc95..b5d9ef4 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -42,7 +42,7 @@ impl DecimalResolution { /// [`DecimalResolution`] can be constructed from a tuple; `(width, height)`. impl From<(usize, usize)> for DecimalResolution { - fn from(value: (usize, usize)) -> Self { DecimalResolution::new(value.0, value.1) } + fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) } } impl FromStr for DecimalResolution { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 0d17686..7aa745d 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -221,7 +221,7 @@ impl DecryptionKey { /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); /// ``` pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) -> &mut Self { - self.key_format = value.map(|v| v.into()); + self.key_format = value.map(Into::into); self } @@ -266,7 +266,7 @@ impl DecryptionKey { &mut self, value: Option<T>, ) -> &mut Self { - self.key_format_versions = value.map(|v| v.into()); + self.key_format_versions = value.map(Into::into); self } } diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 63e9b80..277ab92 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -54,21 +54,21 @@ mod tests { } test_from![ - SignedDecimalFloatingPoint::from(1u8) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1i8) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1u16) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1i16) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1u32) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1i32) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1.0f32) => SignedDecimalFloatingPoint::new(1.0), - SignedDecimalFloatingPoint::from(1.0f64) => SignedDecimalFloatingPoint::new(1.0) + SignedDecimalFloatingPoint::from(1_u8) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_i8) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_u16) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_i16) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_u32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1_i32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1.0_f32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1.0_f64) => SignedDecimalFloatingPoint::new(1.0) ]; #[test] fn test_display() { assert_eq!( SignedDecimalFloatingPoint::new(1.0).to_string(), - 1.0f64.to_string() + 1.0_f64.to_string() ); } diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index 9f48634..076e853 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -229,7 +229,7 @@ impl StreamInf { /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); /// ``` pub fn set_hdcp_level<T: Into<HdcpLevel>>(&mut self, value: Option<T>) -> &mut Self { - self.hdcp_level = value.map(|v| v.into()); + self.hdcp_level = value.map(Into::into); self } } From b18e6ea4fb4a05f72e07419e8bab5164d5e6278a Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 5 Oct 2019 16:24:48 +0200 Subject: [PATCH 09/15] use required_version! macro --- src/error.rs | 11 ---- src/lib.rs | 3 +- src/master_playlist.rs | 15 ----- src/media_playlist.rs | 136 +++++++++++--------------------------- src/media_segment.rs | 146 ++++++++++++++++++++++++++++------------- src/utils.rs | 11 ++++ 6 files changed, 155 insertions(+), 167 deletions(-) diff --git a/src/error.rs b/src/error.rs index c661819..eaf3645 100644 --- a/src/error.rs +++ b/src/error.rs @@ -165,17 +165,6 @@ impl Error { pub(crate) fn io<T: ToString>(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) } - pub(crate) fn required_version<T, U>(required_version: T, specified_version: U) -> Self - where - T: ToString, - U: ToString, - { - Self::from(ErrorKind::VersionError( - required_version.to_string(), - specified_version.to_string(), - )) - } - pub(crate) fn builder_error<T: ToString>(value: T) -> Self { Self::from(ErrorKind::BuilderError(value.to_string())) } diff --git a/src/lib.rs b/src/lib.rs index 2996b1a..2fc3c24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,8 @@ pub use media_segment::{MediaSegment, MediaSegmentBuilder}; pub mod tags; pub mod types; +#[macro_use] +mod utils; mod attribute; mod error; mod line; @@ -51,7 +53,6 @@ mod master_playlist; mod media_playlist; mod media_segment; mod traits; -mod utils; pub use error::Result; pub use traits::*; diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 8c4e295..ff2a944 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -17,8 +17,6 @@ use crate::{Error, RequiredVersion}; #[builder(setter(into, strip_option))] /// Master playlist. pub struct MasterPlaylist { - #[builder(default, setter(skip))] - version_tag: ExtXVersion, #[builder(default)] /// Sets the [`ExtXIndependentSegments`] tag. /// @@ -199,17 +197,6 @@ impl MasterPlaylist { } } -macro_rules! required_version { - ( $( $tag:expr ),* ) => { - ::core::iter::empty() - $( - .chain(::core::iter::once($tag.required_version())) - )* - .max() - .unwrap_or_default() - } -} - impl RequiredVersion for MasterPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ @@ -392,8 +379,6 @@ impl FromStr for MasterPlaylist { // This tag can be ignored, because the // MasterPlaylist will automatically set the // ExtXVersion tag to correct version! - - // builder.version(t.version()); } Tag::ExtInf(_) | Tag::ExtXByteRange(_) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index dba09db..dc360df 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::iter; use std::str::FromStr; use std::time::Duration; @@ -19,15 +18,6 @@ use crate::{Encrypted, Error, RequiredVersion}; #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] pub struct MediaPlaylist { - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility - /// version greater than `version`, - /// `build()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - #[builder(setter(name = "version"))] - version_tag: ExtXVersion, /// Sets the [`ExtXTargetDuration`] tag. target_duration_tag: ExtXTargetDuration, #[builder(default)] @@ -68,13 +58,6 @@ pub struct MediaPlaylist { impl MediaPlaylistBuilder { fn validate(&self) -> Result<(), String> { - let required_version = self.required_version(); - let specified_version = self.version_tag.map_or(required_version, |p| p.version()); - - if required_version > specified_version { - return Err(Error::required_version(required_version, specified_version).to_string()); - } - if let Some(target_duration) = &self.target_duration_tag { self.validate_media_segments(target_duration.duration()) .map_err(|e| e.to_string())?; @@ -89,10 +72,12 @@ impl MediaPlaylistBuilder { for s in segments { // CHECK: `#EXT-X-TARGETDURATION` let segment_duration = s.inf_tag().duration(); - let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { - Duration::from_secs(segment_duration.as_secs()) - } else { - Duration::from_secs(segment_duration.as_secs() + 1) + let rounded_segment_duration = { + if segment_duration.subsec_nanos() < 500_000_000 { + Duration::from_secs(segment_duration.as_secs()) + } else { + Duration::from_secs(segment_duration.as_secs() + 1) + } }; let max_segment_duration = { @@ -149,62 +134,17 @@ impl MediaPlaylistBuilder { impl RequiredVersion for MediaPlaylistBuilder { fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.target_duration_tag - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.media_sequence_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.discontinuity_sequence_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.playlist_type_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.i_frames_only_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.independent_segments_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.start_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain( - self.end_list_tag - .flatten() - .iter() - .map(|p| p.required_version()), - ) - .chain(self.segments.iter().map(|t| { - t.iter() - .map(|p| p.required_version()) - .max() - .unwrap_or(ProtocolVersion::V1) - })) - .max() - .unwrap_or_else(ProtocolVersion::latest) + required_version![ + self.target_duration_tag, + self.media_sequence_tag, + self.discontinuity_sequence_tag, + self.playlist_type_tag, + self.i_frames_only_tag, + self.independent_segments_tag, + self.start_tag, + self.end_list_tag, + self.segments + ] } } @@ -212,9 +152,6 @@ impl MediaPlaylist { /// Returns a builder for [`MediaPlaylist`]. pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } - /// Returns the [`ExtXVersion`] tag contained in the playlist. - pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } - /// Returns the [`ExtXTargetDuration`] tag contained in the playlist. pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } @@ -248,11 +185,27 @@ impl MediaPlaylist { pub const fn segments(&self) -> &Vec<MediaSegment> { &self.segments } } +impl RequiredVersion for MediaPlaylist { + fn required_version(&self) -> ProtocolVersion { + required_version![ + self.target_duration_tag, + self.media_sequence_tag, + self.discontinuity_sequence_tag, + self.playlist_type_tag, + self.i_frames_only_tag, + self.independent_segments_tag, + self.start_tag, + self.end_list_tag, + self.segments + ] + } +} + impl fmt::Display for MediaPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; - if self.version_tag.version() != ProtocolVersion::V1 { - writeln!(f, "{}", self.version_tag)?; + if self.required_version() != ProtocolVersion::V1 { + writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } writeln!(f, "{}", self.target_duration_tag)?; if let Some(value) = &self.media_sequence_tag { @@ -292,9 +245,8 @@ fn parse_media_playlist( let mut has_partial_segment = false; let mut has_discontinuity_tag = false; - let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1 - let mut available_key_tags = vec![]; + let mut available_key_tags: Vec<crate::tags::ExtXKey> = vec![]; for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() { match line { @@ -307,10 +259,6 @@ fn parse_media_playlist( } match tag { Tag::ExtM3u(_) => return Err(Error::invalid_input()), - Tag::ExtXVersion(t) => { - builder.version(t.version()); - has_version = true; - } Tag::ExtInf(t) => { has_partial_segment = true; segment.inf_tag(t); @@ -326,9 +274,7 @@ fn parse_media_playlist( } Tag::ExtXKey(t) => { has_partial_segment = true; - if !available_key_tags.is_empty() { - available_key_tags.push(t); - } else { + if available_key_tags.is_empty() { // An ExtXKey applies to every MediaSegment and to every Media // Initialization Section declared by an EXT-X-MAP tag, that appears // between it and the next EXT-X-KEY tag in the Playlist file with the @@ -343,6 +289,8 @@ fn parse_media_playlist( } }) .collect(); + } else { + available_key_tags.push(t); } } Tag::ExtXMap(mut t) => { @@ -396,7 +344,7 @@ fn parse_media_playlist( Tag::ExtXStart(t) => { builder.start_tag(t); } - Tag::Unknown(_) => { + Tag::Unknown(_) | Tag::ExtXVersion(_) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. } @@ -416,10 +364,6 @@ fn parse_media_playlist( return Err(Error::invalid_input()); } - if !has_version { - builder.version(ProtocolVersion::V1); - } - builder.segments(segments); builder.build().map_err(Error::builder_error) } diff --git a/src/media_segment.rs b/src/media_segment.rs index 7e3722d..2efa2b8 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::iter; use derive_builder::Builder; @@ -37,6 +36,99 @@ pub struct MediaSegment { uri: String, } +impl MediaSegment { + /// Returns a Builder for a [`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<T>(&mut self, value: T) -> &mut Self + where + T: Into<String>, + { + 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<T>(&mut self, value: T) -> &mut Self + where + T: Into<ExtInf>, + { + self.inf_tag = value.into(); + self + } + + /// Returns the [`ExtXByteRange`] tag associated with the media segment. + pub const fn byte_range_tag(&self) -> Option<ExtXByteRange> { self.byte_range_tag } + + /// Sets the [`ExtXByteRange`] tag associated with the media segment. + pub fn set_byte_range_tag<T>(&mut self, value: Option<T>) -> &mut Self + where + T: Into<ExtXByteRange>, + { + 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<ExtXDateRange> { &self.date_range_tag } + + /// Sets the [`ExtXDateRange`] tag associated with the media segment. + pub fn set_date_range_tag<T>(&mut self, value: Option<T>) -> &mut Self + where + T: Into<ExtXDateRange>, + { + 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<ExtXDiscontinuity> { self.discontinuity_tag } + + /// Sets the [`ExtXDiscontinuity`] tag associated with the media segment. + pub fn set_discontinuity_tag<T>(&mut self, value: Option<T>) -> &mut Self + where + T: Into<ExtXDiscontinuity>, + { + 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<ExtXProgramDateTime> { + self.program_date_time_tag + } + + /// Sets the [`ExtXProgramDateTime`] tag associated with the media + /// segment. + pub fn set_program_date_time_tag<T>(&mut self, value: Option<T>) -> &mut Self + where + T: Into<ExtXProgramDateTime>, + { + 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<ExtXMap> { &self.map_tag } + + /// Sets the [`ExtXMap`] tag associated with the media segment. + pub fn set_map_tag<T>(&mut self, value: Option<T>) -> &mut Self + where + T: Into<ExtXMap>, + { + self.map_tag = value.map(Into::into); + self + } +} + impl MediaSegmentBuilder { /// Pushes an [`ExtXKey`] tag. pub fn push_key_tag<VALUE: Into<ExtXKey>>(&mut self, value: VALUE) -> &mut Self { @@ -75,51 +167,17 @@ impl fmt::Display for MediaSegment { } } -impl MediaSegment { - /// Creates a [`MediaSegmentBuilder`]. - pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } - - /// Returns the `URI` of the media segment. - pub const fn uri(&self) -> &String { &self.uri } - - /// Returns the [`ExtInf`] tag associated with the media segment. - pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } - - /// Returns the [`ExtXByteRange`] tag associated with the media segment. - pub const fn byte_range_tag(&self) -> Option<ExtXByteRange> { self.byte_range_tag } - - /// Returns the [`ExtXDateRange`] tag associated with the media segment. - pub const fn date_range_tag(&self) -> &Option<ExtXDateRange> { &self.date_range_tag } - - /// Returns the [`ExtXDiscontinuity`] tag associated with the media segment. - pub const fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> { self.discontinuity_tag } - - /// Returns the [`ExtXProgramDateTime`] tag associated with the media - /// segment. - pub const fn program_date_time_tag(&self) -> Option<ExtXProgramDateTime> { - self.program_date_time_tag - } - - /// Returns the [`ExtXMap`] tag associated with the media segment. - pub const fn map_tag(&self) -> &Option<ExtXMap> { &self.map_tag } -} - impl RequiredVersion for MediaSegment { fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain(self.keys.iter().map(|t| t.required_version())) - .chain(self.map_tag.iter().map(|t| t.required_version())) - .chain(self.byte_range_tag.iter().map(|t| t.required_version())) - .chain(self.date_range_tag.iter().map(|t| t.required_version())) - .chain(self.discontinuity_tag.iter().map(|t| t.required_version())) - .chain( - self.program_date_time_tag - .iter() - .map(|t| t.required_version()), - ) - .chain(iter::once(self.inf_tag.required_version())) - .max() - .unwrap_or_else(ProtocolVersion::latest) + 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 + ] } } diff --git a/src/utils.rs b/src/utils.rs index a48a2eb..9eb7fae 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,16 @@ use crate::Error; +macro_rules! required_version { + ( $( $tag:expr ),* ) => { + ::core::iter::empty() + $( + .chain(::core::iter::once($tag.required_version())) + )* + .max() + .unwrap_or_default() + } +} + macro_rules! impl_from { ( $($( $type:tt ),* => $target:path ),* ) => { use ::core::convert::From; From 3dad1277ca52214aaf84b88a0ddccf1818407587 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 6 Oct 2019 16:37:14 +0200 Subject: [PATCH 10/15] implemented ExtXDateRange --- Cargo.toml | 1 + src/error.rs | 6 + src/tags/media_segment/date_range.rs | 866 ++++++++++++++++++++++++--- src/types/mod.rs | 2 + src/types/value.rs | 106 ++++ 5 files changed, 893 insertions(+), 88 deletions(-) create mode 100644 src/types/value.rs diff --git a/Cargo.toml b/Cargo.toml index 2d8fae9..f69f6d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ derive_builder = "0.7.2" chrono = "0.4.9" strum = { version = "0.16.0", features = ["derive"] } derive_more = "0.15.0" +hex = "0.4.0" [dev-dependencies] clap = "2" diff --git a/src/error.rs b/src/error.rs index eaf3645..c64e031 100644 --- a/src/error.rs +++ b/src/error.rs @@ -209,3 +209,9 @@ impl From<::core::convert::Infallible> for Error { Self::custom("An Infallible error has been returned! (this should never happen!)") } } + +impl From<::hex::FromHexError> for Error { + fn from(value: ::hex::FromHexError) -> Self { + Self::custom(value) // TODO! + } +} diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 12f15db..e984a48 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -3,63 +3,119 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, SecondsFormat}; +use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, Value}; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; /// # [4.3.2.7. EXT-X-DATERANGE] /// /// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 -/// -/// TODO: Implement properly -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, Debug, Clone, PartialEq)] +#[builder(setter(into))] pub struct ExtXDateRange { - /// A string that uniquely identifies a [`ExtXDateRange`] in the Playlist. + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Note /// This attribute is required. id: String, + #[builder(setter(strip_option), default)] /// A client-defined string that specifies some set of attributes and their - /// associated value semantics. All Date Ranges with the same CLASS - /// attribute value MUST adhere to these semantics. This attribute is - /// OPTIONAL. + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Note + /// This attribute is optional. class: Option<String>, - /// The date at which the Date Range begins. This attribute is REQUIRED. + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Note + /// This attribute is required. start_date: DateTime<FixedOffset>, - /// The date at which the Date Range ends. It MUST be equal to or later than - /// the value of the START-DATE attribute. This attribute is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Note + /// This attribute is optional. + /// + /// [`start-date`]: #method.start_date end_date: Option<DateTime<FixedOffset>>, - /// The duration of the Date Range. It MUST NOT be negative. A single - /// instant in time (e.g., crossing a finish line) SHOULD be - /// represented with a duration of 0. This attribute is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Note + /// This attribute is optional. duration: Option<Duration>, - /// The expected duration of the Date Range. It MUST NOT be negative. This - /// attribute SHOULD be used to indicate the expected duration of a - /// Date Range whose actual duration is not yet known. - /// It is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// + /// # Note + /// This attribute is optional. planned_duration: Option<Duration>, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_cmd: Option<String>, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_out: Option<String>, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_in: Option<String>, + #[builder(default)] /// This attribute indicates that the end of the range containing it is - /// equal to the START-DATE of its Following Range. The Following Range - /// is the Date Range of the same CLASS, that has the earliest - /// START-DATE after the START-DATE of the range in question. This - /// attribute is OPTIONAL. + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Note + /// This attribute is optional. end_on_next: bool, + #[builder(default)] /// The "X-" prefix defines a namespace reserved for client-defined - /// attributes. The client-attribute MUST be a legal AttributeName. - /// Clients SHOULD use a reverse-DNS syntax when defining their own - /// attribute names to avoid collisions. The attribute value MUST be - /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- - /// point. An example of a client-defined attribute is X-COM-EXAMPLE- - /// AD-ID="XYZ123". These attributes are OPTIONAL. - client_attributes: BTreeMap<String, String>, + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Note + /// This attribute is optional. + client_attributes: BTreeMap<String, Value>, +} + +impl ExtXDateRangeBuilder { + /// Inserts a key value pair. + pub fn insert_client_attribute<K: ToString, V: Into<Value>>( + &mut self, + key: K, + value: V, + ) -> &mut Self { + if self.client_attributes.is_none() { + self.client_attributes = Some(BTreeMap::new()); + } + + if let Some(client_attributes) = &mut self.client_attributes { + client_attributes.insert(key.to_string(), value.into()); + } else { + unreachable!(); + } + self + } } impl ExtXDateRange { @@ -97,68 +153,542 @@ impl ExtXDateRange { client_attributes: BTreeMap::new(), } } + + /// Returns a builder for [`ExtXDateRange`]. + pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() } + + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// assert_eq!(date_range.id(), &"id".to_string()); + /// ``` + pub const fn id(&self) -> &String { &self.id } + + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// date_range.set_id("new_id"); + /// assert_eq!(date_range.id(), &"new_id".to_string()); + /// ``` + pub fn set_id<T: ToString>(&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<String> { &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<T: ToString>(&mut self, value: Option<T>) -> &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<FixedOffset> { 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<T>(&mut self, value: T) -> &mut Self + where + T: Into<DateTime<FixedOffset>>, + { + 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<DateTime<FixedOffset>> { 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<T>(&mut self, value: Option<T>) -> &mut Self + where + T: Into<DateTime<FixedOffset>>, + { + 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<Duration> { 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<Duration>) -> &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<Duration> { 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<Duration>) -> &mut Self { + self.planned_duration = value; + self + } + + pub const fn scte35_cmd(&self) -> &Option<String> { &self.scte35_cmd } + + pub const fn scte35_in(&self) -> &Option<String> { &self.scte35_in } + + pub const fn scte35_out(&self) -> &Option<String> { &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<String, Value> { &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<String, Value> { + &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<String, Value>) -> &mut Self { + self.client_attributes = value; + self + } } /// This tag requires [`ProtocolVersion::V1`]. -/// -/// # Example -/// ``` -/// # use hls_m3u8::tags::ExtXDateRange; -/// use chrono::offset::TimeZone; -/// use chrono::{DateTime, FixedOffset}; -/// use hls_m3u8::types::ProtocolVersion; -/// use hls_m3u8::RequiredVersion; -/// -/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds -/// -/// let date_range = ExtXDateRange::new( -/// "id", -/// FixedOffset::east(8 * HOURS_IN_SECS) -/// .ymd(2010, 2, 19) -/// .and_hms_milli(14, 54, 23, 31), -/// ); -/// assert_eq!(date_range.required_version(), ProtocolVersion::V1); -/// ``` impl RequiredVersion for ExtXDateRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl fmt::Display for ExtXDateRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "ID={}", quote(&self.id))?; - if let Some(value) = &self.class { - write!(f, ",CLASS={}", quote(value))?; - } - write!(f, ",START-DATE={}", quote(&self.start_date))?; - if let Some(value) = &self.end_date { - write!(f, ",END-DATE={}", quote(value))?; - } - if let Some(value) = &self.duration { - write!(f, ",DURATION={}", value.as_secs_f64())?; - } - if let Some(value) = &self.planned_duration { - write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; - } - if let Some(value) = &self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", quote(value))?; - } - if let Some(value) = &self.scte35_out { - write!(f, ",SCTE35-OUT={}", quote(value))?; - } - if let Some(value) = &self.scte35_in { - write!(f, ",SCTE35-IN={}", quote(value))?; - } - if self.end_on_next { - write!(f, ",END-ON-NEXT=YES",)?; - } - for (k, v) in &self.client_attributes { - write!(f, ",{}={}", k, v)?; - } - Ok(()) - } -} - impl FromStr for ExtXDateRange { type Err = Error; @@ -195,13 +725,13 @@ impl FromStr for ExtXDateRange { "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { if value != "YES" { - return Err(Error::invalid_input()); + return Err(Error::custom("The value of `END-ON-NEXT` has to be `YES`!")); } end_on_next = true; } _ => { if key.starts_with("X-") { - client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); + client_attributes.insert(key.to_ascii_uppercase(), value.parse()?); } else { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an @@ -235,6 +765,60 @@ impl FromStr for ExtXDateRange { } } +impl fmt::Display for ExtXDateRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "ID={}", quote(&self.id))?; + if let Some(value) = &self.class { + write!(f, ",CLASS={}", quote(value))?; + } + + write!( + f, + ",START-DATE={}", + quote(&self.start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + + if let Some(value) = &self.end_date { + write!( + f, + ",END-DATE={}", + quote(value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + } + + if let Some(value) = &self.duration { + write!(f, ",DURATION={}", value.as_secs_f64())?; + } + + if let Some(value) = &self.planned_duration { + write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; + } + + if let Some(value) = &self.scte35_cmd { + write!(f, ",SCTE35-CMD={}", value)?; + } + + if let Some(value) = &self.scte35_out { + write!(f, ",SCTE35-OUT={}", value)?; + } + + if let Some(value) = &self.scte35_in { + write!(f, ",SCTE35-IN={}", value)?; + } + + for (k, v) in &self.client_attributes { + write!(f, ",{}={}", k, v)?; + } + + if self.end_on_next { + write!(f, ",END-ON-NEXT=YES",)?; + } + + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; @@ -242,6 +826,112 @@ mod test { const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + #[test] + fn test_parser() { + assert_eq!( + "#EXT-X-DATERANGE:\ + ID=\"splice-6FFFFFF0\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + PLANNED-DURATION=59.993,\ + SCTE35-OUT=0xFC002F0000000000FF000014056F\ + FFFFF000E011622DCAFF000052636200000000000\ + A0008029896F50000008700000000" + .parse::<ExtXDateRange>() + .unwrap(), + ExtXDateRange::builder() + .id("splice-6FFFFFF0") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .planned_duration(Duration::from_secs_f64(59.993)) + .scte35_out( + "0xFC002F0000000000FF00001\ + 4056FFFFFF000E011622DCAFF0\ + 00052636200000000000A00080\ + 29896F50000008700000000" + ) + .build() + .unwrap() + ); + + assert_eq!( + "#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + CLASS=\"test_class\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-DATE=\"2014-03-05T11:16:00Z\",\ + DURATION=60.1,\ + PLANNED-DURATION=59.993,\ + X-CUSTOM=45.3,\ + SCTE35-CMD=0xFC002F0000000000FF2,\ + SCTE35-OUT=0xFC002F0000000000FF0,\ + SCTE35-IN=0xFC002F0000000000FF1,\ + END-ON-NEXT=YES,\ + UNKNOWN=PHANTOM" + .parse::<ExtXDateRange>() + .unwrap(), + ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0)) + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", 45.3) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build() + .unwrap() + ); + + assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO" + .parse::<ExtXDateRange>() + .is_err()); + + assert!("garbage".parse::<ExtXDateRange>().is_err()); + assert!("".parse::<ExtXDateRange>().is_err()); + + assert!("#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-ON-NEXT=YES" + .parse::<ExtXDateRange>() + .is_err()); + } + + #[test] + fn test_display() { + assert_eq!( + ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0)) + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", 45.3) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build() + .unwrap() + .to_string(), + "#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + CLASS=\"test_class\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-DATE=\"2014-03-05T11:16:00Z\",\ + DURATION=60.1,\ + PLANNED-DURATION=59.993,\ + SCTE35-CMD=0xFC002F0000000000FF2,\ + SCTE35-OUT=0xFC002F0000000000FF0,\ + SCTE35-IN=0xFC002F0000000000FF1,\ + X-CUSTOM=45.3,\ + END-ON-NEXT=YES" + ) + } + #[test] fn test_required_version() { assert_eq!( diff --git a/src/types/mod.rs b/src/types/mod.rs index 64aa441..a1df2bd 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,6 +15,7 @@ mod media_type; mod protocol_version; mod signed_decimal_floating_point; mod stream_inf; +mod value; pub use byte_range::*; pub use channels::*; @@ -32,3 +33,4 @@ pub use media_type::*; pub use protocol_version::*; pub(crate) use signed_decimal_floating_point::*; pub use stream_inf::*; +pub use value::*; diff --git a/src/types/value.rs b/src/types/value.rs new file mode 100644 index 0000000..90c1783 --- /dev/null +++ b/src/types/value.rs @@ -0,0 +1,106 @@ +use std::fmt; +use std::str::FromStr; + +use hex; + +use crate::utils::{quote, unquote}; +use crate::Error; + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// A [`Value`]. +pub enum Value { + /// A [`String`]. + String(String), + /// A sequence of bytes. + Hex(Vec<u8>), + /// A floating point number, that's neither NaN nor infinite! + Float(f64), +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Self::String(value) => write!(f, "{}", quote(value)), + Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)), + Self::Float(value) => write!(f, "{}", value), + } + } +} + +impl FromStr for Value { + type Err = Error; + + fn from_str(input: &str) -> Result<Self, Self::Err> { + if input.starts_with("0x") || input.starts_with("0X") { + Ok(Self::Hex(hex::decode( + input.trim_start_matches("0x").trim_start_matches("0X"), + )?)) + } else { + match input.parse() { + Ok(value) => Ok(Self::Float(value)), + Err(_) => Ok(Self::String(unquote(input))), + } + } + } +} + +impl From<f64> for Value { + fn from(value: f64) -> Self { Self::Float(value) } +} + +impl From<Vec<u8>> for Value { + fn from(value: Vec<u8>) -> Self { Self::Hex(value) } +} + +impl From<String> for Value { + fn from(value: String) -> Self { Self::String(unquote(value)) } +} + +impl From<&str> for Value { + fn from(value: &str) -> Self { Self::String(unquote(value)) } +} + +// impl<T: AsRef<[u8]>> From<T> for Value { +// fn from(value: T) -> Self { Self::Hex(value.as_ref().into()) } +// } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!(Value::Float(1.1).to_string(), "1.1".to_string()); + assert_eq!( + Value::String("&str".to_string()).to_string(), + "\"&str\"".to_string() + ); + assert_eq!( + Value::Hex(vec![1, 2, 3]).to_string(), + "0x010203".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!(Value::Float(1.1), "1.1".parse().unwrap()); + assert_eq!( + Value::String("&str".to_string()), + "\"&str\"".parse().unwrap() + ); + assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap()); + assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap()); + assert!("0x010203Z".parse::<Value>().is_err()); + } + + #[test] + fn test_from() { + assert_eq!(Value::from(1.0_f64), Value::Float(1.0)); + assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string())); + assert_eq!( + Value::from("&str".to_string()), + Value::String("&str".to_string()) + ); + assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3])); + } +} From b1c1ea8bdcfc9e504f79a1256e5a810fb339d86d Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 6 Oct 2019 16:39:18 +0200 Subject: [PATCH 11/15] minor changes + more tests #25 --- src/lib.rs | 8 +- src/line.rs | 22 ++-- src/media_segment.rs | 34 +++++- .../master_playlist/i_frame_stream_inf.rs | 27 +++++ src/tags/master_playlist/media.rs | 12 +- src/tags/media_segment/inf.rs | 11 ++ src/tags/media_segment/map.rs | 110 ++++++++++++++++-- src/tags/media_segment/program_date_time.rs | 85 +++++++++++++- src/types/decimal_floating_point.rs | 4 +- src/types/key_format_versions.rs | 2 +- src/types/signed_decimal_floating_point.rs | 8 +- 11 files changed, 281 insertions(+), 42 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2fc3c24..30fe3cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,10 +36,10 @@ //! assert!(m3u8.parse::<MediaPlaylist>().is_ok()); //! ``` -pub use error::{Error, ErrorKind}; -pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; -pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder}; -pub use media_segment::{MediaSegment, MediaSegmentBuilder}; +pub use error::Error; +pub use master_playlist::MasterPlaylist; +pub use media_playlist::MediaPlaylist; +pub use media_segment::MediaSegment; pub mod tags; pub mod types; diff --git a/src/line.rs b/src/line.rs index 9d66b43..f36782a 100644 --- a/src/line.rs +++ b/src/line.rs @@ -22,40 +22,40 @@ impl FromStr for Lines { let mut stream_inf_line = None; for l in input.lines() { - let line = l.trim(); + let raw_line = l.trim(); - if line.is_empty() { + if raw_line.is_empty() { continue; } - let pline = { - if line.starts_with(tags::ExtXStreamInf::PREFIX) { + let line = { + if raw_line.starts_with(tags::ExtXStreamInf::PREFIX) { stream_inf = true; - stream_inf_line = Some(line); + stream_inf_line = Some(raw_line); continue; - } else if line.starts_with("#EXT") { - Line::Tag(line.parse()?) - } else if line.starts_with('#') { + } else if raw_line.starts_with("#EXT") { + Line::Tag(raw_line.parse()?) + } else if raw_line.starts_with('#') { continue; // ignore comments } else { // stream inf line needs special treatment if stream_inf { stream_inf = false; if let Some(first_line) = stream_inf_line { - let res = Line::Tag(format!("{}\n{}", first_line, line).parse()?); + let res = Line::Tag(format!("{}\n{}", first_line, raw_line).parse()?); stream_inf_line = None; res } else { continue; } } else { - Line::Uri(line.trim().to_string()) + Line::Uri(raw_line.to_string()) } } }; - result.push(pline); + result.push(line); } Ok(result) diff --git a/src/media_segment.rs b/src/media_segment.rs index 2efa2b8..f842d2c 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -38,6 +38,8 @@ pub struct MediaSegment { impl MediaSegment { /// Returns a Builder for a [`MasterPlaylist`]. + /// + /// [`MasterPlaylist`]: crate::MasterPlaylist pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } /// Returns the `URI` of the media segment. @@ -161,7 +163,7 @@ impl fmt::Display for MediaSegment { if let Some(value) = &self.program_date_time_tag { writeln!(f, "{}", value)?; } - writeln!(f, "{},", self.inf_tag)?; + writeln!(f, "{}", self.inf_tag)?; // TODO: there might be a `,` missing writeln!(f, "{}", self.uri)?; Ok(()) } @@ -186,3 +188,33 @@ impl Encrypted for MediaSegment { fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn test_display() { + assert_eq!( + MediaSegment::builder() + .keys(vec![ExtXKey::empty()]) + .map_tag(ExtXMap::new("https://www.example.com/")) + .byte_range_tag(ExtXByteRange::new(20, Some(5))) + //.date_range_tag() // TODO! + .discontinuity_tag(ExtXDiscontinuity) + .inf_tag(ExtInf::new(Duration::from_secs(4))) + .uri("http://www.uri.com/") + .build() + .unwrap() + .to_string(), + "#EXT-X-KEY:METHOD=NONE\n\ + #EXT-X-MAP:URI=\"https://www.example.com/\"\n\ + #EXT-X-BYTERANGE:20@5\n\ + #EXT-X-DISCONTINUITY\n\ + #EXTINF:4,\n\ + http://www.uri.com/\n" + .to_string() + ); + } +} diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 5494015..cd3c2fa 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -191,6 +191,33 @@ impl DerefMut for ExtXIFrameStreamInf { mod test { use super::*; + #[test] + fn test_builder() { + let mut i_frame_stream_inf = + ExtXIFrameStreamInf::new("http://example.com/audio-only.m3u8", 200_000); + + i_frame_stream_inf + .set_average_bandwidth(Some(100_000)) + .set_codecs(Some("mp4a.40.5")) + .set_resolution(1920, 1080) + .set_hdcp_level(Some(HdcpLevel::None)) + .set_video(Some("video")); + + assert_eq!( + ExtXIFrameStreamInf::builder() + .uri("http://example.com/audio-only.m3u8") + .bandwidth(200_000) + .average_bandwidth(100_000) + .codecs("mp4a.40.5") + .resolution((1920, 1080)) + .hdcp_level(HdcpLevel::None) + .video("video") + .build() + .unwrap(), + i_frame_stream_inf + ); + } + #[test] fn test_display() { assert_eq!( diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 16858e2..5c986e1 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -32,7 +32,7 @@ pub struct ExtXMedia { /// # Note /// This attribute is **required**. media_type: MediaType, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the `URI` that identifies the [`Media Playlist`]. /// /// # Note @@ -49,7 +49,7 @@ pub struct ExtXMedia { /// # Note /// This attribute is **required**. group_id: String, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the name of the primary language used in the rendition. /// The value has to conform to [`RFC5646`]. /// @@ -58,7 +58,7 @@ pub struct ExtXMedia { /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 language: Option<String>, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the name of a language associated with the rendition. /// /// # Note @@ -93,14 +93,14 @@ pub struct ExtXMedia { #[builder(default)] /// Sets the value of the `forced` flag. is_forced: bool, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the identifier that specifies a rendition within the segments in /// the media playlist. instream_id: Option<InStreamId>, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the string that represents uniform type identifiers (UTI). characteristics: Option<String>, - #[builder(setter(strip_option, into), default)] + #[builder(setter(strip_option), default)] /// Sets the parameters of the rendition. channels: Option<Channels>, } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 505ed63..9d95cbd 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -226,6 +226,9 @@ mod test { "#EXTINF:5,title".parse::<ExtInf>().unwrap(), ExtInf::with_title(Duration::from_secs(5), "title") ); + + assert!("#EXTINF:".parse::<ExtInf>().is_err()); + assert!("#EXTINF:garbage".parse::<ExtInf>().is_err()); } #[test] @@ -248,4 +251,12 @@ mod test { ProtocolVersion::V3 ); } + + #[test] + fn test_from() { + assert_eq!( + ExtInf::from(Duration::from_secs(1)), + ExtInf::new(Duration::from_secs(1)) + ); + } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 6f3fdfe..4c7bf31 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -7,13 +7,13 @@ use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; use crate::{Encrypted, Error, RequiredVersion}; -/// # [4.4.2.5. EXT-X-MAP] -/// The [`ExtXMap`] tag specifies how to obtain the Media Initialization -/// Section, required to parse the applicable [Media Segment]s. +/// # [4.3.2.5. EXT-X-MAP] /// -/// [Media Segment]: crate::MediaSegment -/// [4.4.2.5. EXT-X-MAP]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.5 +/// The [`ExtXMap`] tag specifies how to obtain the Media Initialization +/// Section, required to parse the applicable [`MediaSegment`]s. +/// +/// [`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)] pub struct ExtXMap { uri: String, @@ -25,6 +25,12 @@ impl ExtXMap { pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; /// Makes a new [`ExtXMap`] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); + /// ``` pub fn new<T: ToString>(uri: T) -> Self { Self { uri: uri.to_string(), @@ -34,6 +40,17 @@ impl ExtXMap { } /// Makes a new [`ExtXMap`] tag with the given range. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::ByteRange; + /// + /// let map = ExtXMap::with_range( + /// "https://prod.mediaspace.com/init.bin", + /// ByteRange::new(9, Some(2)), + /// ); + /// ``` pub fn with_range<T: ToString>(uri: T, range: ByteRange) -> Self { Self { uri: uri.to_string(), @@ -42,12 +59,75 @@ impl ExtXMap { } } - /// Returns the `URI` that identifies a resource, - /// that contains the media initialization section. + /// Returns the `URI` that identifies a resource, that contains the media + /// initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); + /// + /// assert_eq!( + /// map.uri(), + /// &"https://prod.mediaspace.com/init.bin".to_string() + /// ); + /// ``` pub const fn uri(&self) -> &String { &self.uri } + /// Sets the `URI` that identifies a resource, that contains the media + /// initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// let mut map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); + /// + /// map.set_uri("https://dev.mediaspace.com/init.bin"); + /// assert_eq!( + /// map.uri(), + /// &"https://dev.mediaspace.com/init.bin".to_string() + /// ); + /// ``` + pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self { + self.uri = value.to_string(); + self + } + /// Returns the range of the media initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::ByteRange; + /// + /// let map = ExtXMap::with_range( + /// "https://prod.mediaspace.com/init.bin", + /// ByteRange::new(9, Some(2)), + /// ); + /// + /// assert_eq!(map.range(), Some(ByteRange::new(9, Some(2)))); + /// ``` pub const fn range(&self) -> Option<ByteRange> { self.range } + + /// Sets the range of the media initialization section. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::ByteRange; + /// + /// let mut map = ExtXMap::with_range( + /// "https://prod.mediaspace.com/init.bin", + /// ByteRange::new(9, Some(2)), + /// ); + /// + /// map.set_range(Some(ByteRange::new(1, None))); + /// assert_eq!(map.range(), Some(ByteRange::new(1, None))); + /// ``` + pub fn set_range(&mut self, value: Option<ByteRange>) -> &mut Self { + self.range = value; + self + } } impl Encrypted for ExtXMap { @@ -87,7 +167,7 @@ impl FromStr for ExtXMap { match key.as_str() { "URI" => uri = Some(unquote(value)), "BYTERANGE" => { - range = Some((unquote(value).parse())?); + range = Some(unquote(value).parse()?); } _ => { // [6.3.1. General Client Responsibilities] @@ -134,6 +214,12 @@ mod test { ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap() ); + assert_eq!( + ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED" + .parse() + .unwrap() + ); } #[test] @@ -144,4 +230,10 @@ mod test { ProtocolVersion::V6 ); } + + #[test] + fn test_encrypted() { + assert_eq!(ExtXMap::new("foo").keys(), &vec![]); + assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]); + } } diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 9350761..628cd2a 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -2,7 +2,7 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, SecondsFormat}; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -24,8 +24,8 @@ impl ExtXProgramDateTime { /// /// # Example /// ``` + /// # use hls_m3u8::tags::ExtXProgramDateTime; /// use chrono::{FixedOffset, TimeZone}; - /// use hls_m3u8::tags::ExtXProgramDateTime; /// /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds /// @@ -39,22 +39,71 @@ impl ExtXProgramDateTime { /// Returns the date-time of the first sample of the associated media /// segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXProgramDateTime; + /// use chrono::{FixedOffset, TimeZone}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let program_date_time = ExtXProgramDateTime::new( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// assert_eq!( + /// program_date_time.date_time(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31) + /// ); + /// ``` pub const fn date_time(&self) -> DateTime<FixedOffset> { self.0 } /// Sets the date-time of the first sample of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXProgramDateTime; + /// use chrono::{FixedOffset, TimeZone}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut program_date_time = ExtXProgramDateTime::new( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// program_date_time.set_date_time( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// ); + /// + /// assert_eq!( + /// program_date_time.date_time(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ); + /// ``` pub fn set_date_time(&mut self, value: DateTime<FixedOffset>) -> &mut Self { self.0 = value; self } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXProgramDateTime { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXProgramDateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let date_time = self.0.to_rfc3339(); + let date_time = self.0.to_rfc3339_opts(SecondsFormat::Millis, true); write!(f, "{}{}", Self::PREFIX, date_time) } } @@ -83,7 +132,7 @@ impl DerefMut for ExtXProgramDateTime { #[cfg(test)] mod test { use super::*; - use chrono::TimeZone; + use chrono::{Datelike, TimeZone}; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds @@ -126,4 +175,32 @@ mod test { ProtocolVersion::V1 ); } + + #[test] + fn test_deref() { + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ) + .year(), + 2010 + ); + } + + #[test] + fn test_deref_mut() { + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ) + .deref_mut(), + &mut FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ); + } } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 8a072db..2c6d13e 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -23,7 +23,7 @@ impl DecimalFloatingPoint { /// otherwise this function will return an error that has the kind /// `ErrorKind::InvalidInput`. pub fn new(value: f64) -> crate::Result<Self> { - if value.is_sign_negative() || value.is_infinite() { + if value.is_sign_negative() || value.is_infinite() || value.is_nan() { return Err(Error::invalid_input()); } Ok(Self(value)) @@ -54,7 +54,7 @@ impl From<f64> for DecimalFloatingPoint { let mut result = value; // guard against the unlikely case of an infinite value... - if result.is_infinite() { + if result.is_infinite() || result.is_nan() { result = 0.0; } diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 16537a9..b159f8f 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -7,7 +7,7 @@ use crate::types::ProtocolVersion; use crate::utils::{quote, unquote}; use crate::RequiredVersion; -/// A list of [usize], that can be used to indicate which version(s) +/// A list of [`usize`], that can be used to indicate which version(s) /// this instance complies with, if more than one version of a particular /// [`KeyFormat`] is defined. /// diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 277ab92..bb84a76 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -10,20 +10,20 @@ use derive_more::{Display, FromStr}; pub(crate) struct SignedDecimalFloatingPoint(f64); impl SignedDecimalFloatingPoint { - /// Makes a new [SignedDecimalFloatingPoint] instance. + /// Makes a new [`SignedDecimalFloatingPoint`] instance. /// /// # Panics /// The given value must be finite, otherwise this function will panic! pub fn new(value: f64) -> Self { - if value.is_infinite() { - panic!("Floating point value must be finite!"); + if value.is_infinite() || value.is_nan() { + panic!("Floating point value must be finite and not NaN!"); } Self(value) } pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) } - /// Converts [DecimalFloatingPoint] to [f64]. + /// Converts [`DecimalFloatingPoint`] to [`f64`]. pub const fn as_f64(self) -> f64 { self.0 } } From c8020ede8e4d9752d8e6add63be11ec269e56376 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 6 Oct 2019 17:30:05 +0200 Subject: [PATCH 12/15] update dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f69f6d7..6e332e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,11 @@ codecov = { repository = "sile/hls_m3u8" } [dependencies] failure = "0.1.5" -derive_builder = "0.7.2" +derive_builder = "0.8.0" chrono = "0.4.9" strum = { version = "0.16.0", features = ["derive"] } derive_more = "0.15.0" hex = "0.4.0" [dev-dependencies] -clap = "2" +clap = "2.33.0" From e75153ec5e8c7e23e24d17204196c25b84d3c0cf Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 6 Oct 2019 17:30:24 +0200 Subject: [PATCH 13/15] fix backwards compatibility --- src/tags/media_segment/map.rs | 5 +++++ src/traits.rs | 7 +++++++ src/types/decryption_key.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 4c7bf31..be2138a 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -138,7 +138,12 @@ impl Encrypted for ExtXMap { /// This tag requires [`ProtocolVersion::V6`]. impl RequiredVersion for ExtXMap { + // this should return ProtocolVersion::V5, if it does not contain an + // EXT-X-I-FRAMES-ONLY! + // http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/ fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } + + fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } impl fmt::Display for ExtXMap { diff --git a/src/traits.rs b/src/traits.rs index c47defc..af8c5ca 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -120,7 +120,14 @@ pub trait Encrypted { /// ``` pub trait RequiredVersion { /// Returns the protocol compatibility version that this tag requires. + /// + /// # Note + /// This is for the latest working [`ProtocolVersion`] and a client, that + /// only supports an older version would break. fn required_version(&self) -> ProtocolVersion; + + /// The protocol version, in which the tag has been introduced. + fn introduced_version(&self) -> ProtocolVersion { self.required_version() } } impl<T: RequiredVersion> RequiredVersion for Vec<T> { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 7aa745d..5b04e3d 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -482,6 +482,38 @@ mod test { 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::V1 + ); + + 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 + ); + } + + #[test] + fn test_introduced_version() { + assert_eq!( + DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") + .required_version(), + ProtocolVersion::V1 + ); + assert_eq!( DecryptionKey::builder() .method(EncryptionMethod::Aes128) From c53e9e33f140ee450b172d387da2252c539a6907 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 8 Oct 2019 15:42:33 +0200 Subject: [PATCH 14/15] added pretty_assertions This will allow for better troubleshooting of failing test, because you don't have to search for the difference (between left and right). This is especially helpful for larger assertions. --- Cargo.toml | 1 + src/attribute.rs | 1 + src/master_playlist.rs | 1 + src/media_playlist.rs | 1 + src/media_segment.rs | 1 + src/tags/basic/m3u.rs | 1 + src/tags/basic/version.rs | 1 + src/tags/master_playlist/i_frame_stream_inf.rs | 1 + src/tags/master_playlist/media.rs | 1 + src/tags/master_playlist/session_data.rs | 1 + src/tags/master_playlist/session_key.rs | 1 + src/tags/master_playlist/stream_inf.rs | 1 + src/tags/media_playlist/discontinuity_sequence.rs | 1 + src/tags/media_playlist/end_list.rs | 1 + src/tags/media_playlist/i_frames_only.rs | 1 + src/tags/media_playlist/media_sequence.rs | 1 + src/tags/media_playlist/playlist_type.rs | 1 + src/tags/media_playlist/target_duration.rs | 1 + src/tags/media_segment/byte_range.rs | 1 + src/tags/media_segment/date_range.rs | 1 + src/tags/media_segment/discontinuity.rs | 1 + src/tags/media_segment/inf.rs | 1 + src/tags/media_segment/key.rs | 1 + src/tags/media_segment/map.rs | 1 + src/tags/media_segment/program_date_time.rs | 1 + src/tags/shared/independent_segments.rs | 1 + src/tags/shared/start.rs | 1 + src/types/byte_range.rs | 1 + src/types/channels.rs | 1 + src/types/closed_captions.rs | 1 + src/types/decimal_floating_point.rs | 1 + src/types/decimal_resolution.rs | 1 + src/types/decryption_key.rs | 1 + src/types/encryption_method.rs | 1 + src/types/hdcp_level.rs | 1 + src/types/in_stream_id.rs | 1 + src/types/initialization_vector.rs | 1 + src/types/key_format.rs | 1 + src/types/key_format_versions.rs | 1 + src/types/media_type.rs | 1 + src/types/protocol_version.rs | 1 + src/types/signed_decimal_floating_point.rs | 1 + src/types/stream_inf.rs | 1 + src/types/value.rs | 1 + src/utils.rs | 1 + tests/master_playlist.rs | 2 ++ 46 files changed, 47 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 6e332e2..f790b1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ hex = "0.4.0" [dev-dependencies] clap = "2.33.0" +pretty_assertions = "0.6.1" diff --git a/src/attribute.rs b/src/attribute.rs index 1a513b7..95f99a6 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -96,6 +96,7 @@ fn split(value: &str, terminator: char) -> Vec<String> { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index ff2a944..04b2d99 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -445,6 +445,7 @@ impl FromStr for MasterPlaylist { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/media_playlist.rs b/src/media_playlist.rs index dc360df..7d66cb4 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -379,6 +379,7 @@ impl FromStr for MediaPlaylist { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn too_large_segment_duration_test() { diff --git a/src/media_segment.rs b/src/media_segment.rs index f842d2c..65394d0 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -192,6 +192,7 @@ impl Encrypted for MediaSegment { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; use std::time::Duration; #[test] diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 019818e..b1f8592 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -62,6 +62,7 @@ impl FromStr for ExtM3u { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index f7f68ca..e39f079 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -104,6 +104,7 @@ impl FromStr for ExtXVersion { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index cd3c2fa..2e9c9f5 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -190,6 +190,7 @@ impl DerefMut for ExtXIFrameStreamInf { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_builder() { diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 5c986e1..794e131 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -740,6 +740,7 @@ impl FromStr for ExtXMedia { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 9bebffd..a264158 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -324,6 +324,7 @@ impl FromStr for ExtXSessionData { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index bdd7fdb..6bd3731 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -86,6 +86,7 @@ impl DerefMut for ExtXSessionKey { mod test { use super::*; use crate::types::{EncryptionMethod, KeyFormat}; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index cdd65ab..d8cc87d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -376,6 +376,7 @@ impl DerefMut for ExtXStreamInf { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index fbfd851..c49b9d7 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -84,6 +84,7 @@ impl FromStr for ExtXDiscontinuitySequence { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 874445b..430354a 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -45,6 +45,7 @@ impl FromStr for ExtXEndList { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index ac35ed2..c28d830 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -47,6 +47,7 @@ impl FromStr for ExtXIFramesOnly { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index e41a0a6..ed2babf 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -81,6 +81,7 @@ impl FromStr for ExtXMediaSequence { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 1c22b0a..9311191 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -61,6 +61,7 @@ impl FromStr for ExtXPlaylistType { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 83a6aa9..046ccc5 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -73,6 +73,7 @@ impl FromStr for ExtXTargetDuration { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 3ad2ad1..936189a 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -104,6 +104,7 @@ impl FromStr for ExtXByteRange { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index e984a48..93061a7 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -823,6 +823,7 @@ impl fmt::Display for ExtXDateRange { mod test { use super::*; use chrono::offset::TimeZone; + use pretty_assertions::assert_eq; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 90d4e65..7b3fad5 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -44,6 +44,7 @@ impl FromStr for ExtXDiscontinuity { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 9d95cbd..fc1cb51 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -178,6 +178,7 @@ impl From<Duration> for ExtInf { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index b5e0528..4efac85 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -113,6 +113,7 @@ impl DerefMut for ExtXKey { mod test { use super::*; use crate::types::{EncryptionMethod, KeyFormat}; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index be2138a..d702709 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -194,6 +194,7 @@ impl FromStr for ExtXMap { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 628cd2a..5ea34ca 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -133,6 +133,7 @@ impl DerefMut for ExtXProgramDateTime { mod test { use super::*; use chrono::{Datelike, TimeZone}; + use pretty_assertions::assert_eq; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 07e5f78..47b0f66 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -35,6 +35,7 @@ impl FromStr for ExtXIndependentSegments { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 701b792..22a3acb 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -157,6 +157,7 @@ impl FromStr for ExtXStart { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index a1807d1..795514d 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -114,6 +114,7 @@ impl FromStr for ByteRange { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/channels.rs b/src/types/channels.rs index 5cad2dc..effec06 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -103,6 +103,7 @@ impl fmt::Display for Channels { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 2c73a99..93bf543 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -40,6 +40,7 @@ impl FromStr for ClosedCaptions { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 2c6d13e..5e8c2de 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -69,6 +69,7 @@ impl From<f32> for DecimalFloatingPoint { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; macro_rules! test_from { ( $($input:expr),* ) => { diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index b5d9ef4..2a4b4ae 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -68,6 +68,7 @@ impl FromStr for DecimalResolution { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 5b04e3d..f974a42 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -353,6 +353,7 @@ impl fmt::Display for DecryptionKey { mod test { use super::*; use crate::types::EncryptionMethod; + use pretty_assertions::assert_eq; #[test] fn test_builder() { diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index fb3673f..a555009 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -50,6 +50,7 @@ pub enum EncryptionMethod { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index a268181..1d03e2a 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -17,6 +17,7 @@ pub enum HdcpLevel { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 1bd7cab..b39ce5c 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -81,6 +81,7 @@ pub enum InStreamId { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; macro_rules! gen_tests { ( $($string:expr => $enum:expr),* ) => { diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index c19bf50..971bcc9 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -66,6 +66,7 @@ impl FromStr for InitializationVector { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 5b534ff..3ffac49 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -39,6 +39,7 @@ impl RequiredVersion for KeyFormat { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index b159f8f..2b4fc95 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -97,6 +97,7 @@ impl<T: Into<Vec<usize>>> From<T> for KeyFormatVersions { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 52a7e57..19a999f 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -14,6 +14,7 @@ pub enum MediaType { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parser() { diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index d81d0ed..1ea2a0e 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -73,6 +73,7 @@ impl Default for ProtocolVersion { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index bb84a76..54a58e5 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -36,6 +36,7 @@ impl Deref for SignedDecimalFloatingPoint { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; macro_rules! test_from { ( $( $input:expr => $output:expr ),* ) => { diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index 076e853..d460d56 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -300,6 +300,7 @@ impl FromStr for StreamInf { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/types/value.rs b/src/types/value.rs index 90c1783..8c0d961 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -67,6 +67,7 @@ impl From<&str> for Value { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { diff --git a/src/utils.rs b/src/utils.rs index 9eb7fae..d21d9e6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -83,6 +83,7 @@ where #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_parse_yes_or_no() { diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs index 18bcf49..24d901f 100644 --- a/tests/master_playlist.rs +++ b/tests/master_playlist.rs @@ -1,6 +1,8 @@ use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXStreamInf}; use hls_m3u8::MasterPlaylist; +use pretty_assertions::assert_eq; + #[test] fn test_master_playlist() { let master_playlist = "#EXTM3U\n\ From 73d9eb4f7978953cfddc320417e538edcb36619f Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 12 Oct 2019 11:38:28 +0200 Subject: [PATCH 15/15] minor changes --- src/media_playlist.rs | 2 +- src/media_segment.rs | 2 +- src/tags/master_playlist/media.rs | 8 +- src/tags/media_playlist/end_list.rs | 2 +- src/tags/media_playlist/i_frames_only.rs | 2 +- src/tags/media_playlist/media_sequence.rs | 7 +- src/tags/media_playlist/playlist_type.rs | 2 +- src/tags/media_playlist/target_duration.rs | 28 ++- src/tags/media_segment/byte_range.rs | 2 +- src/tags/media_segment/date_range.rs | 10 +- src/tags/media_segment/discontinuity.rs | 2 +- src/tags/media_segment/key.rs | 2 +- src/tags/media_segment/map.rs | 2 +- src/types/decryption_key.rs | 34 +-- src/types/stream_inf.rs | 2 +- tests/master_playlist.rs | 256 ++++++++++++++++++++- tests/media_playlist.rs | 53 +++++ 17 files changed, 351 insertions(+), 65 deletions(-) create mode 100644 tests/media_playlist.rs diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 7d66cb4..a334650 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -14,7 +14,7 @@ use crate::types::ProtocolVersion; use crate::{Encrypted, Error, RequiredVersion}; /// Media playlist. -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] pub struct MediaPlaylist { diff --git a/src/media_segment.rs b/src/media_segment.rs index 65394d0..ea4f160 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -8,7 +8,7 @@ use crate::tags::{ use crate::types::ProtocolVersion; use crate::{Encrypted, RequiredVersion}; -#[derive(Debug, Clone, Builder)] +#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)] #[builder(setter(into, strip_option))] /// Media segment. pub struct MediaSegment { diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 794e131..f43e73f 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -128,9 +128,11 @@ impl ExtXMediaBuilder { } if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { - return Err( - Error::custom("If `DEFAULT` is true, `AUTOSELECT` has to be true too!").to_string(), - ); + return Err(Error::custom(format!( + "If `DEFAULT` is true, `AUTOSELECT` has to be true too, Default: {:?}, Autoselect: {:?}!", + self.is_default, self.is_autoselect + )) + .to_string()); } if media_type != MediaType::Subtitles && self.is_forced.is_some() { diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index 430354a..5463f5e 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -18,7 +18,7 @@ use crate::{Error, RequiredVersion}; /// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.3.4. EXT-X-ENDLIST]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXEndList; impl ExtXEndList { diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index c28d830..c9c4dc9 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -20,7 +20,7 @@ use crate::{Error, RequiredVersion}; /// [`Media Segment`]: crate::MediaSegment /// [4.4.3.6. EXT-X-I-FRAMES-ONLY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXIFramesOnly; impl ExtXIFramesOnly { diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index ed2babf..5e26eb7 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -9,12 +9,6 @@ use crate::{Error, RequiredVersion}; /// The [`ExtXMediaSequence`] tag indicates the Media Sequence Number of /// the first [`Media Segment`] that appears in a Playlist file. /// -/// Its format is: -/// ```text -/// #EXT-X-MEDIA-SEQUENCE:<number> -/// ``` -/// where `number` is a [`u64`]. -/// /// [Media Segment]: crate::MediaSegment /// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2 @@ -61,6 +55,7 @@ impl ExtXMediaSequence { } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXMediaSequence { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 9311191..69c2e52 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -12,7 +12,7 @@ use crate::{Error, RequiredVersion}; /// /// [`Media Playlist`]: crate::MediaPlaylist /// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[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`]. diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 046ccc5..652afb4 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::ops::Deref; use std::str::FromStr; use std::time::Duration; @@ -6,14 +7,13 @@ use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.3.1. EXT-X-TARGETDURATION] -/// The [`ExtXTargetDuration`] tag specifies the maximum [`Media Segment`] +/// # [4.3.3.1. EXT-X-TARGETDURATION] +/// The [`ExtXTargetDuration`] tag specifies the maximum [`MediaSegment`] /// duration. /// -/// [`Media Segment`]: crate::MediaSegment -/// [4.4.3.1. EXT-X-TARGETDURATION]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.3.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +/// [`MediaSegment`]: crate::MediaSegment +/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] pub struct ExtXTargetDuration(Duration); impl ExtXTargetDuration { @@ -31,10 +31,7 @@ impl ExtXTargetDuration { /// /// # Note /// The nanoseconds part of the [`Duration`] will be discarded. - pub const fn new(duration: Duration) -> Self { - // TOOD: round instead of discarding? - Self(Duration::from_secs(duration.as_secs())) - } + pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) } /// Returns the maximum media segment duration. /// @@ -55,6 +52,12 @@ impl RequiredVersion for ExtXTargetDuration { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } +impl Deref for ExtXTargetDuration { + type Target = Duration; + + fn deref(&self) -> &Self::Target { &self.0 } +} + impl fmt::Display for ExtXTargetDuration { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0.as_secs()) @@ -98,4 +101,9 @@ mod test { "#EXT-X-TARGETDURATION:5".parse().unwrap() ); } + + #[test] + fn test_deref() { + assert_eq!(ExtXTargetDuration::new(Duration::from_secs(5)).as_secs(), 5); + } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 936189a..f2aafb4 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -23,7 +23,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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 93061a7..6586f8a 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -12,9 +12,12 @@ 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)] +#[derive(Builder, Debug, Clone, PartialEq, PartialOrd)] #[builder(setter(into))] pub struct ExtXDateRange { /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. @@ -87,7 +90,7 @@ pub struct ExtXDateRange { /// This attribute is optional. end_on_next: bool, #[builder(default)] - /// The "X-" prefix defines a namespace reserved for client-defined + /// 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 @@ -513,10 +516,13 @@ impl ExtXDateRange { self } + /// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf pub const fn scte35_cmd(&self) -> &Option<String> { &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<String> { &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<String> { &self.scte35_out } /// This attribute indicates that the end of the range containing it is diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 7b3fad5..5a40451 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -17,7 +17,7 @@ use crate::{Error, RequiredVersion}; /// [`Media Segment`]: crate::MediaSegment /// [4.4.2.3. EXT-X-DISCONTINUITY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXDiscontinuity; impl ExtXDiscontinuity { diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index 4efac85..4ebe50b 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -22,7 +22,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)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXKey(DecryptionKey); impl ExtXKey { diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index d702709..f63f8d5 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -14,7 +14,7 @@ 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)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXMap { uri: String, range: Option<ByteRange>, diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index f974a42..77d85b6 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -10,7 +10,7 @@ use crate::types::{ use crate::utils::{quote, unquote}; use crate::{Error, RequiredVersion}; -#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[builder(setter(into), build_fn(validate = "Self::validate"))] /// [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`] /// and [`ExtXKey`]. @@ -483,38 +483,6 @@ mod test { 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::V1 - ); - - 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 - ); - } - - #[test] - fn test_introduced_version() { - assert_eq!( - DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") - .required_version(), - ProtocolVersion::V1 - ); - assert_eq!( DecryptionKey::builder() .method(EncryptionMethod::Aes128) diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index d460d56..3f5b4b1 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -11,7 +11,7 @@ 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)] +#[derive(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] #[builder(setter(into, strip_option))] #[builder(derive(Debug, PartialEq))] pub struct StreamInf { diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs index 24d901f..fb2ecfc 100644 --- a/tests/master_playlist.rs +++ b/tests/master_playlist.rs @@ -1,10 +1,12 @@ -use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXStreamInf}; +use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf}; +use hls_m3u8::types::MediaType; use hls_m3u8::MasterPlaylist; use pretty_assertions::assert_eq; #[test] fn test_master_playlist() { + // https://tools.ietf.org/html/rfc8216#section-8.4 let master_playlist = "#EXTM3U\n\ #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\ http://example.com/low.m3u8\n\ @@ -53,6 +55,7 @@ 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\ @@ -115,3 +118,254 @@ fn test_master_playlist_with_i_frames() { master_playlist ); } + +#[test] +fn test_master_playlist_with_alternative_audio() { + // https://tools.ietf.org/html/rfc8216#section-8.6 + // TODO: I think the CODECS=\"..." have to be replaced. + let master_playlist = "#EXTM3U\n\ + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", \ + DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", \ + URI=\"main/english-audio.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", \ + DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", \ + URI=\"main/german-audio.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", \ + DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", \ + URI=\"commentary/audio-only.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n\ + low/video-only.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n\ + mid/video-only.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n\ + hi/video-only.m3u8\n\ + #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n\ + main/english-audio.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .media_tags(vec![ + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("aac") + .name("English") + .is_default(true) + .is_autoselect(true) + .language("en") + .uri("main/english-audio.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("aac") + .name("Deutsch") + .is_default(false) + .is_autoselect(true) + .language("de") + .uri("main/german-audio.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("aac") + .name("Commentary") + .is_default(false) + .is_autoselect(false) + .language("en") + .uri("commentary/audio-only.m3u8") + .build() + .unwrap(), + ]) + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .codecs("...") + .audio("aac") + .uri("low/video-only.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .codecs("...") + .audio("aac") + .uri("mid/video-only.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .codecs("...") + .audio("aac") + .uri("hi/video-only.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .audio("aac") + .uri("main/english-audio.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} + +#[test] +fn test_master_playlist_with_alternative_video() { + // https://tools.ietf.org/html/rfc8216#section-8.7 + let master_playlist = "#EXTM3U\n\ + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", \ + AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", \ + DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", \ + DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n\ + low/main/audio-video.m3u8\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", \ + AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", \ + DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", \ + DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n\ + mid/main/audio-video.m3u8\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\", \ + AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", \ + DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n\ + + #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", \ + DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\ + + #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\" + hi/main/audio-video.m3u8" + .parse::<MasterPlaylist>() + .unwrap(); + + assert_eq!( + MasterPlaylist::builder() + .media_tags(vec![ + // low + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("low") + .name("Main") + .is_default(true) + .is_autoselect(true) + .uri("low/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("low") + .name("Centerfield") + .is_default(false) + .uri("low/centerfield/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("low") + .name("Dugout") + .is_default(false) + .uri("low/dugout/audio-video.m3u8") + .build() + .unwrap(), + // mid + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("mid") + .name("Main") + .is_default(true) + .is_autoselect(true) + .uri("mid/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("mid") + .name("Centerfield") + .is_default(false) + .uri("mid/centerfield/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("mid") + .name("Dugout") + .is_default(false) + .uri("mid/dugout/audio-video.m3u8") + .build() + .unwrap(), + // hi + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("hi") + .name("Main") + .is_default(true) + .is_autoselect(true) + .uri("hi/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("hi") + .name("Centerfield") + .is_default(false) + .uri("hi/centerfield/audio-video.m3u8") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Video) + .group_id("hi") + .name("Dugout") + .is_default(false) + .uri("hi/dugout/audio-video.m3u8") + .build() + .unwrap(), + ]) + .stream_inf_tags(vec![ + ExtXStreamInf::builder() + .bandwidth(1280000) + .codecs("...") + .video("low") + .uri("low/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(2560000) + .codecs("...") + .video("mid") + .uri("mid/main/audio-video.m3u8") + .build() + .unwrap(), + ExtXStreamInf::builder() + .bandwidth(7680000) + .codecs("...") + .video("hi") + .uri("hi/main/audio-video.m3u8") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + master_playlist + ); +} diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs new file mode 100644 index 0000000..7f8a860 --- /dev/null +++ b/tests/media_playlist.rs @@ -0,0 +1,53 @@ +use std::time::Duration; + +use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration}; +use hls_m3u8::{MediaPlaylist, MediaSegment}; +use pretty_assertions::assert_eq; + +#[test] +fn test_media_playlist_with_byterange() { + let media_playlist = "#EXTM3U\n\ + #EXT-X-TARGETDURATION:10\n\ + #EXT-X-VERSION:4\n\ + #EXT-X-MEDIA-SEQUENCE:0\n\ + #EXTINF:10.0,\n\ + #EXT-X-BYTERANGE:75232@0\n\ + video.ts\n\ + #EXT-X-BYTERANGE:82112@752321\n\ + #EXTINF:10.0,\n\ + video.ts\n\ + #EXTINF:10.0,\n\ + #EXT-X-BYTERANGE:69864\n\ + video.ts" + .parse::<MediaPlaylist>() + .unwrap(); + + assert_eq!( + MediaPlaylist::builder() + .target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10))) + .media_sequence_tag(ExtXMediaSequence::new(0)) + .segments(vec![ + MediaSegment::builder() + .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range_tag(ExtXByteRange::new(75232, Some(0))) + .uri("video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range_tag(ExtXByteRange::new(82112, Some(752321))) + .uri("video.ts") + .build() + .unwrap(), + MediaSegment::builder() + .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) + .byte_range_tag(ExtXByteRange::new(69864, None)) + .uri("video.ts") + .build() + .unwrap(), + ]) + .build() + .unwrap(), + media_playlist + ) +}