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, ErrorKind, Result}; use std::fmt; use std::iter; use std::str::FromStr; use std::time::Duration; /// 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, options: MediaPlaylistOptions, } 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) -> 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 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; track_assert!( rounded_segment_duration <= max_segment_duration, ErrorKind::InvalidInput, "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.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() .unwrap_or(ProtocolVersion::V1) } } impl Default for MediaPlaylistBuilder { fn default() -> Self { Self::new() } } /// 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 { /// Returns the `EXT-X-VERSION` tag contained in the playlist. pub fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. pub fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. pub fn media_sequence_tag(&self) -> Option { self.media_sequence_tag } /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist. pub fn discontinuity_sequence_tag(&self) -> Option { self.discontinuity_sequence_tag } /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. pub fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } /// 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 } /// 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_tag.version() != ProtocolVersion::V1 { writeln!(f, "{}", self.version_tag)?; } 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_tag { writeln!(f, "{}", t)?; } if let Some(ref t) = self.playlist_type_tag { writeln!(f, "{}", t)?; } if let Some(ref t) = self.i_frames_only_tag { writeln!(f, "{}", t)?; } if let Some(ref t) = self.independent_segments_tag { writeln!(f, "{}", t)?; } if let Some(ref t) = self.start_tag { writeln!(f, "{}", t)?; } for segment in &self.segments { write!(f, "{}", segment)?; } if let Some(ref t) = self.end_list_tag { writeln!(f, "{}", t)?; } Ok(()) } } impl FromStr for MediaPlaylist { type Err = Error; fn from_str(s: &str) -> Result { track!(MediaPlaylistOptions::new().parse(s)) } } /// Media playlist options. #[derive(Debug, Clone)] pub struct MediaPlaylistOptions { allowable_excess_duration: Duration, } impl MediaPlaylistOptions { /// Makes a new `MediaPlaylistOptions` with the default settings. pub 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) -> 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 Lines::new(m3u8).enumerate() { match track!(line)? { Line::Blank | Line::Comment(_) => {} Line::Tag(tag) => { if i == 0 { track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput); continue; } match tag { Tag::ExtM3u(_) => track_panic!(ErrorKind::InvalidInput), Tag::ExtXVersion(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) => { 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!( builder.target_duration_tag, None, ErrorKind::InvalidInput ); builder.tag(t); } Tag::ExtXMediaSequence(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!(builder.segments.is_empty(), ErrorKind::InvalidInput); track_assert!(!has_discontinuity_tag, ErrorKind::InvalidInput); builder.tag(t); } Tag::ExtXEndList(t) => { track_assert_eq!(builder.end_list_tag, None, ErrorKind::InvalidInput); builder.tag(t); } Tag::ExtXPlaylistType(t) => { track_assert_eq!( builder.playlist_type_tag, None, ErrorKind::InvalidInput ); builder.tag(t); } Tag::ExtXIFramesOnly(t) => { track_assert_eq!( builder.i_frames_only_tag, None, ErrorKind::InvalidInput ); builder.tag(t); } Tag::ExtXMedia(_) | Tag::ExtXStreamInf(_) | Tag::ExtXIFrameStreamInf(_) | Tag::ExtXSessionData(_) | Tag::ExtXSessionKey(_) => { track_panic!(ErrorKind::InvalidInput, "{}", tag) } Tag::ExtXIndependentSegments(t) => { track_assert_eq!( builder.independent_segments_tag, None, ErrorKind::InvalidInput ); builder.tag(t); } Tag::ExtXStart(t) => { track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput); builder.tag(t); } Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. } } } Line::Uri(uri) => { segment.uri(uri); builder.segment(track!(segment.finish())?); segment = MediaSegmentBuilder::new(); has_partial_segment = false; } } } track_assert!(!has_partial_segment, ErrorKind::InvalidInput); track!(builder.finish()) } } impl Default for MediaPlaylistOptions { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn too_large_segment_duration_test() { let m3u8 = "#EXTM3U\n\ #EXT-X-TARGETDURATION:8\n\ #EXT-X-VERSION:3\n\ #EXTINF:9.009,\n\ http://media.example.com/first.ts\n\ #EXTINF:9.509,\n\ http://media.example.com/second.ts\n\ #EXTINF:3.003,\n\ http://media.example.com/third.ts\n\ #EXT-X-ENDLIST"; // Error (allowable segment duration = target duration = 8) assert!(m3u8.parse::().is_err()); // Error (allowable segment duration = 9) assert!(MediaPlaylistOptions::new() .allowable_excess_segment_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)) .parse(m3u8) .is_ok()); } #[test] fn empty_m3u8_parse_test() { let m3u8 = ""; assert!(m3u8.parse::().is_err()); } }