From c336b89981e537aeedceba2c8d6ba9671689b857 Mon Sep 17 00:00:00 2001 From: Rutger Date: Fri, 17 Feb 2017 14:50:50 +0100 Subject: [PATCH] Added feature: writing playlists back to file --- README.md | 2 +- examples/simple.rs | 4 +- examples/with_nom_result.rs | 8 +- src/lib.rs | 63 +++-- src/playlist.rs | 511 +++++++++++++++++++++--------------- tests/lib.rs | 169 +++++++++++- 6 files changed, 515 insertions(+), 242 deletions(-) diff --git a/README.md b/README.md index 6c4c0a4..eeb22fc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ To use this library, add the following dependency to `Cargo.toml`: ```toml [dependencies] -m3u8-rs = "1.0.0" +m3u8-rs = "1.0.2" ``` And add the crate to `lib.rs` diff --git a/examples/simple.rs b/examples/simple.rs index cbec9a3..da1524f 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -12,8 +12,8 @@ fn main() { let parsed = m3u8_rs::parse_playlist_res(&bytes); match parsed { - Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl), - Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl), + Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl), + Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl), Err(e) => println!("Error: {:?}", e) } } diff --git a/examples/with_nom_result.rs b/examples/with_nom_result.rs index 8272e01..0eaa49f 100644 --- a/examples/with_nom_result.rs +++ b/examples/with_nom_result.rs @@ -19,8 +19,8 @@ fn main() { }; match playlist { - Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl), - Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl), + Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{:?}", pl), + Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{:?}", pl), } } @@ -32,8 +32,8 @@ fn main_alt() { let parsed = m3u8_rs::parse_playlist(&bytes); match parsed { - IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl), - IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl), + IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl), + IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl), IResult::Error(e) => panic!("Parsing error: \n{}", e), IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e), } diff --git a/src/lib.rs b/src/lib.rs index 38c9247..f72df7b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,8 @@ //! Parsing a playlist and let the parser figure out if it's a media or master playlist. //! //! ``` -//! extern crate m3u8_rs; //! extern crate nom; +//! extern crate m3u8_rs; //! use m3u8_rs::playlist::Playlist; //! use nom::IResult; //! use std::io::Read; @@ -18,15 +18,15 @@ //! //! // Option 1: fn parse_playlist_res(input) -> Result //! match m3u8_rs::parse_playlist_res(&bytes) { -//! Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl), -//! Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl), +//! Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl), +//! Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl), //! Err(e) => println!("Error: {:?}", e) //! } //! //! // Option 2: fn parse_playlist(input) -> IResult<_, Playlist, _> //! match m3u8_rs::parse_playlist(&bytes) { -//! IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl), -//! IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl), +//! IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl), +//! IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl), //! IResult::Error(e) => panic!("Parsing error: \n{}", e), //! IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e), //! } @@ -35,8 +35,8 @@ //! Parsing a master playlist directly //! //! ``` -//! extern crate m3u8_rs; //! extern crate nom; +//! extern crate m3u8_rs; //! use std::io::Read; //! use nom::IResult; //! @@ -85,8 +85,8 @@ use playlist::*; /// }; /// /// match playlist { -/// Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl), -/// Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl), +/// Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{:?}", pl), +/// Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{:?}", pl), /// } pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> { match is_master_playlist(input) { @@ -110,8 +110,8 @@ pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> { /// let parsed = m3u8_rs::parse_playlist_res(&bytes); /// /// match parsed { -/// Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl), -/// Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl), +/// Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl), +/// Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl), /// Err(e) => println!("Error: {:?}", e) /// } /// ``` @@ -128,11 +128,29 @@ pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> { parse_master_playlist_tags(input).map(MasterPlaylist::from_tags) } +/// Parse input as a master playlist +pub fn parse_master_playlist_res(input: &[u8]) -> Result> { + let parse_result = parse_master_playlist(input); + match parse_result { + IResult::Done(_, playlist) => Ok(playlist), + _ => Err(parse_result), + } +} + /// Parse input as a media playlist pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> { parse_media_playlist_tags(input).map(MediaPlaylist::from_tags) } +/// Parse input as a media playlist +pub fn parse_media_playlist_res(input: &[u8]) -> Result> { + let parse_result = parse_media_playlist(input); + match parse_result { + IResult::Done(_, playlist) => Ok(playlist), + _ => Err(parse_result), + } +} + /// When a media tag or no master tag is found, this returns false. pub fn is_master_playlist(input: &[u8]) -> bool { // Assume it's not a master playlist @@ -302,19 +320,21 @@ pub fn media_playlist_tag(input: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { | map!(chain!(tag!("#EXT-X-TARGETDURATION:") ~ n:float,||n), MediaPlaylistTag::TargetDuration) | map!(chain!(tag!("#EXT-X-MEDIA-SEQUENCE:") ~ n:number,||n), MediaPlaylistTag::MediaSequence) | map!(chain!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE:") ~ n:number,||n), MediaPlaylistTag::DiscontinuitySequence) - | map!(playlist_type_tag, MediaPlaylistTag::PlaylistType) + | map!(chain!(tag!("#EXT-X-PLAYLIST-TYPE:") ~ t:playlist_type, ||t), MediaPlaylistTag::PlaylistType) | map!(tag!("#EXT-X-I-FRAMES-ONLY"), |_| MediaPlaylistTag::IFramesOnly) | map!(start_tag, MediaPlaylistTag::Start) | map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MediaPlaylistTag::IndependentSegments) + | map!(tag!("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList) | map!(media_segment_tag, MediaPlaylistTag::Segment) ) } -named!(pub playlist_type_tag, +named!(pub playlist_type, map_res!( - map_res!(tag!("#EXT-X-PLAYLIST-TYPE:"), str::from_utf8), - MediaPlaylistType::from_str) + map_res!(take_until_either_and_consume!("\r\n"), str::from_utf8), + MediaPlaylistType::from_str + ) ); // ----------------------------------------------------------------------------------------------- @@ -339,7 +359,7 @@ pub enum SegmentTag { pub fn media_segment_tag(input: &[u8]) -> IResult<&[u8], SegmentTag> { alt!(input, map!(chain!(tag!("#EXTINF:") ~ e:duration_title_tag,||e), |(a,b)| SegmentTag::Extinf(a,b)) - | map!(chain!(tag!("#EXT-X-BYTERANGE:") ~ r:byterange_val, || r), SegmentTag::ByteRange) + | map!(chain!(tag!("#EXT-X-BYTERANGE:") ~ r:byte_range_val, || r), SegmentTag::ByteRange) | map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity) | map!(chain!(tag!("#EXT-X-KEY:") ~ k:key, || k), SegmentTag::Key) | map!(chain!(tag!("#EXT-X-MAP:") ~ m:map, || m), SegmentTag::Map) @@ -357,7 +377,7 @@ named!(pub duration_title_tag<(f32, Option)>, chain!( duration: float ~ tag!(",")? - ~ title: opt!(map_res!(take_until_and_consume!("\r\n"), from_utf8_slice)) + ~ title: opt!(map_res!(take_until_either_and_consume!("\r\n,"), from_utf8_slice)) ~ tag!(",")? , || (duration, title) @@ -366,12 +386,7 @@ named!(pub duration_title_tag<(f32, Option)>, named!(pub key, map!(key_value_pairs, Key::from_hashmap)); -named!(pub map, - chain!( - uri: quoted ~ range: opt!(chain!(char!(',') ~ b:byterange_val,||b )), - || Map { uri: uri, byterange: range } - ) -); +named!(pub map, map!(key_value_pairs, Map::from_hashmap)); // ----------------------------------------------------------------------------------------------- // Basic tags @@ -450,7 +465,7 @@ named!(pub number, map_res!(map_res!(digit, str::from_utf8), str::FromStr::from_str) ); -named!(pub byterange_val, +named!(pub byte_range_val, chain!( n: number ~ o: opt!(chain!(char!('@') ~ n:number,||n)) @@ -462,7 +477,7 @@ named!(pub byterange_val, named!(pub float, chain!( left: map_res!(digit, str::from_utf8) - ~ right_opt: opt!(chain!(char!('.') ~ d:map_res!(digit, str::from_utf8),|| d )), + ~ right_opt: opt!(chain!(char!('.') ~ d:map_res!(digit, str::from_utf8),|| d)), || match right_opt { Some(right) => { diff --git a/src/playlist.rs b/src/playlist.rs index 94c8b4a..14f526b 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -3,12 +3,34 @@ //! The main type here is the `Playlist` enum. //! Which is either a `MasterPlaylist` or a `MediaPlaylist`. +use std::io::Write; use std::collections::HashMap; use std::str::FromStr; use std::fmt; use super::*; use std::f32; +macro_rules! write_some_attribute_quoted { + ($w:expr, $tag:expr, $o:expr) => ( + if let &Some(ref v) = $o { write!($w, "{}=\"{}\"", $tag, v) } else { Ok(()) } + ); +} + +macro_rules! write_some_attribute { + ($w:expr, $tag:expr, $o:expr) => ( + if let &Some(ref v) = $o { write!($w, "{}={}", $tag, v) } else { Ok(()) } + ); +} + +macro_rules! bool_default_false { + ($optional:expr) => ( + match $optional { + Some(ref s) if s == "YES" => true, + Some(_) | None => false, + } + ); +} + /// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1), /// can either be a `MasterPlaylist` or a `MediaPlaylist`. /// @@ -16,12 +38,21 @@ use std::f32; /// identify Media Segments. A Playlist is a Master Playlist if all URI /// lines in the Playlist identify Media Playlists. A Playlist MUST be /// either a Media Playlist or a Master Playlist; all other Playlists are invalid. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Playlist { MasterPlaylist(MasterPlaylist), MediaPlaylist(MediaPlaylist), } +impl Playlist { + pub fn write_to(&self, writer: &mut T) -> std::io::Result<()> { + match self { + &Playlist::MasterPlaylist(ref pl) => pl.write_to(writer), + &Playlist::MediaPlaylist(ref pl) => pl.write_to(writer), + } + } +} + // ----------------------------------------------------------------------------------------------- // Master Playlist // ----------------------------------------------------------------------------------------------- @@ -30,7 +61,7 @@ pub enum Playlist { /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4) /// provides a set of Variant Streams, each of which /// describes a different version of the same content. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct MasterPlaylist { pub version: usize, pub variants: Vec, @@ -41,13 +72,13 @@ pub struct MasterPlaylist { } impl MasterPlaylist { + pub fn from_tags(mut tags: Vec) -> MasterPlaylist { let mut master_playlist = MasterPlaylist::default(); let mut alternatives = vec![]; - // println!("Creating master playlist from:", ); while let Some(tag) = tags.pop() { - // println!(" {:?}", tag ); + match tag { MasterPlaylistTag::Version(v) => { master_playlist.version = v; @@ -90,9 +121,33 @@ impl MasterPlaylist { fn get_newest_variant(&mut self) -> Option<&mut VariantStream> { self.variants.iter_mut().rev().find(|v| !v.is_i_frame) } + + pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { + writeln!(w, "{}" ,"#EXTM3U")?; + writeln!(w, "#EXT-X-VERSION:{}", self.version)?; + + for variant in &self.variants { + variant.write_to(w)?; + } + if let Some(ref session_data) = self.session_data { + session_data.write_to(w)?; + } + if let Some(ref session_key) = self.session_key { + session_key.write_to(w)?; + } + if let Some(ref start) = self.start { + start.write_to(w)?; + } + if self.independent_segments { + writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?; + } + + Ok(()) + } } -/// [`#EXT-X-STREAM-INF:`] +/// [`#EXT-X-STREAM-INF: +/// `] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.2) /// [`#EXT-X-I-FRAME-STREAM-INF:`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.3) @@ -108,7 +163,7 @@ impl MasterPlaylist { /// Clients should switch between different Variant Streams to adapt to /// network conditions. Clients should choose Renditions based on user /// preferences. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct VariantStream { pub is_i_frame: bool, pub uri: String, @@ -128,6 +183,7 @@ pub struct VariantStream { } impl VariantStream { + pub fn from_hashmap(mut attrs: HashMap, is_i_frame: bool) -> VariantStream { VariantStream { is_i_frame: is_i_frame, @@ -144,6 +200,37 @@ impl VariantStream { alternatives: vec![], } } + + pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { + + for alternative in &self.alternatives { + alternative.write_to(w)?; + } + + if self.is_i_frame { + write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?; + self.write_stream_inf_common_attributes(w)?; + writeln!(w, "URI=\"{}\"", self.uri) + } + else { + write!(w, "#EXT-X-STREAM-INF:")?; + self.write_stream_inf_common_attributes(w)?; + write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?; + write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?; + write_some_attribute_quoted!(w, ",CLOSED-CAPTIONS", &self.closed_captions)?; + write!(w, "\n")?; + writeln!(w, "{}", self.uri) + } + } + + fn write_stream_inf_common_attributes(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "BANDWIDTH={}", &self.bandwidth)?; + write_some_attribute!(w, ",AVERAGE-BANDWIDTH", &self.average_bandwidth)?; + write!(w, ",CODECS=\"{}\"", &self.codecs)?; + write_some_attribute!(w, ",RESOLUTION", &self.resolution)?; + write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?; + write_some_attribute_quoted!(w, ",VIDEO", &self.video) + } } /// [`#EXT-X-MEDIA:`] @@ -155,7 +242,7 @@ impl VariantStream { /// Media Playlists that contain English, French and Spanish Renditions /// of the same presentation. Or two EXT-X-MEDIA tags can be used to /// identify video-only Media Playlists that show two different camera angles. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct AlternativeMedia { // pub media_type: AlternativeMediaType, @@ -172,6 +259,7 @@ pub struct AlternativeMedia { } impl AlternativeMedia { + pub fn from_hashmap(mut attrs: HashMap) -> AlternativeMedia { AlternativeMedia { media_type: attrs.get("TYPE") @@ -182,16 +270,32 @@ impl AlternativeMedia { language: attrs.remove("LANGUAGE"), assoc_language: attrs.remove("ASSOC-LANGUAGE"), name: attrs.remove("NAME").unwrap_or(String::new()), - default: bool_default_false(attrs.remove("DEFAULT")), - autoselect: bool_default_false(attrs.remove("ASSOC-LANGUAGE")), - forced: bool_default_false(attrs.remove("ASSOC-LANGUAGE")), + default: bool_default_false!(attrs.remove("DEFAULT")), + autoselect: bool_default_false!(attrs.remove("AUTOSELECT")), + forced: bool_default_false!(attrs.remove("FORCED")), instream_id: attrs.remove("INSTREAM-ID"), characteristics: attrs.remove("CHARACTERISTICS"), } } + + pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-MEDIA:")?; + write!(w, "TYPE={}", self.media_type)?; + write_some_attribute_quoted!(w, ",URI", &self.uri)?; + write!(w, ",GROUP-ID=\"{}\"", self.group_id)?; + write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?; + write_some_attribute_quoted!(w, ",ASSOC-LANGUAGE", &self.assoc_language)?; + write!(w, ",NAME=\"{}\"", self.name)?; + if self.default { write!(w, ",DEFAULT=YES")?; } + if self.autoselect { write!(w, ",AUTOSELECT=YES")?; } + if self.forced { write!(w, ",FORCED=YES")?; } + write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?; + write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?; + write!(w, "\n") + } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum AlternativeMediaType { Audio, Video, @@ -219,17 +323,37 @@ impl Default for AlternativeMediaType { } } +impl fmt::Display for AlternativeMediaType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", match self { + &AlternativeMediaType::Audio => "AUDIO", + &AlternativeMediaType::Video => "VIDEO", + &AlternativeMediaType::Subtitles => "SUBTITLES", + &AlternativeMediaType::ClosedCaptions => "CLOSEDCAPTIONS", + }) + } +} + + /// [`#EXT-X-SESSION-KEY:`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5) -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct SessionKey(pub Key); +impl SessionKey { + pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-SESSION-KEY:")?; + self.0.write_attributes_to(w)?; + write!(w, "\n") + } +} + /// [`#EXT-X-SESSION-DATA:`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4) /// The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists /// to be specified in a Master Playlist. This allows the client to /// preload these keys without having to read the Media Playlist(s) first. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct SessionData { pub data_id: String, pub value: String, @@ -243,9 +367,18 @@ impl SessionData { data_id: attrs.remove("DATA-ID").unwrap_or_else(String::new), value: attrs.remove("VALUE").unwrap_or_else(String::new), uri: attrs.remove("URI").unwrap_or_else(String::new), - language: attrs.remove("SUBTITLES"), + language: attrs.remove("LANGUAGE"), } } + + pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-SESSION-DATA:")?; + write!(w, "DATA-ID=\"{}\"", self.data_id)?; + write!(w, ",VALUE=\"{}\"", self.value)?; + write!(w, ",URI=\"{}\"", self.uri)?; + write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?; + write!(w, "\n") + } } // ----------------------------------------------------------------------------------------------- @@ -256,7 +389,7 @@ impl SessionData { /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3) /// contains a list of Media Segments, which when played /// sequentially will play the multimedia presentation. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct MediaPlaylist { pub version: usize, /// `#EXT-X-TARGETDURATION:` @@ -269,7 +402,7 @@ pub struct MediaPlaylist { /// `#EXT-X-ENDLIST` pub end_list: bool, /// `#EXT-X-PLAYLIST-TYPE` - pub playlist_type: MediaPlaylistType, + pub playlist_type: Option, /// `#EXT-X-I-FRAMES-ONLY` pub i_frames_only: bool, /// `#EXT-X-START` @@ -279,13 +412,15 @@ pub struct MediaPlaylist { } impl MediaPlaylist { + pub fn from_tags(mut tags: Vec) -> MediaPlaylist { let mut media_playlist = MediaPlaylist::default(); - let mut next_segment = MediaSegment::new(); + let mut next_segment = MediaSegment::empty(); let mut encryption_key = None; let mut map = None; while let Some(tag) = tags.pop() { + println!("Tag: {:?}\n", &tag); match tag { MediaPlaylistTag::Version(v) => { media_playlist.version = v; @@ -303,7 +438,7 @@ impl MediaPlaylist { media_playlist.end_list = true; } MediaPlaylistTag::PlaylistType(t) => { - media_playlist.playlist_type = t; + media_playlist.playlist_type = Some(t); } MediaPlaylistTag::IFramesOnly => { media_playlist.i_frames_only = true; @@ -343,7 +478,7 @@ impl MediaPlaylist { next_segment.map = map.clone(); next_segment.uri = u; media_playlist.segments.push(next_segment); - next_segment = MediaSegment::new(); + next_segment = MediaSegment::empty(); } _ => (), } @@ -353,11 +488,44 @@ impl MediaPlaylist { } media_playlist } + + pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { + writeln!(w, "{}" ,"#EXTM3U")?; + writeln!(w, "#EXT-X-VERSION:{}", self.version)?; + writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?; + + if self.media_sequence != 0 { + writeln!(w, "#EXT-X-MEDIA-SEQUENCE:{}", self.media_sequence)?; + } + if self.discontinuity_sequence != 0 { + writeln!(w, "#EXT-X-DISCONTINUITY-SEQUENCE:{}", self.discontinuity_sequence)?; + } + if self.end_list { + writeln!(w, "#EXT-X-ENDLIST")?; + } + if let Some(ref v) = self.playlist_type { + writeln!(w, "#EXT-X-PLAYLIST-TYPE:{}", v)?; + } + if self.i_frames_only { + writeln!(w, "#EXT-X-I-FRAMES-ONLY")?; + } + if let Some(ref start) = self.start { + start.write_to(w)?; + } + if self.independent_segments { + writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?; + } + for segment in &self.segments { + segment.write_to(w)?; + } + + Ok(()) + } } /// [`#EXT-X-PLAYLIST-TYPE:`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5) -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum MediaPlaylistType { Event, Vod, @@ -375,6 +543,15 @@ impl FromStr for MediaPlaylistType { } } +impl fmt::Display for MediaPlaylistType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", match self { + &MediaPlaylistType::Event => "EVENT", + &MediaPlaylistType::Vod => "VOD", + }) + } +} + impl Default for MediaPlaylistType { fn default() -> MediaPlaylistType { MediaPlaylistType::Event @@ -387,7 +564,7 @@ impl Default for MediaPlaylistType { /// A [Media Segment](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3) /// is specified by a URI and optionally a byte range. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct MediaSegment { pub uri: String, /// `#EXTINF:,[]` @@ -409,9 +586,45 @@ pub struct MediaSegment { } impl MediaSegment { - pub fn new() -> MediaSegment { + pub fn empty() -> MediaSegment { Default::default() } + + pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { + + if let Some(ref byte_range) = self.byte_range { + write!(w, "#EXT-X-BYTERANGE:")?; + byte_range.write_value_to(w)?; + write!(w, "\n")?; + } + if self.discontinuity { + writeln!(w, "{}", "#EXT-X-DISCONTINUITY")?; + } + if let Some(ref key) = self.key { + write!(w, "#EXT-X-KEY:")?; + key.write_attributes_to(w)?; + write!(w, "\n")?; + } + if let Some(ref map) = self.map { + write!(w, "#EXT-X-MAP:")?; + map.write_attributes_to(w)?; + write!(w, "\n")?; + } + if let Some(ref v) = self.program_date_time { + writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v)?; + } + if let Some(ref v) = self.daterange { + writeln!(w, "#EXT-X-DATERANGE:{}", v)?; + } + + write!(w, "#EXTINF:{},", self.duration)?; + + if let Some(ref v) = self.title { + writeln!(w, "{}", v)?; + } + + writeln!(w, "{}", self.uri) + } } /// [`#EXT-X-KEY:<attribute-list>`] @@ -423,7 +636,7 @@ impl MediaSegment { /// KEYFORMAT attribute (or the end of the Playlist file). Two or more /// EXT-X-KEY tags with different KEYFORMAT attributes MAY apply to the /// same Media Segment if they ultimately produce the same decryption key. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Key { pub method: String, pub uri: Option<String>, @@ -442,6 +655,14 @@ impl Key { keyformatversions: attrs.remove("KEYFORMATVERSIONS"), } } + + pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "METHOD={}", self.method)?; + write_some_attribute_quoted!(w, ",URI", &self.uri)?; + write_some_attribute!(w, ",IV", &self.iv)?; + write_some_attribute!(w, ",KEYFORMAT", &self.keyformat)?; + write_some_attribute!(w, ",KEYFORMATVERSIONS", &self.keyformatversions) + } } /// [`#EXT-X-MAP:<attribute-list>`] @@ -454,31 +675,77 @@ impl Key { /// It applies to every Media Segment that appears after it in the /// Playlist until the next EXT-X-MAP tag or until the end of the /// playlist. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Map { pub uri: String, - pub byterange: Option<ByteRange>, + pub byte_range: Option<ByteRange>, } +impl Map { + pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Map { + Map { + uri: attrs.remove("URI").unwrap_or_default(), + byte_range: attrs.remove("BYTERANGE").map(ByteRange::from), + } + } + + pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "URI=\"{}\"", self.uri)?; + if let Some(ref byte_range) = self.byte_range { + write!(w, ",BYTERANGE=")?; + byte_range.write_value_to(w)?; + } + Ok(()) + } +} + + /// [`#EXT-X-BYTERANGE:<n>[@<o>]`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.2) /// /// The EXT-X-BYTERANGE tag indicates that a Media Segment is a sub-range /// of the resource identified by its URI. It applies only to the next /// URI line that follows it in the Playlist. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ByteRange { pub length: i32, pub offset: Option<i32>, } +impl ByteRange { + pub fn write_value_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "{}", self.length)?; + if let Some(offset) = self.offset { + write!(w, "@{}", offset)?; + } + Ok(()) + } +} + +impl From<String> for ByteRange { + fn from(s: String) -> Self { + let w: &str = &s; + ByteRange::from(w) + } +} + +impl<'a> From<&'a str> for ByteRange { + fn from(s: &'a str) -> Self { + match byte_range_val(s.as_bytes()) { + IResult::Done(_, br) => br, + _ => panic!("Should not happen"), + } + } +} + + /// [`#EXT-X-DATERANGE:<attribute-list>`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7) /// /// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time /// defined by a starting and ending date) with a set of attribute / /// value pairs. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct DateRange { pub id: String, pub class: Option<String>, @@ -500,7 +767,7 @@ pub struct DateRange { /// The EXT-X-START tag indicates a preferred point at which to start /// playing a Playlist. By default, clients SHOULD start playback at /// this point when beginning a playback session. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct Start { pub time_offset: String, pub precise: Option<String>, @@ -513,6 +780,12 @@ impl Start { precise: attrs.remove("PRECISE").or(Some("NO".to_string())), } } + + pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?; + write_some_attribute!(w, ",PRECISE", &self.precise)?; + write!(w, "\n") + } } /// A simple `#EXT-` tag @@ -520,184 +793,4 @@ impl Start { pub struct ExtTag { pub tag: String, pub rest: String, -} - -fn bool_default_false(o: Option<String>) -> bool { - if let Some(str) = o { - if str == "YES" { - return true; - } - } - return false; -} - -// ----------------------------------------------------------------------------------------------- -// Display -// ----------------------------------------------------------------------------------------------- - -impl fmt::Display for Playlist { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - &Playlist::MasterPlaylist(ref p) => write!(f, "{}", p), - &Playlist::MediaPlaylist(ref p) => write!(f, "{}", p), - } - } -} - -impl fmt::Display for MasterPlaylist { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(writeln!(f, - "[Master Playlist, version: {} | {} Streams]\n", - self.version, - self.variants.len())); - - for (i, stream) in self.variants.iter().enumerate() { - try!(write!(f, " {} -> {}", i + 1, stream)) - } - - Ok(()) - } -} - -impl fmt::Display for MediaPlaylist { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(write!(f, "[Media Playlist | duration: {:?} ~ seq: {:?} ~ type: {:?} ~ segments: {}", - self.target_duration, - self.media_sequence, - self.playlist_type, - self.segments.len(), - )); - - if self.i_frames_only { - try!(write!(f, " [iframes only]")); - } - if self.independent_segments { - try!(write!(f, " [independent segments]")); - } - - try!(writeln!(f, "]")); - - for (i, segment) in self.segments.iter().enumerate() { - try!(write!(f, " {} -> {}", i + 1, segment)); - } - - Ok(()) - } -} - -impl fmt::Display for MediaSegment { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(write!(f, "[Segment |")); - - if let &Some(ref v) = &self.title { - try!(write!(f, " title: {:?}", v)); - } - - try!(write!(f, " ~ duration: {:?}", self.duration)); - - if let &Some(ref v) = &self.byte_range { - try!(write!(f, " ~ byterange: {:?}", v)); - } - - if self.discontinuity { - try!(write!(f, " [discontinuity]")); - } - - if let &Some(ref v) = &self.program_date_time { - try!(write!(f, " ~ datetime: {:?}", v)); - } - - if let &Some(ref v) = &self.daterange { - try!(write!(f, " ~ daterange: {:?}", v)); - } - - writeln!(f, " ~ uri: {:?}]", self.uri) - } -} - -impl fmt::Display for VariantStream { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - - match self.is_i_frame { - true => try!(write!(f, "[VariantIFrame |")), - false => try!(write!(f, "[Variant |")), - }; - - try!(write!(f, " uri: {:?}", self.uri)); - - try!(write!(f, " ~ bandwidth: {}", self.bandwidth)); - if let &Some(ref v) = &self.resolution { - try!(write!(f, " ~ res: {}", v)); - } - - try!(write!(f, " ~ alts: {}", self.alternatives.len())); - - if let &Some(ref v) = &self.frame_rate { - try!(write!(f, " ~ fps: {}", v)); - } - - if let &Some(ref v) = &self.audio { - try!(write!(f, " ~ audio: {}", v)); - } - - if let &Some(ref v) = &self.video { - try!(write!(f, " ~ video: {}", v)); - } - - if let &Some(ref v) = &self.subtitles { - try!(write!(f, " ~ subs: {}", v)); - } - - if let &Some(ref v) = &self.closed_captions { - try!(write!(f, " ~ closed_captions: {}", v)); - } - - try!(write!(f, "]")); - try!(write!(f, "\n")); - - for (_, alt) in self.alternatives.iter().enumerate() { - try!(write!(f, "{}", alt)); - } - - Ok(()) - } -} - -impl fmt::Display for AlternativeMedia { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - - try!(write!(f, - "[AlternativeMedia | type: {:?} ~ group: {} ~ name: {:?}", - self.media_type, - self.group_id, - self.name)); - - if let &Some(ref v) = &self.uri { - try!(write!(f, " ~ uri: {:?}", v)); - } - - try!(write!(f, " ~ default: {}", self.default)); - - if let &Some(ref v) = &self.language { - try!(write!(f, " ~ lang: {}", v)); - } - - if let &Some(ref v) = &self.assoc_language { - try!(write!(f, " ~ assoc_language: {}", v)); - } - - try!(write!(f, " ~ autoselect: {}", self.default)); - - try!(write!(f, " ~ forced: {}", self.default)); - - if let &Some(ref v) = &self.instream_id { - try!(write!(f, " ~ instream_id: {}", v)); - } - - if let &Some(ref v) = &self.characteristics { - try!(write!(f, " ~ characteristics: {}", v)); - } - - writeln!(f, "]") - } -} +} \ No newline at end of file diff --git a/tests/lib.rs b/tests/lib.rs index 89d8a18..a496796 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -7,7 +7,9 @@ extern crate m3u8_rs; use std::fs; use std::path; use m3u8_rs::*; +use m3u8_rs::playlist::*; use std::io::Read; +use std::fs::File; use nom::*; use std::collections::HashMap; @@ -34,12 +36,12 @@ fn get_sample_playlist(name: &str) -> String { // Playlist fn print_parse_playlist_test(playlist_name: &str) -> bool { - let input = get_sample_playlist(playlist_name); + let input: String = get_sample_playlist(playlist_name); println!("Parsing playlist file: {:?}", playlist_name); let parsed = parse_playlist(input.as_bytes()); if let IResult::Done(i,o) = parsed { - println!("{}", o); + println!("{:?}", o); true } else { @@ -225,3 +227,166 @@ fn float_should_ignore_trailing_dot() { IResult::Done(".rest".as_bytes(), 33f32) ); } + +#[test] +fn parse_duration_title() { + assert_eq!( + duration_title_tag(b"2.002,title\nrest"), + IResult::Done("rest".as_bytes(), (2.002f32, Some("title".to_string()))) + ); +} + +// ----------------------------------------------------------------------------------------------- +// Creating playlists + +fn print_create_and_parse_playlist(playlist_original: &mut Playlist) -> Playlist { + let mut utf8: Vec<u8> = Vec::new(); + playlist_original.write_to(&mut utf8).unwrap(); + + let m3u8_str: &str = std::str::from_utf8(&utf8).unwrap(); + + let playlist_parsed = match *playlist_original { + Playlist::MasterPlaylist(_) => + Playlist::MasterPlaylist(parse_master_playlist_res(m3u8_str.as_bytes()).unwrap()), + Playlist::MediaPlaylist(_) => + Playlist::MediaPlaylist(parse_media_playlist_res(m3u8_str.as_bytes()).unwrap()), + }; + + print!("\n\n---- utf8 result\n\n{}", m3u8_str); + print!("\n---- Original\n\n{:?}", playlist_original); + print!("\n\n---- Parsed\n\n{:?}\n\n", playlist_parsed); + + playlist_parsed +} + +#[test] +fn create_and_parse_master_playlist_empty() { + let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { ..Default::default() }); + let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); + assert_eq!(playlist_original, playlist_parsed); +} + +#[test] +fn create_and_parse_master_playlist_full() { + + let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { + version: 6, + variants: vec![ + VariantStream { + is_i_frame: false, + uri: "masterplaylist-uri".into(), + bandwidth: "10010010".into(), + average_bandwidth: Some("10010010".into()), + codecs: "TheCODEC".into(), + resolution: Some("1000x3000".into()), + frame_rate: Some("60".into()), + audio: Some("audio".into()), + video: Some("video".into()), + subtitles: Some("subtitles".into()), + closed_captions: Some("closed_captions".into()), + alternatives: vec! [ + AlternativeMedia { + media_type: AlternativeMediaType::Audio, + uri: Some("alt-media-uri".into()), + group_id: "group-id".into(), + language: Some("language".into()), + assoc_language: Some("assoc-language".into()), + name: "Xmedia".into(), + default: true, // Its absence indicates an implicit value of NO + autoselect: true, // Its absence indicates an implicit value of NO + forced: true, // Its absence indicates an implicit value of NO + instream_id: Some("instream_id".into()), + characteristics: Some("characteristics".into()), + } + ] + } + ], + session_data: Some(SessionData { + data_id: "****".into(), + value: "%%%%".into(), + uri: "++++".into(), + language: Some("SessionDataLanguage".into()), + }), + session_key: Some(SessionKey(Key { + method: "AES-128".into(), + uri: Some("https://secure.domain.com".into()), + iv: Some("0xb059217aa2649ce170b734".into()), + keyformat: Some("xXkeyformatXx".into()), + keyformatversions: Some("xXFormatVers".into()), + })), + start: Some(Start { + time_offset: "123123123".into(), + precise: Some("YES".into()), + }), + independent_segments: true, + }); + let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); + assert_eq!(playlist_original, playlist_parsed); +} + +#[test] +fn create_and_parse_media_playlist_empty() { + let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { ..Default::default() }); + let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); + assert_eq!(playlist_original, playlist_parsed); +} + +#[test] +fn create_and_parse_media_playlist_single_segment() { + let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { + segments: vec![ + MediaSegment { + uri: "20140311T113819-01-338559live.ts".into(), + duration: 2.002, + title: Some("hey".into()), + ..Default::default() + }, + ], + ..Default::default() + }); + let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); + assert_eq!(playlist_original, playlist_parsed); +} + +#[test] +fn create_and_parse_media_playlist_full() { + + let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { + version: 4, + target_duration: 3.0, + media_sequence: 338559, + discontinuity_sequence: 1234, + end_list: true, + playlist_type: Some(MediaPlaylistType::Vod), + i_frames_only: true, + start: Some(Start { + time_offset: "9999".into(), + precise: Some("YES".into()), + }), + independent_segments: true, + segments: vec![ + MediaSegment { + uri: "20140311T113819-01-338559live.ts".into(), + duration: 2.002, + title: Some("338559".into()), + byte_range: Some(ByteRange::from("137116@497036")), + discontinuity: true, + key: Some(Key { + method: "AES-128".into(), + uri: Some("https://secure.domain.com".into()), + iv: Some("0xb059217aa2649ce170b734".into()), + keyformat: Some("xXkeyformatXx".into()), + keyformatversions: Some("xXFormatVers".into()), + }), + map: Some(Map { + uri: "www.map-uri.com".into(), + byte_range: Some(ByteRange::from("137116@497036")), + }), + program_date_time: Some("broodlordinfestorgg".into()), + daterange: None, + }, + ], + }); + let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); + assert_eq!(playlist_original, playlist_parsed); +} \ No newline at end of file