From c067448428226695ab91dad2b24ef2f2a3c8a4e7 Mon Sep 17 00:00:00 2001 From: Takeru Ohta Date: Sun, 11 Feb 2018 15:10:52 +0900 Subject: [PATCH] Initial commit --- .gitignore | 4 + Cargo.toml | 7 + README.md | 9 + examples/data/rfc8216_8-1.m3u8 | 12 + examples/data/rfc8216_8-2.m3u8 | 13 + examples/data/rfc8216_8-3.m3u8 | 20 ++ examples/data/rfc8216_8-4.m3u8 | 11 + examples/data/rfc8216_8-5.m3u8 | 14 + examples/data/rfc8216_8-6.m3u8 | 14 + examples/data/rfc8216_8-7.m3u8 | 23 ++ examples/parse.rs | 18 ++ src/attribute.rs | 95 ++++++ src/error.rs | 12 + src/lib.rs | 18 ++ src/line.rs | 98 ++++++ src/string.rs | 32 ++ src/tag.rs | 528 +++++++++++++++++++++++++++++++++ src/version.rs | 45 +++ 18 files changed, 973 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 examples/data/rfc8216_8-1.m3u8 create mode 100644 examples/data/rfc8216_8-2.m3u8 create mode 100644 examples/data/rfc8216_8-3.m3u8 create mode 100644 examples/data/rfc8216_8-4.m3u8 create mode 100644 examples/data/rfc8216_8-5.m3u8 create mode 100644 examples/data/rfc8216_8-6.m3u8 create mode 100644 examples/data/rfc8216_8-7.m3u8 create mode 100644 examples/parse.rs create mode 100644 src/attribute.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/line.rs create mode 100644 src/string.rs create mode 100644 src/tag.rs create mode 100644 src/version.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..143b1ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +/target/ +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0e5a84a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hls_m3u8" +version = "0.1.0" +authors = ["Takeru Ohta "] + +[dependencies] +trackable = "0.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3d0530 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +hls_m3u8 +========= + +References +----------- + +- [HTTP Live Streaming][rfc8216] + +[rfc8216]: https://tools.ietf.org/html/rfc8216 diff --git a/examples/data/rfc8216_8-1.m3u8 b/examples/data/rfc8216_8-1.m3u8 new file mode 100644 index 0000000..549fa9e --- /dev/null +++ b/examples/data/rfc8216_8-1.m3u8 @@ -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 diff --git a/examples/data/rfc8216_8-2.m3u8 b/examples/data/rfc8216_8-2.m3u8 new file mode 100644 index 0000000..33dfbe6 --- /dev/null +++ b/examples/data/rfc8216_8-2.m3u8 @@ -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 diff --git a/examples/data/rfc8216_8-3.m3u8 b/examples/data/rfc8216_8-3.m3u8 new file mode 100644 index 0000000..970a2d4 --- /dev/null +++ b/examples/data/rfc8216_8-3.m3u8 @@ -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 diff --git a/examples/data/rfc8216_8-4.m3u8 b/examples/data/rfc8216_8-4.m3u8 new file mode 100644 index 0000000..e81a437 --- /dev/null +++ b/examples/data/rfc8216_8-4.m3u8 @@ -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 \ No newline at end of file diff --git a/examples/data/rfc8216_8-5.m3u8 b/examples/data/rfc8216_8-5.m3u8 new file mode 100644 index 0000000..4f99863 --- /dev/null +++ b/examples/data/rfc8216_8-5.m3u8 @@ -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 diff --git a/examples/data/rfc8216_8-6.m3u8 b/examples/data/rfc8216_8-6.m3u8 new file mode 100644 index 0000000..52fc123 --- /dev/null +++ b/examples/data/rfc8216_8-6.m3u8 @@ -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 diff --git a/examples/data/rfc8216_8-7.m3u8 b/examples/data/rfc8216_8-7.m3u8 new file mode 100644 index 0000000..13d9c93 --- /dev/null +++ b/examples/data/rfc8216_8-7.m3u8 @@ -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 diff --git a/examples/parse.rs b/examples/parse.rs new file mode 100644 index 0000000..4801174 --- /dev/null +++ b/examples/parse.rs @@ -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)); + } +} diff --git a/src/attribute.rs b/src/attribute.rs new file mode 100644 index 0000000..5241105 --- /dev/null +++ b/src/attribute.rs @@ -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 { + 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 { + 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); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f794141 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,12 @@ +use trackable::error::{ErrorKind as TrackableErrorKind, TrackableError}; + +#[derive(Debug, Clone)] +pub struct Error(TrackableError); +derive_traits_for_trackable_error_newtype!(Error, ErrorKind); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + InvalidInput, + Other, +} +impl TrackableErrorKind for ErrorKind {} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..de79f47 --- /dev/null +++ b/src/lib.rs @@ -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 = std::result::Result; diff --git a/src/line.rs b/src/line.rs new file mode 100644 index 0000000..332fd4d --- /dev/null +++ b/src/line.rs @@ -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> { + 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>; + fn next(&mut self) -> Option { + 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); +// } +// } diff --git a/src/string.rs b/src/string.rs new file mode 100644 index 0000000..9024154 --- /dev/null +++ b/src/string.rs @@ -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>(s: T) -> Result { + // TODO: validate + Ok(M3u8String(s.into())) + } + pub unsafe fn new_unchecked>(s: T) -> Self { + M3u8String(s.into()) + } +} +impl Deref for M3u8String { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl AsRef 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) + } +} diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..b4f12d1 --- /dev/null +++ b/src/tag.rs @@ -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 { + 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 { + 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 { + 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, +} +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 { + 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, +} +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 { + 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 { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXDiscontinuity) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtXKey { + pub method: EncryptionMethod, + pub uri: Option, +} +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 { + 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 { + 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 { +// 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXIndependentSegments) + } +} + +// TODO: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..d64f0d9 --- /dev/null +++ b/src/version.rs @@ -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 { + 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), + }) + } +}