diff --git a/src/lib.rs b/src/lib.rs index 1ea33a3..ab69898 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ extern crate trackable; pub use error::{Error, ErrorKind}; pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; -pub use media_playlist::MediaPlaylist; +pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder}; pub mod segment { //! Media segment. diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 950b02f..64313ae 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,74 +1,274 @@ use std::fmt; +use std::iter; use std::str::FromStr; +use std::time::Duration; use {Error, ErrorKind, Result}; use line::{Line, Lines, Tag}; use media_segment::{MediaSegment, MediaSegmentBuilder}; use tag::{ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, - ExtXTargetDuration, ExtXVersion}; + ExtXTargetDuration, ExtXVersion, MediaPlaylistTag}; use types::ProtocolVersion; -// TODO: There MUST NOT be more than one Media Playlist tag of each type in any Media Playlist. -// TODO: A Media Playlist tag MUST NOT appear in a Master Playlist. +/// Media playlist builder. +#[derive(Debug, Clone)] +pub struct MediaPlaylistBuilder { + version: Option, + target_duration_tag: Option, + 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 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(), + } + } + + /// 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 + } + + /// Builds a `MediaPlaylist` instance. + pub fn finish(self) -> Result { + let required_version = self.required_version(); + let specified_version = self.version.unwrap_or(required_version); + track_assert!( + required_version <= specified_version, + ErrorKind::InvalidInput, + "required_version:{}, specified_version:{}", + required_version, + specified_version, + ); + + let target_duration_tag = + track_assert_some!(self.target_duration_tag, ErrorKind::InvalidInput); + track!(self.validate_media_segments(target_duration_tag.duration()))?; + + 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, + }) + } + + fn validate_media_segments(&self, target_duration: Duration) -> Result<()> { + let target_duration_seconds = target_duration.as_secs(); + + let mut last_range_uri = None; + for s in &self.segments { + // CHECK: `#EXT-X-TARGETDURATION` + let segment_duration = s.inf().duration(); + let segment_duration_seconds = if segment_duration.subsec_nanos() < 500_000_000 { + segment_duration.as_secs() + } else { + segment_duration.as_secs() + 1 + }; + track_assert!( + segment_duration_seconds <= target_duration_seconds, + ErrorKind::InvalidInput, + "Too large segment duration: segment_duration={}, target_duration={}, uri={:?}", + segment_duration_seconds, + target_duration_seconds, + s.uri() + ); + + // CHECK: `#EXT-X-BYTE-RANGE` + if let Some(tag) = s.byte_range_tag() { + if tag.range().start.is_none() { + let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput); + track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput); + } else { + last_range_uri = Some(s.uri()); + } + } else { + last_range_uri = None; + } + } + Ok(()) + } + + fn required_version(&self) -> ProtocolVersion { + iter::empty() + .chain( + self.target_duration_tag + .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())) + .max() + .expect("Never fails") + } +} +impl Default for MediaPlaylistBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Media playlist. #[derive(Debug, Clone)] pub struct MediaPlaylist { - pub version: ExtXVersion, + 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 { + /// Returns the `EXT-X-VERSION` tag contained in the playlist. + pub fn version_tag(&self) -> ExtXVersion { + self.version_tag + } - // TODO: The EXTINF duration of each Media Segment in the Playlist - // file, when rounded to the nearest integer, MUST be less than or equal - // to the target duration - pub target_duration: ExtXTargetDuration, + /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. + pub fn target_duration_tag(&self) -> ExtXTargetDuration { + self.target_duration_tag + } - // TODO: The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media - // Segment in the Playlist. - pub media_sequence: Option, + /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. + pub fn media_sequence_tag(&self) -> Option { + self.media_sequence_tag + } - // TODO: The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first - // Media Segment in the Playlist. - // - // TODO: The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any EXT- - // X-DISCONTINUITY tag. - pub discontinuity_sequence: Option, + /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist. + pub fn discontinuity_sequence_tag(&self) -> Option { + self.discontinuity_sequence_tag + } - pub playlist_type: Option, - pub i_frames_only: Option, - pub independent_segments: Option, - pub start: Option, + /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. + pub fn playlist_type_tag(&self) -> Option { + self.playlist_type_tag + } - pub segments: Vec, + /// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist. + pub fn i_frames_only_tag(&self) -> Option { + self.i_frames_only_tag + } - pub end_list: Option, + /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. + pub fn independent_segments_tag(&self) -> Option { + self.independent_segments_tag + } + + /// Returns the `EXT-X-START` tag contained in the playlist. + pub fn start_tag(&self) -> Option { + self.start_tag + } + + /// Returns the `EXT-X-ENDLIST` tag contained in the playlist. + pub fn end_list_tag(&self) -> Option { + self.end_list_tag + } + + /// Returns the media segments contained in the playlist. + pub fn segments(&self) -> &[MediaSegment] { + &self.segments + } } impl fmt::Display for MediaPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; - if self.version.version() != ProtocolVersion::V1 { - writeln!(f, "{}", self.version)?; + if self.version_tag.version() != ProtocolVersion::V1 { + writeln!(f, "{}", self.version_tag)?; } - writeln!(f, "{}", self.target_duration)?; - if let Some(ref t) = self.media_sequence { + writeln!(f, "{}", self.target_duration_tag)?; + if let Some(ref t) = self.media_sequence_tag { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.discontinuity_sequence { + if let Some(ref t) = self.discontinuity_sequence_tag { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.playlist_type { + if let Some(ref t) = self.playlist_type_tag { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.i_frames_only { + if let Some(ref t) = self.i_frames_only_tag { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.independent_segments { + if let Some(ref t) = self.independent_segments_tag { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.start { + if let Some(ref t) = self.start_tag { writeln!(f, "{}", t)?; } for segment in &self.segments { writeln!(f, "{}", segment)?; } - if let Some(ref t) = self.end_list { + if let Some(ref t) = self.end_list_tag { writeln!(f, "{}", t)?; } Ok(()) @@ -77,18 +277,11 @@ impl fmt::Display for MediaPlaylist { impl FromStr for MediaPlaylist { type Err = Error; fn from_str(s: &str) -> Result { - let mut version = None; - let mut target_duration = None; - let mut media_sequence = None; - let mut discontinuity_sequence = None; - let mut playlist_type = None; - let mut i_frames_only = None; - let mut independent_segments = None; - let mut start = None; - let mut end_list = None; - + let mut builder = MediaPlaylistBuilder::new(); let mut segment = MediaSegmentBuilder::new(); let mut segments = Vec::new(); + let mut has_partial_segment = false; + let mut has_discontinuity_tag = false; for (i, line) in Lines::new(s).enumerate() { match track!(line)? { Line::Blank | Line::Comment(_) => {} @@ -98,59 +291,81 @@ impl FromStr for MediaPlaylist { continue; } match tag { - Tag::ExtM3u(_) => unreachable!(), + Tag::ExtM3u(_) => track_panic!(ErrorKind::InvalidInput), Tag::ExtXVersion(t) => { - track_assert_eq!(version, None, ErrorKind::InvalidInput); - version = Some(t); + track_assert_eq!(builder.version, None, ErrorKind::InvalidInput); + builder.version(t.version()); } Tag::ExtInf(t) => { + has_partial_segment = true; segment.tag(t); } Tag::ExtXByteRange(t) => { - // TODO: If o is not present, a previous Media Segment MUST appear in the - // Playlist file and MUST be a sub-range of the same media resource, or - // the Media Segment is undefined and the client MUST fail to parse the - // Playlist. + 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) => { - track_assert_eq!(target_duration, None, ErrorKind::InvalidInput); - target_duration = Some(t); + track_assert_eq!( + builder.target_duration_tag, + None, + ErrorKind::InvalidInput + ); + builder.tag(t); } Tag::ExtXMediaSequence(t) => { - track_assert_eq!(media_sequence, None, ErrorKind::InvalidInput); - media_sequence = Some(t); + track_assert_eq!( + builder.media_sequence_tag, + None, + ErrorKind::InvalidInput + ); + track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput); + builder.tag(t); } Tag::ExtXDiscontinuitySequence(t) => { - track_assert_eq!(discontinuity_sequence, None, ErrorKind::InvalidInput); - discontinuity_sequence = Some(t); + track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput); + track_assert!(!has_discontinuity_tag, ErrorKind::InvalidInput); + builder.tag(t); } Tag::ExtXEndList(t) => { - track_assert_eq!(end_list, None, ErrorKind::InvalidInput); - end_list = Some(t); + track_assert_eq!(builder.end_list_tag, None, ErrorKind::InvalidInput); + builder.tag(t); } Tag::ExtXPlaylistType(t) => { - track_assert_eq!(playlist_type, None, ErrorKind::InvalidInput); - playlist_type = Some(t); + track_assert_eq!( + builder.playlist_type_tag, + None, + ErrorKind::InvalidInput + ); + builder.tag(t); } Tag::ExtXIFramesOnly(t) => { - track_assert_eq!(i_frames_only, None, ErrorKind::InvalidInput); - i_frames_only = Some(t); + track_assert_eq!( + builder.i_frames_only_tag, + None, + ErrorKind::InvalidInput + ); + builder.tag(t); } Tag::ExtXMedia(_) | Tag::ExtXStreamInf(_) @@ -160,12 +375,16 @@ impl FromStr for MediaPlaylist { track_panic!(ErrorKind::InvalidInput, "{}", tag) } Tag::ExtXIndependentSegments(t) => { - track_assert_eq!(independent_segments, None, ErrorKind::InvalidInput); - independent_segments = Some(t); + track_assert_eq!( + builder.independent_segments_tag, + None, + ErrorKind::InvalidInput + ); + builder.tag(t); } Tag::ExtXStart(t) => { - track_assert_eq!(start, None, ErrorKind::InvalidInput); - start = Some(t); + track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput); + builder.tag(t); } Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] @@ -177,23 +396,11 @@ impl FromStr for MediaPlaylist { segment.uri(uri); segments.push(track!(segment.finish())?); segment = MediaSegmentBuilder::new(); + has_partial_segment = false; } } } - - let target_duration = track_assert_some!(target_duration, ErrorKind::InvalidInput); - // TODO: check compatibility - Ok(MediaPlaylist { - version: version.unwrap_or_else(|| ExtXVersion::new(ProtocolVersion::V1)), - target_duration, - media_sequence, - discontinuity_sequence, - playlist_type, - i_frames_only, - independent_segments, - start, - segments, - end_list, - }) + track_assert!(!has_partial_segment, ErrorKind::InvalidInput); + track!(builder.finish()) } } diff --git a/src/media_segment.rs b/src/media_segment.rs index dd996db..91abb55 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -4,7 +4,7 @@ use std::iter; use {ErrorKind, Result}; use tag::{ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, MediaSegmentTag}; -use types::SingleLineString; +use types::{ProtocolVersion, SingleLineString}; #[derive(Debug, Clone)] pub struct MediaSegmentBuilder { @@ -78,13 +78,17 @@ impl fmt::Display for MediaSegment { } } impl MediaSegment { + pub fn requires_version(&self) -> ProtocolVersion { + // TODO: + ProtocolVersion::V1 + } pub fn uri(&self) -> &str { &self.uri } pub fn inf(&self) -> &ExtInf { &self.ext_inf } - pub fn byte_range(&self) -> Option<&ExtXByteRange> { + pub fn byte_range_tag(&self) -> Option<&ExtXByteRange> { self.tags.iter().filter_map(|t| t.as_byte_range()).nth(0) } pub fn date_range(&self) -> Option<&ExtXDateRange> { diff --git a/src/tag/mod.rs b/src/tag/mod.rs index 0ad5624..b9b2461 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -62,6 +62,33 @@ impl_from!(MasterPlaylistTag, ExtXSessionKey); impl_from!(MasterPlaylistTag, ExtXIndependentSegments); impl_from!(MasterPlaylistTag, ExtXStart); +/// [4.3.3. Media Playlist Tags] +/// +/// See also [4.3.5. Media or Master Playlist Tags] +/// +/// [4.3.3. Media Playlist Tags] https://tools.ietf.org/html/rfc8216#section-4.3.3 +/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5 +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MediaPlaylistTag { + ExtXTargetDuration(ExtXTargetDuration), + ExtXMediaSequence(ExtXMediaSequence), + ExtXDiscontinuitySequence(ExtXDiscontinuitySequence), + ExtXEndList(ExtXEndList), + ExtXPlaylistType(ExtXPlaylistType), + ExtXIFramesOnly(ExtXIFramesOnly), + ExtXIndependentSegments(ExtXIndependentSegments), + ExtXStart(ExtXStart), +} +impl_from!(MediaPlaylistTag, ExtXTargetDuration); +impl_from!(MediaPlaylistTag, ExtXMediaSequence); +impl_from!(MediaPlaylistTag, ExtXDiscontinuitySequence); +impl_from!(MediaPlaylistTag, ExtXEndList); +impl_from!(MediaPlaylistTag, ExtXPlaylistType); +impl_from!(MediaPlaylistTag, ExtXIFramesOnly); +impl_from!(MediaPlaylistTag, ExtXIndependentSegments); +impl_from!(MediaPlaylistTag, ExtXStart); + #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum MediaSegmentTag {