From 2111d047c3da6c42d54ec399a7a5ab298c90c46e Mon Sep 17 00:00:00 2001 From: Takeru Ohta Date: Wed, 14 Feb 2018 00:25:33 +0900 Subject: [PATCH] Add `media_playlist` module --- Cargo.toml | 3 + examples/parse.rs | 22 ++++- src/lib.rs | 2 +- src/media_playlist.rs | 191 ++++++++++++++++++++++++++++++++++++++++++ src/tag.rs | 44 +++++++++- 5 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 src/media_playlist.rs diff --git a/Cargo.toml b/Cargo.toml index 0e5a84a..2c4e602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,6 @@ authors = ["Takeru Ohta "] [dependencies] trackable = "0.2" + +[dev-dependencies] +clap = "2" \ No newline at end of file diff --git a/examples/parse.rs b/examples/parse.rs index 4801174..afdc4a1 100644 --- a/examples/parse.rs +++ b/examples/parse.rs @@ -1,18 +1,36 @@ +extern crate clap; extern crate hls_m3u8; #[macro_use] extern crate trackable; use std::io::{self, Read}; +use clap::{App, Arg}; +use hls_m3u8::media_playlist::MediaPlaylist; use trackable::error::Failure; fn main() { + let matches = App::new("parse") + .arg( + Arg::with_name("M3U8_TYPE") + .long("m3u8-type") + .takes_value(true) + .default_value("media") + .possible_values(&["media", "master"]), + ) + .get_matches(); let mut m3u8 = String::new(); track_try_unwrap!( io::stdin() .read_to_string(&mut m3u8) .map_err(Failure::from_error) ); - for line in hls_m3u8::line::Lines::new(&m3u8) { - println!("{:?}", track_try_unwrap!(line)); + + match matches.value_of("M3U8_TYPE").unwrap() { + "media" => { + let playlist: MediaPlaylist = track_try_unwrap!(m3u8.parse()); + println!("{}", playlist); + } + "master" => unimplemented!(), + _ => unreachable!(), } } diff --git a/src/lib.rs b/src/lib.rs index e8fd53d..21eb165 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,11 @@ extern crate trackable; // pub mod playlist; -// pub mod media_playlist; // pub mod master_playlist; pub use error::{Error, ErrorKind}; pub mod attribute; +pub mod media_playlist; pub mod media_segment; pub mod string; pub mod tag; diff --git a/src/media_playlist.rs b/src/media_playlist.rs new file mode 100644 index 0000000..2a28d97 --- /dev/null +++ b/src/media_playlist.rs @@ -0,0 +1,191 @@ +use std::fmt; +use std::str::FromStr; + +use {Error, ErrorKind, Result}; +use line::{Line, Lines}; +use media_segment::{MediaSegment, MediaSegmentBuilder}; +use tag::{ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, + ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, + ExtXTargetDuration, ExtXVersion, Tag}; +use version::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. +#[derive(Debug, Clone)] +pub struct MediaPlaylist { + pub version: ExtXVersion, + + // 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, + + // TODO: The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media + // Segment in the Playlist. + pub media_sequence: Option, + + // 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, + + pub playlist_type: Option, + pub i_frames_only: Option, + pub independent_segments: Option, + pub start: Option, + + pub segments: Vec, + + pub end_list: Option, +} +impl fmt::Display for MediaPlaylist { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "{}", ExtM3u)?; + if self.version.value() != ProtocolVersion::V1 { + writeln!(f, "{}", self.version)?; + } + writeln!(f, "{}", self.target_duration)?; + if let Some(ref t) = self.media_sequence { + writeln!(f, "{}", t)?; + } + if let Some(ref t) = self.discontinuity_sequence { + writeln!(f, "{}", t)?; + } + if let Some(ref t) = self.playlist_type { + writeln!(f, "{}", t)?; + } + if let Some(ref t) = self.i_frames_only { + writeln!(f, "{}", t)?; + } + if let Some(ref t) = self.independent_segments { + writeln!(f, "{}", t)?; + } + if let Some(ref t) = self.start { + writeln!(f, "{}", t)?; + } + for segment in &self.segments { + writeln!(f, "{}", segment)?; + } + if let Some(ref t) = self.end_list { + writeln!(f, "{}", t)?; + } + Ok(()) + } +} +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 segment = MediaSegmentBuilder::new(); + let mut segments = Vec::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(_) => unreachable!(), + Tag::ExtXVersion(t) => { + track_assert_eq!(version, None, ErrorKind::InvalidInput); + version = Some(t); + } + Tag::ExtInf(t) => { + segment.tag(t); + } + Tag::ExtXByteRange(t) => { + segment.tag(t); + } + Tag::ExtXDiscontinuity(t) => { + segment.tag(t); + } + Tag::ExtXKey(t) => { + segment.tag(t); + } + Tag::ExtXMap(t) => { + segment.tag(t); + } + Tag::ExtXProgramDateTime(t) => { + segment.tag(t); + } + Tag::ExtXDateRange(t) => { + segment.tag(t); + } + Tag::ExtXTargetDuration(t) => { + track_assert_eq!(target_duration, None, ErrorKind::InvalidInput); + target_duration = Some(t); + } + Tag::ExtXMediaSequence(t) => { + track_assert_eq!(media_sequence, None, ErrorKind::InvalidInput); + media_sequence = Some(t); + } + Tag::ExtXDiscontinuitySequence(t) => { + track_assert_eq!(discontinuity_sequence, None, ErrorKind::InvalidInput); + discontinuity_sequence = Some(t); + } + Tag::ExtXEndList(t) => { + track_assert_eq!(end_list, None, ErrorKind::InvalidInput); + end_list = Some(t); + } + Tag::ExtXPlaylistType(t) => { + track_assert_eq!(playlist_type, None, ErrorKind::InvalidInput); + playlist_type = Some(t); + } + Tag::ExtXIFramesOnly(t) => { + track_assert_eq!(i_frames_only, None, ErrorKind::InvalidInput); + i_frames_only = Some(t); + } + Tag::ExtXMedia(_) + | Tag::ExtXStreamInf(_) + | Tag::ExtXIFrameStreamInf(_) + | Tag::ExtXSessionData(_) + | Tag::ExtXSessionKey(_) => { + track_panic!(ErrorKind::InvalidInput, "{}", tag) + } + Tag::ExtXIndependentSegments(t) => { + track_assert_eq!(independent_segments, None, ErrorKind::InvalidInput); + independent_segments = Some(t); + } + Tag::ExtXStart(t) => { + track_assert_eq!(start, None, ErrorKind::InvalidInput); + start = Some(t); + } + } + } + Line::Uri(uri) => { + segment.uri(uri.to_owned()); + segments.push(track!(segment.finish())?); + segment = MediaSegmentBuilder::new(); + } + } + } + + 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, + }) + } +} diff --git a/src/tag.rs b/src/tag.rs index 1094b57..b21c7f8 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -15,6 +15,15 @@ macro_rules! may_invalid { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TagKind { + Basic, + MediaSegment, + MediaPlaylist, + MasterPlaylist, + MediaOrMasterPlaylist, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum MediaSegmentTag { ExtInf(ExtInf), @@ -150,6 +159,32 @@ pub enum Tag { ExtXIndependentSegments(ExtXIndependentSegments), ExtXStart(ExtXStart), } +impl Tag { + pub fn kind(&self) -> TagKind { + match *self { + Tag::ExtM3u(_) | Tag::ExtXVersion(_) => TagKind::Basic, + Tag::ExtInf(_) + | Tag::ExtXByteRange(_) + | Tag::ExtXDiscontinuity(_) + | Tag::ExtXKey(_) + | Tag::ExtXMap(_) + | Tag::ExtXProgramDateTime(_) + | Tag::ExtXDateRange(_) => TagKind::MediaSegment, + Tag::ExtXTargetDuration(_) + | Tag::ExtXMediaSequence(_) + | Tag::ExtXDiscontinuitySequence(_) + | Tag::ExtXEndList(_) + | Tag::ExtXPlaylistType(_) + | Tag::ExtXIFramesOnly(_) => TagKind::MediaPlaylist, + Tag::ExtXMedia(_) + | Tag::ExtXStreamInf(_) + | Tag::ExtXIFrameStreamInf(_) + | Tag::ExtXSessionData(_) + | Tag::ExtXSessionKey(_) => TagKind::MasterPlaylist, + Tag::ExtXIndependentSegments(_) | Tag::ExtXStart(_) => TagKind::MediaOrMasterPlaylist, + } + } +} impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -255,10 +290,17 @@ impl FromStr for ExtM3u { // TODO: A Playlist file MUST NOT contain more than one EXT-X-VERSION tag #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtXVersion { - pub version: ProtocolVersion, + version: ProtocolVersion, } impl ExtXVersion { const PREFIX: &'static str = "#EXT-X-VERSION:"; + + pub fn new(version: ProtocolVersion) -> Self { + ExtXVersion { version } + } + pub fn value(&self) -> ProtocolVersion { + self.version + } } impl fmt::Display for ExtXVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {