From 02d4d3108f41a082f839f07bd85ed3eea166c2e9 Mon Sep 17 00:00:00 2001 From: Vadim Getmanshchuk Date: Sun, 17 Apr 2022 00:29:02 -0700 Subject: [PATCH] I audited the spec and implemented enums for all non-binary attributes that are currently implemented: * HDCP-LEVEL * CLOSED-CAPTIONS * METHOD I don't believe we need to add these for binary attributes, that are either YES or NO and the absence means NO. If you don't believe it's sufficient, feel free to add more enums. --- src/attributes.rs | 159 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/parser.rs | 36 +---------- src/playlist.rs | 47 +++++++------- tests/lib.rs | 2 + 5 files changed, 187 insertions(+), 60 deletions(-) create mode 100644 src/attributes.rs 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); }