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] 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 + ); +}