mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-21 23:01:00 +00:00
Initial commit
This commit is contained in:
commit
c067448428
18 changed files with 973 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "hls_m3u8"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Takeru Ohta <phjgt308@gmail.com>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
trackable = "0.2"
|
9
README.md
Normal file
9
README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
hls_m3u8
|
||||||
|
=========
|
||||||
|
|
||||||
|
References
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- [HTTP Live Streaming][rfc8216]
|
||||||
|
|
||||||
|
[rfc8216]: https://tools.ietf.org/html/rfc8216
|
12
examples/data/rfc8216_8-1.m3u8
Normal file
12
examples/data/rfc8216_8-1.m3u8
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-TARGETDURATION:10
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#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
|
||||||
|
|
||||||
|
# 8.1. Simple Media Playlist
|
13
examples/data/rfc8216_8-2.m3u8
Normal file
13
examples/data/rfc8216_8-2.m3u8
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:8
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:2680
|
||||||
|
|
||||||
|
#EXTINF:7.975,
|
||||||
|
https://priv.example.com/fileSequence2680.ts
|
||||||
|
#EXTINF:7.941,
|
||||||
|
https://priv.example.com/fileSequence2681.ts
|
||||||
|
#EXTINF:7.975,
|
||||||
|
https://priv.example.com/fileSequence2682.ts
|
||||||
|
|
||||||
|
# 8.2. Live Media Playlist Using HTTPS
|
20
examples/data/rfc8216_8-3.m3u8
Normal file
20
examples/data/rfc8216_8-3.m3u8
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#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
|
||||||
|
|
||||||
|
# 8.3. Playlist with Encrypted Media Segments
|
11
examples/data/rfc8216_8-4.m3u8
Normal file
11
examples/data/rfc8216_8-4.m3u8
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#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
|
||||||
|
|
||||||
|
# 8.4. Master Playlist
|
14
examples/data/rfc8216_8-5.m3u8
Normal file
14
examples/data/rfc8216_8-5.m3u8
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=1280000
|
||||||
|
low/audio-video.m3u8
|
||||||
|
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8"
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=2560000
|
||||||
|
mid/audio-video.m3u8
|
||||||
|
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=7680000
|
||||||
|
hi/audio-video.m3u8
|
||||||
|
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8"
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
|
||||||
|
audio-only.m3u8
|
||||||
|
|
||||||
|
# 8.5. Master Playlist with I-Frames
|
14
examples/data/rfc8216_8-6.m3u8
Normal file
14
examples/data/rfc8216_8-6.m3u8
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English", DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en", URI="main/english-audio.m3u8"
|
||||||
|
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch", DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de", URI="main/german-audio.m3u8"
|
||||||
|
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary", DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en", URI="commentary/audio-only.m3u8"
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",AUDIO="aac"
|
||||||
|
low/video-only.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",AUDIO="aac"
|
||||||
|
mid/video-only.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",AUDIO="aac"
|
||||||
|
hi/video-only.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac"
|
||||||
|
main/english-audio.m3u8
|
||||||
|
|
||||||
|
# 8.6. Master Playlist with Alternative Audio
|
23
examples/data/rfc8216_8-7.m3u8
Normal file
23
examples/data/rfc8216_8-7.m3u8
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#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,CODECS="...",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,CODECS="...",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,CODECS="...",VIDEO="hi"
|
||||||
|
hi/main/audio-video.m3u8
|
||||||
|
|
||||||
|
# 8.7. Master Playlist with Alternative Video
|
18
examples/parse.rs
Normal file
18
examples/parse.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
extern crate hls_m3u8;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate trackable;
|
||||||
|
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use trackable::error::Failure;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut m3u8 = String::new();
|
||||||
|
track_try_unwrap!(
|
||||||
|
io::stdin()
|
||||||
|
.read_to_string(&mut m3u8)
|
||||||
|
.map_err(Failure::from_error)
|
||||||
|
);
|
||||||
|
for line in hls_m3u8::line::Lines::new(&m3u8) {
|
||||||
|
println!("{:?}", track_try_unwrap!(line));
|
||||||
|
}
|
||||||
|
}
|
95
src/attribute.rs
Normal file
95
src/attribute.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
use {Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AttributePairs<'a> {
|
||||||
|
input: &'a str,
|
||||||
|
}
|
||||||
|
impl<'a> AttributePairs<'a> {
|
||||||
|
pub fn parse(input: &'a str) -> Self {
|
||||||
|
AttributePairs { input }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_name(&mut self) -> Result<&'a str> {
|
||||||
|
for i in 0..self.input.len() {
|
||||||
|
match self.input.as_bytes()[i] {
|
||||||
|
b'=' => {
|
||||||
|
let (key, _) = self.input.split_at(i);
|
||||||
|
let (_, rest) = self.input.split_at(i + 1);
|
||||||
|
self.input = rest;
|
||||||
|
return Ok(key);
|
||||||
|
}
|
||||||
|
b'A'...b'Z' | b'0'...b'9' | b'-' => {}
|
||||||
|
_ => track_panic!(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Malformed attribute name: {:?}",
|
||||||
|
self.input
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
track_panic!(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"No attribute value: {:?}",
|
||||||
|
self.input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_raw_value(&mut self) -> &'a str {
|
||||||
|
let (value_end, next) = if let Some(i) = self.input.bytes().position(|c| c == b',') {
|
||||||
|
(i, i + 1)
|
||||||
|
} else {
|
||||||
|
(self.input.len(), self.input.len())
|
||||||
|
};
|
||||||
|
let (value, _) = self.input.split_at(value_end);
|
||||||
|
let (_, rest) = self.input.split_at(next);
|
||||||
|
self.input = rest;
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Iterator for AttributePairs<'a> {
|
||||||
|
type Item = Result<(&'a str, &'a str)>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.input.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = || -> Result<(&'a str, &'a str)> {
|
||||||
|
let key = track!(self.parse_name())?;
|
||||||
|
let value = self.parse_raw_value();
|
||||||
|
Ok((key, value))
|
||||||
|
}();
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct QuotedString(String);
|
||||||
|
impl fmt::Display for QuotedString {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{:?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for QuotedString {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let len = s.len();
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
track_assert!(len >= 2, ErrorKind::InvalidInput);
|
||||||
|
track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput);
|
||||||
|
track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) };
|
||||||
|
track_assert!(
|
||||||
|
s.chars().all(|c| c != '\r' && c != '\n' && c != '"'),
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"{:?}",
|
||||||
|
s
|
||||||
|
);
|
||||||
|
Ok(QuotedString(s.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct HexadecimalSequence(Vec<u8>);
|
12
src/error.rs
Normal file
12
src/error.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use trackable::error::{ErrorKind as TrackableErrorKind, TrackableError};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Error(TrackableError<ErrorKind>);
|
||||||
|
derive_traits_for_trackable_error_newtype!(Error, ErrorKind);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
InvalidInput,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
impl TrackableErrorKind for ErrorKind {}
|
18
src/lib.rs
Normal file
18
src/lib.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate trackable;
|
||||||
|
|
||||||
|
// pub mod playlist;
|
||||||
|
// pub mod media_playlist;
|
||||||
|
// pub mod master_playlist;
|
||||||
|
// pub mod media_segment;
|
||||||
|
pub use error::{Error, ErrorKind};
|
||||||
|
|
||||||
|
pub mod attribute;
|
||||||
|
pub mod string;
|
||||||
|
pub mod tag;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
pub mod line;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
98
src/line.rs
Normal file
98
src/line.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use {ErrorKind, Result};
|
||||||
|
use tag::Tag;
|
||||||
|
|
||||||
|
// [rfc8216#section-4.1]
|
||||||
|
// > Playlist files MUST be encoded in UTF-8 [RFC3629]. They MUST NOT
|
||||||
|
// > contain any Byte Order Mark (BOM); clients SHOULD fail to parse
|
||||||
|
// > Playlists that contain a BOM or do not parse as UTF-8. Playlist
|
||||||
|
// > files MUST NOT contain UTF-8 control characters (U+0000 to U+001F and
|
||||||
|
// > U+007F to U+009F), with the exceptions of CR (U+000D) and LF
|
||||||
|
// > (U+000A). All character sequences MUST be normalized according to
|
||||||
|
// > Unicode normalization form "NFC" [UNICODE]. Note that US-ASCII
|
||||||
|
// > [US_ASCII] conforms to these rules.
|
||||||
|
// >
|
||||||
|
// > Lines in a Playlist file are terminated by either a single line feed
|
||||||
|
// > character or a carriage return character followed by a line feed
|
||||||
|
// > character.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Lines<'a> {
|
||||||
|
input: &'a str,
|
||||||
|
}
|
||||||
|
impl<'a> Lines<'a> {
|
||||||
|
pub fn new(input: &'a str) -> Self {
|
||||||
|
Lines { input }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line(&mut self) -> Result<Line<'a>> {
|
||||||
|
let mut end = self.input.len();
|
||||||
|
let mut next_start = self.input.len();
|
||||||
|
let mut adjust = 0;
|
||||||
|
for (i, c) in self.input.char_indices() {
|
||||||
|
match c {
|
||||||
|
'\n' => {
|
||||||
|
next_start = i + 1;
|
||||||
|
end = i - adjust;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
'\r' => {
|
||||||
|
adjust = 1;
|
||||||
|
}
|
||||||
|
'\u{00}'...'\u{1F}' | '\u{7F}'...'\u{9f}' => {
|
||||||
|
track_panic!(ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
adjust = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let raw_line = &self.input[..end];
|
||||||
|
let line = if raw_line.is_empty() {
|
||||||
|
Line::Blank
|
||||||
|
} else if raw_line.starts_with("#EXT") {
|
||||||
|
Line::Tag(track!(raw_line.parse())?)
|
||||||
|
} else if raw_line.starts_with("#") {
|
||||||
|
Line::Comment(raw_line)
|
||||||
|
} else {
|
||||||
|
Line::Uri(raw_line)
|
||||||
|
};
|
||||||
|
self.input = &self.input[next_start..];
|
||||||
|
Ok(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Iterator for Lines<'a> {
|
||||||
|
type Item = Result<Line<'a>>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.input.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match track!(self.read_line()) {
|
||||||
|
Err(e) => Some(Err(e)),
|
||||||
|
Ok(line) => Some(Ok(line)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Line<'a> {
|
||||||
|
Blank,
|
||||||
|
Comment(&'a str),
|
||||||
|
Tag(Tag),
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
Uri(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod test {
|
||||||
|
// use super::*;
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn it_works() {
|
||||||
|
// let mut lines = Lines::new("foo\nbar\r\nbaz");
|
||||||
|
// assert_eq!(lines.next().and_then(|x| x.ok()), Some("foo"));
|
||||||
|
// assert_eq!(lines.next().and_then(|x| x.ok()), Some("bar"));
|
||||||
|
// assert_eq!(lines.next().and_then(|x| x.ok()), Some("baz"));
|
||||||
|
// assert_eq!(lines.next().and_then(|x| x.ok()), None);
|
||||||
|
// }
|
||||||
|
// }
|
32
src/string.rs
Normal file
32
src/string.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct M3u8String(String);
|
||||||
|
impl M3u8String {
|
||||||
|
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
||||||
|
// TODO: validate
|
||||||
|
Ok(M3u8String(s.into()))
|
||||||
|
}
|
||||||
|
pub unsafe fn new_unchecked<T: Into<String>>(s: T) -> Self {
|
||||||
|
M3u8String(s.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for M3u8String {
|
||||||
|
type Target = str;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsRef<str> for M3u8String {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for M3u8String {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
528
src/tag.rs
Normal file
528
src/tag.rs
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
use {Error, ErrorKind, Result};
|
||||||
|
use attribute::{AttributePairs, QuotedString};
|
||||||
|
use string::M3u8String;
|
||||||
|
use version::ProtocolVersion;
|
||||||
|
|
||||||
|
macro_rules! may_invalid {
|
||||||
|
($expr:expr) => {
|
||||||
|
$expr.map_err(|e| track!(Error::from(ErrorKind::InvalidInput.cause(e))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Tag {
|
||||||
|
ExtM3u(ExtM3u),
|
||||||
|
ExtXVersion(ExtXVersion),
|
||||||
|
ExtInf(ExtInf),
|
||||||
|
ExtXByteRange(ExtXByteRange),
|
||||||
|
ExtXDiscontinuity(ExtXDiscontinuity),
|
||||||
|
ExtXKey(ExtXKey),
|
||||||
|
ExtXTargetDuration(ExtXTargetDuration),
|
||||||
|
ExtXMediaSequence(ExtXMediaSequence),
|
||||||
|
ExtXDiscontinuitySequence(ExtXDiscontinuitySequence),
|
||||||
|
ExtXEndList(ExtXEndList),
|
||||||
|
ExtXPlaylistType(ExtXPlaylistType),
|
||||||
|
ExtXIFramesOnly(ExtXIFramesOnly),
|
||||||
|
ExtXIndependentSegments(ExtXIndependentSegments),
|
||||||
|
}
|
||||||
|
impl fmt::Display for Tag {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Tag::ExtM3u(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXVersion(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtInf(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXByteRange(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXDiscontinuity(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXKey(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXTargetDuration(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXMediaSequence(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXDiscontinuitySequence(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXEndList(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXPlaylistType(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXIFramesOnly(ref t) => t.fmt(f),
|
||||||
|
Tag::ExtXIndependentSegments(ref t) => t.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for Tag {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
if s.starts_with(ExtM3u::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtM3u))
|
||||||
|
} else if s.starts_with(ExtXVersion::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXVersion))
|
||||||
|
} else if s.starts_with(ExtInf::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtInf))
|
||||||
|
} else if s.starts_with(ExtXByteRange::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXByteRange))
|
||||||
|
} else if s.starts_with(ExtXDiscontinuity::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXDiscontinuity))
|
||||||
|
} else if s.starts_with(ExtXKey::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXKey))
|
||||||
|
} else if s.starts_with(ExtXTargetDuration::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXTargetDuration))
|
||||||
|
} else if s.starts_with(ExtXMediaSequence::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXMediaSequence))
|
||||||
|
} else if s.starts_with(ExtXDiscontinuitySequence::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXDiscontinuitySequence))
|
||||||
|
} else if s.starts_with(ExtXEndList::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXEndList))
|
||||||
|
} else if s.starts_with(ExtXPlaylistType::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXPlaylistType))
|
||||||
|
} else if s.starts_with(ExtXIFramesOnly::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXIFramesOnly))
|
||||||
|
} else if s.starts_with(ExtXIndependentSegments::PREFIX) {
|
||||||
|
track!(s.parse().map(Tag::ExtXIndependentSegments))
|
||||||
|
} else {
|
||||||
|
// TODO: ignore any unrecognized tags. (section-6.3.1)
|
||||||
|
track_panic!(ErrorKind::InvalidInput, "Unknown tag: {:?}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: MediaSegmentTag
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtM3u;
|
||||||
|
impl ExtM3u {
|
||||||
|
const PREFIX: &'static str = "#EXTM3U";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtM3u {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtM3u {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtM3u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: A Playlist file MUST NOT contain more than one EXT-X-VERSION tag
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXVersion {
|
||||||
|
pub version: ProtocolVersion,
|
||||||
|
}
|
||||||
|
impl ExtXVersion {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-VERSION:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXVersion {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXVersion {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let suffix = s.split_at(Self::PREFIX.len()).1;
|
||||||
|
let version = track!(suffix.parse())?;
|
||||||
|
Ok(ExtXVersion { version })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This tag is REQUIRED for each Media Segment
|
||||||
|
// TODO: if the compatibility version number is less than 3, durations MUST be integers.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtInf {
|
||||||
|
pub duration: Duration,
|
||||||
|
pub title: Option<M3u8String>,
|
||||||
|
}
|
||||||
|
impl ExtInf {
|
||||||
|
const PREFIX: &'static str = "#EXTINF:";
|
||||||
|
|
||||||
|
// TODO: pub fn required_version(&self) -> ProtocolVersion;
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtInf {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
|
||||||
|
let duration = (self.duration.as_secs() as f64)
|
||||||
|
+ (self.duration.subsec_nanos() as f64 / 1_000_000_000.0);
|
||||||
|
write!(f, "{}", duration)?;
|
||||||
|
|
||||||
|
if let Some(ref title) = self.title {
|
||||||
|
write!(f, ",{}", title)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtInf {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ',');
|
||||||
|
|
||||||
|
let duration: f64 = may_invalid!(tokens.next().expect("Never fails").parse())?;
|
||||||
|
let duration = Duration::new(duration as u64, (duration.fract() * 1_000_000_000.0) as u32);
|
||||||
|
|
||||||
|
let title = if let Some(title) = tokens.next() {
|
||||||
|
Some(track!(M3u8String::new(title))?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(ExtInf { duration, title })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If o is not present, a previous Media Segment MUST appear in the Playlist file
|
||||||
|
// TDOO: Use of the EXT-X-BYTERANGE tag REQUIRES a compatibility version number of 4 or greater.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXByteRange {
|
||||||
|
pub length: usize,
|
||||||
|
pub offset: Option<usize>,
|
||||||
|
}
|
||||||
|
impl ExtXByteRange {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXByteRange {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.length)?;
|
||||||
|
if let Some(offset) = self.offset {
|
||||||
|
write!(f, "@{}", offset)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXByteRange {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, '@');
|
||||||
|
|
||||||
|
let length = may_invalid!(tokens.next().expect("Never fails").parse())?;
|
||||||
|
let offset = if let Some(offset) = tokens.next() {
|
||||||
|
Some(may_invalid!(offset.parse())?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(ExtXByteRange { length, offset })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXDiscontinuity;
|
||||||
|
impl ExtXDiscontinuity {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXDiscontinuity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXDiscontinuity {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXDiscontinuity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXKey {
|
||||||
|
pub method: EncryptionMethod,
|
||||||
|
pub uri: Option<QuotedString>,
|
||||||
|
}
|
||||||
|
impl ExtXKey {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXKey {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut method = None;
|
||||||
|
let mut uri = None;
|
||||||
|
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"METHOD" => {
|
||||||
|
method = Some(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"URI" => {
|
||||||
|
uri = Some(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"IV" => unimplemented!(),
|
||||||
|
"KEYFORMAT" => unimplemented!(),
|
||||||
|
"KEYFORMATVERSIONS" => unimplemented!(),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let method = track_assert_some!(method, ErrorKind::InvalidInput);
|
||||||
|
if let EncryptionMethod::None = method {
|
||||||
|
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
||||||
|
} else {
|
||||||
|
track_assert!(uri.is_some(), ErrorKind::InvalidInput);
|
||||||
|
};
|
||||||
|
Ok(ExtXKey { method, uri })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum EncryptionMethod {
|
||||||
|
None,
|
||||||
|
Aes128,
|
||||||
|
SampleAes,
|
||||||
|
}
|
||||||
|
impl fmt::Display for EncryptionMethod {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
EncryptionMethod::None => "NONE".fmt(f),
|
||||||
|
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
||||||
|
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for EncryptionMethod {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"NONE" => Ok(EncryptionMethod::None),
|
||||||
|
"AES-128" => Ok(EncryptionMethod::Aes128),
|
||||||
|
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
||||||
|
_ => track_panic!(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Unknown encryption method: {:?}",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// #[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
// pub struct ExtXProgramDateTime { date_time }
|
||||||
|
// impl ExtXProgramDateTime {
|
||||||
|
// const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||||
|
// }
|
||||||
|
// impl fmt::Display for ExtXProgramDateTime {
|
||||||
|
// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
// write!(f, "{}{}", Self::PREFIX, self.length)?;
|
||||||
|
// if let Some(offset) = self.offset {
|
||||||
|
// write!(f, "@{}", offset)?;
|
||||||
|
// }
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// impl FromStr for ExtXProgramDateTime {
|
||||||
|
// type Err = Error;
|
||||||
|
// fn from_str(s: &str) -> Result<Self> {
|
||||||
|
// track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
// let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, '@');
|
||||||
|
|
||||||
|
// let length = may_invalid!(tokens.next().expect("Never fails").parse())?;
|
||||||
|
// let offset = if let Some(offset) = tokens.next() {
|
||||||
|
// Some(may_invalid!(offset.parse())?)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// };
|
||||||
|
// Ok(ExtXByteRange { length, offset })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
||||||
|
|
||||||
|
// TODO: he EXT-X-TARGETDURATION tag is REQUIRED.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXTargetDuration {
|
||||||
|
pub duration: Duration,
|
||||||
|
}
|
||||||
|
impl ExtXTargetDuration {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXTargetDuration {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.duration.as_secs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXTargetDuration {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXTargetDuration {
|
||||||
|
duration: Duration::from_secs(duration),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXMediaSequence {
|
||||||
|
pub seq_num: u64,
|
||||||
|
}
|
||||||
|
impl ExtXMediaSequence {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXMediaSequence {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXMediaSequence {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXMediaSequence { seq_num })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.
|
||||||
|
// TODO: The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any EXT-X-DISCONTINUITY tag.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXDiscontinuitySequence {
|
||||||
|
pub seq_num: u64,
|
||||||
|
}
|
||||||
|
impl ExtXDiscontinuitySequence {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXDiscontinuitySequence {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXDiscontinuitySequence {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXDiscontinuitySequence { seq_num })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXEndList;
|
||||||
|
impl ExtXEndList {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXEndList {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXEndList {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXEndList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXPlaylistType {
|
||||||
|
pub playlist_type: PlaylistType,
|
||||||
|
}
|
||||||
|
impl ExtXPlaylistType {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXPlaylistType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.playlist_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXPlaylistType {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXPlaylistType { playlist_type })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PlaylistType {
|
||||||
|
Event,
|
||||||
|
Vod,
|
||||||
|
}
|
||||||
|
impl fmt::Display for PlaylistType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
PlaylistType::Event => write!(f, "EVENT"),
|
||||||
|
PlaylistType::Vod => write!(f, "VOD"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for PlaylistType {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"EVENT" => Ok(PlaylistType::Event),
|
||||||
|
"VOD" => Ok(PlaylistType::Vod),
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Media resources containing I-frame segments MUST begin with ...
|
||||||
|
// TODO: Use of the EXT-X-I-FRAMES-ONLY REQUIRES a compatibility version number of 4 or greater.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXIFramesOnly;
|
||||||
|
impl ExtXIFramesOnly {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXIFramesOnly {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXIFramesOnly {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXIFramesOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
||||||
|
|
||||||
|
// 4.3.5. Media or Master Playlist Tags
|
||||||
|
// TODO: A tag that appears in both MUST have the same value; otherwise, clients SHOULD ignore the value in the Media Playlist(s).
|
||||||
|
// TODO: These tags MUST NOT appear more than once in a Playlist.
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXIndependentSegments;
|
||||||
|
impl ExtXIndependentSegments {
|
||||||
|
const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXIndependentSegments {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXIndependentSegments {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXIndependentSegments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
45
src/version.rs
Normal file
45
src/version.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use {Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc8216#section-7
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum ProtocolVersion {
|
||||||
|
V1,
|
||||||
|
V2,
|
||||||
|
V3,
|
||||||
|
V4,
|
||||||
|
V5,
|
||||||
|
V6,
|
||||||
|
V7,
|
||||||
|
}
|
||||||
|
impl fmt::Display for ProtocolVersion {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let n = match *self {
|
||||||
|
ProtocolVersion::V1 => 1,
|
||||||
|
ProtocolVersion::V2 => 2,
|
||||||
|
ProtocolVersion::V3 => 3,
|
||||||
|
ProtocolVersion::V4 => 4,
|
||||||
|
ProtocolVersion::V5 => 5,
|
||||||
|
ProtocolVersion::V6 => 6,
|
||||||
|
ProtocolVersion::V7 => 7,
|
||||||
|
};
|
||||||
|
write!(f, "{}", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ProtocolVersion {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
Ok(match s {
|
||||||
|
"1" => ProtocolVersion::V1,
|
||||||
|
"2" => ProtocolVersion::V2,
|
||||||
|
"3" => ProtocolVersion::V3,
|
||||||
|
"4" => ProtocolVersion::V4,
|
||||||
|
"5" => ProtocolVersion::V5,
|
||||||
|
"6" => ProtocolVersion::V6,
|
||||||
|
"7" => ProtocolVersion::V7,
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue