use crate::line::{Line, Lines, Tag}; use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, QuotedString}; use crate::{Error, ErrorKind, Result}; use std::collections::HashSet; use std::fmt; use std::iter; use std::str::FromStr; /// Master playlist builder. #[derive(Debug, Clone)] pub struct MasterPlaylistBuilder { version: Option, independent_segments_tag: Option, start_tag: Option, media_tags: Vec, stream_inf_tags: Vec, i_frame_stream_inf_tags: Vec, session_data_tags: Vec, session_key_tags: Vec, } impl MasterPlaylistBuilder { /// Makes a new `MasterPlaylistBuilder` instance. pub fn new() -> Self { MasterPlaylistBuilder { version: None, independent_segments_tag: None, start_tag: None, media_tags: Vec::new(), stream_inf_tags: Vec::new(), i_frame_stream_inf_tags: Vec::new(), session_data_tags: Vec::new(), session_key_tags: 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 } /// Adds the given tag to the resulting playlist. /// /// If it is forbidden to have multiple instance of the tag, the existing one will be overwritten. pub fn tag>(&mut self, tag: T) -> &mut Self { match tag.into() { MasterPlaylistTag::ExtXIndependentSegments(t) => { self.independent_segments_tag = Some(t); } MasterPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), MasterPlaylistTag::ExtXMedia(t) => self.media_tags.push(t), MasterPlaylistTag::ExtXStreamInf(t) => self.stream_inf_tags.push(t), MasterPlaylistTag::ExtXIFrameStreamInf(t) => self.i_frame_stream_inf_tags.push(t), MasterPlaylistTag::ExtXSessionData(t) => self.session_data_tags.push(t), MasterPlaylistTag::ExtXSessionKey(t) => self.session_key_tags.push(t), } self } /// Builds a `MasterPlaylist` 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, ); track!(self.validate_stream_inf_tags())?; track!(self.validate_i_frame_stream_inf_tags())?; track!(self.validate_session_data_tags())?; track!(self.validate_session_key_tags())?; Ok(MasterPlaylist { version_tag: ExtXVersion::new(specified_version), independent_segments_tag: self.independent_segments_tag, start_tag: self.start_tag, media_tags: self.media_tags, stream_inf_tags: self.stream_inf_tags, i_frame_stream_inf_tags: self.i_frame_stream_inf_tags, session_data_tags: self.session_data_tags, session_key_tags: self.session_key_tags, }) } fn required_version(&self) -> ProtocolVersion { iter::empty() .chain( self.independent_segments_tag .iter() .map(|t| t.requires_version()), ) .chain(self.start_tag.iter().map(|t| t.requires_version())) .chain(self.media_tags.iter().map(|t| t.requires_version())) .chain(self.stream_inf_tags.iter().map(|t| t.requires_version())) .chain( self.i_frame_stream_inf_tags .iter() .map(|t| t.requires_version()), ) .chain(self.session_data_tags.iter().map(|t| t.requires_version())) .chain(self.session_key_tags.iter().map(|t| t.requires_version())) .max() .expect("Never fails") } fn validate_stream_inf_tags(&self) -> Result<()> { let mut has_none_closed_captions = false; for t in &self.stream_inf_tags { if let Some(group_id) = t.audio() { track_assert!( self.check_media_group(MediaType::Audio, group_id), ErrorKind::InvalidInput, "Unmatched audio group: {:?}", group_id ); } if let Some(group_id) = t.video() { track_assert!( self.check_media_group(MediaType::Video, group_id), ErrorKind::InvalidInput, "Unmatched video group: {:?}", group_id ); } if let Some(group_id) = t.subtitles() { track_assert!( self.check_media_group(MediaType::Subtitles, group_id), ErrorKind::InvalidInput, "Unmatched subtitles group: {:?}", group_id ); } match t.closed_captions() { Some(&ClosedCaptions::GroupId(ref group_id)) => { track_assert!( self.check_media_group(MediaType::ClosedCaptions, group_id), ErrorKind::InvalidInput, "Unmatched closed-captions group: {:?}", group_id ); } Some(&ClosedCaptions::None) => { has_none_closed_captions = true; } None => {} } } if has_none_closed_captions { track_assert!( self.stream_inf_tags .iter() .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)), ErrorKind::InvalidInput ); } Ok(()) } fn validate_i_frame_stream_inf_tags(&self) -> Result<()> { for t in &self.i_frame_stream_inf_tags { if let Some(group_id) = t.video() { track_assert!( self.check_media_group(MediaType::Video, group_id), ErrorKind::InvalidInput, "Unmatched video group: {:?}", group_id ); } } Ok(()) } fn validate_session_data_tags(&self) -> Result<()> { let mut set = HashSet::new(); for t in &self.session_data_tags { track_assert!( set.insert((t.data_id(), t.language())), ErrorKind::InvalidInput, "Conflict: {}", t ); } Ok(()) } fn validate_session_key_tags(&self) -> Result<()> { let mut set = HashSet::new(); for t in &self.session_key_tags { track_assert!( set.insert(t.key()), ErrorKind::InvalidInput, "Conflict: {}", t ); } Ok(()) } fn check_media_group(&self, media_type: MediaType, group_id: &QuotedString) -> bool { self.media_tags .iter() .any(|t| t.media_type() == media_type && t.group_id() == group_id) } } impl Default for MasterPlaylistBuilder { fn default() -> Self { Self::new() } } /// Master playlist. #[derive(Debug, Clone)] pub struct MasterPlaylist { version_tag: ExtXVersion, independent_segments_tag: Option, start_tag: Option, media_tags: Vec, stream_inf_tags: Vec, i_frame_stream_inf_tags: Vec, session_data_tags: Vec, session_key_tags: Vec, } impl MasterPlaylist { /// Returns the `EXT-X-VERSION` tag contained in the playlist. pub fn version_tag(&self) -> ExtXVersion { self.version_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-MEDIA` tags contained in the playlist. pub fn media_tags(&self) -> &[ExtXMedia] { &self.media_tags } /// Returns the `EXT-X-STREAM-INF` tags contained in the playlist. pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] { &self.stream_inf_tags } /// Returns the `EXT-X-I-FRAME-STREAM-INF` tags contained in the playlist. pub fn i_frame_stream_inf_tags(&self) -> &[ExtXIFrameStreamInf] { &self.i_frame_stream_inf_tags } /// Returns the `EXT-X-SESSION-DATA` tags contained in the playlist. pub fn session_data_tags(&self) -> &[ExtXSessionData] { &self.session_data_tags } /// Returns the `EXT-X-SESSION-KEY` tags contained in the playlist. pub fn session_key_tags(&self) -> &[ExtXSessionKey] { &self.session_key_tags } } impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; if self.version_tag.version() != ProtocolVersion::V1 { writeln!(f, "{}", self.version_tag)?; } for t in &self.media_tags { writeln!(f, "{}", t)?; } for t in &self.stream_inf_tags { writeln!(f, "{}", t)?; } for t in &self.i_frame_stream_inf_tags { writeln!(f, "{}", t)?; } for t in &self.session_data_tags { writeln!(f, "{}", t)?; } for t in &self.session_key_tags { 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)?; } Ok(()) } } impl FromStr for MasterPlaylist { type Err = Error; fn from_str(s: &str) -> Result { let mut builder = MasterPlaylistBuilder::new(); for (i, line) in Lines::new(s).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(_) | Tag::ExtXByteRange(_) | Tag::ExtXDiscontinuity(_) | Tag::ExtXKey(_) | Tag::ExtXMap(_) | Tag::ExtXProgramDateTime(_) | Tag::ExtXDateRange(_) | Tag::ExtXTargetDuration(_) | Tag::ExtXMediaSequence(_) | Tag::ExtXDiscontinuitySequence(_) | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { track_panic!(ErrorKind::InvalidInput, "{}", tag) } Tag::ExtXMedia(t) => { builder.tag(t); } Tag::ExtXStreamInf(t) => { builder.tag(t); } Tag::ExtXIFrameStreamInf(t) => { builder.tag(t); } Tag::ExtXSessionData(t) => { builder.tag(t); } Tag::ExtXSessionKey(t) => { builder.tag(t); } 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) => { track_panic!(ErrorKind::InvalidInput, "Unexpected URI: {:?}", uri); } } } track!(builder.finish()) } }