mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-06-02 10:11:21 +00:00
Compare commits
127 commits
Author | SHA1 | Date | |
---|---|---|---|
381ac7732f | |||
e3b6390186 | |||
7f322675eb | |||
c5cceeb4f6 | |||
5109753b96 | |||
487d63da4d | |||
f6af8acbfe | |||
46622345d1 | |||
d8e0283ddb | |||
b9cf88b7ec | |||
a1970192ff | |||
ae31a2741f | |||
e7a6cf943c | |||
48e416cd69 | |||
015b05f26c | |||
b0a9fe2625 | |||
14d24b94c8 | |||
2a76fa549c | |||
18739b59ac | |||
e41c47a8f8 | |||
31b31fd958 | |||
d2881fef08 | |||
bd7cce75e9 | |||
7247e02ee5 | |||
6559e45b49 | |||
0789098d7d | |||
b692ac0808 | |||
85141f6a51 | |||
d941541be8 | |||
7173c26015 | |||
ac0f881eef | |||
212a485687 | |||
dc352b7ef3 | |||
c28cb7f7d6 | |||
f606063330 | |||
210af70f72 | |||
5c842fd9f6 | |||
2f92e3ae8c | |||
3c8368f9a3 | |||
39b52a1d4b | |||
bc8ccf0f5d | |||
5ee1273f7c | |||
44aa097c90 | |||
1bfad5df01 | |||
cca02b239d | |||
836ef1caaf | |||
2fae1d8f20 | |||
53e9439660 | |||
472618e1aa | |||
2432846064 | |||
51fcb70113 | |||
3edf5d1c0f | |||
7e62854e20 | |||
336f11e1ba | |||
a5d8358379 | |||
5500166f74 | |||
4e6ac58d0c | |||
65c295ee02 | |||
a44c2a1a72 | |||
0ed0ce51f8 | |||
81398f86cd | |||
f104d431d9 | |||
0a3fb0e671 | |||
1287975af4 | |||
6ee1b52c01 | |||
303d0ecfce | |||
Rafael Caricio | 3d5599fa28 | ||
Rafael Caricio | 39aab3a2ac | ||
359695a25c | |||
Rafael Caricio | 677027e22c | ||
Rafael Caricio | dc352301a3 | ||
Rafael Caricio | c1ff2b3730 | ||
06162a8554 | |||
46922bdab3 | |||
dc576c8e3c | |||
c3ef5bc16e | |||
5a72e1e875 | |||
05669cab68 | |||
870ca830d3 | |||
85b0826103 | |||
5fe3fc309c | |||
b75379437d | |||
Rafael Caricio | 3e74f7787f | ||
302ff22f31 | |||
b44f6518c8 | |||
37acdb304d | |||
087c47bddd | |||
57d60ba438 | |||
cd9402051e | |||
64de5e4d9b | |||
978e6a7e58 | |||
76aab26b20 | |||
1b18c7902c | |||
e2822e4521 | |||
7a882e5df0 | |||
a081b462d1 | |||
3fee7b9983 | |||
ca9c41d823 | |||
100a57078a | |||
ab9c554eb4 | |||
13405a09eb | |||
b810687652 | |||
4ed378772b | |||
350109e29a | |||
f7587aa264 | |||
e4e1717b0a | |||
b9d377bfa4 | |||
fc9f45dd18 | |||
ed0d35b3a3 | |||
e982070d59 | |||
43c7321fb2 | |||
31e78801f9 | |||
03ec1a4544 | |||
b2150f26e5 | |||
e594ec5792 | |||
af15863688 | |||
67eebd17a0 | |||
ca07767eb4 | |||
22571a3404 | |||
279fefef72 | |||
9d81ce7194 | |||
5389f25f6f | |||
ccf25fefcf | |||
3c8268eace | |||
759809b2b4 | |||
b3bdaa133c | |||
b5b950150f |
27
.github/workflows/rust-windows.yml
vendored
Normal file
27
.github/workflows/rust-windows.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Rust Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Run fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
27
.github/workflows/rust.yml
vendored
Normal file
27
.github/workflows/rust.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Run fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
11
Cargo.toml
11
Cargo.toml
|
@ -1,12 +1,19 @@
|
|||
[package]
|
||||
name = "m3u8-rs"
|
||||
version = "1.0.4"
|
||||
version = "6.0.0"
|
||||
authors = ["Rutger"]
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rutgersc/m3u8-rs"
|
||||
description = "A library for parsing m3u8 files (Apple's HTTP Live Streaming (HLS) protocol)."
|
||||
documentation = "https://rutgersc.github.io/doc/m3u8_rs/index.html"
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
nom = "^1.2.3"
|
||||
nom = { version = "7", optional = true }
|
||||
chrono = { version = "0.4", default-features = false, features = [ "std" ] }
|
||||
|
||||
[features]
|
||||
default = ["parser"]
|
||||
parser = ["nom"]
|
||||
|
||||
|
|
138
README.md
138
README.md
|
@ -1,136 +1,10 @@
|
|||
# m3u8-rs
|
||||
A Rust library for parsing m3u8 playlists (HTTP Live Streaming) [link](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19).
|
||||
# m3u8-rs
|
||||
![crates.io](https://img.shields.io/crates/v/m3u8-rs.svg)
|
||||
[![API](https://docs.rs/m3u8-rs/badge.svg)](https://docs.rs/m3u8-rs)
|
||||
|
||||
A Rust library for parsing m3u8 playlists (HTTP Live Streaming) [link](https://datatracker.ietf.org/doc/html/rfc8216).
|
||||
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.2"
|
||||
```
|
||||
|
||||
And add the crate to `lib.rs`
|
||||
|
||||
```rust
|
||||
extern crate m3u8_rs;
|
||||
```
|
||||
|
||||
Also available on [crates.io](https://crates.io/crates/m3u8-rs)
|
||||
|
||||
# Documentation
|
||||
|
||||
Available [here](https://rutgersc.github.io/doc/m3u8_rs/index.html)
|
||||
|
||||
# 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_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),
|
||||
}
|
||||
```
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
```
|
||||
Examples can be found in the `examples` folder.
|
|
@ -1,7 +1,4 @@
|
|||
extern crate nom;
|
||||
extern crate m3u8_rs;
|
||||
|
||||
use m3u8_rs::playlist::{Playlist};
|
||||
use m3u8_rs::Playlist;
|
||||
use std::io::Read;
|
||||
|
||||
fn main() {
|
||||
|
@ -14,6 +11,6 @@ fn main() {
|
|||
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)
|
||||
Err(e) => println!("Error: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
extern crate nom;
|
||||
extern crate m3u8_rs;
|
||||
|
||||
use m3u8_rs::playlist::{Playlist};
|
||||
use m3u8_rs::Playlist;
|
||||
use std::io::Read;
|
||||
use nom::IResult;
|
||||
|
||||
fn main() {
|
||||
let mut file = std::fs::File::open("playlist.m3u8").unwrap();
|
||||
|
@ -13,9 +9,8 @@ fn main() {
|
|||
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),
|
||||
Result::Ok((_i, playlist)) => playlist,
|
||||
Result::Err(e) => panic!("Parsing error: \n{}", e),
|
||||
};
|
||||
|
||||
match playlist {
|
||||
|
@ -24,6 +19,7 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn main_alt() {
|
||||
let mut file = std::fs::File::open("playlist.m3u8").unwrap();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
|
@ -32,9 +28,8 @@ fn main_alt() {
|
|||
let parsed = m3u8_rs::parse_playlist(&bytes);
|
||||
|
||||
match parsed {
|
||||
IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
|
||||
IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
|
||||
IResult::Error(e) => panic!("Parsing error: \n{}", e),
|
||||
IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
|
||||
Result::Ok((_i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl),
|
||||
Result::Ok((_i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl),
|
||||
Result::Err(e) => panic!("Parsing error: \n{}", e),
|
||||
}
|
||||
}
|
||||
|
|
23
sample-playlists/master-not-ending-in-newline-1.m3u8
Normal file
23
sample-playlists/master-not-ending-in-newline-1.m3u8
Normal file
|
@ -0,0 +1,23 @@
|
|||
#EXTM3U
|
||||
#EXT-X-TWITCH-INFO:NODE="video-edge-346990.cph01",MANIFEST-NODE-TYPE="weaver_cluster",MANIFEST-NODE="video-weaver.cph01",SUPPRESS="false",SERVER-TIME="1603666310.03",TRANSCODESTACK="2017TranscodeX264_V2",USER-IP="81.228.244.140",SERVING-ID="2e8913ca31ab4d54988d8dd87a66d3eb",CLUSTER="cph01",ABS="false",VIDEO-SESSION-ID="709300939666498958",BROADCAST-ID="40219312190",STREAM-TIME="10225.032456",B="false",USER-COUNTRY="SE",MANIFEST-CLUSTER="cph01",ORIGIN="sjc02",C="aHR0cHM6Ly92aWRlby1lZGdlLTNlN2UyOC5wZHgwMS5hYnMuaGxzLnR0dm53Lm5ldC92MS9zZWdtZW50L0NtSVk5WlFDUWp1eTFjMmo0WFB0LUM0eDhSWWZpdlRnRjl6dDJQT083WlRPdzYybC14bkpnYnJxanlBTl82eExfWlcxeS1PS1NyUFBQZnpaSUI0VGxYS3k4YmFvMEpUVjQ0X1Q2X215b1dKZ3NDaVBWaU5CMjIyTlljd2VpQVpaTUxHUElER1ExY1dla1hvejc3LTRPU1BqRVhpUmNDVnM5ZmFnMkVLblk5UTZjaldDaTZwSldVTjl2N01femVRZ1VsSU5YNDNIdFh4RU1ORU5sT2hZX1RXRjExRmJqRVdjMW9OYkRva191c3g0WHdwb29ETVFFZ2Z1NlJFV0pRRERFdGhTTVJDN3J3SFVFb3c4aG9PN0VLQmFOVG1zTGpIUjZ6S3R3RS1nblptRk1HSWFSQ2V6NWtzTlVOREhidW45RXlOOFdIeWJaVG1rZlpfQm9MRjZScXBpbnNUYkJGT0I1VWJUYnFjbU50RnBFcGs5ejFBU29zZHNvM0ZJNGplWWZUU3lIYTFSQU11a0YwQXBJZjNmNHZDaVpNdFN3amh6aFl1Z2dsNnM0a0ZZSWdKUVBPMUhGWktlMjFtak1BOHBycFJoV2NqazN1Z2VvUnI2VGt0S3dzSmdBVjVrWmlRUm9TZ1V5ZE5UUmZJa1hIay1nM1JBaW1FcGpiWnVUYnd0UVRFSjhpNUhBdGtTd25ZUm01WUhrTlpVVnU0TlJyZDdiaElDTEZVWW1xSlRLR3hSU0NXXzRteXlmc0JYeXZFQWFhVFNTLTBtb0dSTmpGZEl5TlotOVN2U3pKZ0UxRkJiSk9pdTVGS1phTDlzNGl3NHYySXJITUF3dDkwbkxWN1QzNTVQQTVIN0pRTlk3MXZMTUNtZGNMMkY4RG1BTE1JNl9lbEZEeml5dThhRFlieXZjbi1LXzJWZmk2azBONjNGSnBKU0F3bDZyaG01Um80bVJ2NFc0TDl3V3JacTNhblNxR2wxWlZJejBUNnZlcFRYU3l5UVl6dzRPYmZ1S0l6OVNGdFNnb0JvSmFiLnRz",D="false"
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="1080p60 (source)",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=9061127,RESOLUTION=1920x1080,CODECS="avc1.64002A,mp4a.40.2",VIDEO="chunked"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/Cu0DdhlwnpcIE03_B0CE88V9Oqeqod_rQRgGhkHtC_a8kMVJczhYuLCbGc04CMESYcRvucOJJccCANfsfli1hJPoqruJD7u9ETSxMIcNUfT54D52gA55ePJXxM5IUddHwZ5lw1vPC4IFC0IPVeUvRFw5g8HbCgRAJD-YV2W90VVcbXoCnNoLtvk_IJYwVk59XGECSzWh9lRRXTuqHgOWaehnTOvBcNLGl_BovpgqnLcMjeJTLhIO9Sna8JqM3ppnl19M8AE9RqrI6P4qsQRPcOmwa19Xx8n7yhBcrkMHVJhatHhUIVnOJ99fmgTul9NPBtRhcn59AVSCcNHrjrMwK0LTWmkD-8rmZaG2ezrLVbC6TzR9LfPsYVPokmAKDyUdOdai0gEMoWI8MvD17gl5bgoWrUHNk458cbtN8PaNrrbRbTC5fweA3Qo4mo4QNv1qOwi2inwKV3Jq81y6A0WG9gq-xQlQQaTk9r_XPnkqmuAFW_SlXHC7fdT8ZQHr3LxvzsQgjBrTo935EK269CMU3uuzOiQIphR0N-NJormgedjw5sZ6WT1N-1UWSliEqJ2CP1F89fPGDfB0Zxi8RUWNY7FJDJBWaofXumtBkObhmUqaPQtm5yq2rto89e0mOFx8pOzFCH556zu0vuHZ4ua4OxIQ3gR52zVBOaNMWs8PGJ7UARoMYEeMrBQEwEssMiG4.m3u8
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p60",NAME="720p60",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=3430252,RESOLUTION=1280x720,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="720p60"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDfH81q-R4CeLFl4jpTunvTt4JycqLEY4s_ysCUBub4j9pBZ-_6XcUIrWcfAgyaxFDNk7U6OkUYiD2jLTsvR4fzLGLzjnCyi604bDe_hek8719_WGgzVYEH6NuqauU_5C0DN6_h79PjTHkdu2Ty3WVlO4wg1nONk1m79n5DyEuxZOWuSQVZ5EIXxRWTMWbJxRNnW-jONMCAogfwrhIckgLOJIHjALdyBKEj7RFLGTsInfK59l7q81FsWFZztgrpIRjOteUdwJpWgnMpSK17rA-Rpy_KaU0oZKVslYMA7ZN2SLh808PKg28A6pqECZYu99Mpi6u_vo9WLU77igUi-P1UWEtuWEBXSPLQ1e5CSlBl7N_O13davOU7VndT8DZXgRwSlv_l3PmUQHG93BNdjxUpxzuc1lUKyde-M0xjhIBqQaYiwZ8UJeEWhuGbG1lsrCEzrsWMwo-FgBIakfVCMVCyQJk2w7E5bscPSQBuXLt4wlYUJZE5oSCGIj58xYZGmXd9ziUho5hH2KX5QA53nwcj4pbwfc-a4kE3ei12XuzCdsnjOgN1MxfCH2lfqHzGk9qxkPAF78e3nXU3U-wud8ZqMpYMXuGtf3ieA8bcXbrT_TftnA2KF1OaZ_Cn0Sv2qp6LHWkcAPPBe1KxkUSELv_6WHWmQDFpHKMYFMXTqgaDMDj0TLeNq1d00BBTw.m3u8
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2380252,RESOLUTION=1280x720,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="720p30"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDrxckkPKJOGpcsyTyuBoy-pTZVCqHuHQzm7N0fmyFc165xORl03bTfVbTY694T2U5WFAir8n62naHRMRgqJGJbvWyPx3lSetz6nKH-mY3TeDv-irBc6AEWfUbMmd5Nfh22CFBg__4iQxgvhru8TDlSDI2jR2pGkWCkN72bGN7iT6AXVcXTg_LVl1Tv1L7Sk8DFXm4jNHUPZo6utaYQ1jzlkEESTUE9PnXqrRRhIsm1HsCLsiUWZuBXyqgSbvJRl9LA1lFSoWEl-oGksc9BtFC5J25589KAc2vbvJpRvkiROlJaSwhD0m2LeOM9MiYU4HsFx94g8EC6WZrIQjWAVn7bcGAey0nsMZbWwBh38nCfRuk4Zh5AR2OvAmYMgFhT8s2kLxUhcrcMzJJpKVF15gRx1xwB4caFMVjtRNulVutR7acOJTVpftf-b2A8nWUiB_EdKTFjhOagYDbMFKUEEhWh159xir1YaVG4bu8qqsxroiToAqiB2MdbpxZnAWE8-ByJ_TgYBTFF7mu2WxTxT58cslGVR1B5EXlg-9RuzJFnE8uxgcwzWyN_lQK6DbIEV9Ge1rQp8HczxZ6Ly1W7OOZ1t8TNaljH0Aq_ZAvuZY-9L5N_pnfzLyk4rhLOsw-W3dk8y2R4jKAMch8QS4SEPqbvo6S7IXCMcEvS2qlWXYaDMVPpybtHPQRXtOUGw.m3u8
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1435252,RESOLUTION=852x480,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="480p30"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDzVCKhT87Cu678cooYX0w6oTbF6DmpnQ2XeBqjM2ru-PpHq7qOeI-ETldFN3cOQBpLWXv_s0wwNIqXei9MeK-teHn4NBK-U6hAFZjYqhdf2iDMAk8aUhPSXOrU9rXIbnYE1MgJsR0TUTqltBn14ZJAzdkMEfZx1PtaCh2E0HjeR8N4-N8wOP0QV7_rj8MMHKlmhHgPJURRUhza3RlIzrbzLyGAgCV64RxpyEsRFrysYxHph9X1yWygwYUyUz-ER6Byj6Ko55qcIUMEDLoR9IhV87V9pFFPhhk0YBYnpQYOdsmMCkRn5ZsRElAld22IuIVAPVEMhOfSQX_Pu2exBCcUIdQ_Sglj_nfWP6_FiGSCvN3LPtD3ZNgU8TXED9PxElHZzqUBU2iSoxxLBoE8C7Vd6BpDeepDuH1jkBxb-WwzMUs7pIdnrB98BNKQgYhMEUiQZARhpyG8NeGhOoMIfXcnLDQw6XmD7PZTM6HXOSGNtIlM8jdZUiDCEBwYMwfDXOoDvGuY9MBDXaIw_uFiWa5CiU44awF1v3-EHmloFUGVQSTS1xHUjywTtC5KUsGPVPjHoShKCWUSRRLaiHVVbXLo0oGXFxuSxiIFzLUUeKa7TNtbcHpyl1aIVgpe0iVPjsCE4duOcWfN0bJeqcSEFSTU15DhJrDLlqrysEI3ssaDLrB_bSkpe3nv55Saw.m3u8
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=630000,RESOLUTION=640x360,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="360p30"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDTjryGQpOcbZo5efDAoWz0JI__T4n7owyQCkQkw5otrJdA4MdYnrWx43dC6zmWGnNxTDEoSOJ3d4kKtPyccQc5fZCRIM0IjSJfK4fvD4VwPBf02eCrEREXqchqhiLhN_QBD163OqCAfoEbvifAqef-C9Czhy7Wy3H8kJkHXko3Yho8Mv1u5ENyjGlRaKBECbZavuTxjbkgzp_mbk60h8sYWbMFnT0xAVb3GEI59NRdd-0bvnu6eEuxJKWaEpqSrJ_m0G77afZ6yFr-aEUf92aEn_azpaFcUDs3Tt_6yub-zrQqtKluRlo25g14YZ18qJhab8FFoaRVKVzZaNZYVBLlf-5Bh8btnpNu8dg9SQe5IbHi6Wp4YSFhWK0BMO49FVdVUDSzxNFKHwiFFLr6kRMC5maVOxmvDY1bhLQjqge1yjVps82ByVasINeq9bxjkWILMRIWbOp8U_0wGXJAN_B7HE1mPYRQ7sfFaK5tQd0Qf_JVWdYZRzd9TvGJv2xgNEUCnbmNuzF2i52zwHTYoigE3YAJkT_zZb3PzWreyEGgoCd30GSfh9FOmeMnNPnb4EkSTxSYyDgDNQhSzmY1tpT9sGhDA_7biNX4vSUVka8zwfd79dTtv-jfuw3BrKxDARbW7aCSahXb7XCRU8SEOeSFUU-PulEu72Hg37qca8aDFFmPsBvds0OR-h-ig.m3u8
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=230000,RESOLUTION=284x160,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="160p30"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CusDgryHl1FRa03tGi6OqC7QbH2IjFzL5P73F5_wjyigOYsZl9IQeDTCNcb3KdPYLWViVYOkblpTiWSZIurBjsLybPM5CyG8ajiQdJmcokxSWuF0JCxKW3B9k4Jzd7kjAcTG2m6Q6AgAVUl-12pmDLPiqJdS9hOmHBBz8Jfqb-nI2CQn96nHXOAv0tn9HDToE_070JGD8IdSMXM1ha8bhASa7O5ykIYR_YsGgcQ8ifWvTikPOWbRel7XAo1ABjSTbhPFhwuhdBuS2R3CcDHNqbmHLbEdQkmi9K2ibgQ9Yq7XkjziTNpFHxh2FuwWcRSgPupd82pVSJIJHAwz9Bz2rSgNnHeZBcCuJ-xa7H2DPyTrWiE915Gr4Sb5xbNMPAf2kIBP-406j3bri43AMTNJIvOOv7E33Gl93iRGdXNE18zi5Bl_mwDAX4_3SE7m10pNRYqduvqgSYBRwSwt2JRPpjzwqL8oLeOeNSGs_N6BFfbkWXT7QZ5oJgZjUFW2RBxVwPnMXuRr1kzw6BD1LrxAZbEQpqcQGKf11AfaAshyT6zKxrpyKh_oC_wl7yu4VOgYeehEezQiSgMyFLkarfOeM6I-o7Usx3jFtChwVQ0QMXq2hGWmy6LMX0d-LrUASBMkxs3Ul7uX2T3gpzqoEFgSEEddVKslKF4gxBbMMRvroOsaDKTtGcbLwO10mWlpbw.m3u8
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="audio_only",AUTOSELECT=NO,DEFAULT=NO
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=166907,CODECS="mp4a.40.2",VIDEO="audio_only"
|
||||
https://video-weaver.cph01.hls.ttvnw.net/v1/playlist/CvADyY7WIxbtDSJ0njAsWODtSlU3th3xEoqA4xy0hB37DMPbQ-UOcK9ipvr4PSV5JZdacS0zHw1B7M7qMxNkGTz-4GresX5omdJK25JFJ3R57TdES2QqOYr6s9Njiov9VfYboWl93cTLeDC4bOk6jWa7CROjMQOdzZQFs7ItRvKWiIOWHJWmp20pkeGJGzKn7djFb1QYrmhEWpel36muLe62rV1iXDeAw2d6wzNH0cXF-Ub09fIawnTwECXvHaQAYMK6Ij4hqdDchOVewvLJQj7vxDLMQhus6bKPbgulojTO0CLvxHdrGydswSbiYYTib_5nIIuMXTNlZbTQ1vyPJgFZlOUHPDy8znoQanX4YQnTdx-9jQ35YA2AhdKJ6edHF3oKy3c-CDxBJIJavZqsMj2Zc_o6oEUQXJfrKAlmWUaYz0FFm6j8PDRGKxpyaiUYokYZ-W0wx_sNJDKOr_Fd_MN8sRCxwiTFRWJ1ht7tumnfpP177P3n3prplM__3cgleAdVwzULqx_5bjVJYbHkcVxCE0iGz21n2UaOWkW1CaFR9QxQunEfDuiTh_w_Ver3M9kWnFpU8S0LxYtjRNlvqU8o1ds4_vgaPNFXrMaN7fAnoN1ETawu5I0yDAWIX6PclLW6K5fyjSaCiCpk3M8O5KYCTRIQqSaiIAjvvYr_qGRuzVDHrRoMJSflyQR53h9-9ZMn.m3u8
|
10
sample-playlists/master-playlist-with-blankline.m3u8
Normal file
10
sample-playlists/master-playlist-with-blankline.m3u8
Normal file
|
@ -0,0 +1,10 @@
|
|||
#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
|
35
sample-playlists/master-with-alternatives-2.m3u8
Normal file
35
sample-playlists/master-with-alternatives-2.m3u8
Normal file
|
@ -0,0 +1,35 @@
|
|||
#EXTM3U
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac_sinewave/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8"
|
||||
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs"
|
||||
gear1/prog_index.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,CODECS="avc1.4d400d",URI="gear1/iframe_index.m3u8"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"
|
||||
gear2/prog_index.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,CODECS="avc1.4d401e",URI="gear2/iframe_index.m3u8"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs"
|
||||
gear3/prog_index.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,CODECS="avc1.4d401f",URI="gear3/iframe_index.m3u8"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs"
|
||||
gear4/prog_index.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,CODECS="avc1.4d401f",URI="gear4/iframe_index.m3u8"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"
|
||||
gear5/prog_index.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,CODECS="avc1.4d401f",URI="gear5/iframe_index.m3u8"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs"
|
||||
gear0/prog_index.m3u8
|
25
sample-playlists/media-playlist-with-cues-1.m3u8
Normal file
25
sample-playlists/media-playlist-with-cues-1.m3u8
Normal file
|
@ -0,0 +1,25 @@
|
|||
#EXTM3U
|
||||
# Borrowed from https://github.com/grafov/m3u8/pull/83
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXTINF:6.000,
|
||||
1.ts
|
||||
#EXT-X-DATERANGE:ID="20",START-DATE="2020-06-03T14:56:00Z",PLANNED-DURATION=19,SCTE35-OUT=0xFC302000000000000000FFF00F05000000147FFFFE001A17B0C0000000000061DFD67D
|
||||
#EXT-X-CUE-OUT:19.0
|
||||
#EXT-X-PROGRAM-DATE-TIME:2020-06-03T14:56:00Z
|
||||
#EXTINF:6.000,
|
||||
2.ts
|
||||
#EXTINF:6.000,
|
||||
3.ts
|
||||
#EXTINF:6.000,
|
||||
4.ts
|
||||
#EXT-X-CUE-IN
|
||||
#EXTINF:6.000,
|
||||
5.ts
|
||||
#EXTINF:6.000,
|
||||
6.ts
|
||||
#EXTINF:6.000,
|
||||
7.ts
|
||||
#EXTINF:6.000,
|
||||
8.ts
|
21
sample-playlists/media-playlist-with-cues.m3u8
Normal file
21
sample-playlists/media-playlist-with-cues.m3u8
Normal file
|
@ -0,0 +1,21 @@
|
|||
#EXTM3U
|
||||
#EXTINF:10,
|
||||
http://media.example.com/fileSequence7796.ts
|
||||
#EXTINF:6,
|
||||
http://media.example.com/fileSequence7797.ts
|
||||
#EXT-X-CUE-OUT:DURATION=30
|
||||
#EXTINF:4,
|
||||
http://media.example.com/fileSequence7798.ts
|
||||
#EXTINF:10,
|
||||
http://media.example.com/fileSequence7799.ts
|
||||
#EXTINF:10,
|
||||
http://media.example.com/fileSequence7800.ts
|
||||
#EXTINF:6,
|
||||
http://media.example.com/fileSequence7801.ts
|
||||
#EXT-X-CUE-IN
|
||||
#EXTINF:4,
|
||||
http://media.example.com/fileSequence7802.ts
|
||||
#EXTINF:10,
|
||||
http://media.example.com/fileSequence7803.ts
|
||||
#EXTINF:3,
|
||||
http://media.example.com/fileSequence7804.ts
|
|
@ -1,5 +1,3 @@
|
|||
# https://developer.apple.com/library/ios/technotes/tn2288/_index.html
|
||||
#
|
||||
#EXTM3U
|
||||
#EXT-X-TARGETDURATION:10
|
||||
#EXT-X-VERSION:3
|
||||
|
@ -12,4 +10,4 @@ ad1.ts
|
|||
#EXTINF:10.0,
|
||||
movieA.ts
|
||||
#EXTINF:10.0,
|
||||
movieB.ts
|
||||
movieB.ts
|
||||
|
|
30
sample-playlists/media-playlist-zero-decimal.m3u8
Normal file
30
sample-playlists/media-playlist-zero-decimal.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.00000,
|
||||
#EXT-X-BYTERANGE:86920@0
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:136595@86920
|
||||
main.aac
|
||||
#EXTINF:9.00000,
|
||||
#EXT-X-BYTERANGE:136567@223515
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:136954@360082
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:137116@497036
|
||||
main.aac
|
||||
#EXTINF:9.00000,
|
||||
#EXT-X-BYTERANGE:136770@634152
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:137219@770922
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:137132@908141
|
||||
main.acc
|
||||
#EXT-X-ENDLIST
|
579
src/lib.rs
579
src/lib.rs
|
@ -1,538 +1,101 @@
|
|||
//! A library to parse m3u8 playlists (HTTP Live Streaming) [link]
|
||||
//! (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19).
|
||||
//! A library to parse m3u8 playlists [HTTP Live Streaming](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19).
|
||||
//!
|
||||
//! #Examples
|
||||
//! # Examples
|
||||
//!
|
||||
//! Parsing a playlist and let the parser figure out if it's a media or master playlist.
|
||||
//!
|
||||
//! ```
|
||||
//! extern crate nom;
|
||||
//! extern crate m3u8_rs;
|
||||
//! use m3u8_rs::playlist::Playlist;
|
||||
//! use m3u8_rs::Playlist;
|
||||
//! use nom::IResult;
|
||||
//! 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 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),
|
||||
//! }
|
||||
//! match m3u8_rs::parse_playlist(&bytes) {
|
||||
//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl),
|
||||
//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl),
|
||||
//! Result::Err(e) => panic!("Parsing error: \n{}", e),
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Parsing a master playlist directly
|
||||
//!
|
||||
//! ```
|
||||
//! extern crate nom;
|
||||
//! extern crate m3u8_rs;
|
||||
//! use std::io::Read;
|
||||
//! use nom::IResult;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap();
|
||||
//! let mut bytes: Vec<u8> = Vec::new();
|
||||
//! file.read_to_end(&mut bytes).unwrap();
|
||||
//!
|
||||
//! if let IResult::Done(_, pl) = m3u8_rs::parse_master_playlist(&bytes) {
|
||||
//! println!("{:?}", pl);
|
||||
//! }
|
||||
//! }
|
||||
//! 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 Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) {
|
||||
//! println!("{:?}", pl);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Creating a playlist and writing it back to a vec/file
|
||||
//!
|
||||
//! ```
|
||||
//! extern crate m3u8_rs;
|
||||
//! use m3u8_rs::playlist::{MediaPlaylist, MediaPlaylistType, MediaSegment};
|
||||
//! use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment};
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let playlist = MediaPlaylist {
|
||||
//! version: 6,
|
||||
//! target_duration: 3.0,
|
||||
//! media_sequence: 338559,
|
||||
//! discontinuity_sequence: 1234,
|
||||
//! end_list: true,
|
||||
//! playlist_type: Some(MediaPlaylistType::Vod),
|
||||
//! segments: vec![
|
||||
//! MediaSegment {
|
||||
//! uri: "20140311T113819-01-338559live.ts".into(),
|
||||
//! duration: 2.002,
|
||||
//! title: Some("title".into()),
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! ],
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//!
|
||||
//! //let mut v: Vec<u8> = Vec::new();
|
||||
//! //playlist.write_to(&mut v).unwrap();
|
||||
//!
|
||||
//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap();
|
||||
//! //playlist.write_to(&mut file).unwrap();
|
||||
//! }
|
||||
//! let playlist = MediaPlaylist {
|
||||
//! version: Some(6),
|
||||
//! target_duration: 3,
|
||||
//! media_sequence: 338559,
|
||||
//! discontinuity_sequence: 1234,
|
||||
//! end_list: true,
|
||||
//! playlist_type: Some(MediaPlaylistType::Vod),
|
||||
//! segments: vec![
|
||||
//! MediaSegment {
|
||||
//! uri: "20140311T113819-01-338559live.ts".into(),
|
||||
//! duration: 2.002,
|
||||
//! title: Some("title".into()),
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! ],
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//!
|
||||
//! //let mut v: Vec<u8> = Vec::new();
|
||||
//! //playlist.write_to(&mut v).unwrap();
|
||||
//!
|
||||
//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap();
|
||||
//! //playlist.write_to(&mut file).unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! Controlling the output precision for floats, such as #EXTINF (default is unset)
|
||||
//!
|
||||
//! ```
|
||||
//! use std::sync::atomic::Ordering;
|
||||
//! use m3u8_rs::{WRITE_OPT_FLOAT_PRECISION, MediaPlaylist, MediaSegment};
|
||||
//!
|
||||
//! WRITE_OPT_FLOAT_PRECISION.store(5, Ordering::Relaxed);
|
||||
//!
|
||||
//! let playlist = MediaPlaylist {
|
||||
//! target_duration: 3,
|
||||
//! segments: vec![
|
||||
//! MediaSegment {
|
||||
//! duration: 2.9,
|
||||
//! title: Some("title".into()),
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! ],
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//!
|
||||
//! let mut v: Vec<u8> = Vec::new();
|
||||
//!
|
||||
//! playlist.write_to(&mut v).unwrap();
|
||||
//! let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
|
||||
//! assert!(m3u8_str.contains("#EXTINF:2.90000,title"));
|
||||
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
mod playlist;
|
||||
pub use playlist::*;
|
||||
|
||||
pub mod playlist;
|
||||
#[cfg(feature = "parser")]
|
||||
mod parser;
|
||||
|
||||
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 master playlist
|
||||
pub fn parse_master_playlist_res(input: &[u8]) -> Result<MasterPlaylist, IResult<&[u8], MasterPlaylist>> {
|
||||
let parse_result = parse_master_playlist(input);
|
||||
match parse_result {
|
||||
IResult::Done(_, playlist) => Ok(playlist),
|
||||
_ => Err(parse_result),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse input as a media playlist
|
||||
pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> {
|
||||
parse_media_playlist_tags(input).map(MediaPlaylist::from_tags)
|
||||
}
|
||||
|
||||
/// Parse input as a media playlist
|
||||
pub fn parse_media_playlist_res(input: &[u8]) -> Result<MediaPlaylist, IResult<&[u8], MediaPlaylist>> {
|
||||
let parse_result = parse_media_playlist(input);
|
||||
match parse_result {
|
||||
IResult::Done(_, playlist) => Ok(playlist),
|
||||
_ => Err(parse_result),
|
||||
}
|
||||
}
|
||||
|
||||
/// When a media tag or no master tag is found, this returns false.
|
||||
pub fn is_master_playlist(input: &[u8]) -> bool {
|
||||
// Assume it's not a master playlist
|
||||
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!(chain!(tag!("#EXT-X-PLAYLIST-TYPE:") ~ t:playlist_type, ||t), MediaPlaylistTag::PlaylistType)
|
||||
| map!(tag!("#EXT-X-I-FRAMES-ONLY"), |_| MediaPlaylistTag::IFramesOnly)
|
||||
| map!(start_tag, MediaPlaylistTag::Start)
|
||||
| map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MediaPlaylistTag::IndependentSegments)
|
||||
| map!(tag!("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList)
|
||||
|
||||
| map!(media_segment_tag, MediaPlaylistTag::Segment)
|
||||
)
|
||||
}
|
||||
|
||||
named!(pub playlist_type<MediaPlaylistType>,
|
||||
map_res!(
|
||||
map_res!(take_until_either_and_consume!("\r\n"), 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:byte_range_val, || r), SegmentTag::ByteRange)
|
||||
| map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity)
|
||||
| map!(chain!(tag!("#EXT-X-KEY:") ~ k:key, || k), SegmentTag::Key)
|
||||
| map!(chain!(tag!("#EXT-X-MAP:") ~ m:map, || m), SegmentTag::Map)
|
||||
| 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_either_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>, map!(key_value_pairs, Map::from_hashmap));
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// 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_or_default()
|
||||
)
|
||||
);
|
||||
|
||||
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 byte_range_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)
|
||||
}
|
||||
#[cfg(feature = "parser")]
|
||||
pub use self::parser::*;
|
||||
|
|
1004
src/parser.rs
Normal file
1004
src/parser.rs
Normal file
File diff suppressed because it is too large
Load diff
1213
src/playlist.rs
1213
src/playlist.rs
File diff suppressed because it is too large
Load diff
532
tests/lib.rs
532
tests/lib.rs
|
@ -1,20 +1,20 @@
|
|||
#![allow(unused_variables, unused_imports, dead_code)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
extern crate m3u8_rs;
|
||||
|
||||
use std::fs;
|
||||
use std::path;
|
||||
use chrono::prelude::*;
|
||||
use m3u8_rs::QuotedOrUnquoted::Quoted;
|
||||
use m3u8_rs::*;
|
||||
use m3u8_rs::playlist::*;
|
||||
use std::io::Read;
|
||||
use std::fs::File;
|
||||
use nom::*;
|
||||
use nom::AsBytes;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{fs, io};
|
||||
|
||||
fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
|
||||
fs::read_dir("sample-playlists\\").unwrap()
|
||||
let path: path::PathBuf = ["sample-playlists"].iter().collect();
|
||||
fs::read_dir(path.to_str().unwrap())
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.map(|dir| dir.path())
|
||||
.filter(|path| path.extension().map_or(false, |ext| ext == "m3u8"))
|
||||
|
@ -23,13 +23,14 @@ fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
|
|||
|
||||
fn getm3u(path: &str) -> String {
|
||||
let mut buf = String::new();
|
||||
let mut file = fs::File::open(path).expect("Can't find m3u8.");
|
||||
let mut file = File::open(path).unwrap_or_else(|_| panic!("Can't find m3u8: {}", path));
|
||||
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))
|
||||
let path: path::PathBuf = ["sample-playlists", name].iter().collect();
|
||||
getm3u(path.to_str().unwrap())
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
@ -40,11 +41,10 @@ fn print_parse_playlist_test(playlist_name: &str) -> bool {
|
|||
println!("Parsing playlist file: {:?}", playlist_name);
|
||||
let parsed = parse_playlist(input.as_bytes());
|
||||
|
||||
if let IResult::Done(i,o) = parsed {
|
||||
if let Ok((i, o)) = &parsed {
|
||||
println!("{:?}", o);
|
||||
true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
println!("Parsing failed:\n {:?}", parsed);
|
||||
false
|
||||
}
|
||||
|
@ -55,14 +55,23 @@ fn playlist_master_with_alternatives() {
|
|||
assert!(print_parse_playlist_test("master-with-alternatives.m3u8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_master_with_alternatives_2_3() {
|
||||
assert!(print_parse_playlist_test("master-with-alternatives-2.m3u8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_master_with_i_frame_stream_inf() {
|
||||
assert!(print_parse_playlist_test("master-with-i-frame-stream-inf.m3u8"));
|
||||
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"));
|
||||
assert!(print_parse_playlist_test(
|
||||
"master-with-multiple-codecs.m3u8"
|
||||
));
|
||||
}
|
||||
|
||||
// -- Media playlists
|
||||
|
@ -74,7 +83,31 @@ fn playlist_media_standard() {
|
|||
|
||||
#[test]
|
||||
fn playlist_media_without_segments() {
|
||||
assert!(print_parse_playlist_test("media-playlist-without-segments.m3u8"));
|
||||
assert!(print_parse_playlist_test(
|
||||
"media-playlist-without-segments.m3u8"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_media_with_cues() {
|
||||
assert!(print_parse_playlist_test("media-playlist-with-cues.m3u8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_media_with_cues1() {
|
||||
assert!(print_parse_playlist_test("media-playlist-with-cues-1.m3u8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_media_with_scte35() {
|
||||
assert!(print_parse_playlist_test("media-playlist-with-scte35.m3u8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_media_with_scte35_1() {
|
||||
assert!(print_parse_playlist_test(
|
||||
"media-playlist-with-scte35-1.m3u8"
|
||||
));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
@ -82,12 +115,23 @@ fn playlist_media_without_segments() {
|
|||
|
||||
#[test]
|
||||
fn playlist_not_ending_in_newline_master() {
|
||||
assert!(print_parse_playlist_test("master-not-ending-in-newline.m3u8"));
|
||||
assert!(print_parse_playlist_test(
|
||||
"master-not-ending-in-newline.m3u8"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_not_ending_in_newline_master1() {
|
||||
assert!(print_parse_playlist_test(
|
||||
"master-not-ending-in-newline-1.m3u8"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_not_ending_in_newline_media() {
|
||||
assert!(print_parse_playlist_test("media-not-ending-in-newline.m3u8"));
|
||||
assert!(print_parse_playlist_test(
|
||||
"media-not-ending-in-newline.m3u8"
|
||||
));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
@ -97,14 +141,14 @@ fn playlist_not_ending_in_newline_media() {
|
|||
fn playlist_type_is_master() {
|
||||
let input = get_sample_playlist("master.m3u8");
|
||||
let result = is_master_playlist(input.as_bytes());
|
||||
assert_eq!(true, result);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn playlist_type_with_unkown_tag() {
|
||||
// fn playlist_type_with_unknown_tag() {
|
||||
// let input = get_sample_playlist("!!");
|
||||
// let result = is_master_playlist(input.as_bytes());
|
||||
// println!("Playlist_type_with_unkown_tag is master playlist: {:?}", result);
|
||||
// println!("Playlist_type_with_unknown_tag is master playlist: {:?}", result);
|
||||
// assert_eq!(true, result);
|
||||
// }
|
||||
|
||||
|
@ -115,127 +159,12 @@ fn playlist_types() {
|
|||
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);
|
||||
|
||||
assert_eq!(path.to_lowercase().contains("master"), 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)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_duration_title() {
|
||||
assert_eq!(
|
||||
duration_title_tag(b"2.002,title\nrest"),
|
||||
IResult::Done("rest".as_bytes(), (2.002f32, Some("title".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Creating playlists
|
||||
|
||||
|
@ -246,10 +175,12 @@ fn print_create_and_parse_playlist(playlist_original: &mut Playlist) -> Playlist
|
|||
let m3u8_str: &str = std::str::from_utf8(&utf8).unwrap();
|
||||
|
||||
let playlist_parsed = match *playlist_original {
|
||||
Playlist::MasterPlaylist(_) =>
|
||||
Playlist::MasterPlaylist(parse_master_playlist_res(m3u8_str.as_bytes()).unwrap()),
|
||||
Playlist::MediaPlaylist(_) =>
|
||||
Playlist::MediaPlaylist(parse_media_playlist_res(m3u8_str.as_bytes()).unwrap()),
|
||||
Playlist::MasterPlaylist(_) => {
|
||||
Playlist::MasterPlaylist(parse_master_playlist_res(m3u8_str.as_bytes()).unwrap())
|
||||
}
|
||||
Playlist::MediaPlaylist(_) => {
|
||||
Playlist::MediaPlaylist(parse_media_playlist_res(m3u8_str.as_bytes()).unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
print!("\n\n---- utf8 result\n\n{}", m3u8_str);
|
||||
|
@ -261,64 +192,132 @@ fn print_create_and_parse_playlist(playlist_original: &mut Playlist) -> Playlist
|
|||
|
||||
#[test]
|
||||
fn create_and_parse_master_playlist_empty() {
|
||||
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { ..Default::default() });
|
||||
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
|
||||
..Default::default()
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_master_playlist_full() {
|
||||
fn create_segment_float_inf() {
|
||||
let playlist = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
version: Some(6),
|
||||
target_duration: 3,
|
||||
media_sequence: 338559,
|
||||
discontinuity_sequence: 1234,
|
||||
end_list: true,
|
||||
playlist_type: Some(MediaPlaylistType::Vod),
|
||||
segments: vec![MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.000f32,
|
||||
title: Some("title".into()),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut v: Vec<u8> = Vec::new();
|
||||
playlist.write_to(&mut v).unwrap();
|
||||
let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
|
||||
assert!(m3u8_str.contains("#EXTINF:2,title"));
|
||||
|
||||
WRITE_OPT_FLOAT_PRECISION.store(5, Ordering::Relaxed);
|
||||
|
||||
playlist.write_to(&mut v).unwrap();
|
||||
let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
|
||||
assert!(m3u8_str.contains("#EXTINF:2.00000,title"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_master_playlist_full() {
|
||||
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
|
||||
version: 6,
|
||||
variants: vec![
|
||||
VariantStream {
|
||||
is_i_frame: false,
|
||||
uri: "masterplaylist-uri".into(),
|
||||
bandwidth: "10010010".into(),
|
||||
average_bandwidth: Some("10010010".into()),
|
||||
codecs: "TheCODEC".into(),
|
||||
resolution: Some("1000x3000".into()),
|
||||
frame_rate: Some("60".into()),
|
||||
audio: Some("audio".into()),
|
||||
video: Some("video".into()),
|
||||
subtitles: Some("subtitles".into()),
|
||||
closed_captions: Some("closed_captions".into()),
|
||||
alternatives: vec! [
|
||||
AlternativeMedia {
|
||||
media_type: AlternativeMediaType::Audio,
|
||||
uri: Some("alt-media-uri".into()),
|
||||
group_id: "group-id".into(),
|
||||
language: Some("language".into()),
|
||||
assoc_language: Some("assoc-language".into()),
|
||||
name: "Xmedia".into(),
|
||||
default: true, // Its absence indicates an implicit value of NO
|
||||
autoselect: true, // Its absence indicates an implicit value of NO
|
||||
forced: true, // Its absence indicates an implicit value of NO
|
||||
instream_id: Some("instream_id".into()),
|
||||
characteristics: Some("characteristics".into()),
|
||||
}
|
||||
]
|
||||
}
|
||||
version: Some(6),
|
||||
alternatives: vec![
|
||||
AlternativeMedia {
|
||||
media_type: AlternativeMediaType::Audio,
|
||||
uri: Some("alt-media-uri".into()),
|
||||
group_id: "group-id".into(),
|
||||
language: Some("language".into()),
|
||||
assoc_language: Some("assoc-language".into()),
|
||||
name: "Xmedia".into(),
|
||||
default: true, // Its absence indicates an implicit value of NO
|
||||
autoselect: true, // Its absence indicates an implicit value of NO
|
||||
forced: false, // Its absence indicates an implicit value of NO
|
||||
instream_id: None,
|
||||
characteristics: Some("characteristics".into()),
|
||||
channels: Some("channels".into()),
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
AlternativeMedia {
|
||||
media_type: AlternativeMediaType::Subtitles,
|
||||
uri: Some("alt-media-uri".into()),
|
||||
group_id: "group-id".into(),
|
||||
language: Some("language".into()),
|
||||
assoc_language: Some("assoc-language".into()),
|
||||
name: "Xmedia".into(),
|
||||
default: true, // Its absence indicates an implicit value of NO
|
||||
autoselect: true, // Its absence indicates an implicit value of NO
|
||||
forced: true, // Its absence indicates an implicit value of NO
|
||||
instream_id: None,
|
||||
characteristics: Some("characteristics".into()),
|
||||
channels: Some("channels".into()),
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
AlternativeMedia {
|
||||
media_type: AlternativeMediaType::ClosedCaptions,
|
||||
uri: None,
|
||||
group_id: "group-id".into(),
|
||||
language: Some("language".into()),
|
||||
assoc_language: Some("assoc-language".into()),
|
||||
name: "Xmedia".into(),
|
||||
default: true, // Its absence indicates an implicit value of NO
|
||||
autoselect: true, // Its absence indicates an implicit value of NO
|
||||
forced: false, // Its absence indicates an implicit value of NO
|
||||
instream_id: Some(InstreamId::CC(1)),
|
||||
characteristics: Some("characteristics".into()),
|
||||
channels: Some("channels".into()),
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
],
|
||||
session_data: Some(SessionData {
|
||||
variants: vec![VariantStream {
|
||||
is_i_frame: false,
|
||||
uri: "masterplaylist-uri".into(),
|
||||
bandwidth: 10010010,
|
||||
average_bandwidth: Some(10010010),
|
||||
codecs: Some("TheCODEC".into()),
|
||||
resolution: Some(Resolution {
|
||||
width: 1000,
|
||||
height: 3000,
|
||||
}),
|
||||
frame_rate: Some(60.0),
|
||||
hdcp_level: Some(HDCPLevel::None),
|
||||
audio: Some("audio".into()),
|
||||
video: Some("video".into()),
|
||||
subtitles: Some("subtitles".into()),
|
||||
closed_captions: Some(ClosedCaptionGroupId::GroupId("closed_captions".into())),
|
||||
other_attributes: Default::default(),
|
||||
}],
|
||||
session_data: vec![SessionData {
|
||||
data_id: "****".into(),
|
||||
value: "%%%%".into(),
|
||||
uri: "++++".into(),
|
||||
language: Some("SessionDataLanguage".into()),
|
||||
}),
|
||||
session_key: Some(SessionKey(Key {
|
||||
method: "AES-128".into(),
|
||||
field: SessionDataField::Value("%%%%".to_string()),
|
||||
language: Some("SessionDataLanguage".into()),
|
||||
other_attributes: Default::default(),
|
||||
}],
|
||||
session_key: vec![SessionKey(Key {
|
||||
method: KeyMethod::AES128,
|
||||
uri: Some("https://secure.domain.com".into()),
|
||||
iv: Some("0xb059217aa2649ce170b734".into()),
|
||||
keyformat: Some("xXkeyformatXx".into()),
|
||||
keyformatversions: Some("xXFormatVers".into()),
|
||||
})),
|
||||
})],
|
||||
start: Some(Start {
|
||||
time_offset: "123123123".into(),
|
||||
precise: Some("YES".into()),
|
||||
time_offset: "123123123".parse().unwrap(),
|
||||
precise: Some(true),
|
||||
other_attributes: Default::default(),
|
||||
}),
|
||||
independent_segments: true,
|
||||
unknown_tags: vec![],
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
|
@ -326,23 +325,24 @@ fn create_and_parse_master_playlist_full() {
|
|||
|
||||
#[test]
|
||||
fn create_and_parse_media_playlist_empty() {
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { ..Default::default() });
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
..Default::default()
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_media_playlist_single_segment() {
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
segments: vec![
|
||||
MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
title: Some("hey".into()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
target_duration: 2,
|
||||
segments: vec![MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
title: Some("hey".into()),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
|
@ -350,43 +350,123 @@ fn create_and_parse_media_playlist_single_segment() {
|
|||
|
||||
#[test]
|
||||
fn create_and_parse_media_playlist_full() {
|
||||
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
version: 4,
|
||||
target_duration: 3.0,
|
||||
version: Some(4),
|
||||
target_duration: 3,
|
||||
media_sequence: 338559,
|
||||
discontinuity_sequence: 1234,
|
||||
end_list: true,
|
||||
playlist_type: Some(MediaPlaylistType::Vod),
|
||||
i_frames_only: true,
|
||||
start: Some(Start {
|
||||
time_offset: "9999".into(),
|
||||
precise: Some("YES".into()),
|
||||
time_offset: "9999".parse().unwrap(),
|
||||
precise: Some(true),
|
||||
other_attributes: Default::default(),
|
||||
}),
|
||||
independent_segments: true,
|
||||
segments: vec![
|
||||
MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
title: Some("338559".into()),
|
||||
byte_range: Some(ByteRange::from("137116@497036")),
|
||||
discontinuity: true,
|
||||
key: Some(Key {
|
||||
method: "AES-128".into(),
|
||||
uri: Some("https://secure.domain.com".into()),
|
||||
iv: Some("0xb059217aa2649ce170b734".into()),
|
||||
keyformat: Some("xXkeyformatXx".into()),
|
||||
keyformatversions: Some("xXFormatVers".into()),
|
||||
}),
|
||||
map: Some(Map {
|
||||
uri: "www.map-uri.com".into(),
|
||||
byte_range: Some(ByteRange::from("137116@497036")),
|
||||
independent_segments: true,
|
||||
segments: vec![MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
title: Some("338559".into()),
|
||||
byte_range: Some(ByteRange {
|
||||
length: 137116,
|
||||
offset: Some(4559),
|
||||
}),
|
||||
discontinuity: true,
|
||||
key: Some(Key {
|
||||
method: KeyMethod::None,
|
||||
uri: Some("https://secure.domain.com".into()),
|
||||
iv: Some("0xb059217aa2649ce170b734".into()),
|
||||
keyformat: Some("xXkeyformatXx".into()),
|
||||
keyformatversions: Some("xXFormatVers".into()),
|
||||
}),
|
||||
map: Some(Map {
|
||||
uri: "www.map-uri.com".into(),
|
||||
byte_range: Some(ByteRange {
|
||||
length: 137116,
|
||||
offset: Some(4559),
|
||||
}),
|
||||
program_date_time: Some("broodlordinfestorgg".into()),
|
||||
daterange: None,
|
||||
},
|
||||
],
|
||||
other_attributes: Default::default(),
|
||||
}),
|
||||
program_date_time: Some(
|
||||
chrono::FixedOffset::east(8 * 3600)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
),
|
||||
daterange: Some(DateRange {
|
||||
id: "9999".into(),
|
||||
class: Some("class".into()),
|
||||
start_date: chrono::FixedOffset::east(8 * 3600)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
end_date: None,
|
||||
duration: None,
|
||||
planned_duration: Some("40.000".parse().unwrap()),
|
||||
x_prefixed: Some(HashMap::from([(
|
||||
"X-client-attribute".into(),
|
||||
"whatever".into(),
|
||||
)])),
|
||||
end_on_next: false,
|
||||
other_attributes: Default::default(),
|
||||
}),
|
||||
unknown_tags: vec![ExtTag {
|
||||
tag: "X-CUE-OUT".into(),
|
||||
rest: Some("DURATION=2.002".into()),
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
unknown_tags: vec![],
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Roundtrip
|
||||
|
||||
#[test]
|
||||
fn parsing_write_to_should_produce_the_same_structure() {
|
||||
for playlist in all_sample_m3u_playlists() {
|
||||
let input = getm3u(playlist.to_str().unwrap());
|
||||
|
||||
let expected = parse_playlist_res(input.as_bytes()).unwrap();
|
||||
let mut written: Vec<u8> = Vec::new();
|
||||
expected.write_to(&mut written).unwrap();
|
||||
|
||||
let actual = parse_playlist_res(&written).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected,
|
||||
actual,
|
||||
"\n\nFailed parser input:\n\n{}\n\nOriginal input:\n\n{}",
|
||||
std::str::from_utf8(&written).unwrap(),
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Failure on arbitrary text files that don't start with #EXTM3U8
|
||||
|
||||
#[test]
|
||||
fn parsing_text_file_should_fail() {
|
||||
let s = "
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
||||
deserunt mollit anim id est laborum.
|
||||
";
|
||||
let res = parse_master_playlist_res(s.as_bytes());
|
||||
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_binary_data_should_fail_cleanly() {
|
||||
let data = (0..1024).map(|i| (i % 255) as u8).collect::<Vec<u8>>();
|
||||
let res = parse_master_playlist_res(&data);
|
||||
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue