diff --git a/src/line.rs b/src/line.rs index 299f5be..c6ba9d2 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,65 +1,48 @@ -use std::convert::TryFrom; -use std::fmt; -use std::str::FromStr; +use core::convert::TryFrom; +use core::fmt; +use core::str::FromStr; use crate::tags; use crate::Error; #[derive(Debug, Clone)] pub(crate) struct Lines<'a> { - lines: ::core::str::Lines<'a>, + lines: ::core::iter::FilterMap<::core::str::Lines<'a>, fn(&'a str) -> Option<&'a str>>, } impl<'a> Iterator for Lines<'a> { type Item = crate::Result>; fn next(&mut self) -> Option { - let mut stream_inf = false; - let mut stream_inf_line = None; + let line = self.lines.next()?; - while let Some(line) = self.lines.next() { - let line = line.trim(); + if line.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) { + let uri = self.lines.next()?; - if line.is_empty() { - continue; - } - - if line.starts_with(tags::ExtXStreamInf::PREFIX) { - stream_inf = true; - stream_inf_line = Some(line); - - continue; - } else if line.starts_with("#EXT") { - return Some(Tag::try_from(line).map(Line::Tag)); - } else if line.starts_with('#') { - continue; // ignore comments - } else { - // stream inf line needs special treatment - if stream_inf { - stream_inf = false; - - if let Some(first_line) = stream_inf_line { - return Some( - tags::ExtXStreamInf::from_str(&format!("{}\n{}", first_line, line)) - .map(|v| Line::Tag(Tag::ExtXStreamInf(v))), - ); - } else { - continue; - } - } else { - return Some(Ok(Line::Uri(line))); - } - } + Some( + tags::VariantStream::from_str(&format!("{}\n{}", line, uri)) + .map(|v| Line::Tag(Tag::VariantStream(v))), + ) + } else if line.starts_with("#EXT") { + Some(Tag::try_from(line).map(Line::Tag)) + } else if line.starts_with('#') { + Some(Ok(Line::Comment(line))) + } else { + Some(Ok(Line::Uri(line))) } - - None } } impl<'a> From<&'a str> for Lines<'a> { fn from(buffer: &'a str) -> Self { Self { - lines: buffer.lines(), + lines: buffer.lines().filter_map(|line| { + if line.trim().is_empty() { + None + } else { + Some(line.trim()) + } + }), } } } @@ -67,6 +50,7 @@ impl<'a> From<&'a str> for Lines<'a> { #[derive(Debug, Clone, PartialEq)] pub(crate) enum Line<'a> { Tag(Tag<'a>), + Comment(&'a str), Uri(&'a str), } @@ -88,12 +72,11 @@ pub(crate) enum Tag<'a> { ExtXPlaylistType(tags::ExtXPlaylistType), ExtXIFramesOnly(tags::ExtXIFramesOnly), ExtXMedia(tags::ExtXMedia), - ExtXStreamInf(tags::ExtXStreamInf), - ExtXIFrameStreamInf(tags::ExtXIFrameStreamInf), ExtXSessionData(tags::ExtXSessionData), ExtXSessionKey(tags::ExtXSessionKey), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), + VariantStream(tags::VariantStream), Unknown(&'a str), } @@ -115,8 +98,7 @@ impl<'a> fmt::Display for Tag<'a> { Self::ExtXPlaylistType(value) => value.fmt(f), Self::ExtXIFramesOnly(value) => value.fmt(f), Self::ExtXMedia(value) => value.fmt(f), - Self::ExtXStreamInf(value) => value.fmt(f), - Self::ExtXIFrameStreamInf(value) => value.fmt(f), + Self::VariantStream(value) => value.fmt(f), Self::ExtXSessionData(value) => value.fmt(f), Self::ExtXSessionKey(value) => value.fmt(f), Self::ExtXIndependentSegments(value) => value.fmt(f), @@ -160,10 +142,13 @@ impl<'a> TryFrom<&'a str> for Tag<'a> { input.parse().map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { input.parse().map(Self::ExtXMedia).map_err(Error::custom) - } else if input.starts_with(tags::ExtXStreamInf::PREFIX) { - input.parse().map(Self::ExtXStreamInf) - } else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - input.parse().map(Self::ExtXIFrameStreamInf) + } else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME) + || input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) + { + input + .parse() + .map(Self::VariantStream) + .map_err(Error::custom) } else if input.starts_with(tags::ExtXSessionData::PREFIX) { input.parse().map(Self::ExtXSessionData) } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index cb88465..f906667 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -7,68 +7,80 @@ use shorthand::ShortHand; use crate::line::{Line, Lines, Tag}; use crate::tags::{ - ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, - ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, + ExtM3u, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, + ExtXVersion, VariantStream, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// Master playlist. +/// The master playlist describes all of the available variants for your +/// content. Each variant is a version of the stream at a particular bitrate +/// and is contained in a separate playlist. #[derive(ShortHand, Debug, Clone, Builder, PartialEq)] #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] #[shorthand(enable(must_use, get_mut, collection_magic))] pub struct MasterPlaylist { - /// The [`ExtXIndependentSegments`] tag of the playlist. + /// The [`ExtXIndependentSegments`] tag signals that all media samples in a + /// [`MediaSegment`] can be decoded without information from other segments. + /// + /// # Note + /// + /// This tag is optional. + /// + /// If this tag is specified it will apply to every [`MediaSegment`] in + /// every [`MediaPlaylist`] in the [`MasterPlaylist`]. + /// + /// [`MediaSegment`]: crate::MediaSegment + /// [`MediaPlaylist`]: crate::MediaPlaylist + #[builder(default)] + independent_segments: Option, + /// The [`ExtXStart`] tag indicates a preferred point at which to start + /// playing a Playlist. /// /// # Note /// /// This tag is optional. #[builder(default)] - independent_segments_tag: Option, - /// The [`ExtXStart`] tag of the playlist. + start: Option, + /// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, + /// that contain alternative renditions of the same content. + /// + /// For example, three [`ExtXMedia`] tags can be used to identify audio-only + /// [`MediaPlaylist`]s, that contain English, French, and Spanish + /// renditions of the same presentation. Or, two [`ExtXMedia`] tags can + /// be used to identify video-only [`MediaPlaylist`]s that show two + /// different camera angles. + /// + /// # Note + /// + /// This tag is optional. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + #[builder(default)] + media: Vec, + /// A list of all streams of this [`MasterPlaylist`]. /// /// # Note /// /// This tag is optional. #[builder(default)] - start_tag: Option, - /// The [`ExtXMedia`] tags of the playlist. - /// - /// # Note - /// - /// This tag is optional. - #[builder(default)] - media_tags: Vec, - /// The [`ExtXStreamInf`] tags of the playlist. - /// - /// # Note - /// - /// This tag is optional. - #[builder(default)] - stream_inf_tags: Vec, - /// The [`ExtXIFrameStreamInf`] tags of the playlist. - /// - /// # Note - /// - /// This tag is optional. - #[builder(default)] - i_frame_stream_inf_tags: Vec, + variants: Vec, /// The [`ExtXSessionData`] tags of the playlist. /// /// # Note /// /// This tag is optional. #[builder(default)] - session_data_tags: Vec, + session_data: Vec, /// The [`ExtXSessionKey`] tags of the playlist. /// /// # Note /// /// This tag is optional. #[builder(default)] - session_key_tags: Vec, + session_keys: Vec, /// A list of tags that are unknown. /// /// # Note @@ -79,6 +91,7 @@ pub struct MasterPlaylist { } impl MasterPlaylist { + // TODO: finish builder example! /// Returns a builder for a [`MasterPlaylist`]. /// /// # Example @@ -88,7 +101,7 @@ impl MasterPlaylist { /// use hls_m3u8::MasterPlaylist; /// /// MasterPlaylist::builder() - /// .start_tag(ExtXStart::new(20.123456)) + /// .start(ExtXStart::new(20.123456)) /// .build()?; /// # Ok::<(), Box>(()) /// ``` @@ -98,101 +111,128 @@ impl MasterPlaylist { impl RequiredVersion for MasterPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ - self.independent_segments_tag, - self.start_tag, - self.media_tags, - self.stream_inf_tags, - self.i_frame_stream_inf_tags, - self.session_data_tags, - self.session_key_tags + self.independent_segments, + self.start, + self.media, + self.variants, + self.session_data, + self.session_keys ] } } impl MasterPlaylistBuilder { fn validate(&self) -> Result<(), String> { - self.validate_stream_inf_tags().map_err(|e| e.to_string())?; - self.validate_i_frame_stream_inf_tags() - .map_err(|e| e.to_string())?; + self.validate_variants().map_err(|e| e.to_string())?; self.validate_session_data_tags() .map_err(|e| e.to_string())?; Ok(()) } - fn validate_stream_inf_tags(&self) -> crate::Result<()> { - if let Some(value) = &self.stream_inf_tags { - let mut has_none_closed_captions = false; + fn validate_variants(&self) -> crate::Result<()> { + if let Some(variants) = &self.variants { + self.validate_stream_inf(variants)?; + self.validate_i_frame_stream_inf(variants)?; + } - for t in value { - if let Some(group_id) = t.audio() { + Ok(()) + } + + fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { + let mut has_none_closed_captions = false; + + for t in value { + if let VariantStream::ExtXStreamInf { + audio, + subtitles, + closed_captions, + stream_data, + .. + } = &t + { + if let Some(group_id) = &audio { if !self.check_media_group(MediaType::Audio, group_id) { return Err(Error::unmatched_group(group_id)); } } - if let Some(group_id) = t.video() { + if let Some(group_id) = &stream_data.video() { if !self.check_media_group(MediaType::Video, group_id) { return Err(Error::unmatched_group(group_id)); } } - if let Some(group_id) = t.subtitles() { + if let Some(group_id) = &subtitles { if !self.check_media_group(MediaType::Subtitles, group_id) { return Err(Error::unmatched_group(group_id)); } } - match &t.closed_captions() { - Some(ClosedCaptions::GroupId(group_id)) => { - if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id)); + + if let Some(closed_captions) = &closed_captions { + match &closed_captions { + ClosedCaptions::GroupId(group_id) => { + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + ClosedCaptions::None => { + has_none_closed_captions = true; } } - Some(ClosedCaptions::None) => { - has_none_closed_captions = true; - } - _ => {} } } - if has_none_closed_captions - && !value - .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) - { - return Err(Error::invalid_input()); - } } + + if has_none_closed_captions + && !value.iter().all(|t| { + if let VariantStream::ExtXStreamInf { + closed_captions, .. + } = &t + { + closed_captions == &Some(ClosedCaptions::None) + } else { + false + } + }) + { + return Err(Error::invalid_input()); + } + Ok(()) } - fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { - if let Some(value) = &self.i_frame_stream_inf_tags { - for t in value { - if let Some(group_id) = t.video() { + fn validate_i_frame_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { + for t in value { + if let VariantStream::ExtXIFrame { stream_data, .. } = &t { + if let Some(group_id) = stream_data.video() { if !self.check_media_group(MediaType::Video, group_id) { return Err(Error::unmatched_group(group_id)); } } } } + Ok(()) } fn validate_session_data_tags(&self) -> crate::Result<()> { let mut set = HashSet::new(); - if let Some(value) = &self.session_data_tags { + + if let Some(value) = &self.session_data { for t in value { if !set.insert((t.data_id(), t.language())) { return Err(Error::custom(format!("Conflict: {}", t))); } } } + Ok(()) } - fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { - if let Some(value) = &self.media_tags { + fn check_media_group>(&self, media_type: MediaType, group_id: T) -> bool { + if let Some(value) = &self.media { value .iter() - .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) + .any(|t| t.media_type() == media_type && t.group_id().as_str() == group_id.as_ref()) } else { false } @@ -206,13 +246,12 @@ impl RequiredVersion for MasterPlaylistBuilder { // not for Option>) // https://github.com/rust-lang/chalk/issues/12 required_version![ - self.independent_segments_tag.flatten(), - self.start_tag.flatten(), - self.media_tags, - self.stream_inf_tags, - self.i_frame_stream_inf_tags, - self.session_data_tags, - self.session_key_tags + self.independent_segments.flatten(), + self.start.flatten(), + self.media, + self.variants, + self.session_data, + self.session_keys ] } } @@ -220,35 +259,32 @@ impl RequiredVersion for MasterPlaylistBuilder { impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; + if self.required_version() != ProtocolVersion::V1 { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } - for t in &self.media_tags { + for t in &self.media { writeln!(f, "{}", t)?; } - for t in &self.stream_inf_tags { + for t in &self.variants { writeln!(f, "{}", t)?; } - for t in &self.i_frame_stream_inf_tags { + for t in &self.session_data { writeln!(f, "{}", t)?; } - for t in &self.session_data_tags { + for t in &self.session_keys { writeln!(f, "{}", t)?; } - for t in &self.session_key_tags { - writeln!(f, "{}", t)?; - } - - if let Some(value) = &self.independent_segments_tag { + if let Some(value) = &self.independent_segments { writeln!(f, "{}", value)?; } - if let Some(value) = &self.start_tag { + if let Some(value) = &self.start { writeln!(f, "{}", value)?; } @@ -267,11 +303,10 @@ impl FromStr for MasterPlaylist { let input = tag(input, ExtM3u::PREFIX)?; let mut builder = Self::builder(); - let mut media_tags = vec![]; - let mut stream_inf_tags = vec![]; - let mut i_frame_stream_inf_tags = vec![]; - let mut session_data_tags = vec![]; - let mut session_key_tags = vec![]; + let mut media = vec![]; + let mut variants = vec![]; + let mut session_data = vec![]; + let mut session_keys = vec![]; let mut unknown_tags = vec![]; for line in Lines::from(input) { @@ -303,25 +338,22 @@ impl FromStr for MasterPlaylist { ))); } Tag::ExtXMedia(t) => { - media_tags.push(t); + media.push(t); } - Tag::ExtXStreamInf(t) => { - stream_inf_tags.push(t); - } - Tag::ExtXIFrameStreamInf(t) => { - i_frame_stream_inf_tags.push(t); + Tag::VariantStream(t) => { + variants.push(t); } Tag::ExtXSessionData(t) => { - session_data_tags.push(t); + session_data.push(t); } Tag::ExtXSessionKey(t) => { - session_key_tags.push(t); + session_keys.push(t); } Tag::ExtXIndependentSegments(t) => { - builder.independent_segments_tag(t); + builder.independent_segments(t); } Tag::ExtXStart(t) => { - builder.start_tag(t); + builder.start(t); } _ => { // [6.3.1. General Client Responsibilities] @@ -333,14 +365,14 @@ impl FromStr for MasterPlaylist { Line::Uri(uri) => { return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); } + _ => {} } } - builder.media_tags(media_tags); - builder.stream_inf_tags(stream_inf_tags); - builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags); - builder.session_data_tags(session_data_tags); - builder.session_key_tags(session_key_tags); + builder.media(media); + builder.variants(variants); + builder.session_data(session_data); + builder.session_keys(session_keys); builder.unknown_tags(unknown_tags); builder.build().map_err(Error::builder) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index d43f5a7..ea1e57d 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -27,56 +27,56 @@ pub struct MediaPlaylist { /// /// This field is required. #[shorthand(enable(copy))] - target_duration_tag: ExtXTargetDuration, + target_duration: ExtXTargetDuration, /// Sets the [`ExtXMediaSequence`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - media_sequence_tag: Option, + media_sequence: Option, /// Sets the [`ExtXDiscontinuitySequence`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - discontinuity_sequence_tag: Option, + discontinuity_sequence: Option, /// Sets the [`ExtXPlaylistType`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - playlist_type_tag: Option, + playlist_type: Option, /// Sets the [`ExtXIFramesOnly`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - i_frames_only_tag: Option, + i_frames_only: Option, /// Sets the [`ExtXIndependentSegments`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - independent_segments_tag: Option, + independent_segments: Option, /// Sets the [`ExtXStart`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - start_tag: Option, + start: Option, /// Sets the [`ExtXEndList`] tag. /// /// # Note /// /// This field is optional. #[builder(default)] - end_list_tag: Option, + end_list: Option, /// A list of all [`MediaSegment`]s. /// /// # Note @@ -110,7 +110,7 @@ pub struct MediaPlaylist { impl MediaPlaylistBuilder { fn validate(&self) -> Result<(), String> { - if let Some(target_duration) = &self.target_duration_tag { + if let Some(target_duration) = &self.target_duration { self.validate_media_segments(target_duration.duration()) .map_err(|e| e.to_string())?; } @@ -189,14 +189,14 @@ impl MediaPlaylistBuilder { impl RequiredVersion for MediaPlaylistBuilder { fn required_version(&self) -> ProtocolVersion { required_version![ - self.target_duration_tag, - self.media_sequence_tag, - self.discontinuity_sequence_tag, - self.playlist_type_tag, - self.i_frames_only_tag, - self.independent_segments_tag, - self.start_tag, - self.end_list_tag, + self.target_duration, + self.media_sequence, + self.discontinuity_sequence, + self.playlist_type, + self.i_frames_only, + self.independent_segments, + self.start, + self.end_list, self.segments ] } @@ -210,14 +210,14 @@ impl MediaPlaylist { impl RequiredVersion for MediaPlaylist { fn required_version(&self) -> ProtocolVersion { required_version![ - self.target_duration_tag, - self.media_sequence_tag, - self.discontinuity_sequence_tag, - self.playlist_type_tag, - self.i_frames_only_tag, - self.independent_segments_tag, - self.start_tag, - self.end_list_tag, + self.target_duration, + self.media_sequence, + self.discontinuity_sequence, + self.playlist_type, + self.i_frames_only, + self.independent_segments, + self.start, + self.end_list, self.segments ] } @@ -231,29 +231,29 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; } - writeln!(f, "{}", self.target_duration_tag)?; + writeln!(f, "{}", self.target_duration)?; - if let Some(value) = &self.media_sequence_tag { + if let Some(value) = &self.media_sequence { writeln!(f, "{}", value)?; } - if let Some(value) = &self.discontinuity_sequence_tag { + if let Some(value) = &self.discontinuity_sequence { writeln!(f, "{}", value)?; } - if let Some(value) = &self.playlist_type_tag { + if let Some(value) = &self.playlist_type { writeln!(f, "{}", value)?; } - if let Some(value) = &self.i_frames_only_tag { + if let Some(value) = &self.i_frames_only { writeln!(f, "{}", value)?; } - if let Some(value) = &self.independent_segments_tag { + if let Some(value) = &self.independent_segments { writeln!(f, "{}", value)?; } - if let Some(value) = &self.start_tag { + if let Some(value) = &self.start { writeln!(f, "{}", value)?; } @@ -261,7 +261,7 @@ impl fmt::Display for MediaPlaylist { write!(f, "{}", segment)?; } - if let Some(value) = &self.end_list_tag { + if let Some(value) = &self.end_list { writeln!(f, "{}", value)?; } @@ -341,10 +341,10 @@ fn parse_media_playlist( segment.date_range_tag(t); } Tag::ExtXTargetDuration(t) => { - builder.target_duration_tag(t); + builder.target_duration(t); } Tag::ExtXMediaSequence(t) => { - builder.media_sequence_tag(t); + builder.media_sequence(t); } Tag::ExtXDiscontinuitySequence(t) => { if segments.is_empty() { @@ -353,29 +353,28 @@ fn parse_media_playlist( if has_discontinuity_tag { return Err(Error::invalid_input()); } - builder.discontinuity_sequence_tag(t); + builder.discontinuity_sequence(t); } Tag::ExtXEndList(t) => { - builder.end_list_tag(t); + builder.end_list(t); } Tag::ExtXPlaylistType(t) => { - builder.playlist_type_tag(t); + builder.playlist_type(t); } Tag::ExtXIFramesOnly(t) => { - builder.i_frames_only_tag(t); + builder.i_frames_only(t); } Tag::ExtXMedia(_) - | Tag::ExtXStreamInf(_) - | Tag::ExtXIFrameStreamInf(_) + | Tag::VariantStream(_) | Tag::ExtXSessionData(_) | Tag::ExtXSessionKey(_) => { return Err(Error::unexpected_tag(tag)); } Tag::ExtXIndependentSegments(t) => { - builder.independent_segments_tag(t); + builder.independent_segments(t); } Tag::ExtXStart(t) => { - builder.start_tag(t); + builder.start(t); } Tag::ExtXVersion(_) => {} Tag::Unknown(_) => { diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs deleted file mode 100644 index 0343564..0000000 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ /dev/null @@ -1,263 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_more::{Deref, DerefMut}; - -use crate::attribute::AttributePairs; -use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder}; -use crate::utils::{quote, tag, unquote}; -use crate::{Error, RequiredVersion}; - -/// # [4.3.5.3. EXT-X-I-FRAME-STREAM-INF] -/// -/// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file, -/// containing the I-frames of a multimedia presentation. -/// -/// I-frames are encoded video frames, whose decoding -/// does not depend on any other frame. -/// -/// [`Master Playlist`]: crate::MasterPlaylist -/// [`Media Playlist`]: crate::MediaPlaylist -/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 -#[derive(Deref, DerefMut, PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXIFrameStreamInf { - uri: String, - #[deref] - #[deref_mut] - stream_inf: StreamInf, -} - -/// Builder for [`ExtXIFrameStreamInf`]. -#[derive(Default, Debug, Clone, PartialEq)] -pub struct ExtXIFrameStreamInfBuilder { - uri: Option, - stream_inf: StreamInfBuilder, -} - -impl ExtXIFrameStreamInfBuilder { - /// An `URI` to the [`MediaPlaylist`] file. - /// - /// [`MediaPlaylist`]: crate::MediaPlaylist - pub fn uri>(&mut self, value: T) -> &mut Self { - self.uri = Some(value.into()); - self - } - - /// The maximum bandwidth of the stream. - pub fn bandwidth(&mut self, value: u64) -> &mut Self { - self.stream_inf.bandwidth(value); - self - } - - /// The average bandwidth of the stream. - pub fn average_bandwidth(&mut self, value: u64) -> &mut Self { - self.stream_inf.average_bandwidth(value); - self - } - - /// Every media format in any of the renditions specified by the Variant - /// Stream. - pub fn codecs>(&mut self, value: T) -> &mut Self { - self.stream_inf.codecs(value); - self - } - - /// The resolution of the stream. - pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self { - self.stream_inf.resolution(value); - self - } - - /// High-bandwidth Digital Content Protection - pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self { - self.stream_inf.hdcp_level(value); - self - } - - /// It indicates the set of video renditions, that should be used when - /// playing the presentation. - pub fn video>(&mut self, value: T) -> &mut Self { - self.stream_inf.video(value); - self - } - - /// Build an [`ExtXIFrameStreamInf`]. - pub fn build(&self) -> crate::Result { - Ok(ExtXIFrameStreamInf { - uri: self - .uri - .clone() - .ok_or_else(|| Error::missing_value("frame rate"))?, - stream_inf: self.stream_inf.build().map_err(Error::builder)?, - }) - } -} - -impl ExtXIFrameStreamInf { - pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; - - /// Makes a new [`ExtXIFrameStreamInf`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); - /// ``` - pub fn new(uri: T, bandwidth: u64) -> Self { - Self { - uri: uri.to_string(), - stream_inf: StreamInf::new(bandwidth), - } - } - - /// Returns a builder for [`ExtXIFrameStreamInf`]. - pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() } - - /// Returns the `URI`, that identifies the associated [`media playlist`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); - /// assert_eq!(stream.uri(), &"https://www.example.com".to_string()); - /// ``` - /// - /// [`media playlist`]: crate::MediaPlaylist - pub const fn uri(&self) -> &String { &self.uri } - - /// Sets the `URI`, that identifies the associated [`media playlist`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// # - /// let mut stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); - /// - /// stream.set_uri("../new/uri"); - /// assert_eq!(stream.uri(), &"../new/uri".to_string()); - /// ``` - /// - /// [`media playlist`]: crate::MediaPlaylist - pub fn set_uri(&mut self, value: T) -> &mut Self { - self.uri = value.to_string(); - self - } -} - -/// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXIFrameStreamInf { - fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } -} - -impl fmt::Display for ExtXIFrameStreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={},{}", quote(&self.uri), self.stream_inf)?; - Ok(()) - } -} - -impl FromStr for ExtXIFrameStreamInf { - type Err = Error; - - fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - - let mut uri = None; - - for (key, value) in AttributePairs::new(input) { - if key == "URI" { - uri = Some(unquote(value)); - } - } - - let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; - - Ok(Self { - uri, - stream_inf: input.parse()?, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_builder() { - let mut i_frame_stream_inf = - ExtXIFrameStreamInf::new("http://example.com/audio-only.m3u8", 200_000); - - i_frame_stream_inf - .set_average_bandwidth(Some(100_000)) - .set_codecs(Some("mp4a.40.5")) - .set_resolution(Some((1920, 1080))) - .set_hdcp_level(Some(HdcpLevel::None)) - .set_video(Some("video")); - - assert_eq!( - ExtXIFrameStreamInf::builder() - .uri("http://example.com/audio-only.m3u8") - .bandwidth(200_000) - .average_bandwidth(100_000) - .codecs("mp4a.40.5") - .resolution((1920, 1080)) - .hdcp_level(HdcpLevel::None) - .video("video") - .build() - .unwrap(), - i_frame_stream_inf - ); - } - - #[test] - fn test_display() { - assert_eq!( - ExtXIFrameStreamInf::new("foo", 1000).to_string(), - "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000" - .parse::() - .unwrap(), - ExtXIFrameStreamInf::new("foo", 1000) - ); - - assert!("garbage".parse::().is_err()); - } - - #[test] - fn test_required_version() { - assert_eq!( - ExtXIFrameStreamInf::new("foo", 1000).required_version(), - ProtocolVersion::V1 - ); - } - - #[test] - fn test_deref() { - assert_eq!( - ExtXIFrameStreamInf::new("https://www.example.com", 20).average_bandwidth(), - None - ) - } - - #[test] - fn test_deref_mut() { - assert_eq!( - ExtXIFrameStreamInf::new("https://www.example.com", 20) - .set_average_bandwidth(Some(4)) - .average_bandwidth(), - Some(4) - ) - } -} diff --git a/src/tags/master_playlist/mod.rs b/src/tags/master_playlist/mod.rs index 293509a..b95f91c 100644 --- a/src/tags/master_playlist/mod.rs +++ b/src/tags/master_playlist/mod.rs @@ -1,11 +1,9 @@ -mod i_frame_stream_inf; mod media; mod session_data; mod session_key; -mod stream_inf; +mod variant_stream; -pub use i_frame_stream_inf::*; pub use media::*; pub use session_data::*; pub use session_key::*; -pub use stream_inf::*; +pub use variant_stream::*; diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs deleted file mode 100644 index fa51342..0000000 --- a/src/tags/master_playlist/stream_inf.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_more::{Deref, DerefMut}; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{ - ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder, -}; -use crate::utils::{quote, tag, unquote}; -use crate::{Error, RequiredVersion}; - -/// # [4.3.4.2. EXT-X-STREAM-INF] -/// -/// The [`ExtXStreamInf`] tag specifies a Variant Stream, which is a set -/// of Renditions that can be combined to play the presentation. The -/// attributes of the tag provide information about the Variant Stream. -/// -/// The URI line that follows the [`ExtXStreamInf`] tag specifies a Media -/// Playlist that carries a rendition of the Variant Stream. The URI -/// line is REQUIRED. Clients that do not support multiple video -/// Renditions SHOULD play this Rendition. -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(Deref, DerefMut, ShortHand, PartialOrd, Debug, Clone, PartialEq)] -#[shorthand(enable(must_use, into))] -pub struct ExtXStreamInf { - /// The `URI` that identifies the associated media playlist. - uri: String, - #[shorthand(enable(skip))] - frame_rate: Option, - /// The group identifier for the audio in the variant stream. - audio: Option, - /// The group identifier for the subtitles in the variant stream. - subtitles: Option, - /// The value of the [`ClosedCaptions`] attribute. - closed_captions: Option, - #[shorthand(enable(skip))] - #[deref] - #[deref_mut] - stream_inf: StreamInf, -} - -#[derive(Default, Debug, Clone)] -/// Builder for [`ExtXStreamInf`]. -pub struct ExtXStreamInfBuilder { - uri: Option, - frame_rate: Option, - audio: Option, - subtitles: Option, - closed_captions: Option, - stream_inf: StreamInfBuilder, -} - -impl ExtXStreamInfBuilder { - /// An `URI` to the [`MediaPlaylist`] file. - /// - /// [`MediaPlaylist`]: crate::MediaPlaylist - pub fn uri>(&mut self, value: T) -> &mut Self { - self.uri = Some(value.into()); - self - } - - /// Maximum frame rate for all the video in the variant stream. - pub fn frame_rate(&mut self, value: f64) -> &mut Self { - self.frame_rate = Some(value.into()); - self - } - - /// The group identifier for the audio in the variant stream. - pub fn audio>(&mut self, value: T) -> &mut Self { - self.audio = Some(value.into()); - self - } - - /// The group identifier for the subtitles in the variant stream. - pub fn subtitles>(&mut self, value: T) -> &mut Self { - self.subtitles = Some(value.into()); - self - } - - /// The value of [`ClosedCaptions`] attribute. - pub fn closed_captions>(&mut self, value: T) -> &mut Self { - self.closed_captions = Some(value.into()); - self - } - - /// The maximum bandwidth of the stream. - pub fn bandwidth(&mut self, value: u64) -> &mut Self { - self.stream_inf.bandwidth(value); - self - } - - /// The average bandwidth of the stream. - pub fn average_bandwidth(&mut self, value: u64) -> &mut Self { - self.stream_inf.average_bandwidth(value); - self - } - - /// Every media format in any of the renditions specified by the Variant - /// Stream. - pub fn codecs>(&mut self, value: T) -> &mut Self { - self.stream_inf.codecs(value); - self - } - - /// The resolution of the stream. - pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self { - self.stream_inf.resolution(value); - self - } - - /// High-bandwidth Digital Content Protection - pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self { - self.stream_inf.hdcp_level(value); - self - } - - /// It indicates the set of video renditions, that should be used when - /// playing the presentation. - pub fn video>(&mut self, value: T) -> &mut Self { - self.stream_inf.video(value); - self - } - - /// Build an [`ExtXStreamInf`]. - pub fn build(&self) -> crate::Result { - Ok(ExtXStreamInf { - uri: self - .uri - .clone() - .ok_or_else(|| Error::missing_value("frame rate"))?, - frame_rate: self.frame_rate, - audio: self.audio.clone(), - subtitles: self.subtitles.clone(), - closed_captions: self.closed_captions.clone(), - stream_inf: self.stream_inf.build().map_err(Error::builder)?, - }) - } -} - -impl ExtXStreamInf { - pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; - - /// Creates a new [`ExtXStreamInf`] tag. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXStreamInf; - /// let stream = ExtXStreamInf::new("https://www.example.com/", 20); - /// ``` - pub fn new(uri: T, bandwidth: u64) -> Self { - Self { - uri: uri.to_string(), - frame_rate: None, - audio: None, - subtitles: None, - closed_captions: None, - stream_inf: StreamInf::new(bandwidth), - } - } - - /// Returns a builder for [`ExtXStreamInf`]. - pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() } - - /// The maximum frame rate for all the video in the variant stream. - #[must_use] - #[inline] - pub fn frame_rate(&self) -> Option { self.frame_rate.map(DecimalFloatingPoint::as_f64) } - - /// The maximum frame rate for all the video in the variant stream. - /// - /// # Panic - /// - /// This function panics, if the float is infinite or negative. - pub fn set_frame_rate>(&mut self, value_0: Option) -> &mut Self { - self.frame_rate = value_0.map(|v| { - DecimalFloatingPoint::new(v.into()).expect("the float must be positive and finite") - }); - self - } -} - -/// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXStreamInf { - fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } -} - -impl fmt::Display for ExtXStreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.stream_inf)?; - - if let Some(value) = &self.frame_rate { - write!(f, ",FRAME-RATE={:.3}", value.as_f64())?; - } - - if let Some(value) = &self.audio { - write!(f, ",AUDIO={}", quote(value))?; - } - - if let Some(value) = &self.subtitles { - write!(f, ",SUBTITLES={}", quote(value))?; - } - - if let Some(value) = &self.closed_captions { - write!(f, ",CLOSED-CAPTIONS={}", value)?; - } - - write!(f, "\n{}", self.uri)?; - Ok(()) - } -} - -impl FromStr for ExtXStreamInf { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut lines = input.lines(); - let first_line = lines - .next() - .ok_or_else(|| Error::missing_value("first_line"))?; - let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?; - - let input = tag(first_line, Self::PREFIX)?; - - let mut frame_rate = None; - let mut audio = None; - let mut subtitles = None; - let mut closed_captions = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "FRAME-RATE" => frame_rate = Some((value.parse())?), - "AUDIO" => audio = Some(unquote(value)), - "SUBTITLES" => subtitles = Some(unquote(value)), - "CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()), - _ => {} - } - } - - Ok(Self { - uri: uri.to_string(), - frame_rate, - audio, - subtitles, - closed_captions, - stream_inf: input.parse()?, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_parser() { - let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com" - .parse::() - .unwrap(); - - assert_eq!( - stream_inf, - ExtXStreamInf::new("http://www.example.com", 1000) - ); - } - - #[test] - fn test_display() { - assert_eq!( - ExtXStreamInf::new("http://www.example.com/", 1000).to_string(), - "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() - ); - } - - #[test] - fn test_required_version() { - assert_eq!( - ProtocolVersion::V1, - ExtXStreamInf::new("http://www.example.com", 1000).required_version() - ); - } - - #[test] - fn test_deref() { - assert_eq!( - ExtXStreamInf::new("http://www.example.com", 1000).bandwidth(), - 1000 - ); - } - - #[test] - fn test_deref_mut() { - assert_eq!( - ExtXStreamInf::new("http://www.example.com", 1000) - .set_bandwidth(1) - .bandwidth(), - 1 - ); - } -} diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs new file mode 100644 index 0000000..ecbd2c4 --- /dev/null +++ b/src/tags/master_playlist/variant_stream.rs @@ -0,0 +1,286 @@ +use core::fmt; +use core::ops::Deref; +use core::str::FromStr; + +use crate::attribute::AttributePairs; +use crate::traits::RequiredVersion; +use crate::types::{ClosedCaptions, ProtocolVersion, StreamData, UFloat}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + +/// A server MAY offer multiple Media Playlist files to provide different +/// encodings of the same presentation. If it does so, it SHOULD provide +/// a Master Playlist file that lists each Variant Stream to allow +/// clients to switch between encodings dynamically. +/// +/// Master Playlists describe regular Variant Streams with EXT-X-STREAM- +/// INF tags and I-frame Variant Streams with EXT-X-I-FRAME-STREAM-INF +/// tags. +/// +/// If an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag contains +/// the CODECS attribute, the attribute value MUST include every media +/// format [RFC6381] present in any Media Segment in any of the +/// Renditions specified by the Variant Stream. +/// +/// The server MUST meet the following constraints when producing Variant +/// Streams in order to allow clients to switch between them seamlessly: +/// +/// o Each Variant Stream MUST present the same content. +/// +/// +/// o Matching content in Variant Streams MUST have matching timestamps. +/// This allows clients to synchronize the media. +/// +/// o Matching content in Variant Streams MUST have matching +/// Discontinuity Sequence Numbers (see Section 4.3.3.3). +/// +/// o Each Media Playlist in each Variant Stream MUST have the same +/// target duration. The only exceptions are SUBTITLES Renditions and +/// Media Playlists containing an EXT-X-I-FRAMES-ONLY tag, which MAY +/// have different target durations if they have an EXT-X-PLAYLIST- +/// TYPE of VOD. +/// +/// o Content that appears in a Media Playlist of one Variant Stream but +/// not in another MUST appear either at the beginning or at the end +/// of the Media Playlist file and MUST NOT be longer than the target +/// duration. +/// +/// o If any Media Playlists have an EXT-X-PLAYLIST-TYPE tag, all Media +/// Playlists MUST have an EXT-X-PLAYLIST-TYPE tag with the same +/// value. +/// +/// o If the Playlist contains an EXT-X-PLAYLIST-TYPE tag with the value +/// of VOD, the first segment of every Media Playlist in every Variant +/// Stream MUST start at the same media timestamp. +/// +/// o If any Media Playlist in a Master Playlist contains an EXT-X- +/// PROGRAM-DATE-TIME tag, then all Media Playlists in that Master +/// Playlist MUST contain EXT-X-PROGRAM-DATE-TIME tags with consistent +/// mappings of date and time to media timestamps. +/// +/// o Each Variant Stream MUST contain the same set of Date Ranges, each +/// one identified by an EXT-X-DATERANGE tag(s) with the same ID +/// attribute value and containing the same set of attribute/value +/// pairs. +/// +/// In addition, for broadest compatibility, Variant Streams SHOULD +/// contain the same encoded audio bitstream. This allows clients to +/// switch between Variant Streams without audible glitching. +/// +/// The rules for Variant Streams also apply to alternative Renditions +/// (see Section 4.3.4.2.1). +/// +/// [RFC6381]: https://tools.ietf.org/html/rfc6381 +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub enum VariantStream { + ExtXIFrame { + /// The URI identifies the I-frame [`MediaPlaylist`] file. + /// That Playlist file must contain an [`ExtXIFramesOnly`] tag. + /// + /// # Note + /// + /// This field is required. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + uri: String, + /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and + /// [`VariantStream::ExtXIFrame`]. + /// + /// # Note + /// + /// This field is optional. + stream_data: StreamData, + }, + ExtXStreamInf { + /// The URI specifies a [`MediaPlaylist`] that carries a rendition of + /// the [`VariantStream`]. Clients that do not support multiple video + /// renditions should play this rendition. + /// + /// # Note + /// + /// This field is required. + /// + /// [`MediaPlaylist`]: crate::MediaPlaylist + uri: String, + /// The value is an unsigned float describing the maximum frame + /// rate for all the video in the [`VariantStream`]. + /// + /// # Note + /// + /// Specifying the frame rate is optional, but is recommended if the + /// [`VariantStream`] includes video. It should be specified if any + /// video exceeds 30 frames per second. + frame_rate: Option, + /// It indicates the set of audio renditions that should be used when + /// playing the presentation. + /// + /// It must match the value of the [`ExtXMedia::group_id`] of an + /// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose + /// [`ExtXMedia::media_type`] is [`MediaType::Audio`]. + /// + /// # Note + /// + /// This field is optional. + /// + /// [`ExtXMedia`]: crate::tags::ExtXMedia + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + /// [`MediaType::Audio`]: crate::types::MediaType::Audio + audio: Option, + /// It indicates the set of subtitle renditions that can be used when + /// playing the presentation. + /// + /// It must match the value of the [`ExtXMedia::group_id`] of an + /// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose + /// [`ExtXMedia::media_type`] is [`MediaType::Subtitles`]. + /// + /// # Note + /// + /// This field is optional. + /// + /// [`ExtXMedia`]: crate::tags::ExtXMedia + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + /// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles + subtitles: Option, + /// It indicates the set of closed-caption renditions that can be used + /// when playing the presentation. + /// + /// # Note + /// + /// This field is optional. + closed_captions: Option, + /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and + /// [`VariantStream::ExtXIFrame`]. + /// + /// # Note + /// + /// This field is optional. + stream_data: StreamData, + }, +} + +impl VariantStream { + pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; + pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:"; +} + +/// This tag requires [`ProtocolVersion::V1`]. +impl RequiredVersion for VariantStream { + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } +} + +impl fmt::Display for VariantStream { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Self::ExtXIFrame { uri, stream_data } => { + write!(f, "{}", Self::PREFIX_EXTXIFRAME)?; + write!(f, "URI={},{}", quote(uri), stream_data)?; + } + Self::ExtXStreamInf { + uri, + frame_rate, + audio, + subtitles, + closed_captions, + stream_data, + } => { + write!(f, "{}{}", Self::PREFIX_EXTXSTREAMINF, stream_data)?; + + if let Some(value) = frame_rate { + write!(f, ",FRAME-RATE={:.3}", value.as_f32())?; + } + + if let Some(value) = audio { + write!(f, ",AUDIO={}", quote(value))?; + } + + if let Some(value) = subtitles { + write!(f, ",SUBTITLES={}", quote(value))?; + } + + if let Some(value) = closed_captions { + write!(f, ",CLOSED-CAPTIONS={}", value)?; + } + + write!(f, "\n{}", uri)?; + } + } + + Ok(()) + } +} + +impl FromStr for VariantStream { + type Err = Error; + + fn from_str(input: &str) -> Result { + if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) { + let uri = AttributePairs::new(input) + .find_map(|(key, value)| { + if key == "URI" { + Some(unquote(value)) + } else { + None + } + }) + .ok_or_else(|| Error::missing_value("URI"))?; + + Ok(Self::ExtXIFrame { + uri, + stream_data: input.parse()?, + }) + } else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) { + let mut lines = input.lines(); + let first_line = lines + .next() + .ok_or_else(|| Error::missing_value("first_line"))?; + let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?; + + let mut frame_rate = None; + let mut audio = None; + let mut subtitles = None; + let mut closed_captions = None; + + for (key, value) in AttributePairs::new(first_line) { + match key { + "FRAME-RATE" => frame_rate = Some(value.parse()?), + "AUDIO" => audio = Some(unquote(value)), + "SUBTITLES" => subtitles = Some(unquote(value)), + "CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()), + _ => {} + } + } + + Ok(Self::ExtXStreamInf { + uri: uri.to_string(), + frame_rate, + audio, + subtitles, + closed_captions, + stream_data: first_line.parse()?, + }) + } else { + // TODO: custom error type? + attach input data + Err(Error::custom(format!( + "invalid start of input, expected either {:?} or {:?}", + Self::PREFIX_EXTXIFRAME, + Self::PREFIX_EXTXSTREAMINF + ))) + } + } +} + +impl Deref for VariantStream { + type Target = StreamData; + + fn deref(&self) -> &Self::Target { + match &self { + Self::ExtXIFrame { stream_data, .. } | Self::ExtXStreamInf { stream_data, .. } => { + stream_data + } + } + } +} diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 93bf543..ae2350b 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -5,14 +5,32 @@ use std::str::FromStr; use crate::utils::{quote, unquote}; /// The identifier of a closed captions group or its absence. -/// -/// See: [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[allow(missing_docs)] +#[non_exhaustive] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum ClosedCaptions { + /// It indicates the set of closed-caption renditions that can be used when + /// playing the presentation. + /// + /// The [`String`] must match [`ExtXMedia::group_id`] elsewhere in the + /// Playlist and it's [`ExtXMedia::media_type`] must be + /// [`MediaType::ClosedCaptions`]. + /// + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + /// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions GroupId(String), + /// [`ClosedCaptions::None`] indicates that there are no closed captions in + /// any [`VariantStream`] in the [`MasterPlaylist`], therefore all + /// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a + /// value of [`ClosedCaptions::None`]. + /// + /// Having [`ClosedCaptions`] in one [`VariantStream`] but not in another + /// can trigger playback inconsistencies. + /// + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`VariantStream`]: crate::tags::VariantStream + /// [`VariantStream::ExtXStreamInf`]: + /// crate::tags::VariantStream::ExtXStreamInf None, } diff --git a/src/types/mod.rs b/src/types/mod.rs index e0af60d..2f00d48 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -11,7 +11,7 @@ mod key_format_versions; mod media_type; mod protocol_version; mod resolution; -mod stream_inf; +mod stream_data; mod value; mod float; @@ -29,7 +29,7 @@ pub use key_format_versions::*; pub use media_type::*; pub use protocol_version::*; pub use resolution::*; -pub use stream_inf::*; +pub use stream_data::*; pub use value::*; pub use float::Float; diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs new file mode 100644 index 0000000..cb4e7bd --- /dev/null +++ b/src/types/stream_data.rs @@ -0,0 +1,370 @@ +use core::fmt; +use core::str::FromStr; + +use derive_builder::Builder; +use shorthand::ShortHand; + +use crate::attribute::AttributePairs; +use crate::types::{HdcpLevel, Resolution}; +use crate::utils::{quote, unquote}; +use crate::Error; + +/// The [`StreamData`] struct contains the data that is shared between both +/// variants of the [`VariantStream`]. +/// +/// [`VariantStream`]: crate::tags::VariantStream +#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] +#[builder(setter(strip_option))] +#[builder(derive(Debug, PartialEq))] +#[shorthand(enable(must_use, into))] +pub struct StreamData { + /// The peak segment bitrate of the [`VariantStream`] in bits per second. + /// + /// If all the [`MediaSegment`]s in a [`VariantStream`] have already been + /// created, the bandwidth value must be the largest sum of peak segment + /// bitrates that is produced by any playable combination of renditions. + /// + /// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just + /// the peak segment bit rate of that [`MediaPlaylist`].) + /// + /// An inaccurate value can cause playback stalls or prevent clients from + /// playing the variant. If the [`MasterPlaylist`] is to be made available + /// before all [`MediaSegment`]s in the presentation have been encoded, the + /// bandwidth value should be the bandwidth value of a representative + /// period of similar content, encoded using the same settings. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_bandwidth(5); + /// assert_eq!(stream.bandwidth(), 5); + /// ``` + /// + /// # Note + /// + /// This field is required. + /// + /// [`VariantStream`]: crate::tags::VariantStream + /// [`MediaSegment`]: crate::MediaSegment + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist + #[shorthand(disable(into))] + bandwidth: u64, + /// The average bandwidth of the stream in bits per second. + /// + /// It represents the average segment bitrate of the [`VariantStream`]. If + /// all the [`MediaSegment`]s in a [`VariantStream`] have already been + /// created, the average bandwidth must be the largest sum of average + /// segment bitrates that is produced by any playable combination of + /// renditions. + /// + /// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just + /// the average segment bitrate of that [`MediaPlaylist`].) + /// + /// An inaccurate value can cause playback stalls or prevent clients from + /// playing the variant. If the [`MasterPlaylist`] is to be made available + /// before all [`MediaSegment`]s in the presentation have been encoded, the + /// average bandwidth should be the average bandwidth of a representative + /// period of similar content, encoded using the same settings. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_average_bandwidth(Some(300)); + /// assert_eq!(stream.average_bandwidth(), Some(300)); + /// ``` + /// + /// # Note + /// + /// This field is optional. + /// + /// [`MediaSegment`]: crate::MediaSegment + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`MediaPlaylist`]: crate::MediaPlaylist + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default)] + #[shorthand(enable(copy), disable(into, option_as_ref))] + average_bandwidth: Option, + /// A string that represents a list of formats, where each format specifies + /// a media sample type that is present in one or more renditions specified + /// by the [`VariantStream`]. + /// + /// Valid format identifiers are those in the ISO Base Media File Format + /// Name Space defined by "The 'Codecs' and 'Profiles' Parameters for + /// "Bucket" Media Types" [RFC6381]. + /// + /// For example, a stream containing AAC low complexity (AAC-LC) audio and + /// H.264 Main Profile Level 3.0 video would have a codecs value of + /// "mp4a.40.2,avc1.4d401e". + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + /// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string())); + /// ``` + /// + /// # Note + /// + /// This field is optional, but every instance of + /// [`VariantStream::ExtXStreamInf`] should include a codecs attribute. + /// + /// [`VariantStream`]: crate::tags::VariantStream + /// [`VariantStream::ExtXStreamInf`]: + /// crate::tags::VariantStream::ExtXStreamInf + /// [RFC6381]: https://tools.ietf.org/html/rfc6381 + #[builder(default, setter(into))] + codecs: Option, + /// The resolution of the stream. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// use hls_m3u8::types::Resolution; + /// + /// let mut stream = StreamData::new(20); + /// + /// stream.set_resolution(Some((1920, 1080))); + /// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080))); + /// # stream.set_resolution(Some((1280, 10))); + /// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10))); + /// ``` + /// + /// # Note + /// + /// This field is optional, but it is recommended if the [`VariantStream`] + /// includes video. + /// + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default, setter(into))] + #[shorthand(enable(copy))] + resolution: Option, + /// High-bandwidth Digital Content Protection level of the + /// [`VariantStream`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// use hls_m3u8::types::HdcpLevel; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_hdcp_level(Some(HdcpLevel::None)); + /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); + /// ``` + /// + /// # Note + /// + /// This field is optional. + /// + /// [`VariantStream`]: crate::tags::VariantStream + #[builder(default)] + #[shorthand(enable(copy), disable(into))] + hdcp_level: Option, + /// It indicates the set of video renditions, that should be used when + /// playing the presentation. + /// + /// It must match the value of the [`ExtXMedia::group_id`] attribute + /// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose + /// [`ExtXMedia::media_type`] attribute is video. It indicates the set of + /// video renditions that should be used when playing the presentation. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let mut stream = StreamData::new(20); + /// + /// stream.set_video(Some("video_01")); + /// assert_eq!(stream.video(), Some(&"video_01".to_string())); + /// ``` + /// + /// # Note + /// + /// This field is optional. + /// + /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id + /// [`ExtXMedia`]: crate::tags::ExtXMedia + /// [`MasterPlaylist`]: crate::MasterPlaylist + /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type + #[builder(default, setter(into))] + video: Option, +} + +impl StreamData { + /// Creates a new [`StreamData`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::StreamData; + /// # + /// let stream = StreamData::new(20); + /// ``` + pub const fn new(bandwidth: u64) -> Self { + Self { + bandwidth, + average_bandwidth: None, + codecs: None, + resolution: None, + hdcp_level: None, + video: None, + } + } + + /// Returns a builder for [`StreamData`]. + /// + /// # Example + /// + /// ``` + /// use hls_m3u8::types::{HdcpLevel, StreamData}; + /// + /// StreamData::builder() + /// .bandwidth(200) + /// .average_bandwidth(15) + /// .codecs("mp4a.40.2,avc1.4d401e") + /// .resolution((1920, 1080)) + /// .hdcp_level(HdcpLevel::Type0) + /// .video("video_01") + /// .build()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() } +} + +impl fmt::Display for StreamData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "BANDWIDTH={}", self.bandwidth)?; + + if let Some(value) = &self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", value)?; + } + if let Some(value) = &self.codecs { + write!(f, ",CODECS={}", quote(value))?; + } + if let Some(value) = &self.resolution { + write!(f, ",RESOLUTION={}", value)?; + } + if let Some(value) = &self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", value)?; + } + if let Some(value) = &self.video { + write!(f, ",VIDEO={}", quote(value))?; + } + Ok(()) + } +} + +impl FromStr for StreamData { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut bandwidth = None; + let mut average_bandwidth = None; + let mut codecs = None; + let mut resolution = None; + let mut hdcp_level = None; + let mut video = None; + + for (key, value) in AttributePairs::new(input) { + match key { + "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), + "AVERAGE-BANDWIDTH" => { + average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) + } + "CODECS" => codecs = Some(unquote(value)), + "RESOLUTION" => resolution = Some(value.parse()?), + "HDCP-LEVEL" => { + hdcp_level = Some(value.parse::().map_err(Error::strum)?) + } + "VIDEO" => video = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized + // AttributeName. + } + } + } + + let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?; + + Ok(Self { + bandwidth, + average_bandwidth, + codecs, + resolution, + hdcp_level, + video, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_display() { + let mut stream_data = StreamData::new(200); + stream_data.set_average_bandwidth(Some(15)); + stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_data.set_resolution(Some((1920, 1080))); + stream_data.set_hdcp_level(Some(HdcpLevel::Type0)); + stream_data.set_video(Some("video")); + + assert_eq!( + stream_data.to_string(), + concat!( + "BANDWIDTH=200,", + "AVERAGE-BANDWIDTH=15,", + "CODECS=\"mp4a.40.2,avc1.4d401e\",", + "RESOLUTION=1920x1080,", + "HDCP-LEVEL=TYPE-0,", + "VIDEO=\"video\"" + ) + .to_string() + ); + } + + #[test] + fn test_parser() { + let mut stream_data = StreamData::new(200); + stream_data.set_average_bandwidth(Some(15)); + stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_data.set_resolution(Some((1920, 1080))); + stream_data.set_hdcp_level(Some(HdcpLevel::Type0)); + stream_data.set_video(Some("video")); + + assert_eq!( + stream_data, + concat!( + "BANDWIDTH=200,", + "AVERAGE-BANDWIDTH=15,", + "CODECS=\"mp4a.40.2,avc1.4d401e\",", + "RESOLUTION=1920x1080,", + "HDCP-LEVEL=TYPE-0,", + "VIDEO=\"video\"" + ) + .parse() + .unwrap() + ); + + assert!("garbage".parse::().is_err()); + } +} diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs deleted file mode 100644 index 15d1af3..0000000 --- a/src/types/stream_inf.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use derive_builder::Builder; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{HdcpLevel, Resolution}; -use crate::utils::{quote, unquote}; -use crate::Error; - -/// # [4.3.4.2. EXT-X-STREAM-INF] -/// -/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)] -#[builder(setter(into, strip_option))] -#[builder(derive(Debug, PartialEq))] -#[shorthand(enable(must_use, into))] -pub struct StreamInf { - /// The peak segment bit rate of the variant stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_bandwidth(5); - /// assert_eq!(stream.bandwidth(), 5); - /// ``` - /// - /// # Note - /// - /// This field is required. - #[shorthand(disable(into))] - bandwidth: u64, - /// The average bandwidth of the stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_average_bandwidth(Some(300)); - /// assert_eq!(stream.average_bandwidth(), Some(300)); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - #[shorthand(enable(copy), disable(into, option_as_ref))] - average_bandwidth: Option, - /// A string that represents the list of codec types contained the variant - /// stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - /// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string())); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - codecs: Option, - /// The resolution of the stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// use hls_m3u8::types::Resolution; - /// - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_resolution(Some((1920, 1080))); - /// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080))); - /// # stream.set_resolution(Some((1280, 10))); - /// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10))); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - #[shorthand(enable(copy))] - resolution: Option, - /// High-bandwidth Digital Content Protection level of the variant stream. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::{HdcpLevel, StreamInf}; - /// # - /// let mut stream = StreamInf::new(20); - /// - /// stream.set_hdcp_level(Some(HdcpLevel::None)); - /// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None)); - /// ``` - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - #[shorthand(enable(copy), disable(into))] - hdcp_level: Option, - /// It indicates the set of video renditions, that should be used when - /// playing the presentation. - /// - /// # Note - /// - /// This field is optional. - #[builder(default)] - video: Option, -} - -impl StreamInf { - /// Creates a new [`StreamInf`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::StreamInf; - /// # - /// let stream = StreamInf::new(20); - /// ``` - pub const fn new(bandwidth: u64) -> Self { - Self { - bandwidth, - average_bandwidth: None, - codecs: None, - resolution: None, - hdcp_level: None, - video: None, - } - } -} - -impl fmt::Display for StreamInf { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BANDWIDTH={}", self.bandwidth)?; - - if let Some(value) = &self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", value)?; - } - if let Some(value) = &self.codecs { - write!(f, ",CODECS={}", quote(value))?; - } - if let Some(value) = &self.resolution { - write!(f, ",RESOLUTION={}", value)?; - } - if let Some(value) = &self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", value)?; - } - if let Some(value) = &self.video { - write!(f, ",VIDEO={}", quote(value))?; - } - Ok(()) - } -} - -impl FromStr for StreamInf { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut bandwidth = None; - let mut average_bandwidth = None; - let mut codecs = None; - let mut resolution = None; - let mut hdcp_level = None; - let mut video = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), - "AVERAGE-BANDWIDTH" => { - average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?) - } - "CODECS" => codecs = Some(unquote(value)), - "RESOLUTION" => resolution = Some(value.parse()?), - "HDCP-LEVEL" => { - hdcp_level = Some(value.parse::().map_err(Error::strum)?) - } - "VIDEO" => video = Some(unquote(value)), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized - // AttributeName. - } - } - } - - let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?; - - Ok(Self { - bandwidth, - average_bandwidth, - codecs, - resolution, - hdcp_level, - video, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_display() { - let mut stream_inf = StreamInf::new(200); - stream_inf.set_average_bandwidth(Some(15)); - stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - stream_inf.set_resolution(Some((1920, 1080))); - stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); - stream_inf.set_video(Some("video")); - - assert_eq!( - stream_inf.to_string(), - "BANDWIDTH=200,\ - AVERAGE-BANDWIDTH=15,\ - CODECS=\"mp4a.40.2,avc1.4d401e\",\ - RESOLUTION=1920x1080,\ - HDCP-LEVEL=TYPE-0,\ - VIDEO=\"video\"" - .to_string() - ); - } - - #[test] - fn test_parser() { - let mut stream_inf = StreamInf::new(200); - stream_inf.set_average_bandwidth(Some(15)); - stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); - stream_inf.set_resolution(Some((1920, 1080))); - stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); - stream_inf.set_video(Some("video")); - - assert_eq!( - stream_inf, - "BANDWIDTH=200,\ - AVERAGE-BANDWIDTH=15,\ - CODECS=\"mp4a.40.2,avc1.4d401e\",\ - RESOLUTION=1920x1080,\ - HDCP-LEVEL=TYPE-0,\ - VIDEO=\"video\"" - .parse() - .unwrap() - ); - - assert_eq!( - stream_inf, - "BANDWIDTH=200,\ - AVERAGE-BANDWIDTH=15,\ - CODECS=\"mp4a.40.2,avc1.4d401e\",\ - RESOLUTION=1920x1080,\ - HDCP-LEVEL=TYPE-0,\ - VIDEO=\"video\",\ - UNKNOWN=\"value\"" - .parse() - .unwrap() - ); - - assert!("garbage".parse::().is_err()); - } -} diff --git a/src/types/ufloat.rs b/src/types/ufloat.rs index ad4d180..1313fcc 100644 --- a/src/types/ufloat.rs +++ b/src/types/ufloat.rs @@ -9,7 +9,7 @@ use crate::Error; /// with a negative float (ex. `-1.1`), [`NaN`], [`INFINITY`] or /// [`NEG_INFINITY`]. /// -/// [`NaN`]: core::f32::NaN +/// [`NaN`]: core::f32::NAN /// [`INFINITY`]: core::f32::INFINITY /// [`NEG_INFINITY`]: core::f32::NEG_INFINITY #[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)] diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs index fb2ecfc..2d35d35 100644 --- a/tests/master_playlist.rs +++ b/tests/master_playlist.rs @@ -1,5 +1,5 @@ -use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf}; -use hls_m3u8::types::MediaType; +use hls_m3u8::tags::{ExtXMedia, VariantStream}; +use hls_m3u8::types::{MediaType, StreamData}; use hls_m3u8::MasterPlaylist; use pretty_assertions::assert_eq; @@ -7,45 +7,71 @@ use pretty_assertions::assert_eq; #[test] fn test_master_playlist() { // https://tools.ietf.org/html/rfc8216#section-8.4 - let master_playlist = "#EXTM3U\n\ - #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\ - http://example.com/low.m3u8\n\ - #EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n\ - http://example.com/mid.m3u8\n\ - #EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n\ - http://example.com/hi.m3u8\n\ - #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\ - http://example.com/audio-only.m3u8" - .parse::() - .unwrap(); + let master_playlist = concat!( + "#EXTM3U\n", + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n", + "http://example.com/low.m3u8\n", + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n", + "http://example.com/mid.m3u8\n", + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n", + "http://example.com/hi.m3u8\n", + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n", + "http://example.com/audio-only.m3u8", + ) + .parse::() + .unwrap(); assert_eq!( MasterPlaylist::builder() - .stream_inf_tags(vec![ - ExtXStreamInf::builder() - .bandwidth(1280000) - .average_bandwidth(1000000) - .uri("http://example.com/low.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(2560000) - .average_bandwidth(2000000) - .uri("http://example.com/mid.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(7680000) - .average_bandwidth(6000000) - .uri("http://example.com/hi.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(65000) - .codecs("mp4a.40.5") - .uri("http://example.com/audio-only.m3u8") - .build() - .unwrap(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "http://example.com/low.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(1280000) + .average_bandwidth(1000000) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/mid.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(2560000) + .average_bandwidth(2000000) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/hi.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(7680000) + .average_bandwidth(6000000) + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "http://example.com/audio-only.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, ]) .build() .unwrap(), @@ -56,62 +82,75 @@ fn test_master_playlist() { #[test] fn test_master_playlist_with_i_frames() { // https://tools.ietf.org/html/rfc8216#section-8.5 - let master_playlist = "#EXTM3U\n\ - #EXT-X-STREAM-INF:BANDWIDTH=1280000\n\ - low/audio-video.m3u8\n\ - #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n\ - #EXT-X-STREAM-INF:BANDWIDTH=2560000\n\ - mid/audio-video.m3u8\n\ - #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n\ - #EXT-X-STREAM-INF:BANDWIDTH=7680000\n\ - hi/audio-video.m3u8\n\ - #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n\ - #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\ - audio-only.m3u8" - .parse::() - .unwrap(); + let master_playlist = concat!( + "#EXTM3U\n", + "#EXT-X-STREAM-INF:BANDWIDTH=1280000\n", + "low/audio-video.m3u8\n", + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=2560000\n", + "mid/audio-video.m3u8\n", + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n", + "hi/audio-video.m3u8\n", + // this one: + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n", + "audio-only.m3u8" + ) + .parse::() + .unwrap(); assert_eq!( MasterPlaylist::builder() - .stream_inf_tags(vec![ - ExtXStreamInf::builder() - .bandwidth(1280000) - .uri("low/audio-video.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(2560000) - .uri("mid/audio-video.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(7680000) - .uri("hi/audio-video.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(65000) - .codecs("mp4a.40.5") - .uri("audio-only.m3u8") - .build() - .unwrap(), - ]) - .i_frame_stream_inf_tags(vec![ - ExtXIFrameStreamInf::builder() - .bandwidth(86000) - .uri("low/iframe.m3u8") - .build() - .unwrap(), - ExtXIFrameStreamInf::builder() - .bandwidth(150000) - .uri("mid/iframe.m3u8") - .build() - .unwrap(), - ExtXIFrameStreamInf::builder() - .bandwidth(550000) - .uri("hi/iframe.m3u8") - .build() - .unwrap(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "low/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(1280000) + }, + VariantStream::ExtXIFrame { + uri: "low/iframe.m3u8".into(), + stream_data: StreamData::new(86000), + }, + VariantStream::ExtXStreamInf { + uri: "mid/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(2560000) + }, + VariantStream::ExtXIFrame { + uri: "mid/iframe.m3u8".into(), + stream_data: StreamData::new(150000), + }, + VariantStream::ExtXStreamInf { + uri: "hi/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::new(7680000) + }, + VariantStream::ExtXIFrame { + uri: "hi/iframe.m3u8".into(), + stream_data: StreamData::new(550000), + }, + VariantStream::ExtXStreamInf { + uri: "audio-only.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, ]) .build() .unwrap(), @@ -123,33 +162,32 @@ fn test_master_playlist_with_i_frames() { fn test_master_playlist_with_alternative_audio() { // https://tools.ietf.org/html/rfc8216#section-8.6 // TODO: I think the CODECS=\"..." have to be replaced. - let master_playlist = "#EXTM3U\n\ - #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", \ - DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", \ - URI=\"main/english-audio.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", \ - DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", \ - URI=\"main/german-audio.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", \ - DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", \ - URI=\"commentary/audio-only.m3u8\"\n\ - - #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n\ - low/video-only.m3u8\n\ - #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n\ - mid/video-only.m3u8\n\ - #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n\ - hi/video-only.m3u8\n\ - #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n\ - main/english-audio.m3u8" - .parse::() - .unwrap(); + let master_playlist = concat!( + "#EXTM3U\n", + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", ", + "DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", ", + "URI=\"main/english-audio.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", ", + "DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", ", + "URI=\"main/german-audio.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", ", + "DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", ", + "URI=\"commentary/audio-only.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n", + "low/video-only.m3u8\n", + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n", + "mid/video-only.m3u8\n", + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n", + "hi/video-only.m3u8\n", + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n", + "main/english-audio.m3u8" + ) + .parse::() + .unwrap(); assert_eq!( MasterPlaylist::builder() - .media_tags(vec![ + .media(vec![ ExtXMedia::builder() .media_type(MediaType::Audio) .group_id("aac") @@ -181,35 +219,55 @@ fn test_master_playlist_with_alternative_audio() { .build() .unwrap(), ]) - .stream_inf_tags(vec![ - ExtXStreamInf::builder() - .bandwidth(1280000) - .codecs("...") - .audio("aac") - .uri("low/video-only.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(2560000) - .codecs("...") - .audio("aac") - .uri("mid/video-only.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(7680000) - .codecs("...") - .audio("aac") - .uri("hi/video-only.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(65000) - .codecs("mp4a.40.5") - .audio("aac") - .uri("main/english-audio.m3u8") - .build() - .unwrap(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "low/video-only.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(1280000) + .codecs("...") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "mid/video-only.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(2560000) + .codecs("...") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "hi/video-only.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(7680000) + .codecs("...") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "main/english-audio.m3u8".into(), + frame_rate: None, + audio: Some("aac".into()), + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(65000) + .codecs("mp4a.40.5") + .build() + .unwrap() + }, ]) .build() .unwrap(), @@ -220,48 +278,39 @@ fn test_master_playlist_with_alternative_audio() { #[test] fn test_master_playlist_with_alternative_video() { // https://tools.ietf.org/html/rfc8216#section-8.7 - let master_playlist = "#EXTM3U\n\ - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", \ - AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", \ - DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", \ - DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\ - - #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n\ - low/main/audio-video.m3u8\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", \ - AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", \ - DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", \ - DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\ - - #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n\ - mid/main/audio-video.m3u8\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\", \ - AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", \ - DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n\ - - #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", \ - DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\ - - #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\" - hi/main/audio-video.m3u8" - .parse::() - .unwrap(); + let master_playlist = concat!( + "#EXTM3U\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", ", + "AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", ", + "DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", ", + "DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n", + "low/main/audio-video.m3u8\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", ", + "AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", ", + "DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", ", + "DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n", + "mid/main/audio-video.m3u8\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\",", + "AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", ", + "DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n", + "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", ", + "DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n", + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"\n", + "hi/main/audio-video.m3u8" + ) + .parse::() + .unwrap(); assert_eq!( MasterPlaylist::builder() - .media_tags(vec![ + .media(vec![ // low ExtXMedia::builder() .media_type(MediaType::Video) @@ -341,28 +390,46 @@ fn test_master_playlist_with_alternative_video() { .build() .unwrap(), ]) - .stream_inf_tags(vec![ - ExtXStreamInf::builder() - .bandwidth(1280000) - .codecs("...") - .video("low") - .uri("low/main/audio-video.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(2560000) - .codecs("...") - .video("mid") - .uri("mid/main/audio-video.m3u8") - .build() - .unwrap(), - ExtXStreamInf::builder() - .bandwidth(7680000) - .codecs("...") - .video("hi") - .uri("hi/main/audio-video.m3u8") - .build() - .unwrap(), + .variants(vec![ + VariantStream::ExtXStreamInf { + uri: "low/main/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(1280000) + .codecs("...") + .video("low") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "mid/main/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(2560000) + .codecs("...") + .video("mid") + .build() + .unwrap() + }, + VariantStream::ExtXStreamInf { + uri: "hi/main/audio-video.m3u8".into(), + frame_rate: None, + audio: None, + subtitles: None, + closed_captions: None, + stream_data: StreamData::builder() + .bandwidth(7680000) + .codecs("...") + .video("hi") + .build() + .unwrap() + }, ]) .build() .unwrap(), diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index 7f8a860..c87bff2 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -6,26 +6,28 @@ use pretty_assertions::assert_eq; #[test] fn test_media_playlist_with_byterange() { - let media_playlist = "#EXTM3U\n\ - #EXT-X-TARGETDURATION:10\n\ - #EXT-X-VERSION:4\n\ - #EXT-X-MEDIA-SEQUENCE:0\n\ - #EXTINF:10.0,\n\ - #EXT-X-BYTERANGE:75232@0\n\ - video.ts\n\ - #EXT-X-BYTERANGE:82112@752321\n\ - #EXTINF:10.0,\n\ - video.ts\n\ - #EXTINF:10.0,\n\ - #EXT-X-BYTERANGE:69864\n\ - video.ts" - .parse::() - .unwrap(); + let media_playlist = concat!( + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:10\n", + "#EXT-X-VERSION:4\n", + "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXTINF:10.0,\n", + "#EXT-X-BYTERANGE:75232@0\n", + "video.ts\n", + "#EXT-X-BYTERANGE:82112@752321\n", + "#EXTINF:10.0,\n", + "video.ts\n", + "#EXTINF:10.0,\n", + "#EXT-X-BYTERANGE:69864\n", + "video.ts\n" + ) + .parse::() + .unwrap(); assert_eq!( MediaPlaylist::builder() - .target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10))) - .media_sequence_tag(ExtXMediaSequence::new(0)) + .target_duration(ExtXTargetDuration::new(Duration::from_secs(10))) + .media_sequence(ExtXMediaSequence::new(0)) .segments(vec![ MediaSegment::builder() .inf_tag(ExtInf::new(Duration::from_secs_f64(10.0))) diff --git a/tests/playlist.rs b/tests/playlist.rs index 25ccabd..c272a4a 100644 --- a/tests/playlist.rs +++ b/tests/playlist.rs @@ -7,18 +7,19 @@ use std::time::Duration; #[test] fn test_simple_playlist() { - let playlist = r#" - #EXTM3U - #EXT-X-TARGETDURATION:5220 - #EXTINF:0, - http://media.example.com/entire1.ts - #EXTINF:5220, - http://media.example.com/entire2.ts - #EXT-X-ENDLIST"#; + let playlist = concat!( + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:5220\n", + "#EXTINF:0,\n", + "http://media.example.com/entire1.ts\n", + "#EXTINF:5220,\n", + "http://media.example.com/entire2.ts\n", + "#EXT-X-ENDLIST\n" + ); let media_playlist = playlist.parse::().unwrap(); assert_eq!( - media_playlist.target_duration_tag(), + media_playlist.target_duration(), ExtXTargetDuration::new(Duration::from_secs(5220)) );