From b1aa51267981dc070b0fefdb28a52c64a28ebbfe Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 13:26:16 +0200 Subject: [PATCH] added master_playlist builder --- Cargo.toml | 1 + src/error.rs | 7 + src/master_playlist.rs | 383 ++++++++++--------------- src/tags/basic/version.rs | 12 + src/tags/master_playlist/stream_inf.rs | 6 +- 5 files changed, 177 insertions(+), 232 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65c0cf4..35c5a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ codecov = {repository = "sile/hls_m3u8"} [dependencies] getset = "0.0.8" failure = "0.1.5" +derive_builder = "0.7.2" [dev-dependencies] clap = "2" diff --git a/src/error.rs b/src/error.rs index 5d089a9..92e77dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,9 @@ pub enum ErrorKind { )] VersionError(String, String), + #[fail(display = "BuilderError: {}", _0)] + BuilderError(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -188,6 +191,10 @@ impl Error { specified_version.to_string(), )) } + + pub(crate) fn builder_error(value: T) -> Self { + Self::from(ErrorKind::BuilderError(value.to_string())) + } } impl From for Error { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 683640d..ed8e880 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,212 +1,21 @@ use std::collections::HashSet; use std::fmt; -use std::iter; use std::str::FromStr; +use derive_builder::Builder; + use crate::line::{Line, Lines, Tag}; use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, - ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, + ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::Error; -/// Master playlist builder. -#[derive(Debug, Clone)] -pub struct MasterPlaylistBuilder { - version: Option, - independent_segments_tag: Option, - start_tag: Option, - media_tags: Vec, - stream_inf_tags: Vec, - i_frame_stream_inf_tags: Vec, - session_data_tags: Vec, - session_key_tags: Vec, -} - -impl MasterPlaylistBuilder { - /// Makes a new `MasterPlaylistBuilder` instance. - pub fn new() -> Self { - MasterPlaylistBuilder { - version: None, - independent_segments_tag: None, - start_tag: None, - media_tags: Vec::new(), - stream_inf_tags: Vec::new(), - i_frame_stream_inf_tags: Vec::new(), - session_data_tags: Vec::new(), - session_key_tags: Vec::new(), - } - } - - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility version greater than `version`, - /// `finish()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - pub fn version(&mut self, version: ProtocolVersion) -> &mut Self { - self.version = Some(version); - self - } - - /// Adds the given tag to the resulting playlist. - /// - /// If it is forbidden to have multiple instance of the tag, the existing one will be overwritten. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MasterPlaylistTag::ExtXIndependentSegments(t) => { - self.independent_segments_tag = Some(t); - } - MasterPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), - MasterPlaylistTag::ExtXMedia(t) => self.media_tags.push(t), - MasterPlaylistTag::ExtXStreamInf(t) => self.stream_inf_tags.push(t), - MasterPlaylistTag::ExtXIFrameStreamInf(t) => self.i_frame_stream_inf_tags.push(t), - MasterPlaylistTag::ExtXSessionData(t) => self.session_data_tags.push(t), - MasterPlaylistTag::ExtXSessionKey(t) => self.session_key_tags.push(t), - } - self - } - - /// Builds a `MasterPlaylist` instance. - pub fn finish(self) -> crate::Result { - let required_version = self.required_version(); - let specified_version = self.version.unwrap_or(required_version); - - if required_version < specified_version { - return Err(Error::required_version(required_version, specified_version)); - } - - (self.validate_stream_inf_tags())?; - (self.validate_i_frame_stream_inf_tags())?; - (self.validate_session_data_tags())?; - (self.validate_session_key_tags())?; - - Ok(MasterPlaylist { - version_tag: ExtXVersion::new(specified_version), - independent_segments_tag: self.independent_segments_tag, - start_tag: self.start_tag, - media_tags: self.media_tags, - stream_inf_tags: self.stream_inf_tags, - i_frame_stream_inf_tags: self.i_frame_stream_inf_tags, - session_data_tags: self.session_data_tags, - session_key_tags: self.session_key_tags, - }) - } - - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.start_tag.iter().map(|t| t.requires_version())) - .chain(self.media_tags.iter().map(|t| t.requires_version())) - .chain(self.stream_inf_tags.iter().map(|t| t.requires_version())) - .chain( - self.i_frame_stream_inf_tags - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.session_data_tags.iter().map(|t| t.requires_version())) - .chain(self.session_key_tags.iter().map(|t| t.requires_version())) - .max() - .expect("Never fails") - } - - fn validate_stream_inf_tags(&self) -> crate::Result<()> { - let mut has_none_closed_captions = false; - for t in &self.stream_inf_tags { - if let Some(group_id) = t.audio() { - if !self.check_media_group(MediaType::Audio, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = t.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = t.subtitles() { - if !self.check_media_group(MediaType::Subtitles, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - match t.closed_captions() { - Some(&ClosedCaptions::GroupId(ref group_id)) => { - if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - Some(&ClosedCaptions::None) => { - has_none_closed_captions = true; - } - None => {} - } - } - if has_none_closed_captions { - if !self - .stream_inf_tags - .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) - { - return Err(Error::invalid_input()); - } - } - - Ok(()) - } - - fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { - for t in &self.i_frame_stream_inf_tags { - if let Some(group_id) = t.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - } - - Ok(()) - } - - fn validate_session_data_tags(&self) -> crate::Result<()> { - let mut set = HashSet::new(); - for t in &self.session_data_tags { - if !set.insert((t.data_id(), t.language())) { - return Err(Error::custom(format!("Conflict: {}", t))); - } - } - - Ok(()) - } - - fn validate_session_key_tags(&self) -> crate::Result<()> { - let mut set = HashSet::new(); - for t in &self.session_key_tags { - if !set.insert(t.key()) { - return Err(Error::custom(format!("Conflict: {}", t))); - } - } - - Ok(()) - } - - fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { - self.media_tags - .iter() - .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) - } -} - -impl Default for MasterPlaylistBuilder { - fn default() -> Self { - Self::new() - } -} - /// Master playlist. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder, Default)] +#[builder(build_fn(validate = "Self::validate"))] +#[builder(setter(into, strip_option), default)] pub struct MasterPlaylist { version_tag: ExtXVersion, independent_segments_tag: Option, @@ -219,6 +28,11 @@ pub struct MasterPlaylist { } impl 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 @@ -260,6 +74,95 @@ impl MasterPlaylist { } } +impl MasterPlaylistBuilder { + pub(crate) fn validate(&self) -> Result<(), String> { + // validate stream inf tags + if let Some(stream_inf_tags) = &self.stream_inf_tags { + let mut has_none_closed_captions = false; + for t in stream_inf_tags { + if let Some(group_id) = t.audio() { + if !self.check_media_group(MediaType::Audio, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + if let Some(group_id) = t.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + if let Some(group_id) = t.subtitles() { + if !self.check_media_group(MediaType::Subtitles, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + match t.closed_captions() { + Some(&ClosedCaptions::GroupId(ref group_id)) => { + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + Some(&ClosedCaptions::None) => { + has_none_closed_captions = true; + } + None => {} + } + } + if has_none_closed_captions { + if !stream_inf_tags + .iter() + .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) + { + return Err(Error::invalid_input().to_string()); + } + } + } + + // validate i_frame_stream_inf_tags + if let Some(i_frame_stream_inf_tags) = &self.i_frame_stream_inf_tags { + for t in i_frame_stream_inf_tags { + if let Some(group_id) = t.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + } + } + + // validate session_data_tags + if let Some(session_data_tags) = &self.session_data_tags { + let mut set = HashSet::new(); + + for t in session_data_tags { + if !set.insert((t.data_id(), t.language())) { + return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + } + } + } + + // validate session_key_tags + if let Some(session_key_tags) = &self.session_key_tags { + let mut set = HashSet::new(); + for t in session_key_tags { + if !set.insert(t.key()) { + return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + } + } + } + + Ok(()) + } + + fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { + if let Some(media_tags) = &self.media_tags { + media_tags + .iter() + .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) + } else { + false + } + } +} + impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -295,7 +198,14 @@ impl FromStr for MasterPlaylist { type Err = Error; fn from_str(input: &str) -> Result { - let mut builder = MasterPlaylistBuilder::new(); + let mut builder = MasterPlaylist::builder(); + + let mut media_tags = vec![]; + let mut stream_inf_tags = vec![]; + let mut i_frame_stream_inf_tags = vec![]; + let mut session_data_tags = vec![]; + let mut session_key_tags = vec![]; + for (i, line) in input.parse::()?.into_iter().enumerate() { match line { Line::Tag(tag) => { @@ -310,10 +220,7 @@ impl FromStr for MasterPlaylist { return Err(Error::invalid_input()); } Tag::ExtXVersion(t) => { - if builder.version.is_some() { - return Err(Error::invalid_input()); - } - builder.version(t.version()); + builder.version_tag(t.version()); } Tag::ExtInf(_) | Tag::ExtXByteRange(_) @@ -331,31 +238,25 @@ impl FromStr for MasterPlaylist { return Err(Error::invalid_input()); // TODO: why? } Tag::ExtXMedia(t) => { - builder.tag(t); + media_tags.push(t); } Tag::ExtXStreamInf(t) => { - builder.tag(t); + stream_inf_tags.push(t); } Tag::ExtXIFrameStreamInf(t) => { - builder.tag(t); + i_frame_stream_inf_tags.push(t); } Tag::ExtXSessionData(t) => { - builder.tag(t); + session_data_tags.push(t); } Tag::ExtXSessionKey(t) => { - builder.tag(t); + session_key_tags.push(t); } Tag::ExtXIndependentSegments(t) => { - if builder.independent_segments_tag.is_some() { - return Err(Error::invalid_input()); - } - builder.tag(t); + builder.independent_segments_tag(t); } Tag::ExtXStart(t) => { - if builder.start_tag.is_some() { - return Err(Error::invalid_input()); - } - builder.tag(t); + builder.start_tag(t); } Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] @@ -369,7 +270,14 @@ impl FromStr for MasterPlaylist { } } } - builder.finish() + + builder.media_tags(media_tags); + builder.stream_inf_tags(stream_inf_tags); + builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags); + builder.session_data_tags(session_data_tags); + builder.session_key_tags(session_key_tags); + + builder.build().map_err(Error::builder_error) } } @@ -379,20 +287,37 @@ mod tests { #[test] fn test_parser() { - let playlist = r#" - #EXTM3U - #EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/low/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/lo_mid/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/hi_mid/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/high/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" - http://example.com/audio/index.m3u8 - "# + r#"#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/low/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/lo_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/hi_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/high/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" +http://example.com/audio/index.m3u8 +"# .parse::() .unwrap(); } + + #[test] + fn test_display() { + let input = r#"#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/low/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/lo_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/hi_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/high/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" +http://example.com/audio/index.m3u8 +"#; + let playlist = input.parse::().unwrap(); + assert_eq!(playlist.to_string(), input); + } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index d81db3f..5f196db 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -36,6 +36,18 @@ impl fmt::Display for ExtXVersion { } } +impl Default for ExtXVersion { + fn default() -> Self { + Self(ProtocolVersion::V1) + } +} + +impl From for ExtXVersion { + fn from(value: ProtocolVersion) -> Self { + Self(value) + } +} + impl FromStr for ExtXVersion { type Err = Error; diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index a172da9..b6aed5f 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -115,12 +115,12 @@ impl fmt::Display for ExtXStreamInf { if let Some(ref x) = self.average_bandwidth { write!(f, ",AVERAGE-BANDWIDTH={}", x)?; } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", quote(x))?; - } if let Some(ref x) = self.resolution { write!(f, ",RESOLUTION={}", x)?; } + if let Some(ref x) = self.codecs { + write!(f, ",CODECS={}", quote(x))?; + } if let Some(ref x) = self.frame_rate { write!(f, ",FRAME-RATE={:.3}", x.as_f64())?; }