From dd1a40abc98a301461c1ec3da0d8ad3ca8aea072 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 21:08:35 +0200 Subject: [PATCH] added media_playlist builder --- src/error.rs | 64 +-- src/lib.rs | 2 +- src/master_playlist.rs | 161 ++++-- src/media_playlist.rs | 590 ++++++++++----------- src/tags/master_playlist/stream_inf.rs | 32 +- src/tags/media_playlist/target_duration.rs | 2 +- 6 files changed, 462 insertions(+), 389 deletions(-) diff --git a/src/error.rs b/src/error.rs index 92e77dd..2a340d2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,56 +6,63 @@ use failure::{Backtrace, Context, Fail}; /// This crate specific `Result` type. pub type Result = std::result::Result; -#[derive(Debug, Fail, Clone)] -pub enum AttributeError { - #[fail(display = "The attribute has an invalid name; {:?}", _0)] - InvalidAttribute(String), - #[fail(display = "A value is missing for the attribute: {}", _0)] - MissingValue(String), -} - +/// The ErrorKind. #[derive(Debug, Fail, Clone)] pub enum ErrorKind { - #[fail(display = "AttributeError: {}", _0)] - AttributeError(AttributeError), - #[fail(display = "UnknownError: {}", _0)] + /// An unknown error occured. UnknownError(String), #[fail(display = "A value is missing for the attribute {}", _0)] + /// A required value is missing. MissingValue(String), #[fail(display = "Invalid Input")] + /// Error for anything. InvalidInput, #[fail(display = "ParseIntError: {}", _0)] + /// Failed to parse a String to int. ParseIntError(String), #[fail(display = "ParseFloatError: {}", _0)] + /// Failed to parse a String to float. ParseFloatError(String), #[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)] - MissingTag { tag: String, input: String }, + /// A tag is missing, that is required at the start of the input. + MissingTag { + /// The required tag. + tag: String, + /// The unparsed input data. + input: String, + }, #[fail(display = "CustomError: {}", _0)] + /// A custom error. Custom(String), #[fail(display = "Unmatched Group: {:?}", _0)] + /// Unmatched Group UnmatchedGroup(String), #[fail(display = "Unknown Protocol version: {:?}", _0)] + /// Unknown m3u8 version. This library supports up to ProtocolVersion 7. UnknownProtocolVersion(String), #[fail(display = "IoError: {}", _0)] + /// Some io error Io(String), #[fail( display = "VersionError: required_version: {:?}, specified_version: {:?}", _0, _1 )] + /// This error occurs, if there is a ProtocolVersion mismatch. VersionError(String, String), #[fail(display = "BuilderError: {}", _0)] + /// An Error from a Builder. BuilderError(String), /// Hints that destructuring should not be exhaustive. @@ -69,6 +76,7 @@ pub enum ErrorKind { } #[derive(Debug)] +/// The Error type of this library. pub struct Error { inner: Context, } @@ -101,33 +109,7 @@ impl From> for Error { } } -macro_rules! from_error { - ( $( $f:tt ),* ) => { - $( - impl From<$f> for ErrorKind { - fn from(value: $f) -> Self { - Self::$f(value) - } - } - )* - } -} - -from_error!(AttributeError); - impl Error { - pub(crate) fn invalid_attribute(value: T) -> Self { - Self::from(ErrorKind::from(AttributeError::InvalidAttribute( - value.to_string(), - ))) - } - - pub(crate) fn missing_attribute_value(value: T) -> Self { - Self::from(ErrorKind::from(AttributeError::MissingValue( - value.to_string(), - ))) - } - pub(crate) fn unknown(value: T) -> Self where T: error::Error, @@ -197,19 +179,19 @@ impl Error { } } -impl From for Error { +impl From<::std::num::ParseIntError> for Error { fn from(value: ::std::num::ParseIntError) -> Self { Error::parse_int_error(value) } } -impl From for Error { +impl From<::std::num::ParseFloatError> for Error { fn from(value: ::std::num::ParseFloatError) -> Self { Error::parse_float_error(value) } } -impl From for Error { +impl From<::std::io::Error> for Error { fn from(value: ::std::io::Error) -> Self { Error::io(value) } diff --git a/src/lib.rs b/src/lib.rs index 135c9ef..75e8bd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ pub use error::{Error, ErrorKind}; pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; -pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder, MediaPlaylistOptions}; +pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder}; pub use media_segment::{MediaSegment, MediaSegmentBuilder}; pub mod tags; diff --git a/src/master_playlist.rs b/src/master_playlist.rs index ed8e880..0b6cd40 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::fmt; +use std::iter; use std::str::FromStr; use derive_builder::Builder; @@ -13,17 +14,34 @@ use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::Error; /// Master playlist. -#[derive(Debug, Clone, Builder, Default)] +#[derive(Debug, Clone, Builder)] #[builder(build_fn(validate = "Self::validate"))] -#[builder(setter(into, strip_option), default)] +#[builder(setter(into, strip_option))] pub struct MasterPlaylist { + #[builder(default, setter(name = "version"))] + /// Sets the protocol compatibility version of the resulting playlist. + /// + /// If the resulting playlist has tags which requires a compatibility version greater than + /// `version`, + /// `build()` method will fail with an `ErrorKind::InvalidInput` error. + /// + /// The default is the maximum version among the tags in the playlist. version_tag: ExtXVersion, + #[builder(default)] + /// Sets the [ExtXIndependentSegments] tag. independent_segments_tag: Option, + #[builder(default)] + /// Sets the [ExtXStart] tag. start_tag: Option, + /// Sets the [ExtXMedia] tag. media_tags: Vec, + /// Sets all [ExtXStreamInf]s. stream_inf_tags: Vec, + /// Sets all [ExtXIFrameStreamInf]s. i_frame_stream_inf_tags: Vec, + /// Sets all [ExtXSessionData]s. session_data_tags: Vec, + /// Sets all [ExtXSessionKey]s. session_key_tags: Vec, } @@ -75,30 +93,100 @@ impl MasterPlaylist { } impl MasterPlaylistBuilder { - pub(crate) fn validate(&self) -> Result<(), String> { - // validate stream inf tags - if let Some(stream_inf_tags) = &self.stream_inf_tags { + fn validate(&self) -> Result<(), String> { + let required_version = self.required_version(); + let specified_version = self + .version_tag + .unwrap_or(required_version.into()) + .version(); + + if required_version > specified_version { + return Err(Error::required_version(required_version, specified_version).to_string()); + } + + self.validate_stream_inf_tags().map_err(|e| e.to_string())?; + self.validate_i_frame_stream_inf_tags() + .map_err(|e| e.to_string())?; + self.validate_session_data_tags() + .map_err(|e| e.to_string())?; + self.validate_session_key_tags() + .map_err(|e| e.to_string())?; + + Ok(()) + } + + fn required_version(&self) -> ProtocolVersion { + iter::empty() + .chain( + self.independent_segments_tag + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.start_tag + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.media_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.stream_inf_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.i_frame_stream_inf_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.session_data_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.session_key_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .max() + .unwrap_or(ProtocolVersion::V7) + } + + fn validate_stream_inf_tags(&self) -> crate::Result<()> { + if let Some(value) = &self.stream_inf_tags { let mut has_none_closed_captions = false; - for t in stream_inf_tags { + + for t in value { if let Some(group_id) = t.audio() { if !self.check_media_group(MediaType::Audio, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } if let Some(group_id) = t.video() { if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } if let Some(group_id) = t.subtitles() { if !self.check_media_group(MediaType::Subtitles, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } match t.closed_captions() { Some(&ClosedCaptions::GroupId(ref group_id)) => { if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } Some(&ClosedCaptions::None) => { @@ -108,53 +196,57 @@ impl MasterPlaylistBuilder { } } if has_none_closed_captions { - if !stream_inf_tags + if !value .iter() .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) { - return Err(Error::invalid_input().to_string()); + return Err(Error::invalid_input()); } } } + Ok(()) + } - // validate i_frame_stream_inf_tags - if let Some(i_frame_stream_inf_tags) = &self.i_frame_stream_inf_tags { - for t in i_frame_stream_inf_tags { + 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() { if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } } } + Ok(()) + } - // validate session_data_tags - if let Some(session_data_tags) = &self.session_data_tags { - let mut set = HashSet::new(); - - for t in session_data_tags { + fn validate_session_data_tags(&self) -> crate::Result<()> { + let mut set = HashSet::new(); + if let Some(value) = &self.session_data_tags { + for t in value { if !set.insert((t.data_id(), t.language())) { - return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + return Err(Error::custom(format!("Conflict: {}", t))); } } } + Ok(()) + } - // validate session_key_tags - if let Some(session_key_tags) = &self.session_key_tags { - let mut set = HashSet::new(); - for t in session_key_tags { + fn validate_session_key_tags(&self) -> crate::Result<()> { + let mut set = HashSet::new(); + if let Some(value) = &self.session_key_tags { + for t in value { if !set.insert(t.key()) { - return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + return Err(Error::custom(format!("Conflict: {}", t))); } } } - Ok(()) } fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { - if let Some(media_tags) = &self.media_tags { - media_tags + if let Some(value) = &self.media_tags { + value .iter() .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) } else { @@ -220,7 +312,7 @@ impl FromStr for MasterPlaylist { return Err(Error::invalid_input()); } Tag::ExtXVersion(t) => { - builder.version_tag(t.version()); + builder.version(t.version()); } Tag::ExtInf(_) | Tag::ExtXByteRange(_) @@ -235,7 +327,10 @@ impl FromStr for MasterPlaylist { | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { - return Err(Error::invalid_input()); // TODO: why? + return Err(Error::custom(format!( + "This tag isn't allowed in a master playlist: {}", + tag + ))); } Tag::ExtXMedia(t) => { media_tags.push(t); @@ -258,7 +353,7 @@ impl FromStr for MasterPlaylist { Tag::ExtXStart(t) => { builder.start_tag(t); } - Tag::Unknown(_) => { + _ => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. // TODO: collect custom tags diff --git a/src/media_playlist.rs b/src/media_playlist.rs index a4074b6..6a860fe 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -3,154 +3,135 @@ use std::iter; use std::str::FromStr; use std::time::Duration; +use derive_builder::Builder; + use crate::line::{Line, Lines, Tag}; use crate::media_segment::{MediaSegment, MediaSegmentBuilder}; use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, - MediaPlaylistTag, }; use crate::types::ProtocolVersion; use crate::Error; -/// Media playlist builder. -#[derive(Debug, Clone)] -pub struct MediaPlaylistBuilder { - version: Option, - target_duration_tag: Option, +/// Media playlist. +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(validate = "Self::validate"))] +#[builder(setter(into, strip_option))] +pub struct MediaPlaylist { + /// Sets the protocol compatibility version of the resulting playlist. + /// + /// If the resulting playlist has tags which requires a compatibility + /// version greater than `version`, + /// `build()` method will fail with an `ErrorKind::InvalidInput` error. + /// + /// The default is the maximum version among the tags in the playlist. + #[builder(setter(name = "version"))] + version_tag: ExtXVersion, + /// Sets the [ExtXTargetDuration] tag. + target_duration_tag: ExtXTargetDuration, + #[builder(default)] + /// Sets the [ExtXMediaSequence] tag. media_sequence_tag: Option, + #[builder(default)] + /// Sets the [ExtXDiscontinuitySequence] tag. discontinuity_sequence_tag: Option, + #[builder(default)] + /// Sets the [ExtXPlaylistType] tag. playlist_type_tag: Option, + #[builder(default)] + /// Sets the [ExtXIFramesOnly] tag. i_frames_only_tag: Option, + #[builder(default)] + /// Sets the [ExtXIndependentSegments] tag. independent_segments_tag: Option, + #[builder(default)] + /// Sets the [ExtXStart] tag. start_tag: Option, + #[builder(default)] + /// Sets the [ExtXEndList] tag. end_list_tag: Option, + /// Sets all [MediaSegment]s. segments: Vec, - options: MediaPlaylistOptions, + /// Sets the allowable excess duration of each media segment in the associated playlist. + /// + /// # Error + /// If there is a media segment of which duration exceeds + /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, + /// the invocation of `MediaPlaylistBuilder::build()` method will fail. + /// + /// The default value is `Duration::from_secs(0)`. + #[builder(default = "Duration::from_secs(0)")] + allowable_excess_duration: Duration, } impl MediaPlaylistBuilder { - /// Makes a new `MediaPlaylistBuilder` instance. - pub fn new() -> Self { - MediaPlaylistBuilder { - version: None, - target_duration_tag: None, - media_sequence_tag: None, - discontinuity_sequence_tag: None, - playlist_type_tag: None, - i_frames_only_tag: None, - independent_segments_tag: None, - start_tag: None, - end_list_tag: None, - segments: Vec::new(), - options: MediaPlaylistOptions::new(), - } - } - - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility version greater than `version`, - /// `finish()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - pub fn version(&mut self, version: ProtocolVersion) -> &mut Self { - self.version = Some(version); - self - } - - /// Sets the given tag to the resulting playlist. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MediaPlaylistTag::ExtXTargetDuration(t) => self.target_duration_tag = Some(t), - MediaPlaylistTag::ExtXMediaSequence(t) => self.media_sequence_tag = Some(t), - MediaPlaylistTag::ExtXDiscontinuitySequence(t) => { - self.discontinuity_sequence_tag = Some(t) - } - MediaPlaylistTag::ExtXPlaylistType(t) => self.playlist_type_tag = Some(t), - MediaPlaylistTag::ExtXIFramesOnly(t) => self.i_frames_only_tag = Some(t), - MediaPlaylistTag::ExtXIndependentSegments(t) => self.independent_segments_tag = Some(t), - MediaPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), - MediaPlaylistTag::ExtXEndList(t) => self.end_list_tag = Some(t), - } - self - } - - /// Adds a media segment to the resulting playlist. - pub fn segment(&mut self, segment: MediaSegment) -> &mut Self { - self.segments.push(segment); - self - } - - /// Sets the options that will be associated to the resulting playlist. - /// - /// The default value is `MediaPlaylistOptions::default()`. - pub fn options(&mut self, options: MediaPlaylistOptions) -> &mut Self { - self.options = options; - self - } - - /// Builds a `MediaPlaylist` instance. - pub fn finish(self) -> crate::Result { + fn validate(&self) -> Result<(), String> { let required_version = self.required_version(); - let specified_version = self.version.unwrap_or(required_version); - if !(required_version <= specified_version) { + let specified_version = self + .version_tag + .unwrap_or(required_version.into()) + .version(); + + if required_version > specified_version { return Err(Error::custom(format!( - "required_version:{}, specified_version:{}", + "required_version: {}, specified_version: {}", required_version, specified_version - ))); + )) + .to_string()); } - let target_duration_tag = self.target_duration_tag.ok_or(Error::invalid_input())?; - self.validate_media_segments(target_duration_tag.duration())?; + if let Some(target_duration) = &self.target_duration_tag { + self.validate_media_segments(target_duration.duration()) + .map_err(|e| e.to_string())?; + } - Ok(MediaPlaylist { - version_tag: ExtXVersion::new(specified_version), - target_duration_tag, - media_sequence_tag: self.media_sequence_tag, - discontinuity_sequence_tag: self.discontinuity_sequence_tag, - playlist_type_tag: self.playlist_type_tag, - i_frames_only_tag: self.i_frames_only_tag, - independent_segments_tag: self.independent_segments_tag, - start_tag: self.start_tag, - end_list_tag: self.end_list_tag, - segments: self.segments, - }) + Ok(()) } fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> { let mut last_range_uri = None; - for s in &self.segments { - // CHECK: `#EXT-X-TARGETDURATION` - let segment_duration = s.inf_tag().duration(); - let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { - Duration::from_secs(segment_duration.as_secs()) - } else { - Duration::from_secs(segment_duration.as_secs() + 1) - }; - let max_segment_duration = target_duration + self.options.allowable_excess_duration; + if let Some(segments) = &self.segments { + for s in segments { + // CHECK: `#EXT-X-TARGETDURATION` + let segment_duration = s.inf_tag().duration(); + let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { + Duration::from_secs(segment_duration.as_secs()) + } else { + Duration::from_secs(segment_duration.as_secs() + 1) + }; - if !(rounded_segment_duration <= max_segment_duration) { - return Err(Error::custom(format!( - "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", - segment_duration, - max_segment_duration, - target_duration, - s.uri() - ))); - } + let max_segment_duration = { + if let Some(value) = &self.allowable_excess_duration { + target_duration + *value + } else { + target_duration + } + }; - // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(tag) = s.byte_range_tag() { - if tag.to_range().start().is_none() { - let last_uri = last_range_uri.ok_or(Error::invalid_input())?; - if last_uri != s.uri() { - return Err(Error::invalid_input()); + if !(rounded_segment_duration <= max_segment_duration) { + return Err(Error::custom(format!( + "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", + segment_duration, + max_segment_duration, + target_duration, + s.uri() + ))); + } + + // CHECK: `#EXT-X-BYTE-RANGE` + if let Some(tag) = s.byte_range_tag() { + if tag.to_range().start().is_none() { + let last_uri = last_range_uri.ok_or(Error::invalid_input())?; + if last_uri != s.uri() { + return Err(Error::invalid_input()); + } + } else { + last_range_uri = Some(s.uri()); } } else { - last_range_uri = Some(s.uri()); + last_range_uri = None; } - } else { - last_range_uri = None; } } Ok(()) @@ -163,49 +144,86 @@ impl MediaPlaylistBuilder { .iter() .map(|t| t.requires_version()), ) - .chain(self.media_sequence_tag.iter().map(|t| t.requires_version())) - .chain( - self.discontinuity_sequence_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.playlist_type_tag.iter().map(|t| t.requires_version())) - .chain(self.i_frames_only_tag.iter().map(|t| t.requires_version())) - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.start_tag.iter().map(|t| t.requires_version())) - .chain(self.end_list_tag.iter().map(|t| t.requires_version())) - .chain(self.segments.iter().map(|s| s.requires_version())) + .chain(self.media_sequence_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.discontinuity_sequence_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.playlist_type_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.i_frames_only_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.independent_segments_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.start_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.end_list_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.segments.iter().map(|t| { + t.iter() + .map(|s| s.requires_version()) + .max() + .unwrap_or(ProtocolVersion::V1) + })) .max() .unwrap_or(ProtocolVersion::V1) } -} -impl Default for MediaPlaylistBuilder { - fn default() -> Self { - Self::new() + /// Adds a media segment to the resulting playlist. + pub fn push_segment>(&mut self, value: VALUE) -> &mut Self { + if let Some(segments) = &mut self.segments { + segments.push(value.into()); + } else { + self.segments = Some(vec![value.into()]); + } + self + } + + /// Parse the rest of the [MediaPlaylist] from an m3u8 file. + pub fn parse(&mut self, input: &str) -> crate::Result { + parse_media_playlist(input, self) } } -/// Media playlist. -#[derive(Debug, Clone)] -pub struct MediaPlaylist { - version_tag: ExtXVersion, - target_duration_tag: ExtXTargetDuration, - media_sequence_tag: Option, - discontinuity_sequence_tag: Option, - playlist_type_tag: Option, - i_frames_only_tag: Option, - independent_segments_tag: Option, - start_tag: Option, - end_list_tag: Option, - segments: Vec, -} - impl MediaPlaylist { + /// Creates a [MediaPlaylistBuilder]. + pub fn builder() -> MediaPlaylistBuilder { + MediaPlaylistBuilder::default() + } /// Returns the `EXT-X-VERSION` tag contained in the playlist. pub const fn version_tag(&self) -> ExtXVersion { self.version_tag @@ -292,161 +310,123 @@ impl fmt::Display for MediaPlaylist { } } +fn parse_media_playlist( + input: &str, + builder: &mut MediaPlaylistBuilder, +) -> crate::Result { + let mut segment = MediaSegmentBuilder::new(); + let mut segments = vec![]; + + let mut has_partial_segment = false; + let mut has_discontinuity_tag = false; + + for (i, line) in input.parse::()?.into_iter().enumerate() { + match line { + Line::Tag(tag) => { + if i == 0 { + if tag != Tag::ExtM3u(ExtM3u) { + return Err(Error::custom("m3u8 doesn't start with #EXTM3U")); + } + continue; + } + match tag { + Tag::ExtM3u(_) => return Err(Error::invalid_input()), + Tag::ExtXVersion(t) => { + builder.version(t.version()); + } + Tag::ExtInf(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXByteRange(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXDiscontinuity(t) => { + has_discontinuity_tag = true; + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXKey(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXMap(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXProgramDateTime(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXDateRange(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXTargetDuration(t) => { + builder.target_duration_tag(t); + } + Tag::ExtXMediaSequence(t) => { + builder.media_sequence_tag(t); + } + Tag::ExtXDiscontinuitySequence(t) => { + if segments.is_empty() { + return Err(Error::invalid_input()); + } + if has_discontinuity_tag { + return Err(Error::invalid_input()); + } + builder.discontinuity_sequence_tag(t); + } + Tag::ExtXEndList(t) => { + builder.end_list_tag(t); + } + Tag::ExtXPlaylistType(t) => { + builder.playlist_type_tag(t); + } + Tag::ExtXIFramesOnly(t) => { + builder.i_frames_only_tag(t); + } + Tag::ExtXMedia(_) + | Tag::ExtXStreamInf(_) + | Tag::ExtXIFrameStreamInf(_) + | Tag::ExtXSessionData(_) + | Tag::ExtXSessionKey(_) => { + return Err(Error::custom(tag)); + } + Tag::ExtXIndependentSegments(t) => { + builder.independent_segments_tag(t); + } + Tag::ExtXStart(t) => { + builder.start_tag(t); + } + Tag::Unknown(_) => { + // [6.3.1. General Client Responsibilities] + // > ignore any unrecognized tags. + } + } + } + Line::Uri(uri) => { + segment.uri(uri); + segments.push(segment.finish()?); + segment = MediaSegmentBuilder::new(); + has_partial_segment = false; + } + } + } + if has_partial_segment { + return Err(Error::invalid_input()); + } + + builder.segments(segments); + builder.build().map_err(Error::builder_error) +} + impl FromStr for MediaPlaylist { type Err = Error; fn from_str(input: &str) -> Result { - MediaPlaylistOptions::new().parse(input) - } -} - -/// Media playlist options. -#[derive(Debug, Clone)] -pub struct MediaPlaylistOptions { - allowable_excess_duration: Duration, -} - -impl MediaPlaylistOptions { - /// Makes a new `MediaPlaylistOptions` with the default settings. - pub const fn new() -> Self { - MediaPlaylistOptions { - allowable_excess_duration: Duration::from_secs(0), - } - } - - /// Sets the allowable excess duration of each media segment in the associated playlist. - /// - /// If there is a media segment of which duration exceeds - /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, - /// the invocation of `MediaPlaylistBuilder::finish()` method will fail. - /// - /// The default value is `Duration::from_secs(0)`. - pub fn allowable_excess_segment_duration( - &mut self, - allowable_excess_duration: Duration, - ) -> &mut Self { - self.allowable_excess_duration = allowable_excess_duration; - self - } - - /// Parses the given M3U8 text with the specified settings. - pub fn parse(&self, m3u8: &str) -> crate::Result { - let mut builder = MediaPlaylistBuilder::new(); - builder.options(self.clone()); - - let mut segment = MediaSegmentBuilder::new(); - let mut has_partial_segment = false; - let mut has_discontinuity_tag = false; - for (i, line) in m3u8.parse::()?.into_iter().enumerate() { - match line { - Line::Tag(tag) => { - if i == 0 { - if tag != Tag::ExtM3u(ExtM3u) { - return Err(Error::invalid_input()); - } - continue; - } - match tag { - Tag::ExtM3u(_) => return Err(Error::invalid_input()), - Tag::ExtXVersion(t) => { - if builder.version.is_some() { - return Err(Error::invalid_input()); - } - builder.version(t.version()); - } - Tag::ExtInf(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXByteRange(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXDiscontinuity(t) => { - has_discontinuity_tag = true; - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXKey(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXMap(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXProgramDateTime(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXDateRange(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXTargetDuration(t) => { - builder.tag(t); - } - Tag::ExtXMediaSequence(t) => { - if builder.segments.is_empty() { - return Err(Error::invalid_input()); - } - builder.tag(t); - } - Tag::ExtXDiscontinuitySequence(t) => { - if builder.segments.is_empty() { - return Err(Error::invalid_input()); - } - if has_discontinuity_tag { - return Err(Error::invalid_input()); - } - builder.tag(t); - } - Tag::ExtXEndList(t) => { - builder.tag(t); - } - Tag::ExtXPlaylistType(t) => { - builder.tag(t); - } - Tag::ExtXIFramesOnly(t) => { - builder.tag(t); - } - Tag::ExtXMedia(_) - | Tag::ExtXStreamInf(_) - | Tag::ExtXIFrameStreamInf(_) - | Tag::ExtXSessionData(_) - | Tag::ExtXSessionKey(_) => { - return Err(Error::custom(tag)); - } - Tag::ExtXIndependentSegments(t) => { - builder.tag(t); - } - Tag::ExtXStart(t) => { - builder.tag(t); - } - Tag::Unknown(_) => { - // [6.3.1. General Client Responsibilities] - // > ignore any unrecognized tags. - } - } - } - Line::Uri(uri) => { - segment.uri(uri); - builder.segment((segment.finish())?); - segment = MediaSegmentBuilder::new(); - has_partial_segment = false; - } - } - } - if has_partial_segment { - return Err(Error::invalid_input()); - } - builder.finish() - } -} - -impl Default for MediaPlaylistOptions { - fn default() -> Self { - Self::new() + parse_media_playlist(input, &mut Self::builder()) } } @@ -471,20 +451,20 @@ mod tests { assert!(m3u8.parse::().is_err()); // Error (allowable segment duration = 9) - assert!(MediaPlaylistOptions::new() - .allowable_excess_segment_duration(Duration::from_secs(1)) + assert!(MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(1)) .parse(m3u8) .is_err()); // Ok (allowable segment duration = 10) - assert!(MediaPlaylistOptions::new() - .allowable_excess_segment_duration(Duration::from_secs(2)) + MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(2)) .parse(m3u8) - .is_ok()); + .unwrap(); } #[test] - fn empty_m3u8_parse_test() { + fn test_parser() { let m3u8 = ""; assert!(m3u8.parse::().is_err()); } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index b6aed5f..671179d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -31,9 +31,9 @@ impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; /// Makes a new `ExtXStreamInf` tag. - pub const fn new(uri: SingleLineString, bandwidth: u64) -> Self { + pub fn new(uri: T, bandwidth: u64) -> Self { ExtXStreamInf { - uri, + uri: SingleLineString::new(uri.to_string()).unwrap(), bandwidth, average_bandwidth: None, codecs: None, @@ -209,11 +209,27 @@ mod test { use super::*; #[test] - fn ext_x_stream_inf() { - let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000); - let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_parser() { + let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo" + .parse::() + .unwrap(); + + assert_eq!(stream_inf, ExtXStreamInf::new("foo", 1000)); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ProtocolVersion::V1, + ExtXStreamInf::new("foo", 1000).requires_version() + ); + } + + #[test] + fn test_display() { + assert_eq!( + ExtXStreamInf::new("foo", 1000).to_string(), + "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo".to_string() + ); } } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 32ca2a9..71b0c66 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -9,7 +9,7 @@ use crate::Error; /// [4.3.3.1. EXT-X-TARGETDURATION] /// /// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct ExtXTargetDuration { duration: Duration, }