mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-12-30 09:50:28 +00:00
Version 1.0.0
This commit is contained in:
parent
d826f75f2a
commit
41ce460d90
27 changed files with 1891 additions and 1 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "m3u8-rs"
|
||||
version = "1.0.0"
|
||||
authors = ["Rutger"]
|
||||
|
||||
[dependencies]
|
||||
nom = "^1.2.3"
|
136
README.md
136
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<u8> = 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<Playlist, IResult>`. 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<u8> = 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<VariantStream>,
|
||||
pub session_data: Option<SessionData>,
|
||||
pub session_key: Option<SessionKey>,
|
||||
pub start: Option<Start>,
|
||||
pub independent_segments: bool,
|
||||
}
|
||||
|
||||
pub struct MediaPlaylist {
|
||||
pub version: usize,
|
||||
pub target_duration: f32,
|
||||
pub media_sequence: i32,
|
||||
pub segments: Vec<MediaSegment>,
|
||||
pub discontinuity_sequence: i32,
|
||||
pub end_list: bool,
|
||||
pub playlist_type: MediaPlaylistType,
|
||||
pub i_frames_only: bool,
|
||||
pub start: Option<Start>,
|
||||
pub independent_segments: bool,
|
||||
}
|
||||
|
||||
pub struct VariantStream {
|
||||
pub is_i_frame: bool,
|
||||
pub uri: String,
|
||||
pub bandwidth: String,
|
||||
pub average_bandwidth: Option<String>,
|
||||
pub codecs: String,
|
||||
pub resolution: Option<String>,
|
||||
pub frame_rate: Option<String>,
|
||||
pub audio: Option<String>,
|
||||
pub video: Option<String>,
|
||||
pub subtitles: Option<String>,
|
||||
pub closed_captions: Option<String>,
|
||||
pub alternatives: Vec<AlternativeMedia>,
|
||||
}
|
||||
|
||||
pub struct MediaSegment {
|
||||
pub uri: String,
|
||||
pub duration: f32,
|
||||
pub title: Option<String>,
|
||||
pub byte_range: Option<ByteRange>,
|
||||
pub discontinuity: bool,
|
||||
pub key: Option<Key>,
|
||||
pub map: Option<Map>,
|
||||
pub program_date_time: Option<String>,
|
||||
pub daterange: Option<String>,
|
||||
}
|
||||
|
||||
```
|
||||
|
|
19
examples/simple.rs
Normal file
19
examples/simple.rs
Normal file
|
@ -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<u8> = 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)
|
||||
}
|
||||
}
|
40
examples/with_nom_result.rs
Normal file
40
examples/with_nom_result.rs
Normal file
|
@ -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<u8> = 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<u8> = 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),
|
||||
}
|
||||
}
|
9
masterplaylist.m3u8
Normal file
9
masterplaylist.m3u8
Normal file
|
@ -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
|
18
mediaplaylist.m3u8
Normal file
18
mediaplaylist.m3u8
Normal file
|
@ -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
|
9
playlist.m3u8
Normal file
9
playlist.m3u8
Normal file
|
@ -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
|
20
sample-playlists/master-usher_result.m3u8
Normal file
20
sample-playlists/master-usher_result.m3u8
Normal file
|
@ -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
|
18
sample-playlists/master-with-alternatives.m3u8
Normal file
18
sample-playlists/master-with-alternatives.m3u8
Normal file
|
@ -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
|
13
sample-playlists/master-with-i-frame-stream-inf.m3u8
Normal file
13
sample-playlists/master-with-i-frame-stream-inf.m3u8
Normal file
|
@ -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"
|
12
sample-playlists/master-with-multiple-codecs.m3u8
Normal file
12
sample-playlists/master-with-multiple-codecs.m3u8
Normal file
|
@ -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
|
16
sample-playlists/master-with-offset.m3u8
Normal file
16
sample-playlists/master-with-offset.m3u8
Normal file
|
@ -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
|
10
sample-playlists/master-with-stream-inf-name.m3u8
Normal file
10
sample-playlists/master-with-stream-inf-name.m3u8
Normal file
|
@ -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
|
12
sample-playlists/master.m3u8
Normal file
12
sample-playlists/master.m3u8
Normal file
|
@ -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
|
16
sample-playlists/masterplaylist.m3u8
Normal file
16
sample-playlists/masterplaylist.m3u8
Normal file
|
@ -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
|
9
sample-playlists/masterplaylist2.m3u8
Normal file
9
sample-playlists/masterplaylist2.m3u8
Normal file
|
@ -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
|
13
sample-playlists/media-playlist-with-byterange.m3u8
Normal file
13
sample-playlists/media-playlist-with-byterange.m3u8
Normal file
|
@ -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
|
15
sample-playlists/media-playlist-with-discontinuity.m3u8
Normal file
15
sample-playlists/media-playlist-with-discontinuity.m3u8
Normal file
|
@ -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
|
11
sample-playlists/media-playlist-with-scte35-1.m3u8
Normal file
11
sample-playlists/media-playlist-with-scte35-1.m3u8
Normal file
|
@ -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
|
11
sample-playlists/media-playlist-with-scte35.m3u8
Normal file
11
sample-playlists/media-playlist-with-scte35.m3u8
Normal file
|
@ -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
|
4
sample-playlists/media-playlist-without-segments.m3u8
Normal file
4
sample-playlists/media-playlist-without-segments.m3u8
Normal file
|
@ -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
|
30
sample-playlists/mediaplaylist-byterange.m3u8
Normal file
30
sample-playlists/mediaplaylist-byterange.m3u8
Normal file
|
@ -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
|
35
sample-playlists/mediaplaylist.m3u8
Normal file
35
sample-playlists/mediaplaylist.m3u8
Normal file
|
@ -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
|
485
src/lib.rs
Normal file
485
src/lib.rs
Normal file
|
@ -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<u8> = Vec::new();
|
||||
//! file.read_to_end(&mut bytes).unwrap();
|
||||
//!
|
||||
//! // Option 1: fn parse_playlist_res(input) -> Result<Playlist, _>
|
||||
//! 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<u8> = 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<u8> = 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<PLaylist,_>.
|
||||
///
|
||||
/// #Examples
|
||||
///
|
||||
/// ```
|
||||
/// use m3u8_rs::playlist::{Playlist};
|
||||
/// use std::io::Read;
|
||||
///
|
||||
/// let mut file = std::fs::File::open("playlist.m3u8").unwrap();
|
||||
/// let mut bytes: Vec<u8> = 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<Playlist, IResult<&[u8], Playlist>> {
|
||||
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<MasterPlaylistTag>> {
|
||||
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<VariantStream>,
|
||||
chain!(tag!("#EXT-X-STREAM-INF:") ~ attributes: key_value_pairs,
|
||||
|| VariantStream::from_hashmap(attributes, false))
|
||||
);
|
||||
|
||||
named!(pub variant_i_frame_stream_tag<VariantStream>,
|
||||
chain!( tag!("#EXT-X-I-FRAME-STREAM-INF:") ~ attributes: key_value_pairs,
|
||||
|| VariantStream::from_hashmap(attributes, true))
|
||||
);
|
||||
|
||||
named!(pub alternative_media_tag<AlternativeMedia>,
|
||||
chain!( tag!("#EXT-X-MEDIA:") ~ attributes: key_value_pairs,
|
||||
|| AlternativeMedia::from_hashmap(attributes))
|
||||
);
|
||||
|
||||
named!(pub session_data_tag<SessionData>,
|
||||
chain!( tag!("#EXT-X-SESSION-DATA:") ~ attributes: key_value_pairs,
|
||||
|| SessionData::from_hashmap(attributes))
|
||||
);
|
||||
|
||||
named!(pub session_key_tag<SessionKey>,
|
||||
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<MediaPlaylistTag>> {
|
||||
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<MediaPlaylistType>,
|
||||
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<String>),
|
||||
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<String>)>,
|
||||
chain!(
|
||||
duration: float
|
||||
~ tag!(",")?
|
||||
~ title: opt!(map_res!(take_until_and_consume!("\r\n"), from_utf8_slice))
|
||||
~ tag!(",")?
|
||||
,
|
||||
|| (duration, title)
|
||||
)
|
||||
);
|
||||
|
||||
named!(pub key<Key>, map!(key_value_pairs, Key::from_hashmap));
|
||||
|
||||
named!(pub map<Map>,
|
||||
chain!(
|
||||
uri: quoted ~ range: opt!(chain!(char!(',') ~ b:byterange_val,||b )),
|
||||
|| Map { uri: uri, byterange: range }
|
||||
)
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Basic tags
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
named!(pub m3u_tag<String>,
|
||||
map_res!(tag!("#EXTM3U"), from_utf8_slice)
|
||||
);
|
||||
|
||||
named!(pub version_tag<usize>,
|
||||
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<Start>,
|
||||
chain!(tag!("#EXT-X-START:") ~ attributes:key_value_pairs, || Start::from_hashmap(attributes))
|
||||
);
|
||||
|
||||
named!(pub ext_tag<ExtTag>,
|
||||
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<String>,
|
||||
chain!(
|
||||
tag!("#") ~ text: map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice),
|
||||
|| text
|
||||
)
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Util
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
named!(pub key_value_pairs(&[u8]) -> HashMap<String, String>,
|
||||
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<String>,
|
||||
delimited!(char!('\"'), map_res!(is_not!("\""), from_utf8_slice), char!('\"'))
|
||||
);
|
||||
|
||||
named!(pub unquoted<String>,
|
||||
map_res!(take_until_either!(",\r\n"), from_utf8_slice)
|
||||
);
|
||||
|
||||
named!(pub consume_line<String>,
|
||||
map_res!(take_until_either_and_consume!("\r\n"), from_utf8_slice)
|
||||
);
|
||||
|
||||
named!(pub number<i32>,
|
||||
map_res!(map_res!(digit, str::from_utf8), str::FromStr::from_str)
|
||||
);
|
||||
|
||||
named!(pub byterange_val<ByteRange>,
|
||||
chain!(
|
||||
n: number
|
||||
~ o: opt!(chain!(char!('@') ~ n:number,||n))
|
||||
,
|
||||
|| ByteRange { length: n, offset: o }
|
||||
)
|
||||
);
|
||||
|
||||
named!(pub float<f32>,
|
||||
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, string::FromUtf8Error> {
|
||||
String::from_utf8(s.to_vec())
|
||||
}
|
||||
|
||||
pub fn from_utf8_slice2(s: &[u8]) -> Result<String, str::Utf8Error> {
|
||||
str::from_utf8(s).map(String::from)
|
||||
}
|
703
src/playlist.rs
Normal file
703
src/playlist.rs
Normal file
|
@ -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<VariantStream>,
|
||||
pub session_data: Option<SessionData>,
|
||||
pub session_key: Option<SessionKey>,
|
||||
pub start: Option<Start>,
|
||||
pub independent_segments: bool,
|
||||
}
|
||||
|
||||
impl MasterPlaylist {
|
||||
pub fn from_tags(mut tags: Vec<MasterPlaylistTag>) -> 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:<attribute-list>`]
|
||||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.2)
|
||||
/// [`#EXT-X-I-FRAME-STREAM-INF:<attribute-list>`]
|
||||
/// (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,
|
||||
|
||||
// <attribute-list>
|
||||
pub bandwidth: String,
|
||||
pub average_bandwidth: Option<String>,
|
||||
pub codecs: String,
|
||||
pub resolution: Option<String>,
|
||||
pub frame_rate: Option<String>,
|
||||
pub audio: Option<String>,
|
||||
pub video: Option<String>,
|
||||
pub subtitles: Option<String>,
|
||||
pub closed_captions: Option<String>,
|
||||
// PROGRAM-ID tag was removed in protocol version 6
|
||||
pub alternatives: Vec<AlternativeMedia>, // EXT-X-MEDIA tags
|
||||
}
|
||||
|
||||
impl VariantStream {
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, String>, 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:<attribute-list>`]
|
||||
/// (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 {
|
||||
// <attribute-list>
|
||||
pub media_type: AlternativeMediaType,
|
||||
pub uri: Option<String>,
|
||||
pub group_id: String,
|
||||
pub language: Option<String>,
|
||||
pub assoc_language: Option<String>,
|
||||
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<String>,
|
||||
pub characteristics: Option<String>,
|
||||
}
|
||||
|
||||
impl AlternativeMedia {
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> 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<AlternativeMediaType, String> {
|
||||
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:<attribute-list>`]
|
||||
/// (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:<attribute-list>`]
|
||||
/// (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<String>,
|
||||
}
|
||||
|
||||
impl SessionData {
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> 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:<s>`
|
||||
pub target_duration: f32,
|
||||
/// `#EXT-X-MEDIA-SEQUENCE:<number>`
|
||||
pub media_sequence: i32,
|
||||
pub segments: Vec<MediaSegment>,
|
||||
/// `#EXT-X-DISCONTINUITY-SEQUENCE:<number>`
|
||||
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<Start>,
|
||||
/// `#EXT-X-INDEPENDENT-SEGMENTS`
|
||||
pub independent_segments: bool,
|
||||
}
|
||||
|
||||
impl MediaPlaylist {
|
||||
pub fn from_tags(mut tags: Vec<MediaPlaylistTag>) -> 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:<EVENT|VOD>`]
|
||||
/// (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<MediaPlaylistType, String> {
|
||||
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:<duration>,[<title>]`
|
||||
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, "]")
|
||||
}
|
||||
}
|
219
tests/lib.rs
Normal file
219
tests/lib.rs
Normal file
|
@ -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)
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue