diff --git a/src/attributes.rs b/src/attributes.rs new file mode 100644 index 0000000..0fb01d9 --- /dev/null +++ b/src/attributes.rs @@ -0,0 +1,159 @@ +use crate::attributes::QuotedOrUnquoted::{Quoted, Unquoted}; +use std::fmt; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum QuotedOrUnquoted { + Unquoted(String), + Quoted(String), +} + +impl Default for QuotedOrUnquoted { + fn default() -> Self { + Quoted(String::new()) + } +} + +impl From<&str> for QuotedOrUnquoted { + fn from(s: &str) -> Self { + if s.starts_with('"') && s.ends_with('"') { + return Quoted(s.trim_matches('"').to_string()); + } + Unquoted(s.to_string()) + } +} + +impl fmt::Display for QuotedOrUnquoted { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Unquoted(s) => write!(f, "{}", s), + Quoted(u) => write!(f, "{}", u), + } + } +} + +// EXT-X-KEY +// +// METHOD +// The value is an enumerated-string that specifies the encryption +// method. The methods defined are: NONE, AES-128, and SAMPLE-AES. +#[allow(non_camel_case_types)] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum KeyMethod { + None, + AES_128, + SAMPLE_AES, + Enum(String), +} + +impl Default for KeyMethod { + fn default() -> Self { + KeyMethod::None + } +} + +impl From for KeyMethod { + fn from(s: QuotedOrUnquoted) -> Self { + match s { + QuotedOrUnquoted::Unquoted(s) if s == "NONE" => KeyMethod::None, + QuotedOrUnquoted::Unquoted(s) if s == "AES-128" => KeyMethod::AES_128, + QuotedOrUnquoted::Unquoted(s) if s == "SAMPLE-AES" => KeyMethod::SAMPLE_AES, + _ => KeyMethod::Enum(s.to_string()), + } + } +} +impl From<&str> for KeyMethod { + fn from(s: &str) -> Self { + QuotedOrUnquoted::from(s).into() + } +} + +impl fmt::Display for KeyMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyMethod::None => write!(f, "NONE"), + KeyMethod::AES_128 => write!(f, "AES-128"), + KeyMethod::SAMPLE_AES => write!(f, "SAMPLE-AES"), + KeyMethod::Enum(s) => write!(f, "{}", s), + } + } +} + +// EXT-X-STREAM-INF: +// +// HDCP-LEVEL +// The value is an enumerated-string; valid strings are TYPE-0, TYPE- +// 1, and NONE +#[allow(non_camel_case_types)] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum HdcpLevel { + Type0, + Type1, + None, + Enum(String), +} + +impl From for HdcpLevel { + fn from(s: QuotedOrUnquoted) -> Self { + match s { + QuotedOrUnquoted::Unquoted(s) if s == "NONE" => HdcpLevel::None, + QuotedOrUnquoted::Unquoted(s) if s == "TYPE-0" => HdcpLevel::Type0, + QuotedOrUnquoted::Unquoted(s) if s == "TYPE-1" => HdcpLevel::Type1, + _ => HdcpLevel::Enum(s.to_string()), + } + } +} + +impl From<&str> for HdcpLevel { + fn from(s: &str) -> Self { + QuotedOrUnquoted::from(s).into() + } +} + +impl fmt::Display for HdcpLevel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HdcpLevel::None => write!(f, "NONE"), + HdcpLevel::Type0 => write!(f, "TYPE-0"), + HdcpLevel::Type1 => write!(f, "Type-1"), + HdcpLevel::Enum(s) => write!(f, "{}", s), + } + } +} + +// EXT-X-STREAM-INF +// +// CLOSED-CAPTIONS +// The value can be either a quoted-string or an enumerated-string +// with the value NONE. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ClosedCaptions { + None, + GroupId(String), + Enum(String), +} + +impl From for ClosedCaptions { + fn from(s: QuotedOrUnquoted) -> Self { + match s { + QuotedOrUnquoted::Unquoted(s) if s == "NONE" => ClosedCaptions::None, + QuotedOrUnquoted::Quoted(gid) => ClosedCaptions::GroupId(gid), + QuotedOrUnquoted::Unquoted(e) => ClosedCaptions::Enum(e), + } + } +} + +impl From<&str> for ClosedCaptions { + fn from(s: &str) -> Self { + QuotedOrUnquoted::from(s).into() + } +} + +impl fmt::Display for ClosedCaptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ClosedCaptions::None => write!(f, "NONE"), + ClosedCaptions::GroupId(gid) => write!(f, "{}", gid), + ClosedCaptions::Enum(e) => write!(f, "{}", e), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 4d9b79e..e749af5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,3 +73,6 @@ mod parser; #[cfg(feature = "parser")] pub use self::parser::*; + +pub mod attributes; +pub use playlist::*; diff --git a/src/parser.rs b/src/parser.rs index 55d7303..710c2ab 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -8,11 +8,11 @@ use nom::combinator::{complete, eof, map, map_res, opt, peek}; use nom::multi::{fold_many0, many0}; use nom::sequence::{delimited, pair, preceded, terminated, tuple}; +use crate::attributes::*; use crate::playlist::*; use nom::IResult; use std::collections::HashMap; use std::f32; -use std::fmt; use std::result::Result; use std::str; use std::str::FromStr; @@ -616,40 +616,6 @@ fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap )(i) } -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum QuotedOrUnquoted { - Unquoted(String), - Quoted(String), -} - -impl Default for QuotedOrUnquoted { - fn default() -> Self { - QuotedOrUnquoted::Quoted(String::new()) - } -} - -impl From<&str> for QuotedOrUnquoted { - fn from(s: &str) -> Self { - if s.starts_with('"') && s.ends_with('"') { - return QuotedOrUnquoted::Quoted(s.trim_matches('"').to_string()); - } - QuotedOrUnquoted::Unquoted(s.to_string()) - } -} - -impl fmt::Display for QuotedOrUnquoted { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - match self { - QuotedOrUnquoted::Unquoted(s) => s, - QuotedOrUnquoted::Quoted(u) => u, - } - ) - } -} - fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, QuotedOrUnquoted)> { map( tuple(( diff --git a/src/playlist.rs b/src/playlist.rs index e0c7013..2518e9e 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -3,11 +3,10 @@ //! The main type here is the `Playlist` enum. //! Which is either a `MasterPlaylist` or a `MediaPlaylist`. -use crate::QuotedOrUnquoted; +use crate::attributes::*; use std::collections::HashMap; use std::f32; use std::fmt; -use std::fmt::Display; use std::io::Write; use std::str::FromStr; @@ -141,11 +140,11 @@ pub struct VariantStream { pub codecs: Option, pub resolution: Option, pub frame_rate: Option, - pub hdcp_level: Option, + pub hdcp_level: Option, pub audio: Option, pub video: Option, pub subtitles: Option, - pub closed_captions: Option, + pub closed_captions: Option, // PROGRAM-ID tag was removed in protocol version 6 } @@ -162,11 +161,11 @@ impl VariantStream { codecs: attrs.remove("CODECS").map(|c| c.to_string()), resolution: attrs.remove("RESOLUTION").map(|r| r.to_string()), frame_rate: attrs.remove("FRAME-RATE").map(|f| f.to_string()), - hdcp_level: attrs.remove("HDCP-LEVEL"), + hdcp_level: attrs.remove("HDCP-LEVEL").map(|h| h.into()), audio: attrs.remove("AUDIO").map(|a| a.to_string()), video: attrs.remove("VIDEO").map(|v| v.to_string()), subtitles: attrs.remove("SUBTITLES").map(|s| s.to_string()), - closed_captions: attrs.remove("CLOSED-CAPTIONS"), + closed_captions: attrs.remove("CLOSED-CAPTIONS").map(|c| c.into()), } } @@ -197,7 +196,7 @@ impl VariantStream { } } -/// [`#EXT-X-MEDIA:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1) +/// [`#EXT-X-MEDIA:`](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-10.txt#section-4.4.6.1) /// /// The EXT-X-MEDIA tag is used to relate Media Playlists that contain /// alternative Renditions (Section 4.3.4.2.1) of the same content. For @@ -226,8 +225,8 @@ impl AlternativeMedia { pub fn from_hashmap(mut attrs: HashMap) -> AlternativeMedia { AlternativeMedia { media_type: attrs - .get("TYPE") - .and_then(|s| AlternativeMediaType::from_str(s.to_string().as_str()).ok()) + .remove("TYPE") + .and_then(|s| AlternativeMediaType::from_str(&s.to_string()).ok()) .unwrap_or_default(), uri: attrs.remove("URI").map(|u| u.to_string()), group_id: attrs.remove("GROUP-ID").unwrap_or_default().to_string(), @@ -267,7 +266,10 @@ impl AlternativeMedia { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] +// EXT-X-MEDIA:TYPE +// The value is an enumerated-string +#[allow(non_camel_case_types)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum AlternativeMediaType { Audio, Video, @@ -297,19 +299,14 @@ impl Default for AlternativeMediaType { AlternativeMediaType::Video } } - impl fmt::Display for AlternativeMediaType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - match *self { - AlternativeMediaType::Audio => "AUDIO", - AlternativeMediaType::Video => "VIDEO", - AlternativeMediaType::Subtitles => "SUBTITLES", - AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS", - } - ) + match self { + AlternativeMediaType::Audio => write!(f, "AUDIO"), + AlternativeMediaType::Video => write!(f, "VIDEO"), + AlternativeMediaType::Subtitles => write!(f, "SUBTITLES"), + AlternativeMediaType::ClosedCaptions => write!(f, "CLOSED-CAPTIONS"), + } } } @@ -584,7 +581,7 @@ impl MediaSegment { /// same Media Segment if they ultimately produce the same decryption key. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct Key { - pub method: String, + pub method: KeyMethod, pub uri: Option, pub iv: Option, pub keyformat: Option, @@ -594,7 +591,7 @@ pub struct Key { impl Key { pub fn from_hashmap(mut attrs: HashMap) -> Key { Key { - method: attrs.remove("METHOD").unwrap_or_default().to_string(), + method: attrs.remove("METHOD").map(|m| m.into()).unwrap_or_default(), uri: attrs.remove("URI").map(|u| u.to_string()), iv: attrs.remove("IV").map(|i| i.to_string()), keyformat: attrs.remove("KEYFORMAT").map(|k| k.to_string()), @@ -714,8 +711,8 @@ pub struct ExtTag { pub rest: Option, } -impl Display for ExtTag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for ExtTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "#EXT-{}", self.tag)?; if let Some(v) = &self.rest { write!(f, ":{}", v)?; diff --git a/tests/lib.rs b/tests/lib.rs index 3bfb798..f96ca63 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,5 +1,6 @@ #![allow(unused_variables, unused_imports, dead_code)] +use m3u8_rs::attributes::*; use m3u8_rs::*; use nom::AsBytes; use std::collections::HashMap; @@ -321,6 +322,7 @@ fn create_and_parse_media_playlist_full() { }], }], }); + println!("hello"); let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); assert_eq!(playlist_original, playlist_parsed); }