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.
This commit is contained in:
Vadim Getmanshchuk 2022-04-17 00:29:02 -07:00
parent ac0f881eef
commit 02d4d3108f
5 changed files with 187 additions and 60 deletions

159
src/attributes.rs Normal file
View file

@ -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<QuotedOrUnquoted> 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<QuotedOrUnquoted> 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<QuotedOrUnquoted> 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),
}
}
}

View file

@ -73,3 +73,6 @@ mod parser;
#[cfg(feature = "parser")]
pub use self::parser::*;
pub mod attributes;
pub use playlist::*;

View file

@ -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<String, QuotedOrUnquoted>
)(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((

View file

@ -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<String>,
pub resolution: Option<String>,
pub frame_rate: Option<String>,
pub hdcp_level: Option<QuotedOrUnquoted>,
pub hdcp_level: Option<HdcpLevel>,
pub audio: Option<String>,
pub video: Option<String>,
pub subtitles: Option<String>,
pub closed_captions: Option<QuotedOrUnquoted>,
pub closed_captions: Option<ClosedCaptions>,
// 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:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1)
/// [`#EXT-X-MEDIA:<attribute-list>`](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<String, QuotedOrUnquoted>) -> 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<AUDIO|VIDEO|SUBTITLES|CLOSED-CAPTIONS>
// 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<String>,
pub iv: Option<String>,
pub keyformat: Option<String>,
@ -594,7 +591,7 @@ pub struct Key {
impl Key {
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> 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<String>,
}
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)?;

View file

@ -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);
}