diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1822426 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "m3u8-rs" +version = "1.0.0" +authors = ["Rutger"] + +[dependencies] +nom = "^1.2.3" diff --git a/README.md b/README.md index 6d5d134..f40284c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,136 @@ # m3u8-rs -m3u8 parser for rust +A Rust library for parsing m3u8 playlists (HTTP Live Streaming) [link](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19). +Uses the [`nom` library](https://github.com/Geal/nom) for all of the parsing. + +# Installation +To use this library, add the following dependency to `Cargo.toml`: + +```toml +[dependencies] +m3u8-rs = "1.0.0" +``` + +And add the crate to `lib.rs` + +```rust +extern crate m3u8_rs; +``` + +Also available on [crates.io]() + +# Documentation + +Available [here]() + +# Examples + +A simple example of parsing a playlist: + +```rust +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) +} + +``` + +In the example above, `parse_playlist_res(&bytes)` returns a `Result`. It uses +the output of `parse_playlist(&bytes)` behind the scenes and just converts the `IResult` to a `Result`. +Here is an example of using the `parse_playlist(&bytes)` with `IResult` directly: + +```rust +use m3u8_rs::playlist::Playlist; +use std::io::Read; +use nom::IResult; + +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::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::Error(e) => panic!("Parsing error: \n{}", e), + IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e), +} +``` + +Currently the parser will succeed even if REQUIRED attributes/tags are missing from a playlist (such as the `#EXT-X-VERSION` tag). +The option to abort parsing when attributes/tags are missing may be something to add later on. + +# Structure Summary + +All of the details about the structs are taken from https://tools.ietf.org/html/draft-pantos-http-live-streaming-19. + + +```rust + +// Short summary of the important structs in playlist.rs: +// +pub enum Playlist { + MasterPlaylist(MasterPlaylist), + MediaPlaylist(MediaPlaylist), +} + +pub struct MasterPlaylist { + pub version: usize, + pub variants: Vec, + pub session_data: Option, + pub session_key: Option, + pub start: Option, + pub independent_segments: bool, +} + +pub struct MediaPlaylist { + pub version: usize, + pub target_duration: f32, + pub media_sequence: i32, + pub segments: Vec, + pub discontinuity_sequence: i32, + pub end_list: bool, + pub playlist_type: MediaPlaylistType, + pub i_frames_only: bool, + pub start: Option, + pub independent_segments: bool, +} + +pub struct VariantStream { + pub is_i_frame: bool, + pub uri: String, + pub bandwidth: String, + pub average_bandwidth: Option, + pub codecs: String, + pub resolution: Option, + pub frame_rate: Option, + pub audio: Option, + pub video: Option, + pub subtitles: Option, + pub closed_captions: Option, + pub alternatives: Vec, +} + +pub struct MediaSegment { + pub uri: String, + pub duration: f32, + pub title: Option, + pub byte_range: Option, + pub discontinuity: bool, + pub key: Option, + pub map: Option, + pub program_date_time: Option, + pub daterange: Option, +} + +``` diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..cbec9a3 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,19 @@ +extern crate nom; +extern crate m3u8_rs; + +use m3u8_rs::playlist::{Playlist}; +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(); + + 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) + } +} diff --git a/examples/with_nom_result.rs b/examples/with_nom_result.rs new file mode 100644 index 0000000..8272e01 --- /dev/null +++ b/examples/with_nom_result.rs @@ -0,0 +1,40 @@ +extern crate nom; +extern crate m3u8_rs; + +use m3u8_rs::playlist::{Playlist}; +use std::io::Read; +use nom::IResult; + +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(); + + let parsed = m3u8_rs::parse_playlist(&bytes); + + let playlist = match parsed { + IResult::Done(i, playlist) => playlist, + IResult::Error(e) => panic!("Parsing error: \n{}", e), + IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e), + }; + + match playlist { + Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl), + Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl), + } +} + +fn main_alt() { + 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); + + 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::Error(e) => panic!("Parsing error: \n{}", e), + IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e), + } +} diff --git a/masterplaylist.m3u8 b/masterplaylist.m3u8 new file mode 100644 index 0000000..c29c0d6 --- /dev/null +++ b/masterplaylist.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000 +http://example.com/low.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000 +http://example.com/mid.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000 +http://example.com/hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" +http://example.com/audio-only.m3u8 \ No newline at end of file diff --git a/mediaplaylist.m3u8 b/mediaplaylist.m3u8 new file mode 100644 index 0000000..b91c02c --- /dev/null +++ b/mediaplaylist.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:7794 +#EXT-X-TARGETDURATION:15 + +#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" + +#EXTINF:2.833, +http://media.example.com/fileSequence52-A.ts +#EXTINF:15.0, +http://media.example.com/fileSequence52-B.ts +#EXTINF:13.333, +http://media.example.com/fileSequence52-C.ts + +#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" + +#EXTINF:15.0, +http://media.example.com/fileSequence53-A.ts \ No newline at end of file diff --git a/playlist.m3u8 b/playlist.m3u8 new file mode 100644 index 0000000..37eea46 --- /dev/null +++ b/playlist.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXTINF:9.009, +http://media.example.com/first.ts +#EXTINF:9.009, +http://media.example.com/second.ts +#EXTINF:3.003, +http://media.example.com/third.ts +#EXT-X-ENDLIST \ No newline at end of file diff --git a/sample-playlists/master-usher_result.m3u8 b/sample-playlists/master-usher_result.m3u8 new file mode 100644 index 0000000..cac163a --- /dev/null +++ b/sample-playlists/master-usher_result.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-TWITCH-INFO:NODE="video47.lhr02",MANIFEST-NODE="video47.lhr02",SERVER-TIME="1463829370.34",USER-IP="145.87.245.122",CLUSTER="lhr02",STREAM-TIME="39299.3432069",MANIFEST-CLUSTER="lhr02" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="Source",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3960281,RESOLUTION=1280x720,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked" +http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/chunked/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=chunked&sig=2491212c28bdc97e74c8755d6cc18f8bdd971f30 +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="high",NAME="High",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1760000,RESOLUTION=1280x720,CODECS="avc1.66.31,mp4a.40.2",VIDEO="high" +http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/high/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=high&sig=be34da4d70ab7cabe773f12a37d863565efe2b46 +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="medium",NAME="Medium",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=928000,RESOLUTION=852x480,CODECS="avc1.66.31,mp4a.40.2",VIDEO="medium" +http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/medium/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=medium&sig=88ee9e7c5416096599e0b46017d7de82f876b8ae +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Low",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=596000,RESOLUTION=640x360,CODECS="avc1.66.31,mp4a.40.2",VIDEO="low" +http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/low/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=low&sig=79398c8f2e8636720d59aa2310e19eff6ab42d46 +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mobile",NAME="Mobile",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=164000,RESOLUTION=400x226,CODECS="avc1.66.31,mp4a.40.2",VIDEO="mobile" +http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/mobile/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=mobile&sig=cc9f889823f25969c8b8d911ea9193dc33ce41dc +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="Audio Only",AUTOSELECT=NO,DEFAULT=NO +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,CODECS="mp4a.40.2",VIDEO="audio_only" +http://video47.lhr02.hls.ttvnw.net/hls32/itmejp_21425103760_456233939/audio_only/index-live.m3u8?token=id=1079441765470438139,bid=21425103760,exp=1463915770,node=video47-1.lhr02.hls.justin.tv,nname=video47.lhr02,fmt=audio_only&sig=e3179b7cacb9530df951f8d683ff02a493100849 diff --git a/sample-playlists/master-with-alternatives.m3u8 b/sample-playlists/master-with-alternatives.m3u8 new file mode 100644 index 0000000..ea6ea2f --- /dev/null +++ b/sample-playlists/master-with-alternatives.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="low" +low/main/audio-video.m3u8 +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="mid" +mid/main/audio-video.m3u8 +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=7680000,VIDEO="hi" +hi/main/audio-video.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" +main/audio-only.m3u8 diff --git a/sample-playlists/master-with-i-frame-stream-inf.m3u8 b/sample-playlists/master-with-i-frame-stream-inf.m3u8 new file mode 100644 index 0000000..73c89d6 --- /dev/null +++ b/sample-playlists/master-with-i-frame-stream-inf.m3u8 @@ -0,0 +1,13 @@ +#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=1280000 +low/audio-video.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8",PROGRAM-ID=1,CODECS="c1",RESOLUTION="1x1",VIDEO="1" +#EXT-X-STREAM-INF:BANDWIDTH=2560000 +mid/audio-video.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8",PROGRAM-ID=1,CODECS="c2",RESOLUTION="2x2",VIDEO="2" +#EXT-X-STREAM-INF:BANDWIDTH=7680000 +hi/audio-video.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8",PROGRAM-ID=1,CODECS="c2",RESOLUTION="2x2",VIDEO="2" +#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" +audio-only.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH="INVALIDBW",URI="hi/iframe.m3u8",PROGRAM-ID=1,CODECS="c2",RESOLUTION="2x2",VIDEO="2" diff --git a/sample-playlists/master-with-multiple-codecs.m3u8 b/sample-playlists/master-with-multiple-codecs.m3u8 new file mode 100644 index 0000000..05315ba --- /dev/null +++ b/sample-playlists/master-with-multiple-codecs.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="avc1.42c015,mp4a.40.2" +chunklist-b300000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000,CODECS="avc1.42c015,mp4a.40.2" +chunklist-b600000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000,CODECS="avc1.42c015,mp4a.40.2" +chunklist-b850000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,CODECS="avc1.42c015,mp4a.40.2" +chunklist-b1000000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000,CODECS="avc1.42c015,mp4a.40.2" +chunklist-b1500000.m3u8 diff --git a/sample-playlists/master-with-offset.m3u8 b/sample-playlists/master-with-offset.m3u8 new file mode 100644 index 0000000..c3799a0 --- /dev/null +++ b/sample-playlists/master-with-offset.m3u8 @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-START:TIME-OFFSET=20.000 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=181000,RESOLUTION=320x180,CODECS="avc1.66.30, mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_0_av.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=318000,RESOLUTION=480x270,CODECS="avc1.66.30, mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_1_av.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=592000,RESOLUTION=512x288,CODECS="avc1.77.30, mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_2_av.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1179000,RESOLUTION=640x360,CODECS="avc1.77.30, mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_3_av.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1915000,RESOLUTION=960x540,CODECS="avc1.77.30, mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_4_av.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3404000,RESOLUTION=1280x720,CODECS="avc1.77.30, mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_5_av.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=55000,CODECS="mp4a.40.2",CLOSED-CAPTIONS=NONE +http://hls.tagesschau.de/i/video/2015/0902/TV-20150902-1214-0601.,webs,websm,webm,webml,webl,webxl,.h264.mp4.csmil/index_0_a.m3u8 diff --git a/sample-playlists/master-with-stream-inf-name.m3u8 b/sample-playlists/master-with-stream-inf-name.m3u8 new file mode 100644 index 0000000..1819969 --- /dev/null +++ b/sample-playlists/master-with-stream-inf-name.m3u8 @@ -0,0 +1,10 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1828000,NAME="3 high",RESOLUTION=896x504 +chunklist_b1828000_t64NCBoaWdo.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=678000,NAME="2 med",RESOLUTION=512x288 +chunklist_b678000_t64MiBtZWQ=.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=438000,NAME="1 low",RESOLUTION=384x216 +chunklist_b438000_t64MSBsb3c=.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,NAME="0 audio" +chunklist_b128000_t64MCBhdWRpbw==.m3u8 diff --git a/sample-playlists/master.m3u8 b/sample-playlists/master.m3u8 new file mode 100644 index 0000000..26fea95 --- /dev/null +++ b/sample-playlists/master.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-STREAM-INF:PROGRAM-ID=9,BANDWIDTH=300000, +chunklist-b300000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000 +chunklist-b600000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000 +chunklist-b850000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000 +chunklist-b1000000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000 +chunklist-b1500000.m3u8 diff --git a/sample-playlists/masterplaylist.m3u8 b/sample-playlists/masterplaylist.m3u8 new file mode 100644 index 0000000..90139f7 --- /dev/null +++ b/sample-playlists/masterplaylist.m3u8 @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="Audio1",NAME="mp4a.40.2_96K_Spanish",LANGUAGE="spa",DEFAULT=YES,AUTOSELECT=YES,URI="A1.m3u8" +A1.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=395000,CODECS="avc1.4d001f,mp4a.40.2",AUDIO="Audio1",RESOLUTION=320x240 +01.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=394000,CODECS="avc1.4d001f,mp4a.40.2",URI="01_iframe_index.m3u8" +01_iframe_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=963000,CODECS="avc1.4d001f,mp4a.40.2",AUDIO="Audio1",RESOLUTION=448x336 +02.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=962000,CODECS="avc1.4d001f,mp4a.40.2",URI="02_iframe_index.m3u8" +02_iframe_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1695000,CODECS="avc1.4d001f,mp4a.40.2",AUDIO="Audio1",RESOLUTION=640x480 +03.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1694000,CODECS="avc1.4d001f,mp4a.40.2",URI="03_iframe_index.m3u8" +03_iframe_index.m3u8 diff --git a/sample-playlists/masterplaylist2.m3u8 b/sample-playlists/masterplaylist2.m3u8 new file mode 100644 index 0000000..dfa56c9 --- /dev/null +++ b/sample-playlists/masterplaylist2.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000 +gear1/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=311111 +gear2/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=484444 +gear3/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=737777 +gear4/prog_index.m3u8 diff --git a/sample-playlists/media-playlist-with-byterange.m3u8 b/sample-playlists/media-playlist-with-byterange.m3u8 new file mode 100644 index 0000000..743901d --- /dev/null +++ b/sample-playlists/media-playlist-with-byterange.m3u8 @@ -0,0 +1,13 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.0, +#EXT-X-BYTERANGE:75232@0 +video.ts +#EXT-X-BYTERANGE:82112@752321 +#EXTINF:10.0, +video.ts +#EXTINF:10.0, +#EXT-X-BYTERANGE:69864 +video.ts diff --git a/sample-playlists/media-playlist-with-discontinuity.m3u8 b/sample-playlists/media-playlist-with-discontinuity.m3u8 new file mode 100644 index 0000000..a570537 --- /dev/null +++ b/sample-playlists/media-playlist-with-discontinuity.m3u8 @@ -0,0 +1,15 @@ +# https://developer.apple.com/library/ios/technotes/tn2288/_index.html +# +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.0, +ad0.ts +#EXTINF:8.0, +ad1.ts +#EXT-X-DISCONTINUITY +#EXTINF:10.0, +movieA.ts +#EXTINF:10.0, +movieB.ts \ No newline at end of file diff --git a/sample-playlists/media-playlist-with-scte35-1.m3u8 b/sample-playlists/media-playlist-with-scte35-1.m3u8 new file mode 100644 index 0000000..fea864c --- /dev/null +++ b/sample-playlists/media-playlist-with-scte35-1.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.000, +media0.ts +#EXT-SCTE35: CUE="/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAA" +#EXTINF:10.000, +media1.ts +#EXTINF:10.000, +media2.ts diff --git a/sample-playlists/media-playlist-with-scte35.m3u8 b/sample-playlists/media-playlist-with-scte35.m3u8 new file mode 100644 index 0000000..f47c907 --- /dev/null +++ b/sample-playlists/media-playlist-with-scte35.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.000, +media0.ts +#EXTINF:10.000, +media1.ts +#EXT-SCTE35: CUE="/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==", ID="123", TIME=123.12 +#EXTINF:10.000, +media2.ts diff --git a/sample-playlists/media-playlist-without-segments.m3u8 b/sample-playlists/media-playlist-without-segments.m3u8 new file mode 100644 index 0000000..76ee188 --- /dev/null +++ b/sample-playlists/media-playlist-without-segments.m3u8 @@ -0,0 +1,4 @@ +#EXTM3U +#EXT-X-VERSION:2 +#EXT-X-TARGETDURATION:9 +#EXT-X-KEY:METHOD=AES-128,URI="http://localhost:20001/key?ecm=AAAAAQAAOpgCAAHFYAaVFH6QrFv2wYU1lEaO2L3fGQB1%2FR3oaD9auWtXNAmcVLxgRTvRlHpqHgXX1YY00%2FpdUiOlgONVbViqou2%2FItyDOWc%3D",IV=0X00000000000000000000000000000000 diff --git a/sample-playlists/mediaplaylist-byterange.m3u8 b/sample-playlists/mediaplaylist-byterange.m3u8 new file mode 100644 index 0000000..5e906fe --- /dev/null +++ b/sample-playlists/mediaplaylist-byterange.m3u8 @@ -0,0 +1,30 @@ +#EXTM3U +#EXT-X-TARGETDURATION:11 +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXTINF:9.98458, +#EXT-X-BYTERANGE:86920@0 +main.aac +#EXTINF:10.00780, +#EXT-X-BYTERANGE:136595@86920 +main.aac +#EXTINF:9.98459, +#EXT-X-BYTERANGE:136567@223515 +main.aac +#EXTINF:10.00780, +#EXT-X-BYTERANGE:136954@360082 +main.aac +#EXTINF:10.00780, +#EXT-X-BYTERANGE:137116@497036 +main.aac +#EXTINF:9.98458, +#EXT-X-BYTERANGE:136770@634152 +main.aac +#EXTINF:10.00780, +#EXT-X-BYTERANGE:137219@770922 +main.aac +#EXTINF:10.00780, +#EXT-X-BYTERANGE:137132@908141 +main.acc +#EXT-X-ENDLIST diff --git a/sample-playlists/mediaplaylist.m3u8 b/sample-playlists/mediaplaylist.m3u8 new file mode 100644 index 0000000..85718fb --- /dev/null +++ b/sample-playlists/mediaplaylist.m3u8 @@ -0,0 +1,35 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:338559 +#EXT-X-KEY:METHOD=AES-128,URI="https://secure.domain.com",IV=0xb059217aa2649ce170b734 +#EXTINF:2.002,338559 +20140311T113819-01-338559live.ts +#EXTINF:2.002,338560 +20140311T113819-01-338560live.ts +#EXTINF:2.002,338561 +20140311T113819-01-338561live.ts +#EXTINF:2.002,338562 +20140311T113819-01-338562live.ts +#EXTINF:2.002,338563 +20140311T113819-01-338563live.ts +#EXTINF:2.002,338564 +20140311T113819-01-338564live.ts +#EXTINF:2.002,338565 +20140311T113819-01-338565live.ts +#EXTINF:2.002,338566 +20140311T113819-01-338566live.ts +#EXTINF:2.002,338567 +20140311T113819-01-338567live.ts +#EXTINF:2.002,338568 +20140311T113819-01-338568live.ts +#EXTINF:2.002,338569 +20140311T113819-01-338569live.ts +#EXTINF:2.002,338570 +20140311T113819-01-338570live.ts +#EXTINF:2.002,338571 +20140311T113819-01-338571live.ts +#EXTINF:2.002,338572 +20140311T113819-01-338572live.ts +#EXTINF:2.002,338573 +20140311T113819-01-338573live.ts diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7aeb33a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,485 @@ +//! 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 m3u8_rs; +//! extern crate nom; +//! use m3u8_rs::playlist::Playlist; +//! use nom::IResult; +//! 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(); +//! +//! // 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), +//! 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::Error(e) => panic!("Parsing error: \n{}", e), +//! IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e), +//! } +//! ``` +//! +//! Parsing a master playlist directly +//! +//! ``` +//! extern crate m3u8_rs; +//! extern crate nom; +//! use std::io::Read; +//! use nom::IResult; +//! +//! 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 IResult::Done(_, pl) = m3u8_rs::parse_master_playlist(&bytes) { +//! println!("{}", pl); +//! } +//! +//! ``` + +#[macro_use] +extern crate nom; + +pub mod playlist; + +use nom::*; +use std::str; +use std::f32; +use std::string; +use std::str::FromStr; +use std::result::Result; +use std::collections::HashMap; +use playlist::*; + +// ----------------------------------------------------------------------------------------------- +// Playlist parser +// ----------------------------------------------------------------------------------------------- + +/// Parse a m3u8 playlist. +/// +/// #Examples +/// +/// 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 { +/// IResult::Done(i, playlist) => playlist, +/// IResult::Error(e) => panic!("Parsing error: \n{}", e), +/// IResult::Incomplete(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 => parse_master_playlist(input).map(Playlist::MasterPlaylist), + false => parse_media_playlist(input).map(Playlist::MediaPlaylist), + } +} + +/// Parse a m3u8 playlist just like `parse_playlist`. This returns a Result. +/// +/// #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::Done(_, playlist) => Ok(playlist), + _ => Err(parse_result), + } +} + +/// Parse input as a master playlist +pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> { + parse_master_playlist_tags(input).map(MasterPlaylist::from_tags) +} + +/// 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) +} + +/// 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::Done(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)>, + chain!( + tag: opt!(alt!( + map!(tag!("#EXT-X-STREAM-INF"), |t| (true, t)) + | map!(tag!("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)) + | map!(tag!("#EXT-X-MEDIA"), |t| (true, t)) + | 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> { + chain!(input, + mut tags: many0!(chain!(m:master_playlist_tag ~ multispace?, || m)) ~ eof, + || { tags.reverse(); tags } + ) +} + +/// 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) + ) +} + +named!(pub variant_stream_tag, + chain!(tag!("#EXT-X-STREAM-INF:") ~ attributes: key_value_pairs, + || VariantStream::from_hashmap(attributes, false)) +); + +named!(pub variant_i_frame_stream_tag, + chain!( tag!("#EXT-X-I-FRAME-STREAM-INF:") ~ attributes: key_value_pairs, + || VariantStream::from_hashmap(attributes, true)) +); + +named!(pub alternative_media_tag, + chain!( tag!("#EXT-X-MEDIA:") ~ attributes: key_value_pairs, + || AlternativeMedia::from_hashmap(attributes)) +); + +named!(pub session_data_tag, + chain!( tag!("#EXT-X-SESSION-DATA:") ~ attributes: key_value_pairs, + || SessionData::from_hashmap(attributes)) +); + +named!(pub session_key_tag, + chain!( 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> { + chain!(input, + mut tags: many0!(chain!(m:media_playlist_tag ~ multispace?, || m)) ~ eof, + || { tags.reverse(); tags } + ) +} + +/// 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!(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!(tag!("#EXT-X-I-FRAMES-ONLY"), |_| MediaPlaylistTag::IFramesOnly) + | map!(start_tag, MediaPlaylistTag::Start) + | map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MediaPlaylistTag::IndependentSegments) + + | map!(media_segment_tag, MediaPlaylistTag::Segment) + ) +} + +named!(pub playlist_type_tag, + map_res!( + map_res!(tag!("#EXT-X-PLAYLIST-TYPE:"), str::from_utf8), + 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!(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!(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) + | map!(chain!(tag!("#EXT-X-PROGRAM-DATE-TIME:") ~ t:consume_line, || t), SegmentTag::ProgramDateTime) + | map!(chain!(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)>, + chain!( + duration: float + ~ tag!(",")? + ~ title: opt!(map_res!(take_until_and_consume!("\r\n"), from_utf8_slice)) + ~ tag!(",")? + , + || (duration, title) + ) +); + +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 } + ) +); + +// ----------------------------------------------------------------------------------------------- +// Basic tags +// ----------------------------------------------------------------------------------------------- + +named!(pub m3u_tag, + map_res!(tag!("#EXTM3U"), from_utf8_slice) +); + +named!(pub version_tag, + chain!( + tag!("#EXT-X-VERSION:") ~ version: map_res!(digit, str::from_utf8), + || version.parse().unwrap() //TODO: or return a default value? + ) +); + +named!(pub start_tag, + chain!(tag!("#EXT-X-START:") ~ attributes:key_value_pairs, || Start::from_hashmap(attributes)) +); + +named!(pub ext_tag, + chain!( + tag!("#EXT-") + ~ tag: map_res!(take_until_and_consume!(":"), from_utf8_slice) + ~ rest: map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice) + , + || ExtTag { tag: tag, rest: rest } + ) +); + +named!(pub comment_tag, + chain!( + tag!("#") ~ text: map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice), + || text + ) +); + +// ----------------------------------------------------------------------------------------------- +// Util +// ----------------------------------------------------------------------------------------------- + +named!(pub key_value_pairs(&[u8]) -> HashMap, + map!( + many0!(chain!(space? ~ k:key_value_pair,|| k)) + , + |pairs: Vec<(String, String)>| { + pairs.into_iter().collect() + } + ) +); + +named!(pub key_value_pair(&[u8]) -> (String, String), + chain!( + peek!(none_of!("\r\n")) + ~ left: map_res!(take_until_and_consume!("="), from_utf8_slice) + ~ right: alt!(quoted | unquoted) + ~ char!(',')? + , + || (left, right) + ) +); + +named!(pub quoted, + delimited!(char!('\"'), map_res!(is_not!("\""), from_utf8_slice), char!('\"')) +); + +named!(pub unquoted, + map_res!(take_until_either!(",\r\n"), from_utf8_slice) +); + +named!(pub consume_line, + map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice) +); + +named!(pub number, + map_res!(map_res!(digit, str::from_utf8), str::FromStr::from_str) +); + +named!(pub byterange_val, + chain!( + n: number + ~ o: opt!(chain!(char!('@') ~ n:number,||n)) + , + || ByteRange { length: n, offset: o } + ) +); + +named!(pub float, + chain!( + left: map_res!(digit, str::from_utf8) + ~ right_opt: opt!(chain!(char!('.') ~ d:map_res!(digit, 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) +} diff --git a/src/playlist.rs b/src/playlist.rs new file mode 100644 index 0000000..94c8b4a --- /dev/null +++ b/src/playlist.rs @@ -0,0 +1,703 @@ +//! Contains all the structs used for parsing. +//! +//! The main type here is the `Playlist` enum. +//! Which is either a `MasterPlaylist` or a `MediaPlaylist`. + +use std::collections::HashMap; +use std::str::FromStr; +use std::fmt; +use super::*; +use std::f32; + +/// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1), +/// can either be a `MasterPlaylist` or a `MediaPlaylist`. +/// +/// A Playlist is a Media Playlist if all URI lines in the Playlist +/// 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)] +pub enum Playlist { + MasterPlaylist(MasterPlaylist), + MediaPlaylist(MediaPlaylist), +} + +// ----------------------------------------------------------------------------------------------- +// Master Playlist +// ----------------------------------------------------------------------------------------------- + +/// A [Master 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)] +pub struct MasterPlaylist { + pub version: usize, + pub variants: Vec, + pub session_data: Option, + pub session_key: Option, + pub start: Option, + pub independent_segments: bool, +} + +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; + } + MasterPlaylistTag::AlternativeMedia(v) => { + alternatives.push(v); + } + MasterPlaylistTag::VariantStream(mut stream) => { + stream.alternatives = alternatives; + alternatives = vec![]; + 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 = Some(data); + } + MasterPlaylistTag::SessionKey(key) => { + master_playlist.session_key = Some(key); + } + MasterPlaylistTag::Start(s) => { + master_playlist.start = Some(s); + } + MasterPlaylistTag::IndependentSegments => { + master_playlist.independent_segments = true; + } + MasterPlaylistTag::Unknown(_) => { + // println!("Unknown master tag \n{:?}\n", t); + } + _ => (), + } + } + + master_playlist + } + + fn get_newest_variant(&mut self) -> Option<&mut VariantStream> { + self.variants.iter_mut().rev().find(|v| !v.is_i_frame) + } +} + +/// [`#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) +/// +/// A Variant Stream includes a Media Playlist that specifies media +/// encoded at a particular bit rate, in a particular format, and at a +/// particular resolution for media containing video. +/// +/// A Variant Stream can also specify a set of Renditions. Renditions +/// are alternate versions of the content, such as audio produced in +/// different languages or video recorded from different camera angles. +/// +/// Clients should switch between different Variant Streams to adapt to +/// network conditions. Clients should choose Renditions based on user +/// preferences. +#[derive(Debug, Default)] +pub struct VariantStream { + pub is_i_frame: bool, + pub uri: String, + + // + pub bandwidth: String, + pub average_bandwidth: Option, + pub codecs: String, + pub resolution: Option, + pub frame_rate: Option, + pub audio: Option, + pub video: Option, + pub subtitles: Option, + pub closed_captions: Option, + // PROGRAM-ID tag was removed in protocol version 6 + pub alternatives: Vec, // EXT-X-MEDIA tags +} + +impl VariantStream { + pub fn from_hashmap(mut attrs: HashMap, is_i_frame: bool) -> VariantStream { + VariantStream { + is_i_frame: is_i_frame, + uri: attrs.remove("URI").unwrap_or_else(String::new), + bandwidth: attrs.remove("BANDWIDTH").unwrap_or_else(String::new), + average_bandwidth: attrs.remove("AVERAGE-BANDWIDTH"), + codecs: attrs.remove("CODECS").unwrap_or_else(String::new), + resolution: attrs.remove("RESOLUTION"), + frame_rate: attrs.remove("FRAME-RATE"), + audio: attrs.remove("AUDIO"), + video: attrs.remove("VIDEO"), + subtitles: attrs.remove("SUBTITLES"), + closed_captions: attrs.remove("CLOSED-CAPTIONS"), + alternatives: vec![], + } + } +} + +/// [`#EXT-X-MEDIA:`] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1) +/// +/// The EXT-X-MEDIA tag is used to relate Media Playlists that contain +/// alternative Renditions (Section 4.3.4.2.1) of the same content. For +/// example, three EXT-X-MEDIA tags can be used to identify audio-only +/// 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)] +pub struct AlternativeMedia { + // + pub media_type: AlternativeMediaType, + pub uri: Option, + pub group_id: String, + pub language: Option, + pub assoc_language: Option, + pub name: String, // All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes. + pub default: bool, // Its absence indicates an implicit value of NO + pub autoselect: bool, // Its absence indicates an implicit value of NO + pub forced: bool, // Its absence indicates an implicit value of NO + pub instream_id: Option, + pub characteristics: Option, +} + +impl AlternativeMedia { + pub fn from_hashmap(mut attrs: HashMap) -> AlternativeMedia { + AlternativeMedia { + media_type: attrs.get("TYPE") + .and_then(|s| AlternativeMediaType::from_str(s).ok()) + .unwrap_or_else(Default::default), + uri: attrs.remove("URI"), + group_id: attrs.remove("GROUP-ID").unwrap_or_else(String::new), + 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")), + instream_id: attrs.remove("INSTREAM-ID"), + characteristics: attrs.remove("CHARACTERISTICS"), + } + } +} + +#[derive(Debug)] +pub enum AlternativeMediaType { + Audio, + Video, + Subtitles, + ClosedCaptions, +} + +impl FromStr for AlternativeMediaType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "AUDIO" => Ok(AlternativeMediaType::Audio), + "VIDEO" => Ok(AlternativeMediaType::Video), + "SUBTITLES" => Ok(AlternativeMediaType::Subtitles), + "CLOSEDCAPTIONS" => Ok(AlternativeMediaType::ClosedCaptions), + _ => Err(format!("Unable to create AlternativeMediaType from {:?}", s)), + } + } +} + +impl Default for AlternativeMediaType { + fn default() -> AlternativeMediaType { + AlternativeMediaType::Video + } +} + +/// [`#EXT-X-SESSION-KEY:`] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5) +#[derive(Debug, Default)] +pub struct SessionKey(pub Key); + +/// [`#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)] +pub struct SessionData { + pub data_id: String, + pub value: String, + pub uri: String, + pub language: Option, +} + +impl SessionData { + pub fn from_hashmap(mut attrs: HashMap) -> SessionData { + 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"), + } + } +} + +// ----------------------------------------------------------------------------------------------- +// Media Playlist +// ----------------------------------------------------------------------------------------------- + +/// A [Media Playlist] +/// (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)] +pub struct MediaPlaylist { + pub version: usize, + /// `#EXT-X-TARGETDURATION:` + pub target_duration: f32, + /// `#EXT-X-MEDIA-SEQUENCE:` + pub media_sequence: i32, + pub segments: Vec, + /// `#EXT-X-DISCONTINUITY-SEQUENCE:` + pub discontinuity_sequence: i32, + /// `#EXT-X-ENDLIST` + pub end_list: bool, + /// `#EXT-X-PLAYLIST-TYPE` + pub playlist_type: MediaPlaylistType, + /// `#EXT-X-I-FRAMES-ONLY` + pub i_frames_only: bool, + /// `#EXT-X-START` + pub start: Option, + /// `#EXT-X-INDEPENDENT-SEGMENTS` + pub independent_segments: bool, +} + +impl MediaPlaylist { + pub fn from_tags(mut tags: Vec) -> MediaPlaylist { + let mut media_playlist = MediaPlaylist::default(); + let mut next_segment = MediaSegment::new(); + 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 = 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::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::new(); + } + _ => (), + } + } + _ => (), + } + } + media_playlist + } +} + +/// [`#EXT-X-PLAYLIST-TYPE:`] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5) +#[derive(Debug)] +pub enum MediaPlaylistType { + Event, + Vod, +} + +impl FromStr for MediaPlaylistType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "EVENT" => Ok(MediaPlaylistType::Event), + "VOD" => Ok(MediaPlaylistType::Vod), + _ => Err(format!("Unable to create MediaPlaylistType from {:?}", s)), + } + } +} + +impl Default for MediaPlaylistType { + fn default() -> MediaPlaylistType { + MediaPlaylistType::Event + } +} + +// ----------------------------------------------------------------------------------------------- +// Media Segment +// ----------------------------------------------------------------------------------------------- + +/// 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)] +pub struct MediaSegment { + pub uri: String, + /// `#EXTINF:,[]` + pub duration: f32, + /// `#EXTINF:<duration>,[<title>]` + pub title: Option<String>, + /// `#EXT-X-BYTERANGE:<n>[@<o>]` + pub byte_range: Option<ByteRange>, + /// `#EXT-X-DISCONTINUITY` + pub discontinuity: bool, + /// `#EXT-X-KEY:<attribute-list>` + pub key: Option<Key>, + /// `#EXT-X-MAP:<attribute-list>` + pub map: Option<Map>, + /// `#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>` + pub program_date_time: Option<String>, + /// `#EXT-X-DATERANGE:<attribute-list>` + pub daterange: Option<String>, +} + +impl MediaSegment { + pub fn new() -> MediaSegment { + Default::default() + } +} + +/// [`#EXT-X-KEY:<attribute-list>`] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4) +/// +/// Media Segments MAY be encrypted. The EXT-X-KEY tag specifies how to +/// decrypt them. It applies to every Media Segment that appears between +/// it and the next EXT-X-KEY tag in the Playlist file with the same +/// 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)] +pub struct Key { + pub method: String, + pub uri: Option<String>, + pub iv: Option<String>, + pub keyformat: Option<String>, + pub keyformatversions: Option<String>, +} + +impl Key { + pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Key { + Key { + method: attrs.remove("METHOD").unwrap_or_else(String::new), + uri: attrs.remove("URI"), + iv: attrs.remove("IV"), + keyformat: attrs.remove("KEYFORMAT"), + keyformatversions: attrs.remove("KEYFORMATVERSIONS"), + } + } +} + +/// [`#EXT-X-MAP:<attribute-list>`] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.5) +/// +/// The EXT-X-MAP tag specifies how to obtain the Media Initialization Section +/// [(Section 3)] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3) +/// required to parse the applicable Media Segments. +/// 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)] +pub struct Map { + pub uri: String, + pub byterange: Option<ByteRange>, +} + +/// [`#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)] +pub struct ByteRange { + pub length: i32, + pub offset: Option<i32>, +} + +/// [`#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)] +pub struct DateRange { + pub id: String, + pub class: Option<String>, + pub start_date: String, + pub end_date: Option<String>, + pub duration: Option<String>, + pub planned_duration: Option<String>, + pub x_prefixed: Option<String>, // X-<client-attribute> + pub end_on_next: bool, +} + +// ----------------------------------------------------------------------------------------------- +// Rest +// ----------------------------------------------------------------------------------------------- + +/// [`#EXT-X-START:<attribute-list>`] +/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.5.2) +/// +/// 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)] +pub struct Start { + pub time_offset: String, + pub precise: Option<String>, +} + +impl Start { + pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Start { + Start { + time_offset: attrs.remove("TIME-OFFSET").unwrap_or_else(String::new), + precise: attrs.remove("PRECISE").or(Some("NO".to_string())), + } + } +} + +/// A simple `#EXT-` tag +#[derive(Debug, Default)] +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, "]") + } +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..edfa8ed --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,219 @@ +#![allow(unused_variables, unused_imports, dead_code)] + +#[macro_use] +extern crate nom; +extern crate m3u8_rs; + +use std::fs; +use std::path; +use m3u8_rs::*; +use std::io::Read; +use nom::*; +use std::collections::HashMap; + +fn all_sample_m3u_playlists() -> Vec<path::PathBuf> { + fs::read_dir("sample-playlists\\").unwrap() + .filter_map(Result::ok) + .map(|dir| dir.path()) + .filter(|path| path.extension().map_or(false, |ext| ext == "m3u8")) + .collect() +} + +fn getm3u(path: &str) -> String { + let mut buf = String::new(); + let mut file = fs::File::open(path).expect("Can't find m3u8."); + let u = file.read_to_string(&mut buf).expect("Can't read file"); + buf +} + +fn get_sample_playlist(name: &str) -> String { + getm3u(&(String::from("sample-playlists\\") + name)) +} + +// ----------------------------------------------------------------------------------------------- +// Playlist + +fn print_parse_playlist_test(playlist_name: &str) -> bool { + let input = 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); + true + } + else { + println!("Parsing failed:\n {:?}", parsed); + false + } +} + +#[test] +fn playlis_master_usher_result() { + assert!(print_parse_playlist_test("master-usher_result.m3u8")); +} + +#[test] +fn playlist_master_with_alternatives() { + assert!(print_parse_playlist_test("master-with-alternatives.m3u8")); +} + +#[test] +fn playlist_master_with_i_frame_stream_inf() { + assert!(print_parse_playlist_test("master-with-i-frame-stream-inf.m3u8")); +} + +#[test] +fn playlist_master_with_multiple_codecs() { + assert!(print_parse_playlist_test("master-with-multiple-codecs.m3u8")); +} + +// -- Media playlists + +#[test] +fn playlist_media_standard() { + assert!(print_parse_playlist_test("mediaplaylist.m3u8")); +} + +#[test] +fn playlist_media_without_segments() { + assert!(print_parse_playlist_test("media-playlist-without-segments.m3u8")); +} + +// ----------------------------------------------------------------------------------------------- +// Playlist type detection tests + +#[test] +fn playlist_type_is_master() { + let input = get_sample_playlist("master.m3u8"); + let result = is_master_playlist(input.as_bytes()); + assert_eq!(true, result); +} + +#[test] +fn playlist_type_with_unkown_tag() { + let input = get_sample_playlist("master-usher_result.m3u8"); + let result = is_master_playlist(input.as_bytes()); + println!("Playlist_type_with_unkown_tag is master playlist: {:?}", result); + assert_eq!(true, result); +} + +#[test] +fn playlist_types() { + for path_buf in all_sample_m3u_playlists() { + let path = path_buf.to_str().unwrap(); + let input = getm3u(path); + let is_master = is_master_playlist(input.as_bytes()); + + assert!(path.to_lowercase().contains("master") == is_master); + + println!("{:?} = {:?}", path, is_master); + } +} + +// ----------------------------------------------------------------------------------------------- +// Variant + +#[test] +fn variant_stream() { + let input = b"#EXT-X-STREAM-INF:BANDWIDTH=300000,CODECS=\"xxx\"\n"; + let result = variant_stream_tag(input); + println!("{:?}", result); +} + + +// ----------------------------------------------------------------------------------------------- +// Other + +#[test] +fn test_key_value_pairs_trailing_equals() { + let res = key_value_pairs(b"BANDWIDTH=395000,CODECS=\"avc1.4d001f,mp4a.40.2\"\r\nrest="); + println!("{:?}\n\n", res); +} + +#[test] +fn test_key_value_pairs_multiple_quoted_values() { + assert_eq!( + key_value_pairs(b"BANDWIDTH=86000,URI=\"low/iframe.m3u8\",PROGRAM-ID=1,RESOLUTION=\"1x1\",VIDEO=1\nrest"), + IResult::Done( + "\nrest".as_bytes(), + vec![ + ("BANDWIDTH".to_string(), "86000".to_string()), + ("URI".to_string(), "low/iframe.m3u8".to_string()), + ("PROGRAM-ID".to_string(), "1".to_string()), + ("RESOLUTION".to_string(), "1x1".to_string()), + ("VIDEO".to_string(), "1".to_string()) + ].into_iter().collect::<HashMap<String,String>>() + ) + ); +} + +#[test] +fn test_key_value_pairs_quotes() { + let res = key_value_pairs(b"BANDWIDTH=300000,CODECS=\"avc1.42c015,mp4a.40.2\"\r\nrest"); + println!("{:?}\n\n", res); +} + +#[test] +fn test_key_value_pairs() { + let res = key_value_pairs(b"BANDWIDTH=300000,RESOLUTION=22x22,VIDEO=1\r\nrest="); + println!("{:?}\n\n", res); +} + + +#[test] +fn test_key_value_pair() { + assert_eq!( + key_value_pair(b"PROGRAM-ID=1,rest"), + IResult::Done( + "rest".as_bytes(), + ("PROGRAM-ID".to_string(), "1".to_string()) + ) + ); +} + +#[test] +fn comment() { + assert_eq!( + comment_tag(b"#Hello\nxxx"), + IResult::Done("xxx".as_bytes(), "Hello".to_string()) + ); +} + +#[test] +fn quotes() { + assert_eq!( + quoted(b"\"value\"rest"), + IResult::Done("rest".as_bytes(), "value".to_string()) + ); +} + +#[test] +fn consume_empty_line() { + let line = consume_line(b"\r\nrest"); + println!("{:?}", line); +} + +#[test] +fn float_() { + assert_eq!( + float(b"33.22rest"), + IResult::Done("rest".as_bytes(), 33.22f32) + ); +} + +#[test] +fn float_no_decimal() { + assert_eq!( + float(b"33rest"), + IResult::Done("rest".as_bytes(), 33f32) + ); +} + +#[test] +fn float_should_ignore_trailing_dot() { + assert_eq!( + float(b"33.rest"), + IResult::Done(".rest".as_bytes(), 33f32) + ); +}