diff --git a/Cargo.toml b/Cargo.toml index 0254a48..7e8545a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,9 @@ documentation = "https://rutgersc.github.io/doc/m3u8_rs/index.html" license = "MIT" [dependencies] -nom = "5.1.0" +nom = { version = "5.1.0", optional = true } + +[features] +default = ["parser"] +parser = ["nom"] + diff --git a/src/lib.rs b/src/lib.rs index b91f567..dcbe36e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,691 +1,9 @@ -//! A library to parse m3u8 playlists (HTTP Live Streaming) [link] -//! (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19). -//! -//! # Examples -//! -//! Parsing a playlist and let the parser figure out if it's a media or master playlist. -//! -//! ``` -//! extern crate nom; -//! extern crate m3u8_rs; -//! use m3u8_rs::playlist::Playlist; -//! use nom::IResult; -//! use std::io::Read; -//! -//! fn main() { -//! let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -//! let mut bytes: Vec = Vec::new(); -//! file.read_to_end(&mut bytes).unwrap(); -//! -//! match m3u8_rs::parse_playlist(&bytes) { -//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), -//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), -//! Result::Err(e) => panic!("Parsing error: \n{}", e), -//! } -//! } -//! ``` -//! -//! Parsing a master playlist directly -//! -//! ``` -//! extern crate nom; -//! extern crate m3u8_rs; -//! use std::io::Read; -//! use nom::IResult; -//! -//! fn main() { -//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap(); -//! let mut bytes: Vec = Vec::new(); -//! file.read_to_end(&mut bytes).unwrap(); -//! -//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) { -//! println!("{:?}", pl); -//! } -//! } -//! -//! ``` -//! -//! Creating a playlist and writing it back to a vec/file -//! -//! ``` -//! extern crate m3u8_rs; -//! use m3u8_rs::playlist::{MediaPlaylist, MediaPlaylistType, MediaSegment}; -//! -//! fn main() { -//! let playlist = MediaPlaylist { -//! version: 6, -//! target_duration: 3.0, -//! media_sequence: 338559, -//! discontinuity_sequence: 1234, -//! end_list: true, -//! playlist_type: Some(MediaPlaylistType::Vod), -//! segments: vec![ -//! MediaSegment { -//! uri: "20140311T113819-01-338559live.ts".into(), -//! duration: 2.002, -//! title: Some("title".into()), -//! ..Default::default() -//! }, -//! ], -//! ..Default::default() -//! }; -//! -//! //let mut v: Vec = Vec::new(); -//! //playlist.write_to(&mut v).unwrap(); -//! -//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -//! //playlist.write_to(&mut file).unwrap(); -//! } -//! -//! ``` - -extern crate nom; - +#[path = "playlist.rs"] pub mod playlist; -use self::nom::character::complete::{digit1, multispace0, space0 }; -use self::nom::{IResult}; -use self::nom::{ delimited,none_of,peek,is_a,is_not,complete,terminated,tag, - alt,do_parse,opt,named,map,map_res,eof,many0,take,take_until,char}; -use self::nom::combinator::map; -use self::nom::character::complete::{line_ending, not_line_ending}; -use std::str; -use std::f32; -use std::string; -use std::str::FromStr; -use std::result::Result; -use std::collections::HashMap; -use playlist::*; +#[path = "parser.rs"] +#[cfg(feature = "parser")] +mod parser; -// ----------------------------------------------------------------------------------------------- -// Playlist parser -// ----------------------------------------------------------------------------------------------- - -/// Parse an m3u8 playlist. -/// -/// # Examples -/// -/// ``` -/// use std::io::Read; -/// use m3u8_rs::playlist::{Playlist}; -/// -/// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -/// let mut bytes: Vec = Vec::new(); -/// file.read_to_end(&mut bytes).unwrap(); -/// -/// let parsed = m3u8_rs::parse_playlist(&bytes); -/// -/// let playlist = match parsed { -/// Result::Ok((i, playlist)) => playlist, -/// Result::Err(e) => panic!("Parsing error: \n{}", e), -/// }; -/// -/// match playlist { -/// 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) { - true => map(parse_master_playlist, Playlist::MasterPlaylist)(input), - false =>map(parse_media_playlist, Playlist::MediaPlaylist)(input), - } -} - -/// Parses an m3u8 playlist just like `parse_playlist`, except that this returns an [std::result::Result](std::result::Result) instead of a [nom::IResult](https://docs.rs/nom/1.2.3/nom/enum.IResult.html). -/// However, since [nom::IResult](nom::IResult) is now an [alias to Result](https://github.com/Geal/nom/blob/master/doc/upgrading_to_nom_5.md), this is no longer needed. -/// -/// # Examples -/// -/// ``` -/// use m3u8_rs::playlist::{Playlist}; -/// use std::io::Read; -/// -/// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -/// let mut bytes: Vec = Vec::new(); -/// file.read_to_end(&mut bytes).unwrap(); -/// -/// 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), -/// Err(e) => println!("Error: {:?}", e) -/// } -/// ``` -pub fn parse_playlist_res(input: &[u8]) -> Result> { - let parse_result = parse_playlist(input); - match parse_result { - IResult::Ok((_, playlist)) => Ok(playlist), - _ => Err(parse_result), - } -} - -/// Parse input as a master playlist -pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> { - map(parse_master_playlist_tags, master_playlist_from_tags)(input) -} - -/// 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::Ok((_, playlist)) => Ok(playlist), - _ => Err(parse_result), - } -} - -/// Parse input as a media playlist -pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> { - map(parse_media_playlist_tags, media_playlist_from_tags)(input) -} - -/// 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::Ok((_, 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 - contains_master_tag(input).map(|t| t.0).unwrap_or(false) -} - -/// Scans input looking for either a master or media `#EXT` tag. -/// -/// Returns `Some(true/false)` when a master/media tag is found. Otherwise returns `None`. -/// -/// - None: Unkown tag or empty line -/// - Some(true, tagstring): Line contains a master playlist tag -/// - Some(false, tagstring): Line contains a media playlist tag -pub fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { - - let mut is_master_opt = None; - let mut current_input: &[u8] = input; - - while is_master_opt == None { - match is_master_playlist_tag_line(current_input) { - IResult::Ok((rest, result)) => { - current_input = rest; - is_master_opt = result; // result can be None (no media or master tag found) - } - _ => break, // Parser error encountered, can't read any more lines. - } - } - - is_master_opt -} - -named!(pub is_master_playlist_tag_line(&[u8]) -> Option<(bool, String)>, - do_parse!( - opt!(is_a!("\r\n")) - >> tag: opt!(alt!( - map!(tag!("#EXT-X-STREAM-INF"), |t| (true, t)) - | map!(tag!("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)) - | map!(terminated!(tag!("#EXT-X-MEDIA"), is_not!("-")), |t| (true, t)) // terminated!() to prevent matching with #EXT-X-MEDIA-SEQUENCE for which we have a separate pattern below - | map!(tag!("#EXT-X-SESSION-KEY"), |t| (true, t)) - | map!(tag!("#EXT-X-SESSION-DATA"), |t| (true, t)) - - | map!(tag!("#EXT-X-TARGETDURATION"), |t| (false, t)) - | map!(tag!("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t)) - | map!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t)) - | map!(tag!("#EXT-X-ENDLIST"), |t| (false, t)) - | map!(tag!("#EXT-X-PLAYLIST-TYPE"), |t| (false, t)) - | map!(tag!("#EXT-X-I-FRAMES-ONLY"), |t| (false, t)) - - | map!(tag!("#EXTINF"), |t| (false, t)) - | map!(tag!("#EXT-X-BYTERANGE"), |t| (false, t)) - | map!(tag!("#EXT-X-DISCONTINUITY"), |t| (false, t)) - | map!(tag!("#EXT-X-KEY"), |t| (false, t)) - | map!(tag!("#EXT-X-MAP"), |t| (false, t)) - | map!(tag!("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t)) - | map!(tag!("#EXT-X-DATERANGE"), |t| (false, t)) - )) - >> consume_line - >> - ( { - tag.map(|(a,b)| (a, from_utf8_slice(b).unwrap())) - } ) - ) -); - -// ----------------------------------------------------------------------------------------------- -// Master Playlist Tags -// ----------------------------------------------------------------------------------------------- - -pub fn parse_master_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec> { - do_parse!(input, - tags: many0!(complete!(do_parse!( m : master_playlist_tag >> multispace0 >> (m) ))) - >> opt!(eof!()) - >> - ( {let mut tags_rev: Vec = tags; tags_rev.reverse(); tags_rev } ) - ) -} - - -/// Contains all the tags required to parse a master playlist. -#[derive(Debug)] -pub enum MasterPlaylistTag { - M3U(String), - Version(usize), - VariantStream(VariantStream), - AlternativeMedia(AlternativeMedia), - SessionData(SessionData), - SessionKey(SessionKey), - Start(Start), - IndependentSegments, - Unknown(ExtTag), - Comment(String), - Uri(String), -} - -pub fn master_playlist_tag(input: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { - alt!(input, - map!(m3u_tag, MasterPlaylistTag::M3U) - | map!(version_tag, MasterPlaylistTag::Version) - - | map!(variant_stream_tag, MasterPlaylistTag::VariantStream) - | map!(variant_i_frame_stream_tag, MasterPlaylistTag::VariantStream) - | map!(alternative_media_tag, MasterPlaylistTag::AlternativeMedia) - | map!(session_data_tag, MasterPlaylistTag::SessionData) - | map!(session_key_tag, MasterPlaylistTag::SessionKey) - | map!(start_tag, MasterPlaylistTag::Start) - | map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MasterPlaylistTag::IndependentSegments) - - | map!(ext_tag, MasterPlaylistTag::Unknown) - | map!(comment_tag, MasterPlaylistTag::Comment) - - | map!(consume_line, MasterPlaylistTag::Uri) - ) -} - -pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist { - let mut master_playlist = MasterPlaylist::default(); - - while let Some(tag) = tags.pop() { - match tag { - MasterPlaylistTag::Version(v) => { - master_playlist.version = v; - } - MasterPlaylistTag::AlternativeMedia(v) => { - master_playlist.alternatives.push(v); - } - MasterPlaylistTag::VariantStream(stream) => { - master_playlist.variants.push(stream); - } - MasterPlaylistTag::Uri(uri) => { - if let Some(stream) = master_playlist.get_newest_variant() { - stream.uri = uri; - } - } - MasterPlaylistTag::SessionData(data) => { - master_playlist.session_data.push(data); - } - MasterPlaylistTag::SessionKey(key) => { - master_playlist.session_key.push(key); - } - MasterPlaylistTag::Start(s) => { - master_playlist.start = Some(s); - } - MasterPlaylistTag::IndependentSegments => { - master_playlist.independent_segments = true; - } - MasterPlaylistTag::Unknown(unknown) => { - master_playlist.unknown_tags.push(unknown); - } - _ => (), - } - } - - master_playlist -} - - -named!(pub variant_stream_tag, - do_parse!(tag!("#EXT-X-STREAM-INF:") >> attributes: key_value_pairs >> - ( VariantStream::from_hashmap(attributes, false))) -); - -named!(pub variant_i_frame_stream_tag, - do_parse!( tag!("#EXT-X-I-FRAME-STREAM-INF:") >> attributes: key_value_pairs >> - ( VariantStream::from_hashmap(attributes, true))) -); - -named!(pub alternative_media_tag, - do_parse!( tag!("#EXT-X-MEDIA:") >> attributes: key_value_pairs >> - ( AlternativeMedia::from_hashmap(attributes))) -); - -named!(pub session_data_tag, - do_parse!( tag!("#EXT-X-SESSION-DATA:") >> - session_data: map_res!(key_value_pairs, |attrs| SessionData::from_hashmap(attrs)) >> - ( session_data)) -); - -named!(pub session_key_tag, - do_parse!( tag!("#EXT-X-SESSION-KEY:") >> session_key: map!(key, SessionKey) >> - ( session_key)) -); - -// ----------------------------------------------------------------------------------------------- -// Media Playlist -// ----------------------------------------------------------------------------------------------- - -pub fn parse_media_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec> { - do_parse!(input, - tags: many0!(complete!(do_parse!(m:media_playlist_tag >> multispace0 >> (m) ))) >> opt!(eof!()) - >> - ( {let mut tags_rev: Vec = tags; tags_rev.reverse(); tags_rev } ) - ) -} - -/// Contains all the tags required to parse a media playlist. -#[derive(Debug)] -pub enum MediaPlaylistTag { - M3U(String), - Version(usize), - Segment(SegmentTag), - TargetDuration(f32), - MediaSequence(i32), - DiscontinuitySequence(i32), - EndList, - PlaylistType(MediaPlaylistType), - IFramesOnly, - Start(Start), - IndependentSegments, -} - -pub fn media_playlist_tag(input: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { - alt!(input, - map!(m3u_tag, MediaPlaylistTag::M3U) - | map!(version_tag, MediaPlaylistTag::Version) - - | map!(do_parse!(tag!("#EXT-X-TARGETDURATION:") >> n:float >> (n)), MediaPlaylistTag::TargetDuration) - | map!(do_parse!(tag!("#EXT-X-MEDIA-SEQUENCE:") >> n:number >> (n)), MediaPlaylistTag::MediaSequence) - | map!(do_parse!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE:") >> n:number >> (n)), MediaPlaylistTag::DiscontinuitySequence) - | map!(do_parse!(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) - ) -} - -pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { - let mut media_playlist = MediaPlaylist::default(); - let mut next_segment = MediaSegment::empty(); - let mut encryption_key = None; - let mut map = None; - - while let Some(tag) = tags.pop() { - - match tag { - MediaPlaylistTag::Version(v) => { - media_playlist.version = v; - } - MediaPlaylistTag::TargetDuration(d) => { - media_playlist.target_duration = d; - } - MediaPlaylistTag::MediaSequence(n) => { - media_playlist.media_sequence = n; - } - MediaPlaylistTag::DiscontinuitySequence(n) => { - media_playlist.discontinuity_sequence = n; - } - MediaPlaylistTag::EndList => { - media_playlist.end_list = true; - } - MediaPlaylistTag::PlaylistType(t) => { - media_playlist.playlist_type = Some(t); - } - MediaPlaylistTag::IFramesOnly => { - media_playlist.i_frames_only = true; - } - MediaPlaylistTag::Start(s) => { - media_playlist.start = Some(s); - } - MediaPlaylistTag::IndependentSegments => { - media_playlist.independent_segments = true; - } - MediaPlaylistTag::Segment(segment_tag) => { - match segment_tag { - SegmentTag::Extinf(d, t) => { - next_segment.duration = d; - next_segment.title = t; - } - SegmentTag::ByteRange(b) => { - next_segment.byte_range = Some(b); - } - SegmentTag::Discontinuity => { - next_segment.discontinuity = true; - } - SegmentTag::Key(k) => { - encryption_key = Some(k); - } - SegmentTag::Map(m) => { - map = Some(m); - } - SegmentTag::ProgramDateTime(d) => { - next_segment.program_date_time = Some(d); - } - SegmentTag::DateRange(d) => { - next_segment.daterange = Some(d); - } - SegmentTag::Unknown(t) => { - media_playlist.unknown_tags.push(t); - } - SegmentTag::Uri(u) => { - next_segment.key = encryption_key.clone(); - next_segment.map = map.clone(); - next_segment.uri = u; - media_playlist.segments.push(next_segment); - next_segment = MediaSegment::empty(); - encryption_key = None; - map = None; - } - _ => (), - } - } - _ => (), - } - } - media_playlist -} - -named!(pub playlist_type, - map_res!( - do_parse!( - p: map_res!(is_not!("\r\n"), str::from_utf8) - >> take!(1) - >> (p) - ), - MediaPlaylistType::from_str - ) -); - -// ----------------------------------------------------------------------------------------------- -// Media Segment -// ----------------------------------------------------------------------------------------------- - -/// All possible media segment tags. -#[derive(Debug)] -pub enum SegmentTag { - Extinf(f32, Option), - ByteRange(ByteRange), - Discontinuity, - Key(Key), - Map(Map), - ProgramDateTime(String), - DateRange(String), - Unknown(ExtTag), - Comment(String), - Uri(String), -} - -pub fn media_segment_tag(input: &[u8]) -> IResult<&[u8], SegmentTag> { - alt!(input, - map!(do_parse!(tag!("#EXTINF:") >> e:duration_title_tag >> (e)), |(a,b)| SegmentTag::Extinf(a,b)) - | map!(do_parse!(tag!("#EXT-X-BYTERANGE:") >> r:byte_range_val >> (r)), SegmentTag::ByteRange) - | map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity) - | map!(do_parse!(tag!("#EXT-X-KEY:") >> k: key >> (k)), SegmentTag::Key) - | map!(do_parse!(tag!("#EXT-X-MAP:") >> m: extmap >> (m)), SegmentTag::Map) - | map!(do_parse!(tag!("#EXT-X-PROGRAM-DATE-TIME:") >> t:consume_line >> (t)), SegmentTag::ProgramDateTime) - | map!(do_parse!(tag!("#EXT-X-DATE-RANGE:") >> t:consume_line >> (t)), SegmentTag::DateRange) - - | map!(ext_tag, SegmentTag::Unknown) - | map!(comment_tag, SegmentTag::Comment) - - | map!(consume_line, SegmentTag::Uri) - ) -} - -named!(pub duration_title_tag<(f32, Option)>, - do_parse!( - duration: float - >> opt!(tag!(",")) - >> title: opt!(map_res!(is_not!("\r\n,"), from_utf8_slice)) - >> take!(1) - >> opt!(tag!(",")) - >> - (duration, title) - ) -); - -named!(pub key, map!(key_value_pairs, Key::from_hashmap)); - -named!(pub extmap, map!(key_value_pairs, |attrs| Map { - uri: attrs.get("URI").map(|u| u.clone()).unwrap_or_default(), - byte_range: attrs.get("BYTERANGE").map(|range| { - match byte_range_val(range.as_bytes()) { - IResult::Ok((_, br)) => br, - _ => panic!("Should not happen"), - } - }), -})); - -// ----------------------------------------------------------------------------------------------- -// Basic tags -// ----------------------------------------------------------------------------------------------- - -named!(pub m3u_tag, - map_res!(tag!("#EXTM3U"), from_utf8_slice) -); - -named!(pub version_tag, - do_parse!( - tag!("#EXT-X-VERSION:") >> version: map_res!(digit1, str::from_utf8) >> - (version.parse().unwrap_or_default()) - ) -); - -named!(pub start_tag, - do_parse!(tag!("#EXT-X-START:") >> attributes:key_value_pairs >> - (Start::from_hashmap(attributes)) - ) -); - -named!(pub ext_tag, - do_parse!( - tag!("#EXT-") - >> tag: map_res!(take_until!(":"), from_utf8_slice) - >> take!(1) - >> rest: map_res!(is_not!("\r\n"), from_utf8_slice) - >> take!(1) - >> - (ExtTag { tag: tag, rest: rest }) - ) -); - -named!(pub comment_tag, - do_parse!( - tag!("#") >> text: map_res!(is_not!("\r\n"), from_utf8_slice) - >> take!(1) - >> (text) - ) -); - -// ----------------------------------------------------------------------------------------------- -// Util -// ----------------------------------------------------------------------------------------------- - -named!(pub key_value_pairs(&[u8]) -> HashMap, - map!( - many0!(do_parse!(space0 >> k:key_value_pair >> (k) )) - , - |pairs: Vec<(String, String)>| { - pairs.into_iter().collect() - } - ) -); - -named!(pub key_value_pair(&[u8]) -> (String, String), - do_parse!( - peek!(none_of!("\r\n")) - >> left: map_res!(take_until!("="), from_utf8_slice) - >> take!(1) - >> right: alt!(quoted | unquoted) - >> opt!(char!(',')) - >> - (left, right) - ) -); - -named!(pub quoted, - delimited!(char!('\"'), map_res!(is_not!("\""), from_utf8_slice), char!('\"')) -); - -named!(pub unquoted, - map_res!(is_not!(",\r\n"), from_utf8_slice) -); - -named!(pub consume_line, - do_parse!( - line: map_res!(not_line_ending, from_utf8_slice) - >> opt!(line_ending) - >> (line) - ) -); - -named!(pub number, - map_res!(map_res!(digit1, str::from_utf8), str::FromStr::from_str) -); - -named!(pub byte_range_val, - do_parse!( - n: number - >> o: opt!(do_parse!(char!('@') >> n:number >> (n) )) >> - (ByteRange { length: n, offset: o }) - ) -); - -named!(pub float, - do_parse!( - left: map_res!(digit1, str::from_utf8) - >> right_opt: opt!(do_parse!(char!('.') >> d:map_res!(digit1, str::from_utf8) >> (d) )) - >> - ( - match right_opt { - Some(right) => { - let mut num = String::from(left); - num.push('.'); - num.push_str(right); - num.parse().unwrap() - }, - None => left.parse().unwrap(), - }) - ) -); - -pub fn from_utf8_slice(s: &[u8]) -> Result { - String::from_utf8(s.to_vec()) -} - -pub fn from_utf8_slice2(s: &[u8]) -> Result { - str::from_utf8(s).map(String::from) -} +#[cfg(feature = "parser")] +pub use self::parser::*; \ No newline at end of file diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..6fe3fb5 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,683 @@ +//! A library to parse m3u8 playlists (HTTP Live Streaming) [link] +//! (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19). +//! +//! # Examples +//! +//! Parsing a playlist and let the parser figure out if it's a media or master playlist. +//! +//! ``` +//! extern crate nom; +//! extern crate m3u8_rs; +//! use m3u8_rs::playlist::Playlist; +//! use nom::IResult; +//! use std::io::Read; +//! +//! fn main() { +//! let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +//! let mut bytes: Vec = Vec::new(); +//! file.read_to_end(&mut bytes).unwrap(); +//! +//! match m3u8_rs::parse_playlist(&bytes) { +//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), +//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), +//! Result::Err(e) => panic!("Parsing error: \n{}", e), +//! } +//! } +//! ``` +//! +//! Parsing a master playlist directly +//! +//! ``` +//! extern crate nom; +//! extern crate m3u8_rs; +//! use std::io::Read; +//! use nom::IResult; +//! +//! fn main() { +//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap(); +//! let mut bytes: Vec = Vec::new(); +//! file.read_to_end(&mut bytes).unwrap(); +//! +//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) { +//! println!("{:?}", pl); +//! } +//! } +//! +//! ``` +//! +//! Creating a playlist and writing it back to a vec/file +//! +//! ``` +//! extern crate m3u8_rs; +//! use m3u8_rs::playlist::{MediaPlaylist, MediaPlaylistType, MediaSegment}; +//! +//! fn main() { +//! let playlist = MediaPlaylist { +//! version: 6, +//! target_duration: 3.0, +//! media_sequence: 338559, +//! discontinuity_sequence: 1234, +//! end_list: true, +//! playlist_type: Some(MediaPlaylistType::Vod), +//! segments: vec![ +//! MediaSegment { +//! uri: "20140311T113819-01-338559live.ts".into(), +//! duration: 2.002, +//! title: Some("title".into()), +//! ..Default::default() +//! }, +//! ], +//! ..Default::default() +//! }; +//! +//! //let mut v: Vec = Vec::new(); +//! //playlist.write_to(&mut v).unwrap(); +//! +//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +//! //playlist.write_to(&mut file).unwrap(); +//! } +//! +//! ``` + +extern crate nom; + +pub mod playlist; + +use self::nom::character::complete::{digit1, multispace0, space0 }; +use self::nom::{IResult}; +use self::nom::{ delimited,none_of,peek,is_a,is_not,complete,terminated,tag, + alt,do_parse,opt,named,map,map_res,eof,many0,take,take_until,char}; +use self::nom::combinator::map; +use self::nom::character::complete::{line_ending, not_line_ending}; +use std::str; +use std::f32; +use std::string; +use std::str::FromStr; +use std::result::Result; +use std::collections::HashMap; +use playlist::*; + +/// Parse an m3u8 playlist. +/// +/// # Examples +/// +/// ``` +/// use std::io::Read; +/// use m3u8_rs::playlist::{Playlist}; +/// +/// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +/// let mut bytes: Vec = Vec::new(); +/// file.read_to_end(&mut bytes).unwrap(); +/// +/// let parsed = m3u8_rs::parse_playlist(&bytes); +/// +/// let playlist = match parsed { +/// Result::Ok((i, playlist)) => playlist, +/// Result::Err(e) => panic!("Parsing error: \n{}", e), +/// }; +/// +/// match playlist { +/// 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) { + true => map(parse_master_playlist, Playlist::MasterPlaylist)(input), + false =>map(parse_media_playlist, Playlist::MediaPlaylist)(input), + } +} + +/// Parses an m3u8 playlist just like `parse_playlist`, except that this returns an [std::result::Result](std::result::Result) instead of a [nom::IResult](https://docs.rs/nom/1.2.3/nom/enum.IResult.html). +/// However, since [nom::IResult](nom::IResult) is now an [alias to Result](https://github.com/Geal/nom/blob/master/doc/upgrading_to_nom_5.md), this is no longer needed. +/// +/// # Examples +/// +/// ``` +/// use m3u8_rs::playlist::{Playlist}; +/// use std::io::Read; +/// +/// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +/// let mut bytes: Vec = Vec::new(); +/// file.read_to_end(&mut bytes).unwrap(); +/// +/// 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), +/// Err(e) => println!("Error: {:?}", e) +/// } +/// ``` +pub fn parse_playlist_res(input: &[u8]) -> Result> { + let parse_result = parse_playlist(input); + match parse_result { + IResult::Ok((_, playlist)) => Ok(playlist), + _ => Err(parse_result), + } +} + +/// Parse input as a master playlist +pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> { + map(parse_master_playlist_tags, master_playlist_from_tags)(input) +} + +/// 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::Ok((_, playlist)) => Ok(playlist), + _ => Err(parse_result), + } +} + +/// Parse input as a media playlist +pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> { + map(parse_media_playlist_tags, media_playlist_from_tags)(input) +} + +/// 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::Ok((_, 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 + contains_master_tag(input).map(|t| t.0).unwrap_or(false) +} + +/// Scans input looking for either a master or media `#EXT` tag. +/// +/// Returns `Some(true/false)` when a master/media tag is found. Otherwise returns `None`. +/// +/// - None: Unkown tag or empty line +/// - Some(true, tagstring): Line contains a master playlist tag +/// - Some(false, tagstring): Line contains a media playlist tag +pub fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { + + let mut is_master_opt = None; + let mut current_input: &[u8] = input; + + while is_master_opt == None { + match is_master_playlist_tag_line(current_input) { + IResult::Ok((rest, result)) => { + current_input = rest; + is_master_opt = result; // result can be None (no media or master tag found) + } + _ => break, // Parser error encountered, can't read any more lines. + } + } + + is_master_opt +} + +named!(pub is_master_playlist_tag_line(&[u8]) -> Option<(bool, String)>, + do_parse!( + opt!(is_a!("\r\n")) + >> tag: opt!(alt!( + map!(tag!("#EXT-X-STREAM-INF"), |t| (true, t)) + | map!(tag!("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)) + | map!(terminated!(tag!("#EXT-X-MEDIA"), is_not!("-")), |t| (true, t)) // terminated!() to prevent matching with #EXT-X-MEDIA-SEQUENCE for which we have a separate pattern below + | map!(tag!("#EXT-X-SESSION-KEY"), |t| (true, t)) + | map!(tag!("#EXT-X-SESSION-DATA"), |t| (true, t)) + + | map!(tag!("#EXT-X-TARGETDURATION"), |t| (false, t)) + | map!(tag!("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t)) + | map!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t)) + | map!(tag!("#EXT-X-ENDLIST"), |t| (false, t)) + | map!(tag!("#EXT-X-PLAYLIST-TYPE"), |t| (false, t)) + | map!(tag!("#EXT-X-I-FRAMES-ONLY"), |t| (false, t)) + + | map!(tag!("#EXTINF"), |t| (false, t)) + | map!(tag!("#EXT-X-BYTERANGE"), |t| (false, t)) + | map!(tag!("#EXT-X-DISCONTINUITY"), |t| (false, t)) + | map!(tag!("#EXT-X-KEY"), |t| (false, t)) + | map!(tag!("#EXT-X-MAP"), |t| (false, t)) + | map!(tag!("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t)) + | map!(tag!("#EXT-X-DATERANGE"), |t| (false, t)) + )) + >> consume_line + >> + ( { + tag.map(|(a,b)| (a, from_utf8_slice(b).unwrap())) + } ) + ) +); + +// ----------------------------------------------------------------------------------------------- +// Master Playlist Tags +// ----------------------------------------------------------------------------------------------- + +pub fn parse_master_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec> { + do_parse!(input, + tags: many0!(complete!(do_parse!( m : master_playlist_tag >> multispace0 >> (m) ))) + >> opt!(eof!()) + >> + ( {let mut tags_rev: Vec = tags; tags_rev.reverse(); tags_rev } ) + ) +} + + +/// Contains all the tags required to parse a master playlist. +#[derive(Debug)] +pub enum MasterPlaylistTag { + M3U(String), + Version(usize), + VariantStream(VariantStream), + AlternativeMedia(AlternativeMedia), + SessionData(SessionData), + SessionKey(SessionKey), + Start(Start), + IndependentSegments, + Unknown(ExtTag), + Comment(String), + Uri(String), +} + +pub fn master_playlist_tag(input: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { + alt!(input, + map!(m3u_tag, MasterPlaylistTag::M3U) + | map!(version_tag, MasterPlaylistTag::Version) + + | map!(variant_stream_tag, MasterPlaylistTag::VariantStream) + | map!(variant_i_frame_stream_tag, MasterPlaylistTag::VariantStream) + | map!(alternative_media_tag, MasterPlaylistTag::AlternativeMedia) + | map!(session_data_tag, MasterPlaylistTag::SessionData) + | map!(session_key_tag, MasterPlaylistTag::SessionKey) + | map!(start_tag, MasterPlaylistTag::Start) + | map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MasterPlaylistTag::IndependentSegments) + + | map!(ext_tag, MasterPlaylistTag::Unknown) + | map!(comment_tag, MasterPlaylistTag::Comment) + + | map!(consume_line, MasterPlaylistTag::Uri) + ) +} + +pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist { + let mut master_playlist = MasterPlaylist::default(); + + while let Some(tag) = tags.pop() { + match tag { + MasterPlaylistTag::Version(v) => { + master_playlist.version = v; + } + MasterPlaylistTag::AlternativeMedia(v) => { + master_playlist.alternatives.push(v); + } + MasterPlaylistTag::VariantStream(stream) => { + master_playlist.variants.push(stream); + } + MasterPlaylistTag::Uri(uri) => { + if let Some(stream) = master_playlist.get_newest_variant() { + stream.uri = uri; + } + } + MasterPlaylistTag::SessionData(data) => { + master_playlist.session_data.push(data); + } + MasterPlaylistTag::SessionKey(key) => { + master_playlist.session_key.push(key); + } + MasterPlaylistTag::Start(s) => { + master_playlist.start = Some(s); + } + MasterPlaylistTag::IndependentSegments => { + master_playlist.independent_segments = true; + } + MasterPlaylistTag::Unknown(unknown) => { + master_playlist.unknown_tags.push(unknown); + } + _ => (), + } + } + + master_playlist +} + + +named!(pub variant_stream_tag, + do_parse!(tag!("#EXT-X-STREAM-INF:") >> attributes: key_value_pairs >> + ( VariantStream::from_hashmap(attributes, false))) +); + +named!(pub variant_i_frame_stream_tag, + do_parse!( tag!("#EXT-X-I-FRAME-STREAM-INF:") >> attributes: key_value_pairs >> + ( VariantStream::from_hashmap(attributes, true))) +); + +named!(pub alternative_media_tag, + do_parse!( tag!("#EXT-X-MEDIA:") >> attributes: key_value_pairs >> + ( AlternativeMedia::from_hashmap(attributes))) +); + +named!(pub session_data_tag, + do_parse!( tag!("#EXT-X-SESSION-DATA:") >> + session_data: map_res!(key_value_pairs, |attrs| SessionData::from_hashmap(attrs)) >> + ( session_data)) +); + +named!(pub session_key_tag, + do_parse!( tag!("#EXT-X-SESSION-KEY:") >> session_key: map!(key, SessionKey) >> + ( session_key)) +); + +// ----------------------------------------------------------------------------------------------- +// Media Playlist +// ----------------------------------------------------------------------------------------------- + +pub fn parse_media_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec> { + do_parse!(input, + tags: many0!(complete!(do_parse!(m:media_playlist_tag >> multispace0 >> (m) ))) >> opt!(eof!()) + >> + ( {let mut tags_rev: Vec = tags; tags_rev.reverse(); tags_rev } ) + ) +} + +/// Contains all the tags required to parse a media playlist. +#[derive(Debug)] +pub enum MediaPlaylistTag { + M3U(String), + Version(usize), + Segment(SegmentTag), + TargetDuration(f32), + MediaSequence(i32), + DiscontinuitySequence(i32), + EndList, + PlaylistType(MediaPlaylistType), + IFramesOnly, + Start(Start), + IndependentSegments, +} + +pub fn media_playlist_tag(input: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { + alt!(input, + map!(m3u_tag, MediaPlaylistTag::M3U) + | map!(version_tag, MediaPlaylistTag::Version) + + | map!(do_parse!(tag!("#EXT-X-TARGETDURATION:") >> n:float >> (n)), MediaPlaylistTag::TargetDuration) + | map!(do_parse!(tag!("#EXT-X-MEDIA-SEQUENCE:") >> n:number >> (n)), MediaPlaylistTag::MediaSequence) + | map!(do_parse!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE:") >> n:number >> (n)), MediaPlaylistTag::DiscontinuitySequence) + | map!(do_parse!(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) + ) +} + +pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { + let mut media_playlist = MediaPlaylist::default(); + let mut next_segment = MediaSegment::empty(); + let mut encryption_key = None; + let mut map = None; + + while let Some(tag) = tags.pop() { + + match tag { + MediaPlaylistTag::Version(v) => { + media_playlist.version = v; + } + MediaPlaylistTag::TargetDuration(d) => { + media_playlist.target_duration = d; + } + MediaPlaylistTag::MediaSequence(n) => { + media_playlist.media_sequence = n; + } + MediaPlaylistTag::DiscontinuitySequence(n) => { + media_playlist.discontinuity_sequence = n; + } + MediaPlaylistTag::EndList => { + media_playlist.end_list = true; + } + MediaPlaylistTag::PlaylistType(t) => { + media_playlist.playlist_type = Some(t); + } + MediaPlaylistTag::IFramesOnly => { + media_playlist.i_frames_only = true; + } + MediaPlaylistTag::Start(s) => { + media_playlist.start = Some(s); + } + MediaPlaylistTag::IndependentSegments => { + media_playlist.independent_segments = true; + } + MediaPlaylistTag::Segment(segment_tag) => { + match segment_tag { + SegmentTag::Extinf(d, t) => { + next_segment.duration = d; + next_segment.title = t; + } + SegmentTag::ByteRange(b) => { + next_segment.byte_range = Some(b); + } + SegmentTag::Discontinuity => { + next_segment.discontinuity = true; + } + SegmentTag::Key(k) => { + encryption_key = Some(k); + } + SegmentTag::Map(m) => { + map = Some(m); + } + SegmentTag::ProgramDateTime(d) => { + next_segment.program_date_time = Some(d); + } + SegmentTag::DateRange(d) => { + next_segment.daterange = Some(d); + } + SegmentTag::Unknown(t) => { + media_playlist.unknown_tags.push(t); + } + SegmentTag::Uri(u) => { + next_segment.key = encryption_key.clone(); + next_segment.map = map.clone(); + next_segment.uri = u; + media_playlist.segments.push(next_segment); + next_segment = MediaSegment::empty(); + encryption_key = None; + map = None; + } + _ => (), + } + } + _ => (), + } + } + media_playlist +} + +named!(pub playlist_type, + map_res!( + do_parse!( + p: map_res!(is_not!("\r\n"), str::from_utf8) + >> take!(1) + >> (p) + ), + MediaPlaylistType::from_str + ) +); + +// ----------------------------------------------------------------------------------------------- +// Media Segment +// ----------------------------------------------------------------------------------------------- + +/// All possible media segment tags. +#[derive(Debug)] +pub enum SegmentTag { + Extinf(f32, Option), + ByteRange(ByteRange), + Discontinuity, + Key(Key), + Map(Map), + ProgramDateTime(String), + DateRange(String), + Unknown(ExtTag), + Comment(String), + Uri(String), +} + +pub fn media_segment_tag(input: &[u8]) -> IResult<&[u8], SegmentTag> { + alt!(input, + map!(do_parse!(tag!("#EXTINF:") >> e:duration_title_tag >> (e)), |(a,b)| SegmentTag::Extinf(a,b)) + | map!(do_parse!(tag!("#EXT-X-BYTERANGE:") >> r:byte_range_val >> (r)), SegmentTag::ByteRange) + | map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity) + | map!(do_parse!(tag!("#EXT-X-KEY:") >> k: key >> (k)), SegmentTag::Key) + | map!(do_parse!(tag!("#EXT-X-MAP:") >> m: extmap >> (m)), SegmentTag::Map) + | map!(do_parse!(tag!("#EXT-X-PROGRAM-DATE-TIME:") >> t:consume_line >> (t)), SegmentTag::ProgramDateTime) + | map!(do_parse!(tag!("#EXT-X-DATE-RANGE:") >> t:consume_line >> (t)), SegmentTag::DateRange) + + | map!(ext_tag, SegmentTag::Unknown) + | map!(comment_tag, SegmentTag::Comment) + + | map!(consume_line, SegmentTag::Uri) + ) +} + +named!(pub duration_title_tag<(f32, Option)>, + do_parse!( + duration: float + >> opt!(tag!(",")) + >> title: opt!(map_res!(is_not!("\r\n,"), from_utf8_slice)) + >> take!(1) + >> opt!(tag!(",")) + >> + (duration, title) + ) +); + +named!(pub key, map!(key_value_pairs, Key::from_hashmap)); + +named!(pub extmap, map!(key_value_pairs, |attrs| Map { + uri: attrs.get("URI").map(|u| u.clone()).unwrap_or_default(), + byte_range: attrs.get("BYTERANGE").map(|range| { + match byte_range_val(range.as_bytes()) { + IResult::Ok((_, br)) => br, + _ => panic!("Should not happen"), + } + }), +})); + +// ----------------------------------------------------------------------------------------------- +// Basic tags +// ----------------------------------------------------------------------------------------------- + +named!(pub m3u_tag, + map_res!(tag!("#EXTM3U"), from_utf8_slice) +); + +named!(pub version_tag, + do_parse!( + tag!("#EXT-X-VERSION:") >> version: map_res!(digit1, str::from_utf8) >> + (version.parse().unwrap_or_default()) + ) +); + +named!(pub start_tag, + do_parse!(tag!("#EXT-X-START:") >> attributes:key_value_pairs >> + (Start::from_hashmap(attributes)) + ) +); + +named!(pub ext_tag, + do_parse!( + tag!("#EXT-") + >> tag: map_res!(take_until!(":"), from_utf8_slice) + >> take!(1) + >> rest: map_res!(is_not!("\r\n"), from_utf8_slice) + >> take!(1) + >> + (ExtTag { tag: tag, rest: rest }) + ) +); + +named!(pub comment_tag, + do_parse!( + tag!("#") >> text: map_res!(is_not!("\r\n"), from_utf8_slice) + >> take!(1) + >> (text) + ) +); + +// ----------------------------------------------------------------------------------------------- +// Util +// ----------------------------------------------------------------------------------------------- + +named!(pub key_value_pairs(&[u8]) -> HashMap, + map!( + many0!(do_parse!(space0 >> k:key_value_pair >> (k) )) + , + |pairs: Vec<(String, String)>| { + pairs.into_iter().collect() + } + ) +); + +named!(pub key_value_pair(&[u8]) -> (String, String), + do_parse!( + peek!(none_of!("\r\n")) + >> left: map_res!(take_until!("="), from_utf8_slice) + >> take!(1) + >> right: alt!(quoted | unquoted) + >> opt!(char!(',')) + >> + (left, right) + ) +); + +named!(pub quoted, + delimited!(char!('\"'), map_res!(is_not!("\""), from_utf8_slice), char!('\"')) +); + +named!(pub unquoted, + map_res!(is_not!(",\r\n"), from_utf8_slice) +); + +named!(pub consume_line, + do_parse!( + line: map_res!(not_line_ending, from_utf8_slice) + >> opt!(line_ending) + >> (line) + ) +); + +named!(pub number, + map_res!(map_res!(digit1, str::from_utf8), str::FromStr::from_str) +); + +named!(pub byte_range_val, + do_parse!( + n: number + >> o: opt!(do_parse!(char!('@') >> n:number >> (n) )) >> + (ByteRange { length: n, offset: o }) + ) +); + +named!(pub float, + do_parse!( + left: map_res!(digit1, str::from_utf8) + >> right_opt: opt!(do_parse!(char!('.') >> d:map_res!(digit1, str::from_utf8) >> (d) )) + >> + ( + match right_opt { + Some(right) => { + let mut num = String::from(left); + num.push('.'); + num.push_str(right); + num.parse().unwrap() + }, + None => left.parse().unwrap(), + }) + ) +); + +pub fn from_utf8_slice(s: &[u8]) -> Result { + String::from_utf8(s.to_vec()) +}