From a797e401ed37aa2482536fb12d139c4e93a4e673 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Mar 2020 15:58:43 +0100 Subject: [PATCH] improve MasterPlaylist --- src/master_playlist.rs | 476 ++++++++++++++++++++++++++++++----------- 1 file changed, 349 insertions(+), 127 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 7e6b88a..272a5b7 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -3,7 +3,6 @@ use std::fmt; use std::str::FromStr; use derive_builder::Builder; -use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::tags::{ @@ -16,37 +15,112 @@ use crate::{Error, RequiredVersion}; /// The master playlist describes all of the available variants for your /// content. +/// /// Each variant is a version of the stream at a particular bitrate and is -/// contained in a separate playlist. -#[derive(ShortHand, Debug, Clone, Builder, PartialEq)] +/// contained in a separate playlist called [`MediaPlaylist`]. +/// +/// # Examples +/// +/// A [`MasterPlaylist`] can be parsed from a `str`: +/// +/// ``` +/// use core::str::FromStr; +/// use hls_m3u8::MasterPlaylist; +/// +/// // the concat! macro joins multiple `&'static str`. +/// let master_playlist = concat!( +/// "#EXTM3U\n", +/// "#EXT-X-STREAM-INF:", +/// "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n", +/// "http://example.com/low/index.m3u8\n", +/// "#EXT-X-STREAM-INF:", +/// "BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n", +/// "http://example.com/lo_mid/index.m3u8\n", +/// "#EXT-X-STREAM-INF:", +/// "BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n", +/// "http://example.com/hi_mid/index.m3u8\n", +/// "#EXT-X-STREAM-INF:", +/// "BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n", +/// "http://example.com/high/index.m3u8\n", +/// "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n", +/// "http://example.com/audio/index.m3u8\n" +/// ) +/// .parse::()?; +/// +/// println!("{}", master_playlist.has_independent_segments); +/// # Ok::<(), hls_m3u8::Error>(()) +/// ``` +/// +/// or it can be constructed through a builder +/// +/// ``` +/// # use hls_m3u8::MasterPlaylist; +/// use hls_m3u8::tags::{ExtXStart, VariantStream}; +/// use hls_m3u8::types::{Float, StreamData}; +/// +/// MasterPlaylist::builder() +/// .variant_streams(vec![ +/// VariantStream::ExtXStreamInf { +/// uri: "http://example.com/low/index.m3u8".into(), +/// frame_rate: None, +/// audio: None, +/// subtitles: None, +/// closed_captions: None, +/// stream_data: StreamData::builder() +/// .bandwidth(150000) +/// .codecs(&["avc1.42e00a", "mp4a.40.2"]) +/// .resolution((416, 234)) +/// .build() +/// .unwrap(), +/// }, +/// VariantStream::ExtXStreamInf { +/// uri: "http://example.com/lo_mid/index.m3u8".into(), +/// frame_rate: None, +/// audio: None, +/// subtitles: None, +/// closed_captions: None, +/// stream_data: StreamData::builder() +/// .bandwidth(240000) +/// .codecs(&["avc1.42e00a", "mp4a.40.2"]) +/// .resolution((416, 234)) +/// .build() +/// .unwrap(), +/// }, +/// ]) +/// .has_independent_segments(true) +/// .start(ExtXStart::new(Float::new(1.23))) +/// .build()?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// [`MediaPlaylist`]: crate::MediaPlaylist +#[derive(Debug, Clone, Builder, PartialEq, Default)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] -#[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MasterPlaylist { - /// The [`ExtXIndependentSegments`] tag signals that all media samples in a - /// [`MediaSegment`] can be decoded without information from other segments. + /// Indicates that all media samples in a [`MediaSegment`] can be + /// decoded without information from other segments. /// - /// # Note + /// ### Note /// - /// This tag is optional. - /// - /// If this tag is specified it will apply to every [`MediaSegment`] in - /// every [`MediaPlaylist`] in the [`MasterPlaylist`]. + /// This field is optional and by default `false`. If the field is `true`, + /// it applies to every [`MediaSegment`] in every [`MediaPlaylist`] of this + /// [`MasterPlaylist`]. /// /// [`MediaSegment`]: crate::MediaSegment /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - independent_segments: Option, - /// The [`ExtXStart`] tag indicates a preferred point at which to start - /// playing a Playlist. + pub has_independent_segments: bool, + /// A preferred point at which to start playing a playlist. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional and by default the playlist should be played from + /// the start. #[builder(default)] - start: Option, - /// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, - /// that contain alternative renditions of the same content. + pub start: Option, + /// A list of all [`ExtXMedia`] tags, which describe an alternative + /// rendition. /// /// For example, three [`ExtXMedia`] tags can be used to identify audio-only /// [`MediaPlaylist`]s, that contain English, French, and Spanish @@ -54,76 +128,165 @@ pub struct MasterPlaylist { /// be used to identify video-only [`MediaPlaylist`]s that show two /// different camera angles. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. /// /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - media: Vec, + pub media: Vec, /// A list of all streams of this [`MasterPlaylist`]. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. #[builder(default)] - variants: Vec, + pub variant_streams: Vec, /// The [`ExtXSessionData`] tag allows arbitrary session data to be /// carried in a [`MasterPlaylist`]. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. #[builder(default)] - session_data: Vec, - /// This is a list of [`ExtXSessionKey`]s, that allows the client to preload + pub session_data: Vec, + /// A list of [`ExtXSessionKey`]s, that allows the client to preload /// these keys without having to read the [`MediaPlaylist`]s first. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. /// /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - session_keys: Vec, - /// This is a list of all tags that could not be identified while parsing - /// the input. + pub session_keys: Vec, + /// A list of all tags that could not be identified while parsing the input. /// - /// # Note + /// ### Note /// - /// This tag is optional. + /// This field is optional. #[builder(default)] - unknown_tags: Vec, + pub unknown_tags: Vec, + #[builder(default, field(private))] + __non_exhaustive: (), } impl MasterPlaylist { - // TODO: finish builder example! /// Returns a builder for a [`MasterPlaylist`]. /// /// # Example /// /// ``` /// # use hls_m3u8::MasterPlaylist; - /// use hls_m3u8::tags::ExtXStart; - /// use hls_m3u8::types::Float; + /// use hls_m3u8::tags::{ExtXStart, VariantStream}; + /// use hls_m3u8::types::{Float, StreamData}; /// /// MasterPlaylist::builder() - /// .start(ExtXStart::new(Float::new(20.3))) + /// .variant_streams(vec![ + /// VariantStream::ExtXStreamInf { + /// uri: "http://example.com/low/index.m3u8".into(), + /// frame_rate: None, + /// audio: None, + /// subtitles: None, + /// closed_captions: None, + /// stream_data: StreamData::builder() + /// .bandwidth(150000) + /// .codecs(&["avc1.42e00a", "mp4a.40.2"]) + /// .resolution((416, 234)) + /// .build() + /// .unwrap(), + /// }, + /// VariantStream::ExtXStreamInf { + /// uri: "http://example.com/lo_mid/index.m3u8".into(), + /// frame_rate: None, + /// audio: None, + /// subtitles: None, + /// closed_captions: None, + /// stream_data: StreamData::builder() + /// .bandwidth(240000) + /// .codecs(&["avc1.42e00a", "mp4a.40.2"]) + /// .resolution((416, 234)) + /// .build() + /// .unwrap(), + /// }, + /// ]) + /// .has_independent_segments(true) + /// .start(ExtXStart::new(Float::new(1.23))) /// .build()?; /// # Ok::<(), Box>(()) /// ``` #[must_use] #[inline] pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } + + /// Returns all streams, which have an audio group id. + pub fn audio_streams(&self) -> impl Iterator { + self.variant_streams.iter().filter(|stream| { + if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream { + true + } else { + false + } + }) + } + + /// Returns all streams, which have a video group id. + pub fn video_streams(&self) -> impl Iterator { + self.variant_streams.iter().filter(|stream| { + if let VariantStream::ExtXStreamInf { stream_data, .. } = stream { + stream_data.video().is_some() + } else if let VariantStream::ExtXIFrame { stream_data, .. } = stream { + stream_data.video().is_some() + } else { + false + } + }) + } + + /// Returns all streams, which have no group id. + pub fn unassociated_streams(&self) -> impl Iterator { + self.variant_streams.iter().filter(|stream| { + if let VariantStream::ExtXStreamInf { + stream_data, + audio: None, + subtitles: None, + closed_captions: None, + .. + } = stream + { + stream_data.video().is_none() + } else if let VariantStream::ExtXIFrame { stream_data, .. } = stream { + stream_data.video().is_none() + } else { + false + } + }) + } + + /// Returns all `ExtXMedia` tags, associated with the provided stream. + pub fn associated_with<'a>( + &'a self, + stream: &'a VariantStream, + ) -> impl Iterator + 'a { + self.media + .iter() + .filter(move |media| stream.is_associated(media)) + } } impl RequiredVersion for MasterPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ - self.independent_segments, + { + if self.has_independent_segments { + Some(ExtXIndependentSegments) + } else { + None + } + }, self.start, self.media, - self.variants, + self.variant_streams, self.session_data, self.session_keys ] @@ -132,82 +295,72 @@ impl RequiredVersion for MasterPlaylist { impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { - self.validate_variants().map_err(|e| e.to_string())?; + if let Some(variant_streams) = &self.variant_streams { + self.validate_variants(variant_streams) + .map_err(|e| e.to_string())?; + } + self.validate_session_data_tags() .map_err(|e| e.to_string())?; Ok(()) } - fn validate_variants(&self) -> crate::Result<()> { - if let Some(variants) = &self.variants { - self.validate_stream_inf(variants)?; - self.validate_i_frame_stream_inf(variants)?; - } - - Ok(()) - } - - fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { + fn validate_variants(&self, variant_streams: &[VariantStream]) -> crate::Result<()> { let mut closed_captions_none = false; - for t in value { - if let VariantStream::ExtXStreamInf { - audio, - subtitles, - closed_captions, - stream_data, - .. - } = &t - { - if let Some(group_id) = &audio { - if !self.check_media_group(MediaType::Audio, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = &stream_data.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = &subtitles { - if !self.check_media_group(MediaType::Subtitles, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - - if let Some(closed_captions) = &closed_captions { - match &closed_captions { - ClosedCaptions::GroupId(group_id) => { - if closed_captions_none { - return Err(Error::custom( - "If one ClosedCaptions is None all have to be None!", - )); - } - - if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id)); - } + for variant in variant_streams { + match &variant { + VariantStream::ExtXStreamInf { + audio, + subtitles, + closed_captions, + stream_data, + .. + } => { + if let Some(group_id) = &audio { + if !self.check_media_group(MediaType::Audio, group_id) { + return Err(Error::unmatched_group(group_id)); } - _ => { - if !closed_captions_none { - closed_captions_none = true; + } + + if let Some(group_id) = &stream_data.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + + if let Some(group_id) = &subtitles { + if !self.check_media_group(MediaType::Subtitles, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + + if let Some(closed_captions) = &closed_captions { + match &closed_captions { + ClosedCaptions::GroupId(group_id) => { + if closed_captions_none { + return Err(Error::custom("ClosedCaptions has to be `None`")); + } + + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + _ => { + if !closed_captions_none { + closed_captions_none = true; + } } } } } - } - } - Ok(()) - } - - fn validate_i_frame_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { - for t in value { - if let VariantStream::ExtXIFrame { stream_data, .. } = &t { - if let Some(group_id) = stream_data.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); + VariantStream::ExtXIFrame { stream_data, .. } => { + if let Some(group_id) = stream_data.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } } } } @@ -219,12 +372,12 @@ impl MasterPlaylistBuilder { fn validate_session_data_tags(&self) -> crate::Result<()> { let mut set = HashSet::new(); - if let Some(value) = &self.session_data { - set.reserve(value.len()); + if let Some(values) = &self.session_data { + set.reserve(values.len()); - for t in value { - if !set.insert((t.data_id(), t.language())) { - return Err(Error::custom(format!("Conflict: {}", t))); + for tag in values { + if !set.insert((tag.data_id(), tag.language())) { + return Err(Error::custom(format!("conflict: {}", tag))); } } } @@ -250,10 +403,16 @@ impl RequiredVersion for MasterPlaylistBuilder { // not for Option>) // https://github.com/rust-lang/chalk/issues/12 required_version![ - self.independent_segments.flatten(), + { + if self.has_independent_segments.unwrap_or(false) { + Some(ExtXIndependentSegments) + } else { + None + } + }, self.start.flatten(), self.media, - self.variants, + self.variant_streams, self.session_data, self.session_keys ] @@ -272,7 +431,7 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", value)?; } - for value in &self.variants { + for value in &self.variant_streams { writeln!(f, "{}", value)?; } @@ -284,8 +443,8 @@ impl fmt::Display for MasterPlaylist { writeln!(f, "{}", value)?; } - if let Some(value) = &self.independent_segments { - writeln!(f, "{}", value)?; + if self.has_independent_segments { + writeln!(f, "{}", ExtXIndependentSegments)?; } if let Some(value) = &self.start { @@ -308,7 +467,7 @@ impl FromStr for MasterPlaylist { let mut builder = Self::builder(); let mut media = vec![]; - let mut variants = vec![]; + let mut variant_streams = vec![]; let mut session_data = vec![]; let mut session_keys = vec![]; let mut unknown_tags = vec![]; @@ -336,16 +495,13 @@ impl FromStr for MasterPlaylist { | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { - return Err(Error::custom(format!( - "This tag isn't allowed in a master playlist: {}", - tag - ))); + return Err(Error::unexpected_tag(tag)); } Tag::ExtXMedia(t) => { media.push(t); } Tag::VariantStream(t) => { - variants.push(t); + variant_streams.push(t); } Tag::ExtXSessionData(t) => { session_data.push(t); @@ -353,8 +509,8 @@ impl FromStr for MasterPlaylist { Tag::ExtXSessionKey(t) => { session_keys.push(t); } - Tag::ExtXIndependentSegments(t) => { - builder.independent_segments(t); + Tag::ExtXIndependentSegments(_) => { + builder.has_independent_segments(true); } Tag::ExtXStart(t) => { builder.start(t); @@ -367,14 +523,14 @@ impl FromStr for MasterPlaylist { } } Line::Uri(uri) => { - return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); + return Err(Error::custom(format!("unexpected uri: {:?}", uri))); } _ => {} } } builder.media(media); - builder.variants(variants); + builder.variant_streams(variant_streams); builder.session_data(session_data); builder.session_keys(session_keys); builder.unknown_tags(unknown_tags); @@ -389,6 +545,72 @@ mod tests { use crate::types::StreamData; use pretty_assertions::assert_eq; + #[test] + fn test_audio_streams() { + let astreams = vec![ + VariantStream::ExtXStreamInf { + uri: "http://example.com/low/index.m3u8".into(), + frame_rate: None, + audio: Some("ag0".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(150000) + .codecs(&["avc1.42e00a", "mp4a.40.2"]) + .resolution((416, 234)) + .build() + .unwrap(), + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/lo_mid/index.m3u8".into(), + frame_rate: None, + audio: Some("ag1".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(240000) + .codecs(&["avc1.42e00a", "mp4a.40.2"]) + .resolution((416, 234)) + .build() + .unwrap(), + }, + ]; + + let master_playlist = MasterPlaylist::builder() + .variant_streams(astreams.clone()) + .media(vec![ + ExtXMedia::builder() + .media_type(MediaType::Audio) + .uri("https://www.example.com/ag0.m3u8") + .group_id("ag0") + .language("english") + .name("alternative rendition for ag0") + .build() + .unwrap(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .uri("https://www.example.com/ag1.m3u8") + .group_id("ag1") + .language("english") + .name("alternative rendition for ag1") + .build() + .unwrap(), + ]) + .build() + .unwrap(); + + assert_eq!( + master_playlist.variant_streams, + master_playlist.audio_streams().collect::>() + ); + + let mut audio_streams = master_playlist.audio_streams(); + + assert_eq!(audio_streams.next(), Some(&astreams[0])); + assert_eq!(audio_streams.next(), Some(&astreams[1])); + assert_eq!(audio_streams.next(), None); + } + #[test] fn test_parser() { assert_eq!( @@ -412,7 +634,7 @@ mod tests { .parse::() .unwrap(), MasterPlaylist::builder() - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "http://example.com/low/index.m3u8".into(), frame_rate: None, @@ -487,7 +709,7 @@ mod tests { fn test_display() { assert_eq!( MasterPlaylist::builder() - .variants(vec![ + .variant_streams(vec![ VariantStream::ExtXStreamInf { uri: "http://example.com/low/index.m3u8".into(), frame_rate: None,