mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-12-22 22:46:37 +00:00
b5b950150f
Hardcoding the windows path separator isn't a good idea for portability :)
394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
#![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 m3u8_rs::playlist::*;
|
|
use std::io::Read;
|
|
use std::fs::File;
|
|
use nom::*;
|
|
use std::collections::HashMap;
|
|
|
|
fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
|
|
let path: std::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"))
|
|
.collect()
|
|
}
|
|
|
|
fn getm3u(path: &str) -> String {
|
|
let mut buf = String::new();
|
|
let mut file = fs::File::open(path).expect(&format!("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 {
|
|
let path: std::path::PathBuf = ["sample-playlists", name].iter().collect();
|
|
getm3u(path.to_str().unwrap())
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Playlist
|
|
|
|
fn print_parse_playlist_test(playlist_name: &str) -> bool {
|
|
let input: String = get_sample_playlist(playlist_name);
|
|
println!("Parsing playlist file: {:?}", playlist_name);
|
|
let parsed = parse_playlist(input.as_bytes());
|
|
|
|
if let IResult::Done(i,o) = parsed {
|
|
println!("{:?}", o);
|
|
true
|
|
}
|
|
else {
|
|
println!("Parsing failed:\n {:?}", parsed);
|
|
false
|
|
}
|
|
}
|
|
|
|
#[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 with no newline end
|
|
|
|
#[test]
|
|
fn playlist_not_ending_in_newline_master() {
|
|
assert!(print_parse_playlist_test("master-not-ending-in-newline.m3u8"));
|
|
}
|
|
|
|
#[test]
|
|
fn playlist_not_ending_in_newline_media() {
|
|
assert!(print_parse_playlist_test("media-not-ending-in-newline.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("!!");
|
|
// 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)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_duration_title() {
|
|
assert_eq!(
|
|
duration_title_tag(b"2.002,title\nrest"),
|
|
IResult::Done("rest".as_bytes(), (2.002f32, Some("title".to_string())))
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Creating playlists
|
|
|
|
fn print_create_and_parse_playlist(playlist_original: &mut Playlist) -> Playlist {
|
|
let mut utf8: Vec<u8> = Vec::new();
|
|
playlist_original.write_to(&mut utf8).unwrap();
|
|
|
|
let m3u8_str: &str = std::str::from_utf8(&utf8).unwrap();
|
|
|
|
let playlist_parsed = match *playlist_original {
|
|
Playlist::MasterPlaylist(_) =>
|
|
Playlist::MasterPlaylist(parse_master_playlist_res(m3u8_str.as_bytes()).unwrap()),
|
|
Playlist::MediaPlaylist(_) =>
|
|
Playlist::MediaPlaylist(parse_media_playlist_res(m3u8_str.as_bytes()).unwrap()),
|
|
};
|
|
|
|
print!("\n\n---- utf8 result\n\n{}", m3u8_str);
|
|
print!("\n---- Original\n\n{:?}", playlist_original);
|
|
print!("\n\n---- Parsed\n\n{:?}\n\n", playlist_parsed);
|
|
|
|
playlist_parsed
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_parse_master_playlist_empty() {
|
|
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { ..Default::default() });
|
|
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
|
assert_eq!(playlist_original, playlist_parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_parse_master_playlist_full() {
|
|
|
|
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
|
|
version: 6,
|
|
variants: vec![
|
|
VariantStream {
|
|
is_i_frame: false,
|
|
uri: "masterplaylist-uri".into(),
|
|
bandwidth: "10010010".into(),
|
|
average_bandwidth: Some("10010010".into()),
|
|
codecs: "TheCODEC".into(),
|
|
resolution: Some("1000x3000".into()),
|
|
frame_rate: Some("60".into()),
|
|
audio: Some("audio".into()),
|
|
video: Some("video".into()),
|
|
subtitles: Some("subtitles".into()),
|
|
closed_captions: Some("closed_captions".into()),
|
|
alternatives: vec! [
|
|
AlternativeMedia {
|
|
media_type: AlternativeMediaType::Audio,
|
|
uri: Some("alt-media-uri".into()),
|
|
group_id: "group-id".into(),
|
|
language: Some("language".into()),
|
|
assoc_language: Some("assoc-language".into()),
|
|
name: "Xmedia".into(),
|
|
default: true, // Its absence indicates an implicit value of NO
|
|
autoselect: true, // Its absence indicates an implicit value of NO
|
|
forced: true, // Its absence indicates an implicit value of NO
|
|
instream_id: Some("instream_id".into()),
|
|
characteristics: Some("characteristics".into()),
|
|
}
|
|
]
|
|
}
|
|
],
|
|
session_data: Some(SessionData {
|
|
data_id: "****".into(),
|
|
value: "%%%%".into(),
|
|
uri: "++++".into(),
|
|
language: Some("SessionDataLanguage".into()),
|
|
}),
|
|
session_key: Some(SessionKey(Key {
|
|
method: "AES-128".into(),
|
|
uri: Some("https://secure.domain.com".into()),
|
|
iv: Some("0xb059217aa2649ce170b734".into()),
|
|
keyformat: Some("xXkeyformatXx".into()),
|
|
keyformatversions: Some("xXFormatVers".into()),
|
|
})),
|
|
start: Some(Start {
|
|
time_offset: "123123123".into(),
|
|
precise: Some("YES".into()),
|
|
}),
|
|
independent_segments: true,
|
|
});
|
|
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
|
assert_eq!(playlist_original, playlist_parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_parse_media_playlist_empty() {
|
|
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { ..Default::default() });
|
|
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
|
assert_eq!(playlist_original, playlist_parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_parse_media_playlist_single_segment() {
|
|
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
|
segments: vec![
|
|
MediaSegment {
|
|
uri: "20140311T113819-01-338559live.ts".into(),
|
|
duration: 2.002,
|
|
title: Some("hey".into()),
|
|
..Default::default()
|
|
},
|
|
],
|
|
..Default::default()
|
|
});
|
|
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
|
assert_eq!(playlist_original, playlist_parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_parse_media_playlist_full() {
|
|
|
|
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
|
version: 4,
|
|
target_duration: 3.0,
|
|
media_sequence: 338559,
|
|
discontinuity_sequence: 1234,
|
|
end_list: true,
|
|
playlist_type: Some(MediaPlaylistType::Vod),
|
|
i_frames_only: true,
|
|
start: Some(Start {
|
|
time_offset: "9999".into(),
|
|
precise: Some("YES".into()),
|
|
}),
|
|
independent_segments: true,
|
|
segments: vec![
|
|
MediaSegment {
|
|
uri: "20140311T113819-01-338559live.ts".into(),
|
|
duration: 2.002,
|
|
title: Some("338559".into()),
|
|
byte_range: Some(ByteRange::from("137116@497036")),
|
|
discontinuity: true,
|
|
key: Some(Key {
|
|
method: "AES-128".into(),
|
|
uri: Some("https://secure.domain.com".into()),
|
|
iv: Some("0xb059217aa2649ce170b734".into()),
|
|
keyformat: Some("xXkeyformatXx".into()),
|
|
keyformatversions: Some("xXFormatVers".into()),
|
|
}),
|
|
map: Some(Map {
|
|
uri: "www.map-uri.com".into(),
|
|
byte_range: Some(ByteRange::from("137116@497036")),
|
|
}),
|
|
program_date_time: Some("broodlordinfestorgg".into()),
|
|
daterange: None,
|
|
},
|
|
],
|
|
});
|
|
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
|
assert_eq!(playlist_original, playlist_parsed);
|
|
}
|